progress bar started inlining with the stage status and action buttons visibility enhanced

This commit is contained in:
laxmanhalaki 2026-02-19 20:44:21 +05:30
parent e786ea12cf
commit 3208a1ea7f
6 changed files with 1263 additions and 671 deletions

View File

@ -53,6 +53,8 @@ export const API = {
submitKTMatrix: (data: any) => client.post('/assessment/kt-matrix', data), submitKTMatrix: (data: any) => client.post('/assessment/kt-matrix', data),
submitLevel2Feedback: (data: any) => client.post('/assessment/level2-feedback', data), submitLevel2Feedback: (data: any) => client.post('/assessment/level2-feedback', data),
getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`), getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`),
updateRecommendation: (data: any) => client.post('/assessment/recommendation', data),
updateInterviewDecision: (data: any) => client.post('/assessment/decision', data),
// Collaboration & Participants // Collaboration & Participants
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }), getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
@ -67,6 +69,14 @@ export const API = {
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data), updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
deleteUser: (id: string) => client.delete(`/admin/users/${id}`), deleteUser: (id: string) => client.delete(`/admin/users/${id}`),
// Email Templates
getEmailTemplates: () => client.get('/admin/email-templates'),
getEmailTemplate: (id: string) => client.get(`/admin/email-templates/${id}`),
createEmailTemplate: (data: any) => client.post('/admin/email-templates', data),
updateEmailTemplate: (id: string, data: any) => client.put(`/admin/email-templates/${id}`, data),
deleteEmailTemplate: (id: string) => client.delete(`/admin/email-templates/${id}`),
previewEmailTemplate: (data: any) => client.post('/admin/email-templates/preview', data),
// Prospective Login // Prospective Login
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }), sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }), verifyOtp: (phone: string, otp: string) => client.post('/prospective-login/verify-otp', { phone, otp }),

File diff suppressed because it is too large Load Diff

View File

@ -84,7 +84,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
assignedUsers: [], // Keeping this for UI compatibility if needed assignedUsers: [], // Keeping this for UI compatibility if needed
assignedTo: app.assignedTo, // Add this field for filtering assignedTo: app.assignedTo, // Add this field for filtering
progress: app.progressPercentage || 0, progress: app.progressPercentage || 0,
isShortlisted: true, // Show all for admin view isShortlisted: app.ddLeadShortlisted || app.isShortlisted || false, // Use actual backend flags
// Add other fields to match interface // Add other fields to match interface
companyName: app.companyName, companyName: app.companyName,
source: app.source, source: app.source,
@ -119,13 +119,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter; const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
const matchesStatus = statusFilter === 'all' || app.status === statusFilter; const matchesStatus = statusFilter === 'all' || app.status === statusFilter;
const isShortlisted = app.isShortlisted === true; // Only show shortlisted applications const isShortlisted = app.isShortlisted === true;
const notExcluded = !excludedApplicationIds.includes(app.id); // Exclude APP-005, 006, 007, 008 const isNotQuestionnaireStage = !['Questionnaire Pending', 'Questionnaire Completed', 'Submitted'].includes(app.status);
const notExcluded = !excludedApplicationIds.includes(app.id);
// New Filter: My Assignments // New Filter: My Assignments
const matchesAssignment = !showMyAssignments || ((app as any).assignedTo === currentUser?.id); const matchesAssignment = !showMyAssignments || ((app as any).assignedTo === currentUser?.id);
return matchesSearch && matchesLocation && matchesStatus && isShortlisted && notExcluded && matchesAssignment; return matchesSearch && matchesLocation && matchesStatus && isShortlisted && isNotQuestionnaireStage && notExcluded && matchesAssignment;
}) })
.sort((a, b) => { .sort((a, b) => {
if (sortBy === 'date') { if (sortBy === 'date') {

View File

@ -30,7 +30,11 @@ import {
UserCog, UserCog,
Bell, Bell,
AlertTriangle, AlertTriangle,
X X,
Loader2,
Play,
Copy,
Check
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
@ -140,19 +144,137 @@ interface SLAConfig {
interface EmailTemplate { interface EmailTemplate {
id: string; id: string;
name: string; name: string;
templateCode?: string;
subject: string; subject: string;
body?: string;
trigger: string; trigger: string;
lastModified: string; isActive?: boolean;
createdAt?: string;
updatedAt?: string;
} }
// Template Scenarios Configuration
const TEMPLATE_SCENARIOS = [
{
category: "Authentication",
scenarios: [
{
name: "OTP Verification",
code: "AUTH_OTP",
description: "Sent when a user attempts to login or register.",
variables: ["{{otp}}", "{{expiry_minutes}}"],
sampleSubject: "Your Verification Code",
sampleBody: "Your OTP is <b>{{otp}}</b>. It expires in {{expiry_minutes}} minutes."
},
{
name: "Welcome Email",
code: "USER_WELCOME",
description: "Sent to new users upon account creation.",
variables: ["{{name}}", "{{email}}", "{{role}}", "{{login_link}}"],
sampleSubject: "Welcome to Dealer Portal",
sampleBody: "Hello {{name}},<br>Your account has been created as a {{role}}.<br>Login here: {{login_link}}"
}
]
},
{
category: "Dealer Application",
scenarios: [
{
name: "Application Received",
code: "APPLICATION_RECEIVED",
description: "Sent to applicant after submitting the form.",
variables: ["{{applicant_name}}", "{{application_id}}"],
sampleSubject: "Application Received - {{application_id}}",
sampleBody: "Dear {{applicant_name}},<br>We have received your application (ID: {{application_id}})."
},
{
name: "Clarification Requested",
code: "CLARIFICATION_REQUESTED",
description: "Sent when admin requests more info.",
variables: ["{{applicant_name}}", "{{application_id}}", "{{clarification_details}}", "{{link}}"],
sampleSubject: "Action Required: Application Clarification",
sampleBody: "Dear {{applicant_name}},<br>Please provide the following information: {{clarification_details}}<br>Link: {{link}}"
},
{
name: "Application Approved",
code: "APPLICATION_APPROVED",
description: "Sent when application is approved.",
variables: ["{{applicant_name}}", "{{application_id}}"],
sampleSubject: "Congratulations! Application Approved",
sampleBody: "Dear {{applicant_name}},<br>Your application {{application_id}} is approved."
},
{
name: "Application Rejected",
code: "APPLICATION_REJECTED",
description: "Sent when application is rejected.",
variables: ["{{applicant_name}}", "{{application_id}}", "{{rejection_reason}}"],
sampleSubject: "Update on your Application",
sampleBody: "Dear {{applicant_name}},<br>Your application {{application_id}} was not successful due to: {{rejection_reason}}."
}
]
},
{
category: "Interview Process",
scenarios: [
{
name: "Interview Scheduled",
code: "INTERVIEW_SCHEDULED",
description: "Sent when an interview is scheduled.",
variables: ["{{applicant_name}}", "{{round_name}}", "{{interview_date}}", "{{interview_time}}", "{{meeting_link}}", "{{interviewer_name}}"],
sampleSubject: "Interview Scheduled: {{round_name}}",
sampleBody: "Dear {{applicant_name}},<br>Your {{round_name}} interview is scheduled on {{interview_date}} at {{interview_time}}.<br>Link: {{meeting_link}}"
},
{
name: "Interview Feedback",
code: "INTERVIEW_FEEDBACK",
description: "Sent to admin/relevant parties after feedback.",
variables: ["{{applicant_name}}", "{{round_name}}", "{{status}}", "{{score}}"],
sampleSubject: "Interview Feedback Submitted",
sampleBody: "Feedback for {{applicant_name}} ({{round_name}}): Status {{status}}, Score {{score}}."
}
]
},
{
category: "Onboarding & Documents",
scenarios: [
{
name: "LOI Issued",
code: "LOI_ISSUED",
description: "Sent when Letter of Intent is issued.",
variables: ["{{applicant_name}}", "{{loi_amount}}", "{{valid_until}}", "{{payment_link}}"],
sampleSubject: "Letter of Intent Issued",
sampleBody: "Dear {{applicant_name}},<br>Your LOI is ready. Amount: {{loi_amount}}. Valid until: {{valid_until}}.<br>Pay here: {{payment_link}}"
},
{
name: "LOA Issued",
code: "LOA_ISSUED",
description: "Sent when Letter of Acceptance is issued.",
variables: ["{{applicant_name}}", "{{dealer_code}}"],
sampleSubject: "Welcome Aboard! LOA Issued",
sampleBody: "Dear {{applicant_name}},<br>Welcome! Your Dealer Code is {{dealer_code}}."
}
]
}
];
interface Location { interface Location {
id: string; id: string;
state: string; state?: string | { stateName: string; id: string };
city: string; city?: string;
district: string; areaName: string;
activeFrom: string; district?: string | { districtName: string; state?: { stateName: string; id: string }; id: string };
activeTo: string; stateId?: string; // Sometimes flattened
status: 'Active' | 'Inactive'; districtId?: string; // Sometimes flattened
pincode: string;
activeFrom?: string;
activeTo?: string;
isActive: boolean;
manager?: {
id: string;
fullName: string;
email?: string;
};
status?: 'Active' | 'Inactive'; // Keep for backward compatibility if needed
} }
interface ZonalManager { interface ZonalManager {
@ -241,6 +363,7 @@ interface UserAssignment {
employeeId?: string; employeeId?: string;
status: 'Active' | 'Inactive'; status: 'Active' | 'Inactive';
permissions?: string[]; permissions?: string[];
asmCode?: string; // Added for ASM role tracking
} }
export function MasterPage() { export function MasterPage() {
@ -363,7 +486,7 @@ export function MasterPage() {
setLoading(true); setLoading(true);
try { try {
// Fetch critical data first // Fetch critical data first
const [rolesRes, zonesRes, permsRes, regionsRes, usersRes, statesRes, asmRes] = await Promise.all([ const [rolesRes, zonesRes, permsRes, regionsRes, usersRes, statesRes, asmRes, emailTemplatesRes] = await Promise.all([
masterService.getRoles().catch(e => ({ success: false, error: e })), masterService.getRoles().catch(e => ({ success: false, error: e })),
masterService.getZones().catch(e => ({ success: false, error: e })), masterService.getZones().catch(e => ({ success: false, error: e })),
masterService.getPermissions().catch(e => ({ success: false, error: e })), masterService.getPermissions().catch(e => ({ success: false, error: e })),
@ -371,7 +494,8 @@ export function MasterPage() {
masterService.getUsers().catch(e => ({ success: false, error: e })), masterService.getUsers().catch(e => ({ success: false, error: e })),
masterService.getStates().catch(e => ({ success: false, error: e })), masterService.getStates().catch(e => ({ success: false, error: e })),
// Explicitly fetch Area Managers from the new dedicated endpoint // Explicitly fetch Area Managers from the new dedicated endpoint
masterService.getAreaManagers().catch(e => ({ success: false, error: e })) masterService.getAreaManagers().catch(e => ({ success: false, error: e })),
masterService.getEmailTemplates().catch(e => ({ success: false, error: e }))
]); ]);
// Fetch extensive data independently so it doesn't block critical UI // Fetch extensive data independently so it doesn't block critical UI
@ -523,6 +647,10 @@ export function MasterPage() {
}))); })));
} }
if (emailTemplatesRes && emailTemplatesRes.success) {
setEmailTemplates(emailTemplatesRes.data);
}
} catch (error) { } catch (error) {
console.error('Error fetching master data:', error); console.error('Error fetching master data:', error);
toast.error('Failed to load configuration data'); toast.error('Failed to load configuration data');
@ -644,16 +772,14 @@ export function MasterPage() {
}, },
]); ]);
// Mock data for email templates // Email Template State
const [emailTemplates] = useState<EmailTemplate[]>([ const [emailTemplates, setEmailTemplates] = useState<EmailTemplate[]>([]);
{ id: '1', name: 'Application Received', subject: 'Your Royal Enfield Dealership Application', trigger: 'On submission', lastModified: 'Dec 20, 2024' }, const [testDataInput, setTestDataInput] = useState<string>('{}');
{ id: '2', name: 'Interview Scheduled', subject: 'Interview Scheduled - Royal Enfield Dealership', trigger: 'When interview scheduled', lastModified: 'Dec 18, 2024' }, const [previewContent, setPreviewContent] = useState<{ subject: string, html: string } | null>(null);
{ id: '3', name: 'Application Approved', subject: 'Congratulations! Your Application is Approved', trigger: 'On approval', lastModified: 'Dec 15, 2024' }, const [previewLoading, setPreviewLoading] = useState(false);
{ id: '4', name: 'Application Rejected', subject: 'Update on Your Dealership Application', trigger: 'On rejection', lastModified: 'Dec 15, 2024' }, const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
{ id: '5', name: 'Document Required', subject: 'Additional Documents Required', trigger: 'When documents requested', lastModified: 'Dec 10, 2024' },
{ id: '6', name: 'Payment Reminder', subject: 'Payment Reminder - Royal Enfield Dealership', trigger: 'Payment pending', lastModified: 'Dec 12, 2024' }, // ... (keep existing handleSaveRole)
{ id: '7', name: 'SLA Breach Warning', subject: 'Action Required - Application Pending', trigger: 'Before SLA breach', lastModified: 'Dec 08, 2024' },
]);
@ -726,9 +852,103 @@ export function MasterPage() {
setSlaEscalations([]); setSlaEscalations([]);
}; };
const handleSaveTemplate = () => { // Email Template Handlers
toast.success('Email template saved successfully!'); const handlePreviewTemplate = async () => {
setShowTemplateDialog(false); try {
setPreviewLoading(true);
setPreviewContent(null);
let parsedData = {};
try {
parsedData = JSON.parse(testDataInput);
} catch (e) {
toast.error("Please enter valid JSON for test data");
setPreviewLoading(false);
return;
}
const response = await masterService.previewEmailTemplate({
subject: editingTemplate?.subject,
body: editingTemplate?.body,
data: parsedData
}) as any;
if (response.success && response.data) {
setPreviewContent(response.data);
} else {
toast.error(response.message || "Could not generate preview");
}
} catch (error) {
console.error('Preview error:', error);
toast.error("Failed to generate preview");
} finally {
setPreviewLoading(false);
}
};
const handleSaveTemplate = async () => {
try {
if (!editingTemplate?.name || !editingTemplate?.templateCode || !editingTemplate?.subject || !editingTemplate?.body) {
toast.error("Please fill in all required fields (Name, Code, Subject, Body)");
return;
}
const templateData = {
...editingTemplate,
isActive: editingTemplate.isActive ?? true // Default to true if undefined
};
if (editingTemplate.id) {
// Update
const response = await masterService.updateEmailTemplate(editingTemplate.id, templateData) as any;
if (response.success) {
toast.success("Template updated successfully");
setShowTemplateDialog(false);
// Refresh list
const listRes = await masterService.getEmailTemplates() as any;
if (listRes.success) setEmailTemplates(listRes.data);
} else {
toast.error(response.message || "Failed to update template");
}
} else {
// Create
const response = await masterService.createEmailTemplate(templateData) as any;
if (response.success) {
toast.success("Template created successfully");
setShowTemplateDialog(false);
const listRes = await masterService.getEmailTemplates() as any;
if (listRes.success) setEmailTemplates(listRes.data);
} else {
toast.error(response.message || "Failed to create template");
}
}
} catch (error) {
console.error('Save template error:', error);
toast.error("Failed to save template");
}
};
const handleEditTemplate = (template: EmailTemplate) => {
setEditingTemplate(template);
setShowTemplateDialog(true);
setPreviewContent(null);
setTestDataInput('{}');
};
const handleDeleteTemplate = async (id: string) => {
if (!confirm('Are you sure you want to delete this template?')) return;
try {
const response = await masterService.deleteEmailTemplate(id);
if (response.success) {
toast.success("Template deleted");
const listRes = await masterService.getEmailTemplates();
if (listRes.success) setEmailTemplates(listRes.data);
} else {
toast.error(response.message || "Failed to delete template");
}
} catch (error) {
toast.error("Failed to delete template");
}
}; };
const handleSaveLocation = async () => { const handleSaveLocation = async () => {
@ -1942,56 +2162,100 @@ export function MasterPage() {
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-amber-600" /> <Mail className="w-4 h-4 text-amber-600" />
<span>{template.name}</span> <span>{template.name || template.templateCode}</span>
</div> </div>
</TableCell> </TableCell>
<TableCell className="text-slate-600">{template.subject}</TableCell> <TableCell className="text-slate-600 max-w-xs truncate">{template.subject}</TableCell>
<TableCell> <TableCell>
<Badge variant="outline">{template.trigger}</Badge> <Badge variant="outline">{template.templateCode || '-'}</Badge>
</TableCell>
<TableCell className="text-slate-600">
{template.updatedAt ? new Date(template.updatedAt).toLocaleDateString() : '-'}
</TableCell> </TableCell>
<TableCell className="text-slate-600">{template.lastModified}</TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex gap-2 justify-end"> <div className="flex gap-2 justify-end">
<Button variant="outline" size="sm" onClick={() => setShowTemplateDialog(true)}> <Button variant="outline" size="sm" onClick={() => handleEditTemplate(template)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm"> <Button variant="outline" size="sm" className="text-red-600 hover:bg-red-50" onClick={() => handleDeleteTemplate(template.id)}>
<FileText className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
))} ))}
{emailTemplates.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-slate-500">
No templates found. Create one to get started.
</TableCell>
</TableRow>
)}
</TableBody> </TableBody>
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
{/* Template Variables Card */} {/* Template Scenarios Reference */}
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Available Variables</CardTitle> <CardTitle>Template Reference Guide</CardTitle>
<CardDescription>Use these variables in your templates</CardDescription> <CardDescription>
Standard codes and available variables for system scenarios.
Use these codes to ensure the system sends the correct email.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="grid grid-cols-3 gap-3"> <div className="space-y-6">
{[ {TEMPLATE_SCENARIOS.map((category) => (
'{{applicant_name}}', <div key={category.category} className="space-y-3">
'{{application_id}}', <h3 className="font-semibold text-slate-900 flex items-center gap-2">
'{{location}}', <Badge variant="outline" className="bg-slate-100">{category.category}</Badge>
'{{interview_date}}', </h3>
'{{interview_time}}', <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
'{{reviewer_name}}', {category.scenarios.map((scenario) => (
'{{status}}', <div key={scenario.code} className="border rounded-lg p-3 hover:bg-slate-50 transition-colors">
'{{reason}}', <div className="flex justify-between items-start mb-2">
'{{payment_amount}}', <div>
'{{due_date}}', <h4 className="font-medium text-sm text-slate-800">{scenario.name}</h4>
'{{company_name}}', <div className="flex items-center gap-2 mt-1">
'{{support_email}}' <code className="text-xs bg-slate-100 px-1.5 py-0.5 rounded text-amber-700 font-mono">
].map((variable) => ( {scenario.code}
<Badge key={variable} variant="outline" className="justify-center py-2"> </code>
{variable} <Button
</Badge> variant="ghost"
size="sm"
className="h-5 w-5 p-0"
onClick={() => {
navigator.clipboard.writeText(scenario.code);
toast.success("Code copied!");
}}
>
<Copy className="w-3 h-3 text-slate-400" />
</Button>
</div>
</div>
</div>
<p className="text-xs text-slate-500 mb-2">{scenario.description}</p>
<div className="flex flex-wrap gap-1.5">
{scenario.variables.map((v) => (
<Badge
key={v}
variant="secondary"
className="text-[10px] cursor-pointer hover:bg-slate-200"
onClick={() => {
navigator.clipboard.writeText(v);
toast.success("Variable copied!");
}}
>
{v}
</Badge>
))}
</div>
</div>
))}
</div>
</div>
))} ))}
</div> </div>
</CardContent> </CardContent>
@ -2456,59 +2720,151 @@ export function MasterPage() {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Add/Edit Template Dialog */}
<Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}> <Dialog open={showTemplateDialog} onOpenChange={setShowTemplateDialog}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto"> <DialogContent className="sm:max-w-7xl w-full max-h-[95vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>Email Template</DialogTitle> <DialogTitle>{editingTemplate?.id ? 'Edit Email Template' : 'Add Email Template'}</DialogTitle>
<DialogDescription>Configure automated email template</DialogDescription> <DialogDescription>Configure automated email template with dynamic content</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4">
<div> {!editingTemplate?.id && (
<Label>Template Name</Label> <div className="mb-6 p-4 bg-slate-50 border rounded-lg">
<Input placeholder="e.g., Application Received" className="mt-2" /> <Label className="mb-2 block text-xs font-semibold text-slate-500 uppercase tracking-wider">Quick Start from Scenario</Label>
</div> <Select onValueChange={(code) => {
<div> const scenario = TEMPLATE_SCENARIOS.flatMap(c => c.scenarios).find(s => s.code === code);
<Label>Email Subject</Label> if (scenario) {
<Input placeholder="Subject line for the email" className="mt-2" /> setEditingTemplate({
</div> ...editingTemplate!,
<div> name: scenario.name,
<Label>Trigger Event</Label> templateCode: scenario.code,
<Select> subject: scenario.sampleSubject,
<SelectTrigger className="mt-2"> body: scenario.sampleBody
<SelectValue placeholder="Select trigger" /> });
// Also set sample test data
const sampleJson = scenario.variables.reduce((acc, v) => {
const key = v.replace(/{{|}}/g, '');
acc[key] = `Test ${key}`;
return acc;
}, {} as any);
setTestDataInput(JSON.stringify(sampleJson, null, 2));
toast.success(`Loaded template for ${scenario.name}`);
}
}}>
<SelectTrigger>
<SelectValue placeholder="Select a system scenario..." />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="submission">On Application Submission</SelectItem> {TEMPLATE_SCENARIOS.map((category) => (
<SelectItem value="approval">On Approval</SelectItem> <div key={category.category}>
<SelectItem value="rejection">On Rejection</SelectItem> <SelectItem value={category.category} disabled className="font-semibold opacity-100 bg-slate-50">
<SelectItem value="interview">Interview Scheduled</SelectItem> {category.category}
<SelectItem value="document">Document Request</SelectItem> </SelectItem>
<SelectItem value="payment">Payment Required</SelectItem> {category.scenarios.map((scenario) => (
<SelectItem key={scenario.code} value={scenario.code} className="pl-6">
{scenario.name} ({scenario.code})
</SelectItem>
))}
</div>
))}
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div> )}
<Label>Email Body</Label>
<Textarea <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
placeholder="Use variables like {{applicant_name}}, {{application_id}}, etc." <div className="space-y-4">
className="mt-2" <div>
rows={8} <Label>Template Name</Label>
/> <Input
placeholder="e.g., Application Received"
className="mt-2"
value={editingTemplate?.name || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, name: e.target.value })}
/>
</div>
<div>
<Label>Template Code (Unique)</Label>
<Input
placeholder="e.g., APPLICATION_RECEIVED"
className="mt-2"
value={editingTemplate?.templateCode || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, templateCode: e.target.value })}
/>
</div>
<div>
<Label>Email Subject</Label>
<Input
placeholder="Subject line with {{variables}}"
className="mt-2"
value={editingTemplate?.subject || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, subject: e.target.value })}
/>
</div>
<div>
<Label>Email Body (Handlebars supported)</Label>
<Textarea
placeholder="Hello {{applicant_name}}, ..."
className="mt-2 font-mono text-sm"
rows={12}
value={editingTemplate?.body || ''}
onChange={(e) => setEditingTemplate({ ...editingTemplate!, body: e.target.value })}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="active"
checked={editingTemplate?.isActive ?? true}
onCheckedChange={(checked) => setEditingTemplate({ ...editingTemplate!, isActive: checked })}
/>
<Label htmlFor="active">Active template</Label>
</div>
</div> </div>
<div className="flex items-center space-x-2">
<Switch id="active" defaultChecked /> {/* Preview Section */}
<Label htmlFor="active">Active template</Label> <div className="space-y-4 lg:border-l lg:pl-6">
</div> <h3 className="font-semibold text-slate-900">Preview & Test</h3>
<div className="flex gap-3 pt-4"> <div>
<Button variant="outline" className="flex-1" onClick={() => setShowTemplateDialog(false)}> <Label>Test Data (JSON)</Label>
Cancel <Textarea
</Button> placeholder='{"applicant_name": "John Doe"}'
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleSaveTemplate}> className="mt-2 font-mono text-xs"
Save Template rows={6}
value={testDataInput}
onChange={(e) => setTestDataInput(e.target.value)}
/>
</div>
<Button
variant="secondary"
className="w-full"
onClick={handlePreviewTemplate}
disabled={previewLoading}
>
{previewLoading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Play className="w-4 h-4 mr-2" />}
Generate Preview
</Button> </Button>
{previewContent && (
<div className="mt-4 border rounded-lg overflow-hidden flex flex-col max-h-[500px]">
<div className="bg-slate-100 p-2 border-b text-xs font-mono text-slate-500 shrink-0">
Subject: {previewContent.subject}
</div>
<div className="p-4 bg-white prose prose-sm max-w-none overflow-auto flex-1">
<div dangerouslySetInnerHTML={{ __html: previewContent.html }} />
</div>
</div>
)}
</div> </div>
</div> </div>
<div className="flex gap-3 pt-4 border-t mt-4">
<Button variant="outline" className="flex-1" onClick={() => setShowTemplateDialog(false)}>
Cancel
</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleSaveTemplate}>
Save Template
</Button>
</div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -69,5 +69,31 @@ export const masterService = {
updateUser: async (id: string, data: any) => { updateUser: async (id: string, data: any) => {
const response = await API.updateUser(id, data); const response = await API.updateUser(id, data);
return response.data; return response.data;
},
// Email Templates
getEmailTemplates: async () => {
const response = await API.getEmailTemplates();
return response.data;
},
getEmailTemplate: async (id: string) => {
const response = await API.getEmailTemplate(id);
return response.data;
},
createEmailTemplate: async (data: any) => {
const response = await API.createEmailTemplate(data);
return response.data;
},
updateEmailTemplate: async (id: string, data: any) => {
const response = await API.updateEmailTemplate(id, data);
return response.data;
},
deleteEmailTemplate: async (id: string) => {
const response = await API.deleteEmailTemplate(id);
return response.data;
},
previewEmailTemplate: async (data: any) => {
const response = await API.previewEmailTemplate(data);
return response.data;
} }
}; };

View File

@ -67,7 +67,8 @@ export const onboardingService = {
getInterviews: async (applicationId: string) => { getInterviews: async (applicationId: string) => {
try { try {
const response: any = await API.getInterviews(applicationId); const response: any = await API.getInterviews(applicationId);
return response.data?.data || response.data; const data = response.data?.data || response.data;
return Array.isArray(data) ? data : [];
} catch (error) { } catch (error) {
console.error('Get interviews error:', error); console.error('Get interviews error:', error);
throw error; throw error;
@ -116,5 +117,31 @@ export const onboardingService = {
console.error('Submit Level 2 Feedback error:', error); console.error('Submit Level 2 Feedback error:', error);
throw error; throw error;
} }
},
updateRecommendation: async (data: any) => {
try {
const response: any = await API.updateRecommendation(data);
if (response.ok) {
return response.data;
} else {
throw new Error(response.problem || 'Failed to update recommendation');
}
} catch (error) {
console.error('Update recommendation error:', error);
throw error;
}
},
updateInterviewDecision: async (data: any) => {
try {
const response: any = await API.updateInterviewDecision(data);
if (response.ok) {
return response.data;
} else {
throw new Error(response.problem || 'Failed to update interview decision');
}
} catch (error) {
console.error('Update interview decision error:', error);
throw error;
}
} }
}; };