Compare commits

..

7 Commits

Author SHA1 Message Date
c23593bb11 contitutional and relocation changes done based on document alignment 2026-05-06 10:45:55 +05:30
b357dbdcbb stage names modified and calendar addd in opportunity requests 2026-05-04 13:26:51 +05:30
2f82699572 notif
ication service enhanced even more detailed way added more templates documentented i splitted based on modulewise joint approval added for resignation flow, upload ppt document with new docment type add for DD Lead user
2026-04-30 18:52:17 +05:30
95032cf2a7 added joint approval in resignation for RBM DD-ZM approval stage 2026-04-29 16:37:06 +05:30
352c656a9e theme color changed to red i multiple places 2026-04-28 20:42:15 +05:30
2a289e433c stage transition bug resolved 2026-04-28 13:48:05 +05:30
f3927b4686 progress track isue for interview level 1 fixed 2026-04-28 08:17:23 +05:30
63 changed files with 1582 additions and 785 deletions

View File

@ -77,11 +77,15 @@ export default function App() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const currentRole = currentUser?.role || currentUser?.roleCode || ''; const currentRole = currentUser?.role || currentUser?.roleCode || '';
const normalizedRole = String(currentRole).trim().toLowerCase(); const hasRole = (roles: string[]) => {
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; const userRole = String(currentUser?.role || '').toLowerCase();
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const userRoleCode = String(currentUser?.roleCode || '').toLowerCase();
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; return normalizedTargetRoles.includes(userRole) || normalizedTargetRoles.includes(userRoleCode);
};
const resignationRoles = ['DD Admin', 'DD_ADMIN', 'ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'LEGAL_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const terminationRoles = ['ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'ZBH', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Legal Admin', 'LEGAL_ADMIN', 'Legal', 'DD Admin', 'DD_ADMIN', 'CCO', 'CEO', 'Super Admin', 'SUPER_ADMIN'];
const fnfRoles = ['DD Admin', 'DD_ADMIN', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Finance', 'Finance Admin', 'FINANCE_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const financeRoles = ['Finance', 'Finance Admin']; const financeRoles = ['Finance', 'Finance Admin'];
useEffect(() => { useEffect(() => {

View File

@ -43,6 +43,7 @@ export const API = {
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params), exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
getApplications: (params?: any) => client.get('/onboarding/applications', params), getApplications: (params?: any) => client.get('/onboarding/applications', params),
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data), shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
sendBulkReminders: (data: { applicationIds: string[] }) => client.post('/onboarding/applications/reminders', data),
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`), getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data), updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data),
getLatestQuestionnaire: () => client.get('/questionnaire/latest'), getLatestQuestionnaire: () => client.get('/questionnaire/latest'),

View File

@ -175,7 +175,7 @@ export function ApprovalPoliciesPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2"> <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Settings2 className="w-6 h-6 text-amber-600" /> <Settings2 className="w-6 h-6 text-re-red" />
Approval Policies Approval Policies
</h1> </h1>
<p className="text-slate-500">Configure stage-level approvers, mode, and minimum approvals.</p> <p className="text-slate-500">Configure stage-level approvers, mode, and minimum approvals.</p>
@ -185,7 +185,7 @@ export function ApprovalPoliciesPage() {
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh Refresh
</Button> </Button>
<Button className="bg-amber-600 hover:bg-amber-700" onClick={openCreateModal}> <Button className="bg-re-red hover:bg-re-red-hover" onClick={openCreateModal}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add New Policy Add New Policy
</Button> </Button>
@ -244,7 +244,7 @@ export function ApprovalPoliciesPage() {
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="text-amber-600 hover:text-amber-700 hover:bg-amber-50 h-8 px-2" className="text-re-red hover:text-re-red-hover hover:bg-red-50 h-8 px-2"
onClick={() => openEditModal(policy)} onClick={() => openEditModal(policy)}
> >
<Edit2 className="w-4 h-4 mr-1.5" /> <Edit2 className="w-4 h-4 mr-1.5" />
@ -264,7 +264,7 @@ export function ApprovalPoliciesPage() {
<DialogContent className="sm:max-w-[480px] overflow-visible"> <DialogContent className="sm:max-w-[480px] overflow-visible">
<DialogHeader className="gap-1 pb-2 border-b"> <DialogHeader className="gap-1 pb-2 border-b">
<DialogTitle className="text-base flex items-center gap-2"> <DialogTitle className="text-base flex items-center gap-2">
{isEditMode ? <Edit2 className="w-4 h-4 text-amber-600" /> : <Plus className="w-4 h-4 text-amber-600" />} {isEditMode ? <Edit2 className="w-4 h-4 text-re-red" /> : <Plus className="w-4 h-4 text-re-red" />}
{isEditMode ? 'Edit Policy' : 'Create New Policy'} {isEditMode ? 'Edit Policy' : 'Create New Policy'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[11px]"> <DialogDescription className="text-[11px]">
@ -307,7 +307,7 @@ export function ApprovalPoliciesPage() {
))} ))}
<SelectGroup> <SelectGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<SelectItem value="CUSTOM" className="text-xs font-semibold text-amber-600 italic"> <SelectItem value="CUSTOM" className="text-xs font-semibold text-re-red italic">
+ Enter Custom Stage Code + Enter Custom Stage Code
</SelectItem> </SelectItem>
</SelectGroup> </SelectGroup>
@ -376,10 +376,10 @@ export function ApprovalPoliciesPage() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between h-8 text-[11px] text-slate-500 font-normal border-slate-200"> <Button variant="outline" size="sm" className="w-full justify-between h-8 text-[11px] text-slate-500 font-normal border-slate-200">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Plus className="w-3 h-3 text-amber-600" /> <Plus className="w-3 h-3 text-re-red" />
<span>Add Roles...</span> <span>Add Roles...</span>
</div> </div>
<Badge variant="secondary" className="bg-amber-50 text-amber-700 border-transparent text-[9px] px-1 h-4"> <Badge variant="secondary" className="bg-red-50 text-re-red-hover border-transparent text-[9px] px-1 h-4">
{draft.requiredRoles.length} {draft.requiredRoles.length}
</Badge> </Badge>
</Button> </Button>
@ -445,7 +445,7 @@ export function ApprovalPoliciesPage() {
<Button variant="outline" size="sm" className="text-xs h-8" onClick={() => setIsModalOpen(false)}> <Button variant="outline" size="sm" className="text-xs h-8" onClick={() => setIsModalOpen(false)}>
Cancel Cancel
</Button> </Button>
<Button className="bg-amber-600 hover:bg-amber-700 h-8 text-xs font-semibold" onClick={savePolicy}> <Button className="bg-re-red hover:bg-re-red-hover h-8 text-xs font-semibold" onClick={savePolicy}>
<Save className="w-3 h-3 mr-1.5" /> <Save className="w-3 h-3 mr-1.5" />
{isEditMode ? 'Save Changes' : 'Create Policy'} {isEditMode ? 'Save Changes' : 'Create Policy'}
</Button> </Button>

View File

@ -204,7 +204,7 @@ const QuestionnaireBuilder: React.FC = () => {
if (fetching) { if (fetching) {
return ( return (
<div className="flex justify-center p-12"> <div className="flex justify-center p-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-re-red"></div>
</div> </div>
); );
} }
@ -230,7 +230,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div> </div>
<div className="flex flex-wrap items-center gap-4 w-full md:w-auto"> <div className="flex flex-wrap items-center gap-4 w-full md:w-auto">
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg border ${totalWeight === 100 ? 'bg-green-50 border-green-200 text-green-700' : 'bg-amber-50 border-amber-200 text-amber-700'}`}> <div className={`flex items-center gap-2 px-3 py-2 rounded-lg border ${totalWeight === 100 ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-re-red-hover'}`}>
{totalWeight !== 100 && <AlertCircle size={16} />} {totalWeight !== 100 && <AlertCircle size={16} />}
<span className="text-sm font-semibold">Total Score: {totalWeight}/100</span> <span className="text-sm font-semibold">Total Score: {totalWeight}/100</span>
</div> </div>
@ -240,13 +240,13 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={version} value={version}
onChange={(e) => setVersion(e.target.value)} onChange={(e) => setVersion(e.target.value)}
className="border border-slate-300 p-2 rounded-lg w-full md:w-48 text-sm focus:ring-2 focus:ring-amber-500 outline-none" className="border border-slate-300 p-2 rounded-lg w-full md:w-48 text-sm focus:ring-2 focus:ring-red-500 outline-none"
placeholder="Version Name (e.g. v2.0)" placeholder="Version Name (e.g. v2.0)"
/> />
<button <button
onClick={handleSave} onClick={handleSave}
disabled={loading} disabled={loading}
className="bg-amber-600 text-white px-6 py-2 rounded-lg flex items-center gap-2 hover:bg-amber-700 disabled:bg-slate-300 disabled:cursor-not-allowed transition shadow-sm font-medium whitespace-nowrap" className="bg-re-red text-white px-6 py-2 rounded-lg flex items-center gap-2 hover:bg-re-red-hover disabled:bg-slate-300 disabled:cursor-not-allowed transition shadow-sm font-medium whitespace-nowrap"
> >
<Save size={18} /> {loading ? 'Saving...' : 'Publish'} <Save size={18} /> {loading ? 'Saving...' : 'Publish'}
</button> </button>
@ -271,7 +271,7 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={q.questionText} value={q.questionText}
onChange={(e) => updateQuestion(index, 'questionText', e.target.value)} onChange={(e) => updateQuestion(index, 'questionText', e.target.value)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none transition-shadow" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none transition-shadow"
placeholder="Enter your question here..." placeholder="Enter your question here..."
/> />
</div> </div>
@ -281,7 +281,7 @@ const QuestionnaireBuilder: React.FC = () => {
<select <select
value={q.sectionName} value={q.sectionName}
onChange={(e) => updateQuestion(index, 'sectionName', e.target.value)} onChange={(e) => updateQuestion(index, 'sectionName', e.target.value)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none bg-white"
> >
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)} {SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
@ -292,7 +292,7 @@ const QuestionnaireBuilder: React.FC = () => {
<select <select
value={q.inputType} value={q.inputType}
onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)} onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none bg-white"
> >
<option value="text">Text Input</option> <option value="text">Text Input</option>
<option value="email">Email Address</option> <option value="email">Email Address</option>
@ -313,17 +313,17 @@ const QuestionnaireBuilder: React.FC = () => {
type="number" type="number"
value={isNaN(q.weight) ? 0 : q.weight} value={isNaN(q.weight) ? 0 : q.weight}
onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value) || 0)} onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value) || 0)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none pl-3 pr-8"
title="Weightage" title="Weightage"
/> />
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-xs font-bold">%</span> <span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-xs font-bold">%</span>
</div> </div>
</div> </div>
{/* <div className="flex items-center gap-2 px-3 py-2.5 bg-white border border-slate-200 rounded-lg cursor-pointer hover:border-amber-300 transition-colors" {/* <div className="flex items-center gap-2 px-3 py-2.5 bg-white border border-slate-200 rounded-lg cursor-pointer hover:border-red-300 transition-colors"
onClick={() => updateQuestion(index, 'isMandatory', !q.isMandatory)} onClick={() => updateQuestion(index, 'isMandatory', !q.isMandatory)}
> >
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${q.isMandatory ? 'bg-amber-600 border-amber-600' : 'border-slate-300'}`}> <div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${q.isMandatory ? 'bg-re-red border-re-red' : 'border-slate-300'}`}>
{q.isMandatory && <div className="w-2 h-2 bg-white rounded-sm" />} {q.isMandatory && <div className="w-2 h-2 bg-white rounded-sm" />}
</div> </div>
<span className="text-xs font-medium text-slate-600 select-none">Req.</span> <span className="text-xs font-medium text-slate-600 select-none">Req.</span>
@ -345,7 +345,7 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={opt.text} value={opt.text}
onChange={(e) => updateOption(index, optIndex, 'text', e.target.value)} onChange={(e) => updateOption(index, optIndex, 'text', e.target.value)}
className="flex-1 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none" className="flex-1 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-red-500 outline-none"
placeholder={`Option ${optIndex + 1}`} placeholder={`Option ${optIndex + 1}`}
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -356,7 +356,7 @@ const QuestionnaireBuilder: React.FC = () => {
max={isNaN(q.weight) ? 0 : q.weight} max={isNaN(q.weight) ? 0 : q.weight}
min={0} min={0}
onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)} onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)}
className={`w-20 border ${(opt.score > q.weight) || isNaN(opt.score) ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none`} className={`w-20 border ${(opt.score > q.weight) || isNaN(opt.score) ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-red-500 outline-none`}
/> />
</div> </div>
<button <button
@ -371,7 +371,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div> </div>
<button <button
onClick={() => addOption(index)} onClick={() => addOption(index)}
className="mt-3 text-sm flex items-center gap-1 text-amber-600 hover:text-amber-700 font-medium" className="mt-3 text-sm flex items-center gap-1 text-re-red hover:text-re-red-hover font-medium"
> >
<Plus size={16} /> Add Option <Plus size={16} /> Add Option
</button> </button>
@ -392,7 +392,7 @@ const QuestionnaireBuilder: React.FC = () => {
<button <button
onClick={addQuestion} onClick={addQuestion}
className="mt-8 w-full border-2 border-dashed border-slate-300 p-4 rounded-xl text-slate-500 hover:border-amber-500 hover:text-amber-600 hover:bg-amber-50/30 flex justify-center items-center gap-2 transition-all font-medium" className="mt-8 w-full border-2 border-dashed border-slate-300 p-4 rounded-xl text-slate-500 hover:border-red-500 hover:text-re-red hover:bg-red-50/30 flex justify-center items-center gap-2 transition-all font-medium"
> >
<Plus size={20} /> Add Another Question <Plus size={20} /> Add Another Question
</button> </button>

View File

@ -47,7 +47,7 @@ const QuestionnaireList: React.FC = () => {
</div> </div>
<button <button
onClick={() => navigate('/questionnaire-builder')} onClick={() => navigate('/questionnaire-builder')}
className="bg-amber-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-amber-700 transition" className="bg-re-red text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-re-red-hover transition"
> >
<Plus size={20} /> Create New Version <Plus size={20} /> Create New Version
</button> </button>
@ -55,14 +55,14 @@ const QuestionnaireList: React.FC = () => {
{loading ? ( {loading ? (
<div className="flex justify-center p-12"> <div className="flex justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-re-red"></div>
</div> </div>
) : versions.length === 0 ? ( ) : versions.length === 0 ? (
<div className="text-center p-12 bg-white rounded-lg shadow-sm border border-slate-200"> <div className="text-center p-12 bg-white rounded-lg shadow-sm border border-slate-200">
<p className="text-slate-500 mb-4">No questionnaire versions found.</p> <p className="text-slate-500 mb-4">No questionnaire versions found.</p>
<button <button
onClick={() => navigate('/questionnaire-builder')} onClick={() => navigate('/questionnaire-builder')}
className="text-amber-600 font-medium hover:underline" className="text-re-red font-medium hover:underline"
> >
Create your first version Create your first version
</button> </button>
@ -102,7 +102,7 @@ const QuestionnaireList: React.FC = () => {
<td className="p-4 text-right"> <td className="p-4 text-right">
<button <button
onClick={() => navigate(`/questionnaire-builder/${v.id}`)} onClick={() => navigate(`/questionnaire-builder/${v.id}`)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-medium text-slate-600 hover:text-amber-600 hover:bg-amber-50 transition border border-slate-200 hover:border-amber-200" className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-medium text-slate-600 hover:text-re-red hover:bg-red-50 transition border border-slate-200 hover:border-red-200"
> >
<Edit2 size={14} /> Edit / Clone <Edit2 size={14} /> Edit / Clone
</button> </button>

View File

@ -219,7 +219,7 @@ export function UserManagementPage() {
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2"> <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Users className="w-6 h-6 text-amber-600" /> <Users className="w-6 h-6 text-re-red" />
User Management User Management
</h1> </h1>
<p className="text-slate-500">Manage system users, roles, and access permissions.</p> <p className="text-slate-500">Manage system users, roles, and access permissions.</p>
@ -232,7 +232,7 @@ export function UserManagementPage() {
zoneId: '', regionId: '', stateId: '', districtId: '' zoneId: '', regionId: '', stateId: '', districtId: ''
}); setShowUserModal(true); }); setShowUserModal(true);
}} }}
className="bg-amber-600 hover:bg-amber-700 text-white shrink-0" className="bg-re-red hover:bg-re-red-hover text-white shrink-0"
> >
<UserPlus className="w-4 h-4 mr-2" /> <UserPlus className="w-4 h-4 mr-2" />
Add New User Add New User
@ -318,7 +318,7 @@ export function UserManagementPage() {
<TableRow key={user.id} className="hover:bg-slate-50/50"> <TableRow key={user.id} className="hover:bg-slate-50/50">
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center font-bold"> <div className="w-10 h-10 rounded-full bg-red-100 text-re-red-hover flex items-center justify-center font-bold">
{user.fullName?.charAt(0) || user.email?.charAt(0)} {user.fullName?.charAt(0) || user.email?.charAt(0)}
</div> </div>
<div> <div>
@ -384,7 +384,7 @@ export function UserManagementPage() {
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="ghost" size="icon" onClick={() => handleEditUser(user)}> <Button variant="ghost" size="icon" onClick={() => handleEditUser(user)}>
<Edit2 className="w-4 h-4 text-slate-400 hover:text-amber-600" /> <Edit2 className="w-4 h-4 text-slate-400 hover:text-re-red" />
</Button> </Button>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<Trash2 className="w-4 h-4 text-slate-400 hover:text-red-600" /> <Trash2 className="w-4 h-4 text-slate-400 hover:text-red-600" />
@ -569,7 +569,7 @@ export function UserManagementPage() {
<DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg"> <DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg">
<Button variant="outline" onClick={() => setShowUserModal(false)}>Cancel</Button> <Button variant="outline" onClick={() => setShowUserModal(false)}>Cancel</Button>
<Button className="bg-amber-600 hover:bg-amber-700 text-white" onClick={handleSubmit}> <Button className="bg-re-red hover:bg-re-red-hover text-white" onClick={handleSubmit}>
{editingUser ? 'Save Changes' : 'Create User'} {editingUser ? 'Save Changes' : 'Create User'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -47,12 +47,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const currentRole = currentUser?.role || currentUser?.roleCode || ''; const currentRole = currentUser?.role || currentUser?.roleCode || '';
const normalizedRole = String(currentRole).trim().toLowerCase(); const hasRole = (roles: string[]) => {
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
const userRole = String(currentUser?.role || '').toLowerCase();
const userRoleCode = String(currentUser?.roleCode || '').toLowerCase();
return normalizedTargetRoles.includes(userRole) || normalizedTargetRoles.includes(userRoleCode);
};
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; const resignationRoles = ['DD Admin', 'DD_ADMIN', 'ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'LEGAL_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const terminationRoles = ['ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'ZBH', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Legal Admin', 'LEGAL_ADMIN', 'Legal', 'DD Admin', 'DD_ADMIN', 'CCO', 'CEO', 'Super Admin', 'SUPER_ADMIN'];
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; const fnfRoles = ['DD Admin', 'DD_ADMIN', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Finance', 'Finance Admin', 'FINANCE_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const canSeeResignation = hasRole(resignationRoles); const canSeeResignation = hasRole(resignationRoles);
const canSeeTermination = hasRole(terminationRoles); const canSeeTermination = hasRole(terminationRoles);
const canSeeFnF = hasRole(fnfRoles); const canSeeFnF = hasRole(fnfRoles);

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})}
/> />
{formData.ownRoyalEnfield === 'yes' && (
<div className="relative"> <div className="relative">
<select <select
className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none disabled:bg-slate-50" className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none"
value={formData.royalEnfieldModel} value={formData.royalEnfieldModel}
disabled={formData.ownRoyalEnfield !== 'yes'}
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

@ -1,6 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react" import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage" import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage"
import { toast } from "sonner"
jest.mock("sonner", () => ({ jest.mock("sonner", () => ({
toast: { toast: {
@ -138,4 +139,54 @@ describe("ConstitutionalChangePage", () => {
expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument() expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument()
}) })
it("shows backend duplicate-open message on create conflict", async () => {
const user = userEvent.setup()
const { API } = await import("@/api/API")
;(API.getDealers as jest.Mock).mockResolvedValueOnce({
data: {
success: true,
data: [
{
user: { id: "dealer-user-1" },
constitutionType: "Proprietorship",
businessName: "Dealer A",
legalName: "Dealer A Pvt",
dealerCode: { dealerCode: "DLR-1" },
},
],
},
})
;(API.createConstitutionalChange as jest.Mock).mockRejectedValueOnce({
response: {
data: {
message:
"Open constitutional request CCR-1 already exists at ASM Review. Complete it before creating a new one.",
},
},
})
setup()
await user.click(screen.getByRole("button", { name: /new request/i }))
await screen.findByRole("heading", {
name: /create constitutional change request/i,
})
await user.click(screen.getByRole("combobox", { name: /dealer/i }))
await user.click(await screen.findByText(/DLR-1 — Dealer A/i))
await user.click(screen.getByRole("combobox", { name: /proposed constitution/i }))
await user.click(await screen.findByText(/^Partnership$/i))
const reasonField = screen.getByLabelText(/reason for constitutional change/i)
await user.type(reasonField, "Need to onboard new partner")
await user.click(screen.getByRole("button", { name: /submit request/i }))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
expect.stringContaining("Open constitutional request CCR-1 already exists")
)
})
})
}) })

View File

@ -44,7 +44,7 @@ const formatStageRole = (role: string) =>
// Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution // Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution
const documentRequirements: Record<string, number[]> = { const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
'LLP': [1, 2, 3, 7, 8, 9, 10, 11, 16], 'LLP': [1, 2, 3, 7, 8, 10, 11, 16],
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Proprietorship': [1, 2, 3, 10, 16] 'Proprietorship': [1, 2, 3, 10, 16]
@ -151,6 +151,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false); const [isActionLoading, setIsActionLoading] = useState(false);
/** Set when POST /action returns 4xx (apisauce does not throw — must check response.ok). */
const [actionDialogError, setActionDialogError] = useState<string | null>(null);
const [isUploadingDoc, setIsUploadingDoc] = useState(false); const [isUploadingDoc, setIsUploadingDoc] = useState(false);
const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false); const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false);
const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null); const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null);
@ -415,6 +417,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => { const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => {
setActionType(type); setActionType(type);
setActionDialogError(null);
setIsActionDialogOpen(true); setIsActionDialogOpen(true);
}; };
@ -436,6 +439,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
try { try {
setIsActionLoading(true); setIsActionLoading(true);
setActionDialogError(null);
const actionPayload = const actionPayload =
actionType === 'approve' actionType === 'approve'
? OFFBOARDING_ACTIONS.APPROVE ? OFFBOARDING_ACTIONS.APPROVE
@ -448,7 +452,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
comments comments
}) as any; }) as any;
if (response.data.success) { const payload = response?.data as { success?: boolean; message?: string } | undefined;
/** apisauce returns { ok: false } on 4xx without throwing — must branch on this. */
if (response?.ok && payload?.success) {
const actionText = const actionText =
actionType === 'approve' ? 'approved' : actionType === 'approve' ? 'approved' :
actionType === 'reject' ? 'rejected' : actionType === 'reject' ? 'rejected' :
@ -457,12 +463,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
toast.success(`Request ${actionText} successfully`); toast.success(`Request ${actionText} successfully`);
setIsActionDialogOpen(false); setIsActionDialogOpen(false);
setComments(''); setComments('');
setActionDialogError(null);
await fetchRequestDetails(); await fetchRequestDetails();
return;
} }
const message =
payload?.message ||
(response as any)?.data?.error ||
'Failed to submit action';
setActionDialogError(message);
const docGate = /mandatory documents/i.test(message);
toast.error(message, { duration: docGate ? 14000 : 8000 });
} catch (error) { } catch (error) {
console.error('Submit action error:', error); console.error('Submit action error:', error);
const message = (error as any)?.response?.data?.message || 'Failed to submit action'; const message =
toast.error(message); (error as any)?.response?.data?.message ||
(error as any)?.message ||
'Failed to submit action';
setActionDialogError(message);
toast.error(message, { duration: /mandatory documents/i.test(message) ? 14000 : 8000 });
} finally { } finally {
setIsActionLoading(false); setIsActionLoading(false);
} }
@ -1261,7 +1281,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</div> </div>
{/* Action Dialog */} {/* Action Dialog */}
<Dialog open={isActionDialogOpen} onOpenChange={setIsActionDialogOpen}> <Dialog
open={isActionDialogOpen}
onOpenChange={(open) => {
setIsActionDialogOpen(open);
if (!open) setActionDialogError(null);
}}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -1278,6 +1304,24 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmitAction} className="space-y-4"> <form onSubmit={handleSubmitAction} className="space-y-4">
{actionDialogError && (
<div
role="alert"
className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-900 flex gap-2"
>
<AlertCircle className="w-5 h-5 shrink-0 text-red-600" aria-hidden />
<div className="min-w-0">
<p className="font-medium">This action was not completed</p>
<p className="mt-1 whitespace-pre-wrap break-words">{actionDialogError}</p>
{/mandatory documents/i.test(actionDialogError) && (
<p className="mt-2 text-red-800">
Use the <strong>Documents</strong> tab to upload every required file for this constitution
type, then approve again.
</p>
)}
</div>
</div>
)}
<div> <div>
<Label htmlFor="comments"> <Label htmlFor="comments">
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'} {actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}

View File

@ -32,7 +32,7 @@ interface ConstitutionalChangePageProps {
// Document requirements mapping (keys = DB `changeType` ENUM values) // Document requirements mapping (keys = DB `changeType` ENUM values)
const documentRequirements: Record<string, number[]> = { const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
'LLP': [1, 2, 3, 7, 8, 9, 10, 16], 'LLP': [1, 2, 3, 7, 8, 10, 11, 16],
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Proprietorship': [1, 2, 3, 10, 16] 'Proprietorship': [1, 2, 3, 10, 16]
}; };
@ -73,10 +73,8 @@ const getTypeColor = (type: string) => {
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300'; case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300'; case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
case 'LLP': case 'LLP':
case 'LLP Conversion':
return 'bg-indigo-100 text-indigo-700 border-indigo-300'; return 'bg-indigo-100 text-indigo-700 border-indigo-300';
case 'Private Limited': case 'Private Limited':
case 'Pvt Ltd':
return 'bg-cyan-100 text-cyan-700 border-cyan-300'; return 'bg-cyan-100 text-cyan-700 border-cyan-300';
default: return 'bg-slate-100 text-slate-700 border-slate-300'; default: return 'bg-slate-100 text-slate-700 border-slate-300';
} }

View File

@ -270,12 +270,13 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<ul className="text-blue-800 text-sm space-y-1"> <ul className="text-blue-800 text-sm space-y-1">
<li> GST Registration Certificate</li> <li> GST Registration Certificate</li>
<li> Firm PAN Copy</li> <li> Firm PAN Copy</li>
<li> Partnership Deed (if applicable)</li> <li> Self-attested KYC documents</li>
<li> LLP Agreement (if applicable)</li> <li> Business Purchase Agreement (BPA)</li>
<li> Certificate of Incorporation (if applicable)</li> <li> Partnership Agreement / Firm Registration (if target is Partnership)</li>
<li> MOA & AOA (if applicable)</li> <li> LLP Agreement / COI (if target is LLP)</li>
<li> Board Resolution</li> <li> MOA, AOA, COI (if target is Private Limited)</li>
<li> Aadhaar & PAN of all partners/directors</li> <li> Cancelled Cheque</li>
<li> Declaration / Authorization Letter</li>
</ul> </ul>
</div> </div>

View File

@ -181,7 +181,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
return ( return (
<div key={state} className="mb-4 last:mb-0"> <div key={state} className="mb-4 last:mb-0">
<h4 className="text-sm text-amber-700 mb-2 pb-1 border-b border-slate-200">{state}</h4> <h4 className="text-sm text-re-red-hover mb-2 pb-1 border-b border-slate-200">{state}</h4>
<div className="space-y-2 ml-2"> <div className="space-y-2 ml-2">
{districts.map((district: any) => ( {districts.map((district: any) => (
<div key={district.id}> <div key={district.id}>
@ -280,7 +280,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save DD-AM</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save DD-AM</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -41,7 +41,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<CardTitle>District Development Area Managers (DD-AM)</CardTitle> <CardTitle>District Development Area Managers (DD-AM)</CardTitle>
<CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription> <CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription>
</div> </div>
<Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddASM} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add DD-AM Add DD-AM
</Button> </Button>
@ -86,7 +86,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<Badge <Badge
key={idx} key={idx}
variant={isShared ? "outline" : "secondary"} variant={isShared ? "outline" : "secondary"}
className={`text-xs ${isShared ? "border-amber-300 bg-amber-50 text-amber-700 font-medium" : ""}`} className={`text-xs ${isShared ? "border-red-300 bg-red-50 text-re-red-hover font-medium" : ""}`}
title={isShared ? `Also managed by: ${otherManagers.join(', ')}` : undefined} title={isShared ? `Also managed by: ${otherManagers.join(', ')}` : undefined}
> >
{areaName} {areaName}
@ -112,7 +112,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditASM(asm)}> <Button variant="ghost" size="sm" onClick={() => onEditASM(asm)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteASM(asm.id, asm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteASM(asm.id, asm.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -83,7 +83,7 @@ export const AutoAssignmentSettings: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center p-12 space-y-4"> <div className="flex flex-col items-center justify-center p-12 space-y-4">
<RefreshCcw className="w-8 h-8 animate-spin text-amber-600" /> <RefreshCcw className="w-8 h-8 animate-spin text-re-red" />
<p className="text-slate-500 font-medium">Loading governance controls...</p> <p className="text-slate-500 font-medium">Loading governance controls...</p>
</div> </div>
); );
@ -94,8 +94,8 @@ export const AutoAssignmentSettings: React.FC = () => {
<Card className="border-none shadow-md overflow-hidden bg-white/50 backdrop-blur-sm"> <Card className="border-none shadow-md overflow-hidden bg-white/50 backdrop-blur-sm">
<CardHeader className="bg-gradient-to-r from-slate-900 to-slate-800 text-white p-6"> <CardHeader className="bg-gradient-to-r from-slate-900 to-slate-800 text-white p-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-amber-500/20 rounded-lg"> <div className="p-2 bg-red-500/20 rounded-lg">
<Settings2 className="w-6 h-6 text-amber-400" /> <Settings2 className="w-6 h-6 text-red-400" />
</div> </div>
<div> <div>
<CardTitle className="text-xl font-bold">Auto-Assignment Governance</CardTitle> <CardTitle className="text-xl font-bold">Auto-Assignment Governance</CardTitle>
@ -148,6 +148,7 @@ export const AutoAssignmentSettings: React.FC = () => {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Switch <Switch
className="data-[state=checked]:bg-re-red"
checked={isEnabled} checked={isEnabled}
onCheckedChange={(val) => { onCheckedChange={(val) => {
handleToggle(mod.key, val); handleToggle(mod.key, val);
@ -164,9 +165,9 @@ export const AutoAssignmentSettings: React.FC = () => {
})} })}
</div> </div>
<div className="mt-8 p-4 bg-amber-50 border border-amber-100 rounded-lg flex items-start gap-3"> <div className="mt-8 p-4 bg-red-50 border border-red-100 rounded-lg flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /> <Info className="w-5 h-5 text-re-red shrink-0 mt-0.5" />
<div className="text-sm text-amber-800"> <div className="text-sm text-red-800">
<p className="font-semibold mb-1">Impact of Manual Mode:</p> <p className="font-semibold mb-1">Impact of Manual Mode:</p>
<p>Turning OFF auto-assignment will ONLY affect new requests. Existing requests will retain their current participant mappings. You will need to use the "Add Participant" button in the worknotes or application details to grant access to stakeholders.</p> <p>Turning OFF auto-assignment will ONLY affect new requests. Existing requests will retain their current participant mappings. You will need to use the "Add Participant" button in the worknotes or application details to grant access to stakeholders.</p>
</div> </div>

View File

@ -127,7 +127,7 @@ export const DDLeadDialog: React.FC<DDLeadDialogProps> = ({
<div className="flex gap-3 pt-6"> <div className="flex gap-3 pt-6">
<Button variant="outline" className="flex-1 border-slate-200" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1 border-slate-200" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 shadow-sm" onClick={onSave}>Save DD Lead</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover shadow-sm" onClick={onSave}>Save DD Lead</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -36,7 +36,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<CardTitle>DD-Leads (Dealer Development Lead)</CardTitle> <CardTitle>DD-Leads (Dealer Development Lead)</CardTitle>
<CardDescription>Manage DD-Leads and their zonal assignments</CardDescription> <CardDescription>Manage DD-Leads and their zonal assignments</CardDescription>
</div> </div>
<Button onClick={onAddLead} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddLead} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add DD-Lead Add DD-Lead
</Button> </Button>
@ -60,7 +60,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<TableRow key={lead.id}> <TableRow key={lead.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="w-4 h-4 text-amber-600" /> <Users className="w-4 h-4 text-re-red" />
<span className="font-medium">{lead.leadCode || 'N/A'}</span> <span className="font-medium">{lead.leadCode || 'N/A'}</span>
</div> </div>
</TableCell> </TableCell>
@ -95,7 +95,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditLead(lead)}> <Button variant="ghost" size="sm" onClick={() => onEditLead(lead)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteLead(lead.id, lead.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteLead(lead.id, lead.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -193,7 +193,10 @@ export const DealerAsmAssignment: React.FC = () => {
</TableCell> </TableCell>
<TableCell>{dealer.dealerCode || 'N/A'}</TableCell> <TableCell>{dealer.dealerCode || 'N/A'}</TableCell>
<TableCell> <TableCell>
<Badge variant={String(dealer.status || '').toLowerCase() === 'active' ? 'default' : 'secondary'}> <Badge
variant={String(dealer.status || '').toLowerCase() === 'active' ? 'default' : 'secondary'}
className={String(dealer.status || '').toLowerCase() === 'active' ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : ''}
>
{dealer.status || 'Unknown'} {dealer.status || 'Unknown'}
</Badge> </Badge>
</TableCell> </TableCell>
@ -215,7 +218,7 @@ export const DealerAsmAssignment: React.FC = () => {
onChange={(val) => setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))} onChange={(val) => setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))}
className="flex-1 min-w-[180px]" className="flex-1 min-w-[180px]"
/> />
<Button size="sm" className="shrink-0" onClick={() => saveMapping(dealer.dealerId)}> <Button size="sm" className="shrink-0 bg-re-red hover:bg-re-red-hover text-white" onClick={() => saveMapping(dealer.dealerId)}>
Assign Assign
</Button> </Button>
</div> </div>

View File

@ -179,7 +179,7 @@ export const DocumentConfigManagement: React.FC = () => {
if (metadataLoading) { if (metadataLoading) {
return ( return (
<div className="h-96 flex flex-col items-center justify-center gap-4"> <div className="h-96 flex flex-col items-center justify-center gap-4">
<Database className="w-10 h-10 text-amber-600 animate-bounce" /> <Database className="w-10 h-10 text-re-red animate-bounce" />
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Connecting to Governance Engine...</p> <p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Connecting to Governance Engine...</p>
</div> </div>
); );
@ -189,8 +189,8 @@ export const DocumentConfigManagement: React.FC = () => {
<Card className="border-slate-200 shadow-sm overflow-hidden bg-white"> <Card className="border-slate-200 shadow-sm overflow-hidden bg-white">
<CardHeader className="bg-slate-50/80 border-b border-slate-200 py-4 relative"> <CardHeader className="bg-slate-50/80 border-b border-slate-200 py-4 relative">
{backgroundLoading && ( {backgroundLoading && (
<div className="absolute top-0 left-0 right-0 h-1 bg-amber-100 overflow-hidden"> <div className="absolute top-0 left-0 right-0 h-1 bg-red-100 overflow-hidden">
<div className="h-full bg-amber-600 animate-[loading_1.5s_infinite_linear]" style={{width: '30%', transformOrigin: 'left'}} /> <div className="h-full bg-re-red animate-[loading_1.5s_infinite_linear]" style={{width: '30%', transformOrigin: 'left'}} />
<style>{` <style>{`
@keyframes loading { @keyframes loading {
0% { left: -30%; } 0% { left: -30%; }
@ -202,7 +202,7 @@ export const DocumentConfigManagement: React.FC = () => {
<div className="flex flex-row items-center justify-between mb-4"> <div className="flex flex-row items-center justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-xl shadow-sm ring-1 ring-slate-200 text-amber-600"> <div className="p-2 bg-white rounded-xl shadow-sm ring-1 ring-slate-200 text-re-red">
<Layers className="w-5 h-5" /> <Layers className="w-5 h-5" />
</div> </div>
<div> <div>
@ -218,7 +218,7 @@ export const DocumentConfigManagement: React.FC = () => {
<div className="flex gap-4"> <div className="flex gap-4">
<div className="w-64"> <div className="w-64">
<Select value={selectedModule} onValueChange={(val) => { setSelectedModule(val); setPage(1); }}> <Select value={selectedModule} onValueChange={(val) => { setSelectedModule(val); setPage(1); }}>
<SelectTrigger className="h-10 rounded-xl bg-white border-slate-200 focus:ring-amber-500 font-bold text-slate-700 shadow-sm"> <SelectTrigger className="h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 font-bold text-slate-700 shadow-sm">
<SelectValue placeholder="Target Module" /> <SelectValue placeholder="Target Module" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl shadow-2xl border-none"> <SelectContent className="rounded-xl shadow-2xl border-none">
@ -236,7 +236,7 @@ export const DocumentConfigManagement: React.FC = () => {
placeholder="Search policies, stages or documents..." placeholder="Search policies, stages or documents..."
value={search} value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }} onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="pl-10 h-10 rounded-xl bg-white border-slate-200 focus:ring-amber-500 shadow-sm font-medium" className="pl-10 h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 shadow-sm font-medium"
/> />
</div> </div>
</div> </div>
@ -244,7 +244,7 @@ export const DocumentConfigManagement: React.FC = () => {
<CardContent className="p-0 min-h-[400px] relative"> <CardContent className="p-0 min-h-[400px] relative">
{loading ? ( {loading ? (
<div className="absolute inset-0 z-10 bg-white/60 backdrop-blur-[1px] flex flex-col items-center justify-center gap-3"> <div className="absolute inset-0 z-10 bg-white/60 backdrop-blur-[1px] flex flex-col items-center justify-center gap-3">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
<span className="text-slate-500 text-sm font-bold animate-pulse">Syncing Policies...</span> <span className="text-slate-500 text-sm font-bold animate-pulse">Syncing Policies...</span>
</div> </div>
) : null} ) : null}
@ -274,7 +274,7 @@ export const DocumentConfigManagement: React.FC = () => {
) : configs.map((config) => ( ) : configs.map((config) => (
<TableRow key={config.id} className="hover:bg-slate-50/80 transition-colors group h-14"> <TableRow key={config.id} className="hover:bg-slate-50/80 transition-colors group h-14">
<TableCell> <TableCell>
<div className="font-bold text-slate-900 group-hover:text-amber-700 transition-colors uppercase text-[12px]">{config.documentType}</div> <div className="font-bold text-slate-900 group-hover:text-re-red-hover transition-colors uppercase text-[12px]">{config.documentType}</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="bg-blue-50/50 border-blue-100 text-blue-700 font-bold px-2 py-0.5 whitespace-nowrap text-[10px] rounded-md uppercase"> <Badge variant="outline" className="bg-blue-50/50 border-blue-100 text-blue-700 font-bold px-2 py-0.5 whitespace-nowrap text-[10px] rounded-md uppercase">
@ -305,7 +305,7 @@ export const DocumentConfigManagement: React.FC = () => {
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
{config.isMandatory && ( {config.isMandatory && (
<Badge className="bg-red-600 text-white border-transparent text-[10px] font-bold h-5 px-1.5 rounded-sm uppercase tracking-tighter">BLOCKING</Badge> <Badge className="bg-re-red text-white border-transparent text-[10px] font-bold h-5 px-1.5 rounded-sm uppercase tracking-tighter">BLOCKING</Badge>
)} )}
{!config.isActive && ( {!config.isActive && (
<Badge className="bg-slate-200 text-slate-500 border-transparent text-[10px] h-5 px-1.5 rounded-sm uppercase tracking-tighter">DORMANT</Badge> <Badge className="bg-slate-200 text-slate-500 border-transparent text-[10px] h-5 px-1.5 rounded-sm uppercase tracking-tighter">DORMANT</Badge>
@ -320,7 +320,7 @@ export const DocumentConfigManagement: React.FC = () => {
<Button variant="ghost" size="icon" onClick={() => openEdit(config)} className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-transform active:scale-90"> <Button variant="ghost" size="icon" onClick={() => openEdit(config)} className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-transform active:scale-90">
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(config.id)} className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-transform active:scale-90"> <Button variant="ghost" size="icon" onClick={() => handleDelete(config.id)} className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-lg transition-transform active:scale-90">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
@ -333,7 +333,7 @@ export const DocumentConfigManagement: React.FC = () => {
{/* Pagination Controls */} {/* Pagination Controls */}
<div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 border-t border-slate-200 mt-auto"> <div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 border-t border-slate-200 mt-auto">
<div className="text-[11px] text-slate-500 font-bold uppercase tracking-tight"> <div className="text-[11px] text-slate-500 font-bold uppercase tracking-tight">
Dataset Index <span className="text-slate-900 border-b border-slate-300 mx-1">{configs.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, pagination.total)}</span> Total Found <span className="text-amber-700 font-extrabold ml-1">{pagination.total}</span> Dataset Index <span className="text-slate-900 border-b border-slate-300 mx-1">{configs.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, pagination.total)}</span> Total Found <span className="text-re-red-hover font-extrabold ml-1">{pagination.total}</span>
</div> </div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Button <Button
@ -346,7 +346,7 @@ export const DocumentConfigManagement: React.FC = () => {
<ChevronLeft className="w-4 h-4 mr-1 text-slate-600" /> Prev <ChevronLeft className="w-4 h-4 mr-1 text-slate-600" /> Prev
</Button> </Button>
<div className="flex items-center px-4 h-9 bg-white border border-slate-200 rounded-xl text-xs font-extrabold text-slate-800 shadow-inner"> <div className="flex items-center px-4 h-9 bg-white border border-slate-200 rounded-xl text-xs font-extrabold text-slate-800 shadow-inner">
<span className="text-amber-600">{page}</span> <span className="mx-2 text-slate-300">/</span> {pagination.pages} <span className="text-re-red">{page}</span> <span className="mx-2 text-slate-300">/</span> {pagination.pages}
</div> </div>
<Button <Button
variant="outline" variant="outline"
@ -366,7 +366,7 @@ export const DocumentConfigManagement: React.FC = () => {
<DialogHeader className="bg-slate-900 text-white p-7"> <DialogHeader className="bg-slate-900 text-white p-7">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md ring-1 ring-white/20"> <div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md ring-1 ring-white/20">
<Settings2 className="w-7 h-7 text-amber-400" /> <Settings2 className="w-7 h-7 text-red-400" />
</div> </div>
<div> <div>
<DialogTitle className="text-2xl font-black tracking-tight uppercase"> <DialogTitle className="text-2xl font-black tracking-tight uppercase">
@ -385,12 +385,12 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.module} value={formData.module}
onValueChange={(val) => setFormData(prev => ({ ...prev, module: val, stageCode: 'General' }))} onValueChange={(val) => setFormData(prev => ({ ...prev, module: val, stageCode: 'General' }))}
> >
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm bg-slate-50 font-black text-xs uppercase"> <SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-slate-50 font-black text-xs uppercase">
<SelectValue placeholder="Module" /> <SelectValue placeholder="Module" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl"> <SelectContent className="rounded-xl border-none shadow-2xl">
{modules.map(m => ( {modules.map(m => (
<SelectItem key={m} value={m} className="py-3 px-4 rounded-lg focus:bg-amber-50 font-black text-[10px] uppercase"> <SelectItem key={m} value={m} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">
{m.replace(/_/g, ' ')} {m.replace(/_/g, ' ')}
</SelectItem> </SelectItem>
))} ))}
@ -403,12 +403,12 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.stageCode} value={formData.stageCode}
onValueChange={(val) => setFormData(prev => ({ ...prev, stageCode: val }))} onValueChange={(val) => setFormData(prev => ({ ...prev, stageCode: val }))}
> >
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm bg-white font-black text-xs uppercase"> <SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-white font-black text-xs uppercase">
<SelectValue placeholder="Select Stage" /> <SelectValue placeholder="Select Stage" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl"> <SelectContent className="rounded-xl border-none shadow-2xl">
{(stagesMap[formData.module] || ['General']).map(stage => ( {(stagesMap[formData.module] || ['General']).map(stage => (
<SelectItem key={stage} value={stage} className="py-3 px-4 rounded-lg focus:bg-amber-50 font-black text-[10px] uppercase">{stage}</SelectItem> <SelectItem key={stage} value={stage} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">{stage}</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@ -421,28 +421,28 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.documentType} value={formData.documentType}
onChange={(e) => setFormData(prev => ({ ...prev, documentType: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, documentType: e.target.value }))}
placeholder="e.g., PAN Card, Blueprint" placeholder="e.g., PAN Card, Blueprint"
className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm font-black text-sm uppercase placeholder:font-bold placeholder:text-slate-300" className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm font-black text-sm uppercase placeholder:font-bold placeholder:text-slate-300"
/> />
</div> </div>
<div className="space-y-4 p-5 bg-slate-50 rounded-2xl border border-slate-100 shadow-inner"> <div className="space-y-4 p-5 bg-slate-50 rounded-2xl border border-slate-100 shadow-inner">
<Label className="text-slate-900 font-black flex items-center gap-2 mb-2 text-[11px] uppercase tracking-wider"> <Label className="text-slate-900 font-black flex items-center gap-2 mb-2 text-[11px] uppercase tracking-wider">
<ShieldCheck className="w-4 h-4 text-amber-600" /> Visibility Matrix <ShieldCheck className="w-4 h-4 text-re-red" /> Visibility Matrix
</Label> </Label>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{ROLE_LIST.map((role: string) => ( {ROLE_LIST.map((role: string) => (
<div <div
key={role} key={role}
className={`flex items-center space-x-2 p-3 rounded-xl border transition-all cursor-pointer group active:scale-95 ${formData.allowedRoles.includes(role) ? 'bg-amber-50 border-amber-300 shadow-sm' : 'bg-white border-slate-200 hover:border-amber-200 hover:shadow-sm'}`} className={`flex items-center space-x-2 p-3 rounded-xl border transition-all cursor-pointer group active:scale-95 ${formData.allowedRoles.includes(role) ? 'bg-red-50 border-red-300 shadow-sm' : 'bg-white border-slate-200 hover:border-red-200 hover:shadow-sm'}`}
onClick={() => toggleRole(role)} onClick={() => toggleRole(role)}
> >
<Checkbox <Checkbox
id={`role-${role}`} id={`role-${role}`}
checked={formData.allowedRoles.includes(role)} checked={formData.allowedRoles.includes(role)}
onCheckedChange={() => toggleRole(role)} onCheckedChange={() => toggleRole(role)}
className="w-4 h-4 data-[state=checked]:bg-amber-600 data-[state=checked]:border-amber-600 rounded" className="w-4 h-4 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded"
/> />
<Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-amber-800' : 'text-slate-500 group-hover:text-amber-700'}`}>{role}</Label> <Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-red-800' : 'text-slate-500 group-hover:text-re-red-hover'}`}>{role}</Label>
</div> </div>
))} ))}
</div> </div>
@ -457,7 +457,7 @@ export const DocumentConfigManagement: React.FC = () => {
id="mandatory" id="mandatory"
checked={formData.isMandatory} checked={formData.isMandatory}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isMandatory: !!checked }))} onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isMandatory: !!checked }))}
className="w-5 h-5 border-slate-300 data-[state=checked]:bg-red-600 data-[state=checked]:border-red-600 rounded-md" className="w-5 h-5 border-slate-300 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded-md"
/> />
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="mandatory" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-red-900 transition-colors uppercase tracking-tight">Mandatory Policy</Label> <Label htmlFor="mandatory" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-red-900 transition-colors uppercase tracking-tight">Mandatory Policy</Label>

View File

@ -89,9 +89,9 @@ export const EmailTemplateBodyEditor = React.forwardRef<EmailTemplateBodyEditorH
</TabsList> </TabsList>
{advanced && ( {advanced && (
<Alert className="border-amber-200 bg-amber-50 py-2"> <Alert className="border-red-200 bg-red-50 py-2">
<Info className="h-4 w-4 text-amber-700" /> <Info className="h-4 w-4 text-re-red-hover" />
<AlertDescription className="text-[11px] text-amber-900"> <AlertDescription className="text-[11px] text-red-900">
This template uses layout partials (<code className="font-mono">{'{{> ...}}'}</code>), block helpers, or a full HTML This template uses layout partials (<code className="font-mono">{'{{> ...}}'}</code>), block helpers, or a full HTML
document. Edit it in <strong>HTML source</strong> so nothing is stripped. Use placeholders on the left to insert document. Edit it in <strong>HTML source</strong> so nothing is stripped. Use placeholders on the left to insert
fields safely. fields safely.

View File

@ -45,8 +45,8 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
<TableRow key={template.id}> <TableRow key={template.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-amber-50 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-red-50 rounded-lg flex items-center justify-center">
<Mail className="w-4 h-4 text-amber-600" /> <Mail className="w-4 h-4 text-re-red" />
</div> </div>
<span className="font-medium text-slate-900">{template.name || template.templateCode}</span> <span className="font-medium text-slate-900">{template.name || template.templateCode}</span>
</div> </div>
@ -68,7 +68,7 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditTemplate(template)}> <Button variant="ghost" size="sm" onClick={() => onEditTemplate(template)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteTemplate(template.id)} className="text-red-500 hover:text-red-600 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteTemplate(template.id)} className="text-red-500 hover:text-re-red hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -316,7 +316,7 @@ const InterviewConfigManagement: React.FC = () => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800"> <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-800">
No active configuration found. Click "Publish New Version" to create one, or "Reset to Defaults" to initialize system defaults. No active configuration found. Click "Publish New Version" to create one, or "Reset to Defaults" to initialize system defaults.
</div> </div>
)} )}
@ -338,7 +338,7 @@ const InterviewConfigManagement: React.FC = () => {
<Button variant="ghost" size="sm" onClick={() => cfg.id && handleEdit(cfg.id)}> <Button variant="ghost" size="sm" onClick={() => cfg.id && handleEdit(cfg.id)}>
<Edit3 size={14} /> <Edit3 size={14} />
</Button> </Button>
<Button variant="ghost" size="sm" className="text-red-600 hover:text-red-700" onClick={() => cfg.id && handleDelete(cfg.id)}> <Button variant="ghost" size="sm" className="text-re-red hover:text-re-red-hover" onClick={() => cfg.id && handleDelete(cfg.id)}>
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>
</div> </div>
@ -366,7 +366,7 @@ const InterviewConfigManagement: React.FC = () => {
</DialogDescription> </DialogDescription>
</div> </div>
{editingConfig?.configType === 'KT_MATRIX' && ( {editingConfig?.configType === 'KT_MATRIX' && (
<div className={`px-3 py-1.5 rounded-md text-[10px] font-black uppercase tracking-tight ${totalWeight === 100 ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' : 'bg-amber-50 text-amber-700 border border-amber-100'}`}> <div className={`px-3 py-1.5 rounded-md text-[10px] font-black uppercase tracking-tight ${totalWeight === 100 ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' : 'bg-red-50 text-re-red-hover border border-red-100'}`}>
Weight: {totalWeight}% / 100% Weight: {totalWeight}% / 100%
</div> </div>
)} )}
@ -535,7 +535,7 @@ const InterviewConfigManagement: React.FC = () => {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2"> <div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2"> <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<div className="w-1 h-3 bg-amber-400 rounded-full" /> Selection Choices Profile <div className="w-1 h-3 bg-red-400 rounded-full" /> Selection Choices Profile
</p> </p>
<Button variant="ghost" size="sm" className="h-7 px-3 text-[10px] font-bold uppercase text-slate-600 hover:bg-white border border-transparent hover:border-slate-200" onClick={() => addOption(index)}> <Button variant="ghost" size="sm" className="h-7 px-3 text-[10px] font-bold uppercase text-slate-600 hover:bg-white border border-transparent hover:border-slate-200" onClick={() => addOption(index)}>
<Plus className="w-3 h-3 mr-1.5" /> Append Option <Plus className="w-3 h-3 mr-1.5" /> Append Option

View File

@ -55,7 +55,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
setLocationDistrict(''); setLocationDistrict('');
}} }}
> >
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder="Select state" /> <SelectValue placeholder="Select state" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -71,7 +71,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">City</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">City</Label>
<Input <Input
placeholder="Enter city name" placeholder="Enter city name"
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500" className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-red-500/30 focus-visible:border-red-500"
value={locationCity} value={locationCity}
onChange={(e) => setLocationCity(e.target.value)} onChange={(e) => setLocationCity(e.target.value)}
/> />
@ -79,7 +79,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div> <div>
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label>
<Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}> <Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}>
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder={locationState ? 'Select district' : 'Select state first'} /> <SelectValue placeholder={locationState ? 'Select district' : 'Select state first'} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -120,7 +120,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div> <div>
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Opportunity</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Opportunity</Label>
<Select value={locationStatus} onValueChange={setLocationStatus}> <Select value={locationStatus} onValueChange={setLocationStatus}>
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder="Is this an active Opportunity?" /> <SelectValue placeholder="Is this an active Opportunity?" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -132,7 +132,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white shadow-md hover:shadow-lg transition-all" onClick={onSave}>Save Location</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover text-white shadow-md hover:shadow-lg transition-all" onClick={onSave}>Save Location</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -53,7 +53,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
placeholder="Search locations..." placeholder="Search locations..."
value={searchTerm} value={searchTerm}
onChange={(e) => onSearch(e.target.value)} onChange={(e) => onSearch(e.target.value)}
className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-amber-500 w-64 transition-all" className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-red-500 w-64 transition-all"
/> />
</div> </div>
@ -80,7 +80,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700 whitespace-nowrap"> <Button onClick={onAddLocation} className="bg-re-red hover:bg-re-red-hover whitespace-nowrap">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Location Add Location
</Button> </Button>
@ -112,7 +112,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<TableRow key={district.id}> <TableRow key={district.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-amber-600" /> <MapPin className="w-4 h-4 text-re-red" />
<span className="font-medium">{district.stateName || 'N/A'}</span> <span className="font-medium">{district.stateName || 'N/A'}</span>
</div> </div>
</TableCell> </TableCell>
@ -151,7 +151,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<Button variant="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0"> <Button variant="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0">
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => onDeleteLocation(district.id)} className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"> <Button variant="outline" size="sm" onClick={() => onDeleteLocation(district.id)} className="h-8 w-8 p-0 text-re-red hover:bg-red-50 hover:text-re-red-hover">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
@ -163,7 +163,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
</Table> </Table>
{isAreasLoading && ( {isAreasLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/10 backdrop-blur-[1px]"> <div className="absolute inset-0 flex items-center justify-center bg-white/10 backdrop-blur-[1px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-re-red"></div>
</div> </div>
)} )}
</div> </div>

View File

@ -189,7 +189,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
<div> <div>
<Label>States Covered</Label> <Label>States Covered</Label>
{!selectedRegionZone && ( {!selectedRegionZone && (
<p className="text-xs text-amber-600 mt-1">Select a zone first to see available states</p> <p className="text-xs text-re-red mt-1">Select a zone first to see available states</p>
)} )}
<div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50"> <div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
{statesForZone.length === 0 ? ( {statesForZone.length === 0 ? (
@ -234,7 +234,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
<TooltipProvider> <TooltipProvider>
{districtsByState.map(({ stateName, districts }) => ( {districtsByState.map(({ stateName, districts }) => (
<div key={stateName} className="mb-4 last:mb-0"> <div key={stateName} className="mb-4 last:mb-0">
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2 pb-1 border-b border-slate-200"> <h4 className="text-xs font-semibold text-re-red-hover uppercase tracking-wide mb-2 pb-1 border-b border-slate-200">
{stateName} {stateName}
</h4> </h4>
<div className="space-y-2 ml-1"> <div className="space-y-2 ml-1">
@ -302,7 +302,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
{/* Actions */} {/* Actions */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Regional Office</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Regional Office</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -29,7 +29,7 @@ export const RegionalManagement: React.FC<RegionalManagementProps> = ({
<CardTitle>Regional Offices</CardTitle> <CardTitle>Regional Offices</CardTitle>
<CardDescription>Manage regional offices within zones</CardDescription> <CardDescription>Manage regional offices within zones</CardDescription>
</div> </div>
<Button onClick={onAddRegion} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddRegion} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Regional Office Add Regional Office
</Button> </Button>
@ -114,7 +114,7 @@ export const RegionalManagement: React.FC<RegionalManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditRegion(region)}> <Button variant="ghost" size="sm" onClick={() => onEditRegion(region)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteRegion(region.id, region.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteRegion(region.id, region.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -79,8 +79,8 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
}, },
{ {
title: "Application Stage Access", title: "Application Stage Access",
color: "from-amber-50 to-orange-50 border-amber-200", color: "from-red-50 to-orange-50 border-red-200",
textColor: "text-amber-900", textColor: "text-red-900",
permissions: [ permissions: [
{ id: "stage:initial_review", label: "Initial Review" }, { id: "stage:initial_review", label: "Initial Review" },
{ id: "stage:field_verification", label: "Field Verification" }, { id: "stage:field_verification", label: "Field Verification" },
@ -126,7 +126,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
<div className="space-y-5"> <div className="space-y-5">
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2"> <h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> <span className="w-1.5 h-1.5 bg-red-500 rounded-full"></span>
Configure Default Permissions Configure Default Permissions
</h4> </h4>

View File

@ -22,7 +22,7 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
<CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle> <CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle>
<CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription> <CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription>
</div> </div>
<Button onClick={onAddRole} className="bg-amber-600 hover:bg-amber-700 h-9"> <Button onClick={onAddRole} className="bg-re-red hover:bg-re-red-hover h-9">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Role Add Role
</Button> </Button>
@ -34,7 +34,7 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
<div key={role.id} className="border rounded-lg p-4 space-y-3 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow"> <div key={role.id} className="border rounded-lg p-4 space-y-3 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-amber-600" /> <Shield className="w-5 h-5 text-re-red" />
<h3 className="text-slate-900 font-bold">{role.name}</h3> <h3 className="text-slate-900 font-bold">{role.name}</h3>
</div> </div>
<Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium"> <Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium">

View File

@ -29,7 +29,7 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
<div key={sla.id} className="border rounded-lg p-4 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow"> <div key={sla.id} className="border rounded-lg p-4 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-amber-600" /> <Clock className="w-5 h-5 text-re-red" />
<div> <div>
<h4 className="text-slate-900 font-medium">{sla.activityName}</h4> <h4 className="text-slate-900 font-medium">{sla.activityName}</h4>
<p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p> <p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p>
@ -73,7 +73,7 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
<div className="border-l-2 border-red-400 pl-3"> <div className="border-l-2 border-red-400 pl-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-600" /> <AlertTriangle className="w-4 h-4 text-re-red" />
<span className="text-xs text-slate-700">Escalations ({sla.escalationConfigs?.length || 0})</span> <span className="text-xs text-slate-700">Escalations ({sla.escalationConfigs?.length || 0})</span>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">

View File

@ -184,6 +184,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<div className="flex items-center space-x-2 pt-8"> <div className="flex items-center space-x-2 pt-8">
<Switch <Switch
id="isActive" id="isActive"
className="data-[state=checked]:bg-re-red"
checked={formData.isActive} checked={formData.isActive}
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })} onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
/> />
@ -252,7 +253,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between border-b pb-2"> <div className="flex items-center justify-between border-b pb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-red-600" /> <AlertTriangle className="w-4 h-4 text-re-red" />
<h4 className="font-medium text-sm">Escalation Levels</h4> <h4 className="font-medium text-sm">Escalation Levels</h4>
</div> </div>
<Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs"> <Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs">
@ -264,7 +265,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
{formData.escalationConfigs.map((esc: any, idx: number) => ( {formData.escalationConfigs.map((esc: any, idx: number) => (
<div key={idx} className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3"> <div key={idx} className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-100"> <Badge variant="outline" className="bg-red-50 text-re-red-hover border-red-100">
Level {esc.level} Level {esc.level}
</Badge> </Badge>
<Button <Button
@ -345,7 +346,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<Button type="button" variant="outline" onClick={onClose} disabled={loading}> <Button type="button" variant="outline" onClick={onClose} disabled={loading}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading}> <Button type="submit" disabled={loading} className="bg-re-red hover:bg-re-red-hover text-white">
{loading ? 'Saving...' : 'Save Configuration'} {loading ? 'Saving...' : 'Save Configuration'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -54,7 +54,7 @@ export const SecurityDepositMaster: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center p-20 space-y-4"> <div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-10 h-10 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div> <div className="w-10 h-10 border-4 border-re-red border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 animate-pulse">Loading settings...</p> <p className="text-slate-600 animate-pulse">Loading settings...</p>
</div> </div>
); );
@ -65,8 +65,8 @@ export const SecurityDepositMaster: React.FC = () => {
<Card className="border-none shadow-lg bg-white/80 backdrop-blur-md"> <Card className="border-none shadow-lg bg-white/80 backdrop-blur-md">
<CardHeader className="py-4 border-b bg-slate-50/50"> <CardHeader className="py-4 border-b bg-slate-50/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-red-100 flex items-center justify-center">
<Settings className="w-5 h-5 text-amber-600" /> <Settings className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<CardTitle className="text-lg font-bold text-slate-900"> <CardTitle className="text-lg font-bold text-slate-900">
@ -98,7 +98,7 @@ export const SecurityDepositMaster: React.FC = () => {
<Input <Input
id={`amount-${config.id}`} id={`amount-${config.id}`}
type="number" type="number"
className="pl-8 h-9 text-base font-bold bg-slate-50/50 border-slate-200 focus:ring-amber-500 focus:border-amber-500 rounded-lg" className="pl-8 h-9 text-base font-bold bg-slate-50/50 border-slate-200 focus:ring-red-500 focus:border-red-500 rounded-lg"
value={config.value?.amount || ''} value={config.value?.amount || ''}
onChange={(e) => handleUpdateAmount(config.id, e.target.value)} onChange={(e) => handleUpdateAmount(config.id, e.target.value)}
/> />
@ -109,7 +109,7 @@ export const SecurityDepositMaster: React.FC = () => {
<Button <Button
onClick={() => handleSave(config)} onClick={() => handleSave(config)}
disabled={isSaving === config.id} disabled={isSaving === config.id}
className="h-9 px-4 bg-amber-600 hover:bg-amber-700 text-white rounded-lg shadow-md shadow-amber-600/10 active:scale-95 transition-all flex items-center gap-1.5 group" className="h-9 px-4 bg-re-red hover:bg-re-red-hover text-white rounded-lg shadow-md shadow-re-red/10 active:scale-95 transition-all flex items-center gap-1.5 group"
> >
{isSaving === config.id ? ( {isSaving === config.id ? (
<RefreshCw className="w-4 h-4 animate-spin" /> <RefreshCw className="w-4 h-4 animate-spin" />
@ -140,13 +140,13 @@ export const SecurityDepositMaster: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
<div className="bg-amber-50/50 rounded-xl p-4 border border-amber-100/50 flex items-start gap-3"> <div className="bg-red-50/50 rounded-xl p-4 border border-red-100/50 flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
<Settings className="w-4 h-4 text-amber-700" /> <Settings className="w-4 h-4 text-re-red-hover" />
</div> </div>
<div> <div>
<h5 className="font-bold text-amber-900 text-sm">Super Admin Notice</h5> <h5 className="font-bold text-red-900 text-sm">Super Admin Notice</h5>
<p className="text-[11px] text-amber-800/80 leading-snug"> <p className="text-[11px] text-red-800/80 leading-snug">
Updates made here take immediate effect. These values define the default expected amounts for all current and future onboarding payments. Updates made here take immediate effect. These values define the default expected amounts for all current and future onboarding payments.
</p> </p>
</div> </div>

View File

@ -96,7 +96,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200 space-y-4"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-200 space-y-4">
<h3 className="text-sm font-semibold text-slate-800 flex items-center gap-2"> <h3 className="text-sm font-semibold text-slate-800 flex items-center gap-2">
<Settings className="w-4 h-4 text-amber-600" /> <Settings className="w-4 h-4 text-re-red" />
General Settings General Settings
</h3> </h3>
<div> <div>
@ -149,12 +149,12 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</div> </div>
</div> </div>
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100"> <div className="bg-red-50 p-4 rounded-lg border border-red-100">
<h3 className="text-sm font-semibold text-amber-900 flex items-center gap-2 mb-3"> <h3 className="text-sm font-semibold text-red-900 flex items-center gap-2 mb-3">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />
Available Placeholders Available Placeholders
</h3> </h3>
<p className="text-[10px] text-amber-700 mb-4 leading-relaxed"> <p className="text-[10px] text-re-red-hover mb-4 leading-relaxed">
Click a placeholder to insert it at the cursor. Click a placeholder to insert it at the cursor.
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -164,14 +164,14 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
key={p} key={p}
type="button" type="button"
onClick={() => insertPlaceholder(p)} onClick={() => insertPlaceholder(p)}
className="px-2 py-1 bg-white border border-amber-200 rounded text-[11px] font-mono text-amber-800 hover:bg-amber-600 hover:text-white hover:border-amber-600 transition-all flex items-center gap-1 shadow-sm" className="px-2 py-1 bg-white border border-red-200 rounded text-[11px] font-mono text-red-800 hover:bg-re-red hover:text-white hover:border-re-red transition-all flex items-center gap-1 shadow-sm"
> >
{`{{${p}}}`} {`{{${p}}}`}
</button> </button>
)) ))
) : ( ) : (
<div className="w-full py-4 text-center border-2 border-dashed border-amber-200 rounded-lg"> <div className="w-full py-4 text-center border-2 border-dashed border-red-200 rounded-lg">
<p className="text-[10px] text-amber-600">No placeholders defined for this trigger</p> <p className="text-[10px] text-re-red">No placeholders defined for this trigger</p>
</div> </div>
)} )}
</div> </div>
@ -182,7 +182,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="lg:col-span-4 space-y-4 flex flex-col h-full min-h-[600px]"> <div className="lg:col-span-4 space-y-4 flex flex-col h-full min-h-[600px]">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="font-semibold text-slate-900 flex items-center gap-2"> <h3 className="font-semibold text-slate-900 flex items-center gap-2">
<Edit2 className="w-4 h-4 text-amber-600" /> <Edit2 className="w-4 h-4 text-re-red" />
Template Designer Template Designer
</h3> </h3>
</div> </div>
@ -230,7 +230,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
'{"applicantName": "Rajesh Kumar", "location": "Mumbai South", "applicationId": "APP-2026-X12", "link": "https://example.com/app", "portalLink": "https://example.com/app"}' '{"applicantName": "Rajesh Kumar", "location": "Mumbai South", "applicationId": "APP-2026-X12", "link": "https://example.com/app", "portalLink": "https://example.com/app"}'
) )
} }
className="text-[10px] text-amber-600 hover:underline" className="text-[10px] text-re-red hover:underline"
> >
Reset to Sample Reset to Sample
</button> </button>
@ -302,7 +302,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-amber-600 hover:bg-amber-700" className="flex-1 bg-re-red hover:bg-re-red-hover"
type="button" type="button"
onClick={() => handleSaveTemplate(composeFullBody())} onClick={() => handleSaveTemplate(composeFullBody())}
disabled={!editingTemplate?.id || !editingTemplate?.templateCode?.trim()} disabled={!editingTemplate?.id || !editingTemplate?.templateCode?.trim()}

View File

@ -44,7 +44,7 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-3 h-3 text-amber-600" /> <Shield className="w-3 h-3 text-re-red" />
<span className="text-sm font-medium">{user.role}</span> <span className="text-sm font-medium">{user.role}</span>
</div> </div>
</TableCell> </TableCell>

View File

@ -146,7 +146,7 @@ export const ZMDialog: React.FC<ZMDialogProps> = ({
<div className="flex gap-3 pt-6"> <div className="flex gap-3 pt-6">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Zonal Manager</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Zonal Manager</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -33,7 +33,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<CardTitle>Zonal Managers (DD-ZM)</CardTitle> <CardTitle>Zonal Managers (DD-ZM)</CardTitle>
<CardDescription>Manage Zonal Managers and their region assignments</CardDescription> <CardDescription>Manage Zonal Managers and their region assignments</CardDescription>
</div> </div>
<Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddZM} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add ZM Add ZM
</Button> </Button>
@ -99,7 +99,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditZM(zm)}> <Button variant="ghost" size="sm" onClick={() => onEditZM(zm)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteZM(zm.id, zm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteZM(zm.id, zm.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -27,7 +27,7 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<CardTitle>Zone Details</CardTitle> <CardTitle>Zone Details</CardTitle>
<CardDescription>Geographical coverage and state mappings for each zone</CardDescription> <CardDescription>Geographical coverage and state mappings for each zone</CardDescription>
</div> </div>
<Button onClick={onAddZone} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddZone} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Zone Add Zone
</Button> </Button>
@ -40,7 +40,7 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<div key={zone.id} className="border rounded-lg p-5 space-y-4 bg-gradient-to-br from-white to-slate-50"> <div key={zone.id} className="border rounded-lg p-5 space-y-4 bg-gradient-to-br from-white to-slate-50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-lg flex items-center justify-center shadow-md"> <div className="w-12 h-12 bg-gradient-to-br from-red-500 to-re-red rounded-lg flex items-center justify-center shadow-md">
<Globe className="w-6 h-6 text-white" /> <Globe className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
@ -86,11 +86,11 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<Label className="text-xs text-slate-600 mb-2 block"> <Label className="text-xs text-slate-600 mb-2 block">
Zonal Business Head (ZBH) Zonal Business Head (ZBH)
</Label> </Label>
<div className="bg-amber-50 border border-amber-100 rounded-lg p-3 space-y-2"> <div className="bg-red-50 border border-red-100 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-amber-600" /> <Shield className="w-4 h-4 text-re-red" />
<span className="text-sm font-semibold text-slate-900">{zone.zonalBusinessHead.name}</span> <span className="text-sm font-semibold text-slate-900">{zone.zonalBusinessHead.name}</span>
<Badge className="bg-amber-600 text-white text-[10px] ml-auto">ZBH</Badge> <Badge className="bg-re-red text-white text-[10px] ml-auto">ZBH</Badge>
</div> </div>
<div className="flex items-center gap-2 ml-6 text-slate-600"> <div className="flex items-center gap-2 ml-6 text-slate-600">

View File

@ -106,7 +106,7 @@ export const ZoneDialog: React.FC<ZoneDialogProps> = ({
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Zone</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Zone</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -19,14 +19,14 @@ export const ZonesOverview: React.FC<ZonesOverviewProps> = ({ selectedZone, onZo
return ( return (
<Card <Card
key={zone.id} key={zone.id}
className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-amber-600 shadow-lg' : 'hover:border-amber-400' className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-re-red shadow-lg' : 'hover:border-red-400'
}`} }`}
onClick={() => onZoneClick(zone.id)} onClick={() => onZoneClick(zone.id)}
> >
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-amber-600" /> <Globe className="w-5 h-5 text-re-red" />
<CardTitle className="text-lg">{zone.name.toUpperCase().endsWith('ZONE') ? zone.name : `${zone.name} Zone`}</CardTitle> <CardTitle className="text-lg">{zone.name.toUpperCase().endsWith('ZONE') ? zone.name : `${zone.name} Zone`}</CardTitle>
</div> </div>
<Badge variant="outline" className="text-xs">{zone.code}</Badge> <Badge variant="outline" className="text-xs">{zone.code}</Badge>

View File

@ -4,7 +4,6 @@ import {
Tabs, TabsContent, TabsList, TabsTrigger Tabs, TabsContent, TabsList, TabsTrigger
} from '@/components/ui/tabs'; } from '@/components/ui/tabs';
import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react'; import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner'; import { toast } from 'sonner';
// Services & Hooks // Services & Hooks
@ -438,39 +437,38 @@ export const MasterPage: React.FC = () => {
<h1 className="text-slate-900 mb-2 font-bold text-2xl">Master Configuration</h1> <h1 className="text-slate-900 mb-2 font-bold text-2xl">Master Configuration</h1>
<p className="text-slate-600">Centralized governance for locations, roles, and operational policies</p> <p className="text-slate-600">Centralized governance for locations, roles, and operational policies</p>
</div> </div>
<Badge className="bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-1">Admin Control Panel</Badge>
</div> </div>
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center p-20 space-y-4"> <div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-12 h-12 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div> <div className="w-12 h-12 border-4 border-re-red border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 font-medium animate-pulse">Synchronizing Global Settings...</p> <p className="text-slate-600 font-medium animate-pulse">Synchronizing Global Settings...</p>
</div> </div>
) : ( ) : (
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-8 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1"> <TabsList className="grid w-full grid-cols-8 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1">
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Globe className="w-4 h-4" /> Organisation <Globe className="w-4 h-4" /> Organisation
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="roles" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="roles" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Shield className="w-4 h-4" /> Roles <Shield className="w-4 h-4" /> Roles
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="templates" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="templates" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Mail className="w-4 h-4" /> Emails <Mail className="w-4 h-4" /> Emails
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<MapPin className="w-4 h-4" /> Locations <MapPin className="w-4 h-4" /> Locations
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<SlidersHorizontal className="w-4 h-4" /> Approvals <SlidersHorizontal className="w-4 h-4" /> Approvals
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="documents" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="documents" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<FileText className="w-4 h-4" /> Docs Config <FileText className="w-4 h-4" /> Docs Config
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="governance" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="governance" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<Settings2 className="w-4 h-4" /> Governance <Settings2 className="w-4 h-4" /> Governance
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<Settings className="w-4 h-4" /> App Settings <Settings className="w-4 h-4" /> App Settings
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@ -600,9 +598,9 @@ export const MasterPage: React.FC = () => {
)} )}
{/* Main Dialogs */} {/* Main Dialogs */}
<ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> <ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} />
<RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} regionDescription={regionDescription} setRegionDescription={setRegionDescription} selectedRegionZone={selectedRegionZone} setSelectedRegionZone={setSelectedRegionZone} regionalManagerId={regionalManagerId} setRegionalManagerId={setRegionalManagerId} selectedRegionStates={selectedRegionDistricts} setSelectedRegionStates={setSelectedRegionDistricts} onSave={handleSaveRegion} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> <RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} regionDescription={regionDescription} setRegionDescription={setRegionDescription} selectedRegionZone={selectedRegionZone} setSelectedRegionZone={setSelectedRegionZone} regionalManagerId={regionalManagerId} setRegionalManagerId={setRegionalManagerId} selectedRegionStates={selectedRegionDistricts} setSelectedRegionStates={setSelectedRegionDistricts} onSave={handleSaveRegion} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} />
<ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} asmRoleCode={asmRoleCode} setAsmRoleCode={setAsmRoleCode} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} districtsAssignedToOthers={districtsAssignedToOthers} getDistrictsForSelectedState={(state) => getDistrictsForSelectedState(state, selectedASMRegion || undefined)} /> <ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} asmRoleCode={asmRoleCode} setAsmRoleCode={setAsmRoleCode} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} districtsAssignedToOthers={districtsAssignedToOthers} getDistrictsForSelectedState={(state) => getDistrictsForSelectedState(state, selectedASMRegion || undefined)} />
<ZMDialog <ZMDialog
isOpen={showZMDialog} isOpen={showZMDialog}
onOpenChange={setShowZMDialog} onOpenChange={setShowZMDialog}
@ -616,7 +614,7 @@ export const MasterPage: React.FC = () => {
selectedRegions={selectedZMRegions} selectedRegions={selectedZMRegions}
setSelectedRegions={setSelectedZMRegions} setSelectedRegions={setSelectedZMRegions}
onSave={handleSaveZM} onSave={handleSaveZM}
userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms}
/> />
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} /> <TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} /> <LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} />

View File

@ -73,7 +73,7 @@ export const SLAConfigPage: React.FC = () => {
<RefreshCw className={`w-4 h-4 mr-2 ${loadingMore ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loadingMore ? 'animate-spin' : ''}`} />
Initialize Defaults Initialize Defaults
</Button> </Button>
<Button onClick={handleAddSLA} disabled={loading}> <Button onClick={handleAddSLA} disabled={loading} className="bg-re-red hover:bg-re-red-hover text-white">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Manual SLA Add Manual SLA
</Button> </Button>
@ -136,14 +136,14 @@ export const SLAConfigPage: React.FC = () => {
<div className="border-l-2 border-red-400 pl-3"> <div className="border-l-2 border-red-400 pl-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-600" /> <AlertTriangle className="w-4 h-4 text-re-red" />
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Escalations ({sla.escalationConfigs?.length || 0})</span> <span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Escalations ({sla.escalationConfigs?.length || 0})</span>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
{(sla.escalationConfigs || []).map((esc: any, idx: number) => ( {(sla.escalationConfigs || []).map((esc: any, idx: number) => (
<div key={idx} className="text-[11px]"> <div key={idx} className="text-[11px]">
<div className="flex items-center gap-1.5 text-slate-900 font-medium"> <div className="flex items-center gap-1.5 text-slate-900 font-medium">
<Badge variant="outline" className="text-[9px] h-4 px-1 bg-red-50 border-red-200 text-red-700"> <Badge variant="outline" className="text-[9px] h-4 px-1 bg-red-50 border-red-200 text-re-red-hover">
L{esc.level} L{esc.level}
</Badge> </Badge>
<span>after {esc.timeValue} {esc.timeUnit}</span> <span>after {esc.timeValue} {esc.timeUnit}</span>

View File

@ -35,6 +35,8 @@ interface ApplicationDetailsActionModalsProps {
setInterviewIdToCancel: (value: string) => void; setInterviewIdToCancel: (value: string) => void;
isCancellingInterview: boolean; isCancellingInterview: boolean;
handleConfirmCancelInterview: () => void; handleConfirmCancelInterview: () => void;
interviewToReschedule: any;
setInterviewToReschedule: (value: any) => void;
interviewType: string; interviewType: string;
setInterviewType: (value: string) => void; setInterviewType: (value: string) => void;
interviewMode: string; interviewMode: string;
@ -99,6 +101,8 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
setInterviewIdToCancel, setInterviewIdToCancel,
isCancellingInterview, isCancellingInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
interviewToReschedule,
setInterviewToReschedule,
interviewType, interviewType,
setInterviewType, setInterviewType,
interviewMode, interviewMode,
@ -252,10 +256,13 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal}> <Dialog open={showScheduleModal} onOpenChange={(open) => {
setShowScheduleModal(open);
if (!open) setInterviewToReschedule(null);
}}>
<DialogContent data-testid="onboarding-schedule-modal"> <DialogContent data-testid="onboarding-schedule-modal">
<DialogHeader> <DialogHeader>
<DialogTitle>Schedule Interview</DialogTitle> <DialogTitle>{interviewToReschedule ? 'Reschedule Interview' : 'Schedule Interview'}</DialogTitle>
<DialogDescription>Set up an interview session with the applicant and relevant team members.</DialogDescription> <DialogDescription>Set up an interview session with the applicant and relevant team members.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
@ -305,8 +312,13 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
)} )}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowScheduleModal(false)} disabled={isScheduling} data-testid="onboarding-schedule-cancel-button">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => {
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleScheduleInterview} disabled={isScheduling} data-testid="onboarding-schedule-submit-button">{isScheduling ? 'Scheduling...' : 'Schedule'}</Button> setShowScheduleModal(false);
setInterviewToReschedule(null);
}} disabled={isScheduling} data-testid="onboarding-schedule-cancel-button">Cancel</Button>
<Button className="flex-1 bg-primary-600 hover:bg-primary-700" onClick={handleScheduleInterview} disabled={isScheduling} data-testid="onboarding-schedule-submit-button">
{isScheduling ? (interviewToReschedule ? 'Rescheduling...' : 'Scheduling...') : (interviewToReschedule ? 'Reschedule' : 'Schedule')}
</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -157,10 +157,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</div> </div>
))} ))}
<div className="space-y-2 border-t border-border pt-6"> <div className="space-y-2 border-t border-border pt-6">
<Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Notes <span className="font-normal text-muted-foreground">(optional)</span></Label> <Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Remarks <span className="text-red-500">*</span></Label>
<Textarea <Textarea
id="kt-matrix-remarks" id="kt-matrix-remarks"
placeholder="Optional remarks…" placeholder="Enter remarks..."
className="min-h-[96px] resize-y text-sm leading-relaxed" className="min-h-[96px] resize-y text-sm leading-relaxed"
value={ktMatrixRemarks} value={ktMatrixRemarks}
onChange={(e) => setKtMatrixRemarks(e.target.value)} onChange={(e) => setKtMatrixRemarks(e.target.value)}
@ -186,7 +186,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p> <p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p>
<div className="flex gap-2 sm:shrink-0"> <div className="flex gap-2 sm:shrink-0">
<Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button> <Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button>
<Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || ktCriteria.length === 0 || Object.keys(ktMatrixSelectedValues).length < ktCriteria.length} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button> <Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || ktCriteria.length === 0 || Object.keys(ktMatrixSelectedValues).length < ktCriteria.length || !ktMatrixRemarks?.trim()} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@ -208,19 +208,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent> <SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
</Select> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level2Recommendation} onValueChange={setLevel2Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level2-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l2Fields.length === 0 && ( {l2Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
@ -255,6 +243,19 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
)} )}
</div> </div>
))} ))}
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level2Recommendation} onValueChange={setLevel2Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level2-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2 || l2Fields.length === 0} data-testid="onboarding-level2-feedback-submit">{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}</Button> <Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2 || l2Fields.length === 0} data-testid="onboarding-level2-feedback-submit">{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}</Button>
@ -311,19 +312,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent> <SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
</Select> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level3Recommendation} onValueChange={setLevel3Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level3-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l3Fields.length === 0 && ( {l3Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
@ -358,6 +347,19 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
)} )}
</div> </div>
))} ))}
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level3Recommendation} onValueChange={setLevel3Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level3-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3 || l3Fields.length === 0} data-testid="onboarding-level3-feedback-submit">{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}</Button> <Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3 || l3Fields.length === 0} data-testid="onboarding-level3-feedback-submit">{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}</Button>
@ -375,7 +377,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
{!showUploadForm ? ( {!showUploadForm ? (
<div className="flex-1 flex flex-col min-h-0 space-y-4"> <div className="flex-1 flex flex-col min-h-0 space-y-4">
{getDocumentsForStage(selectedStage || '').length > 0 ? ( {getDocumentsForStage(selectedStage || '').length > 0 ? (
<div className="flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container"> <div className="custom-scrollbar-x-slim flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container">
<Table className="w-full table-auto"> <Table className="w-full table-auto">
<TableHeader className="bg-slate-50/80 sticky top-0 z-10"> <TableHeader className="bg-slate-50/80 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-b"> <TableRow className="hover:bg-transparent border-b">

View File

@ -9,6 +9,7 @@ import {
Clock, Clock,
ClipboardList, ClipboardList,
Download, Download,
Eye,
FileText, FileText,
GitBranch, GitBranch,
Lock, Lock,
@ -49,7 +50,7 @@ interface ApplicationDetailsTabsProps {
setShowDocumentsModal: (value: boolean) => void; setShowDocumentsModal: (value: boolean) => void;
setShowUploadForm: (value: boolean) => void; setShowUploadForm: (value: boolean) => void;
handleRetriggerEvaluators: () => void; handleRetriggerEvaluators: () => void;
handleCancelInterview: (interviewId: any) => void; handleRescheduleInterview: (interview: any) => void;
setSelectedEvaluationForView: (value: any) => void; setSelectedEvaluationForView: (value: any) => void;
setShowFeedbackDetailsModal: (value: boolean) => void; setShowFeedbackDetailsModal: (value: boolean) => void;
renderFddAuditContent: () => React.ReactNode; renderFddAuditContent: () => React.ReactNode;
@ -84,7 +85,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
setShowDocumentsModal, setShowDocumentsModal,
setShowUploadForm, setShowUploadForm,
handleRetriggerEvaluators, handleRetriggerEvaluators,
handleCancelInterview, handleRescheduleInterview,
setSelectedEvaluationForView, setSelectedEvaluationForView,
setShowFeedbackDetailsModal, setShowFeedbackDetailsModal,
renderFddAuditContent, renderFddAuditContent,
@ -488,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>
</> </>
@ -556,11 +557,17 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
</TableCell> </TableCell>
<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={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}>
<Eye className="w-3 h-3 text-slate-500" />
</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" /> <Download className="w-3 h-3 text-slate-500" />
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
@ -620,11 +627,11 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-8 px-2" className="text-primary-600 hover:text-primary-700 hover:bg-primary-50 h-8 px-2"
data-testid={`onboarding-interview-cancel-${idx}`} data-testid={`onboarding-interview-reschedule-${idx}`}
onClick={() => handleCancelInterview(interview.id)} onClick={() => handleRescheduleInterview(interview)}
> >
Cancel Reschedule
</Button> </Button>
)} )}
</TableCell> </TableCell>

View File

@ -17,10 +17,15 @@ interface UseApplicationDetailsAdminActionsParams {
participantType: string; participantType: string;
users: any[]; users: any[];
interviewDate: string; interviewDate: string;
setInterviewDate: Dispatch<SetStateAction<string>>;
interviewType: string; interviewType: string;
setInterviewType: Dispatch<SetStateAction<string>>;
interviewMode: string; interviewMode: string;
setInterviewMode: Dispatch<SetStateAction<string>>;
meetingLink: string; meetingLink: string;
setMeetingLink: Dispatch<SetStateAction<string>>;
location: string; location: string;
setLocation: Dispatch<SetStateAction<string>>;
scheduledInterviewParticipants: any[]; scheduledInterviewParticipants: any[];
uploadFile: File | null; uploadFile: File | null;
uploadDocType: string; uploadDocType: string;
@ -45,6 +50,8 @@ interface UseApplicationDetailsAdminActionsParams {
setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>; setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>;
interviewIdToCancel: string; interviewIdToCancel: string;
setInterviewIdToCancel: Dispatch<SetStateAction<string>>; setInterviewIdToCancel: Dispatch<SetStateAction<string>>;
interviewToReschedule: any;
setInterviewToReschedule: Dispatch<SetStateAction<any>>;
setIsCancellingInterview: Dispatch<SetStateAction<boolean>>; setIsCancellingInterview: Dispatch<SetStateAction<boolean>>;
setIsUploading: Dispatch<SetStateAction<boolean>>; setIsUploading: Dispatch<SetStateAction<boolean>>;
setShowUploadForm: Dispatch<SetStateAction<boolean>>; setShowUploadForm: Dispatch<SetStateAction<boolean>>;
@ -79,10 +86,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
participantType, participantType,
users, users,
interviewDate, interviewDate,
setInterviewDate,
interviewType, interviewType,
setInterviewType,
interviewMode, interviewMode,
setInterviewMode,
meetingLink, meetingLink,
setMeetingLink,
location, location,
setLocation,
scheduledInterviewParticipants, scheduledInterviewParticipants,
uploadFile, uploadFile,
uploadDocType, uploadDocType,
@ -107,6 +119,8 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setShowCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, interviewIdToCancel,
setInterviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview, setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
@ -176,7 +190,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
}, [currentUser, application, setUsers]); }, [currentUser, application, setUsers]);
const prefillInterviewParticipants = useCallback(() => { const prefillInterviewParticipants = useCallback(() => {
if (!showScheduleModal || !application) return; if (!showScheduleModal || !application || interviewToReschedule) return;
const levelNum = parseInt(interviewType.replace('level', '')) || 1; const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const requiredRolesByLevel: Record<number, string[]> = { const requiredRolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'], 1: ['DD-ZM', 'RBM'],
@ -233,7 +247,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
}); });
setScheduledInterviewParticipants(unique); setScheduledInterviewParticipants(unique);
}, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]); }, [showScheduleModal, application, interviewType, interviewToReschedule, setScheduledInterviewParticipants]);
const handleScheduleInterview = async () => { const handleScheduleInterview = async () => {
if (!interviewDate) { if (!interviewDate) {
@ -242,20 +256,32 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
try { try {
setIsScheduling(true); setIsScheduling(true);
await onboardingService.scheduleInterview({ const payload = {
applicationId: application?.id, applicationId: application?.id,
level: interviewType, level: interviewType,
scheduledAt: interviewDate, scheduledAt: interviewDate,
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'virtual' ? meetingLink : location, location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map((p) => p.id), participants: scheduledInterviewParticipants.map((p) => p.id),
};
if (interviewToReschedule) {
await onboardingService.updateInterview(interviewToReschedule.id, {
...payload,
status: 'Scheduled',
}); });
toast.success('Interview rescheduled successfully');
} else {
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully'); toast.success('Interview scheduled successfully');
}
setShowScheduleModal(false); setShowScheduleModal(false);
setInterviewToReschedule(null);
await fetchInterviews(); await fetchInterviews();
await fetchApplication(); await fetchApplication();
} catch { } catch {
toast.error('Failed to schedule interview'); toast.error(interviewToReschedule ? 'Failed to reschedule interview' : 'Failed to schedule interview');
} finally { } finally {
setIsScheduling(false); setIsScheduling(false);
} }
@ -266,6 +292,24 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setShowCancelInterviewModal(true); setShowCancelInterviewModal(true);
}; };
const handleRescheduleInterview = async (interview: any) => {
setInterviewToReschedule(interview);
setInterviewType(`level${interview.level}`);
setInterviewMode(interview.interviewType?.toLowerCase().includes('virtual') ? 'virtual' : 'physical');
setInterviewDate(interview.scheduleDate ? (() => {
const d = new Date(interview.scheduleDate);
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
})() : '');
if (interview.interviewType?.toLowerCase().includes('virtual')) {
setMeetingLink(interview.linkOrLocation || '');
} else {
setLocation(interview.linkOrLocation || '');
}
const participants = (interview.participants || []).map((p: any) => p.user || p).filter(Boolean);
setScheduledInterviewParticipants(participants);
setShowScheduleModal(true);
};
const handleConfirmCancelInterview = async () => { const handleConfirmCancelInterview = async () => {
if (!interviewIdToCancel) return; if (!interviewIdToCancel) return;
try { try {
@ -606,6 +650,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
fetchUsers, fetchUsers,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,

View File

@ -6,6 +6,106 @@ interface UseApplicationDetailsPermissionsParams {
eorProgress: number; eorProgress: number;
} }
function sameUserId(a: unknown, b: unknown): boolean {
if (a == null || b == null) return false;
return String(a).trim() === String(b).trim();
}
/** Backend / DB may use different casing (e.g. default `scheduled` vs created `Scheduled`). */
function normalizeInterviewStatus(status: unknown): string {
return String(status ?? '')
.trim()
.toLowerCase()
.replace(/\s+/g, ' ');
}
function isActiveInterviewStatus(status: unknown): boolean {
const n = normalizeInterviewStatus(status);
if (!n) return false;
return (
n === 'scheduled' ||
n === 'rescheduled' ||
n === 'pending' ||
n === 'in progress' ||
n === 'inprogress'
);
}
function isCompletedInterviewStatus(status: unknown): boolean {
return normalizeInterviewStatus(status) === 'completed';
}
function userIsInterviewParticipant(interview: any, userId: unknown): boolean {
if (!userId || !interview?.participants?.length) return false;
return interview.participants.some(
(p: any) => sameUserId(p.userId, userId) || sameUserId(p.user?.id, userId),
);
}
/** Which interview level the application is currently in (for feedback UI). */
function inferInterviewLevelFromApplicationStatus(status: unknown): number | undefined {
const s = String(status ?? '').trim();
const map: Record<string, number> = {
'Level 1 Interview Pending': 1,
'Level 1 Recommended': 1,
'Level 2 Interview Pending': 2,
'Level 2 Recommended': 2,
'Level 3 Interview Pending': 3,
'Level 3 Recommended': 3,
};
return map[s];
}
function normalizeRoleToken(value: unknown): string {
return String(value ?? '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, '');
}
/**
* Allow feedback when scheduling forgot to attach this user as InterviewParticipant but they are a
* designated evaluator for this level (matches backend interview policy / schedule prefill roles).
*/
function userRoleEligibleForInterviewLevel(user: any, level: number): boolean {
if (!user) return false;
const privilegedRoles = ['Super Admin', 'DD Admin'];
if (
privilegedRoles.includes(String(user.role ?? '')) ||
privilegedRoles.includes(String(user.roleCode ?? ''))
) {
return true;
}
const rolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'DD ZM', 'RBM'],
2: ['DD Lead', 'ZBH'],
3: ['NBH', 'DD Head'],
};
const allowedRaw = rolesByLevel[level];
if (!allowedRaw?.length) return false;
const allowed = allowedRaw.map(normalizeRoleToken);
const candidates = [
user.role,
user.roleCode,
user.roleName,
user.role?.roleCode,
user.role?.roleName,
]
.filter(Boolean)
.map(normalizeRoleToken);
for (const c of candidates) {
if (!c) continue;
for (const a of allowed) {
if (!a) continue;
if (c === a || c.includes(a) || a.includes(c)) return true;
}
}
return false;
}
export function useApplicationDetailsPermissions({ export function useApplicationDetailsPermissions({
application, application,
interviews, interviews,
@ -15,24 +115,46 @@ export function useApplicationDetailsPermissions({
}: UseApplicationDetailsPermissionsParams) { }: UseApplicationDetailsPermissionsParams) {
const interviewsList = Array.isArray(interviews) ? interviews : []; const interviewsList = Array.isArray(interviews) ? interviews : [];
const activeInterviewForUser = interviewsList.find((i: any) => const stageInterviewLevel = inferInterviewLevelFromApplicationStatus(application?.status);
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
i.participants?.some((p: any) => p.userId === currentUser?.id) /** Prefer participant match on the interview row that matches current application stage when possible. */
const participantActiveInterview =
(stageInterviewLevel != null
? interviewsList.find(
(i: any) =>
isActiveInterviewStatus(i.status) &&
userIsInterviewParticipant(i, currentUser?.id) &&
Number(i.level) === stageInterviewLevel,
)
: undefined) ??
interviewsList.find(
(i: any) => isActiveInterviewStatus(i.status) && userIsInterviewParticipant(i, currentUser?.id),
); );
const lastInterviewForUser = [...interviewsList].reverse().find((i: any) => /** Same stage + active interview + evaluator role — covers missing / partial participant rows. */
i.participants?.some((p: any) => p.userId === currentUser?.id) const roleFallbackActiveInterview =
); stageInterviewLevel != null &&
currentUser &&
userRoleEligibleForInterviewLevel(currentUser, stageInterviewLevel)
? interviewsList.find(
(i: any) =>
Number(i.level) === stageInterviewLevel && isActiveInterviewStatus(i.status),
)
: undefined;
const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( const activeInterviewForUser = participantActiveInterview ?? roleFallbackActiveInterview;
(e: any) => e.evaluatorId === currentUser?.id
); const lastInterviewForUser = interviewsList.find((i: any) => userIsInterviewParticipant(i, currentUser?.id));
const currentUserEvaluation =
activeInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id)) ??
lastInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id));
const isInterviewCompleted = (level: number) => const isInterviewCompleted = (level: number) =>
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Completed'); interviewsList.some((i: any) => Number(i.level) === level && isCompletedInterviewStatus(i.status));
const isInterviewActive = (level: number) => const isInterviewActive = (level: number) =>
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Scheduled'); interviewsList.some((i: any) => Number(i.level) === level && isActiveInterviewStatus(i.status));
const hasSubmittedFeedback = !!currentUserEvaluation; const hasSubmittedFeedback = !!currentUserEvaluation;

View File

@ -6,14 +6,16 @@ interface UseApplicationDetailsStageDataParams {
interviews: any[]; interviews: any[];
eorData: any; eorData: any;
getDeposit: (type: string) => any; getDeposit: (type: string) => any;
documentConfigs?: any[];
} }
export function useApplicationDetailsStageData({ export function useApplicationDetailsStageData({
application, application,
documents, documents,
interviews, interviews: _interviews,
eorData, eorData,
getDeposit, getDeposit,
documentConfigs = [],
}: UseApplicationDetailsStageDataParams) { }: UseApplicationDetailsStageDataParams) {
const normalizeRole = (value: unknown): string => const normalizeRole = (value: unknown): string =>
String(value || '') String(value || '')
@ -38,33 +40,25 @@ export function useApplicationDetailsStageData({
return (documents || []).some((d) => d.documentType === docType); return (documents || []).some((d) => d.documentType === docType);
}; };
const isInterviewScheduled = (level: number | string) => { const getStageStatus = (stageName: string, fallbackStatus: ProcessStage['status'] = 'pending'): ProcessStage['status'] => {
return (interviews || []).some((i) => (i.level === level || i.level === level.toString()) && i.status?.toLowerCase() === 'scheduled');
};
const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => {
const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName);
if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) { return backendStage?.status ? (backendStage.status as any) : fallbackStatus;
return backendStage.status as any;
}
return fallbackLogic();
}; };
const processStages: ProcessStage[] = [ const processStages: ProcessStage[] = [
{ id: 1, name: 'Submitted', status: 'completed', date: application.submissionDate, description: 'Application submitted', documentsUploaded: 3 }, { id: 1, name: 'Submitted', status: 'completed', date: application.submissionDate, description: 'Application submitted', documentsUploaded: 3 },
{ {
id: 2, name: 'Questionnaire', status: getStageStatus('Questionnaire', () => id: 2, name: 'Questionnaire', status: getStageStatus('Questionnaire'),
['Questionnaire Completed', 'Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Questionnaire Pending' ? 'active' : 'pending'),
date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0 date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0
}, },
{ {
id: 3, name: 'Shortlist', status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), id: 3, name: 'Shortlist', status: getStageStatus('Shortlist'),
date: application.shortlistDate, description: 'Application shortlisted by DD', date: application.shortlistDate, description: 'Application shortlisted by DD',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.participantType === 'assignee').map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.participantType === 'assignee').map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`))),
documentsUploaded: 2 documentsUploaded: 2
}, },
{ {
id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 1 Interview Pending' && isInterviewScheduled(1)) ? 'active' : 'pending'), id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview'),
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 || [])
@ -80,7 +74,7 @@ export function useApplicationDetailsStageData({
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : 'pending'), id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview'),
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 || [])
@ -96,7 +90,7 @@ export function useApplicationDetailsStageData({
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : 'pending'), id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview'),
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 || [])
@ -111,31 +105,50 @@ export function useApplicationDetailsStageData({
)), )),
documentsUploaded: 2 documentsUploaded: 2
}, },
{ id: 7, name: 'FDD', status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 }, { id: 7, name: 'FDD', status: getStageStatus('FDD'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 },
{ {
id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval', () => ['Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval'),
date: application.loiApprovalDate, description: 'Letter of Intent approval', date: application.loiApprovalDate, description: 'Letter of Intent approval',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 9, name: 'Security Details', status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Security Details' || application.status === 'Payment Pending' ? 'active' : 'pending'), id: 9, name: 'Security Details', status: getStageStatus('Security Details'),
date: application.securityDetailsDate, description: 'Security verification', documentsUploaded: 3 date: application.securityDetailsDate, description: 'Security verification', documentsUploaded: 3
}, },
{ {
id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'), id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue'),
date: application.loiIssueDate, description: 'Letter of Intent issued', documentsUploaded: 1 date: application.loiIssueDate, description: 'Letter of Intent issued', isParallel: true,
branches: [
{
name: 'LOI Documents', color: 'green', stages:
documentConfigs.some((c: any) => c.stageCode === 'LOI Issue')
? documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue').map((c: any, i: number) => ({
id: `10a-${i}`,
name: c.documentType,
status: isDocumentUploaded(c.documentType) ? 'completed' : 'active',
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-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' },
]
}
]
}, },
{ {
id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'), 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: [ {
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' }, { id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
{ id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' }, { 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' }, { 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: [ },
{
name: 'Statutory Documents', color: 'green', stages: [
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' }, { 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-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' }, { id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
@ -147,19 +160,20 @@ export function useApplicationDetailsStageData({
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' }, { 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-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' }, { id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
]}, ]
},
] ]
}, },
{ {
id: 12, name: 'LOA', status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'), id: 12, name: 'LOA', status: getStageStatus('LOA'),
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified', isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))),
description: 'Letter of Authorization' description: 'Letter of Authorization'
}, },
{ id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), description: 'Essential Operating Requirements' }, { id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
{ id: 14, name: 'Inauguration', status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), description: 'Dealership inauguration' }, { id: 14, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
{ id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : ['Inauguration', 'Approved'].includes(application.status) ? 'active' : 'pending'), description: 'Dealer profile active' }, { id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
]; ];
const eorChecklist = [ const eorChecklist = [

View File

@ -19,6 +19,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [showScheduleModal, setShowScheduleModal] = useState(false); const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false); const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false);
const [interviewIdToCancel, setInterviewIdToCancel] = useState(''); const [interviewIdToCancel, setInterviewIdToCancel] = useState('');
const [interviewToReschedule, setInterviewToReschedule] = useState<any>(null);
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
@ -98,6 +99,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal, showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel, interviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule, setInterviewToReschedule,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,

View File

@ -26,7 +26,6 @@ import {
Download, Download,
Grid3x3, Grid3x3,
List, List,
Mail,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
Loader2 Loader2
@ -193,9 +192,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
} }
}; };
const handleBulkReminders = () => {
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
};
// For DD's All Applications page, only show initial statuses // For DD's All Applications page, only show initial statuses
const statusOptions: ApplicationStatus[] = [ const statusOptions: ApplicationStatus[] = [
@ -356,16 +352,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<> <>
<Button
variant="outline"
size="sm"
onClick={handleBulkReminders}
data-testid="onboarding-all-apps-reminders-btn"
>
<Mail className="w-4 h-4 mr-2" />
Send Reminders ({selectedIds.length})
</Button>
<Button <Button
size="sm" size="sm"
onClick={handleShortlist} onClick={handleShortlist}

View File

@ -62,6 +62,7 @@ export const ApplicationDetails = () => {
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal, showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel, interviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule, setInterviewToReschedule,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,
@ -266,6 +267,7 @@ export const ApplicationDetails = () => {
handleRemoveInterviewer, handleRemoveInterviewer,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,
@ -291,10 +293,15 @@ export const ApplicationDetails = () => {
participantType, participantType,
users, users,
interviewDate, interviewDate,
setInterviewDate,
interviewType, interviewType,
setInterviewType,
interviewMode, interviewMode,
setInterviewMode,
meetingLink, meetingLink,
setMeetingLink,
location, location,
setLocation,
scheduledInterviewParticipants, scheduledInterviewParticipants,
uploadFile, uploadFile,
uploadDocType, uploadDocType,
@ -319,6 +326,8 @@ export const ApplicationDetails = () => {
setShowCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, interviewIdToCancel,
setInterviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview, setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
@ -359,6 +368,7 @@ export const ApplicationDetails = () => {
interviews, interviews,
eorData, eorData,
getDeposit, getDeposit,
documentConfigs,
}); });
const { const {
@ -444,6 +454,7 @@ export const ApplicationDetails = () => {
setShowUploadForm={setShowUploadForm} setShowUploadForm={setShowUploadForm}
handleRetriggerEvaluators={handleRetriggerEvaluators} handleRetriggerEvaluators={handleRetriggerEvaluators}
handleCancelInterview={handleCancelInterview} handleCancelInterview={handleCancelInterview}
handleRescheduleInterview={handleRescheduleInterview}
setSelectedEvaluationForView={setSelectedEvaluationForView} setSelectedEvaluationForView={setSelectedEvaluationForView}
setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal}
renderFddAuditContent={renderFddAuditContent} renderFddAuditContent={renderFddAuditContent}
@ -532,6 +543,8 @@ export const ApplicationDetails = () => {
setInterviewIdToCancel={setInterviewIdToCancel} setInterviewIdToCancel={setInterviewIdToCancel}
isCancellingInterview={isCancellingInterview} isCancellingInterview={isCancellingInterview}
handleConfirmCancelInterview={handleConfirmCancelInterview} handleConfirmCancelInterview={handleConfirmCancelInterview}
interviewToReschedule={interviewToReschedule}
setInterviewToReschedule={setInterviewToReschedule}
interviewType={interviewType} interviewType={interviewType}
setInterviewType={setInterviewType} setInterviewType={setInterviewType}
interviewMode={interviewMode} interviewMode={interviewMode}

View File

@ -1,4 +1,5 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { ApplicationStatus, Application } from '@/lib/mock-data'; import { ApplicationStatus, Application } from '@/lib/mock-data';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
@ -14,7 +15,8 @@ import {
import { import {
Search, Search,
Download, Download,
Mail Mail,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
@ -53,6 +55,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all'); const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all');
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [isSendingReminders, setIsSendingReminders] = useState(false);
const [sortBy, setSortBy] = useState<'date'>('date'); const [sortBy, setSortBy] = useState<'date'>('date');
const [showNewApplicationModal, setShowNewApplicationModal] = useState(false); const [showNewApplicationModal, setShowNewApplicationModal] = useState(false);
const [showMyAssignments, setShowMyAssignments] = useState(false); const [showMyAssignments, setShowMyAssignments] = useState(false);
@ -153,9 +156,22 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
} }
}; };
const handleBulkReminders = () => { const handleBulkReminders = async () => {
alert(`Sending reminders to ${selectedIds.length} applicants`); if (selectedIds.length === 0) return;
try {
setIsSendingReminders(true);
const res = await onboardingService.sendBulkReminders(selectedIds);
if (res.success) {
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
setSelectedIds([]); setSelectedIds([]);
}
} catch (error: any) {
console.error('Failed to send reminders:', error);
toast.error(error.message || 'Failed to send reminders');
} finally {
setIsSendingReminders(false);
}
}; };
const handleExport = () => { const handleExport = () => {
@ -283,9 +299,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleBulkReminders} onClick={handleBulkReminders}
disabled={isSendingReminders}
data-testid="onboarding-applications-reminders-button" data-testid="onboarding-applications-reminders-button"
> >
{isSendingReminders ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Mail className="w-4 h-4 mr-2" /> <Mail className="w-4 h-4 mr-2" />
)}
Send Reminders ({selectedIds.length}) Send Reminders ({selectedIds.length})
</Button> </Button>
)} )}

View File

@ -11,6 +11,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { cn } from '@/components/ui/utils';
import { import {
Search, Search,
Download, Download,
@ -182,9 +190,12 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
setApplicationsData(mappedApps); setApplicationsData(mappedApps);
// Extract unique locations // Extract unique locations and states from the returned data
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean); const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
setLocations(uniqueLocations); setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
setStates(prev => Array.from(new Set([...prev, ...newStates])));
} catch (error) { } catch (error) {
console.error('Failed to fetch applications:', error); console.error('Failed to fetch applications:', error);
toast.error('Failed to load non-opportunity requests'); toast.error('Failed to load non-opportunity requests');
@ -247,29 +258,55 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative w-full md:w-36"> <Popover>
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <PopoverTrigger asChild>
<Input <Button
type="date" variant="outline"
value={fromDate} className={cn(
onChange={(e) => setFromDate(e.target.value)} "w-full md:w-36 justify-start text-left font-normal h-10 px-3",
className="pl-10 text-xs" !fromDate && "text-muted-foreground"
placeholder="From" )}
data-testid="onboarding-non-opps-from-date" data-testid="onboarding-non-opps-from-date-trigger"
>
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{fromDate ? format(new Date(fromDate), "PP") : <span className="text-xs text-slate-500">From Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={fromDate ? new Date(fromDate) : undefined}
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/> />
</div> </PopoverContent>
</Popover>
<span className="text-slate-400">to</span> <span className="text-slate-400">to</span>
<div className="relative w-full md:w-36">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <Popover>
<Input <PopoverTrigger asChild>
type="date" <Button
value={toDate} variant="outline"
onChange={(e) => setToDate(e.target.value)} className={cn(
className="pl-10 text-xs" "w-full md:w-36 justify-start text-left font-normal h-10 px-3",
placeholder="To" !toDate && "text-muted-foreground"
data-testid="onboarding-non-opps-to-date" )}
data-testid="onboarding-non-opps-to-date-trigger"
>
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{toDate ? format(new Date(toDate), "PP") : <span className="text-xs text-slate-500">To Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={toDate ? new Date(toDate) : undefined}
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/> />
</div> </PopoverContent>
</Popover>
</div> </div>
<Select value={locationFilter} onValueChange={setLocationFilter}> <Select value={locationFilter} onValueChange={setLocationFilter}>
@ -284,6 +321,22 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="ghost"
size="sm"
className="text-slate-500 hover:text-slate-700 h-10 px-3"
onClick={() => {
setFromDate('');
setToDate('');
setLocationFilter('all');
setStateFilter('all');
setSearchQuery('');
}}
data-testid="onboarding-non-opps-clear-filters"
>
Clear Filters
</Button>
<Select value={stateFilter} onValueChange={setStateFilter}> <Select value={stateFilter} onValueChange={setStateFilter}>
<SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select"> <SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select">
<SelectValue placeholder="All States" /> <SelectValue placeholder="All States" />

View File

@ -11,6 +11,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { cn } from '@/components/ui/utils';
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
@ -27,7 +35,6 @@ import {
Mail, Mail,
Grid3x3, Grid3x3,
List, List,
AlertCircle,
Loader2, Loader2,
Calendar, Calendar,
ArrowUpDown ArrowUpDown
@ -67,6 +74,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [paginationMeta, setPaginationMeta] = useState<any>(null); const [paginationMeta, setPaginationMeta] = useState<any>(null);
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [isSendingReminders, setIsSendingReminders] = useState(false);
const [showShortlistModal, setShowShortlistModal] = useState(false); const [showShortlistModal, setShowShortlistModal] = useState(false);
const [shortlistRemark, setShortlistRemark] = useState(''); const [shortlistRemark, setShortlistRemark] = useState('');
@ -160,9 +168,13 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
setApplicationsData(mappedApps); setApplicationsData(mappedApps);
// Extract unique locations for filtering // Extract unique locations and states from the returned data
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean); // Note: This appends new ones to the existing list to ensure all found locations are selectable
setLocations(uniqueLocations); const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
setStates(prev => Array.from(new Set([...prev, ...newStates])));
} catch (error) { } catch (error) {
console.error('Failed to fetch applications:', error); console.error('Failed to fetch applications:', error);
toast.error('Failed to load opportunity requests'); toast.error('Failed to load opportunity requests');
@ -231,12 +243,25 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
} }
}; };
const handleBulkReminders = () => { const handleBulkReminders = async () => {
if (selectedIds.length === 0) { if (selectedIds.length === 0) {
toast.error('Please select at least one application'); toast.error('Please select at least one application');
return; return;
} }
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
try {
setIsSendingReminders(true);
const res = await onboardingService.sendBulkReminders(selectedIds);
if (res.success) {
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
setSelectedIds([]);
}
} catch (error: any) {
console.error('Failed to send reminders:', error);
toast.error(error.message || 'Failed to send reminders');
} finally {
setIsSendingReminders(false);
}
}; };
const handleExport = async () => { const handleExport = async () => {
@ -380,20 +405,6 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Info Banner */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4" data-testid="onboarding-opp-requests-banner">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-amber-900 mb-1">DD Lead Workflow - Opportunity Requests</h3>
<p className="text-amber-800">
This page shows <strong>applications where dealerships are being offered</strong> at the applicant's preferred location.
These have been shortlisted by DD and are waiting for your review. Select and <strong>Shortlist</strong> promising candidates
to move them to the <strong>Dealership Requests</strong> page for further processing.
</p>
</div>
</div>
</div>
{/* Header with Filters */} {/* Header with Filters */}
<div className="bg-white rounded-lg border border-slate-200 p-6"> <div className="bg-white rounded-lg border border-slate-200 p-6">
@ -424,6 +435,23 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="ghost"
size="sm"
className="text-slate-500 hover:text-slate-700 h-9"
onClick={() => {
setFromDate('');
setToDate('');
setStatusFilter('all');
setLocationFilter('all');
setStateFilter('all');
setSearchQuery('');
}}
data-testid="onboarding-opp-requests-clear-filters"
>
Clear Filters
</Button>
<Select value={stateFilter} onValueChange={setStateFilter}> <Select value={stateFilter} onValueChange={setStateFilter}>
<SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select"> <SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select">
<SelectValue placeholder="Filter by state" /> <SelectValue placeholder="Filter by state" />
@ -449,29 +477,55 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</Select> </Select>
<div className="flex items-center gap-2 flex-1 md:flex-none"> <div className="flex items-center gap-2 flex-1 md:flex-none">
<div className="relative w-full md:w-40"> <Popover>
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <PopoverTrigger asChild>
<Input <Button
type="date" variant="outline"
value={fromDate} className={cn(
onChange={(e) => setFromDate(e.target.value)} "w-full md:w-40 justify-start text-left font-normal h-9 px-3",
className="pl-10 text-xs" !fromDate && "text-muted-foreground"
placeholder="From" )}
data-testid="onboarding-opp-requests-from-date" data-testid="onboarding-opp-requests-from-date-trigger"
>
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{fromDate ? format(new Date(fromDate), "PPP") : <span className="text-xs">From Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={fromDate ? new Date(fromDate) : undefined}
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/> />
</div> </PopoverContent>
</Popover>
<span className="text-slate-400">to</span> <span className="text-slate-400">to</span>
<div className="relative w-full md:w-40">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <Popover>
<Input <PopoverTrigger asChild>
type="date" <Button
value={toDate} variant="outline"
onChange={(e) => setToDate(e.target.value)} className={cn(
className="pl-10 text-xs" "w-full md:w-40 justify-start text-left font-normal h-9 px-3",
placeholder="To" !toDate && "text-muted-foreground"
data-testid="onboarding-opp-requests-to-date" )}
data-testid="onboarding-opp-requests-to-date-trigger"
>
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{toDate ? format(new Date(toDate), "PPP") : <span className="text-xs">To Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={toDate ? new Date(toDate) : undefined}
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/> />
</div> </PopoverContent>
</Popover>
</div> </div>
<Select value={sortBy} onValueChange={setSortBy}> <Select value={sortBy} onValueChange={setSortBy}>
@ -526,9 +580,14 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleBulkReminders} onClick={handleBulkReminders}
disabled={isSendingReminders}
data-testid="onboarding-opp-requests-bulk-reminder-btn" data-testid="onboarding-opp-requests-bulk-reminder-btn"
> >
{isSendingReminders ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Mail className="w-4 h-4 mr-2" /> <Mail className="w-4 h-4 mr-2" />
)}
Send Reminders ({selectedIds.length}) Send Reminders ({selectedIds.length})
</Button> </Button>

View File

@ -29,11 +29,9 @@ const workflowStages = [
{ id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' }, { id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' },
{ id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' }, { id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' },
{ id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' }, { id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' },
{ id: 6, name: 'DD Head Approval', key: 'DD_HEAD_APPROVAL', role: 'DD Head' }, { id: 6, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' },
{ id: 7, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' }, { id: 7, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' },
{ id: 8, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' }, { id: 8, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
{ id: 9, name: 'NBH Clearance with EOR', key: 'NBH_CLEARANCE_EOR', role: 'NBH' },
{ id: 10, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
]; ];
/** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */ /** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */
@ -296,15 +294,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
if (response.data.success) { if (response.data.success) {
const req = response.data.request; const req = response.data.request;
setRequest(req); setRequest(req);
const currentStage = req.currentStage;
if (
currentStage === 'NBH_CLEARANCE_EOR' ||
currentStage === 'NBH Clearance with EOR' ||
req.status === 'Completed'
) {
fetchEorChecklist(req.id);
}
} }
} catch (error) { } catch (error) {
console.error('Fetch relocation request details error:', error); console.error('Fetch relocation request details error:', error);
@ -351,7 +340,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries); const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries);
const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs); const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs);
const dbOrdinal = request ? getDbStageOrdinal() : 1; const dbOrdinal = request ? getDbStageOrdinal() : 1;
/** Audit/timeline can reference later steps (e.g. NBH EOR) while the request still sits at NBH Approval — do not use that to drive the active step. */ /** Audit/timeline can reference later steps while the request still sits in a prior stage — do not use that to drive the active step. */
const workflowProgressMismatch = const workflowProgressMismatch =
Boolean(request) && Boolean(request) &&
Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal && Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal &&
@ -365,7 +354,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
request?.status === 'Completed' || request?.status === 'Completed' ||
request?.currentStage === 'Completed' || request?.currentStage === 'Completed' ||
dbOrdinal >= workflowStages.length + 1; dbOrdinal >= workflowStages.length + 1;
/** Match backend: N/10 while on pipeline (NBH EOR = 9 → 90%); 100% only when completed — avoids stale API 100% at NBH EOR. */ /** Match backend: N/(pipeline+1) while in flight; 100% only when completed. */
const timelineProgressPct = allWorkflowComplete const timelineProgressPct = allWorkflowComplete
? 100 ? 100
: Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100)); : Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100));
@ -422,7 +411,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
request.currentStage && request.currentStage &&
request.currentStage !== 'ASM Review' && request.currentStage !== 'ASM Review' &&
request.currentStage !== 'Rejected'; request.currentStage !== 'Rejected';
const canRevoke = showActions && ['ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || ''); const canRevoke = showActions && ['ZBH', 'DD Lead', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || '');
const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance'; const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance';
const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0)); const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0));
@ -696,9 +685,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<TabsList className="w-max min-w-full justify-start"> <TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger> <TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger> <TabsTrigger value="documents">Documents</TabsTrigger>
{(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && (
<TabsTrigger value="eor">EOR Checklist</TabsTrigger>
)}
<TabsTrigger value="history">History & Audit Trail</TabsTrigger> <TabsTrigger value="history">History & Audit Trail</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@ -1039,7 +1025,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{doc.status === 'Pending Verification' && (() => { {doc.status === 'Pending Verification' && (() => {
const role = currentUser?.role || currentUser?.roleCode || ''; const role = currentUser?.role || currentUser?.roleCode || '';
// SRS — only authorized review roles can verify relocation documents // SRS — only authorized review roles can verify relocation documents
return ['DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); return ['DD Lead', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role);
})() && ( })() && (
<> <>
<Button <Button
@ -1134,7 +1120,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{(!eorChecklist.items || eorChecklist.items.length === 0) ? ( {(!eorChecklist.items || eorChecklist.items.length === 0) ? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm"> <TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm">
No checklist rows returned. Use &quot;Try Refreshing&quot; above or reload the page; rows are created when the request enters NBH Clearance with EOR. No checklist rows returned. Use &quot;Try Refreshing&quot; above or reload the page.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (

View File

@ -9,7 +9,7 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -44,8 +44,9 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn']; const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn'];
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = { const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'], 'Request Submitted': ['Submission', 'Submitted', 'Initiation'],
'RBM': ['RBM', 'RBM Review', 'Regional Review'], 'ASM': ['ASM', 'ASM 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'],
@ -97,6 +98,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [uploadFile, setUploadFile] = useState<File | null>(null); const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]); const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState(''); const [uploadStage, setUploadStage] = useState('');
const hasUploadedPPT = useMemo(() => {
const allDocs = [
...(resignationData?.documents || []),
...(resignationData?.uploadedDocuments || [])
];
return allDocs.some(doc =>
(doc.documentType || doc.type) === 'PPT Presentation'
);
}, [resignationData]);
const fetchResignation = async () => { const fetchResignation = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -127,18 +138,19 @@ 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: 'Request Submitted', key: 'Request Submitted', description: 'Dealer submitted the resignation request' },
{ id: 2, name: 'RBM Review', key: 'RBM', description: 'Regional Business Manager evaluation' }, { id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { id: 3, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' }, { id: 4, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' }, { id: 5, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
{ id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' }, { id: 6, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }, { id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
{ id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' }, { id: 8, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' } { id: 9, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
{ id: 10, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
]; ];
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed']; const stagesOrdered = ['Request Submitted', 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed'];
const legalStageApproved = (() => { const legalStageApproved = (() => {
if (!resignationData) return false; if (!resignationData) return false;
@ -157,6 +169,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const atLegal = stage === 'legal' || stage === 'legal - resignation letter'; const atLegal = stage === 'legal' || stage === 'legal - resignation letter';
const legalApprovedTransition = const legalApprovedTransition =
targetStage === 'legal' || targetStage === 'legal' ||
targetStage === 'dd admin' ||
targetStage === 'f&f initiated' || targetStage === 'f&f initiated' ||
targetStage === 'fnf_initiated' || targetStage === 'fnf_initiated' ||
action.includes('approved'); action.includes('approved');
@ -174,7 +187,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const status = resignationData.status; const status = resignationData.status;
const userRole = currentUser.role; const userRole = currentUser.role;
// Final states where no more actions are possible const isZmRbmStage = currentStage === 'RBM' || currentStage === 'RBM Review' || currentStage === 'RBM + DD-ZM Review';
const userRoleCode = String(currentUser.roleCode || currentUser.role || '').trim().toUpperCase();
// Check if current user already partially approved this request at this stage
const hasAlreadyPartiallyApproved = isZmRbmStage && auditLogs.some(log =>
log.action === 'PARTIAL_APPROVE' &&
(log.actor?.id === currentUser.id || log.actorId === currentUser.id || log.actor?.email === currentUser.email || log.userEmail === currentUser.email) &&
(log.details?.roleCode === userRoleCode || (log.details?.roleCode === 'DD-ZM' && userRoleCode === 'DD ZM'))
);
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Revoked'].includes(status); const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Revoked'].includes(status);
// Check if it's already in the settlement phase // Check if it's already in the settlement phase
@ -184,29 +206,48 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const nbhIndex = stagesOrdered.indexOf('NBH'); const nbhIndex = stagesOrdered.indexOf('NBH');
const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex; const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex;
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage]; const isCurrentlyAssigned = userRoleCode === 'SUPER_ADMIN' ||
(isZmRbmStage && (userRoleCode === 'RBM' || userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM')) ||
userRole === STAGE_TO_ROLE_MAP[currentStage];
const isDDLeadStage = currentStage === 'DD Lead' || currentStage === 'DD Lead Review';
const isDDLead = userRoleCode === 'DD_LEAD' || userRoleCode === 'DD LEAD';
const isLwdReached = (() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const lwdString = resignationData.lastOperationalDateServices || resignationData.lastOperationalDateSales;
if (!lwdString) return true;
const lwd = new Date(lwdString);
lwd.setHours(0, 0, 0, 0);
return today >= lwd;
})();
const canApprove = isCurrentlyAssigned && const canApprove = isCurrentlyAssigned &&
!isFinalState && !isFinalState &&
!isSettlementPhase && !isSettlementPhase &&
!(currentStage === 'Legal' && legalStageApproved); !hasAlreadyPartiallyApproved &&
!(currentStage === 'Legal' && legalStageApproved) &&
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
!(currentStage === 'DD Admin' && !isLwdReached);
return { return {
canApprove, canApprove,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState, canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
canRevoke: (userRole === 'Super Admin' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase, canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) && canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
!isSettlementPhase && !isFinalState, !isSettlementPhase && !isFinalState && currentStage === 'Legal' && isLwdReached,
canAssign: userRole !== 'Dealer' && !isFinalState canAssign: userRole !== 'Dealer' && !isFinalState
}; };
}; };
const permissions = getResignationPermissions(); const permissions = getResignationPermissions();
const isNationalLevel = ['Super Admin', 'DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Legal Admin', 'DD-ZM'].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'],
@ -446,6 +487,31 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span> <span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{/* Debug for PPT button visibility */}
{(() => {
const roleNormalized = String(currentUser?.roleCode || currentUser?.role || '').trim().toUpperCase();
const isDDLeadUser = roleNormalized === 'DD LEAD' || roleNormalized === 'DD_LEAD';
const isDDLeadStageCurrent = ['DD Lead', 'DD Lead Review', 'DDL Review'].includes(resignationData?.currentStage);
if (isDDLeadUser && isDDLeadStageCurrent) {
return (
<Button
size="sm"
variant="outline"
className="text-amber-700 border-amber-300 hover:bg-amber-50 shadow-sm"
onClick={() => {
setUploadDocType('PPT Presentation');
setUploadStage('DD Lead');
setShowUploadDialog(true);
}}
>
<Upload className="w-4 h-4 mr-2" />
Upload PPT
</Button>
);
}
return null;
})()}
{permissions.canApprove && ( {permissions.canApprove && (
<Button <Button
size="sm" size="sm"
@ -562,6 +628,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,20 +819,28 @@ 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="space-y-4 mt-3">
{stageTimelineEntries.map((entry: any, i: number) => (
<div key={i} className="space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase"> <Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase">
{timelineEntry.user || 'System'} {entry.user || 'System'}
</Badge> </Badge>
<span className="text-[10px] text-slate-500 italic"> <span className="text-[10px] text-slate-500 italic">
{timelineEntry.action} {entry.action}
</span>
<span className="text-[10px] text-slate-400 ml-auto">
{formatDateTime(entry.timestamp || entry.createdAt)}
</span> </span>
</div> </div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm"> <div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm">
{timelineEntry.comments || timelineEntry.remarks || 'No remarks provided.'} {entry.comments || entry.remarks || 'No remarks provided.'}
</div> </div>
</div> </div>
))}
</div>
)} )}
</div> </div>
</div> </div>
@ -938,6 +1015,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 className="w-full border-collapse">
<TableHeader>
<TableRow className="bg-slate-50/50">
<TableHead className="min-w-[120px]">Stage</TableHead>
<TableHead className="min-w-[120px]">Approver</TableHead>
<TableHead className="min-w-[200px]">Action</TableHead>
<TableHead className="w-full min-w-[300px]">Remarks</TableHead>
<TableHead className="min-w-[180px] text-right">Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(resignationData?.timeline || []).length > 0 ? (
resignationData.timeline.map((entry: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{entry.stage}</TableCell>
<TableCell>
<Badge variant="outline">{entry.user || 'System'}</Badge>
</TableCell>
<TableCell className="whitespace-normal break-words">{entry.action}</TableCell>
<TableCell className="whitespace-normal break-words">
{entry.remarks || entry.comments || '-'}
</TableCell>
<TableCell className="text-slate-500 whitespace-nowrap text-right">
{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

@ -1,4 +1,4 @@
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload } from 'lucide-react'; import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload, PauseCircle } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@ -16,6 +16,11 @@ import { terminationService } from '@/services/termination.service';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { formatTerminationStatusLabel } from '@/lib/terminationDisplay';
import {
getJointRoundCutoffMsFromTimeline,
isAuditLogInCurrentJointRound
} from '@/lib/terminationJointReviewRound';
import { TERMINATION_DOCUMENT_TYPES, TERMINATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions'; import { TERMINATION_DOCUMENT_TYPES, TERMINATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions';
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles'; import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles';
@ -27,7 +32,7 @@ interface TerminationDetailsProps {
export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) { export function TerminationDetails({ terminationId, onBack, currentUser }: TerminationDetailsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | null }>({ open: false, type: null }); const [actionDialog, setActionDialog] = useState<{ open: boolean; type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold' | null }>({ open: false, type: null });
const [remarks, setRemarks] = useState(''); const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState(''); const [assignToUser, setAssignToUser] = useState('');
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] }); const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
@ -153,7 +158,41 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}; };
// Check if user can push to F&F (DD Lead and above) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'DD_HEAD', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode);
const stageAliases: Record<string, string[]> = {
'Submitted': ['Submitted'],
'RBM + DD-ZM Review': ['RBM + DD-ZM Review'],
'ZBH Review': ['ZBH Review'],
'DD Lead Review': ['DD Lead Review'],
'Legal Verification': ['Legal Verification'],
'DD Head Review': ['DD Head Review'],
'NBH Evaluation': ['NBH Evaluation'],
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
'Evaluation of Dealer SCN Response': ['Evaluation of Dealer SCN Response', 'Personal Hearing'],
'NBH Final Approval': ['NBH Final Approval'],
'CCO Approval': ['CCO Approval'],
'CEO Final Approval': ['CEO Final Approval'],
'Legal - Termination Letter': ['Legal - Termination Letter'],
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
};
const stageSequence = [
'Submitted',
'RBM + DD-ZM Review',
'ZBH Review',
'DD Lead Review',
'Legal Verification',
'DD Head Review',
'NBH Evaluation',
'Show Cause Notice (SCN)',
'Evaluation of Dealer SCN Response',
'NBH Final Approval',
'CCO Approval',
'CEO Final Approval',
'Legal - Termination Letter',
'Dealer Terminated'
];
// Centralized Permissions Utility for Termination logic (Robust Validation) // Centralized Permissions Utility for Termination logic (Robust Validation)
const getTerminationPermissions = () => { const getTerminationPermissions = () => {
@ -164,28 +203,67 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const currentStage = terminationData.currentStage; const currentStage = terminationData.currentStage;
const status = terminationData.status; const status = terminationData.status;
const userRole = currentUser.role || currentUser.roleCode; const userRole = currentUser.role || currentUser.roleCode;
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage); const isScnStage = ['Show Cause Notice (SCN)', 'SCN'].includes(currentStage);
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated'; const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED'; const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
const scnJointRoundCutoffMs = getJointRoundCutoffMsFromTimeline(terminationData.timeline, 'scn_response_eval');
const rbmJointRoundCutoffMs = getJointRoundCutoffMsFromTimeline(terminationData.timeline, 'rbm_review');
const isScnResponseEvalStage =
currentStage === 'Evaluation of Dealer SCN Response' || currentStage === 'Personal Hearing';
const jointRoundCutoffMs =
currentStage === 'RBM + DD-ZM Review' ? rbmJointRoundCutoffMs : isScnResponseEvalStage ? scnJointRoundCutoffMs : null;
const userHasApprovedJointly = auditLogs.some(log => {
if (!isAuditLogInCurrentJointRound(log, jointRoundCutoffMs)) return false;
const logUserId = log.userId || log.user?.id || log.actor?.id || log.actorId;
const isThisUser = String(logUserId) === String(currentUser.id);
const actionText = (log.action || log.description || '').toUpperCase();
const isPartialApprove = actionText.includes('PARTIAL_APPROVE') || actionText.includes('PARTIAL APPROVED');
const logStage = log.details?.stage || log.stage || '';
const isRbmReviewLog = logStage === 'RBM + DD-ZM Review' || (log.remarks || '').includes('Partial approval by');
const isPersonalHearingLog =
logStage === 'Evaluation of Dealer SCN Response' ||
logStage === 'Personal Hearing' ||
(log.remarks || '').includes('SCN Response Review by');
const stageMatches =
(currentStage === 'RBM + DD-ZM Review' && isRbmReviewLog) ||
(isScnResponseEvalStage && isPersonalHearingLog);
return isThisUser && isPartialApprove && stageMatches;
});
const isStage = (stageName: string) => {
const aliases = stageAliases[stageName] || [stageName];
return aliases.includes(currentStage);
};
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || ( const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
(currentStage === 'RBM Review' && userRole === 'RBM') || (isStage('RBM + DD-ZM Review') && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
(currentStage === 'ZBH Review' && userRole === 'ZBH') || (isStage('ZBH Review') && userRole === 'ZBH') ||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') || (isStage('DD Lead Review') && userRole === 'DD Lead') ||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') || (isStage('Legal Verification') && userRole === 'Legal Admin') ||
(currentStage === 'DD Head Review' && userRole === 'DD Head') || (isStage('DD Head Review') && (userRole === 'DD Head' || userRole === 'DD_HEAD')) ||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') || (isStage('NBH Evaluation') && userRole === 'NBH') ||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') || (isStage('Evaluation of Dealer SCN Response') && ['DD Lead', 'ZBH', 'RBM', 'DD Head', 'DD_HEAD'].includes(userRole) && !userHasApprovedJointly) ||
(currentStage === 'CCO Approval' && userRole === 'CCO') || (isStage('NBH Final Approval') && userRole === 'NBH') ||
(currentStage === 'CEO Final Approval' && userRole === 'CEO') || (isStage('CCO Approval') && userRole === 'CCO') ||
(currentStage === 'Legal - Termination Letter' && userRole === 'Legal Admin') (isStage('CEO Final Approval') && userRole === 'CEO') ||
(isStage('Legal - Termination Letter') && userRole === 'Legal Admin')
); );
return { return {
canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage), canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage),
canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState, canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState,
canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState, canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'DD Lead', 'Super Admin'].includes(userRole)) && !isFinalState,
canHold:
(isStage('NBH Evaluation') || isStage('NBH Final Approval')) &&
(userRole === 'NBH' || userRole === 'Super Admin') &&
status !== 'On Hold' &&
!isFinalState,
canFinalize: ( canFinalize: (
(currentStage === 'NBH Final Approval' && userRole === 'NBH') || (currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
(currentStage === 'CCO Approval' && userRole === 'CCO') || (currentStage === 'CCO Approval' && userRole === 'CCO') ||
@ -205,39 +283,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const request = terminationData || {}; const request = terminationData || {};
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage); const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
const stageAliases: Record<string, string[]> = {
'Submitted': ['Submitted', 'Request Initiated'],
'RBM Review': ['RBM Review'],
'ZBH Review': ['ZBH Review'],
'DD Lead Review': ['DD Lead Review'],
'Legal Verification': ['Legal Verification'],
'DD Head Review': ['DD Head Review'],
'NBH Evaluation': ['NBH Evaluation'],
'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'],
'Personal Hearing': ['Personal Hearing'],
'NBH Final Approval': ['NBH Final Approval'],
'CCO Approval': ['CCO Approval'],
'CEO Final Approval': ['CEO Final Approval'],
'Legal - Termination Letter': ['Legal - Termination Letter'],
'Dealer Terminated': ['Terminated', 'Dealer Terminated']
};
const stageSequence = [
'Submitted',
'RBM Review',
'ZBH Review',
'DD Lead Review',
'Legal Verification',
'DD Head Review',
'NBH Evaluation',
'Show Cause Notice (SCN)',
'Personal Hearing',
'NBH Final Approval',
'CCO Approval',
'CEO Final Approval',
'Legal - Termination Letter',
'Dealer Terminated'
];
const resolveCanonicalStage = (currentStage?: string) => { const resolveCanonicalStage = (currentStage?: string) => {
if (!currentStage) return ''; if (!currentStage) return '';
@ -301,21 +346,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return acc; return acc;
}, {}); }, {});
const getLatestStageTimelineEntry = (stageName: string) => { const getStageTimelineEntries = (stageName: string) => {
const aliases = stageAliases[stageName] || [stageName]; const aliases = stageAliases[stageName] || [stageName];
const entries = (request.timeline || []).filter((entry: any) => aliases.includes(entry.stage)); const entries = (request.timeline || []).filter((entry: any) =>
aliases.includes(entry.stage) ||
if (entries.length === 0) return null; (stageName === 'Submitted' && (entry.stage === 'Submitted' || entry.stage === 'Request Initiated'))
// Keep submitted row anchored to initiation details, not later stage-transition remarks.
if (stageName === 'Submitted') {
const initiatedEntry = entries.find((entry: any) =>
String(entry?.action || '').toLowerCase().includes('initiated')
); );
return initiatedEntry || entries[0];
}
return entries[entries.length - 1]; // Sort by timestamp
return entries.sort((a: any, b: any) =>
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
);
}; };
const progressStages = [ const progressStages = [
@ -332,9 +373,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}, },
{ {
id: 2, id: 2,
name: 'RBM Review', name: 'RBM + DD-ZM Review',
status: getProgressStatus('RBM Review'), status: getProgressStatus('RBM + DD-ZM Review'),
description: 'Regional Business Manager review' description: 'Joint review and approval by RBM and DD-ZM'
}, },
{ {
id: 3, id: 3,
@ -374,9 +415,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}, },
{ {
id: 9, id: 9,
name: 'Personal Hearing', name: 'Evaluation of Dealer SCN Response',
status: getProgressStatus('Personal Hearing'), status: getProgressStatus('Evaluation of Dealer SCN Response'),
description: 'Evaluation of SCN response & Hearing' description: 'Joint evaluation of SCN response by DD-Lead, ZBH, RBM, and DD-Head'
}, },
{ {
id: 10, id: 10,
@ -420,7 +461,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
setStageDocumentsDialog({ open: true, stageName, documents }); setStageDocumentsDialog({ open: true, stageName, documents });
}; };
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke') => { const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'hold') => {
setActionDialog({ open: true, type }); setActionDialog({ open: true, type });
}; };
@ -448,27 +489,34 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
setIsProcessing(true); setIsProcessing(true);
try { try {
if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke') { let response: any;
await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks); if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke' || actionType === 'hold') {
response = await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks);
} else if (actionType === 'pushfnf') { } else if (actionType === 'pushfnf') {
// Logic for push to fnf (using existing service if available) response = await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
} else { } else {
toast.error('Action logic not fully implemented for this type'); toast.error('Action logic not fully implemented for this type');
setIsProcessing(false); setIsProcessing(false);
return; return;
} }
if (response && (response.success === false || response.ok === false)) {
console.error('[TerminationDetails] Action failed:', response);
toast.error(response.message || response.data?.message || 'Failed to perform action');
setIsProcessing(false);
return;
}
const actionMessages: Record<string, string> = { const actionMessages: Record<string, string> = {
approve: 'Request approved and forwarded', approve: 'Request approved and forwarded',
withdrawal: 'Request withdrawn successfully', withdrawal: 'Request withdrawn successfully',
sendBack: 'Request sent back for clarification', sendBack: 'Request sent back for clarification',
assign: `Request assigned to ${assignToUser}`, assign: `Request assigned successfully`,
pushfnf: 'Request pushed to F&F successfully', pushfnf: 'Request pushed to F&F successfully',
revoke: 'Request revoked and withdrawn' revoke: 'Request revoked and withdrawn'
}; };
toast.success(actionMessages[actionType!] || 'Action completed'); toast.success(actionMessages[actionType!] || response?.message || 'Action completed');
setActionDialog({ open: false, type: null }); setActionDialog({ open: false, type: null });
setRemarks(''); setRemarks('');
setAssignToUser(''); setAssignToUser('');
@ -527,7 +575,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
? 'bg-red-100 text-red-700 border-red-300' ? 'bg-red-100 text-red-700 border-red-300'
: 'bg-yellow-100 text-yellow-700 border-yellow-300' : 'bg-yellow-100 text-yellow-700 border-yellow-300'
}> }>
{request.status === 'Settled' ? 'Completed' : (request.status || 'Pending')} {request.status === 'Settled' ? 'Completed' : formatTerminationStatusLabel(request.status || 'Pending')}
</Badge> </Badge>
</div> </div>
</div> </div>
@ -536,6 +584,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<Card className="border-amber-200 shadow-sm"> <Card className="border-amber-200 shadow-sm">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
{(request.currentStage === 'Evaluation of Dealer SCN Response' || request.currentStage === 'Personal Hearing') && (
<Alert className="mb-4 bg-blue-50 border-blue-200">
<AlertTitle className="text-blue-800 text-sm font-semibold">Joint Review Stage</AlertTitle>
<AlertDescription className="text-blue-700 text-xs">
This stage requires a joint evaluation of the SCN response by the <strong>DD-Lead, ZBH, RBM, and DD-Head</strong>.
The case will only advance to NBH Final Approval once all four stakeholders have recorded their review.
</AlertDescription>
</Alert>
)}
{/* Primary Actions Row */} {/* Primary Actions Row */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -590,6 +647,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
)} )}
</> </>
)} )}
{permissions.canHold && (
<Button
size="sm"
variant="outline"
className="border-orange-200 text-orange-700 hover:bg-orange-50"
onClick={() => handleAction('hold')}
>
<PauseCircle className="w-4 h-4 mr-2" />
Hold Decision
</Button>
)}
{permissions.canFinalize && ( {permissions.canFinalize && (
<Button <Button
size="sm" size="sm"
@ -844,7 +912,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="space-y-4"> <div className="space-y-4">
{progressStages.map((stage, index) => { {progressStages.map((stage, index) => {
const documentCount = stageDocuments[stage.name]?.length || 0; const documentCount = stageDocuments[stage.name]?.length || 0;
const timelineEntry = getLatestStageTimelineEntry(stage.name); const stageEntries = getStageTimelineEntries(stage.name);
return ( return (
<div key={stage.id} className="flex gap-4"> <div key={stage.id} className="flex gap-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -884,29 +952,59 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</button> </button>
)} )}
</div> </div>
{(timelineEntry?.timestamp || stage.date) && ( {(stageEntries[0]?.timestamp || stage.date) && (
<div className="flex items-center gap-1 text-sm text-slate-600"> <div className="flex items-center gap-1 text-xs text-slate-500 bg-slate-50 px-2 py-0.5 rounded-full border border-slate-100">
<Calendar className="w-4 h-4" /> <Calendar className="w-3 h-3" />
<span>{formatDateTime(timelineEntry?.timestamp || stage.date)}</span> <span>{formatDateTime(stageEntries[0]?.timestamp || stage.date)}</span>
</div> </div>
)} )}
</div> </div>
<p className="text-slate-600 text-sm">{stage.description}</p> <p className="text-slate-600 text-sm">{stage.description}</p>
{timelineEntry && ( {stageEntries.length > 0 && (
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-3">
{stageEntries.map((entry: any, entryIdx: number) => {
const rawRemarks = entry.remarks || entry.comments || '';
const isAttachment = rawRemarks?.startsWith('Attachment:');
const remarksContent = isAttachment
? rawRemarks.replace('Attachment:', '').trim()
: rawRemarks;
return (
<div key={entryIdx} className="group">
<div className="flex items-center gap-2 mb-1">
<Badge className={`
text-[10px] h-4 px-1.5
${entry.action?.toLowerCase().includes('rejected') || entry.action?.toLowerCase().includes('revoked')
? 'bg-red-100 text-red-700'
: entry.action?.toLowerCase().includes('approved')
? 'bg-emerald-100 text-emerald-700'
: 'bg-blue-100 text-blue-700'}
`}>
{entry.action || 'Action'}
</Badge>
<span className="text-[10px] text-slate-500 font-medium">
by {entry.user || 'System'}{entry.role ? ` (${entry.role})` : ''} {formatDateTime(entry.timestamp)}
</span>
</div>
<div className={`
p-2.5 rounded-lg border text-sm
${isAttachment
? 'bg-amber-50/50 border-amber-100 text-amber-900'
: 'bg-slate-50 border-slate-100 text-slate-700'}
`}>
{isAttachment ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge className="bg-blue-100 text-blue-700 border-blue-300">{timelineEntry.action || 'Updated'}</Badge> <FileText className="w-3.5 h-3.5 text-amber-600" />
<span className="text-xs text-slate-500">by {timelineEntry.user || 'System'}</span> <span className="font-medium truncate">{remarksContent}</span>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
<div className="space-y-2">
<div>
<Label className="text-xs text-slate-600">Remarks:</Label>
<p className="text-sm text-slate-700 mt-1">{timelineEntry.remarks || 'No remarks provided.'}</p>
</div> </div>
) : (
<p className="leading-relaxed">{remarksContent || 'No remarks provided.'}</p>
)}
</div> </div>
</div> </div>
);
})}
</div> </div>
)} )}
</div> </div>
@ -1064,6 +1162,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
{actionDialog.type === 'revoke' && 'Revoke Termination Request'} {actionDialog.type === 'revoke' && 'Revoke Termination Request'}
{actionDialog.type === 'assign' && 'Assign to User'} {actionDialog.type === 'assign' && 'Assign to User'}
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'} {actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
{actionDialog.type === 'hold' && 'Hold Termination Case'}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{actionDialog.type === 'assign' {actionDialog.type === 'assign'
@ -1126,6 +1225,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
className={ className={
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' : actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' : actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
actionDialog.type === 'hold' ? 'bg-orange-600 hover:bg-orange-700' :
'bg-blue-600 hover:bg-blue-700' 'bg-blue-600 hover:bg-blue-700'
} }
> >

View File

@ -23,6 +23,7 @@ import {
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { User } from '@/lib/mock-data'; import { User } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { formatTerminationStatusLabel } from '@/lib/terminationDisplay';
interface TerminationPageProps { interface TerminationPageProps {
currentUser: User | null; currentUser: User | null;
@ -51,6 +52,8 @@ const getStatusColor = (status: string) => {
return 'bg-blue-100 text-blue-700 border-blue-300'; return 'bg-blue-100 text-blue-700 border-blue-300';
}; };
const formatStatus = formatTerminationStatusLabel;
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) { export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealers, setDealers] = useState<any[]>([]); const [dealers, setDealers] = useState<any[]>([]);
@ -103,7 +106,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
}; };
useEffect(() => { useEffect(() => {
if (!isDialogOpen || !isDDLead) return; if (!isDialogOpen || !canCreateTermination) return;
let cancelled = false; let cancelled = false;
(async () => { (async () => {
@ -254,7 +257,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
} }
}; };
const isDDLead = currentUser?.role === 'DD Lead'; const allowedRoles = ['DD Lead', 'ASM', 'DD Admin', 'DD AM', 'Super Admin'];
const canCreateTermination = currentUser?.role && allowedRoles.includes(currentUser.role);
// Map terminations to tab-specific views (already filtered by backend, but need variables for render) // Map terminations to tab-specific views (already filtered by backend, but need variables for render)
const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : []; const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : [];
@ -312,14 +316,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<CardTitle>Termination Requests</CardTitle> <CardTitle>Termination Requests</CardTitle>
<CardDescription> <CardDescription>
Manage dealer termination proceedings and legal compliance Manage dealer termination proceedings and legal compliance
{!isDDLead && (
<span className="block mt-1 text-red-600">
Note: Only DD Lead can create termination requests. Current role: {currentUser?.role || 'Not logged in'}
</span>
)}
</CardDescription> </CardDescription>
</div> </div>
{isDDLead && ( {canCreateTermination && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-red-600 hover:bg-red-700"> <Button className="bg-red-600 hover:bg-red-700">
@ -408,7 +407,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<SelectContent> <SelectContent>
<SelectItem value="Working Capital">Working Capital</SelectItem> <SelectItem value="Working Capital">Working Capital</SelectItem>
<SelectItem value="Performance Issues">Performance Issues</SelectItem> <SelectItem value="Performance Issues">Performance Issues</SelectItem>
<SelectItem value="Unethical Practical">Unethical Practical</SelectItem> <SelectItem value="Unethical Practice">Unethical Practice</SelectItem>
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem> <SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
<SelectItem value="Others">Others</SelectItem> <SelectItem value="Others">Others</SelectItem>
</SelectContent> </SelectContent>
@ -495,12 +494,12 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getSeverityColor(request.severity || 'Medium')}> <Badge className={getSeverityColor(request.severity || 'Medium')}>
{request.severity || 'Normal'} {request.severity || 'Normal'}
</Badge> </Badge>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
@ -518,7 +517,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Current Stage</p> <p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p> <p>{formatStatus(request.currentStage)}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Proposed LWD</p> <p className="text-slate-600">Proposed LWD</p>
@ -570,9 +569,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
@ -586,7 +585,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Current Stage</p> <p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p> <p>{formatStatus(request.currentStage)}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
@ -632,9 +631,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">

View File

@ -4,24 +4,14 @@
const PRIVATE_LIMITED = 'Private Limited'; const PRIVATE_LIMITED = 'Private Limited';
const LLP = 'LLP'; const LLP = 'LLP';
const LLP_CONVERSION = 'LLP Conversion';
const PARTNERSHIP = 'Partnership'; const PARTNERSHIP = 'Partnership';
const PARTNERSHIP_CHANGE = 'Partnership Change';
const PROPRIETORSHIP = 'Proprietorship'; const PROPRIETORSHIP = 'Proprietorship';
const DIRECTOR_CHANGE = 'Director Change';
const OWNERSHIP_TRANSFER = 'Ownership Transfer';
const COMPANY_FORMATION = 'Company Formation';
const ALL: string[] = [ const ALL: string[] = [
PROPRIETORSHIP, PROPRIETORSHIP,
PARTNERSHIP, PARTNERSHIP,
LLP_CONVERSION,
LLP, LLP,
PRIVATE_LIMITED, PRIVATE_LIMITED
COMPANY_FORMATION,
OWNERSHIP_TRANSFER,
PARTNERSHIP_CHANGE,
DIRECTOR_CHANGE
]; ];
export function isRegisteredConstitutionalChangeType(value: string): boolean { export function isRegisteredConstitutionalChangeType(value: string): boolean {
@ -44,14 +34,9 @@ export function normalizeToConstitutionalChangeType(raw: string | null | undefin
) { ) {
return PRIVATE_LIMITED; return PRIVATE_LIMITED;
} }
if (compact.includes('llp') && compact.includes('conversion')) return LLP_CONVERSION;
if (compact.includes('llp')) return LLP; if (compact.includes('llp')) return LLP;
if (compact.includes('partnership') && compact.includes('change')) return PARTNERSHIP_CHANGE;
if (compact.includes('partnership')) return PARTNERSHIP; if (compact.includes('partnership')) return PARTNERSHIP;
if (compact.includes('proprietorship') || compact === 'sole proprietorship') return PROPRIETORSHIP; if (compact.includes('proprietorship') || compact === 'sole proprietorship') return PROPRIETORSHIP;
if (compact.includes('director')) return DIRECTOR_CHANGE;
if (compact.includes('ownership') && compact.includes('transfer')) return OWNERSHIP_TRANSFER;
if (compact.includes('company') && compact.includes('formation')) return COMPANY_FORMATION;
const exact = ALL.find((v) => v.toLowerCase() === s.toLowerCase()); const exact = ALL.find((v) => v.toLowerCase() === s.toLowerCase());
return exact || null; return exact || null;
} }

View File

@ -2,20 +2,27 @@
export type UserRole = export type UserRole =
| 'DD-ZM' | 'DD-ZM'
| 'DD_ZM'
| 'RBM' | 'RBM'
| 'DD' | 'DD'
| 'ZBH' | 'ZBH'
| 'DD Lead' | 'DD Lead'
| 'DD_LEAD'
| 'DD Head' | 'DD Head'
| 'DD_HEAD'
| 'NBH' | 'NBH'
| 'DD Admin' | 'DD Admin'
| 'DD_ADMIN'
| 'Legal Admin' | 'Legal Admin'
| 'LEGAL_ADMIN'
| 'Super Admin' | 'Super Admin'
| 'SUPER_ADMIN'
| 'DD AM' | 'DD AM'
| 'FDD' | 'FDD'
| 'DDL' | 'DDL'
| 'Finance' | 'Finance'
| 'Finance Admin' | 'Finance Admin'
| 'FINANCE_ADMIN'
| 'Dealer' | 'Dealer'
| 'ASM' | 'ASM'
| 'CCO' | 'CCO'

View File

@ -2,13 +2,15 @@ export const RESIGNATION_DOCUMENT_TYPES = [
"Resignation Letter", "Resignation Letter",
"Dealer Undertaking", "Dealer Undertaking",
"Approval Note", "Approval Note",
"Legal Communication", "Resignation Acceptance Letter",
"Handover Document", "Handover Document",
"Settlement Supporting Document", "Settlement Supporting Document",
"PPT Presentation",
"Other", "Other",
] as const; ] as const;
export const RESIGNATION_STAGE_OPTIONS = [ export const RESIGNATION_STAGE_OPTIONS = [
"Request Submitted",
"ASM", "ASM",
"RBM", "RBM",
"ZBH", "ZBH",
@ -32,7 +34,7 @@ export const TERMINATION_DOCUMENT_TYPES = [
export const TERMINATION_STAGE_OPTIONS = [ export const TERMINATION_STAGE_OPTIONS = [
"Submitted", "Submitted",
"RBM Review", "RBM + DD-ZM Review",
"ZBH Review", "ZBH Review",
"DD Lead Review", "DD Lead Review",
"Legal Verification", "Legal Verification",

View File

@ -0,0 +1,5 @@
/** Legacy workflow used "Personal Hearing"; UI and newer APIs use "SCN Response Evaluation" wording. */
export function formatTerminationStatusLabel(value: string | null | undefined): string {
if (!value) return 'Pending';
return value.replace(/Personal Hearing/gi, 'SCN Response Evaluation');
}

View File

@ -0,0 +1,64 @@
/** Mirrors backend terminationJointReviewRound.util.ts — keep send-back / reconsider detection aligned. */
const norm = (s: string | undefined | null) =>
String(s || '')
.toLowerCase()
.replace(/\s+/g, ' ')
.trim();
const SCN_CANONICAL = 'Evaluation of Dealer SCN Response';
export const isScnResponseJointTargetStage = (targetStage: string | undefined | null): boolean => {
const n = norm(targetStage);
if (!n) return false;
if (n === norm(SCN_CANONICAL)) return true;
if (n.includes('evaluation') && n.includes('scn') && n.includes('response')) return true;
if (n.includes('personal hearing')) return true;
return false;
};
export const isRbmJointTargetStage = (targetStage: string | undefined | null): boolean => {
const n = norm(targetStage);
return n.includes('rbm') && (n.includes('dd-zm') || n.includes('dd zm'));
};
function isSendBackOrReconsiderTimelineAction(action: string | undefined | null): boolean {
const a = norm(action);
return (
a.includes('sent back') ||
a.includes('send back') ||
a.includes('reconsider') ||
a.includes('reconsideration')
);
}
export type JointRoundTimelineMode = 'scn_response_eval' | 'rbm_review';
export function getJointRoundCutoffMsFromTimeline(
timeline: unknown,
mode: JointRoundTimelineMode
): number | null {
if (!Array.isArray(timeline) || timeline.length === 0) return null;
const matcher = mode === 'scn_response_eval' ? isScnResponseJointTargetStage : isRbmJointTargetStage;
const arr = timeline as Record<string, unknown>[];
for (let i = arr.length - 1; i >= 0; i--) {
const e = arr[i];
if (!isSendBackOrReconsiderTimelineAction(e?.action as string)) continue;
if (!matcher(e?.targetStage as string)) continue;
const t = e?.timestamp != null ? new Date(e.timestamp as string | number | Date).getTime() : NaN;
if (!Number.isNaN(t)) return t;
}
return null;
}
export function auditLogTimestampMs(log: { createdAt?: string | Date; timestamp?: string | Date }): number {
const raw = log.createdAt ?? log.timestamp;
if (raw == null) return 0;
const t = new Date(raw).getTime();
return Number.isNaN(t) ? 0 : t;
}
export function isAuditLogInCurrentJointRound(log: { createdAt?: string | Date; timestamp?: string | Date }, cutoffMs: number | null): boolean {
if (cutoffMs == null) return true;
return auditLogTimestampMs(log) >= cutoffMs;
}

View File

@ -115,6 +115,11 @@ export const onboardingService = {
if (!response.ok) throw new Error(response.data?.message || 'Failed to perform bulk conversion'); if (!response.ok) throw new Error(response.data?.message || 'Failed to perform bulk conversion');
return response.data; return response.data;
}, },
sendBulkReminders: async (applicationIds: string[]) => {
const response: any = await API.sendBulkReminders({ applicationIds });
if (!response.ok) throw new Error(response.data?.message || 'Failed to send reminders');
return response.data;
},
createDealer: async (data: any) => { createDealer: async (data: any) => {
const response: any = await API.createDealer(data); const response: any = await API.createDealer(data);
if (!response.ok) throw new Error(response.data?.message || 'Failed to create dealer profile'); if (!response.ok) throw new Error(response.data?.message || 'Failed to create dealer profile');

View File

@ -10,7 +10,10 @@
--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-600: #da291c;
--primary-700: #b82216;
--primary-50: #fef2f2;
--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;
@ -99,6 +102,9 @@
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --color-primary: var(--primary);
--color-primary-600: var(--primary-600);
--color-primary-700: var(--primary-700);
--color-primary-50: var(--primary-50);
--color-primary-foreground: var(--primary-foreground); --color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);
@ -298,6 +304,29 @@ html {
scrollbar-color: #e2e8f0 transparent; scrollbar-color: #e2e8f0 transparent;
} }
/* Extra-thin, subtle horizontal scrollbar (documents modal tables) */
.custom-scrollbar-x-slim::-webkit-scrollbar {
height: 3px;
}
.custom-scrollbar-x-slim::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb {
background: #f1f5f9;
border-radius: 9999px;
}
.custom-scrollbar-x-slim::-webkit-scrollbar-thumb:hover {
background: #e2e8f0;
}
.custom-scrollbar-x-slim {
scrollbar-width: thin;
scrollbar-color: #f1f5f9 transparent;
}
/* Extra-thin, light vertical scrollbar (e.g. modals) */ /* Extra-thin, light vertical scrollbar (e.g. modals) */
.custom-scrollbar-slim::-webkit-scrollbar { .custom-scrollbar-slim::-webkit-scrollbar {
width: 2px; width: 2px;