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";
@ -56,9 +56,9 @@ interface FnFDetailsProps {
} }
const ALL_DEPARTMENTS = [ const ALL_DEPARTMENTS = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department', 'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department', 'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department', 'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department' 'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
]; ];
@ -87,7 +87,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
}); });
const [clearanceFile, setClearanceFile] = useState<File | null>(null); const [clearanceFile, setClearanceFile] = useState<File | null>(null);
useEffect(() => { useEffect(() => {
fetchFnFDetails(); fetchFnFDetails();
fetchAuditLogs(); fetchAuditLogs();
@ -96,7 +96,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const normalizeDepartment = (name: string) => { const normalizeDepartment = (name: string) => {
if (!name) return name; if (!name) return name;
let inputName = name.trim(); let inputName = name.trim();
// Exact match first // Exact match first
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase()); const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
if (exactMatch) return exactMatch; if (exactMatch) return exactMatch;
@ -216,9 +216,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
mappedCase.netAmount = mappedCase.totalPayableAmount - mappedCase.totalRecoveryAmount - mappedCase.totalDeductions; mappedCase.netAmount = mappedCase.totalPayableAmount - mappedCase.totalRecoveryAmount - mappedCase.totalDeductions;
mappedCase.departmentResponses = [ mappedCase.departmentResponses = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department', 'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department', 'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department', 'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department' 'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
].map((deptName: string) => { ].map((deptName: string) => {
const c = (s.clearances || []).find( const c = (s.clearances || []).find(
@ -253,8 +253,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const duesFlow = const duesFlow =
netAmount > 0 ? ("payable" as const) netAmount > 0 ? ("payable" as const)
: netAmount < 0 ? ("recovery" as const) : netAmount < 0 ? ("recovery" as const)
: null; : null;
return { return {
id: c?.id || `dept-${deptName}`, id: c?.id || `dept-${deptName}`,
@ -304,7 +304,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
participants: s.participants || [] participants: s.participants || []
}; };
setFnfCase(finalMapped); setFnfCase(finalMapped);
// Sync bank details from the pre-fetched data // Sync bank details from the pre-fetched data
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails; const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
if (preFetchedBankDetails && preFetchedBankDetails.length > 0) { if (preFetchedBankDetails && preFetchedBankDetails.length > 0) {
@ -349,7 +349,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
e.preventDefault(); e.preventDefault();
const formData = new FormData(e.currentTarget); const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries()); const data = Object.fromEntries(formData.entries());
try { try {
setIsSubmittingBank(true); setIsSubmittingBank(true);
const dealerId = fnfCase?.outlet?.dealer?.id || fnfCase?.dealerId; const dealerId = fnfCase?.outlet?.dealer?.id || fnfCase?.dealerId;
@ -357,13 +357,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
toast.error("Dealer information missing"); toast.error("Dealer information missing");
return; return;
} }
const response = await API.saveBankDetail(dealerId, { const response = await API.saveBankDetail(dealerId, {
...data, ...data,
id: editingBank?.id, id: editingBank?.id,
isPrimary: formData.get("isPrimary") === "on", isPrimary: formData.get("isPrimary") === "on",
}) as any; }) as any;
if (response.data.success) { if (response.data.success) {
toast.success("Bank details saved successfully"); toast.success("Bank details saved successfully");
fetchBankDetails(dealerId); fetchBankDetails(dealerId);
@ -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,11 +560,10 @@ 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`}>
<CardContent className="p-6 flex items-center justify-between"> <CardContent className="p-6 flex items-center justify-between">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-full"> <div className="p-3 bg-white/20 rounded-full">
@ -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,21 +775,20 @@ 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, )
)
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: responsesReceived > 0 : responsesReceived > 0
? "bg-amber-100 border-amber-600" ? "bg-amber-100 border-amber-600"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
{responsesReceived === totalDepartments || {responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) ? ( ) ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
) : responsesReceived > 0 ? ( ) : responsesReceived > 0 ? (
<Users className="w-6 h-6 text-amber-600" /> <Users className="w-6 h-6 text-amber-600" />
@ -785,14 +797,13 @@ 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, )
)
? "bg-green-300" ? "bg-green-300"
: "bg-slate-200" : "bg-slate-200"
}`} }`}
></div> ></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
@ -804,9 +815,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Badge <Badge
className={ className={
responsesReceived === totalDepartments || responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) )
? "bg-green-600" ? "bg-green-600"
: responsesReceived > 0 : responsesReceived > 0
? "bg-amber-600" ? "bg-amber-600"
@ -814,9 +825,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
} }
> >
{responsesReceived === totalDepartments || {responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) )
? "Completed" ? "Completed"
: responsesReceived > 0 : responsesReceived > 0
? "In Progress" ? "In Progress"
@ -834,9 +845,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Card <Card
className={ className={
responsesReceived === totalDepartments || responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes( ["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) )
? "bg-green-50 border-green-200" ? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200" : "bg-blue-50 border-amber-200"
} }
@ -902,13 +913,12 @@ 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"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
@ -919,11 +929,10 @@ 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"
}`} }`}
></div> ></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
@ -957,58 +966,58 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{["Finance Approval", "Completed"].includes( {["Finance Approval", "Completed"].includes(
fnfCase.status, fnfCase.status,
) && ( ) && (
<Card <Card
className={ className={
fnfCase.status === "Completed" fnfCase.status === "Completed"
? "bg-green-50 border-green-200" ? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200" : "bg-blue-50 border-amber-200"
} }
> >
<CardContent className="p-4"> <CardContent className="p-4">
<div className="grid grid-cols-3 gap-4"> <div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-green-100 rounded-lg"> <div className="text-center p-3 bg-green-100 rounded-lg">
<p className="text-xs text-green-700 mb-1"> <p className="text-xs text-green-700 mb-1">
Payable Amount Payable Amount
</p> </p>
<p className="text-green-900"> <p className="text-green-900">
{fnfCase.totalPayableAmount?.toLocaleString() || {fnfCase.totalPayableAmount?.toLocaleString() ||
"0"} "0"}
</p> </p>
</div> </div>
<div className="text-center p-3 bg-red-100 rounded-lg"> <div className="text-center p-3 bg-red-100 rounded-lg">
<p className="text-xs text-red-700 mb-1"> <p className="text-xs text-red-700 mb-1">
Receivable amount Receivable amount
</p> </p>
<p className="text-red-900"> <p className="text-red-900">
{fnfCase.totalRecoveryAmount?.toLocaleString() || {fnfCase.totalRecoveryAmount?.toLocaleString() ||
"0"} "0"}
</p> </p>
</div> </div>
<div className="text-center p-3 bg-amber-100 rounded-lg"> <div className="text-center p-3 bg-amber-100 rounded-lg">
<p className="text-xs text-blue-700 mb-1"> <p className="text-xs text-blue-700 mb-1">
Net Amount Net Amount
</p> </p>
<p <p
className={ className={
(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalRecoveryAmount || 0) >
(fnfCase.totalPayableAmount || 0) (fnfCase.totalPayableAmount || 0)
? "text-red-900" ? "text-red-900"
: "text-green-900" : "text-green-900"
} }
> >
{Math.abs( {Math.abs(
(fnfCase.totalRecoveryAmount || 0) - (fnfCase.totalRecoveryAmount || 0) -
(fnfCase.totalPayableAmount || 0), (fnfCase.totalPayableAmount || 0),
).toLocaleString()} ).toLocaleString()}
</p> </p>
</div>
</div> </div>
</div> </CardContent>
</CardContent> </Card>
</Card> )}
)}
</div> </div>
</div> </div>
@ -1016,13 +1025,12 @@ 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"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
@ -1033,11 +1041,10 @@ 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"
}`} }`}
></div> ></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
@ -1095,11 +1102,10 @@ 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"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
@ -1108,11 +1114,10 @@ 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"
}`} }`}
></div> ></div>
</div> </div>
<div className="flex-1 pb-8"> <div className="flex-1 pb-8">
@ -1146,11 +1151,10 @@ 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"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<CheckCircle2 className="w-6 h-6 text-green-600" /> <CheckCircle2 className="w-6 h-6 text-green-600" />
@ -1407,11 +1411,10 @@ 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"
}`} }`}
> >
{dept.amount.toLocaleString()} {dept.amount.toLocaleString()}
</span> </span>
@ -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,11 +1540,10 @@ 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"
}`} }`}
> >
{Math.abs(fnfCase.netAmount || 0).toLocaleString()} {Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</p> </p>
@ -1644,11 +1646,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const path = doc.url; const path = doc.url;
const fullPath = const fullPath =
path.startsWith("/uploads/") && path.startsWith("/uploads/") &&
!path.startsWith("/uploads/documents/") !path.startsWith("/uploads/documents/")
? path.replace( ? path.replace(
"/uploads/", "/uploads/",
"/uploads/documents/", "/uploads/documents/",
) )
: path; : path;
setPreviewDocument({ setPreviewDocument({
fileName: doc.name, fileName: doc.name,
@ -1678,7 +1680,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Dealer bank accounts for settlement disbursement Dealer bank accounts for settlement disbursement
</CardDescription> </CardDescription>
</div> </div>
<Button <Button
onClick={() => { onClick={() => {
setEditingBank(null); setEditingBank(null);
setIsBankModalOpen(true); setIsBankModalOpen(true);
@ -1709,7 +1711,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<p className="text-xs text-slate-500">{bank.branchName}</p> <p className="text-xs text-slate-500">{bank.branchName}</p>
</div> </div>
</div> </div>
<div className="space-y-3 mb-4"> <div className="space-y-3 mb-4">
<div> <div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</p> <p className="text-[10px] text-slate-500 uppercase font-bold">Account Holder</p>
@ -1730,9 +1732,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</div> </div>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100"> <div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 text-amber-600" className="h-8 text-amber-600"
onClick={() => { onClick={() => {
setEditingBank(bank); setEditingBank(bank);
@ -1742,9 +1744,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Pencil className="w-3 h-3 mr-1" /> <Pencil className="w-3 h-3 mr-1" />
Edit Edit
</Button> </Button>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 text-red-600" className="h-8 text-red-600"
onClick={() => handleDeleteBank(bank.id)} onClick={() => handleDeleteBank(bank.id)}
> >
@ -1759,8 +1761,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50"> <div className="col-span-full py-12 text-center border-2 border-dashed rounded-lg bg-slate-50">
<Building2 className="w-12 h-12 text-slate-300 mx-auto mb-3" /> <Building2 className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-600">No bank details found</p> <p className="text-slate-600">No bank details found</p>
<Button <Button
variant="link" variant="link"
onClick={() => setIsBankModalOpen(true)} onClick={() => setIsBankModalOpen(true)}
> >
Add first bank account Add first bank account
@ -1793,17 +1795,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2"> <p className="font-semibold text-slate-900 flex items-center gap-2">
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />} {log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />}
{(log.description && !log.newData?.action) ? log.description : ( {(log.description && !log.newData?.action) ? log.description : (
<> <>
{getFriendlyActionName(log.newData?.action || log.action)} {getFriendlyActionName(log.newData?.action || log.action)}
{log.newData?.department && ( {log.newData?.department && (
<span className="text-amber-600 ml-1 font-bold"> <span className="text-amber-600 ml-1 font-bold">
- {log.newData.department} - {log.newData.department}
</span> </span>
)} )}
</> </>
)} )}
</p> </p>
<span className="text-xs text-slate-500"> <span className="text-xs text-slate-500">
{formatDateTime(log.createdAt || log.timestamp)} {formatDateTime(log.createdAt || log.timestamp)}
@ -1812,7 +1814,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex items-center gap-2 text-sm text-slate-600 mb-2"> <div className="flex items-center gap-2 text-sm text-slate-600 mb-2">
<Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge> <Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge>
</div> </div>
{(log.newData?.remarks || log.remarks) && ( {(log.newData?.remarks || log.remarks) && (
<div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700"> <div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700">
{log.newData?.remarks || log.remarks} {log.newData?.remarks || log.remarks}
@ -1968,12 +1970,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Dialog> </Dialog>
{/* Bank Details Modal */} {/* Bank Details Modal */}
<BankDetailsModal <BankDetailsModal
isOpen={isBankModalOpen} isOpen={isBankModalOpen}
onClose={() => { onClose={() => {
setIsBankModalOpen(false); setIsBankModalOpen(false);
setEditingBank(null); setEditingBank(null);
}} }}
onSubmit={handleUpsertBank} onSubmit={handleUpsertBank}
editingBank={editingBank} editingBank={editingBank}
isSubmitting={isSubmittingBank} isSubmitting={isSubmittingBank}

View File

@ -33,7 +33,7 @@ const getStatusColor = (status: string) => {
}; };
const getTypeColor = (type: string) => { const getTypeColor = (type: string) => {
return type === 'Resignation' return type === 'Resignation'
? 'bg-amber-100 text-amber-700 border-amber-300' ? 'bg-amber-100 text-amber-700 border-amber-300'
: 'bg-red-100 text-red-700 border-red-300'; : 'bg-red-100 text-red-700 border-red-300';
}; };
@ -42,7 +42,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
const [settlements, setSettlements] = useState<any[]>([]); const [settlements, setSettlements] = useState<any[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const canSendToStakeholders = currentUser && const canSendToStakeholders = currentUser &&
['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
useEffect(() => { useEffect(() => {
@ -118,7 +118,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Newly created</p> <p className="text-slate-600">Newly created</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Clearance</CardDescription> <CardDescription>Clearance</CardDescription>
@ -130,7 +130,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Department / legal stage</p> <p className="text-slate-600">Department / legal stage</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Finance Approval</CardDescription> <CardDescription>Finance Approval</CardDescription>
@ -142,7 +142,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Ready for finance review</p> <p className="text-slate-600">Ready for finance review</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Completed</CardDescription> <CardDescription>Completed</CardDescription>
@ -154,7 +154,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Finalized</p> <p className="text-slate-600">Finalized</p>
</CardContent> </CardContent>
</Card> </Card>
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>All Cases</CardDescription> <CardDescription>All Cases</CardDescription>
@ -184,7 +184,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<TabsTrigger value="finance">Finance Approval</TabsTrigger> <TabsTrigger value="finance">Finance Approval</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger> <TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList> </TabsList>
{/* Initiated Tab */} {/* Initiated Tab */}
<TabsContent value="initiated" className="mt-6"> <TabsContent value="initiated" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -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,9 +253,9 @@ 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"
onClick={() => onViewDetails(fnfCase.id)} onClick={() => onViewDetails(fnfCase.id)}
> >
@ -275,7 +275,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)} )}
</div> </div>
</TabsContent> </TabsContent>
{/* All Cases Tab */} {/* All Cases Tab */}
<TabsContent value="all" className="mt-6"> <TabsContent value="all" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -284,18 +284,16 @@ 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 ${fnfCase.status === 'Initiated' ? 'text-blue-600' :
<IndianRupee className={`w-6 h-6 ${ (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
fnfCase.status === 'Initiated' ? 'text-blue-600' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' : 'text-green-600'
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' : }`} />
'text-green-600'
}`} />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
@ -328,9 +326,9 @@ 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"
className="text-blue-600 border-blue-300 hover:bg-blue-50" className="text-blue-600 border-blue-300 hover:bg-blue-50"
onClick={() => handleSendToStakeholders(fnfCase.id)} onClick={() => handleSendToStakeholders(fnfCase.id)}
@ -338,9 +336,9 @@ 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"
onClick={() => onViewDetails(fnfCase.id)} onClick={() => onViewDetails(fnfCase.id)}
> >
@ -354,7 +352,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
))} ))}
</div> </div>
</TabsContent> </TabsContent>
{/* Clearance Tab */} {/* Clearance Tab */}
<TabsContent value="clearance" className="mt-6"> <TabsContent value="clearance" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -395,8 +393,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
</div> </div>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => onViewDetails(fnfCase.id)} onClick={() => onViewDetails(fnfCase.id)}
className="ml-4" className="ml-4"
@ -416,7 +414,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)} )}
</div> </div>
</TabsContent> </TabsContent>
{/* Finance Approval Tab */} {/* Finance Approval Tab */}
<TabsContent value="finance" className="mt-6"> <TabsContent value="finance" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -459,8 +457,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
</div> </div>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => onViewDetails(fnfCase.id)} onClick={() => onViewDetails(fnfCase.id)}
className="ml-4" className="ml-4"
@ -480,7 +478,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)} )}
</div> </div>
</TabsContent> </TabsContent>
{/* Completed Tab */} {/* Completed Tab */}
<TabsContent value="completed" className="mt-6"> <TabsContent value="completed" className="mt-6">
<div className="space-y-4"> <div className="space-y-4">
@ -519,8 +517,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div> </div>
</div> </div>
</div> </div>
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
onClick={() => onViewDetails(fnfCase.id)} onClick={() => onViewDetails(fnfCase.id)}
className="ml-4" className="ml-4"

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>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <div className="flex flex-wrap gap-2">
Type: {request.relocationType} <Badge variant="outline" className="border-slate-300 text-slate-700">
</Badge> Type: {request.relocationType}
</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';
@ -26,6 +35,32 @@ const getStatusColor = (status: string) => {
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) { export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
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);