ClientBulkAdjustModal.tsx 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. import { useEffect, useState } from 'react';
  2. import { useTranslation } from 'react-i18next';
  3. import { Alert, Form, InputNumber, Modal, Select, message } from 'antd';
  4. import { FormProvider, useForm } from 'react-hook-form';
  5. import { ClientBulkAdjustFormSchema, type ClientBulkAdjustFormValues } from '@/schemas/client';
  6. import { TLS_FLOW_CONTROL } from '@/schemas/primitives/flow';
  7. import { FormField } from '@/components/form/rhf';
  8. const GB = 1024 * 1024 * 1024;
  9. const FLOW_CLEAR = 'none';
  10. const EMPTY: ClientBulkAdjustFormValues = { addDays: 0, addGB: 0, flow: '' };
  11. interface ClientBulkAdjustModalProps {
  12. open: boolean;
  13. count: number;
  14. onOpenChange: (open: boolean) => void;
  15. onSubmit: (addDays: number, addBytes: number, flow: string) => Promise<{ adjusted: number; skipped?: { email: string; reason: string }[] } | null>;
  16. }
  17. export default function ClientBulkAdjustModal({ open, count, onOpenChange, onSubmit }: ClientBulkAdjustModalProps) {
  18. const { t } = useTranslation();
  19. const [messageApi, messageContextHolder] = message.useMessage();
  20. const [submitting, setSubmitting] = useState(false);
  21. const methods = useForm<ClientBulkAdjustFormValues>({ defaultValues: EMPTY });
  22. useEffect(() => {
  23. if (open) methods.reset(EMPTY);
  24. }, [open, methods]);
  25. async function handleOk() {
  26. const values = methods.getValues();
  27. const validated = ClientBulkAdjustFormSchema.safeParse({
  28. addDays: Math.trunc(Number(values.addDays) || 0),
  29. addGB: Number(values.addGB) || 0,
  30. flow: values.flow,
  31. });
  32. if (!validated.success) {
  33. messageApi.warning(t(validated.error.issues[0]?.message ?? 'somethingWentWrong'));
  34. return;
  35. }
  36. const { addDays: days, addGB: gb, flow: flowValue } = validated.data;
  37. setSubmitting(true);
  38. try {
  39. const bytes = Math.trunc(gb * GB);
  40. const result = await onSubmit(days, bytes, flowValue);
  41. if (!result) return;
  42. const ok = result.adjusted ?? 0;
  43. const skipped = result.skipped?.length ?? 0;
  44. if (skipped === 0) {
  45. messageApi.success(t('pages.clients.toasts.bulkAdjusted', { count: ok }));
  46. } else {
  47. const firstReason = result.skipped?.[0]?.reason ?? '';
  48. messageApi.warning(firstReason
  49. ? `${t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped })} — ${firstReason}`
  50. : t('pages.clients.toasts.bulkAdjustedMixed', { ok, skipped }));
  51. }
  52. onOpenChange(false);
  53. } finally {
  54. setSubmitting(false);
  55. }
  56. }
  57. return (
  58. <>
  59. {messageContextHolder}
  60. <Modal
  61. open={open}
  62. title={t('pages.clients.bulkAdjustTitle', { count })}
  63. okText={t('apply')}
  64. cancelText={t('cancel')}
  65. confirmLoading={submitting}
  66. onOk={handleOk}
  67. onCancel={() => onOpenChange(false)}
  68. destroyOnHidden
  69. >
  70. <Alert
  71. type="info"
  72. showIcon
  73. style={{ marginBottom: 16 }}
  74. title={t('pages.clients.bulkAdjustHint')}
  75. />
  76. <FormProvider {...methods}>
  77. <Form layout="vertical">
  78. <FormField name="addDays" label={t('pages.clients.addDays')}>
  79. <InputNumber style={{ width: '100%' }} step={1} precision={0} />
  80. </FormField>
  81. <FormField name="addGB" label={t('pages.clients.addTrafficGB')}>
  82. <InputNumber style={{ width: '100%' }} step={1} />
  83. </FormField>
  84. <FormField name="flow" label={t('pages.clients.bulkFlow')}>
  85. <Select
  86. style={{ width: '100%' }}
  87. options={[
  88. { value: '', label: t('pages.clients.bulkFlowNoChange') },
  89. { value: FLOW_CLEAR, label: t('pages.clients.bulkFlowDisable') },
  90. ...Object.values(TLS_FLOW_CONTROL).map((k) => ({ value: k, label: k })),
  91. ]}
  92. />
  93. </FormField>
  94. </Form>
  95. </FormProvider>
  96. </Modal>
  97. </>
  98. );
  99. }