feat: Add quota management features and localization updates

This commit is contained in:
web
2025-09-11 03:34:36 -07:00
parent e2d83ec9e6
commit fce627ba11
33 changed files with 2716 additions and 93 deletions
@@ -47,7 +47,7 @@ export default function EmailBroadcastForm() {
const emailBroadcastSchema = z.object({
subject: z.string().min(1, t('subject') + ' ' + t('cannotBeEmpty')),
content: z.string().min(1, t('content') + ' ' + t('cannotBeEmpty')),
scope: z.string(),
scope: z.number(),
register_start_time: z.string().optional(),
register_end_time: z.string().optional(),
additional: z
@@ -83,7 +83,7 @@ export default function EmailBroadcastForm() {
defaultValues: {
subject: '',
content: '',
scope: 'all',
scope: 1, // ScopeAll
register_start_time: '',
register_end_time: '',
additional: '',
@@ -99,7 +99,7 @@ export default function EmailBroadcastForm() {
try {
// Call API to get actual recipient count
const scope = formData.scope || 'all';
const scope = formData.scope || 1; // Default to ScopeAll
// Convert dates to timestamps if they exist
let register_start_time: number = 0;
@@ -153,8 +153,10 @@ export default function EmailBroadcastForm() {
// Listen to form changes
const watchedValues = form.watch();
// Use useEffect to respond to form changes
// Use useEffect to respond to form changes, but only when sheet is open
useEffect(() => {
if (!open) return; // Only calculate when sheet is open
const debounceTimer = setTimeout(() => {
calculateRecipients();
}, 500); // Add debounce to avoid too frequent API calls
@@ -162,6 +164,7 @@ export default function EmailBroadcastForm() {
return () => clearTimeout(debounceTimer);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
open, // Add open dependency
watchedValues.scope,
watchedValues.register_start_time,
watchedValues.register_end_time,
@@ -310,20 +313,27 @@ export default function EmailBroadcastForm() {
render={({ field }) => (
<FormItem>
<FormLabel>{t('sendScope')}</FormLabel>
<Select onValueChange={field.onChange} value={field.value || 'all'}>
<Select
onValueChange={(value) => field.onChange(parseInt(value))}
value={field.value?.toString() || '1'}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t('selectSendScope')} />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value='all'>{t('allUsers')}</SelectItem>
<SelectItem value='active'>{t('subscribedUsersOnly')}</SelectItem>
<SelectItem value='expired'>
<SelectItem value='1'>{t('allUsers')}</SelectItem> {/* ScopeAll */}
<SelectItem value='2'>{t('subscribedUsersOnly')}</SelectItem>{' '}
{/* ScopeActive */}
<SelectItem value='3'>
{t('expiredSubscriptionUsersOnly')}
</SelectItem>
<SelectItem value='none'>{t('noSubscriptionUsersOnly')}</SelectItem>
<SelectItem value='skip'>{t('specificUsersOnly')}</SelectItem>
</SelectItem>{' '}
{/* ScopeExpired */}
<SelectItem value='4'>{t('noSubscriptionUsersOnly')}</SelectItem>{' '}
{/* ScopeNone */}
<SelectItem value='5'>{t('specificUsersOnly')}</SelectItem>{' '}
{/* ScopeSkip */}
</SelectContent>
</Select>
<FormDescription>{t('sendScopeDescription')}</FormDescription>
@@ -356,7 +366,7 @@ export default function EmailBroadcastForm() {
<FormControl>
<EnhancedInput
type='datetime-local'
disabled={form.watch('scope') === 'skip'}
disabled={form.watch('scope') === 5} // ScopeSkip
value={field.value}
onValueChange={field.onChange}
/>
@@ -374,7 +384,7 @@ export default function EmailBroadcastForm() {
<FormControl>
<EnhancedInput
type='datetime-local'
disabled={form.watch('scope') === 'skip'}
disabled={form.watch('scope') === 5} // ScopeSkip
value={field.value}
onValueChange={field.onChange}
/>
@@ -33,6 +33,7 @@ export default function EmailTaskManager() {
const t = useTranslations('marketing');
const [refreshing, setRefreshing] = useState<Record<number, boolean>>({});
const [selectedTask, setSelectedTask] = useState<API.BatchSendEmailTask | null>(null);
const [open, setOpen] = useState(false);
// Get task status
const refreshTaskStatus = async (taskId: number) => {
@@ -86,7 +87,7 @@ export default function EmailTaskManager() {
};
return (
<Sheet>
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<div className='flex cursor-pointer items-center justify-between transition-colors'>
<div className='flex items-center gap-3'>
@@ -114,7 +115,6 @@ export default function EmailTaskManager() {
{
accessorKey: 'subject',
header: t('subject'),
size: 200,
cell: ({ row }) => (
<div
className='max-w-[200px] truncate font-medium'
@@ -127,29 +127,28 @@ export default function EmailTaskManager() {
{
accessorKey: 'scope',
header: t('recipientType'),
size: 120,
cell: ({ row }) => {
const scope = row.getValue('scope') as string;
const scope = row.original.scope;
const scopeLabels = {
all: t('allUsers'),
active: t('subscribedUsers'),
expired: t('expiredUsers'),
none: t('nonSubscribers'),
skip: t('specificUsers'),
1: t('allUsers'), // ScopeAll
2: t('subscribedUsers'), // ScopeActive
3: t('expiredUsers'), // ScopeExpired
4: t('nonSubscribers'), // ScopeNone
5: t('specificUsers'), // ScopeSkip
};
return scopeLabels[scope as keyof typeof scopeLabels] || scope;
return (
scopeLabels[scope as keyof typeof scopeLabels] || `${t('scope')} ${scope}`
);
},
},
{
accessorKey: 'status',
header: t('status'),
size: 100,
cell: ({ row }) => getStatusBadge(row.getValue('status') as number),
},
{
accessorKey: 'progress',
header: t('progress'),
size: 150,
cell: ({ row }) => {
const task = row.original as API.BatchSendEmailTask;
const progress = task.total > 0 ? (task.current / task.total) * 100 : 0;
@@ -171,24 +170,22 @@ export default function EmailTaskManager() {
);
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
size: 150,
cell: ({ row }) => {
const createdAt = row.getValue('created_at') as number;
return formatDate(createdAt);
},
},
{
accessorKey: 'scheduled',
header: t('sendTime'),
size: 150,
cell: ({ row }) => {
const scheduled = row.getValue('scheduled') as number;
return scheduled && scheduled > 0 ? formatDate(scheduled) : '--';
},
},
{
accessorKey: 'created_at',
header: t('createdAt'),
cell: ({ row }) => {
const createdAt = row.getValue('created_at') as number;
return formatDate(createdAt);
},
},
]}
request={async (pagination, filters) => {
const response = await getBatchSendEmailTaskList({
@@ -215,11 +212,11 @@ export default function EmailTaskManager() {
key: 'scope',
placeholder: t('sendScope'),
options: [
{ label: t('allUsers'), value: 'all' },
{ label: t('subscribedUsers'), value: 'active' },
{ label: t('expiredUsers'), value: 'expired' },
{ label: t('nonSubscribers'), value: 'none' },
{ label: t('specificUsers'), value: 'skip' },
{ label: t('allUsers'), value: '1' },
{ label: t('subscribedUsers'), value: '2' },
{ label: t('expiredUsers'), value: '3' },
{ label: t('nonSubscribers'), value: '4' },
{ label: t('specificUsers'), value: '5' },
],
},
]}
@@ -276,9 +273,9 @@ export default function EmailTaskManager() {
disabled={refreshing[row.id]}
>
{refreshing[row.id] ? (
<Icon icon='mdi:loading' className='mr-2 h-3 w-3 animate-spin' />
<Icon icon='mdi:loading' className='h-3 w-3 animate-spin' />
) : (
<Icon icon='mdi:refresh' className='mr-2 h-3 w-3' />
<Icon icon='mdi:refresh' className='h-3 w-3' />
)}
</Button>,
...([0, 1].includes(row.status)