in-app notiofication enhanced an SLA fe
implementation done partially
This commit is contained in:
parent
37b3075a08
commit
01e22e4aa7
@ -35,6 +35,7 @@ import { FinanceFnFDetailsPage } from '@/features/fnf/pages/FinanceFnFDetailsPag
|
||||
import { MasterPage } from '@/features/master/pages/MasterPage';
|
||||
import { UserManagementPage } from '@/components/admin/UserManagementPage';
|
||||
import { ApprovalPoliciesPage } from '@/components/admin/ApprovalPoliciesPage';
|
||||
import { SLAConfigPage } from '@/features/master/pages/SLAConfigPage';
|
||||
import { ConstitutionalChangePage } from '@/features/constitutional/pages/ConstitutionalChangePage';
|
||||
import { ConstitutionalChangeDetails } from '@/features/constitutional/pages/ConstitutionalChangeDetails';
|
||||
import { RelocationRequestPage } from '@/features/relocation/pages/RelocationRequestPage';
|
||||
@ -235,7 +236,7 @@ export default function App() {
|
||||
|
||||
{/* All Applications */}
|
||||
<Route path="/all-applications" element={
|
||||
hasRole(['DD']) ? <AllApplicationsPage onViewDetails={(id: string) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
|
||||
hasRole(['DD', 'DD Admin', 'Super Admin']) ? <AllApplicationsPage onViewDetails={(id: string) => navigate(`/applications/${id}`)} initialFilter="all" /> : <Navigate to="/dashboard" />
|
||||
} />
|
||||
|
||||
{/* FDD Routes - Integrated into Layout */}
|
||||
@ -254,6 +255,11 @@ export default function App() {
|
||||
? <ApprovalPoliciesPage />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/sla-configurations" element={
|
||||
(hasRole(['Super Admin', 'DD Admin', 'DD Lead']))
|
||||
? <SLAConfigPage />
|
||||
: <Navigate to="/dashboard" />
|
||||
} />
|
||||
<Route path="/master" element={<MasterPage />} />
|
||||
<Route path="/questions" element={<QuestionnaireList />} />
|
||||
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
||||
|
||||
@ -153,6 +153,7 @@ export const API = {
|
||||
createResignation: (data: any) => client.post('/resignation', data),
|
||||
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
||||
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data),
|
||||
withdrawResignation: (id: string, reason: string) => client.post(`/resignation/${id}/withdraw`, { reason }),
|
||||
|
||||
getTerminations: () => client.get('/termination'),
|
||||
createTermination: (data: any) => client.post('/termination', data),
|
||||
@ -164,6 +165,9 @@ export const API = {
|
||||
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
||||
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
||||
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
|
||||
uploadFnFDocument: (id: string, data: any) => client.post(`/settlement/fnf/${id}/documents`, data, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
}),
|
||||
getSettlementDepartments: () => client.get('/settlement/departments'),
|
||||
|
||||
// Line items
|
||||
@ -199,7 +203,9 @@ export const API = {
|
||||
uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }),
|
||||
|
||||
// SLA
|
||||
getSlaConfigs: () => client.get('/sla/configs'),
|
||||
getSlaConfigs: () => client.get('/master/sla-configs'),
|
||||
saveSlaConfig: (data: any) => client.post('/master/sla-configs', data),
|
||||
initializeDefaultSlas: () => client.post('/master/sla-configs/initialize'),
|
||||
|
||||
// System Configs
|
||||
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
|
||||
|
||||
@ -6,7 +6,6 @@ import {
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
Inbox,
|
||||
UserMinus,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
@ -79,13 +78,15 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
||||
];
|
||||
|
||||
/*
|
||||
// Add All Applications for DD role (before Dealership Requests)
|
||||
if (hasRole(['DD'])) {
|
||||
if (hasRole(['DD', 'DD Admin', 'Super Admin'])) {
|
||||
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
||||
}
|
||||
*/
|
||||
|
||||
// Add All Requests for DD Lead role (before Dealership Requests)
|
||||
if (hasRole(['DD Lead', 'Super Admin'])) {
|
||||
if (hasRole(['DD Lead', 'DD Admin', 'Super Admin'])) {
|
||||
menuItems.splice(1, 0, {
|
||||
id: 'all-requests',
|
||||
label: 'All Requests',
|
||||
@ -102,6 +103,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
||||
// Add Master for Super Admin, DD Admin, and DD Lead
|
||||
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
||||
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
||||
menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw });
|
||||
}
|
||||
|
||||
if (hasRole(['Super Admin'])) {
|
||||
|
||||
@ -76,7 +76,7 @@ const documentNames: Record<number, string> = {
|
||||
|
||||
// Helper functions moved above component to avoid lint errors
|
||||
const getTypeColor = (type: string) => {
|
||||
switch(type) {
|
||||
switch (type) {
|
||||
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 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
||||
@ -264,8 +264,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
|
||||
// For terminal states, resolve the last active stage from timeline
|
||||
if (['Rejected', 'Revoked', 'Withdrawn'].includes(request.status) && (currentStage === 'Rejected' || currentStage === 'Revoked' || !currentStage)) {
|
||||
const lastEntry = [...(request.timeline || [])].reverse().find(e => e.stage && !['Rejected', 'Revoked', 'REJECTED', 'REVOKED'].includes(e.stage));
|
||||
if (lastEntry) currentStage = lastEntry.stage;
|
||||
const lastEntry = [...(request.timeline || [])].reverse().find(e => e.stage && !['Rejected', 'Revoked', 'REJECTED', 'REVOKED'].includes(e.stage));
|
||||
if (lastEntry) currentStage = lastEntry.stage;
|
||||
}
|
||||
|
||||
const stageMap: Record<string, number> = {
|
||||
@ -382,7 +382,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
const jointZmRbmMeta = (meta as any)?.jointApprovals?.zmRbm || {};
|
||||
const isZmRbmStage = currentStage === 'ZM/RBM Review' || currentStage === 'ZM+RBM Review';
|
||||
const actorKey = (userRoleCode === 'RBM') ? 'RBM' :
|
||||
(userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM' || userRoleCode === 'ZM') ? 'DD-ZM' : null;
|
||||
(userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM' || userRoleCode === 'ZM') ? 'DD-ZM' : null;
|
||||
|
||||
// GAP CLOSURE: Rely primarily on metadata for joint approval status.
|
||||
const hasCurrentUserApprovedZmRbm = isZmRbmStage && actorKey && Boolean(jointZmRbmMeta?.[actorKey]?.approvedByUserId);
|
||||
@ -451,9 +451,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
if (response.data.success) {
|
||||
const actionText =
|
||||
actionType === 'approve' ? 'approved' :
|
||||
actionType === 'reject' ? 'rejected' :
|
||||
actionType === 'sendBack' ? 'sent back' :
|
||||
'revoked';
|
||||
actionType === 'reject' ? 'rejected' :
|
||||
actionType === 'sendBack' ? 'sent back' :
|
||||
'revoked';
|
||||
toast.success(`Request ${actionText} successfully`);
|
||||
setIsActionDialogOpen(false);
|
||||
setComments('');
|
||||
@ -612,12 +612,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<div>
|
||||
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
|
||||
{request.outlet?.type || 'Proprietorship'}
|
||||
<Badge className={getTypeColor(request.oldValue || request.currentConstitution || request.outlet?.type || 'Proprietorship')}>
|
||||
{request.oldValue || request.currentConstitution || request.outlet?.type || 'Proprietorship'}
|
||||
</Badge>
|
||||
<ArrowRight className="w-4 h-4 text-slate-400" />
|
||||
<Badge className={getTypeColor(request.changeType)}>
|
||||
{request.changeType}
|
||||
<Badge className={getTypeColor(request.newValue || request.changeType)}>
|
||||
{request.newValue || request.changeType}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
@ -723,131 +723,129 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
workflowStages.map((stage, index) => {
|
||||
/**
|
||||
* While DB stage is still `Submitted`, the filing step is already done; the queue is at ASM.
|
||||
* Show “Submitted” as completed and “ASM Review” as in progress (no extra dealer action).
|
||||
*/
|
||||
// Adjusted logic for streamlined workflow (no separate 'Submitted' stage gate)
|
||||
const isCompleted =
|
||||
flowComplete ||
|
||||
index < currentStageIndex - 1;
|
||||
const isCurrent =
|
||||
!flowComplete &&
|
||||
index === currentStageIndex - 1;
|
||||
workflowStages.map((stage, index) => {
|
||||
/**
|
||||
* While DB stage is still `Submitted`, the filing step is already done; the queue is at ASM.
|
||||
* Show “Submitted” as completed and “ASM Review” as in progress (no extra dealer action).
|
||||
*/
|
||||
// Adjusted logic for streamlined workflow (no separate 'Submitted' stage gate)
|
||||
const isCompleted =
|
||||
flowComplete ||
|
||||
index < currentStageIndex - 1;
|
||||
const isCurrent =
|
||||
!flowComplete &&
|
||||
index === currentStageIndex - 1;
|
||||
|
||||
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
||||
const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks;
|
||||
const isJointZmRbmStage = stage.name === 'ZM/RBM Review';
|
||||
let metaObj = request.metadata || {};
|
||||
if (typeof metaObj === 'string') {
|
||||
try { metaObj = JSON.parse(metaObj); } catch (e) { metaObj = {}; }
|
||||
}
|
||||
const jointZmRbmMetaObj = (metaObj as any)?.jointApprovals?.zmRbm || {};
|
||||
const rbmApproval = jointZmRbmMetaObj?.RBM;
|
||||
const ddZmApproval = jointZmRbmMetaObj?.['DD-ZM'];
|
||||
const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase();
|
||||
const currentRoleApproval =
|
||||
currentRoleNormalized === 'RBM'
|
||||
? rbmApproval
|
||||
: (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM')
|
||||
? ddZmApproval
|
||||
: null;
|
||||
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
||||
const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks;
|
||||
const isJointZmRbmStage = stage.name === 'ZM/RBM Review';
|
||||
let metaObj = request.metadata || {};
|
||||
if (typeof metaObj === 'string') {
|
||||
try { metaObj = JSON.parse(metaObj); } catch (e) { metaObj = {}; }
|
||||
}
|
||||
const jointZmRbmMetaObj = (metaObj as any)?.jointApprovals?.zmRbm || {};
|
||||
const rbmApproval = jointZmRbmMetaObj?.RBM;
|
||||
const ddZmApproval = jointZmRbmMetaObj?.['DD-ZM'];
|
||||
const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase();
|
||||
const currentRoleApproval =
|
||||
currentRoleNormalized === 'RBM'
|
||||
? rbmApproval
|
||||
: (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM')
|
||||
? ddZmApproval
|
||||
: null;
|
||||
|
||||
|
||||
return (
|
||||
<div key={stage.id} className="flex items-start gap-4">
|
||||
{/* Status Icon */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
||||
isCompleted ? 'bg-green-100' :
|
||||
isCurrent ? 'bg-amber-100' :
|
||||
'bg-slate-100'
|
||||
}`}>
|
||||
{isCompleted ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||
) : isCurrent ? (
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-slate-400" />
|
||||
return (
|
||||
<div key={stage.id} className="flex items-start gap-4">
|
||||
{/* Status Icon */}
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${isCompleted ? 'bg-green-100' :
|
||||
isCurrent ? 'bg-amber-100' :
|
||||
'bg-slate-100'
|
||||
}`}>
|
||||
{isCompleted ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||
) : isCurrent ? (
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
) : (
|
||||
<AlertCircle className="w-5 h-5 text-slate-400" />
|
||||
)}
|
||||
</div>
|
||||
{index < workflowStages.length - 1 && (
|
||||
<div className={`w-0.5 h-12 ${isCompleted ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
{index < workflowStages.length - 1 && (
|
||||
<div className={`w-0.5 h-12 ${
|
||||
isCompleted ? 'bg-green-300' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Stage Info */}
|
||||
<div className={`flex-1 pb-8 ${isCurrent ? 'bg-amber-50 -ml-4 pl-4 pr-4 py-3 rounded-lg border border-amber-200' : ''}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}>
|
||||
{formatStageLabel(stage.name)}
|
||||
</h4>
|
||||
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||
{`Responsible: ${formatStageRole(stage.role)}`}
|
||||
</p>
|
||||
{/* Stage Info */}
|
||||
<div className={`flex-1 pb-8 ${isCurrent ? 'bg-amber-50 -ml-4 pl-4 pr-4 py-3 rounded-lg border border-amber-200' : ''}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}>
|
||||
{formatStageLabel(stage.name)}
|
||||
</h4>
|
||||
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}>
|
||||
{`Responsible: ${formatStageRole(stage.role)}`}
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<Badge className={
|
||||
isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
|
||||
isCurrent ? 'bg-amber-100 text-amber-700 border-amber-300' :
|
||||
'bg-slate-100 text-slate-500 border-slate-300'
|
||||
}>
|
||||
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
{timelineEntry && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-600">
|
||||
<Badge variant="outline" className="text-[11px] normal-case">
|
||||
Last updated by: {timelineEntry.user || timelineEntry.userName || 'System'}
|
||||
</Badge>
|
||||
<span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
|
||||
</div>
|
||||
{explicitFeedback && (
|
||||
<div className="p-2 rounded border border-slate-200 bg-slate-50 text-sm text-slate-700">
|
||||
{explicitFeedback}
|
||||
</div>
|
||||
)}
|
||||
<Badge className={
|
||||
isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
|
||||
isCurrent ? 'bg-amber-100 text-amber-700 border-amber-300' :
|
||||
'bg-slate-100 text-slate-500 border-slate-300'
|
||||
}>
|
||||
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{isJointZmRbmStage && (
|
||||
<div className="mt-3 rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<p className="text-xs text-slate-600 mb-2">Joint approval status</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge className={rbmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||
RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||
</Badge>
|
||||
<Badge className={ddZmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||
</Badge>
|
||||
{currentRoleApproval?.approvedByUserId && (
|
||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
||||
Approved by you
|
||||
{timelineEntry && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-600">
|
||||
<Badge variant="outline" className="text-[11px] normal-case">
|
||||
Last updated by: {timelineEntry.user || timelineEntry.userName || 'System'}
|
||||
</Badge>
|
||||
<span>{formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}</span>
|
||||
</div>
|
||||
{explicitFeedback && (
|
||||
<div className="p-2 rounded border border-slate-200 bg-slate-50 text-sm text-slate-700">
|
||||
{explicitFeedback}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(rbmApproval?.remarks || ddZmApproval?.remarks) && (
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="rounded border border-slate-200 bg-white p-2">
|
||||
<p className="text-[11px] text-slate-500 mb-1">RBM Comment</p>
|
||||
<p className="text-sm text-slate-700">{rbmApproval?.remarks || '-'}</p>
|
||||
</div>
|
||||
<div className="rounded border border-slate-200 bg-white p-2">
|
||||
<p className="text-[11px] text-slate-500 mb-1">DD-ZM Comment</p>
|
||||
<p className="text-sm text-slate-700">{ddZmApproval?.remarks || '-'}</p>
|
||||
</div>
|
||||
)}
|
||||
{isJointZmRbmStage && (
|
||||
<div className="mt-3 rounded-md border border-slate-200 bg-slate-50 p-3">
|
||||
<p className="text-xs text-slate-600 mb-2">Joint approval status</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Badge className={rbmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||
RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||
</Badge>
|
||||
<Badge className={ddZmApproval?.approvedByUserId ? 'bg-green-100 text-green-700 border-green-300' : 'bg-slate-100 text-slate-500 border-slate-300'}>
|
||||
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
|
||||
</Badge>
|
||||
{currentRoleApproval?.approvedByUserId && (
|
||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">
|
||||
Approved by you
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(rbmApproval?.remarks || ddZmApproval?.remarks) && (
|
||||
<div className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-2">
|
||||
<div className="rounded border border-slate-200 bg-white p-2">
|
||||
<p className="text-[11px] text-slate-500 mb-1">RBM Comment</p>
|
||||
<p className="text-sm text-slate-700">{rbmApproval?.remarks || '-'}</p>
|
||||
</div>
|
||||
<div className="rounded border border-slate-200 bg-white p-2">
|
||||
<p className="text-[11px] text-slate-500 mb-1">DD-ZM Comment</p>
|
||||
<p className="text-sm text-slate-700">{ddZmApproval?.remarks || '-'}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
@ -927,10 +925,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
return (
|
||||
<div
|
||||
key={docNum}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${
|
||||
isRejected ? 'bg-red-50 border-red-200' :
|
||||
ok ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
|
||||
}`}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border ${isRejected ? 'bg-red-50 border-red-200' :
|
||||
ok ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
{isRejected ? (
|
||||
@ -1013,29 +1010,34 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<Button size="sm" variant="outline" className="h-8 w-8 p-0" title="Download document">
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
{doc.status !== 'Verified' && doc.status !== 'Rejected' && currentUser?.role !== 'Dealer' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => handleVerifyDocument(doc, index)}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-red-300 text-red-700 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
setRejectDocIndex(index);
|
||||
setRejectDocReason('');
|
||||
setRejectDocDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{doc.status !== 'Verified' && doc.status !== 'Rejected' && (() => {
|
||||
const role = currentUser?.role || currentUser?.roleCode || '';
|
||||
// SRS §12.2 — only authorized review roles can verify constitutional documents
|
||||
const canVerifyDocs = ['DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role);
|
||||
return canVerifyDocs;
|
||||
})() && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700"
|
||||
onClick={() => handleVerifyDocument(doc, index)}
|
||||
>
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="border-red-300 text-red-700 hover:bg-red-50"
|
||||
onClick={() => {
|
||||
setRejectDocIndex(index);
|
||||
setRejectDocReason('');
|
||||
setRejectDocDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
@ -1053,56 +1055,73 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
|
||||
{/* History Tab */}
|
||||
{/* History Tab */}
|
||||
<TabsContent value="history" className="mt-0">
|
||||
<div className="space-y-4">
|
||||
{historyEntries.map((entry: any, index: number) => {
|
||||
const pres = getConstitutionalHistoryPresentation(entry);
|
||||
return (
|
||||
<div key={entry.id || index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${
|
||||
pres.variant === 'success' ? 'bg-green-100' :
|
||||
pres.variant === 'danger' ? 'bg-red-100' :
|
||||
pres.variant === 'pending' ? 'bg-amber-100' :
|
||||
'bg-slate-100'
|
||||
}`}>
|
||||
{pres.variant === 'success' ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||
) : pres.variant === 'danger' ? (
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
) : pres.variant === 'pending' ? (
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-slate-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="text-slate-900">{entry.stage || entry.action}</h4>
|
||||
<p className="text-slate-600 text-sm">{entry.userName || 'System'}</p>
|
||||
</div>
|
||||
<Badge className={getStatusColor(pres.badge)}>
|
||||
{pres.badge}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-2 p-3 bg-slate-50 rounded border border-slate-100 italic text-slate-700 text-sm">
|
||||
<span className="font-semibold non-italic text-slate-500 mr-2">Comments:</span>
|
||||
{/*
|
||||
Audit API puts user-entered text in `remarks` and a generated summary in `description`
|
||||
(e.g. "Approval - Stage: ZM/RBM Review"). Prefer remarks so History matches the approve modal / Work Notes.
|
||||
*/}
|
||||
{String(entry.remarks || '').trim() ||
|
||||
String(entry.description || '').trim() ||
|
||||
'No remarks provided'}
|
||||
</div>
|
||||
<p className="text-slate-400 text-xs mt-2 font-medium uppercase tracking-wider">
|
||||
{formatDateTime(entry.timestamp || entry.createdAt)}
|
||||
</p>
|
||||
|
||||
// Clean actor name — deduplicate "Legal Admin · Legal Admin" pattern
|
||||
const rawActor = entry.actor?.name || entry.userName || 'System';
|
||||
const actorParts = rawActor.split('·').map((p: string) => p.trim());
|
||||
const actorName = [...new Set(actorParts)].join(', ');
|
||||
|
||||
// Clean action label — strip [Master data] and UUID/technical prefixes
|
||||
const rawStage = String(entry.stage || entry.action || '').trim();
|
||||
const cleanStage = rawStage
|
||||
.replace(/\[Master data\]\s*/i, '')
|
||||
.replace(/Constitutional change [A-Z0-9-]+ (completed|updated):/i, '$1:')
|
||||
.replace(/dealer constitution updated from "(.*)" to "(.*)"\.?/i, '"$1" → "$2"')
|
||||
.trim();
|
||||
|
||||
// Human-readable heading: prefer stage, fall back to cleaned description
|
||||
const heading = cleanStage || pres.badge || 'Action';
|
||||
|
||||
// Remarks: prefer user-entered remarks, strip auto-generated system strings
|
||||
const rawRemarks = String(entry.remarks || '').trim();
|
||||
const rawDescription = String(entry.description || '').trim();
|
||||
const systemPrefixes = [/^Approval\s*-\s*Stage:/i, /^Record (created|updated)/i, /^Document (uploaded|verified|rejected)/i];
|
||||
const isSystemDesc = systemPrefixes.some(re => re.test(rawDescription));
|
||||
const displayRemarks = rawRemarks || (!isSystemDesc ? rawDescription : '') || null;
|
||||
|
||||
return (
|
||||
<div key={entry.id || index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${pres.variant === 'success' ? 'bg-green-100' :
|
||||
pres.variant === 'danger' ? 'bg-red-100' :
|
||||
pres.variant === 'pending' ? 'bg-amber-100' :
|
||||
'bg-slate-100'
|
||||
}`}>
|
||||
{pres.variant === 'success' ? (
|
||||
<CheckCircle2 className="w-5 h-5 text-green-600" />
|
||||
) : pres.variant === 'danger' ? (
|
||||
<AlertCircle className="w-5 h-5 text-red-600" />
|
||||
) : pres.variant === 'pending' ? (
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
) : (
|
||||
<Clock className="w-5 h-5 text-slate-500" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="text-slate-900 capitalize">{heading}</h4>
|
||||
<p className="text-slate-500 text-sm">by <span className="font-medium text-slate-700">{actorName}</span></p>
|
||||
</div>
|
||||
<Badge className={getStatusColor(pres.badge)}>
|
||||
{pres.badge.replace(/_/g, ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
{displayRemarks && (
|
||||
<div className="mt-2 p-3 bg-slate-50 rounded border border-slate-100 text-slate-700 text-sm">
|
||||
{displayRemarks}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-slate-400 text-xs mt-2 font-medium uppercase tracking-wider">
|
||||
{formatDateTime(entry.timestamp || entry.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
})}
|
||||
{historyEntries.length === 0 && (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
@ -1219,7 +1238,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-t border-slate-200 pt-3 mt-3">
|
||||
<div className="border-t border-slate-200 pt-3 mt-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full border-blue-700 text-blue-800 hover:bg-blue-50"
|
||||
@ -1247,9 +1266,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{actionType === 'approve' ? 'Approve request' :
|
||||
actionType === 'reject' ? 'Reject proposal' :
|
||||
actionType === 'sendBack' ? 'Send back to previous stage' :
|
||||
'Revoke request'}
|
||||
actionType === 'reject' ? 'Reject proposal' :
|
||||
actionType === 'sendBack' ? 'Send back to previous stage' :
|
||||
'Revoke request'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionType === 'sendBack' || actionType === 'revoke'
|
||||
@ -1263,15 +1282,15 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
<Label htmlFor="comments">
|
||||
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="comments"
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
placeholder={actionType === 'sendBack' || actionType === 'revoke' ? 'Enter mandatory remarks for Work Notes…' : 'Enter your comments…'}
|
||||
rows={4}
|
||||
required={actionType !== 'approve'}
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
id="comments"
|
||||
value={comments}
|
||||
onChange={(e) => setComments(e.target.value)}
|
||||
placeholder={actionType === 'sendBack' || actionType === 'revoke' ? 'Enter mandatory remarks for Work Notes…' : 'Enter your comments…'}
|
||||
rows={4}
|
||||
required={actionType !== 'approve'}
|
||||
/>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
@ -1281,28 +1300,28 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className={
|
||||
actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||
<Button
|
||||
type="submit"
|
||||
className={
|
||||
actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' :
|
||||
actionType === 'reject' ? 'bg-red-600 hover:bg-red-700' :
|
||||
actionType === 'sendBack' ? 'bg-amber-600 hover:bg-amber-700' :
|
||||
'bg-orange-600 hover:bg-orange-700'
|
||||
}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
actionType === 'approve' ? 'Approve' :
|
||||
actionType === 'sendBack' ? 'bg-amber-600 hover:bg-amber-700' :
|
||||
'bg-orange-600 hover:bg-orange-700'
|
||||
}
|
||||
disabled={isActionLoading}
|
||||
>
|
||||
{isActionLoading ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
</>
|
||||
) : (
|
||||
actionType === 'approve' ? 'Approve' :
|
||||
actionType === 'reject' ? 'Reject' :
|
||||
actionType === 'sendBack' ? 'Send back' :
|
||||
'Revoke'
|
||||
)}
|
||||
</Button>
|
||||
actionType === 'sendBack' ? 'Send back' :
|
||||
'Revoke'
|
||||
)}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
|
||||
@ -101,7 +101,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
|
||||
color: 'bg-yellow-500',
|
||||
trend: { value: 2, isPositive: false },
|
||||
filter: 'all',
|
||||
action: 'all-applications' // Special action to navigate to all applications page
|
||||
action: 'opportunity-requests' // Changed from all-applications as per user request to hide that option
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
@ -258,7 +258,14 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
uploadedOn: formatDateTime(c.clearedAt),
|
||||
type: `${c.department} Proof`,
|
||||
url: c.supportingDocument
|
||||
}))
|
||||
})),
|
||||
...(s.clearanceDocuments || []).map((doc: any) => ({
|
||||
name: doc.name || doc.supportingDocument?.split('/').pop() || 'Document',
|
||||
size: 'N/A',
|
||||
uploadedOn: formatDateTime(doc.clearedAt || s.createdAt),
|
||||
type: 'Finance Upload',
|
||||
url: doc.supportingDocument
|
||||
}))
|
||||
]
|
||||
};
|
||||
|
||||
@ -448,8 +455,6 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
adjustments: '0'
|
||||
});
|
||||
|
||||
const [uploadedDocuments, setUploadedDocuments] = useState<any[]>([]);
|
||||
|
||||
// Handlers for Payables
|
||||
const handleAddPayable = async () => {
|
||||
if (!newPayable.department || !newPayable.description || !newPayable.amount) {
|
||||
@ -702,17 +707,26 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileUpload = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const handleFileUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (files && files.length > 0) {
|
||||
const newDocs = Array.from(files).map(file => ({
|
||||
name: file.name,
|
||||
size: `${(file.size / 1024).toFixed(0)} KB`,
|
||||
uploadedOn: new Date().toISOString(),
|
||||
type: 'Settlement Verification'
|
||||
}));
|
||||
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
|
||||
toast.success(`${files.length} document(s) uploaded successfully`);
|
||||
setLoading(true);
|
||||
try {
|
||||
let successCount = 0;
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', files[i]);
|
||||
const response: any = await API.uploadFnFDocument(fnfId, formData);
|
||||
if (response.data?.success) successCount++;
|
||||
}
|
||||
|
||||
toast.success(`${successCount} document(s) uploaded successfully`);
|
||||
fetchFnFDetails(false); // Fetch latest documents
|
||||
} catch (error) {
|
||||
toast.error('Failed to upload document(s)');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -1798,9 +1812,48 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
<p className="text-sm text-slate-500">{doc.size} • {doc.type} • Uploaded on {doc.uploadedOn}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="outline" size="sm">
|
||||
Download
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
{doc.url && doc.url !== '#' && (
|
||||
<button
|
||||
onClick={() => setPreviewDocument({
|
||||
fileName: doc.name,
|
||||
filePath: doc.url,
|
||||
documentType: doc.type
|
||||
})}
|
||||
className="text-amber-600 hover:text-amber-700 text-[10px] font-semibold flex items-center gap-1"
|
||||
>
|
||||
<Paperclip className="w-3 h-3" /> PREVIEW
|
||||
</button>
|
||||
)}
|
||||
<Button variant="outline" size="sm" onClick={async () => {
|
||||
if (doc.url && doc.url !== '#') {
|
||||
try {
|
||||
const response = await fetch(doc.url);
|
||||
const blob = await response.blob();
|
||||
const blobUrl = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = blobUrl;
|
||||
link.download = doc.name || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(blobUrl);
|
||||
} catch (e) {
|
||||
// Fallback if CORS prevents blob fetch
|
||||
const link = document.createElement('a');
|
||||
link.href = doc.url;
|
||||
link.download = doc.name || 'download';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
} else {
|
||||
toast.error('Document URL not available');
|
||||
}
|
||||
}}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -1838,23 +1891,6 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
</Button>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{uploadedDocuments.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<Label>Uploaded Documents</Label>
|
||||
{uploadedDocuments.map((doc: any, index: number) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-green-50 rounded-lg border border-green-200">
|
||||
<div className="flex items-center gap-3">
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
<div>
|
||||
<p className="text-slate-900">{doc.name}</p>
|
||||
<p className="text-sm text-slate-500">{doc.size} • {doc.type}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -314,7 +314,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant={fnfCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
|
||||
className={fnfCase.status === 'Settlement Approved' ? 'bg-green-600' : 'bg-amber-600'}
|
||||
className={fnfCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-amber-600 text-white'}
|
||||
>
|
||||
{fnfCase.status}
|
||||
</Badge>
|
||||
@ -695,7 +695,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
||||
<p className="text-sm text-slate-500">Status</p>
|
||||
<Badge
|
||||
variant={selectedCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
|
||||
className={selectedCase.status === 'Settlement Approved' ? 'bg-green-600' : 'bg-amber-600'}
|
||||
className={selectedCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-amber-600 text-white'}
|
||||
>
|
||||
{selectedCase.status}
|
||||
</Badge>
|
||||
|
||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Clock, Plus, Edit2, AlertTriangle, Bell, Save } from 'lucide-react';
|
||||
import { Clock, Pen, Bell, AlertTriangle, CheckCircle } from 'lucide-react';
|
||||
import { RootState } from '@/store';
|
||||
|
||||
interface SLAConfigurationProps {
|
||||
@ -18,31 +18,34 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>SLA & Escalation Matrix</CardTitle>
|
||||
<CardDescription>Configure Turn Around Time (TAT) and escalation rules for each process stage</CardDescription>
|
||||
<CardTitle>SLA Configuration</CardTitle>
|
||||
<CardDescription>Configure service level agreements, reminders, and escalations for each stage</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-4">
|
||||
{slaConfigs.map((sla) => (
|
||||
<div key={sla.id} className="border rounded-xl p-5 space-y-4 bg-white shadow-sm hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<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-center gap-3">
|
||||
<div className="w-10 h-10 bg-amber-50 rounded-lg flex items-center justify-center border border-amber-100">
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
<div>
|
||||
<h4 className="text-slate-900 font-medium">{sla.stage}</h4>
|
||||
<p className="text-xs text-slate-500">Target TAT: <span className="text-amber-600 font-bold">{sla.days} Days</span></p>
|
||||
<h4 className="text-slate-900 font-medium">{sla.activityName}</h4>
|
||||
<p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={sla.enabled ? "default" : "secondary"} className={sla.enabled ? "bg-emerald-500" : ""}>
|
||||
{sla.enabled ? 'Enabled' : 'Disabled'}
|
||||
<Badge className={sla.isActive ? "bg-green-600" : "bg-slate-400"}>
|
||||
{sla.isActive ? (
|
||||
<><CheckCircle className="w-3 h-3 mr-1" /> Active</>
|
||||
) : (
|
||||
'Inactive'
|
||||
)}
|
||||
</Badge>
|
||||
<Button variant="ghost" size="sm" onClick={() => onConfigureSLA(sla)} className="text-slate-400 hover:text-amber-600">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
<Button variant="outline" size="sm" onClick={() => onConfigureSLA(sla)} className="h-8 gap-1.5 px-3">
|
||||
<Pen className="w-3 h-3 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -51,42 +54,60 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
|
||||
<div className="border-l-2 border-blue-400 pl-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Bell className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-xs text-slate-700 font-medium">Reminders ({sla.reminders.length})</span>
|
||||
<span className="text-xs text-slate-700">Reminders ({sla.reminders?.length || 0})</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{sla.reminders.map((reminder: any, idx: number) => (
|
||||
{(sla.reminders || []).map((reminder: any, idx: number) => (
|
||||
<div key={idx} className="text-xs text-slate-600 flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1 bg-blue-50 border-blue-200">
|
||||
{reminder.time} {reminder.unit}
|
||||
<Badge variant="outline" className="text-xs bg-blue-50 border-blue-200">
|
||||
{reminder.timeValue} {reminder.timeUnit}
|
||||
</Badge>
|
||||
<span>before</span>
|
||||
<span>before SLA</span>
|
||||
</div>
|
||||
))}
|
||||
{(!sla.reminders || sla.reminders.length === 0) && (
|
||||
<p className="text-[10px] text-slate-400">No reminders configured</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-red-400 pl-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="w-4 h-4 text-red-600" />
|
||||
<span className="text-xs text-slate-700 font-medium">Escalations ({sla.escalations.length})</span>
|
||||
<span className="text-xs text-slate-700">Escalations ({sla.escalationConfigs?.length || 0})</span>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{sla.escalations.map((escalation: any, idx: number) => (
|
||||
{(sla.escalationConfigs || []).map((esc: any, idx: number) => (
|
||||
<div key={idx} className="text-xs">
|
||||
<Badge variant="outline" className="text-[10px] h-4 px-1 bg-red-50 border-red-200">
|
||||
L{escalation.level}
|
||||
</Badge>
|
||||
<span className="text-slate-400 ml-1">→ {escalation.userEmail.split('@')[0]}</span>
|
||||
<div className="flex items-center gap-1 text-slate-900">
|
||||
<Badge variant="outline" className="text-xs bg-red-50 border-red-200">
|
||||
L{esc.level}
|
||||
</Badge>
|
||||
<span>after {esc.timeValue} {esc.timeUnit}</span>
|
||||
</div>
|
||||
<p className="text-slate-500 ml-9">→ {esc.notifyEmail}</p>
|
||||
</div>
|
||||
))}
|
||||
{(!sla.escalationConfigs || sla.escalationConfigs.length === 0) && (
|
||||
<p className="text-[10px] text-slate-400">No escalations configured</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{slaConfigs.length === 0 && (
|
||||
<div className="text-center py-12 border-2 border-dashed rounded-xl">
|
||||
<Clock className="w-12 h-12 text-slate-300 mx-auto mb-3" />
|
||||
<p className="text-slate-500 font-medium">No SLA configurations found</p>
|
||||
<p className="text-slate-400 text-sm">Create a migration to seed default TAT values</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
||||
356
src/features/master/components/SLADialog.tsx
Normal file
356
src/features/master/components/SLADialog.tsx
Normal file
@ -0,0 +1,356 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Plus, Trash2, Bell, AlertTriangle } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
import { masterService } from '@/services/master.service';
|
||||
import { ROLES, STAGES_MAP } from '@/lib/constants';
|
||||
|
||||
interface SLADialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
sla: any | null;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSave }) => {
|
||||
const [formData, setFormData] = useState<any>({
|
||||
activityName: '',
|
||||
tatHours: 24,
|
||||
tatUnit: 'hours',
|
||||
isActive: true,
|
||||
reminders: [],
|
||||
escalationConfigs: []
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (sla) {
|
||||
setFormData({
|
||||
...sla,
|
||||
reminders: sla.reminders || [],
|
||||
escalationConfigs: sla.escalationConfigs || []
|
||||
});
|
||||
}
|
||||
}, [sla]);
|
||||
|
||||
const handleAddReminder = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
reminders: [...formData.reminders, { timeValue: 1, timeUnit: 'days', isEnabled: true }]
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveReminder = (index: number) => {
|
||||
const newReminders = [...formData.reminders];
|
||||
newReminders.splice(index, 1);
|
||||
setFormData({ ...formData, reminders: newReminders });
|
||||
};
|
||||
|
||||
const handleAddEscalation = () => {
|
||||
setFormData({
|
||||
...formData,
|
||||
escalationConfigs: [
|
||||
...formData.escalationConfigs,
|
||||
{ level: formData.escalationConfigs.length + 1, timeValue: 1, timeUnit: 'days', notifyEmail: '' }
|
||||
]
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveEscalation = (index: number) => {
|
||||
const newEsc = [...formData.escalationConfigs];
|
||||
newEsc.splice(index, 1);
|
||||
// Re-adjust levels
|
||||
const adjustedEsc = newEsc.map((e, i) => ({ ...e, level: i + 1 }));
|
||||
setFormData({ ...formData, escalationConfigs: adjustedEsc });
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!formData.activityName || !formData.ownerRole) {
|
||||
toast.error('Activity Name and Owner Role are required');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await masterService.saveSlaConfig(formData);
|
||||
toast.success(sla?.id ? 'SLA Configuration updated' : 'New SLA Configuration created');
|
||||
onSave();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Save SLA error:', error);
|
||||
toast.error('Failed to save SLA configuration');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{sla?.id ? 'Configure SLA' : 'Add New SLA'}: {formData.activityName || 'New Activity'}</DialogTitle>
|
||||
<DialogDescription>
|
||||
Define the Turn Around Time and notification rules for this stage.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6 py-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="activityName">Activity Name (Workflow Stage)</Label>
|
||||
<Select
|
||||
value={formData.activityName}
|
||||
onValueChange={(value) => {
|
||||
let inferredRole = formData.ownerRole;
|
||||
// Try to find the default owner in any module
|
||||
for (const moduleStages of Object.values(STAGES_MAP)) {
|
||||
if ((moduleStages as any)[value]) {
|
||||
inferredRole = (moduleStages as any)[value];
|
||||
break;
|
||||
}
|
||||
}
|
||||
setFormData({ ...formData, activityName: value, ownerRole: inferredRole });
|
||||
}}
|
||||
disabled={!!sla?.id}
|
||||
>
|
||||
<SelectTrigger id="activityName">
|
||||
<SelectValue placeholder="Select Stage" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.entries(STAGES_MAP).map(([module, stages]) => (
|
||||
<React.Fragment key={module}>
|
||||
<div className="px-2 py-1.5 text-xs font-semibold text-slate-500 bg-slate-50">{module}</div>
|
||||
{Object.keys(stages).map(stage => (
|
||||
<SelectItem key={stage} value={stage}>{stage}</SelectItem>
|
||||
))}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="ownerRole">Owner Role (Auto-resolved)</Label>
|
||||
<Select
|
||||
value={formData.ownerRole}
|
||||
onValueChange={(value) => setFormData({ ...formData, ownerRole: value })}
|
||||
disabled={!!sla?.id}
|
||||
>
|
||||
<SelectTrigger id="ownerRole">
|
||||
<SelectValue placeholder="Select Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(ROLES).map(role => (
|
||||
<SelectItem key={role} value={role}>{role}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tatHours">Target TAT</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="tatHours"
|
||||
type="number"
|
||||
value={formData.tatHours}
|
||||
onChange={(e) => setFormData({ ...formData, tatHours: parseInt(e.target.value) })}
|
||||
/>
|
||||
<Select
|
||||
value={formData.tatUnit}
|
||||
onValueChange={(value) => setFormData({ ...formData, tatUnit: value })}
|
||||
>
|
||||
<SelectTrigger className="w-32">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2 pt-8">
|
||||
<Switch
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
|
||||
/>
|
||||
<Label htmlFor="isActive">Active SLA Tracking</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Bell className="w-4 h-4 text-blue-600" />
|
||||
<h4 className="font-medium text-sm">Reminders</h4>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleAddReminder} className="h-7 text-xs">
|
||||
<Plus className="w-3 h-3 mr-1" /> Add Reminder
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{formData.reminders.map((reminder: any, idx: number) => (
|
||||
<div key={idx} className="flex items-center gap-3 bg-slate-50 p-2 rounded-lg border border-slate-100">
|
||||
<span className="text-xs font-medium text-slate-500 w-12 text-center">Before</span>
|
||||
<Input
|
||||
type="number"
|
||||
className="w-20 h-8"
|
||||
value={reminder.timeValue}
|
||||
onChange={(e) => {
|
||||
const newReminders = [...formData.reminders];
|
||||
newReminders[idx].timeValue = parseInt(e.target.value);
|
||||
setFormData({ ...formData, reminders: newReminders });
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={reminder.timeUnit}
|
||||
onValueChange={(value) => {
|
||||
const newReminders = [...formData.reminders];
|
||||
newReminders[idx].timeUnit = value;
|
||||
setFormData({ ...formData, reminders: newReminders });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-24 h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveReminder(idx)}
|
||||
className="h-8 w-8 p-0 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{formData.reminders.length === 0 && (
|
||||
<p className="text-center text-xs text-slate-400 py-2">No reminders set</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between border-b pb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 text-red-600" />
|
||||
<h4 className="font-medium text-sm">Escalation Levels</h4>
|
||||
</div>
|
||||
<Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs">
|
||||
<Plus className="w-3 h-3 mr-1" /> Add Level
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{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 className="flex items-center justify-between">
|
||||
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-100">
|
||||
Level {esc.level}
|
||||
</Badge>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleRemoveEscalation(idx)}
|
||||
className="h-7 w-7 p-0 text-slate-400 hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[10px] uppercase text-slate-500">Escalate After Breaching By</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
type="number"
|
||||
className="h-8"
|
||||
value={esc.timeValue}
|
||||
onChange={(e) => {
|
||||
const newEsc = [...formData.escalationConfigs];
|
||||
newEsc[idx].timeValue = parseInt(e.target.value);
|
||||
setFormData({ ...formData, escalationConfigs: newEsc });
|
||||
}}
|
||||
/>
|
||||
<Select
|
||||
value={esc.timeUnit}
|
||||
onValueChange={(value) => {
|
||||
const newEsc = [...formData.escalationConfigs];
|
||||
newEsc[idx].timeUnit = value;
|
||||
setFormData({ ...formData, escalationConfigs: newEsc });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-24 h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="hours">Hours</SelectItem>
|
||||
<SelectItem value="days">Days</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-[10px] uppercase text-slate-500">Notification Recipient (Role)</Label>
|
||||
<Select
|
||||
value={esc.notifyRole || ''}
|
||||
onValueChange={(value) => {
|
||||
const newEsc = [...formData.escalationConfigs];
|
||||
newEsc[idx].notifyRole = value;
|
||||
newEsc[idx].notifyEmail = ''; // Clear explicit email
|
||||
setFormData({ ...formData, escalationConfigs: newEsc });
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue placeholder="Select Recipient Role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{Object.values(ROLES).map(role => (
|
||||
<SelectItem key={role} value={role}>{role}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{formData.escalationConfigs.length === 0 && (
|
||||
<p className="text-center text-xs text-slate-400 py-2">No escalation levels defined</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="pt-4 border-t">
|
||||
<Button type="button" variant="outline" onClick={onClose} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? 'Saving...' : 'Save Configuration'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import {
|
||||
Tabs, TabsContent, TabsList, TabsTrigger
|
||||
} from '@/components/ui/tabs';
|
||||
import { Globe, Shield, Clock, Mail, MapPin, SlidersHorizontal, Settings, FileText } from 'lucide-react';
|
||||
import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -18,7 +18,6 @@ import { RegionalManagement } from '@/features/master/components/RegionalManagem
|
||||
import { ASMManagement } from '@/features/master/components/ASMManagement';
|
||||
import { ZMManagement } from '@/features/master/components/ZMManagement';
|
||||
import { UserManagementTable } from '@/features/master/components/UserManagementTable';
|
||||
import { SLAConfiguration } from '@/features/master/components/SLAConfiguration';
|
||||
import { RolePermissions } from '@/features/master/components/RolePermissions';
|
||||
import { RoleDialog } from '@/features/master/components/RoleDialog';
|
||||
import { AddRoleDialog } from '@/features/master/components/AddRoleDialog';
|
||||
@ -396,7 +395,7 @@ export const MasterPage: React.FC = () => {
|
||||
const payload = {
|
||||
id: editingLocationId,
|
||||
stateId: locationState,
|
||||
stateName: selectedState?.name || selectedState?.stateName || '',
|
||||
stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '',
|
||||
districtId: locationDistrict,
|
||||
name: locationCity || selectedDistrict?.name || 'New Location',
|
||||
city: locationCity,
|
||||
@ -440,16 +439,13 @@ export const MasterPage: React.FC = () => {
|
||||
</div>
|
||||
) : (
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-7 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-6 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">
|
||||
<Globe className="w-4 h-4" /> Organisation
|
||||
</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">
|
||||
<Shield className="w-4 h-4" /> Roles
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sla" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white">
|
||||
<Clock className="w-4 h-4" /> SLA Config
|
||||
</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">
|
||||
<Mail className="w-4 h-4" /> Emails
|
||||
</TabsTrigger>
|
||||
@ -505,10 +501,6 @@ export const MasterPage: React.FC = () => {
|
||||
onEditRole={handleEditRole} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sla" className="animate-in fade-in duration-300">
|
||||
<SLAConfiguration onConfigureSLA={() => toast.info('SLA Matrix Configuration interface being updated')} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="templates" className="animate-in fade-in duration-300">
|
||||
<EmailTemplates
|
||||
onEditTemplate={(template) => {
|
||||
|
||||
180
src/features/master/pages/SLAConfigPage.tsx
Normal file
180
src/features/master/pages/SLAConfigPage.tsx
Normal file
@ -0,0 +1,180 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { RootState } from '@/store';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Clock, Plus, Pen, Bell, AlertTriangle, CheckCircle, RefreshCw } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { masterService } from '@/services/master.service';
|
||||
import { setMasterData } from '@/store/slices/masterSlice';
|
||||
import { SLADialog } from '@/features/master/components/SLADialog';
|
||||
|
||||
export const SLAConfigPage: React.FC = () => {
|
||||
const dispatch = useDispatch();
|
||||
const { slaConfigs, loading } = useSelector((state: RootState) => state.master);
|
||||
const [showSLADialog, setShowSLADialog] = useState(false);
|
||||
const [selectedSLA, setSelectedSLA] = useState<any>(null);
|
||||
|
||||
const fetchConfigs = async () => {
|
||||
try {
|
||||
const res = await masterService.getSlaConfigs() as any;
|
||||
if (res && res.success) {
|
||||
dispatch(setMasterData({ slaConfigs: res.data }));
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to fetch SLA configurations');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfigs();
|
||||
}, []);
|
||||
|
||||
const handleInitialize = async () => {
|
||||
try {
|
||||
setLoadingMore(true);
|
||||
const res = await masterService.initializeDefaultSlas() as any;
|
||||
if (res && res.success) {
|
||||
toast.success('Default SLAs initialized successfully');
|
||||
fetchConfigs();
|
||||
}
|
||||
} catch (error) {
|
||||
toast.error('Failed to initialize default SLAs');
|
||||
} finally {
|
||||
setLoadingMore(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = (sla: any) => {
|
||||
setSelectedSLA(sla);
|
||||
setShowSLADialog(true);
|
||||
};
|
||||
|
||||
const handleAddSLA = () => {
|
||||
setSelectedSLA(null);
|
||||
setShowSLADialog(true);
|
||||
};
|
||||
|
||||
const [loadingMore, setLoadingMore] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-7xl mx-auto">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
||||
<Clock className="w-6 h-6 text-amber-600" />
|
||||
SLA & Escalation Matrix
|
||||
</h1>
|
||||
<p className="text-slate-500">Configure Turn Around Time (TAT) and escalation rules for each process stage</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button variant="outline" onClick={handleInitialize} disabled={loading || loadingMore}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${loadingMore ? 'animate-spin' : ''}`} />
|
||||
Initialize Defaults
|
||||
</Button>
|
||||
<Button onClick={handleAddSLA} disabled={loading}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Manual SLA
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={fetchConfigs} disabled={loading}>
|
||||
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{slaConfigs.map((sla) => (
|
||||
<Card key={sla.id} className="border-slate-200 hover:shadow-md transition-shadow group overflow-hidden">
|
||||
<CardHeader className="pb-3 bg-gradient-to-br from-white to-slate-50/50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-10 h-10 rounded-xl bg-amber-50 border border-amber-100 flex items-center justify-center">
|
||||
<Clock className="w-5 h-5 text-amber-600" />
|
||||
</div>
|
||||
<div>
|
||||
<CardTitle className="text-lg">{sla.activityName}</CardTitle>
|
||||
<CardDescription className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className="font-semibold text-amber-700">Target TAT: {sla.tatHours} {sla.tatUnit}</span>
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={sla.isActive ? "default" : "secondary"} className={sla.isActive ? "bg-emerald-600" : "bg-slate-400"}>
|
||||
{sla.isActive ? (
|
||||
<><CheckCircle className="w-3 h-3 mr-1" /> Active</>
|
||||
) : (
|
||||
'Disabled'
|
||||
)}
|
||||
</Badge>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleEdit(sla)} className="h-8 w-8 text-slate-400 hover:text-amber-600">
|
||||
<Pen className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-4 grid grid-cols-2 gap-4">
|
||||
<div className="border-l-2 border-blue-400 pl-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Bell className="w-4 h-4 text-blue-600" />
|
||||
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Reminders ({sla.reminders?.length || 0})</span>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
{(sla.reminders || []).map((reminder: any, idx: number) => (
|
||||
<div key={idx} className="text-xs text-slate-600 flex items-center gap-1">
|
||||
<Badge variant="outline" className="text-[10px] h-4.5 bg-blue-50 border-blue-200">
|
||||
{reminder.timeValue} {reminder.timeUnit}
|
||||
</Badge>
|
||||
<span>before SLA</span>
|
||||
</div>
|
||||
))}
|
||||
{(!sla.reminders || sla.reminders.length === 0) && (
|
||||
<p className="text-[10px] text-slate-400 italic">None configured</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-l-2 border-red-400 pl-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="w-4 h-4 text-red-600" />
|
||||
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Escalations ({sla.escalationConfigs?.length || 0})</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{(sla.escalationConfigs || []).map((esc: any, idx: number) => (
|
||||
<div key={idx} className="text-[11px]">
|
||||
<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">
|
||||
L{esc.level}
|
||||
</Badge>
|
||||
<span>after {esc.timeValue} {esc.timeUnit}</span>
|
||||
</div>
|
||||
<p className="text-slate-500 ml-8 font-mono truncate">{esc.notifyEmail}</p>
|
||||
</div>
|
||||
))}
|
||||
{(!sla.escalationConfigs || sla.escalationConfigs.length === 0) && (
|
||||
<p className="text-[10px] text-slate-400 italic">None configured</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{slaConfigs.length === 0 && !loading && (
|
||||
<div className="lg:col-span-2 py-20 text-center border-2 border-dashed rounded-2xl bg-slate-50/50">
|
||||
<Clock className="w-12 h-12 text-slate-200 mx-auto mb-4" />
|
||||
<h3 className="text-slate-600 font-medium">No SLA Workflows found</h3>
|
||||
<p className="text-slate-400 text-sm">Please initialize default configurations from the admin tools</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SLADialog
|
||||
isOpen={showSLADialog}
|
||||
onClose={() => setShowSLADialog(false)}
|
||||
sla={selectedSLA}
|
||||
onSave={fetchConfigs}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -1,7 +1,6 @@
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
Check,
|
||||
CheckCircle,
|
||||
CheckCircle2,
|
||||
@ -12,7 +11,6 @@ import {
|
||||
Download,
|
||||
FileText,
|
||||
GitBranch,
|
||||
Info,
|
||||
Lock,
|
||||
RefreshCw,
|
||||
ShieldCheck,
|
||||
@ -433,7 +431,10 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
||||
setShowDocumentsModal(true);
|
||||
if (stageDocs.length === 0) setShowUploadForm(true);
|
||||
}}
|
||||
className="text-[10px] font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1 transition-colors"
|
||||
className={cn(
|
||||
"text-[10px] font-medium flex items-center gap-1 transition-colors",
|
||||
branchColor === 'blue' ? "text-blue-600 hover:text-blue-800" : "text-green-600 hover:text-green-800"
|
||||
)}
|
||||
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`}
|
||||
>
|
||||
<FileText className="w-2.5 h-2.5" />
|
||||
|
||||
@ -81,7 +81,7 @@ export function useApplicationDetailsStageData({
|
||||
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'),
|
||||
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
|
||||
branches: [
|
||||
{ name: 'Architectural Work', color: 'blue', 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-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' },
|
||||
|
||||
@ -1019,29 +1019,33 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
</Button>
|
||||
{doc.status === 'Pending Verification' && currentUser?.role !== 'Dealer' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1"
|
||||
onClick={() => handleVerifyDocument(doc.id)}
|
||||
title="Verify Document"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="h-8 gap-1"
|
||||
onClick={() => handleRejectDocument(doc.id)}
|
||||
title="Reject Document"
|
||||
>
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{doc.status === 'Pending Verification' && (() => {
|
||||
const role = currentUser?.role || currentUser?.roleCode || '';
|
||||
// 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);
|
||||
})() && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 bg-green-600 hover:bg-green-700 text-white gap-1"
|
||||
onClick={() => handleVerifyDocument(doc.id)}
|
||||
title="Verify Document"
|
||||
>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
Verify
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
className="h-8 gap-1"
|
||||
onClick={() => handleRejectDocument(doc.id)}
|
||||
title="Reject Document"
|
||||
>
|
||||
<AlertCircle className="w-4 h-4" />
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User } from 'lucide-react';
|
||||
import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User, XCircle } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
@ -6,11 +6,22 @@ import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { resignationService } from '@/services/resignation.service';
|
||||
import { formatDateTime } from '@/components/ui/utils';
|
||||
import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '@/lib/offboardingDocumentOptions';
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
|
||||
interface DealerResignationDetailsPageProps {
|
||||
resignationId: string;
|
||||
@ -19,7 +30,7 @@ interface DealerResignationDetailsPageProps {
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
|
||||
if (status === 'Rejected') return 'bg-red-100 text-red-700 border-red-300';
|
||||
if (status === 'Rejected' || status === 'Withdrawn') return 'bg-red-100 text-red-700 border-red-300';
|
||||
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
||||
return 'bg-slate-100 text-slate-700 border-slate-300';
|
||||
};
|
||||
@ -33,6 +44,9 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
||||
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
|
||||
const [uploadStage, setUploadStage] = useState<string>('');
|
||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||
const [withdrawing, setWithdrawing] = useState(false);
|
||||
const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false);
|
||||
const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal');
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetails = async () => {
|
||||
@ -104,6 +118,20 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
||||
}
|
||||
};
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
try {
|
||||
setWithdrawing(true);
|
||||
await resignationService.withdraw(resignationId, withdrawalReason);
|
||||
toast.success('Resignation request withdrawn successfully');
|
||||
setIsWithdrawDialogOpen(false);
|
||||
await refreshDetails();
|
||||
} catch (error: any) {
|
||||
toast.error(error?.response?.data?.message || 'Failed to withdraw resignation');
|
||||
} finally {
|
||||
setWithdrawing(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-[320px] flex items-center justify-center">
|
||||
@ -138,14 +166,61 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
Back
|
||||
</Button>
|
||||
<div>
|
||||
<div className="flex-1">
|
||||
<h1 className="text-slate-900">Resignation Request Details</h1>
|
||||
<p className="text-slate-600 text-sm">
|
||||
Track your request progress and uploaded documents
|
||||
</p>
|
||||
</div>
|
||||
{details.status !== 'Withdrawn' &&
|
||||
details.status !== 'Completed' &&
|
||||
details.status !== 'Rejected' &&
|
||||
!['NBH', 'DD Admin', 'Legal', 'F&F Initiated'].includes(details.currentStage) && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="bg-red-600 hover:bg-red-700"
|
||||
onClick={() => setIsWithdrawDialogOpen(true)}
|
||||
disabled={withdrawing}
|
||||
>
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
{withdrawing ? 'Withdrawing...' : 'Withdraw Request'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AlertDialog open={isWithdrawDialogOpen} onOpenChange={setIsWithdrawDialogOpen}>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action will withdraw your resignation request. This action cannot be undone and you will need to submit a new request if you change your mind.
|
||||
</AlertDialogDescription>
|
||||
<div className="mt-4 space-y-2">
|
||||
<Label htmlFor="withdrawal-reason">Reason for withdrawal</Label>
|
||||
<Input
|
||||
id="withdrawal-reason"
|
||||
placeholder="Please enter a reason..."
|
||||
value={withdrawalReason}
|
||||
onChange={(e) => setWithdrawalReason(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel disabled={withdrawing}>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
handleWithdraw();
|
||||
}}
|
||||
className="bg-red-600 hover:bg-red-700 text-white"
|
||||
disabled={withdrawing}
|
||||
>
|
||||
{withdrawing ? 'Withdrawing...' : 'Yes, Withdraw Request'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
|
||||
@ -192,7 +192,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
||||
userRole === 'Super Admin'
|
||||
) && ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && !isFinalState,
|
||||
canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState,
|
||||
canPushToFnF: canPushToFnF && !isSettlementPhase && ['Legal - Termination Letter', 'Terminated', 'Dealer Terminated'].includes(currentStage),
|
||||
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
||||
isFinalState,
|
||||
isSettlementPhase
|
||||
@ -250,6 +250,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
const getProgressStatus = (stageName: string) => {
|
||||
const isTerminal = ['Rejected', 'Revoked', 'Withdrawn'].includes(request.status);
|
||||
const isSuccessFinal = ['Completed', 'Terminated', 'Settled', 'F&F Initiated', 'FNF_INITIATED'].includes(request.status) || request.currentStage === 'Terminated';
|
||||
|
||||
// For terminal states, we determine the last active stage from the timeline to keep the track visible
|
||||
let currentStageForProgress = request.currentStage || request.status;
|
||||
@ -262,6 +263,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
const currentIndex = stageSequence.indexOf(currentCanonical);
|
||||
const stageIndex = stageSequence.indexOf(stageName);
|
||||
|
||||
// If workflow finished successfully or entered F&F, ALL reached stages (including Terminated itself) turn completed
|
||||
if (isSuccessFinal && stageIndex <= currentIndex) {
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
if (stageIndex === -1) return 'pending';
|
||||
if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending';
|
||||
|
||||
@ -271,8 +277,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return 'active';
|
||||
}
|
||||
|
||||
if (stageName === 'Dealer Terminated' && currentIndex >= stageIndex) return 'completed';
|
||||
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
|
||||
65
src/lib/constants.ts
Normal file
65
src/lib/constants.ts
Normal file
@ -0,0 +1,65 @@
|
||||
export const ROLES = {
|
||||
DD: 'DD',
|
||||
DD_ZM: 'DD-ZM',
|
||||
RBM: 'RBM',
|
||||
ZBH: 'ZBH',
|
||||
DD_LEAD: 'DD Lead',
|
||||
DD_HEAD: 'DD Head',
|
||||
NBH: 'NBH',
|
||||
DD_ADMIN: 'DD Admin',
|
||||
LEGAL_ADMIN: 'Legal Admin',
|
||||
SUPER_ADMIN: 'Super Admin',
|
||||
DD_AM: 'DD AM',
|
||||
ASM: 'ASM',
|
||||
FINANCE: 'Finance',
|
||||
DEALER: 'Dealer',
|
||||
ARCHITECTURE: 'ARCHITECTURE',
|
||||
FDD: 'FDD',
|
||||
CCO: 'CCO',
|
||||
CEO: 'CEO'
|
||||
} as const;
|
||||
|
||||
export const STAGES_MAP = {
|
||||
'ONBOARDING': {
|
||||
'General': ROLES.DD_ADMIN,
|
||||
'KYC': ROLES.DD_ADMIN,
|
||||
'Level 1 Interview': ROLES.RBM,
|
||||
'Level 2 Interview': ROLES.ZBH,
|
||||
'Level 3 Interview': ROLES.NBH,
|
||||
'FDD': ROLES.FDD,
|
||||
'LOI Approval': `${ROLES.DD_HEAD},${ROLES.NBH}`,
|
||||
'LOA Approval': ROLES.NBH,
|
||||
'LOI Issue': ROLES.DD_ADMIN,
|
||||
'Architecture Team Assigned': ROLES.ARCHITECTURE,
|
||||
'Architecture Document Upload': ROLES.ARCHITECTURE,
|
||||
'Architecture Team Completion': ROLES.ARCHITECTURE,
|
||||
'EOR': ROLES.DD_ADMIN,
|
||||
'Inauguration': ROLES.DD_ADMIN
|
||||
},
|
||||
'RESIGNATION': {
|
||||
'Submission': ROLES.DEALER,
|
||||
'Regional Review': ROLES.RBM,
|
||||
'ZM Review': ROLES.DD_ZM,
|
||||
'ZBH Review': ROLES.ZBH,
|
||||
'Finance Review': ROLES.FINANCE,
|
||||
'DDL Review': ROLES.DD_LEAD,
|
||||
'Approved': ROLES.DD_HEAD
|
||||
},
|
||||
'RELOCATION': {
|
||||
'Initiated': ROLES.DEALER,
|
||||
'ASM Review': ROLES.ASM,
|
||||
'ZM Review': ROLES.DD_ZM,
|
||||
'ZBH Review': ROLES.ZBH,
|
||||
'Completed': ROLES.DD_HEAD
|
||||
},
|
||||
'CONSTITUTIONAL_CHANGE': {
|
||||
'Draft': ROLES.DEALER,
|
||||
'Legal Review': ROLES.LEGAL_ADMIN,
|
||||
'Approved': ROLES.DD_HEAD
|
||||
},
|
||||
'TERMINATION': {
|
||||
'Hearing': ROLES.NBH,
|
||||
'Review': ROLES.DD_LEAD,
|
||||
'Closed': ROLES.DD_HEAD
|
||||
}
|
||||
} as const;
|
||||
@ -133,6 +133,14 @@ export const masterService = {
|
||||
const response = await API.getSlaConfigs();
|
||||
return response.data;
|
||||
},
|
||||
saveSlaConfig: async (data: any) => {
|
||||
const response = await API.saveSlaConfig(data);
|
||||
return response.data;
|
||||
},
|
||||
initializeDefaultSlas: async () => {
|
||||
const response = await (API as any).initializeDefaultSlas();
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Semantic wrappers for consistent API
|
||||
saveZone: async (data: any) => {
|
||||
|
||||
@ -45,5 +45,14 @@ export const resignationService = {
|
||||
console.error('Upload resignation document error:', error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
withdraw: async (id: string, reason: string) => {
|
||||
try {
|
||||
const response: any = await API.withdrawResignation(id, reason);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Withdraw resignation error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user