Re_Figma_Code/src/components/admin/Form16AdminConfig/Form16AdminConfig.tsx

586 lines
28 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Switch } from '@/components/ui/switch';
import { Textarea } from '@/components/ui/textarea';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { FileText, Database, Bell, Loader2, Plus, Trash2, Save } from 'lucide-react';
import {
getForm16Config,
putForm16Config,
type Form16AdminConfig as Form16ConfigType,
type Form16NotificationItem,
type Form16Notification26AsItem,
} from '@/services/adminApi';
import { toast } from 'sonner';
function isValidEmail(s: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s.trim());
}
interface ViewerTableProps {
emails: string[];
onAdd: (email: string) => void;
onRemove: (email: string) => void;
placeholder?: string;
}
function ViewerTable({ emails, onAdd, onRemove, placeholder }: ViewerTableProps) {
const [input, setInput] = useState('');
const add = () => {
const e = input.trim().toLowerCase();
if (!e) return;
if (!isValidEmail(e)) {
toast.error('Please enter a valid email address');
return;
}
if (emails.includes(e)) {
toast.error('This email is already in the list');
return;
}
onAdd(e);
setInput('');
};
return (
<div className="space-y-3">
<div className="flex gap-2">
<Input
type="email"
placeholder={placeholder ?? 'e.g., user@royalenfield.com'}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), add())}
className="flex-1"
/>
<Button type="button" variant="outline" size="sm" onClick={add} className="shrink-0 gap-1">
<Plus className="w-4 h-4" />
Add
</Button>
</div>
{emails.length > 0 ? (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Email</TableHead>
<TableHead className="w-[80px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{emails.map((email) => (
<TableRow key={email}>
<TableCell className="font-medium">{email}</TableCell>
<TableCell className="text-right">
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => onRemove(email)}
className="text-destructive hover:text-destructive"
>
<Trash2 className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<p className="text-sm text-muted-foreground py-4 text-center rounded-md border border-dashed">
No viewers added. Add emails above or leave empty to allow all RE users with access.
</p>
)}
</div>
);
}
function defaultNotif(enabled: boolean, template: string): Form16NotificationItem {
return { enabled, template };
}
const default26AsNotif = (): Form16Notification26AsItem => ({
enabled: true,
templateRe: '26AS data has been added. Please review and use for matching dealer Form 16 submissions.',
templateDealers: 'New 26AS data has been uploaded. You can now submit your Form 16 for the relevant quarter if you havent already.',
});
export function Form16AdminConfig() {
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [submissionViewerEmails, setSubmissionViewerEmails] = useState<string[]>([]);
const [twentySixAsViewerEmails, setTwentySixAsViewerEmails] = useState<string[]>([]);
const [reminderEnabled, setReminderEnabled] = useState(true);
const [reminderDays, setReminderDays] = useState(7);
const [notification26AsDataAdded, setNotification26AsDataAdded] = useState<Form16Notification26AsItem>(default26AsNotif());
const [notificationForm16SuccessCreditNote, setNotificationForm16SuccessCreditNote] = useState<Form16NotificationItem>(defaultNotif(true, 'Form 16 submitted successfully. Credit note: [CreditNoteRef].'));
const [notificationForm16Unsuccessful, setNotificationForm16Unsuccessful] = useState<Form16NotificationItem>(defaultNotif(true, 'Form 16 submission was unsuccessful. Issue: [Issue]. Please review.'));
const [alertSubmitForm16Enabled, setAlertSubmitForm16Enabled] = useState(true);
const [alertSubmitForm16FrequencyDays, setAlertSubmitForm16FrequencyDays] = useState(0);
const [alertSubmitForm16FrequencyHours, setAlertSubmitForm16FrequencyHours] = useState(24);
const [alertSubmitForm16RunAtTime, setAlertSubmitForm16RunAtTime] = useState('09:00');
const [alertSubmitForm16Template, setAlertSubmitForm16Template] = useState('Please submit your Form 16 at your earliest. [Name], due date: [DueDate].');
const [reminderNotificationEnabled, setReminderNotificationEnabled] = useState(true);
const [reminderFrequencyDays, setReminderFrequencyDays] = useState(0);
const [reminderFrequencyHours, setReminderFrequencyHours] = useState(12);
const [reminderRunAtTime, setReminderRunAtTime] = useState('10:00');
const [reminderNotificationTemplate, setReminderNotificationTemplate] = useState('Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.');
useEffect(() => {
let mounted = true;
getForm16Config()
.then((config: Form16ConfigType) => {
if (!mounted) return;
setSubmissionViewerEmails(config.submissionViewerEmails ?? []);
setTwentySixAsViewerEmails(config.twentySixAsViewerEmails ?? []);
setReminderEnabled(config.reminderEnabled ?? true);
setReminderDays(typeof config.reminderDays === 'number' ? config.reminderDays : 7);
if (config.notification26AsDataAdded) {
const n = config.notification26AsDataAdded as Form16Notification26AsItem & { template?: string };
setNotification26AsDataAdded({
enabled: n.enabled ?? true,
templateRe: n.templateRe ?? n.template ?? default26AsNotif().templateRe,
templateDealers: n.templateDealers ?? default26AsNotif().templateDealers,
});
}
if (config.notificationForm16SuccessCreditNote) setNotificationForm16SuccessCreditNote(config.notificationForm16SuccessCreditNote);
if (config.notificationForm16Unsuccessful) setNotificationForm16Unsuccessful(config.notificationForm16Unsuccessful);
setAlertSubmitForm16Enabled(config.alertSubmitForm16Enabled ?? true);
setAlertSubmitForm16FrequencyDays(config.alertSubmitForm16FrequencyDays ?? 0);
setAlertSubmitForm16FrequencyHours(config.alertSubmitForm16FrequencyHours ?? 24);
setAlertSubmitForm16RunAtTime(config.alertSubmitForm16RunAtTime !== undefined && config.alertSubmitForm16RunAtTime !== null ? config.alertSubmitForm16RunAtTime : '09:00');
setAlertSubmitForm16Template(config.alertSubmitForm16Template ?? 'Please submit your Form 16 at your earliest. [Name], due date: [DueDate].');
setReminderNotificationEnabled(config.reminderNotificationEnabled ?? true);
setReminderFrequencyDays(config.reminderFrequencyDays ?? 0);
setReminderFrequencyHours(config.reminderFrequencyHours ?? 12);
setReminderRunAtTime(config.reminderRunAtTime !== undefined && config.reminderRunAtTime !== null ? config.reminderRunAtTime : '10:00');
setReminderNotificationTemplate(config.reminderNotificationTemplate ?? 'Reminder: Form 16 submission is pending. [Name], [Request ID]. Please review.');
})
.catch(() => {
if (mounted) toast.error('Failed to load Form 16 configuration');
})
.finally(() => {
if (mounted) setLoading(false);
});
return () => {
mounted = false;
};
}, []);
const handleSave = async () => {
setSaving(true);
try {
await putForm16Config({
submissionViewerEmails,
twentySixAsViewerEmails,
reminderEnabled,
reminderDays: Math.max(1, Math.min(365, reminderDays)) || 7,
notification26AsDataAdded,
notificationForm16SuccessCreditNote,
notificationForm16Unsuccessful,
alertSubmitForm16Enabled,
alertSubmitForm16FrequencyDays: Math.max(0, Math.min(365, alertSubmitForm16FrequencyDays)),
alertSubmitForm16FrequencyHours: Math.max(0, Math.min(168, alertSubmitForm16FrequencyHours)),
alertSubmitForm16RunAtTime: alertSubmitForm16RunAtTime ?? '',
alertSubmitForm16Template,
reminderNotificationEnabled,
reminderFrequencyDays: Math.max(0, Math.min(365, reminderFrequencyDays)),
reminderFrequencyHours: Math.max(0, Math.min(168, reminderFrequencyHours)),
reminderRunAtTime: reminderRunAtTime ?? '',
reminderNotificationTemplate,
});
toast.success('Form 16 configuration saved');
} catch {
toast.error('Failed to save Form 16 configuration');
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-8 h-8 animate-spin text-re-green" />
</div>
);
}
return (
<div className="space-y-6">
{/* Page header */}
<div>
<h2 className="text-2xl font-bold tracking-tight">Form 16 Administration</h2>
<p className="text-muted-foreground mt-1">
Configure Form 16 access, who can view submission data and 26AS, and notification settings.
</p>
</div>
{/* Summary cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-muted-foreground">Submission data viewers (RE)</p>
<p className="text-2xl font-semibold">{submissionViewerEmails.length}</p>
<p className="text-xs text-muted-foreground mt-0.5">Who can see Form 16 submissions</p>
</div>
<div className="p-4 bg-muted/50 rounded-lg">
<p className="text-sm text-muted-foreground">26AS viewers (RE)</p>
<p className="text-2xl font-semibold">{twentySixAsViewerEmails.length}</p>
<p className="text-xs text-muted-foreground mt-0.5">Who can see 26AS page</p>
</div>
<div className="p-4 bg-green-50 rounded-lg">
<p className="text-sm text-muted-foreground">Reminders to dealers</p>
<p className="text-2xl font-semibold text-green-600">{reminderEnabled ? 'On' : 'Off'}</p>
<p className="text-xs text-muted-foreground mt-0.5">Pending Form 16 reminder schedule</p>
</div>
<div className="p-4 bg-purple-50 rounded-lg">
<p className="text-sm text-muted-foreground">Email / in-app notifications</p>
<p className="text-2xl font-semibold text-purple-600">
{[
notification26AsDataAdded?.enabled,
notificationForm16SuccessCreditNote?.enabled,
notificationForm16Unsuccessful?.enabled,
alertSubmitForm16Enabled,
reminderNotificationEnabled,
].filter(Boolean).length}{' '}
/ 5 enabled
</p>
<p className="text-xs text-muted-foreground mt-0.5">To dealers and RE as per rules below</p>
</div>
</div>
{/* Submission data viewers */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5" />
Submission data who can see
</CardTitle>
<CardDescription>
Users with these email addresses can see Form 16 submission data (and the Form 16 menu in the sidebar). Use the <strong>exact login email</strong> of each user (the same email they use to sign in). Leave the list empty to allow all RE users with Form 16 access.
</CardDescription>
</CardHeader>
<CardContent>
<ViewerTable
emails={submissionViewerEmails}
onAdd={(email) => setSubmissionViewerEmails((prev) => [...prev, email].sort())}
onRemove={(email) => setSubmissionViewerEmails((prev) => prev.filter((e) => e !== email))}
placeholder="e.g., user@royalenfield.com"
/>
</CardContent>
</Card>
{/* 26AS viewers */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Database className="w-5 h-5" />
26AS page and button who can see
</CardTitle>
<CardDescription>
Users with these email addresses can see the 26AS page and 26AS menu item. Use the <strong>exact login email</strong> of each user. Leave empty to allow all RE users.
</CardDescription>
</CardHeader>
<CardContent>
<ViewerTable
emails={twentySixAsViewerEmails}
onAdd={(email) => setTwentySixAsViewerEmails((prev) => [...prev, email].sort())}
onRemove={(email) => setTwentySixAsViewerEmails((prev) => prev.filter((e) => e !== email))}
placeholder="e.g., user@royalenfield.com"
/>
</CardContent>
</Card>
{/* Notifications and reminders (simple toggles) */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="w-5 h-5" />
Reminder schedule (for dealers)
</CardTitle>
<CardDescription>
When reminders are enabled, dealers with pending Form 16 for a quarter are reminded at this interval. Set how often (in days) the system may send them a reminder to submit Form 16.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="flex items-center justify-between">
<Label htmlFor="reminder-enabled">Enable reminders to dealers</Label>
<Switch
id="reminder-enabled"
checked={reminderEnabled}
onCheckedChange={setReminderEnabled}
/>
</div>
<div className="space-y-2 max-w-xs">
<Label htmlFor="reminder-days">Remind dealers every (days)</Label>
<Input
id="reminder-days"
type="number"
min={1}
max={365}
value={reminderDays}
onChange={(e) => setReminderDays(parseInt(e.target.value, 10) || 7)}
/>
</div>
</CardContent>
</Card>
{/* Notification Configuration Form 16 events */}
<Card>
<CardHeader>
<CardTitle>Email and in-app notifications</CardTitle>
<CardDescription>
Configure who receives each notification, what triggers it, and when it is sent. Templates support placeholders such as [Name], [Request ID], [DueDate].
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-3">
<h4 className="font-medium">Form 16 notifications recipient and trigger</h4>
{/* 26AS data added separate message for RE users and for dealers */}
<div className="flex items-start justify-between gap-4 p-4 bg-muted/50 rounded-lg">
<div className="flex-1 min-w-0 space-y-3">
<p className="font-medium">26AS data added</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Sent to:</span> RE users who can view 26AS, and separately to all dealers. <span className="font-medium text-foreground">When:</span> As soon as new 26AS data is uploaded.
</p>
<div className="space-y-2">
<div>
<Label className="text-xs text-muted-foreground">Message to RE users</Label>
<Textarea
rows={2}
value={notification26AsDataAdded.templateRe ?? ''}
onChange={(e) => setNotification26AsDataAdded((prev) => ({ ...prev, templateRe: e.target.value }))}
className="resize-none text-sm mt-1"
placeholder="e.g. 26AS data has been added. Please review..."
/>
</div>
<div>
<Label className="text-xs text-muted-foreground">Message to dealers</Label>
<Textarea
rows={2}
value={notification26AsDataAdded.templateDealers ?? ''}
onChange={(e) => setNotification26AsDataAdded((prev) => ({ ...prev, templateDealers: e.target.value }))}
className="resize-none text-sm mt-1"
placeholder="e.g. New 26AS data has been uploaded. You can submit Form 16..."
/>
</div>
<p className="text-xs text-muted-foreground">Placeholders: [Name], [Request ID]</p>
</div>
</div>
<Switch
checked={notification26AsDataAdded.enabled}
onCheckedChange={(enabled) => setNotification26AsDataAdded((prev) => ({ ...prev, enabled }))}
/>
</div>
{/* Successful Form 16 with credit note */}
<div className="flex items-start justify-between gap-4 p-4 bg-muted/50 rounded-lg">
<div className="flex-1 min-w-0">
<p className="font-medium">Form 16 success credit note issued</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Sent to:</span> The dealer who submitted the Form 16. <span className="font-medium text-foreground">When:</span> Immediately after their submission is matched with 26AS and a credit note is generated.
</p>
<div className="mt-2">
<Textarea
rows={2}
value={notificationForm16SuccessCreditNote.template ?? ''}
onChange={(e) => setNotificationForm16SuccessCreditNote((prev) => ({ ...prev, template: e.target.value }))}
className="resize-none text-sm"
placeholder="Message template..."
/>
<p className="text-xs text-muted-foreground mt-1">Placeholders: [Name], [CreditNoteRef], [Request ID]</p>
</div>
</div>
<Switch
checked={notificationForm16SuccessCreditNote.enabled}
onCheckedChange={(enabled) => setNotificationForm16SuccessCreditNote((prev) => ({ ...prev, enabled }))}
/>
</div>
{/* Unsuccessful Form 16 */}
<div className="flex items-start justify-between gap-4 p-4 bg-muted/50 rounded-lg">
<div className="flex-1 min-w-0">
<p className="font-medium">Form 16 unsuccessful (mismatch or issue)</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Sent to:</span> The dealer who submitted. <span className="font-medium text-foreground">When:</span> When their submission fails (e.g. value mismatch with 26AS, duplicate, or validation error) so they can correct and resubmit.
</p>
<div className="mt-2">
<Textarea
rows={2}
value={notificationForm16Unsuccessful.template ?? ''}
onChange={(e) => setNotificationForm16Unsuccessful((prev) => ({ ...prev, template: e.target.value }))}
className="resize-none text-sm"
placeholder="Message template..."
/>
<p className="text-xs text-muted-foreground mt-1">Placeholders: [Name], [Issue], [Request ID]</p>
</div>
</div>
<Switch
checked={notificationForm16Unsuccessful.enabled}
onCheckedChange={(enabled) => setNotificationForm16Unsuccessful((prev) => ({ ...prev, enabled }))}
/>
</div>
{/* Alert to submit Form 16 (auto, configurable) */}
<div className="flex items-start justify-between gap-4 p-4 bg-muted/50 rounded-lg">
<div className="flex-1 min-w-0">
<p className="font-medium">Alert submit Form 16 (to dealers who havent submitted)</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Sent to:</span> Dealers who have not yet submitted Form 16 for the current FY. <span className="font-medium text-foreground">When:</span> Daily at the time below (server timezone). All settings are API-driven from this config.
</p>
<div className="mt-2 space-y-2">
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label htmlFor="alert-run-at" className="text-sm whitespace-nowrap">Run daily at (optional):</Label>
<Input
id="alert-run-at"
type="time"
value={alertSubmitForm16RunAtTime}
onChange={(e) => setAlertSubmitForm16RunAtTime(e.target.value)}
className="w-28"
/>
{alertSubmitForm16RunAtTime ? (
<Button type="button" variant="ghost" size="sm" className="text-muted-foreground" onClick={() => setAlertSubmitForm16RunAtTime('')}>
Clear
</Button>
) : null}
</div>
<span className="text-xs text-muted-foreground">24h, server TZ. Leave empty to disable daily run.</span>
</div>
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label htmlFor="alert-freq-days" className="text-sm whitespace-nowrap">Frequency (days):</Label>
<Input
id="alert-freq-days"
type="number"
min={0}
max={365}
value={alertSubmitForm16FrequencyDays}
onChange={(e) => setAlertSubmitForm16FrequencyDays(Math.max(0, parseInt(e.target.value, 10) || 0))}
className="w-20"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="alert-freq-hours" className="text-sm whitespace-nowrap">Hours:</Label>
<Input
id="alert-freq-hours"
type="number"
min={0}
max={168}
value={alertSubmitForm16FrequencyHours}
onChange={(e) => setAlertSubmitForm16FrequencyHours(Math.max(0, parseInt(e.target.value, 10) || 0))}
className="w-20"
/>
</div>
</div>
<div>
<Textarea
rows={2}
value={alertSubmitForm16Template}
onChange={(e) => setAlertSubmitForm16Template(e.target.value)}
className="resize-none text-sm mt-1"
placeholder="Message template for alert to dealers..."
/>
<p className="text-xs text-muted-foreground mt-1">Placeholders: [Name], [DueDate], [Request ID]</p>
</div>
</div>
</div>
<Switch
checked={alertSubmitForm16Enabled}
onCheckedChange={setAlertSubmitForm16Enabled}
/>
</div>
{/* Reminder notification (pending Form 16) */}
<div className="flex items-start justify-between gap-4 p-4 bg-muted/50 rounded-lg">
<div className="flex-1 min-w-0">
<p className="font-medium">Reminder pending Form 16 (to dealers)</p>
<p className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">Sent to:</span> Dealers who have at least one open Form 16 submission without a credit note. <span className="font-medium text-foreground">When:</span> Daily at the time below (server timezone). All settings are API-driven from this config.
</p>
<div className="mt-2 space-y-2">
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label htmlFor="reminder-run-at" className="text-sm whitespace-nowrap">Run daily at (optional):</Label>
<Input
id="reminder-run-at"
type="time"
value={reminderRunAtTime}
onChange={(e) => setReminderRunAtTime(e.target.value)}
className="w-28"
/>
{reminderRunAtTime ? (
<Button type="button" variant="ghost" size="sm" className="text-muted-foreground" onClick={() => setReminderRunAtTime('')}>
Clear
</Button>
) : null}
</div>
<span className="text-xs text-muted-foreground">24h, server TZ. Leave empty to disable daily run.</span>
</div>
<div className="flex flex-wrap items-center gap-3">
<div className="flex items-center gap-2">
<Label htmlFor="reminder-freq-days" className="text-sm whitespace-nowrap">Frequency (days):</Label>
<Input
id="reminder-freq-days"
type="number"
min={0}
max={365}
value={reminderFrequencyDays}
onChange={(e) => setReminderFrequencyDays(Math.max(0, parseInt(e.target.value, 10) || 0))}
className="w-20"
/>
</div>
<div className="flex items-center gap-2">
<Label htmlFor="reminder-freq-hours" className="text-sm whitespace-nowrap">Hours:</Label>
<Input
id="reminder-freq-hours"
type="number"
min={0}
max={168}
value={reminderFrequencyHours}
onChange={(e) => setReminderFrequencyHours(Math.max(0, parseInt(e.target.value, 10) || 0))}
className="w-20"
/>
</div>
</div>
<div>
<Textarea
rows={2}
value={reminderNotificationTemplate}
onChange={(e) => setReminderNotificationTemplate(e.target.value)}
className="resize-none text-sm mt-1"
placeholder="Message template for reminder to dealers..."
/>
<p className="text-xs text-muted-foreground mt-1">Placeholders: [Name], [Request ID], [Status], [TAT]</p>
</div>
</div>
</div>
<Switch
checked={reminderNotificationEnabled}
onCheckedChange={setReminderNotificationEnabled}
/>
</div>
</div>
</CardContent>
</Card>
<div className="flex justify-end">
<Button onClick={handleSave} disabled={saving} className="bg-re-green hover:bg-re-green/90 gap-2">
{saving ? <Loader2 className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
Save Form 16 configuration
</Button>
</div>
</div>
);
}