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),
|
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
@ -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') {
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
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);
|
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,58 +2162,102 @@ 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
|
||||||
|
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>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
@ -2456,51 +2720,144 @@ 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 className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>Email Body</Label>
|
<Label>Template Name</Label>
|
||||||
<Textarea
|
<Input
|
||||||
placeholder="Use variables like {{applicant_name}}, {{application_id}}, etc."
|
placeholder="e.g., Application Received"
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
rows={8}
|
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>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch id="active" defaultChecked />
|
<Switch
|
||||||
|
id="active"
|
||||||
|
checked={editingTemplate?.isActive ?? true}
|
||||||
|
onCheckedChange={(checked) => setEditingTemplate({ ...editingTemplate!, isActive: checked })}
|
||||||
|
/>
|
||||||
<Label htmlFor="active">Active template</Label>
|
<Label htmlFor="active">Active template</Label>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-3 pt-4">
|
</div>
|
||||||
|
|
||||||
|
{/* 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)}>
|
<Button variant="outline" className="flex-1" onClick={() => setShowTemplateDialog(false)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
@ -2508,7 +2865,6 @@ export function MasterPage() {
|
|||||||
Save Template
|
Save Template
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user