sla feature addd and templates also included in the sed file user deacticated changed to in active

This commit is contained in:
laxman h 2026-04-21 19:08:50 +05:30
parent 01e22e4aa7
commit 6c7640737e
12 changed files with 724 additions and 472 deletions

View File

@ -20,7 +20,7 @@ export const API = {
createRegion: (data: any) => client.post('/master/regions', data), createRegion: (data: any) => client.post('/master/regions', data),
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data), updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
getRegions: () => client.get('/master/regions'), getRegions: () => client.get('/master/regions'),
getOutlets: () => client.get('/master/outlets'), getOutlets: () => client.get('/outlets'),
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`), getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params), getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params),
getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params), getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params),

View File

@ -480,8 +480,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
toast.success('Payable item added'); toast.success('Payable item added');
fetchFnFDetails(); fetchFnFDetails();
} }
} catch (error) { } catch (error: any) {
toast.error('Failed to add payable item'); toast.error(error.response?.data?.message || 'Failed to add payable item');
} }
}; };
@ -521,8 +521,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
}); });
toast.success('Changes saved'); toast.success('Changes saved');
fetchFnFDetails(false); fetchFnFDetails(false);
} catch (error) { } catch (error: any) {
toast.error('Failed to update item'); toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false); fetchFnFDetails(false);
} }
}; };
@ -536,8 +536,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
toast.info('Payable item removed'); toast.info('Payable item removed');
fetchFnFDetails(); fetchFnFDetails();
} }
} catch (error) { } catch (error: any) {
toast.error('Failed to delete item'); toast.error(error.response?.data?.message || 'Failed to delete item');
} }
}; };
@ -566,8 +566,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
toast.success('Receivable item added'); toast.success('Receivable item added');
fetchFnFDetails(); fetchFnFDetails();
} }
} catch (error) { } catch (error: any) {
toast.error('Failed to add receivable item'); toast.error(error.response?.data?.message || 'Failed to add receivable item');
} }
}; };
@ -607,8 +607,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
}); });
toast.success('Changes saved'); toast.success('Changes saved');
fetchFnFDetails(false); fetchFnFDetails(false);
} catch (error) { } catch (error: any) {
toast.error('Failed to update item'); toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false); fetchFnFDetails(false);
} }
}; };
@ -619,8 +619,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
setReceivableItems(receivableItems.filter(item => item.id !== id)); setReceivableItems(receivableItems.filter(item => item.id !== id));
toast.info('Receivable item removed'); toast.info('Receivable item removed');
fetchFnFDetails(); fetchFnFDetails();
} catch (error) { } catch (error: any) {
toast.error('Failed to delete item'); toast.error(error.response?.data?.message || 'Failed to delete item');
} }
}; };
@ -649,8 +649,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
toast.success('Deduction item added'); toast.success('Deduction item added');
fetchFnFDetails(); fetchFnFDetails();
} }
} catch (error) { } catch (error: any) {
toast.error('Failed to add deduction item'); toast.error(error.response?.data?.message || 'Failed to add deduction item');
} }
}; };
@ -690,8 +690,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
}); });
toast.success('Changes saved'); toast.success('Changes saved');
fetchFnFDetails(false); fetchFnFDetails(false);
} catch (error) { } catch (error: any) {
toast.error('Failed to update item'); toast.error(error.response?.data?.message || 'Failed to update item');
fetchFnFDetails(false); fetchFnFDetails(false);
} }
}; };

View File

@ -21,7 +21,7 @@ import {
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Plus, Pencil, Trash2, Building2, CreditCard, Landmark } from "lucide-react"; import { Plus, Pencil, Trash2, Building2, Landmark } from "lucide-react";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -42,7 +42,7 @@ import {
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { settlementService } from "@/services/settlement.service";
import { API } from "@/api/API"; import { API } from "@/api/API";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { DocumentPreviewModal } from "@/components/ui/DocumentPreviewModal"; import { DocumentPreviewModal } from "@/components/ui/DocumentPreviewModal";
@ -429,22 +429,30 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
currentUser.role, currentUser.role,
); );
const canRespondToDepartment = (departmentName: string) => { const canRespondToDepartment = (dept: any) => {
if (!fnfCase || !dept) return false;
const role = String(currentUser?.role || "").toLowerCase(); const role = String(currentUser?.role || "").toLowerCase();
if (!role) return false; if (!role) return false;
const isGlobalResponder = // 1. If any user (including Admin) has already responded, hide the button to prevent double-submission
role.includes("super admin") || const hasAlreadyResponded = dept.status !== "Pending";
role.includes("finance") || if (hasAlreadyResponded) return false;
role.includes("dd admin");
if (isGlobalResponder) return true;
const deptKeyword = departmentName.replace(" Department", "").toLowerCase(); // 2. Case Level Closure: Finance Approval or Completed states lock the window
const isWindowClosed = ["Finance Approval", "Completed"].includes(fnfCase.status);
const hasOverridePower = role.includes("super admin") || role.includes("finance") || role.includes("dd admin");
if (isWindowClosed && !hasOverridePower) return false;
// 3. Normal Role Check
if (hasOverridePower) return true;
const deptKeyword = dept.departmentName.replace(" Department", "").toLowerCase();
return role.includes(deptKeyword); return role.includes(deptKeyword);
}; };
const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) => const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) =>
canRespondToDepartment(dept.departmentName), canRespondToDepartment(dept),
); );
const handleUpdateClearance = async () => { const handleUpdateClearance = async () => {
@ -463,7 +471,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
formData.append("type", clearanceForm.type); formData.append("type", clearanceForm.type);
if (clearanceFile) formData.append("file", clearanceFile); if (clearanceFile) formData.append("file", clearanceFile);
await API.updateFnFClearance(fnfId, selectedDept.clearanceId, formData); const response = await API.updateFnFClearance(fnfId, selectedDept.clearanceId, formData) as any;
if (!response.ok) {
toast.error(response.data?.message || "Failed to update department clearance");
setIsUpdatingClearance(false);
return;
}
toast.success(`Clearance updated for ${selectedDept.departmentName}`); toast.success(`Clearance updated for ${selectedDept.departmentName}`);
setShowClearanceDialog(false); setShowClearanceDialog(false);
@ -546,8 +560,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Net Balance Banner */} {/* Net Balance Banner */}
<Card className={`border-none shadow-md bg-gradient-to-r ${ <Card className={`border-none shadow-md bg-gradient-to-r ${(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
? "from-red-600 to-red-500" ? "from-red-600 to-red-500"
: "from-green-600 to-green-500" : "from-green-600 to-green-500"
} text-white`}> } text-white`}>
@ -617,7 +630,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
View Work Notes View Work Notes
</Button> </Button>
{canSendToStakeholders && fnfCase.status === "New" && ( {/* {canSendToStakeholders && fnfCase.status === "New" && (
<Button <Button
className="bg-amber-600 hover:bg-blue-700" className="bg-amber-600 hover:bg-blue-700"
onClick={() => setSendStakeholdersDialog(true)} onClick={() => setSendStakeholdersDialog(true)}
@ -625,7 +638,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Send to Stakeholders Send to Stakeholders
</Button> </Button>
)} )} */}
</div> </div>
</div> </div>
@ -762,8 +775,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center"> <div className="flex shrink-0 flex-col items-center">
<div <div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${ className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${responsesReceived === totalDepartments ||
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) )
@ -785,8 +797,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)} )}
</div> </div>
<div <div
className={`w-0.5 h-full mt-2 ${ className={`w-0.5 h-full mt-2 ${responsesReceived === totalDepartments ||
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) )
@ -902,8 +913,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center"> <div className="flex shrink-0 flex-col items-center">
<div <div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${ className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600" ? "bg-amber-100 border-amber-600"
@ -919,8 +929,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)} )}
</div> </div>
<div <div
className={`w-0.5 h-full mt-2 ${ className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-300" ? "bg-green-300"
: "bg-slate-200" : "bg-slate-200"
}`} }`}
@ -1016,8 +1025,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center"> <div className="flex shrink-0 flex-col items-center">
<div <div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${ className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600" ? "bg-amber-100 border-amber-600"
@ -1033,8 +1041,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)} )}
</div> </div>
<div <div
className={`w-0.5 h-full mt-2 ${ className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-300" ? "bg-green-300"
: "bg-slate-200" : "bg-slate-200"
}`} }`}
@ -1095,8 +1102,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center"> <div className="flex shrink-0 flex-col items-center">
<div <div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${ className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
@ -1108,8 +1114,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)} )}
</div> </div>
<div <div
className={`w-0.5 h-full mt-2 ${ className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-300" ? "bg-green-300"
: "bg-slate-200" : "bg-slate-200"
}`} }`}
@ -1146,8 +1151,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start"> <div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center"> <div className="flex shrink-0 flex-col items-center">
<div <div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${ className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
@ -1407,8 +1411,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableCell> <TableCell>
{dept.amount ? ( {dept.amount ? (
<span <span
className={`font-semibold tabular-nums ${ className={`font-semibold tabular-nums ${dept.duesFlow === "recovery"
dept.duesFlow === "recovery"
? "text-red-700" ? "text-red-700"
: "text-emerald-700" : "text-emerald-700"
}`} }`}
@ -1425,7 +1428,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableCell> </TableCell>
{canAnyDepartmentRespond && ( {canAnyDepartmentRespond && (
<TableCell> <TableCell>
{canRespondToDepartment(dept.departmentName) ? ( {canRespondToDepartment(dept) ? (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
@ -1537,8 +1540,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200"> <div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p> <p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<p <p
className={`text-3xl font-extrabold ${ className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
(fnfCase.netAmount || 0) < 0
? "text-red-600" ? "text-red-600"
: "text-green-600" : "text-green-600"
}`} }`}

View File

@ -243,7 +243,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
{canSendToStakeholders && ( {/* {canSendToStakeholders && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -253,7 +253,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Send to Stakeholders Send to Stakeholders
</Button> </Button>
)} )} */}
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -284,14 +284,12 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className={`p-3 rounded-lg ${ <div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-blue-100' :
fnfCase.status === 'Initiated' ? 'bg-blue-100' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
'bg-green-100' 'bg-green-100'
}`}> }`}>
<IndianRupee className={`w-6 h-6 ${ <IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-blue-600' :
fnfCase.status === 'Initiated' ? 'text-blue-600' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600' 'text-green-600'
@ -328,7 +326,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
<div className="flex items-center gap-2 ml-4"> <div className="flex items-center gap-2 ml-4">
{canSendToStakeholders && fnfCase.status === 'Initiated' && ( {/* {canSendToStakeholders && fnfCase.status === 'Initiated' && (
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
@ -338,7 +336,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Send to Stakeholders Send to Stakeholders
</Button> </Button>
)} )} */}
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"

View File

@ -334,6 +334,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType); if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
else if (!selectedStage || selectedStage === 'General') { else if (!selectedStage || selectedStage === 'General') {
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other']; filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
} else if (selectedStage?.toLowerCase().includes('architecture')) {
filteredDocs = ['Architecture Blueprint', 'Site Plan', 'Proposed Site City Map', 'Site Readiness Report', 'Architecture Completion Certificate', 'Other'];
} else if (selectedStage?.toLowerCase().includes('fdd')) {
filteredDocs = ['FDD Final Audit Report', 'Bank Statement', 'Income Tax Returns (ITR)', 'CIBIL Report', 'Other'];
} else filteredDocs = baseDocs; } else filteredDocs = baseDocs;
if (selectedStage?.startsWith('EOR: ')) { if (selectedStage?.startsWith('EOR: ')) {
const eorItem = selectedStage.replace('EOR: ', ''); const eorItem = selectedStage.replace('EOR: ', '');
@ -499,241 +503,6 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={showDocumentsModal} onOpenChange={(open) => { setShowDocumentsModal(open); if (!open) setShowUploadForm(false); }}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6">
<DialogHeader className="pb-4">
<DialogTitle className="text-xl font-bold flex items-center gap-2"><FileText className="w-5 h-5 text-amber-600" />Documents - {selectedStage || 'General'}</DialogTitle>
<DialogDescription className="text-slate-500">View and manage documents uploaded for this stage.</DialogDescription>
</DialogHeader>
{!showUploadForm ? (
<div className="flex-1 flex flex-col min-h-0 space-y-4">
{getDocumentsForStage(selectedStage || '').length > 0 ? (
<div className="flex-1 overflow-auto border rounded-lg border-slate-200">
<Table className="w-full table-auto">
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-b">
<TableHead className="w-[45%] min-w-[150px] font-semibold text-slate-900 py-3">Document Name</TableHead>
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Type</TableHead>
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Upload Date</TableHead>
<TableHead className="w-[15%] min-w-[140px] font-semibold text-slate-900 py-3">Uploaded By</TableHead>
<TableHead className="text-right w-[10%] min-w-[80px] font-semibold text-slate-900 py-3">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{getDocumentsForStage(selectedStage || '').map((doc: any) => (
<TableRow key={doc.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-3"><div className="flex items-center gap-2 min-w-0"><FileText className="w-4 h-4 text-slate-400 shrink-0" /><span className="truncate font-medium text-slate-700" title={doc.fileName}>{doc.fileName}</span></div></TableCell>
<TableCell className="py-3"><Badge variant="outline" className="capitalize whitespace-nowrap font-normal border-slate-200 bg-white">{doc.documentType?.toLowerCase() || 'Other'}</Badge></TableCell>
<TableCell className="py-3 whitespace-nowrap text-slate-600">{formatDateTime(doc.createdAt)}</TableCell>
<TableCell className="py-3 text-slate-600">{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}</TableCell>
<TableCell className="text-right py-3">
<div className="flex gap-1 justify-end">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}><Eye className="w-4 h-4" /></Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full" onClick={() => { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }}><Download className="w-4 h-4" /></Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div>
)}
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)}><Upload className="w-5 h-5 mr-3" />Upload Document</Button>
<Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)}>Close</Button>
</div>
</div>
) : (
<div className="space-y-6 py-4">
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Stage context</Label>
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}>
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select stage" /></SelectTrigger>
<SelectContent>
<SelectItem value="null">General / No Stage</SelectItem>
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Document Type</Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}>
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select type" /></SelectTrigger>
<SelectContent>
{(() => {
const baseDocs = ['Other'];
const stageConfigs = documentConfigs.filter((c: any) => {
const cfgStage = c.stageCode?.trim();
const selStage = (selectedStage || 'General').trim();
if (cfgStage === selStage) return true;
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true;
if (!selectedStage && cfgStage === 'General') return true;
return false;
});
let filteredDocs: string[] = [];
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
else if (!selectedStage || selectedStage === 'General') {
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
} else filteredDocs = baseDocs;
if (selectedStage?.startsWith('EOR: ')) {
const eorItem = selectedStage.replace('EOR: ', '');
if (!filteredDocs.includes(eorItem)) filteredDocs = [eorItem, ...filteredDocs];
}
return Array.from(new Set(filteredDocs)).map((doc, idx) => <SelectItem key={`${doc}-${idx}`} value={doc}>{doc}</SelectItem>);
})()}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Select File</Label>
<Input type="file" className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer" onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)} />
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-4">
<Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading}>Cancel</Button>
<Button className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading}>
{isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
<DocumentPreviewModal isOpen={showPreviewModal} onClose={() => setShowPreviewModal(false)} document={previewDoc} />
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /><div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]"><ShieldCheck className="w-10 h-10 text-amber-500" /></div></div>
<div className="p-8 space-y-6 bg-white">
<DialogHeader>
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation</Label>
<div className="flex gap-2">
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
<Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)}>{rec}</Button>
))}
</div>
</div>
)}
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
<Textarea placeholder="Summarize key financial findings or discrepancies..." className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm" value={fddAuditFindings} onChange={(e) => setFddAuditFindings(e.target.value)} />
</div>
</div>
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100"><Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /><p className="text-[11px] text-amber-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd}>Cancel</Button>
<Button
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500"
disabled={isFinalizingFdd || !fddAuditFindings}
onClick={async () => {
try {
setIsFinalizingFdd(true);
await onboardingService.submitStageDecision({
applicationId: application!.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Approved',
remarks: (currentUser?.role === 'FDD' || currentUser?.roleCode === 'FDD')
? `Findings: ${fddAuditFindings}`
: `[RECOMMENDATION: ${fddAuditRecommendation}] \nFindings: ${fddAuditFindings}`,
nextStatus: 'LOI In Progress',
nextProgress: 65
});
toast.success('FDD Audit finalized and submitted.');
setShowFddFinalizeModal(false);
fetchApplication();
} catch {
toast.error('Submission failed');
} finally {
setIsFinalizingFdd(false);
}
}}
>
{isFinalizingFdd ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={showFddFlagModal} onOpenChange={setShowFddFlagModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" /><div className="w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center relative z-10 shadow-[0_0_40px_rgba(220,38,38,0.2)]"><ShieldAlert className="w-10 h-10 text-red-500" /></div></div>
<div className="p-8 space-y-6 bg-white text-center">
<DialogHeader>
<DialogTitle className="text-2xl font-black text-slate-900 tracking-tight">Flag Non-Responsive</DialogTitle>
<DialogDescription className="text-slate-500 pt-2 leading-relaxed text-sm font-medium">Are you sure you want to flag this applicant? This will notify the DD Admin that the audit cannot proceed due to applicant's non-cooperation.</DialogDescription>
</DialogHeader>
<div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100"><AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" /><p className="text-[11px] text-red-800 text-left font-medium">"Applicant is unresponsive to multiple queries and financial document requests."</p></div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFlagModal(false)} disabled={isFddFlagging}>Go Back</Button>
<Button
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-red-600"
disabled={isFddFlagging}
onClick={async () => {
try {
setIsFddFlagging(true);
await onboardingService.submitStageDecision({
applicationId: application!.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Rejected',
remarks: 'Applicant is non-responsive to FDD queries.'
});
toast.error('Applicant flagged as non-responsive.');
setShowFddFlagModal(false);
fetchApplication();
} catch {
toast.error('Action failed');
} finally {
setIsFddFlagging(false);
}
}}
>
{isFddFlagging ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Flag'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl">
<div className="bg-amber-600 p-8 text-white">
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div>
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p>
</div>
<div className="p-8 space-y-6 bg-white">
<div className="space-y-2">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution</Label>
<Select value={tempFirmType} onValueChange={setTempFirmType}>
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500"><SelectValue placeholder="Select Firm Type" /></SelectTrigger>
<SelectContent>
<SelectItem value="Proprietorship">Proprietorship</SelectItem>
<SelectItem value="Partnership">Partnership</SelectItem>
<SelectItem value="Limited Liability partnership">LLP (Limited Liability partnership)</SelectItem>
<SelectItem value="Private Limited Company">Private Limited Company</SelectItem>
<SelectItem value="Public Limited Company">Public Limited Company</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3 pt-2">
<Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType}>Cancel</Button>
<Button className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType}>{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button>
</div>
</div>
</DialogContent>
</Dialog>
</> </>
); );
} }

View File

@ -146,6 +146,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId; reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
} }
} }
reqParams.isExternal = false;
const response = await onboardingService.getUsers(reqParams); const response = await onboardingService.getUsers(reqParams);
if (Array.isArray(response)) setUsers(response); if (Array.isArray(response)) setUsers(response);
else if (response && Array.isArray(response.data)) setUsers(response.data); else if (response && Array.isArray(response.data)) setUsers(response.data);

View File

@ -455,17 +455,18 @@ export function FDDApplicationDetails() {
</div> </div>
{/* Right Column: Applicant Meta & Guidelines */} {/* Right Column: Applicant Meta & Guidelines */}
<div className="space-y-6"> {/* Right Column: Applicant Meta & Guidelines */}
<div className="space-y-4">
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-profile-card"> <Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-profile-card">
<CardHeader className="border-b border-slate-100 px-6 py-4"> <CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Applicant Profile</CardTitle> <CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Applicant Profile</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-6 space-y-4"> <CardContent className="p-5 space-y-4">
<div className="space-y-1 pb-4 border-b border-slate-50" data-testid="onboarding-fdd-details-target-loc"> <div className="space-y-1 pb-3 border-b border-slate-50" data-testid="onboarding-fdd-details-target-loc">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Target Location</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Target Location</p>
<p className="text-sm font-extrabold text-slate-900">{application.city}, {application.state}</p> <p className="text-sm font-extrabold text-slate-900">{application.city}, {application.state}</p>
</div> </div>
<div className="grid grid-cols-2 gap-4 text-xs" data-testid="onboarding-fdd-details-profile-meta"> <div className="grid grid-cols-2 gap-3 text-xs" data-testid="onboarding-fdd-details-profile-meta">
<div className="space-y-1"> <div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Education</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Education</p>
<p className="font-bold text-slate-800">{application.education || 'N/A'}</p> <p className="font-bold text-slate-800">{application.education || 'N/A'}</p>
@ -484,7 +485,7 @@ export function FDDApplicationDetails() {
</div> </div>
</div> </div>
<div className="space-y-1 pt-4 border-t border-slate-50 text-xs" data-testid="onboarding-fdd-details-communication"> <div className="space-y-1 pt-3 border-t border-slate-50 text-xs" data-testid="onboarding-fdd-details-communication">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Communication</p> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Communication</p>
<p className="font-bold text-slate-800">{application.email}</p> <p className="font-bold text-slate-800">{application.email}</p>
<p className="text-slate-500 font-medium">{application.phone}</p> <p className="text-slate-500 font-medium">{application.phone}</p>
@ -492,6 +493,63 @@ export function FDDApplicationDetails() {
</CardContent> </CardContent>
</Card> </Card>
{/* Statutory Details Card */}
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-statutory-card">
<CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Statutory Details</CardTitle>
</CardHeader>
<CardContent className="p-5 space-y-3">
<div className="grid grid-cols-1 gap-2.5 text-xs">
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Constitution Type</p>
<p className="font-bold text-slate-800">{application.constitutionType || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">PAN Number</p>
<p className="font-bold text-slate-800 uppercase tracking-tight">{application.panNumber || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">GST Number</p>
<p className="font-bold text-slate-800 uppercase tracking-tight">{application.gstNumber || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Registered Address</p>
<p className="font-medium text-slate-700 leading-relaxed text-[11px]">{application.registeredAddress || 'N/A'}</p>
</div>
</div>
</CardContent>
</Card>
{/* Bank Details Card */}
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-bank-card">
<CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Bank Details</CardTitle>
</CardHeader>
<CardContent className="p-5 space-y-3">
<div className="grid grid-cols-1 gap-2.5 text-xs">
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Account Holder</p>
<p className="font-bold text-slate-800">{application.accountHolderName || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Bank Name</p>
<p className="font-bold text-slate-800">{application.bankName || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Account Number</p>
<p className="font-bold text-slate-800 tabular-nums">{application.accountNumber || 'N/A'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">IFSC Code & Branch</p>
<p className="font-bold text-slate-800 uppercase tracking-tight">
{application.ifscCode || 'N/A'}
{application.branchName && <span className="text-slate-400 font-medium ml-2"> {application.branchName}</span>}
</p>
</div>
</div>
</CardContent>
</Card>
<div className="p-6 bg-slate-900 rounded-lg text-white font-medium" data-testid="onboarding-fdd-details-instructions"> <div className="p-6 bg-slate-900 rounded-lg text-white font-medium" data-testid="onboarding-fdd-details-instructions">
<h4 className="text-sm font-bold mb-2">Instructions</h4> <h4 className="text-sm font-bold mb-2">Instructions</h4>
<ul className="text-xs text-slate-300 space-y-2 list-disc pl-4"> <ul className="text-xs text-slate-300 space-y-2 list-disc pl-4">

View File

@ -86,7 +86,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const fetchUsers = async () => { const fetchUsers = async () => {
try { try {
const response = await adminService.getAllUsers(); const response = await adminService.getAllUsers({ isExternal: false });
// Defensive check for array data // Defensive check for array data
const users = (response && response.success && Array.isArray(response.data)) const users = (response && response.success && Array.isArray(response.data))
? response.data ? response.data
@ -261,12 +261,12 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800', 'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
'Questionnaire Completed': 'bg-cyan-100 text-cyan-800', 'Questionnaire Completed': 'bg-cyan-100 text-cyan-800',
'Shortlisted': 'bg-purple-100 text-purple-800', 'Shortlisted': 'bg-purple-100 text-purple-800',
'Level 1 Pending': 'bg-orange-100 text-orange-800', 'Level 1 Interview Pending': 'bg-orange-100 text-orange-800',
'Level 1 Approved': 'bg-green-100 text-green-800', 'Level 1 Approved': 'bg-green-100 text-green-800',
'Level 2 Pending': 'bg-orange-100 text-orange-800', 'Level 2 Interview Pending': 'bg-orange-100 text-orange-800',
'Level 2 Approved': 'bg-green-100 text-green-800', 'Level 2 Approved': 'bg-green-100 text-green-800',
'Level 2 Recommended': 'bg-teal-100 text-teal-800', 'Level 2 Recommended': 'bg-teal-100 text-teal-800',
'Level 3 Pending': 'bg-orange-100 text-orange-800', 'Level 3 Interview Pending': 'bg-orange-100 text-orange-800',
'FDD Verification': 'bg-indigo-100 text-indigo-800', 'FDD Verification': 'bg-indigo-100 text-indigo-800',
'Payment Pending': 'bg-amber-100 text-amber-800', 'Payment Pending': 'bg-amber-100 text-amber-800',
'LOI Issued': 'bg-sky-100 text-sky-800', 'LOI Issued': 'bg-sky-100 text-sky-800',
@ -290,7 +290,24 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
'Inauguration': 'bg-green-100 text-green-800', 'Inauguration': 'bg-green-100 text-green-800',
'Approved': 'bg-green-100 text-green-800', 'Approved': 'bg-green-100 text-green-800',
'Rejected': 'bg-red-100 text-red-800', 'Rejected': 'bg-red-100 text-red-800',
'Disqualified': 'bg-gray-100 text-gray-800' 'Disqualified': 'bg-gray-100 text-gray-800',
'In Review': 'bg-slate-100 text-slate-800',
'Level 3 Approved': 'bg-green-100 text-green-800',
'LOI In Progress': 'bg-sky-50 text-sky-700',
'LOI Approved': 'bg-green-100 text-green-800',
'Security Details In Progress': 'bg-blue-50 text-blue-700',
'Security Details Approved': 'bg-green-100 text-green-800',
'Security Details': 'bg-blue-100 text-blue-800',
'LOI Issued In Progress': 'bg-sky-50 text-sky-700',
'Statutory Work In Progress': 'bg-emerald-50 text-emerald-700',
'Statutory Work Completed': 'bg-green-100 text-green-800',
'Architecture Work In Progress': 'bg-blue-50 text-blue-700',
'Architecture Work Completed': 'bg-green-100 text-green-800',
'Dealer Code Generation In Progress': 'bg-purple-50 text-purple-700',
'Dealer Code Generated': 'bg-green-100 text-green-800',
'LOA Issued': 'bg-pink-100 text-pink-800',
'EOR Complete': 'bg-violet-100 text-violet-800',
'Onboarded': 'bg-green-200 text-green-900'
}; };
return colors[status] || 'bg-gray-100 text-gray-800'; return colors[status] || 'bg-gray-100 text-gray-800';
}; };

View File

@ -37,6 +37,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
const [newState, setNewState] = useState(''); const [newState, setNewState] = useState('');
const [newAddress, setNewAddress] = useState(''); const [newAddress, setNewAddress] = useState('');
const [reason, setReason] = useState(''); const [reason, setReason] = useState('');
const [distance, setDistance] = useState('');
const [propertyType, setPropertyType] = useState('');
const [expectedDate, setExpectedDate] = useState('');
const [newLat, setNewLat] = useState('');
const [newLong, setNewLong] = useState('');
// State/District dropdown data // State/District dropdown data
const [states, setStates] = useState<any[]>([]); const [states, setStates] = useState<any[]>([]);
@ -139,6 +144,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
return; return;
} }
if (!distance.trim() || !propertyType || !expectedDate) {
toast.error('Please fill all mandatory fields (Distance, Property Type, Date)');
return;
}
try { try {
setSubmitting(true); setSubmitting(true);
const payload = { const payload = {
@ -152,8 +162,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
newState, newState,
newDistrictId: selectedDistrictId || null, newDistrictId: selectedDistrictId || null,
newStateId: selectedStateId || null, newStateId: selectedStateId || null,
distance,
reason, reason,
proposedDate: null propertyType,
proposedDate: expectedDate,
proposedLatitude: newLat ? parseFloat(newLat) : null,
proposedLongitude: newLong ? parseFloat(newLong) : null,
currentLatitude: selectedOutlet.latitude || null,
currentLongitude: selectedOutlet.longitude || null
}; };
await dealerService.submitRelocationRequest(payload); await dealerService.submitRelocationRequest(payload);
@ -167,6 +183,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
setNewState(''); setNewState('');
setNewAddress(''); setNewAddress('');
setReason(''); setReason('');
setDistance('');
setPropertyType('');
setExpectedDate('');
setNewLat('');
setNewLong('');
if (onViewDetails) {
fetchData();
}
} catch (error) { } catch (error) {
console.error('Submit relocation error:', error); console.error('Submit relocation error:', error);
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request')); toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
@ -316,6 +340,43 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
/> />
</div> </div>
<div className="space-y-2">
<Label htmlFor="distance">Estimated Distance from Current Location (in km) *</Label>
<Input
id="distance"
placeholder="e.g. 5.5 km"
value={distance}
onChange={(e) => setDistance(e.target.value)}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="propertyType">Property Type *</Label>
<Select value={propertyType} onValueChange={setPropertyType} required>
<SelectTrigger id="propertyType">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Owned">Owned</SelectItem>
<SelectItem value="Leased">Leased</SelectItem>
<SelectItem value="Rented">Rented</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="expectedDate">Expected Relocation Date *</Label>
<Input
id="expectedDate"
type="date"
value={expectedDate}
onChange={(e) => setExpectedDate(e.target.value)}
required
/>
</div>
</div>
{/* Reason */} {/* Reason */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="reason">Reason for Relocation *</Label> <Label htmlFor="reason">Reason for Relocation *</Label>

View File

@ -647,9 +647,26 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p> <p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p>
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="border-slate-300 text-slate-700"> <Badge variant="outline" className="border-slate-300 text-slate-700">
Type: {request.relocationType} Type: {request.relocationType}
</Badge> </Badge>
{request.distance && (
<Badge variant="outline" className="border-amber-200 bg-amber-50 text-amber-700">
Distance: {request.distance}
</Badge>
)}
{request.propertyType && (
<Badge variant="outline" className="border-blue-200 bg-blue-50 text-blue-700">
Property: {request.propertyType}
</Badge>
)}
{request.expectedRelocationDate && (
<Badge variant="outline" className="border-purple-200 bg-purple-50 text-purple-700">
Expected Date: {request.expectedRelocationDate}
</Badge>
)}
</div>
</div> </div>
</div> </div>
<div> <div>

View File

@ -4,6 +4,12 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { Plus } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { User } from '@/lib/mock-data'; import { User } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
@ -15,6 +21,9 @@ interface RelocationRequestPageProps {
onViewDetails: (id: string) => void; onViewDetails: (id: string) => void;
} }
const getApiErrorMessage = (error: any, fallback: string) =>
error?.response?.data?.message || error?.data?.message || error?.message || fallback;
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300'; if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300'; if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -27,6 +36,32 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
const [requests, setRequests] = useState<any[]>([]); const [requests, setRequests] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
// Relocation Creation State (for Super Admin)
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [outlets, setOutlets] = useState<any[]>([]);
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
const [newCity, setNewCity] = useState('');
const [newState, setNewState] = useState('');
const [newAddress, setNewAddress] = useState('');
const [reason, setReason] = useState('');
const [distance, setDistance] = useState('');
const [propertyType, setPropertyType] = useState('');
const [expectedDate, setExpectedDate] = useState('');
const [newLat, setNewLat] = useState('');
const [newLong, setNewLong] = useState('');
const [submitting, setSubmitting] = useState(false);
// Master Data
const [states, setStates] = useState<any[]>([]);
const [districts, setDistricts] = useState<any[]>([]);
const [selectedStateId, setSelectedStateId] = useState('');
const [selectedDistrictId, setSelectedDistrictId] = useState('');
const [masterDataLoading, setMasterDataLoading] = useState(false);
// Constants
const isSuperAdmin = currentUser?.role === 'Super Admin' || currentUser?.roleCode === 'Super Admin';
const isCompletedRequest = (request: any) => const isCompletedRequest = (request: any) =>
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed'; request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
@ -38,8 +73,121 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
useEffect(() => { useEffect(() => {
fetchRequests(); fetchRequests();
if (isSuperAdmin) {
fetchOutlets();
fetchMasterData();
}
}, []); }, []);
const fetchOutlets = async () => {
try {
const response = await API.getOutlets() as any;
if (response.data.success) {
setOutlets(response.data.outlets || response.data.data?.outlets || response.data.data || []);
}
} catch (error) {
console.error('Fetch outlets error:', error);
}
};
const fetchMasterData = async () => {
try {
setMasterDataLoading(true);
const [statesRes, districtsRes] = await Promise.all([
API.getStates().catch(() => ({ success: false })) as Promise<any>,
API.getDistricts({ limit: 'all' }).catch(() => ({ success: false })) as Promise<any>
]);
const statesData = statesRes?.data?.success ? (statesRes.data.data?.states || statesRes.data.data || []) : [];
const districtsData = districtsRes?.data?.success ? (districtsRes.data.data?.districts || districtsRes.data.data || []) : [];
setStates(statesData);
setDistricts(districtsData);
} catch (error) {
console.error('Fetch master data error:', error);
} finally {
setMasterDataLoading(false);
}
};
const handleStateChange = (stateId: string) => {
setSelectedStateId(stateId);
setSelectedDistrictId('');
const selectedState = states.find(s => s.id === stateId);
if (selectedState) {
setNewState(selectedState.name);
}
};
const handleDistrictChange = (districtId: string) => {
setSelectedDistrictId(districtId);
const selectedDistrict = districts.find(d => d.id === districtId);
if (selectedDistrict) {
setNewCity(selectedDistrict.name);
}
};
const handleSubmitRequest = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedOutlet || !newCity || !newState || !newAddress || !reason || !distance || !propertyType || !expectedDate) {
toast.error('Please fill all mandatory fields');
return;
}
try {
setSubmitting(true);
const payload = {
outletId: selectedOutlet.id,
relocationType: 'Intercity',
currentAddress: selectedOutlet.address || '',
currentCity: selectedOutlet.city || '',
currentState: selectedOutlet.state || '',
newAddress,
newCity,
newState,
newDistrictId: selectedDistrictId || null,
newStateId: selectedStateId || null,
reason,
distance,
propertyType,
proposedDate: expectedDate,
newLatitude: newLat ? parseFloat(newLat) : null,
newLongitude: newLong ? parseFloat(newLong) : null,
currentLatitude: selectedOutlet.latitude || null,
currentLongitude: selectedOutlet.longitude || null
};
const response = await API.createRelocationRequest(payload) as any;
if (response.data.success) {
toast.success(`Relocation request submitted successfully for ${selectedOutlet.name}`);
setIsDialogOpen(false);
fetchRequests();
// Reset form
setSelectedOutlet(null);
setNewCity('');
setNewState('');
setNewAddress('');
setReason('');
setDistance('');
setPropertyType('');
setExpectedDate('');
setNewLat('');
setNewLong('');
}
} catch (error) {
console.error('Submit relocation error:', error);
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
} finally {
setSubmitting(false);
}
};
const filteredDistricts = selectedStateId
? districts.filter(d => d.stateId === selectedStateId)
: districts;
const fetchRequests = async () => { const fetchRequests = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -98,6 +246,187 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
Note: Relocation requests are initiated by the dealer. Note: Relocation requests are initiated by the dealer.
</span> </span>
</div> </div>
{isSuperAdmin && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700 text-white">
<Plus className="w-4 h-4 mr-2" />
New Relocation Request
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Submit Relocation Request</DialogTitle>
<DialogDescription>
Create a new relocation request on behalf of a dealer
</DialogDescription>
</DialogHeader>
<form onSubmit={handleSubmitRequest} className="space-y-4">
{/* Select Outlet */}
<div className="space-y-2">
<Label htmlFor="outlet">Select Outlet to Relocate *</Label>
<Select
value={selectedOutlet?.id}
onValueChange={(val) => setSelectedOutlet(outlets.find(o => o.id === val))}
required
>
<SelectTrigger>
<SelectValue placeholder="Select an outlet" />
</SelectTrigger>
<SelectContent>
{outlets.map((outlet) => (
<SelectItem key={outlet.id} value={outlet.id}>
{outlet.name} ({outlet.code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{selectedOutlet && (
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2 text-sm">
<h3 className="text-slate-900 font-medium">Current Location</h3>
<p className="text-slate-600">{selectedOutlet.address}, {selectedOutlet.city}, {selectedOutlet.state} - {selectedOutlet.pincode}</p>
</div>
)}
{/* New Location Details - State/District Dropdowns */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="newState">Proposed State *</Label>
<Select
value={selectedStateId}
onValueChange={handleStateChange}
required
disabled={masterDataLoading}
>
<SelectTrigger id="newState">
<SelectValue placeholder={masterDataLoading ? "Loading..." : "Select state"} />
</SelectTrigger>
<SelectContent>
{states.map((state) => (
<SelectItem key={state.id} value={state.id}>
{state.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="newCity">Proposed City/District *</Label>
<Select
value={selectedDistrictId}
onValueChange={handleDistrictChange}
required
disabled={!selectedStateId || masterDataLoading}
>
<SelectTrigger id="newCity">
<SelectValue placeholder={!selectedStateId ? "Select state first" : masterDataLoading ? "Loading..." : "Select district"} />
</SelectTrigger>
<SelectContent>
{filteredDistricts.map((district) => (
<SelectItem key={district.id} value={district.id}>
{district.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="newAddress">Proposed Full Address *</Label>
<Textarea
id="newAddress"
placeholder="Enter detailed address of the proposed new location..."
value={newAddress}
onChange={(e) => setNewAddress(e.target.value)}
rows={3}
required
/>
</div>
<div className="space-y-2">
<Label htmlFor="distance">Estimated Distance from Current Location (in km) *</Label>
<Input
id="distance"
type="text"
placeholder="e.g. 5.5 km"
value={distance}
onChange={(e) => setDistance(e.target.value)}
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="propertyType">Property Type *</Label>
<Select value={propertyType} onValueChange={setPropertyType} required>
<SelectTrigger id="propertyType">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Owned">Owned</SelectItem>
<SelectItem value="Leased">Leased</SelectItem>
<SelectItem value="Rented">Rented</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="expectedDate">Expected Relocation Date *</Label>
<Input
id="expectedDate"
type="date"
value={expectedDate}
onChange={(e) => setExpectedDate(e.target.value)}
required
/>
</div>
</div>
{/* Reason */}
<div className="space-y-2">
<Label htmlFor="reason">Reason for Relocation *</Label>
<Textarea
id="reason"
placeholder="Why is this relocation requested?"
value={reason}
onChange={(e) => setReason(e.target.value)}
rows={4}
required
/>
</div>
<DialogFooter>
<Button
type="button"
variant="outline"
onClick={() => setIsDialogOpen(false)}
>
Cancel
</Button>
<Button
type="submit"
className="bg-amber-600 hover:bg-amber-700 text-white"
disabled={submitting}
>
{submitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Submitting...
</>
) : (
'Submit Relocation Request'
)}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
)}
</div> </div>
{/* Statistics Cards */} {/* Statistics Cards */}
@ -199,7 +528,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>
@ -274,7 +603,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>
@ -346,7 +675,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>

View File

@ -11,9 +11,9 @@ export const adminService = {
); );
}, },
async getAllUsers() { async getAllUsers(params?: any) {
try { try {
const response = await API.getUsers() as any; const response = await API.getUsers(params) as any;
return response.data; return response.data;
} catch (error: any) { } catch (error: any) {
console.error('Error fetching users:', error); console.error('Error fetching users:', error);