progress bar started inlining with the stage status and action buttons visibility enhanced
This commit is contained in:
parent
e786ea12cf
commit
3208a1ea7f
@ -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
@ -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') {
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user