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),
submitLevel2Feedback: (data: any) => client.post('/assessment/level2-feedback', data),
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
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),
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
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
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
assignedTo: app.assignedTo, // Add this field for filtering
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
companyName: app.companyName,
source: app.source,
@ -119,13 +119,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter;
const matchesStatus = statusFilter === 'all' || app.status === statusFilter;
const isShortlisted = app.isShortlisted === true; // Only show shortlisted applications
const notExcluded = !excludedApplicationIds.includes(app.id); // Exclude APP-005, 006, 007, 008
const isShortlisted = app.isShortlisted === true;
const isNotQuestionnaireStage = !['Questionnaire Pending', 'Questionnaire Completed', 'Submitted'].includes(app.status);
const notExcluded = !excludedApplicationIds.includes(app.id);
// New Filter: My Assignments
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) => {
if (sortBy === 'date') {

View File

@ -30,7 +30,11 @@ import {
UserCog,
Bell,
AlertTriangle,
X
X,
Loader2,
Play,
Copy,
Check
} from 'lucide-react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
@ -140,19 +144,137 @@ interface SLAConfig {
interface EmailTemplate {
id: string;
name: string;
templateCode?: string;
subject: string;
body?: 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 {
id: string;
state: string;
city: string;
district: string;
activeFrom: string;
activeTo: string;
status: 'Active' | 'Inactive';
state?: string | { stateName: string; id: string };
city?: string;
areaName: string;
district?: string | { districtName: string; state?: { stateName: string; id: string }; id: string };
stateId?: string; // Sometimes flattened
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 {
@ -241,6 +363,7 @@ interface UserAssignment {
employeeId?: string;
status: 'Active' | 'Inactive';
permissions?: string[];
asmCode?: string; // Added for ASM role tracking
}
export function MasterPage() {
@ -363,7 +486,7 @@ export function MasterPage() {
setLoading(true);
try {
// 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.getZones().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.getStates().catch(e => ({ success: false, error: e })),
// 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
@ -523,6 +647,10 @@ export function MasterPage() {
})));
}
if (emailTemplatesRes && emailTemplatesRes.success) {
setEmailTemplates(emailTemplatesRes.data);
}
} catch (error) {
console.error('Error fetching master data:', error);
toast.error('Failed to load configuration data');
@ -644,16 +772,14 @@ export function MasterPage() {
},
]);
// Mock data for email templates
const [emailTemplates] = useState<EmailTemplate[]>([
{ id: '1', name: 'Application Received', subject: 'Your Royal Enfield Dealership Application', trigger: 'On submission', lastModified: 'Dec 20, 2024' },
{ id: '2', name: 'Interview Scheduled', subject: 'Interview Scheduled - Royal Enfield Dealership', trigger: 'When interview scheduled', lastModified: 'Dec 18, 2024' },
{ id: '3', name: 'Application Approved', subject: 'Congratulations! Your Application is Approved', trigger: 'On approval', lastModified: 'Dec 15, 2024' },
{ id: '4', name: 'Application Rejected', subject: 'Update on Your Dealership Application', trigger: 'On rejection', lastModified: 'Dec 15, 2024' },
{ 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' },
{ id: '7', name: 'SLA Breach Warning', subject: 'Action Required - Application Pending', trigger: 'Before SLA breach', lastModified: 'Dec 08, 2024' },
]);
// Email Template State
const [emailTemplates, setEmailTemplates] = useState<EmailTemplate[]>([]);
const [testDataInput, setTestDataInput] = useState<string>('{}');
const [previewContent, setPreviewContent] = useState<{ subject: string, html: string } | null>(null);
const [previewLoading, setPreviewLoading] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<EmailTemplate | null>(null);
// ... (keep existing handleSaveRole)
@ -726,9 +852,103 @@ export function MasterPage() {
setSlaEscalations([]);
};
const handleSaveTemplate = () => {
toast.success('Email template saved successfully!');
setShowTemplateDialog(false);
// Email Template Handlers
const handlePreviewTemplate = async () => {
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 () => {
@ -1942,56 +2162,100 @@ export function MasterPage() {
<TableCell>
<div className="flex items-center gap-2">
<Mail className="w-4 h-4 text-amber-600" />
<span>{template.name}</span>
<span>{template.name || template.templateCode}</span>
</div>
</TableCell>
<TableCell className="text-slate-600">{template.subject}</TableCell>
<TableCell className="text-slate-600 max-w-xs truncate">{template.subject}</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 className="text-slate-600">{template.lastModified}</TableCell>
<TableCell className="text-right">
<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" />
</Button>
<Button variant="outline" size="sm">
<FileText className="w-4 h-4" />
<Button variant="outline" size="sm" className="text-red-600 hover:bg-red-50" onClick={() => handleDeleteTemplate(template.id)}>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</TableCell>
</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>
</Table>
</CardContent>
</Card>
{/* Template Variables Card */}
{/* Template Scenarios Reference */}
<Card>
<CardHeader>
<CardTitle>Available Variables</CardTitle>
<CardDescription>Use these variables in your templates</CardDescription>
<CardTitle>Template Reference Guide</CardTitle>
<CardDescription>
Standard codes and available variables for system scenarios.
Use these codes to ensure the system sends the correct email.
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 gap-3">
{[
'{{applicant_name}}',
'{{application_id}}',
'{{location}}',
'{{interview_date}}',
'{{interview_time}}',
'{{reviewer_name}}',
'{{status}}',
'{{reason}}',
'{{payment_amount}}',
'{{due_date}}',
'{{company_name}}',
'{{support_email}}'
].map((variable) => (
<Badge key={variable} variant="outline" className="justify-center py-2">
{variable}
</Badge>
<div className="space-y-6">
{TEMPLATE_SCENARIOS.map((category) => (
<div key={category.category} className="space-y-3">
<h3 className="font-semibold text-slate-900 flex items-center gap-2">
<Badge variant="outline" className="bg-slate-100">{category.category}</Badge>
</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{category.scenarios.map((scenario) => (
<div key={scenario.code} className="border rounded-lg p-3 hover:bg-slate-50 transition-colors">
<div className="flex justify-between items-start mb-2">
<div>
<h4 className="font-medium text-sm text-slate-800">{scenario.name}</h4>
<div className="flex items-center gap-2 mt-1">
<code className="text-xs bg-slate-100 px-1.5 py-0.5 rounded text-amber-700 font-mono">
{scenario.code}
</code>
<Button
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>
</CardContent>
@ -2456,59 +2720,151 @@ export function MasterPage() {
</DialogContent>
</Dialog>
{/* Add/Edit Template Dialog */}
<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>
<DialogTitle>Email Template</DialogTitle>
<DialogDescription>Configure automated email template</DialogDescription>
<DialogTitle>{editingTemplate?.id ? 'Edit Email Template' : 'Add Email Template'}</DialogTitle>
<DialogDescription>Configure automated email template with dynamic content</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Template Name</Label>
<Input placeholder="e.g., Application Received" className="mt-2" />
</div>
<div>
<Label>Email Subject</Label>
<Input placeholder="Subject line for the email" className="mt-2" />
</div>
<div>
<Label>Trigger Event</Label>
<Select>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select trigger" />
{!editingTemplate?.id && (
<div className="mb-6 p-4 bg-slate-50 border rounded-lg">
<Label className="mb-2 block text-xs font-semibold text-slate-500 uppercase tracking-wider">Quick Start from Scenario</Label>
<Select onValueChange={(code) => {
const scenario = TEMPLATE_SCENARIOS.flatMap(c => c.scenarios).find(s => s.code === code);
if (scenario) {
setEditingTemplate({
...editingTemplate!,
name: scenario.name,
templateCode: scenario.code,
subject: scenario.sampleSubject,
body: scenario.sampleBody
});
// 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>
<SelectContent>
<SelectItem value="submission">On Application Submission</SelectItem>
<SelectItem value="approval">On Approval</SelectItem>
<SelectItem value="rejection">On Rejection</SelectItem>
<SelectItem value="interview">Interview Scheduled</SelectItem>
<SelectItem value="document">Document Request</SelectItem>
<SelectItem value="payment">Payment Required</SelectItem>
{TEMPLATE_SCENARIOS.map((category) => (
<div key={category.category}>
<SelectItem value={category.category} disabled className="font-semibold opacity-100 bg-slate-50">
{category.category}
</SelectItem>
{category.scenarios.map((scenario) => (
<SelectItem key={scenario.code} value={scenario.code} className="pl-6">
{scenario.name} ({scenario.code})
</SelectItem>
))}
</div>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Email Body</Label>
<Textarea
placeholder="Use variables like {{applicant_name}}, {{application_id}}, etc."
className="mt-2"
rows={8}
/>
)}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
<div className="space-y-4">
<div>
<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 className="flex items-center space-x-2">
<Switch id="active" defaultChecked />
<Label htmlFor="active">Active template</Label>
</div>
<div className="flex gap-3 pt-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
{/* Preview Section */}
<div className="space-y-4 lg:border-l lg:pl-6">
<h3 className="font-semibold text-slate-900">Preview & Test</h3>
<div>
<Label>Test Data (JSON)</Label>
<Textarea
placeholder='{"applicant_name": "John Doe"}'
className="mt-2 font-mono text-xs"
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>
{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 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>
</Dialog>

View File

@ -69,5 +69,31 @@ export const masterService = {
updateUser: async (id: string, data: any) => {
const response = await API.updateUser(id, 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) => {
try {
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) {
console.error('Get interviews error:', error);
throw error;
@ -116,5 +117,31 @@ export const onboardingService = {
console.error('Submit Level 2 Feedback error:', 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;
}
}
};