'use client'; import useGlobalStore from '@/config/use-global'; import { useNode } from '@/store/node'; 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/custom-components/combobox'; import { ArrayInput } from '@workspace/ui/custom-components/dynamic-Inputs'; import { JSONEditor } from '@workspace/ui/custom-components/editor'; import { EnhancedInput } from '@workspace/ui/custom-components/enhanced-input'; import { Icon } from '@workspace/ui/custom-components/icon'; import { evaluateWithPrecision, unitConversion } from '@workspace/ui/utils'; import { CreditCard, Server, Settings } from 'lucide-react'; import { useTranslations } from 'next-intl'; import { assign, shake } from 'radash'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useForm } from 'react-hook-form'; import { toast } from 'sonner'; import { z } from 'zod'; 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 = useTranslations('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(), }); 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(() => { return () => { 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)} /> )} />
( {t('form.traffic')} unitConversion('bytesToGb', value)} formatOutput={(value) => unitConversion('gbToBytes', value)} suffix='GB' onValueChange={(value) => { form.setValue(field.name, value); }} /> )} /> ( {t('form.speedLimit')} unitConversion('bitsToMb', value)} formatOutput={(value) => unitConversion('mbToBits', value)} suffix='Mbps' onValueChange={(value) => { form.setValue(field.name, value); }} /> )} /> ( {t('form.deviceLimit')} { form.setValue(field.name, value); }} /> )} />
( {t('form.inventory')} { form.setValue(field.name, value); }} /> )} /> ( {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, }} /> )} />
( {t('form.unitPrice')} unitConversion('centsToDollars', value)} formatOutput={(value) => unitConversion('dollarsToCents', value)} 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)} 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(), }, ]} value={field.value} 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, ); } }} /> {t('form.discountDescription')} )} /> ( {t('form.deductionRatio')} { form.setValue(field.name, value); }} /> {t('form.deductionRatioDescription')} )} /> (
{t('form.renewalReset')} {t('form.renewalResetDescription')}
)} /> (
{t('form.purchaseWithDiscount')} {t('form.purchaseWithDiscountDescription')}
{ form.setValue(field.name, value); }} />
)} />
( {t('form.nodeGroup')} {tagGroups.map((tag) => { const value = field.value || []; const tagId = tag; const nodesWithTag = getNodesByTag(tag); return (
{ 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 (
{ return checked ? form.setValue(field.name, [...value, item.id]) : form.setValue( field.name, value.filter((value: number) => value !== item.id), ); }} />
); })}
)} />
); }