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 { MasterPage } from '@/features/master/pages/MasterPage';
|
||||||
import { UserManagementPage } from '@/components/admin/UserManagementPage';
|
import { UserManagementPage } from '@/components/admin/UserManagementPage';
|
||||||
import { ApprovalPoliciesPage } from '@/components/admin/ApprovalPoliciesPage';
|
import { ApprovalPoliciesPage } from '@/components/admin/ApprovalPoliciesPage';
|
||||||
|
import { SLAConfigPage } from '@/features/master/pages/SLAConfigPage';
|
||||||
import { ConstitutionalChangePage } from '@/features/constitutional/pages/ConstitutionalChangePage';
|
import { ConstitutionalChangePage } from '@/features/constitutional/pages/ConstitutionalChangePage';
|
||||||
import { ConstitutionalChangeDetails } from '@/features/constitutional/pages/ConstitutionalChangeDetails';
|
import { ConstitutionalChangeDetails } from '@/features/constitutional/pages/ConstitutionalChangeDetails';
|
||||||
import { RelocationRequestPage } from '@/features/relocation/pages/RelocationRequestPage';
|
import { RelocationRequestPage } from '@/features/relocation/pages/RelocationRequestPage';
|
||||||
@ -235,7 +236,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* All Applications */}
|
{/* All Applications */}
|
||||||
<Route path="/all-applications" element={
|
<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 */}
|
{/* FDD Routes - Integrated into Layout */}
|
||||||
@ -254,6 +255,11 @@ export default function App() {
|
|||||||
? <ApprovalPoliciesPage />
|
? <ApprovalPoliciesPage />
|
||||||
: <Navigate to="/dashboard" />
|
: <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="/master" element={<MasterPage />} />
|
||||||
<Route path="/questions" element={<QuestionnaireList />} />
|
<Route path="/questions" element={<QuestionnaireList />} />
|
||||||
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
||||||
|
|||||||
@ -153,6 +153,7 @@ export const API = {
|
|||||||
createResignation: (data: any) => client.post('/resignation', data),
|
createResignation: (data: any) => client.post('/resignation', data),
|
||||||
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
||||||
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, 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'),
|
getTerminations: () => client.get('/termination'),
|
||||||
createTermination: (data: any) => client.post('/termination', data),
|
createTermination: (data: any) => client.post('/termination', data),
|
||||||
@ -164,6 +165,9 @@ export const API = {
|
|||||||
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`),
|
||||||
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`),
|
||||||
updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data),
|
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'),
|
getSettlementDepartments: () => client.get('/settlement/departments'),
|
||||||
|
|
||||||
// Line items
|
// Line items
|
||||||
@ -199,7 +203,9 @@ export const API = {
|
|||||||
uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }),
|
uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }),
|
||||||
|
|
||||||
// SLA
|
// 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
|
// System Configs
|
||||||
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
|
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Search,
|
Search,
|
||||||
Inbox,
|
|
||||||
UserMinus,
|
UserMinus,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronUp,
|
ChevronUp,
|
||||||
@ -79,13 +78,15 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
{ id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/*
|
||||||
// Add All Applications for DD role (before Dealership Requests)
|
// 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 });
|
menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox });
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Add All Requests for DD Lead role (before Dealership Requests)
|
// 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, {
|
menuItems.splice(1, 0, {
|
||||||
id: 'all-requests',
|
id: 'all-requests',
|
||||||
label: 'All Requests',
|
label: 'All Requests',
|
||||||
@ -102,6 +103,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
// Add Master for Super Admin, DD Admin, and DD Lead
|
// Add Master for Super Admin, DD Admin, and DD Lead
|
||||||
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) {
|
||||||
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
menuItems.push({ id: 'master', label: 'Master', icon: Settings });
|
||||||
|
menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (hasRole(['Super Admin'])) {
|
if (hasRole(['Super Admin'])) {
|
||||||
|
|||||||
@ -76,7 +76,7 @@ const documentNames: Record<number, string> = {
|
|||||||
|
|
||||||
// Helper functions moved above component to avoid lint errors
|
// Helper functions moved above component to avoid lint errors
|
||||||
const getTypeColor = (type: string) => {
|
const getTypeColor = (type: string) => {
|
||||||
switch(type) {
|
switch (type) {
|
||||||
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': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
case 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300';
|
||||||
@ -612,12 +612,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<div>
|
<div>
|
||||||
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
|
<p className="text-slate-600 text-sm mb-2">Constitutional Change</p>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge className={getTypeColor(request.outlet?.type || 'Proprietorship')}>
|
<Badge className={getTypeColor(request.oldValue || request.currentConstitution || request.outlet?.type || 'Proprietorship')}>
|
||||||
{request.outlet?.type || 'Proprietorship'}
|
{request.oldValue || request.currentConstitution || request.outlet?.type || 'Proprietorship'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<ArrowRight className="w-4 h-4 text-slate-400" />
|
<ArrowRight className="w-4 h-4 text-slate-400" />
|
||||||
<Badge className={getTypeColor(request.changeType)}>
|
<Badge className={getTypeColor(request.newValue || request.changeType)}>
|
||||||
{request.changeType}
|
{request.newValue || request.changeType}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -759,8 +759,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<div key={stage.id} className="flex items-start gap-4">
|
<div key={stage.id} className="flex items-start gap-4">
|
||||||
{/* Status Icon */}
|
{/* Status Icon */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${isCompleted ? 'bg-green-100' :
|
||||||
isCompleted ? 'bg-green-100' :
|
|
||||||
isCurrent ? 'bg-amber-100' :
|
isCurrent ? 'bg-amber-100' :
|
||||||
'bg-slate-100'
|
'bg-slate-100'
|
||||||
}`}>
|
}`}>
|
||||||
@ -773,8 +772,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{index < workflowStages.length - 1 && (
|
{index < workflowStages.length - 1 && (
|
||||||
<div className={`w-0.5 h-12 ${
|
<div className={`w-0.5 h-12 ${isCompleted ? 'bg-green-300' : 'bg-slate-200'
|
||||||
isCompleted ? 'bg-green-300' : 'bg-slate-200'
|
|
||||||
}`} />
|
}`} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -927,8 +925,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={docNum}
|
key={docNum}
|
||||||
className={`flex items-center justify-between p-3 rounded-lg border ${
|
className={`flex items-center justify-between p-3 rounded-lg border ${isRejected ? 'bg-red-50 border-red-200' :
|
||||||
isRejected ? 'bg-red-50 border-red-200' :
|
|
||||||
ok ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
|
ok ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@ -1013,7 +1010,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<Button size="sm" variant="outline" className="h-8 w-8 p-0" title="Download document">
|
<Button size="sm" variant="outline" className="h-8 w-8 p-0" title="Download document">
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{doc.status !== 'Verified' && doc.status !== 'Rejected' && currentUser?.role !== 'Dealer' && (
|
{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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -1058,10 +1060,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{historyEntries.map((entry: any, index: number) => {
|
{historyEntries.map((entry: any, index: number) => {
|
||||||
const pres = getConstitutionalHistoryPresentation(entry);
|
const pres = getConstitutionalHistoryPresentation(entry);
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div key={entry.id || index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
|
<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 ${
|
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${pres.variant === 'success' ? 'bg-green-100' :
|
||||||
pres.variant === 'success' ? 'bg-green-100' :
|
|
||||||
pres.variant === 'danger' ? 'bg-red-100' :
|
pres.variant === 'danger' ? 'bg-red-100' :
|
||||||
pres.variant === 'pending' ? 'bg-amber-100' :
|
pres.variant === 'pending' ? 'bg-amber-100' :
|
||||||
'bg-slate-100'
|
'bg-slate-100'
|
||||||
@ -1077,29 +1102,23 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-slate-900">{entry.stage || entry.action}</h4>
|
<h4 className="text-slate-900 capitalize">{heading}</h4>
|
||||||
<p className="text-slate-600 text-sm">{entry.userName || 'System'}</p>
|
<p className="text-slate-500 text-sm">by <span className="font-medium text-slate-700">{actorName}</span></p>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={getStatusColor(pres.badge)}>
|
<Badge className={getStatusColor(pres.badge)}>
|
||||||
{pres.badge}
|
{pres.badge.replace(/_/g, ' ')}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2 p-3 bg-slate-50 rounded border border-slate-100 italic text-slate-700 text-sm">
|
{displayRemarks && (
|
||||||
<span className="font-semibold non-italic text-slate-500 mr-2">Comments:</span>
|
<div className="mt-2 p-3 bg-slate-50 rounded border border-slate-100 text-slate-700 text-sm">
|
||||||
{/*
|
{displayRemarks}
|
||||||
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>
|
</div>
|
||||||
|
)}
|
||||||
<p className="text-slate-400 text-xs mt-2 font-medium uppercase tracking-wider">
|
<p className="text-slate-400 text-xs mt-2 font-medium uppercase tracking-wider">
|
||||||
{formatDateTime(entry.timestamp || entry.createdAt)}
|
{formatDateTime(entry.timestamp || entry.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -1263,6 +1282,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
<Label htmlFor="comments">
|
<Label htmlFor="comments">
|
||||||
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
|
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
|
||||||
</Label>
|
</Label>
|
||||||
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="comments"
|
id="comments"
|
||||||
value={comments}
|
value={comments}
|
||||||
@ -1271,7 +1291,6 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
|
|||||||
rows={4}
|
rows={4}
|
||||||
required={actionType !== 'approve'}
|
required={actionType !== 'approve'}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
|
|||||||
color: 'bg-yellow-500',
|
color: 'bg-yellow-500',
|
||||||
trend: { value: 2, isPositive: false },
|
trend: { value: 2, isPositive: false },
|
||||||
filter: 'all',
|
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,6 +258,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
uploadedOn: formatDateTime(c.clearedAt),
|
uploadedOn: formatDateTime(c.clearedAt),
|
||||||
type: `${c.department} Proof`,
|
type: `${c.department} Proof`,
|
||||||
url: c.supportingDocument
|
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'
|
adjustments: '0'
|
||||||
});
|
});
|
||||||
|
|
||||||
const [uploadedDocuments, setUploadedDocuments] = useState<any[]>([]);
|
|
||||||
|
|
||||||
// Handlers for Payables
|
// Handlers for Payables
|
||||||
const handleAddPayable = async () => {
|
const handleAddPayable = async () => {
|
||||||
if (!newPayable.department || !newPayable.description || !newPayable.amount) {
|
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;
|
const files = event.target.files;
|
||||||
if (files && files.length > 0) {
|
if (files && files.length > 0) {
|
||||||
const newDocs = Array.from(files).map(file => ({
|
setLoading(true);
|
||||||
name: file.name,
|
try {
|
||||||
size: `${(file.size / 1024).toFixed(0)} KB`,
|
let successCount = 0;
|
||||||
uploadedOn: new Date().toISOString(),
|
for (let i = 0; i < files.length; i++) {
|
||||||
type: 'Settlement Verification'
|
const formData = new FormData();
|
||||||
}));
|
formData.append('file', files[i]);
|
||||||
setUploadedDocuments([...uploadedDocuments, ...newDocs]);
|
const response: any = await API.uploadFnFDocument(fnfId, formData);
|
||||||
toast.success(`${files.length} document(s) uploaded successfully`);
|
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,10 +1812,49 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
<p className="text-sm text-slate-500">{doc.size} • {doc.type} • Uploaded on {doc.uploadedOn}</p>
|
<p className="text-sm text-slate-500">{doc.size} • {doc.type} • Uploaded on {doc.uploadedOn}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm">
|
<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
|
Download
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@ -1838,23 +1891,6 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -314,7 +314,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge
|
<Badge
|
||||||
variant={fnfCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
|
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}
|
{fnfCase.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
@ -695,7 +695,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
|
|||||||
<p className="text-sm text-slate-500">Status</p>
|
<p className="text-sm text-slate-500">Status</p>
|
||||||
<Badge
|
<Badge
|
||||||
variant={selectedCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
|
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}
|
{selectedCase.status}
|
||||||
</Badge>
|
</Badge>
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Badge } from '@/components/ui/badge';
|
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';
|
import { RootState } from '@/store';
|
||||||
|
|
||||||
interface SLAConfigurationProps {
|
interface SLAConfigurationProps {
|
||||||
@ -18,31 +18,34 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>SLA & Escalation Matrix</CardTitle>
|
<CardTitle>SLA Configuration</CardTitle>
|
||||||
<CardDescription>Configure Turn Around Time (TAT) and escalation rules for each process stage</CardDescription>
|
<CardDescription>Configure service level agreements, reminders, and escalations for each stage</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="space-y-4">
|
||||||
{slaConfigs.map((sla) => (
|
{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 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-center justify-between">
|
<div className="flex items-start justify-between mb-3">
|
||||||
<div className="flex items-center gap-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" />
|
<Clock className="w-5 h-5 text-amber-600" />
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-slate-900 font-medium">{sla.stage}</h4>
|
<h4 className="text-slate-900 font-medium">{sla.activityName}</h4>
|
||||||
<p className="text-xs text-slate-500">Target TAT: <span className="text-amber-600 font-bold">{sla.days} Days</span></p>
|
<p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant={sla.enabled ? "default" : "secondary"} className={sla.enabled ? "bg-emerald-500" : ""}>
|
<Badge className={sla.isActive ? "bg-green-600" : "bg-slate-400"}>
|
||||||
{sla.enabled ? 'Enabled' : 'Disabled'}
|
{sla.isActive ? (
|
||||||
|
<><CheckCircle className="w-3 h-3 mr-1" /> Active</>
|
||||||
|
) : (
|
||||||
|
'Inactive'
|
||||||
|
)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="ghost" size="sm" onClick={() => onConfigureSLA(sla)} className="text-slate-400 hover:text-amber-600">
|
<Button variant="outline" size="sm" onClick={() => onConfigureSLA(sla)} className="h-8 gap-1.5 px-3">
|
||||||
<Edit2 className="w-4 h-4" />
|
<Pen className="w-3 h-3 mr-2" />
|
||||||
|
Edit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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="border-l-2 border-blue-400 pl-3">
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
<Bell className="w-4 h-4 text-blue-600" />
|
<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>
|
||||||
<div className="space-y-1">
|
<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">
|
<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">
|
<Badge variant="outline" className="text-xs bg-blue-50 border-blue-200">
|
||||||
{reminder.time} {reminder.unit}
|
{reminder.timeValue} {reminder.timeUnit}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span>before</span>
|
<span>before SLA</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{(!sla.reminders || sla.reminders.length === 0) && (
|
||||||
|
<p className="text-[10px] text-slate-400">No reminders configured</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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-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>
|
||||||
<div className="space-y-1">
|
<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">
|
<div key={idx} className="text-xs">
|
||||||
<Badge variant="outline" className="text-[10px] h-4 px-1 bg-red-50 border-red-200">
|
<div className="flex items-center gap-1 text-slate-900">
|
||||||
L{escalation.level}
|
<Badge variant="outline" className="text-xs bg-red-50 border-red-200">
|
||||||
|
L{esc.level}
|
||||||
</Badge>
|
</Badge>
|
||||||
<span className="text-slate-400 ml-1">→ {escalation.userEmail.split('@')[0]}</span>
|
<span>after {esc.timeValue} {esc.timeUnit}</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-slate-500 ml-9">→ {esc.notifyEmail}</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{(!sla.escalationConfigs || sla.escalationConfigs.length === 0) && (
|
||||||
|
<p className="text-[10px] text-slate-400">No escalations configured</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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 {
|
import {
|
||||||
Tabs, TabsContent, TabsList, TabsTrigger
|
Tabs, TabsContent, TabsList, TabsTrigger
|
||||||
} from '@/components/ui/tabs';
|
} 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 { Badge } from '@/components/ui/badge';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -18,7 +18,6 @@ import { RegionalManagement } from '@/features/master/components/RegionalManagem
|
|||||||
import { ASMManagement } from '@/features/master/components/ASMManagement';
|
import { ASMManagement } from '@/features/master/components/ASMManagement';
|
||||||
import { ZMManagement } from '@/features/master/components/ZMManagement';
|
import { ZMManagement } from '@/features/master/components/ZMManagement';
|
||||||
import { UserManagementTable } from '@/features/master/components/UserManagementTable';
|
import { UserManagementTable } from '@/features/master/components/UserManagementTable';
|
||||||
import { SLAConfiguration } from '@/features/master/components/SLAConfiguration';
|
|
||||||
import { RolePermissions } from '@/features/master/components/RolePermissions';
|
import { RolePermissions } from '@/features/master/components/RolePermissions';
|
||||||
import { RoleDialog } from '@/features/master/components/RoleDialog';
|
import { RoleDialog } from '@/features/master/components/RoleDialog';
|
||||||
import { AddRoleDialog } from '@/features/master/components/AddRoleDialog';
|
import { AddRoleDialog } from '@/features/master/components/AddRoleDialog';
|
||||||
@ -396,7 +395,7 @@ export const MasterPage: React.FC = () => {
|
|||||||
const payload = {
|
const payload = {
|
||||||
id: editingLocationId,
|
id: editingLocationId,
|
||||||
stateId: locationState,
|
stateId: locationState,
|
||||||
stateName: selectedState?.name || selectedState?.stateName || '',
|
stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '',
|
||||||
districtId: locationDistrict,
|
districtId: locationDistrict,
|
||||||
name: locationCity || selectedDistrict?.name || 'New Location',
|
name: locationCity || selectedDistrict?.name || 'New Location',
|
||||||
city: locationCity,
|
city: locationCity,
|
||||||
@ -440,16 +439,13 @@ export const MasterPage: React.FC = () => {
|
|||||||
</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-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">
|
<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
|
<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-amber-600 data-[state=active]:text-white">
|
||||||
<Shield className="w-4 h-4" /> Roles
|
<Shield className="w-4 h-4" /> Roles
|
||||||
</TabsTrigger>
|
</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">
|
<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
|
<Mail className="w-4 h-4" /> Emails
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@ -505,10 +501,6 @@ export const MasterPage: React.FC = () => {
|
|||||||
onEditRole={handleEditRole} />
|
onEditRole={handleEditRole} />
|
||||||
</TabsContent>
|
</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">
|
<TabsContent value="templates" className="animate-in fade-in duration-300">
|
||||||
<EmailTemplates
|
<EmailTemplates
|
||||||
onEditTemplate={(template) => {
|
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 { toast } from 'sonner';
|
||||||
import {
|
import {
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Calendar,
|
|
||||||
Check,
|
Check,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
@ -12,7 +11,6 @@ import {
|
|||||||
Download,
|
Download,
|
||||||
FileText,
|
FileText,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Info,
|
|
||||||
Lock,
|
Lock,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
@ -433,7 +431,10 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
|||||||
setShowDocumentsModal(true);
|
setShowDocumentsModal(true);
|
||||||
if (stageDocs.length === 0) setShowUploadForm(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}`}
|
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-2.5 h-2.5" />
|
<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'),
|
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,
|
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
|
||||||
branches: [
|
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-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' },
|
||||||
|
|||||||
@ -1019,7 +1019,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
>
|
>
|
||||||
<Download className="w-4 h-4" />
|
<Download className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
{doc.status === 'Pending Verification' && currentUser?.role !== 'Dealer' && (
|
{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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -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 { useEffect, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { toast } from 'sonner';
|
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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { resignationService } from '@/services/resignation.service';
|
import { resignationService } from '@/services/resignation.service';
|
||||||
import { formatDateTime } from '@/components/ui/utils';
|
import { formatDateTime } from '@/components/ui/utils';
|
||||||
import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '@/lib/offboardingDocumentOptions';
|
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 {
|
interface DealerResignationDetailsPageProps {
|
||||||
resignationId: string;
|
resignationId: string;
|
||||||
@ -19,7 +30,7 @@ interface DealerResignationDetailsPageProps {
|
|||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
|
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';
|
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';
|
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 [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
|
||||||
const [uploadStage, setUploadStage] = useState<string>('');
|
const [uploadStage, setUploadStage] = useState<string>('');
|
||||||
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
|
const [withdrawing, setWithdrawing] = useState(false);
|
||||||
|
const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false);
|
||||||
|
const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchDetails = async () => {
|
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) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[320px] flex items-center justify-center">
|
<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" />
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<div>
|
<div className="flex-1">
|
||||||
<h1 className="text-slate-900">Resignation Request Details</h1>
|
<h1 className="text-slate-900">Resignation Request Details</h1>
|
||||||
<p className="text-slate-600 text-sm">
|
<p className="text-slate-600 text-sm">
|
||||||
Track your request progress and uploaded documents
|
Track your request progress and uploaded documents
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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>
|
</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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="flex items-center gap-2">
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
|||||||
@ -192,7 +192,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
(currentStage === 'CEO Final Approval' && userRole === 'CEO') ||
|
||||||
userRole === 'Super Admin'
|
userRole === 'Super Admin'
|
||||||
) && ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && !isFinalState,
|
) && ['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,
|
canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState,
|
||||||
isFinalState,
|
isFinalState,
|
||||||
isSettlementPhase
|
isSettlementPhase
|
||||||
@ -250,6 +250,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
|
|
||||||
const getProgressStatus = (stageName: string) => {
|
const getProgressStatus = (stageName: string) => {
|
||||||
const isTerminal = ['Rejected', 'Revoked', 'Withdrawn'].includes(request.status);
|
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
|
// For terminal states, we determine the last active stage from the timeline to keep the track visible
|
||||||
let currentStageForProgress = request.currentStage || request.status;
|
let currentStageForProgress = request.currentStage || request.status;
|
||||||
@ -262,6 +263,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
const currentIndex = stageSequence.indexOf(currentCanonical);
|
const currentIndex = stageSequence.indexOf(currentCanonical);
|
||||||
const stageIndex = stageSequence.indexOf(stageName);
|
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 (stageIndex === -1) return 'pending';
|
||||||
if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending';
|
if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending';
|
||||||
|
|
||||||
@ -271,8 +277,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
|||||||
return 'active';
|
return 'active';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stageName === 'Dealer Terminated' && currentIndex >= stageIndex) return 'completed';
|
|
||||||
|
|
||||||
return 'pending';
|
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();
|
const response = await API.getSlaConfigs();
|
||||||
return response.data;
|
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
|
// Semantic wrappers for consistent API
|
||||||
saveZone: async (data: any) => {
|
saveZone: async (data: any) => {
|
||||||
|
|||||||
@ -45,5 +45,14 @@ export const resignationService = {
|
|||||||
console.error('Upload resignation document error:', error);
|
console.error('Upload resignation document error:', error);
|
||||||
throw 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