import { useEffect, useId, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useFormContext, useWatch } from 'react-hook-form'; import { useQuery } from '@tanstack/react-query'; import { Button, Col, Form, InputNumber, Row, Select, Space, Typography } from 'antd'; import { FormField } from '@/components/form/rhf'; import { ClientRenewalPreviewSchema } from '@/generated/zod'; import { HttpUtil } from '@/utils'; import type { ClientFormValues } from '@/schemas/client'; type RenewalFields = Pick; type RenewalMode = 'none' | 'interval' | 'weekly' | 'monthly'; function RenewalDate({ value }: { value: string }) { // Keep the server's wall time and per-date offset, including across DST. // Parsing into a browser-local Date would show a different renewal boundary. const display = value .replace('T', ' ') .replace(/(Z|[+-]\d{2}:\d{2})$/, (offset) => ` UTC${offset === 'Z' ? '+00:00' : offset}`); return ( ); } export default function ClientRenewalFields({ active, expiryTime, resetCount = 0, bulk = false, delayedStart = false, setExpiry, }: { active: boolean; expiryTime: number; resetCount?: number; bulk?: boolean; delayedStart?: boolean; setExpiry: (expiry: number) => void; }) { const { t, i18n } = useTranslation(); const formId = useId(); const modeId = 'client-renewal-mode-' + formId; const { control, setValue } = useFormContext(); const [reset, resetDay, resetWeekday, resetMax] = useWatch({ control, name: ['reset', 'resetDay', 'resetWeekday', 'resetMax'], }); const mode: RenewalMode = resetDay > 0 ? 'monthly' : resetWeekday > 0 ? 'weekly' : reset > 0 ? 'interval' : 'none'; const request = useMemo( () => ({ expiryTime, reset: reset || 0, resetDay: resetDay || 0, resetWeekday: resetWeekday || 0, resetMax: resetMax || 0, resetCount, }), [expiryTime, reset, resetDay, resetWeekday, resetMax, resetCount], ); const [debounced, setDebounced] = useState(request); useEffect(() => { const timer = setTimeout(() => setDebounced(request), 250); return () => clearTimeout(timer); }, [request]); const query = useQuery({ queryKey: ['clients', 'renewalPreview', debounced], enabled: active && mode !== 'none' && request === debounced, retry: false, queryFn: async () => { const msg = await HttpUtil.post('/panel/api/clients/renewalPreview', debounced, { headers: { 'Content-Type': 'application/json' }, silent: true, }); if (!msg?.success) throw new Error(msg?.msg || 'Renewal preview failed'); return ClientRenewalPreviewSchema.parse(msg.obj); }, }); const preview = request === debounced ? query.data : undefined; const weekdayFormatter = new Intl.DateTimeFormat(i18n.language, { weekday: 'long', timeZone: 'UTC', }); function changeMode(next: RenewalMode) { setValue('reset', next === 'interval' ? Math.max(1, reset || 0) : 0); setValue('resetDay', next === 'monthly' ? Math.max(1, resetDay || 0) : 0); setValue('resetWeekday', next === 'weekly' ? Math.max(1, resetWeekday || 0) : 0); } return ( <> ({ value: i + 1, label: weekdayFormatter.format(new Date(Date.UTC(2026, 0, i + 5))), }))} /> )} {mode !== 'none' && ( Number(v) || 0 }} > )} {mode !== 'none' && ( <> {mode === 'interval' ? t('pages.clients.renewIntervalDesc') : mode === 'weekly' ? t('pages.clients.renewWeeklyDesc') : t('pages.clients.renewScheduleDesc')} {query.isError && request === debounced && ( {t('pages.clients.renewPreviewError')} )} {preview && ( {t('pages.clients.renewPreview', { zone: preview.timeZone === 'Local' ? t('pages.clients.renewServerLocal') : preview.timeZone, })} {t('pages.clients.renewPreviewHint')} {delayedStart || preview.delayedStart ? ( {t('pages.clients.renewFirstUse')} ) : expiryTime === 0 ? ( <> {t('pages.clients.renewNeedsExpiry')} {preview.suggestedExpiryTime > 0 && ( )} ) : ( <> {t('pages.clients.renewAt')}: {t('pages.clients.renewValidThrough')}:{' '} {preview.nextExpiry && ( {t('pages.clients.renewNextExpiry')}:{' '} )} {t('pages.clients.renewPeriods', { count: preview.renewals })} {!preview.canRenew && ( {t('pages.clients.renewUnavailable')} )} )} )} )} ); }