✨ feat: Add quota management features and localization updates
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user