added joint approval in resignation for RBM DD-ZM approval stage

This commit is contained in:
laxman h 2026-04-29 16:37:06 +05:30
parent 352c656a9e
commit 95032cf2a7
5 changed files with 187 additions and 74 deletions

View File

@ -65,6 +65,20 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
} }
}; };
const isEmailValid = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const isMobileValid = (mobile: string) => /^[0-9]{10}$/.test(mobile);
const isPincodeValid = (pincode: string) => /^[0-9]{6}$/.test(pincode);
const isFormValid = Boolean(
formData.country && formData.stateId && formData.districtId && formData.name &&
formData.interestedCity && formData.email && formData.pincode && formData.mobile &&
formData.ownRoyalEnfield && formData.age && formData.education &&
formData.companyName && formData.source && formData.existingDealer &&
formData.description && formData.address && formData.acceptTerms &&
otpVerified && isEmailValid(formData.email) && isMobileValid(formData.mobile) && isPincodeValid(formData.pincode) &&
(formData.ownRoyalEnfield === 'no' || (formData.ownRoyalEnfield === 'yes' && formData.royalEnfieldModel))
);
const handleVerifyMobile = () => { const handleVerifyMobile = () => {
if (!formData.mobile || formData.mobile.length < 10) { if (!formData.mobile || formData.mobile.length < 10) {
toast.error('Please enter a valid mobile number'); toast.error('Please enter a valid mobile number');
@ -93,6 +107,11 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
return; return;
} }
if (formData.ownRoyalEnfield === 'yes' && !formData.royalEnfieldModel) {
toast.error('Please select your motorcycle model');
return;
}
if (!formData.acceptTerms) { if (!formData.acceptTerms) {
toast.error('Please accept the terms and conditions'); toast.error('Please accept the terms and conditions');
return; return;
@ -145,10 +164,10 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
}; };
const reModels = [ const reModels = [
"Classic 650", "Scram 440", "Goan Classic 350", "Bear 650", "Guerrilla 450", "Continental GT", "Interceptor 650", "Himalayan", "Classic 350",
"Shotgun 650", "Himalayan 450", "Bullet 350", "Super Meteor 650", "Hunter 350", "Classic 500", "Thunderbird 350", "Thunderbird 500", "Thunderbird X 350",
"Scram 411", "Meteor 350", "Interceptor INT 650", "Continental GT 650", "Thunderbird X 500", "Bullet 350", "Bullet 500", "Bullet ES",
"Classic 350", "Other Royal Enfield motorcycle" "Bullet Trials 350", "Bullet Trials 500", "Other Royal Enfield motorcycle"
]; ];
const sourceOptions = [ const sourceOptions = [
@ -257,24 +276,35 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onChange={(e) => setFormData({...formData, interestedCity: e.target.value})} onChange={(e) => setFormData({...formData, interestedCity: e.target.value})}
/> />
<Input <Input
type="email"
placeholder="Email Id*" placeholder="Email Id*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})} onChange={(e) => setFormData({...formData, email: e.target.value})}
/> />
<Input <Input
type="text"
maxLength={6}
placeholder="Pincode*" placeholder="Pincode*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.pincode} value={formData.pincode}
onChange={(e) => setFormData({...formData, pincode: e.target.value})} onChange={(e) => {
const val = e.target.value.replace(/\D/g, '');
setFormData({...formData, pincode: val});
}}
/> />
<div className="relative"> <div className="relative">
<Input <Input
type="text"
maxLength={10}
placeholder="Mobile No.*" placeholder="Mobile No.*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.mobile} value={formData.mobile}
onChange={(e) => setFormData({...formData, mobile: e.target.value})} onChange={(e) => {
const val = e.target.value.replace(/\D/g, '');
setFormData({...formData, mobile: val});
}}
/> />
{!otpVerified ? ( {!otpVerified ? (
<button <button
@ -301,7 +331,13 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
type="radio" type="radio"
className="hidden" className="hidden"
checked={formData.ownRoyalEnfield === val} checked={formData.ownRoyalEnfield === val}
onChange={() => setFormData({...formData, ownRoyalEnfield: val})} onChange={() => {
setFormData({
...formData,
ownRoyalEnfield: val,
royalEnfieldModel: val === 'no' ? '' : formData.royalEnfieldModel
});
}}
/> />
<span className="text-[14px] capitalize">{val}</span> <span className="text-[14px] capitalize">{val}</span>
</label> </label>
@ -316,18 +352,19 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onChange={(e) => setFormData({...formData, age: e.target.value})} onChange={(e) => setFormData({...formData, age: e.target.value})}
/> />
<div className="relative"> {formData.ownRoyalEnfield === 'yes' && (
<select <div className="relative">
className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none disabled:bg-slate-50" <select
value={formData.royalEnfieldModel} className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none"
disabled={formData.ownRoyalEnfield !== 'yes'} value={formData.royalEnfieldModel}
onChange={(e) => setFormData({...formData, royalEnfieldModel: e.target.value})} onChange={(e) => setFormData({...formData, royalEnfieldModel: e.target.value})}
> >
<option value="">Motorcycle Owned</option> <option value="">Select Motorcycle*</option>
{reModels.map(m => <option key={m} value={m}>{m}</option>)} {reModels.map(m => <option key={m} value={m}>{m}</option>)}
</select> </select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" /> <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
</div> </div>
)}
<Input <Input
placeholder="Education Qualification*" placeholder="Education Qualification*"
@ -406,14 +443,15 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onCheckedChange={(checked) => setFormData({...formData, acceptTerms: checked as boolean})} onCheckedChange={(checked) => setFormData({...formData, acceptTerms: checked as boolean})}
/> />
<label htmlFor="terms" className="text-[14px] font-medium cursor-pointer"> <label htmlFor="terms" className="text-[14px] font-medium cursor-pointer">
I accept the <b>terms and conditions</b> as well as <b>privacy policy</b>. I accept the <b>terms and conditions</b> as well as <b>privacy policy</b>.<span className="text-red-500">*</span>
</label> </label>
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="h-12 px-10 bg-black text-white flex items-center gap-3 hover:bg-slate-900 transition-colors" disabled={!isFormValid}
className="h-12 px-10 bg-black text-white flex items-center gap-3 hover:bg-slate-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<span className="font-bold uppercase tracking-wider text-[14px]">Submit</span> <span className="font-bold uppercase tracking-wider text-[14px]">Submit</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />

View File

@ -489,7 +489,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
</button> </button>
</div> </div>
<p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}> <p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}>
{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : branchStage.status === 'active' ? 'Evaluating' : 'Pending'} {isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'}
</p> </p>
</div> </div>
</> </>
@ -558,14 +558,14 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<TableCell> <TableCell>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button size="sm" variant="outline" data-testid={`onboarding-document-preview-${idx}`} onClick={() => { <Button size="sm" variant="outline" data-testid={`onboarding-document-preview-${idx}`} onClick={() => {
setPreviewDoc(doc); setPreviewDoc(doc);
setShowPreviewModal(true); setShowPreviewModal(true);
}}> }}>
<Eye className="w-3 h-3 text-slate-500" /> <Eye className="w-3 h-3 text-slate-500" />
</Button> </Button>
<Button size="sm" variant="outline" data-testid={`onboarding-document-download-${idx}`} onClick={() => { <Button size="sm" variant="outline" data-testid={`onboarding-document-download-${idx}`} onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
window.open(`${baseUrl}/${doc.filePath}`, '_blank'); window.open(`${baseUrl}/${doc.filePath}`, '_blank');
}}> }}>
<Download className="w-3 h-3 text-slate-500" /> <Download className="w-3 h-3 text-slate-500" />
</Button> </Button>

View File

@ -62,9 +62,9 @@ export function useApplicationDetailsStageData({
date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation', date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === 1 ||
p.metadata?.interviewLevel === '1' || p.metadata?.interviewLevel === '1' ||
p.metadata?.allAssignments?.includes(1) || p.metadata?.allAssignments?.includes(1) ||
p.metadata?.allAssignments?.includes('1') || p.metadata?.allAssignments?.includes('1') ||
hasAnyRole(p, ['DD-ZM', 'RBM']) hasAnyRole(p, ['DD-ZM', 'RBM'])
@ -78,9 +78,9 @@ export function useApplicationDetailsStageData({
date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation', date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 2 || p.metadata?.interviewLevel === 2 ||
p.metadata?.interviewLevel === '2' || p.metadata?.interviewLevel === '2' ||
p.metadata?.allAssignments?.includes(2) || p.metadata?.allAssignments?.includes(2) ||
p.metadata?.allAssignments?.includes('2') || p.metadata?.allAssignments?.includes('2') ||
hasAnyRole(p, ['DD Lead', 'ZBH']) hasAnyRole(p, ['DD Lead', 'ZBH'])
@ -94,9 +94,9 @@ export function useApplicationDetailsStageData({
date: application.level3InterviewDate, description: 'NBH + DD Head evaluation', date: application.level3InterviewDate, description: 'NBH + DD Head evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 3 || p.metadata?.interviewLevel === 3 ||
p.metadata?.interviewLevel === '3' || p.metadata?.interviewLevel === '3' ||
p.metadata?.allAssignments?.includes(3) || p.metadata?.allAssignments?.includes(3) ||
p.metadata?.allAssignments?.includes('3') || p.metadata?.allAssignments?.includes('3') ||
hasAnyRole(p, ['NBH', 'DD Head']) hasAnyRole(p, ['NBH', 'DD Head'])
@ -120,15 +120,16 @@ export function useApplicationDetailsStageData({
id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue'), id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue'),
date: application.loiIssueDate, description: 'Letter of Intent issued', isParallel: true, date: application.loiIssueDate, description: 'Letter of Intent issued', isParallel: true,
branches: [ branches: [
{ name: 'LOI Documents', color: 'blue', stages: {
documentConfigs.some((c: any) => c.stageCode === 'LOI Issue') name: 'LOI Documents', color: 'green', stages:
? documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue').map((c: any, i: number) => ({ documentConfigs.some((c: any) => c.stageCode === 'LOI Issue')
? documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue').map((c: any, i: number) => ({
id: `10a-${i}`, id: `10a-${i}`,
name: c.documentType, name: c.documentType,
status: isDocumentUploaded(c.documentType) ? 'completed' : 'active', status: isDocumentUploaded(c.documentType) ? 'completed' : 'active',
description: c.isMandatory ? `Upload ${c.documentType} (Mandatory)` : `Upload ${c.documentType}` description: c.isMandatory ? `Upload ${c.documentType} (Mandatory)` : `Upload ${c.documentType}`
})) }))
: [ : [
{ id: '10a-1', name: 'Letter of Intent', status: isDocumentUploaded('Letter of Intent') || isDocumentUploaded('LOI') ? 'completed' : 'active', description: 'Letter of Intent document' }, { id: '10a-1', name: 'Letter of Intent', status: isDocumentUploaded('Letter of Intent') || isDocumentUploaded('LOI') ? 'completed' : 'active', description: 'Letter of Intent document' },
{ id: '10a-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' }, { id: '10a-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' },
] ]
@ -139,24 +140,28 @@ export function useApplicationDetailsStageData({
id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation'), id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation'),
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true, date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
branches: [ branches: [
{ name: 'Architectural Work', color: 'green', stages: [ {
{ id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' }, name: 'Architectural Work', color: 'green', stages: [
{ id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' }, { id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
{ id: '11a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' }, { id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
]}, { id: '11a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' },
{ name: 'Statutory Documents', color: 'green', stages: [ ]
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' }, },
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' }, {
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' }, name: 'Statutory Documents', color: 'green', stages: [
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' }, { id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' }, { id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' }, { id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' }, { id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' }, { id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' }, { id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' }, { id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }, { id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
]}, { id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
]
},
] ]
}, },
{ {

View File

@ -45,7 +45,7 @@ const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WI
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = { const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'], 'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'],
'RBM': ['RBM', 'RBM Review', 'Regional Review'], 'RBM': ['RBM', 'RBM Review', 'Regional Review', 'RBM + DD-ZM Review'],
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'], 'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'], 'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
'NBH': ['NBH', 'NBH Approval', 'NBH Review'], 'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
@ -128,7 +128,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// Progress stages logic based on live data // Progress stages logic based on live data
const progressStages = [ const progressStages = [
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' }, { id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
{ id: 2, name: 'RBM Review', key: 'RBM', description: 'Regional Business Manager evaluation' }, { id: 2, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' }, { id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' }, { id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
@ -203,10 +203,11 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}; };
const permissions = getResignationPermissions(); const permissions = getResignationPermissions();
const isNationalLevel = ['Super Admin', 'DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Legal Admin'].includes(currentUser?.role || '');
const stageAliases: Record<string, string[]> = { const stageAliases: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Request Initiated'], 'ASM': ['ASM', 'ASM Review', 'Request Initiated'],
'RBM': ['RBM', 'RBM Review'], 'RBM': ['RBM', 'RBM Review', 'RBM + DD-ZM Review'],
'ZBH': ['ZBH', 'ZBH Review'], 'ZBH': ['ZBH', 'ZBH Review'],
'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'], 'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'],
'NBH': ['NBH', 'NBH Approval', 'NBH Review'], 'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
@ -562,6 +563,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger> <TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger> <TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger> <TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
{isNationalLevel && (
<TabsTrigger value="approvals" className="data-[state=active]:bg-white">Approval Summary</TabsTrigger>
)}
</TabsList> </TabsList>
{/* Details Tab */} {/* Details Tab */}
@ -750,19 +754,27 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
<p className="text-slate-600 text-sm mb-1">{stage.description}</p> <p className="text-slate-600 text-sm mb-1">{stage.description}</p>
{timelineEntry && (
<div className="space-y-2"> {stageTimelineEntries.length > 0 && (
<div className="flex items-center gap-2"> <div className="space-y-4 mt-3">
<Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase"> {stageTimelineEntries.map((entry: any, i: number) => (
{timelineEntry.user || 'System'} <div key={i} className="space-y-2">
</Badge> <div className="flex items-center gap-2">
<span className="text-[10px] text-slate-500 italic"> <Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase">
{timelineEntry.action} {entry.user || 'System'}
</span> </Badge>
</div> <span className="text-[10px] text-slate-500 italic">
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm"> {entry.action}
{timelineEntry.comments || timelineEntry.remarks || 'No remarks provided.'} </span>
</div> <span className="text-[10px] text-slate-400 ml-auto">
{formatDateTime(entry.timestamp || entry.createdAt)}
</span>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm">
{entry.comments || entry.remarks || 'No remarks provided.'}
</div>
</div>
))}
</div> </div>
)} )}
</div> </div>
@ -938,6 +950,64 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
{/* Approval Summary Tab */}
{isNationalLevel && (
<TabsContent value="approvals">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Approval Summary</CardTitle>
<CardDescription>Comprehensive view of all approvals and remarks</CardDescription>
</div>
{permissions.canApprove && (
<Button onClick={() => handleAction('approve')} className="bg-green-600 hover:bg-green-700">
<Check className="w-4 h-4 mr-2" />
Approve Request
</Button>
)}
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Stage</TableHead>
<TableHead>Approver</TableHead>
<TableHead>Action</TableHead>
<TableHead>Remarks</TableHead>
<TableHead>Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(resignationData?.timeline || []).length > 0 ? (
resignationData.timeline.map((entry: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium whitespace-nowrap">{entry.stage}</TableCell>
<TableCell>
<Badge variant="outline">{entry.user || 'System'}</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">{entry.action}</TableCell>
<TableCell className="max-w-[400px]">
{entry.remarks || entry.comments || '-'}
</TableCell>
<TableCell className="text-slate-500 whitespace-nowrap">
{formatDateTime(entry.timestamp || entry.createdAt)}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-6 text-slate-500">
No approval records found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
)}
</Tabs> </Tabs>
{/* Action Dialogs */} {/* Action Dialogs */}

View File

@ -10,7 +10,7 @@
--card-foreground: oklch(0.145 0 0); --card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: #daaa00; --primary: #da291c;
--primary-foreground: oklch(1 0 0); --primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.0058 264.53); --secondary: oklch(0.95 0.0058 264.53);
--secondary-foreground: #030213; --secondary-foreground: #030213;