"use client"; import { zodResolver } from "@hookform/resolvers/zod"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; import { Button } from "@workspace/ui/components/button"; import { Checkbox } from "@workspace/ui/components/checkbox"; import { Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage, } from "@workspace/ui/components/form"; import { Label } from "@workspace/ui/components/label"; import { ScrollArea } from "@workspace/ui/components/scroll-area"; import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle, SheetTrigger, } from "@workspace/ui/components/sheet"; import { Switch } from "@workspace/ui/components/switch"; import { Tabs, TabsContent, TabsList, TabsTrigger, } from "@workspace/ui/components/tabs"; import { Combobox } from "@workspace/ui/composed/combobox"; import { ArrayInput } from "@workspace/ui/composed/dynamic-Inputs"; import { JSONEditor } from "@workspace/ui/composed/editor/index"; import { EnhancedInput } from "@workspace/ui/composed/enhanced-input"; import { Icon } from "@workspace/ui/composed/icon"; import { evaluateWithPrecision, unitConversion, } from "@workspace/ui/utils/unit-conversions"; import { CreditCard, Server, Settings } from "lucide-react"; import { assign, shake } from "radash"; import { useCallback, useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { z } from "zod"; import { useGlobalStore } from "@/stores/global"; import { useNode } from "@/stores/node"; interface SubscribeFormProps { onSubmit: (data: T) => Promise | boolean; initialValues?: T; loading?: boolean; trigger: string; title: string; } const defaultValues = { inventory: 0, speed_limit: 0, device_limit: 0, traffic: 0, quota: 0, discount: [], language: "", node_tags: [], nodes: [], unit_time: "Month", deduction_ratio: 0, purchase_with_discount: false, reset_cycle: 0, renewal_reset: false, deduction_mode: "auto", }; export default function SubscribeForm>({ onSubmit, initialValues, loading, trigger, title, }: Readonly>) { const { common } = useGlobalStore(); const { currency } = common; const { t } = useTranslation("product"); const [open, setOpen] = useState(false); const updateTimeoutRef = useRef(null); const formSchema = z.object({ name: z.string(), description: z.string().optional(), unit_price: z.number(), unit_time: z.string(), replacement: z.number().optional(), discount: z .array( z.object({ quantity: z.number(), discount: z.number(), }) ) .optional(), inventory: z.number().optional(), speed_limit: z.number().optional(), device_limit: z.number().optional(), traffic: z.number().optional(), quota: z.number().optional(), language: z.string().optional(), node_tags: z.array(z.string()).optional(), nodes: z.array(z.number()).optional(), deduction_ratio: z.number().optional(), allow_deduction: z.boolean().optional(), reset_cycle: z.number().optional(), renewal_reset: z.boolean().optional(), show_original_price: z.boolean().optional(), }); const form = useForm>({ resolver: zodResolver(formSchema), defaultValues: assign( defaultValues, shake(initialValues, (value) => value === null) as Record ), }); const debouncedCalculateDiscount = useCallback( ( values: any[], fieldName: string, lastChangedField?: string, changedIndex?: number ) => { if (updateTimeoutRef.current) { clearTimeout(updateTimeoutRef.current); } updateTimeoutRef.current = setTimeout(() => { const { unit_price } = form.getValues(); if (!(unit_price && values?.length)) return; let hasChanges = false; const calculatedValues = values.map((item: any, index: number) => { const result = { ...item }; if (changedIndex !== undefined && index !== changedIndex) { return result; } const quantity = Number(item.quantity) || 0; const discount = Number(item.discount) || 0; const price = Number(item.price) || 0; switch (lastChangedField) { case "quantity": case "discount": if (quantity > 0 && discount > 0) { const newPrice = evaluateWithPrecision( `${unit_price} * ${quantity} * ${discount} / 100` ); if (Math.abs(newPrice - price) > 0.01) { result.price = newPrice; hasChanges = true; } } break; case "price": if (quantity > 0 && price > 0) { const newDiscount = evaluateWithPrecision( `${price} / ${quantity} / ${unit_price} * 100` ); if (Math.abs(newDiscount - discount) > 0.01) { result.discount = Math.min(100, Math.max(0, newDiscount)); hasChanges = true; } } else if (discount > 0 && price > 0) { const newQuantity = evaluateWithPrecision( `${price} / ${unit_price} / ${discount} * 100` ); if ( Math.abs(newQuantity - quantity) > 0.01 && newQuantity > 0 ) { result.quantity = Math.max(1, Math.round(newQuantity)); hasChanges = true; } } break; default: if (quantity > 0 && discount > 0 && price === 0) { result.price = evaluateWithPrecision( `${unit_price} * ${quantity} * ${discount} / 100` ); hasChanges = true; } else if (quantity > 0 && price > 0 && discount === 0) { const newDiscount = evaluateWithPrecision( `${price} / ${quantity} / ${unit_price} * 100` ); result.discount = Math.min(100, Math.max(0, newDiscount)); hasChanges = true; } else if (discount > 0 && price > 0 && quantity === 0) { const newQuantity = evaluateWithPrecision( `${price} / ${unit_price} / ${discount} * 100` ); if (newQuantity > 0) { result.quantity = Math.max(1, Math.round(newQuantity)); hasChanges = true; } } break; } return result; }); if (hasChanges) { form.setValue(fieldName as any, calculatedValues, { shouldDirty: true, }); } }, 300); }, [form] ); useEffect(() => { form?.reset( assign( defaultValues, shake(initialValues, (value) => value === null) as Record ) ); const discount = form.getValues("discount") || []; if (discount.length > 0) { debouncedCalculateDiscount(discount, "discount"); } }, [form, initialValues, open]); useEffect( () => () => { if (updateTimeoutRef.current) { clearTimeout(updateTimeoutRef.current); } }, [] ); async function handleSubmit(data: { [x: string]: any }) { const bool = await onSubmit(data as T); if (bool) setOpen(false); } const { getAllAvailableTags, getNodesByTag, getNodesWithoutTags } = useNode(); const tagGroups = getAllAvailableTags(); const unit_time = form.watch("unit_time"); return ( {title}
{t("form.basic")} {t("form.pricing")} {t("form.nodes")}
( {t("form.name")} { form.setValue(field.name, value); }} /> )} /> ( {t("form.language")} {t("form.languageDescription")} form.setValue(field.name, v as string) } placeholder={t("form.languagePlaceholder")} /> )} />
( {t("form.traffic")} unitConversion("bytesToGb", value) } formatOutput={(value) => unitConversion("gbToBytes", value) } onValueChange={(value) => { form.setValue(field.name, value); }} suffix="GB" /> )} /> ( {t("form.speedLimit")} unitConversion("bitsToMb", value) } formatOutput={(value) => unitConversion("mbToBits", value) } onValueChange={(value) => { form.setValue(field.name, value); }} suffix="Mbps" /> )} /> ( {t("form.deviceLimit")} { form.setValue(field.name, value); }} /> )} />
( {t("form.inventory")} { form.setValue(field.name, value); }} placeholder={t("form.unlimitedInventory")} step={1} type="number" value={field.value} /> {t("form.inventoryDescription")} )} /> ( {t("form.quota")} { form.setValue(field.name, value); }} /> )} />
( { form.setValue( field.name, JSON.stringify(value) ); }} placeholder={{ description: "description", features: [ { type: "default", icon: "", label: "label", }, ], }} schema={{ type: "object", properties: { description: { type: "string", description: "A brief description of the item.", }, features: { type: "array", items: { type: "object", properties: { icon: { type: "string", description: "Enter an Iconify icon identifier (e.g., 'mdi:account').", pattern: "^[a-z0-9]+:[a-z0-9-]+$", examples: [ "uil:shield-check", "uil:shield-exclamation", "uil:database", "uil:server", ], }, label: { type: "string", description: "The label describing the feature.", }, type: { type: "string", enum: [ "default", "success", "destructive", ], description: "The type of feature, limited to specific values.", }, }, }, description: "A list of feature objects.", }, }, required: ["description", "features"], additionalProperties: false, }} title={t("form.description")} value={field.value && JSON.parse(field.value)} /> )} />
( {t("form.unitPrice")} unitConversion("centsToDollars", value) } formatOutput={(value) => unitConversion("dollarsToCents", value) } min={0} onValueChange={(value) => { form.setValue(field.name, value); }} /> )} /> ( {t("form.unitTime")} { if (value) { form.setValue(field.name, value); } }} options={[ { label: t("form.NoLimit"), value: "NoLimit", }, { label: t("form.Year"), value: "Year" }, { label: t("form.Month"), value: "Month" }, { label: t("form.Day"), value: "Day" }, { label: t("form.Hour"), value: "Hour" }, { label: t("form.Minute"), value: "Minute" }, ]} /> )} /> ( {t("form.replacement")} unitConversion("centsToDollars", value) } formatOutput={(value) => unitConversion("dollarsToCents", value) } min={0} onValueChange={(value) => { form.setValue(field.name, value); }} /> )} /> ( {t("form.resetCycle")} placeholder={t("form.selectResetCycle")} {...field} onChange={(value) => { if (typeof value === "number") { form.setValue(field.name, value); } }} options={[ { label: t("form.noReset"), value: 0 }, { label: t("form.resetOn1st"), value: 1 }, { label: t("form.monthlyReset"), value: 2 }, { label: t("form.annualReset"), value: 3 }, ]} /> )} />
( {t("form.discount")} fields={[ { name: "quantity", type: "number", step: 1, min: 1, suffix: unit_time && t(`form.${unit_time}`), }, { name: "discount", type: "number", min: 1, max: 100, step: 1, placeholder: t("form.discountPercent"), suffix: "%", }, { name: "price", placeholder: t("form.discount_price"), type: "number", min: 0, step: 0.01, prefix: currency.currency_symbol, formatInput: (value) => unitConversion("centsToDollars", value), formatOutput: (value) => unitConversion( "dollarsToCents", value ).toString(), }, ]} onChange={( newValues: (API.SubscribeDiscount & { price?: number; })[] ) => { const oldValues = field.value || []; let lastChangedField: string | undefined; let changedIndex: number | undefined; for ( let i = 0; i < Math.max(newValues.length, oldValues.length); i++ ) { const newItem = newValues[i] || {}; const oldItem = oldValues[i] || {}; if ( (newItem as any).quantity !== (oldItem as any).quantity ) { lastChangedField = "quantity"; changedIndex = i; break; } if ( (newItem as any).discount !== (oldItem as any).discount ) { lastChangedField = "discount"; changedIndex = i; break; } if ( (newItem as any).price !== (oldItem as any).price ) { lastChangedField = "price"; changedIndex = i; break; } } form.setValue(field.name, newValues, { shouldDirty: true, }); if (newValues?.length > 0) { debouncedCalculateDiscount( newValues, field.name, lastChangedField, changedIndex ); } }} value={field.value} /> {t("form.discountDescription")} )} /> ( {t("form.deductionRatio")} { form.setValue(field.name, value); }} placeholder="Auto" suffix="%" /> {t("form.deductionRatioDescription")} )} /> (
{t("form.renewalReset")} {t("form.renewalResetDescription")}
)} /> (
{t("form.purchaseWithDiscount")} {t("form.purchaseWithDiscountDescription")}
{ form.setValue(field.name, value); }} />
)} /> (
{t("form.showOriginalPrice")} {t("form.showOriginalPriceDescription")}
{ form.setValue(field.name, value); }} />
)} />
( {t("form.nodeGroup")} {tagGroups.map((tag) => { const value = field.value || []; const tagId = tag; const nodesWithTag = getNodesByTag(tag); return (
checked ? form.setValue(field.name, [ ...value, tagId, ] as any) : form.setValue( field.name, value.filter( (v: any) => v !== tagId ) ) } />
    {getNodesByTag(tag).map((node) => (
  • {node.name} {node.address}:{node.port} {node.protocol}
  • ))}
); })}
)} /> ( {t("form.node")}
{getNodesWithoutTags().map((item) => { const value = field.value || []; return (
checked ? form.setValue(field.name, [ ...value, item.id, ]) : form.setValue( field.name, value.filter( (value: number) => value !== item.id ) ) } />
); })}
)} />
); }