dispatch feature approve restriction and some few bugs covered from tracker
This commit is contained in:
parent
06116af31a
commit
dc49fa9065
@ -158,11 +158,17 @@ export const API = {
|
||||
getResignations: (params?: any) => client.get('/resignation', params),
|
||||
createResignation: (data: any) => client.post('/resignation', data),
|
||||
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
||||
dispatchResignation: (id: string, data?: any) => client.post(`/resignation/${id}/dispatch`, data),
|
||||
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data),
|
||||
withdrawResignation: (id: string, reason: string) => client.post(`/resignation/${id}/withdraw`, { reason }),
|
||||
|
||||
getTerminations: (params?: any) => client.get('/termination', params),
|
||||
createTermination: (data: any) => client.post('/termination', data),
|
||||
createTermination: (data: any) => {
|
||||
const isFormData = typeof FormData !== 'undefined' && data instanceof FormData;
|
||||
return client.post('/termination', data, isFormData ? {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
} : undefined);
|
||||
},
|
||||
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
|
||||
|
||||
getOnboardingPayments: () => client.get('/settlement/onboarding'),
|
||||
|
||||
@ -33,8 +33,10 @@ import {
|
||||
Trash2,
|
||||
Save,
|
||||
Paperclip,
|
||||
FileDown
|
||||
FileDown,
|
||||
MessageSquare
|
||||
} from 'lucide-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
|
||||
import { formatDateTime } from '@/lib/dateUtils';
|
||||
@ -80,6 +82,7 @@ interface FinancialLineItem {
|
||||
}
|
||||
|
||||
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
|
||||
const navigate = useNavigate();
|
||||
const [fnfCase, setFnfCase] = useState<any>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('overview');
|
||||
@ -801,14 +804,31 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl mb-1">F&F Settlement Review</h1>
|
||||
<p className="text-slate-600">Full & Final Settlement for {fnfCase.dealerName}</p>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button variant="outline" size="icon" onClick={onBack}>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Button>
|
||||
<div>
|
||||
<h1 className="text-3xl mb-1">F&F Settlement Review</h1>
|
||||
<p className="text-slate-600">Full & Final Settlement for {fnfCase.dealerName}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
navigate(`/worknotes/fnf/${fnfId}`, {
|
||||
state: {
|
||||
applicationName: fnfCase.dealerName || 'F&F Settlement',
|
||||
registrationNumber: fnfCase.caseNumber || fnfCase.settlementId || '',
|
||||
participants: fnfCase.participants || []
|
||||
}
|
||||
})
|
||||
}
|
||||
>
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
View Work Notes
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Status Banner */}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
CheckCircle2,
|
||||
AlertCircle,
|
||||
Loader2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@ -40,7 +41,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { Progress } from "@/components/ui/progress";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { API } from "@/api/API";
|
||||
@ -86,6 +87,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
type: "Receivable",
|
||||
});
|
||||
const [clearanceFile, setClearanceFile] = useState<File | null>(null);
|
||||
const [isUploadingDocs, setIsUploadingDocs] = useState(false);
|
||||
const documentInputRef = useRef<HTMLInputElement>(null);
|
||||
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
|
||||
const [uploadDocName, setUploadDocName] = useState("");
|
||||
const [uploadDocType, setUploadDocType] = useState("");
|
||||
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@ -300,6 +307,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
status: "Finance",
|
||||
url: doc.filePath,
|
||||
})),
|
||||
...(s.clearanceDocuments || [])
|
||||
.filter((d: any) => d?.supportingDocument)
|
||||
.map((d: any, idx: number) => ({
|
||||
id: d.id || `fnf-doc-${idx}`,
|
||||
name: d.name || (d.supportingDocument || "").split("/").pop(),
|
||||
type: d.documentType || d.department || "F&F Document",
|
||||
uploadDate: d.clearedAt ? formatDateTime(d.clearedAt) : "-",
|
||||
status: "Attached",
|
||||
url: d.supportingDocument,
|
||||
})),
|
||||
],
|
||||
participants: s.participants || []
|
||||
};
|
||||
@ -423,9 +440,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
|
||||
const fnfAge = calculateAge(fnfCase.submittedOn);
|
||||
|
||||
const currentUserRole = String(currentUser?.role || "").toLowerCase();
|
||||
const canViewDocuments =
|
||||
currentUserRole.includes("super admin") || currentUserRole.includes("dd admin");
|
||||
|
||||
const canRespondToDepartment = (dept: any) => {
|
||||
if (!fnfCase || !dept) return false;
|
||||
const role = String(currentUser?.role || "").toLowerCase();
|
||||
const role = currentUserRole;
|
||||
if (!role) return false;
|
||||
|
||||
// 1. If any user (including Admin) has already responded, hide the button to prevent double-submission
|
||||
@ -486,6 +507,61 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handlePickDocumentFile = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = event.target.files?.[0] || null;
|
||||
setUploadFile(file);
|
||||
};
|
||||
|
||||
const resetUploadDialog = () => {
|
||||
setUploadDialogOpen(false);
|
||||
setUploadDocName("");
|
||||
setUploadDocType("");
|
||||
setUploadFile(null);
|
||||
if (documentInputRef.current) documentInputRef.current.value = "";
|
||||
};
|
||||
|
||||
const handleSubmitDocumentUpload = async () => {
|
||||
const trimmedName = uploadDocName.trim();
|
||||
if (!trimmedName) {
|
||||
toast.error("Please enter a document name.");
|
||||
return;
|
||||
}
|
||||
if (!uploadFile) {
|
||||
toast.error("Please choose a file to upload.");
|
||||
return;
|
||||
}
|
||||
if (!fnfId) {
|
||||
toast.error("Cannot upload — settlement id is missing.");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploadingDocs(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("file", uploadFile);
|
||||
formData.append("documentName", trimmedName);
|
||||
if (uploadDocType.trim()) {
|
||||
formData.append("documentType", uploadDocType.trim());
|
||||
}
|
||||
|
||||
const response: any = await API.uploadFnFDocument(fnfId, formData);
|
||||
if (response.data?.success) {
|
||||
toast.success("Document uploaded successfully");
|
||||
resetUploadDialog();
|
||||
fetchFnFDetails(false);
|
||||
} else {
|
||||
toast.error(response.data?.message || "Failed to upload document");
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error("Upload F&F document error:", error);
|
||||
toast.error(
|
||||
error?.response?.data?.message || "Failed to upload document",
|
||||
);
|
||||
} finally {
|
||||
setIsUploadingDocs(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendToStakeholders = () => {
|
||||
toast.success("Notifications sent to all 16 departments");
|
||||
setSendStakeholdersDialog(false);
|
||||
@ -691,7 +767,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
<TabsTrigger value="progress">Progress</TabsTrigger>
|
||||
<TabsTrigger value="details">Case Details</TabsTrigger>
|
||||
<TabsTrigger value="departments">Department Responses</TabsTrigger>
|
||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||
{canViewDocuments && (
|
||||
<TabsTrigger value="documents">Documents</TabsTrigger>
|
||||
)}
|
||||
{/* Bank Details tab hidden temporarily */}
|
||||
{/* <TabsTrigger value="bank">Bank Details</TabsTrigger> */}
|
||||
<TabsTrigger value="audit">Audit Trail</TabsTrigger>
|
||||
@ -1590,14 +1668,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
{/* Documents Tab */}
|
||||
{/* Documents Tab — admin / super admin only */}
|
||||
{canViewDocuments && (
|
||||
<TabsContent value="documents">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Documents</CardTitle>
|
||||
<CardDescription>
|
||||
All NOC documents and due statements from departments
|
||||
</CardDescription>
|
||||
<CardHeader className="flex flex-row items-start justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Documents</CardTitle>
|
||||
<CardDescription>
|
||||
All NOC documents and due statements from departments
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={() => setUploadDialogOpen(true)}
|
||||
disabled={isUploadingDocs}
|
||||
className="bg-re-red hover:bg-re-red-hover text-white"
|
||||
data-testid="fnf-upload-docs-btn"
|
||||
>
|
||||
{isUploadingDocs ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isUploadingDocs ? "Uploading..." : "Upload Document"}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
@ -1611,7 +1707,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{fnfCase.documents.map((doc: any) => (
|
||||
{(fnfCase.documents || []).length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={5}
|
||||
className="text-center text-slate-500 py-8"
|
||||
>
|
||||
No documents uploaded yet. Click <span className="font-medium">Upload Documents</span> to add files.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{(fnfCase.documents || []).map((doc: any) => (
|
||||
<TableRow key={doc.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -1661,6 +1767,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Bank Details Tab */}
|
||||
<TabsContent value="bank">
|
||||
@ -1961,6 +2068,96 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Upload Document Dialog */}
|
||||
<Dialog
|
||||
open={uploadDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) resetUploadDialog();
|
||||
else setUploadDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Upload Document</DialogTitle>
|
||||
<DialogDescription>
|
||||
Provide a name for this document and attach a file. The name is required.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 py-2">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fnf-upload-doc-name">
|
||||
Document Name <span className="text-red-600">*</span>
|
||||
</Label>
|
||||
<input
|
||||
id="fnf-upload-doc-name"
|
||||
type="text"
|
||||
placeholder="e.g. Final NOC — Sales"
|
||||
className="flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
|
||||
value={uploadDocName}
|
||||
onChange={(e) => setUploadDocName(e.target.value)}
|
||||
data-testid="fnf-upload-doc-name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fnf-upload-doc-type">Document Type (optional)</Label>
|
||||
<input
|
||||
id="fnf-upload-doc-type"
|
||||
type="text"
|
||||
placeholder="e.g. NOC, Statement, Receipt"
|
||||
className="flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
|
||||
value={uploadDocType}
|
||||
onChange={(e) => setUploadDocType(e.target.value)}
|
||||
data-testid="fnf-upload-doc-type"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="fnf-upload-doc-file">
|
||||
File <span className="text-red-600">*</span>
|
||||
</Label>
|
||||
<input
|
||||
id="fnf-upload-doc-file"
|
||||
ref={documentInputRef}
|
||||
type="file"
|
||||
className="flex w-full text-sm"
|
||||
onChange={handlePickDocumentFile}
|
||||
data-testid="fnf-upload-docs-input"
|
||||
/>
|
||||
{uploadFile && (
|
||||
<p className="text-xs text-slate-500 truncate">
|
||||
Selected: <span className="font-medium">{uploadFile.name}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={resetUploadDialog}
|
||||
disabled={isUploadingDocs}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-re-red hover:bg-re-red-hover text-white"
|
||||
onClick={handleSubmitDocumentUpload}
|
||||
disabled={isUploadingDocs || !uploadDocName.trim() || !uploadFile}
|
||||
data-testid="fnf-upload-doc-submit"
|
||||
>
|
||||
{isUploadingDocs ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{isUploadingDocs ? "Uploading..." : "Upload"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Bank Details Modal */}
|
||||
<BankDetailsModal
|
||||
isOpen={isBankModalOpen}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, AlertCircle, Loader2, Upload, Ban } from 'lucide-react';
|
||||
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, AlertCircle, Loader2, Upload, Ban, Mail } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@ -42,6 +42,7 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
|
||||
'RBM': 'RBM',
|
||||
'ZBH': 'ZBH',
|
||||
'DD Lead': 'DD Lead',
|
||||
'DD Head': 'DD Head',
|
||||
'NBH': 'NBH',
|
||||
'DD Admin': 'DD Admin',
|
||||
'Legal': 'Legal Admin'
|
||||
@ -56,6 +57,7 @@ const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
|
||||
'RBM': ['RBM', 'RBM Review', 'Regional Review', 'RBM + DD-ZM Review'],
|
||||
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
|
||||
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
|
||||
'DD Head': ['DD Head', 'DD Head Review', 'DDH Review'],
|
||||
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
||||
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
||||
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
|
||||
@ -92,7 +94,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
};
|
||||
|
||||
const navigate = useNavigate();
|
||||
const [actionDialog, setActionDialog] = useState<{ open: boolean, type: 'approve' | 'reject' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | null }>({ open: false, type: null });
|
||||
const [actionDialog, setActionDialog] = useState<{ open: boolean, type: 'approve' | 'reject' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'dispatch' | null }>({ open: false, type: null });
|
||||
const [remarks, setRemarks] = useState('');
|
||||
const [assignToUser, setAssignToUser] = useState<string>('');
|
||||
const [userSearchQuery, setUserSearchQuery] = useState('');
|
||||
@ -154,16 +156,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{ id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
|
||||
{ id: 3, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
|
||||
{ id: 4, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
|
||||
{ id: 5, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' },
|
||||
{ id: 6, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
||||
{ id: 8, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
||||
{ id: 9, name: 'Awaiting F&F', key: 'Awaiting F&F', description: 'Internal review complete — start Full & Final using Push to F&F when ready' },
|
||||
{ id: 10, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
||||
{ id: 11, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||
{ id: 5, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead consolidated review' },
|
||||
{ id: 6, name: 'DD Head Review', key: 'DD Head', description: 'DD Head final dealer development approval' },
|
||||
{ id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
|
||||
{ id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
|
||||
{ id: 9, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
|
||||
{ id: 10, name: 'Awaiting F&F', key: 'Awaiting F&F', description: 'Internal review complete — start Full & Final using Push to F&F when ready' },
|
||||
{ id: 11, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
|
||||
{ id: 12, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
|
||||
];
|
||||
|
||||
const stagesOrdered = ['Request Submitted', 'ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'Awaiting F&F', 'F&F Initiated', 'Completed'];
|
||||
const stagesOrdered = ['Request Submitted', 'ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal', 'DD Admin', 'Awaiting F&F', 'F&F Initiated', 'Completed'];
|
||||
|
||||
const legalStageApproved = (() => {
|
||||
if (!resignationData) return false;
|
||||
@ -194,7 +197,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
|
||||
const getResignationPermissions = () => {
|
||||
if (!resignationData || !currentUser) {
|
||||
return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
|
||||
return { canApprove: false, canDispatch: false, dispatchMissed: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
|
||||
}
|
||||
|
||||
const currentStage = resignationData.currentStage;
|
||||
@ -255,6 +258,61 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
const fnfPushRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
|
||||
const fnfPushLegacyRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
|
||||
|
||||
// Dispatch action — Legal uploaded the acceptance letter and DD Admin (or
|
||||
// Super Admin) must dispatch a formal copy to the dealer. The button stays
|
||||
// visible from the DD Admin step through Awaiting F&F / F&F Initiated so
|
||||
// an admin who skipped the step earlier can still send the letter
|
||||
// retroactively. Once dispatched it disappears (audit log is the source of
|
||||
// truth) so we don't re-send duplicates.
|
||||
const isDDAdminStage = currentStage === 'DD Admin' || currentStage === 'DD Admin Review';
|
||||
const isDDAdmin = userRoleCode === 'DD_ADMIN' || userRole === 'DD Admin';
|
||||
const isSuperAdmin = userRoleCode === 'SUPER_ADMIN' || userRole === 'Super Admin';
|
||||
|
||||
const allResignationDocs: any[] = [
|
||||
...(resignationData.documents || []),
|
||||
...(resignationData.uploadedDocuments || [])
|
||||
];
|
||||
const hasAcceptanceLetter = allResignationDocs.some((doc: any) => {
|
||||
const docType = String(doc?.documentType || doc?.type || '').toLowerCase();
|
||||
const docStage = String(doc?.stage || '').toLowerCase();
|
||||
return docType.includes('acceptance letter') || docStage === 'legal';
|
||||
});
|
||||
|
||||
const hasBeenDispatched =
|
||||
auditLogs.some((log: any) => {
|
||||
const action = String(log?.action || '').toUpperCase();
|
||||
const desc = String(log?.description || log?.details?.action || '').toLowerCase();
|
||||
return (
|
||||
action === 'RESIGNATION_LETTER_DISPATCHED' ||
|
||||
desc.includes('resignation letter dispatched')
|
||||
);
|
||||
}) ||
|
||||
(resignationData.timeline || []).some((entry: any) =>
|
||||
String(entry?.action || '').toLowerCase().includes('resignation letter dispatched')
|
||||
);
|
||||
|
||||
const ddAdminIdx = stagesOrdered.indexOf('DD Admin');
|
||||
const completedIdx = stagesOrdered.indexOf('Completed');
|
||||
const currentStageIdx = stagesOrdered.indexOf(
|
||||
stagesOrdered.find(
|
||||
(key) => key === currentStage || (RESIGNATION_STAGE_ALIASES[key] || []).includes(currentStage)
|
||||
) || currentStage
|
||||
);
|
||||
const isAtOrAfterDDAdmin =
|
||||
ddAdminIdx !== -1 &&
|
||||
currentStageIdx !== -1 &&
|
||||
currentStageIdx >= ddAdminIdx &&
|
||||
(completedIdx === -1 || currentStageIdx < completedIdx);
|
||||
|
||||
const canDispatch =
|
||||
isAtOrAfterDDAdmin &&
|
||||
(isDDAdmin || isSuperAdmin) &&
|
||||
hasAcceptanceLetter &&
|
||||
!hasBeenDispatched &&
|
||||
!isFinalState;
|
||||
|
||||
const dispatchMissed = canDispatch && !isDDAdminStage;
|
||||
|
||||
const canApprove = isCurrentlyAssigned &&
|
||||
!isFinalState &&
|
||||
!isSettlementPhase &&
|
||||
@ -262,10 +320,15 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
!(currentStage === 'Legal' && legalStageApproved) &&
|
||||
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
|
||||
!(currentStage === 'DD Admin' && !isLwdReached) &&
|
||||
// Dispatch replaces Approve at the DD Admin stage so the admin must
|
||||
// explicitly send the acceptance letter to the dealer.
|
||||
!isDDAdminStage &&
|
||||
!isAwaitingFnfGate;
|
||||
|
||||
return {
|
||||
canApprove,
|
||||
canDispatch,
|
||||
dispatchMissed,
|
||||
// SRS §7.3.2: Send Back returns to DD Admin for correction. Legal Admin only drafts/uploads
|
||||
// the Resignation Acceptance Letter and cannot send the case back to earlier reviewers.
|
||||
canSendBack:
|
||||
@ -298,6 +361,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
'RBM': ['RBM', 'RBM Review', 'RBM + DD-ZM Review'],
|
||||
'ZBH': ['ZBH', 'ZBH Review'],
|
||||
'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'],
|
||||
'DD Head': ['DD Head', 'DD Head Review', 'Head Review'],
|
||||
'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
|
||||
'DD Admin': ['DD Admin', 'DD Admin Review'],
|
||||
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
|
||||
@ -350,7 +414,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
return 'pending';
|
||||
};
|
||||
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke') => {
|
||||
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'dispatch') => {
|
||||
setActionDialog({ open: true, type });
|
||||
};
|
||||
|
||||
@ -369,7 +433,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
};
|
||||
|
||||
const handleSubmitAction = async () => {
|
||||
if (!remarks && !['assign', 'pushfnf'].includes(actionDialog.type || '')) {
|
||||
if (!remarks && !['assign', 'pushfnf', 'dispatch'].includes(actionDialog.type || '')) {
|
||||
toast.error('Please provide remarks (min 5 characters)');
|
||||
return;
|
||||
}
|
||||
@ -1032,6 +1096,36 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{permissions.canDispatch && (
|
||||
<>
|
||||
{permissions.dispatchMissed && (
|
||||
<div className="rounded-md border border-amber-300 bg-amber-50 text-amber-800 px-3 py-2 text-xs flex items-start gap-2">
|
||||
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
|
||||
<span>
|
||||
Resignation acceptance letter was not dispatched at the DD Admin step.
|
||||
Please send it to the dealer now.
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
disabled={isSubmitting}
|
||||
className={`w-full font-bold ${
|
||||
permissions.dispatchMissed
|
||||
? 'bg-amber-600 hover:bg-amber-700'
|
||||
: 'bg-re-red hover:bg-re-red-hover'
|
||||
}`}
|
||||
onClick={() => handleAction('dispatch')}
|
||||
>
|
||||
{isSubmitting && actionDialog.type === 'dispatch' ? (
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
) : (
|
||||
<Mail className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
{permissions.dispatchMissed ? 'Dispatch Resignation Letter (Pending)' : 'Dispatch Resignation Letter'}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{permissions.canSendBack && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@ -1130,13 +1224,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{actionDialog.type === 'revoke' && 'Revoke Resignation Request'}
|
||||
{actionDialog.type === 'assign' && 'Assign to User'}
|
||||
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
|
||||
{actionDialog.type === 'dispatch' && 'Dispatch Resignation Letter'}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{actionDialog.type === 'assign'
|
||||
? 'Select a user to assign this request to'
|
||||
: actionDialog.type === 'pushfnf'
|
||||
? 'This will move the resignation request to F&F for dues clearance'
|
||||
: 'Please provide remarks for this action'
|
||||
: actionDialog.type === 'dispatch'
|
||||
? 'The Legal-issued acceptance letter will be emailed to the dealer and the request will advance to Awaiting F&F. Remarks are optional.'
|
||||
: 'Please provide remarks for this action'
|
||||
}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
@ -1270,7 +1367,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{isSubmitting ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Processing...
|
||||
{actionDialog.type === 'dispatch' ? 'Dispatching...' : 'Processing...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
@ -1280,6 +1377,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
{actionDialog.type === 'revoke' && 'Revoke'}
|
||||
{actionDialog.type === 'assign' && 'Assign'}
|
||||
{actionDialog.type === 'pushfnf' && 'Push to F&F'}
|
||||
{actionDialog.type === 'dispatch' && 'Send to Dealer'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload, PauseCircle } from 'lucide-react';
|
||||
import { ArrowLeft, Check, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, AlertTriangle, Send, ShieldCheck, Loader2, Upload, PauseCircle, FastForward, Info } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
@ -263,7 +263,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return aliases.includes(currentStage);
|
||||
};
|
||||
|
||||
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
|
||||
// Only Super Admin can bypass workflow steps. All other roles must match
|
||||
// the stage they are assigned to. DD Admin is an administrative role and
|
||||
// intentionally has no approve/reject privilege over any stage.
|
||||
const isCurrentlyAssigned = userRole === 'Super Admin' || (
|
||||
(isStage('RBM + DD-ZM Review') && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
|
||||
(isStage('ZBH Review') && userRole === 'ZBH') ||
|
||||
(isStage('DD Lead Review') && userRole === 'DD Lead') ||
|
||||
@ -323,6 +326,16 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return matched || normalized;
|
||||
};
|
||||
|
||||
const hasTimelineActivity = (stageName: string) => {
|
||||
const aliases = stageAliases[stageName] || [stageName];
|
||||
return (request.timeline || []).some((entry: any) => {
|
||||
if (!entry?.stage) return false;
|
||||
if (aliases.includes(entry.stage)) return true;
|
||||
if (stageName === 'Submitted' && (entry.stage === 'Submitted' || entry.stage === 'Request Initiated')) return true;
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
const getProgressStatus = (stageName: string) => {
|
||||
const isTerminal = ['Rejected', 'Revoked', 'Withdrawn'].includes(request.status);
|
||||
const isSuccessFinal = [
|
||||
@ -348,15 +361,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
// If workflow finished successfully or entered F&F, ALL reached stages (including Terminated itself) turn completed
|
||||
if (isSuccessFinal && stageIndex <= currentIndex) {
|
||||
if (stageIndex < currentIndex && stageName !== 'Submitted' && !hasTimelineActivity(stageName)) {
|
||||
return 'skipped';
|
||||
}
|
||||
return 'completed';
|
||||
}
|
||||
|
||||
if (stageIndex === -1) return 'pending';
|
||||
if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending';
|
||||
|
||||
if (stageIndex < currentIndex) return 'completed';
|
||||
if (stageIndex < currentIndex) {
|
||||
if (stageName !== 'Submitted' && !hasTimelineActivity(stageName)) {
|
||||
return 'skipped';
|
||||
}
|
||||
return 'completed';
|
||||
}
|
||||
if (stageIndex === currentIndex) {
|
||||
if (isTerminal) return 'completed'; // Or 'rejected' if component supports it
|
||||
if (isTerminal) return 'completed';
|
||||
return 'active';
|
||||
}
|
||||
|
||||
@ -397,6 +418,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
);
|
||||
};
|
||||
|
||||
const isUnethicalCategory = String(request.category || '').trim().toLowerCase().includes('unethical');
|
||||
const fastTrackReason = isUnethicalCategory
|
||||
? 'Unethical Practice category — request was escalated directly to DD Lead Review.'
|
||||
: null;
|
||||
|
||||
const progressStages = [
|
||||
{
|
||||
id: 1,
|
||||
@ -803,6 +829,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<CardDescription>Track the termination request approval process</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{fastTrackReason && progressStages.some(s => s.status === 'skipped') && (
|
||||
<Alert className="mb-4 border-amber-300 bg-amber-50 text-amber-900">
|
||||
<Info className="h-4 w-4 text-amber-600" />
|
||||
<AlertTitle>Fast-tracked workflow</AlertTitle>
|
||||
<AlertDescription>
|
||||
{fastTrackReason} Stages marked “Skipped” below were intentionally bypassed and never reviewed.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
<div className="space-y-4">
|
||||
{progressStages.map((stage, index) => {
|
||||
const documentCount = stageDocuments[stage.name]?.length || 0;
|
||||
@ -812,19 +847,23 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<div className="flex flex-col items-center">
|
||||
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${stage.status === 'completed' ? 'bg-green-100 text-green-600' :
|
||||
stage.status === 'active' ? 'bg-red-50 text-re-red' :
|
||||
stage.status === 'skipped' ? 'bg-amber-50 text-amber-600 border border-dashed border-amber-300' :
|
||||
'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
{stage.status === 'completed' ? (
|
||||
<Check className="w-5 h-5" />
|
||||
) : stage.status === 'active' ? (
|
||||
<AlertTriangle className="w-5 h-5" />
|
||||
) : stage.status === 'skipped' ? (
|
||||
<FastForward className="w-5 h-5" />
|
||||
) : (
|
||||
<span>{stage.id}</span>
|
||||
)}
|
||||
</div>
|
||||
{index < progressStages.length - 1 && (
|
||||
<div className={`w-0.5 ${stage.remarks ? 'h-32' : 'h-16'
|
||||
} ${stage.status === 'completed' ? 'bg-green-300' : 'bg-slate-200'
|
||||
} ${stage.status === 'completed' ? 'bg-green-300' :
|
||||
stage.status === 'skipped' ? 'bg-amber-200' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
@ -834,8 +873,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<h3 className={
|
||||
stage.status === 'completed' ? 'text-green-600' :
|
||||
stage.status === 'active' ? 'text-re-red' :
|
||||
stage.status === 'skipped' ? 'text-amber-700' :
|
||||
'text-slate-400'
|
||||
}>{stage.name}</h3>
|
||||
{stage.status === 'skipped' && (
|
||||
<Badge
|
||||
className="bg-amber-100 text-amber-800 border-amber-300 hover:bg-amber-100"
|
||||
title={fastTrackReason || 'This stage was bypassed by the workflow.'}
|
||||
>
|
||||
Skipped
|
||||
</Badge>
|
||||
)}
|
||||
{documentCount > 0 && (
|
||||
<button
|
||||
onClick={() => handleViewStageDocuments(stage.name)}
|
||||
@ -854,6 +902,14 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm">{stage.description}</p>
|
||||
{stage.status === 'skipped' && (
|
||||
<p className="text-xs text-amber-700 mt-1 flex items-center gap-1">
|
||||
<FastForward className="w-3 h-3" />
|
||||
{fastTrackReason
|
||||
? `Bypassed — ${fastTrackReason}`
|
||||
: 'This stage was bypassed by the workflow and not reviewed.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{stageEntries.length > 0 && (
|
||||
<div className="mt-3 space-y-3">
|
||||
|
||||
@ -78,7 +78,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
reason: '',
|
||||
proposedLwd: '',
|
||||
comments: '',
|
||||
document: null as File | null
|
||||
documents: [] as File[]
|
||||
});
|
||||
|
||||
const fetchTerminations = async () => {
|
||||
@ -214,6 +214,41 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
setAutoFilledData(mapDealerToFormData(matchedDealer));
|
||||
};
|
||||
|
||||
const isSuperAdmin = currentUser?.role === 'Super Admin';
|
||||
const isPresentationMandatory = !isSuperAdmin;
|
||||
|
||||
const isPptFile = (file: File) => {
|
||||
const name = file.name.toLowerCase();
|
||||
return name.endsWith('.ppt') || name.endsWith('.pptx');
|
||||
};
|
||||
|
||||
const handleFilesPicked = (files: FileList | null, inputEl?: HTMLInputElement | null) => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
setFormData((prev) => {
|
||||
const existing = prev.documents;
|
||||
const seen = new Set(existing.map((f) => `${f.name}::${f.size}`));
|
||||
const additions: File[] = [];
|
||||
Array.from(files).forEach((file) => {
|
||||
const key = `${file.name}::${file.size}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
additions.push(file);
|
||||
}
|
||||
});
|
||||
return { ...prev, documents: [...existing, ...additions] };
|
||||
});
|
||||
|
||||
if (inputEl) inputEl.value = '';
|
||||
};
|
||||
|
||||
const removeDocumentAt = (index: number) => {
|
||||
setFormData({
|
||||
...formData,
|
||||
documents: formData.documents.filter((_, i) => i !== index)
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!autoFilledData) {
|
||||
@ -221,43 +256,50 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = {
|
||||
dealerId: autoFilledData.dealerId || autoFilledData.id,
|
||||
category: formData.terminationCategory,
|
||||
reason: formData.reason,
|
||||
proposedLwd: formData.proposedLwd,
|
||||
comments: formData.comments
|
||||
};
|
||||
if (isPresentationMandatory) {
|
||||
if (formData.documents.length === 0) {
|
||||
toast.error('Please upload at least one Presentation (.ppt or .pptx)');
|
||||
return;
|
||||
}
|
||||
if (!formData.documents.some(isPptFile)) {
|
||||
toast.error('At least one PowerPoint file (.ppt or .pptx) is required');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!payload.dealerId) {
|
||||
try {
|
||||
const dealerId = autoFilledData.dealerId || autoFilledData.id;
|
||||
if (!dealerId) {
|
||||
toast.error('Dealer record not found for the selected dealer');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await API.createTermination(payload);
|
||||
let requestBody: any;
|
||||
if (formData.documents.length > 0) {
|
||||
const fd = new FormData();
|
||||
fd.append('dealerId', String(dealerId));
|
||||
fd.append('category', formData.terminationCategory);
|
||||
fd.append('reason', formData.reason);
|
||||
fd.append('proposedLwd', formData.proposedLwd);
|
||||
fd.append('comments', formData.comments);
|
||||
formData.documents.forEach((file) => fd.append('files', file));
|
||||
requestBody = fd;
|
||||
} else {
|
||||
requestBody = {
|
||||
dealerId,
|
||||
category: formData.terminationCategory,
|
||||
reason: formData.reason,
|
||||
proposedLwd: formData.proposedLwd,
|
||||
comments: formData.comments
|
||||
};
|
||||
}
|
||||
|
||||
const response = await API.createTermination(requestBody);
|
||||
const data = response.data as any;
|
||||
if (data?.success) {
|
||||
// Use termination.id which is the UUID
|
||||
const newId = data.termination?.id;
|
||||
|
||||
// Upload document if selected
|
||||
if (newId && formData.document) {
|
||||
const docFormData = new FormData();
|
||||
docFormData.append('file', formData.document);
|
||||
docFormData.append('documentType', 'Termination Recommendation');
|
||||
docFormData.append('stage', 'Submitted');
|
||||
|
||||
try {
|
||||
await API.uploadTerminationDocument(newId, docFormData);
|
||||
toast.success('Termination request and supporting document submitted');
|
||||
} catch (docErr) {
|
||||
console.error('Error uploading supporting document:', docErr);
|
||||
toast.warning('Termination created, but document upload failed. You can upload it from the details page.');
|
||||
}
|
||||
} else {
|
||||
toast.success('Termination request submitted successfully');
|
||||
}
|
||||
toast.success(formData.documents.length > 0
|
||||
? 'Termination request and documents submitted'
|
||||
: 'Termination request submitted successfully');
|
||||
|
||||
setIsDialogOpen(false);
|
||||
fetchTerminations();
|
||||
@ -271,7 +313,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
reason: '',
|
||||
proposedLwd: '',
|
||||
comments: '',
|
||||
document: null
|
||||
documents: []
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
@ -465,12 +507,45 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="document">Upload Supporting Document</Label>
|
||||
<Label htmlFor="documents">
|
||||
{isPresentationMandatory ? 'Upload Documents *' : 'Upload Supporting Documents'}
|
||||
</Label>
|
||||
<Input
|
||||
id="document"
|
||||
id="documents"
|
||||
type="file"
|
||||
onChange={(e) => setFormData({...formData, document: e.target.files?.[0] || null})}
|
||||
multiple
|
||||
accept=".ppt,.pptx,.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
|
||||
onChange={(e) => handleFilesPicked(e.target.files, e.currentTarget)}
|
||||
required={isPresentationMandatory && formData.documents.length === 0}
|
||||
/>
|
||||
{isPresentationMandatory && (
|
||||
<p className="text-xs text-slate-500">
|
||||
At least one PowerPoint (.ppt / .pptx) is mandatory. You can also attach MOM, dealer commitments, and other supporting files (PDF / DOC / XLS / image).
|
||||
</p>
|
||||
)}
|
||||
{formData.documents.length > 0 && (
|
||||
<div className="border rounded-md divide-y bg-slate-50">
|
||||
{formData.documents.map((file, idx) => (
|
||||
<div key={`${file.name}-${idx}`} className="flex items-center justify-between px-3 py-2 text-sm">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<span className="truncate">{file.name}</span>
|
||||
{isPptFile(file) && (
|
||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">Presentation</Badge>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => removeDocumentAt(idx)}
|
||||
className="text-red-600 hover:text-red-700"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
@ -15,6 +15,7 @@ export const RESIGNATION_STAGE_OPTIONS = [
|
||||
"RBM",
|
||||
"ZBH",
|
||||
"DD Lead",
|
||||
"DD Head",
|
||||
"NBH",
|
||||
"DD Admin",
|
||||
"Legal",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user