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),
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
getRegions: () => client.get('/master/regions'),
getOutlets: () => client.get('/master/outlets'),
getOutlets: () => client.get('/outlets'),
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
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),

View File

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

View File

@ -21,7 +21,7 @@ import {
} from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
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 {
Dialog,
DialogContent,
@ -42,7 +42,7 @@ import {
import { Progress } from "@/components/ui/progress";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import { settlementService } from "@/services/settlement.service";
import { API } from "@/api/API";
import { useNavigate } from "react-router-dom";
import { DocumentPreviewModal } from "@/components/ui/DocumentPreviewModal";
@ -56,9 +56,9 @@ interface FnFDetailsProps {
}
const ALL_DEPARTMENTS = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT 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);
useEffect(() => {
fetchFnFDetails();
fetchAuditLogs();
@ -96,7 +96,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const normalizeDepartment = (name: string) => {
if (!name) return name;
let inputName = name.trim();
// Exact match first
const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase());
if (exactMatch) return exactMatch;
@ -216,9 +216,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
mappedCase.netAmount = mappedCase.totalPayableAmount - mappedCase.totalRecoveryAmount - mappedCase.totalDeductions;
mappedCase.departmentResponses = [
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department',
'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department',
'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department',
'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department'
].map((deptName: string) => {
const c = (s.clearances || []).find(
@ -253,8 +253,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const duesFlow =
netAmount > 0 ? ("payable" as const)
: netAmount < 0 ? ("recovery" as const)
: null;
: netAmount < 0 ? ("recovery" as const)
: null;
return {
id: c?.id || `dept-${deptName}`,
@ -304,7 +304,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
participants: s.participants || []
};
setFnfCase(finalMapped);
// Sync bank details from the pre-fetched data
const preFetchedBankDetails = s.bankDetails || s.dealer?.bankDetails || s.outlet?.dealer?.dealerProfile?.bankDetails;
if (preFetchedBankDetails && preFetchedBankDetails.length > 0) {
@ -349,7 +349,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const data = Object.fromEntries(formData.entries());
try {
setIsSubmittingBank(true);
const dealerId = fnfCase?.outlet?.dealer?.id || fnfCase?.dealerId;
@ -357,13 +357,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
toast.error("Dealer information missing");
return;
}
const response = await API.saveBankDetail(dealerId, {
...data,
id: editingBank?.id,
isPrimary: formData.get("isPrimary") === "on",
}) as any;
if (response.data.success) {
toast.success("Bank details saved successfully");
fetchBankDetails(dealerId);
@ -429,22 +429,30 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
currentUser.role,
);
const canRespondToDepartment = (departmentName: string) => {
const canRespondToDepartment = (dept: any) => {
if (!fnfCase || !dept) return false;
const role = String(currentUser?.role || "").toLowerCase();
if (!role) return false;
const isGlobalResponder =
role.includes("super admin") ||
role.includes("finance") ||
role.includes("dd admin");
if (isGlobalResponder) return true;
// 1. If any user (including Admin) has already responded, hide the button to prevent double-submission
const hasAlreadyResponded = dept.status !== "Pending";
if (hasAlreadyResponded) return false;
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);
};
const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) =>
canRespondToDepartment(dept.departmentName),
canRespondToDepartment(dept),
);
const handleUpdateClearance = async () => {
@ -463,7 +471,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
formData.append("type", clearanceForm.type);
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}`);
setShowClearanceDialog(false);
@ -546,11 +560,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
return (
<div className="space-y-6">
{/* Net Balance Banner */}
<Card className={`border-none shadow-md bg-gradient-to-r ${
(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
<Card className={`border-none shadow-md bg-gradient-to-r ${(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
? "from-red-600 to-red-500"
: "from-green-600 to-green-500"
} text-white`}>
} text-white`}>
<CardContent className="p-6 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="p-3 bg-white/20 rounded-full">
@ -617,7 +630,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
View Work Notes
</Button>
{canSendToStakeholders && fnfCase.status === "New" && (
{/* {canSendToStakeholders && fnfCase.status === "New" && (
<Button
className="bg-amber-600 hover:bg-blue-700"
onClick={() => setSendStakeholdersDialog(true)}
@ -625,7 +638,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Send className="w-4 h-4 mr-2" />
Send to Stakeholders
</Button>
)}
)} */}
</div>
</div>
@ -762,21 +775,20 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center">
<div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
? "bg-green-100 border-green-600"
: responsesReceived > 0
? "bg-amber-100 border-amber-600"
: "bg-slate-100 border-slate-300"
}`}
}`}
>
{responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
) ? (
["Finance Approval", "Completed"].includes(
fnfCase.status,
) ? (
<Check className="w-6 h-6 text-green-600" />
) : responsesReceived > 0 ? (
<Users className="w-6 h-6 text-amber-600" />
@ -785,14 +797,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)}
</div>
<div
className={`w-0.5 h-full mt-2 ${
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
className={`w-0.5 h-full mt-2 ${responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
? "bg-green-300"
: "bg-slate-200"
}`}
}`}
></div>
</div>
<div className="flex-1 pb-8">
@ -804,9 +815,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Badge
className={
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
? "bg-green-600"
: responsesReceived > 0
? "bg-amber-600"
@ -814,9 +825,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
}
>
{responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
? "Completed"
: responsesReceived > 0
? "In Progress"
@ -834,9 +845,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Card
className={
responsesReceived === totalDepartments ||
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
["Finance Approval", "Completed"].includes(
fnfCase.status,
)
? "bg-green-50 border-green-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 shrink-0 flex-col items-center">
<div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
fnfCase.status === "Completed"
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600"
: "bg-slate-100 border-slate-300"
}`}
}`}
>
{fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" />
@ -919,11 +929,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)}
</div>
<div
className={`w-0.5 h-full mt-2 ${
fnfCase.status === "Completed"
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
? "bg-green-300"
: "bg-slate-200"
}`}
}`}
></div>
</div>
<div className="flex-1 pb-8">
@ -957,58 +966,58 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{["Finance Approval", "Completed"].includes(
fnfCase.status,
) && (
<Card
className={
fnfCase.status === "Completed"
? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200"
}
>
<CardContent className="p-4">
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-green-100 rounded-lg">
<p className="text-xs text-green-700 mb-1">
Payable Amount
</p>
<p className="text-green-900">
{fnfCase.totalPayableAmount?.toLocaleString() ||
"0"}
</p>
</div>
<div className="text-center p-3 bg-red-100 rounded-lg">
<p className="text-xs text-red-700 mb-1">
Receivable amount
</p>
<p className="text-red-900">
{fnfCase.totalRecoveryAmount?.toLocaleString() ||
"0"}
</p>
</div>
<div className="text-center p-3 bg-amber-100 rounded-lg">
<p className="text-xs text-blue-700 mb-1">
Net Amount
</p>
<p
className={
(fnfCase.totalRecoveryAmount || 0) >
(fnfCase.totalPayableAmount || 0)
? "text-red-900"
: "text-green-900"
}
>
{Math.abs(
(fnfCase.totalRecoveryAmount || 0) -
<Card
className={
fnfCase.status === "Completed"
? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200"
}
>
<CardContent className="p-4">
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-green-100 rounded-lg">
<p className="text-xs text-green-700 mb-1">
Payable Amount
</p>
<p className="text-green-900">
{fnfCase.totalPayableAmount?.toLocaleString() ||
"0"}
</p>
</div>
<div className="text-center p-3 bg-red-100 rounded-lg">
<p className="text-xs text-red-700 mb-1">
Receivable amount
</p>
<p className="text-red-900">
{fnfCase.totalRecoveryAmount?.toLocaleString() ||
"0"}
</p>
</div>
<div className="text-center p-3 bg-amber-100 rounded-lg">
<p className="text-xs text-blue-700 mb-1">
Net Amount
</p>
<p
className={
(fnfCase.totalRecoveryAmount || 0) >
(fnfCase.totalPayableAmount || 0)
? "text-red-900"
: "text-green-900"
}
>
{Math.abs(
(fnfCase.totalRecoveryAmount || 0) -
(fnfCase.totalPayableAmount || 0),
).toLocaleString()}
</p>
).toLocaleString()}
</p>
</div>
</div>
</div>
</CardContent>
</Card>
)}
</CardContent>
</Card>
)}
</div>
</div>
@ -1016,13 +1025,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex gap-4 items-start">
<div className="flex shrink-0 flex-col items-center">
<div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
fnfCase.status === "Completed"
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600"
: "bg-slate-100 border-slate-300"
}`}
}`}
>
{fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" />
@ -1033,11 +1041,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)}
</div>
<div
className={`w-0.5 h-full mt-2 ${
fnfCase.status === "Completed"
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
? "bg-green-300"
: "bg-slate-200"
}`}
}`}
></div>
</div>
<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 shrink-0 flex-col items-center">
<div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
fnfCase.status === "Completed"
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600"
: "bg-slate-100 border-slate-300"
}`}
}`}
>
{fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" />
@ -1108,11 +1114,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
)}
</div>
<div
className={`w-0.5 h-full mt-2 ${
fnfCase.status === "Completed"
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
? "bg-green-300"
: "bg-slate-200"
}`}
}`}
></div>
</div>
<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 shrink-0 flex-col items-center">
<div
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
fnfCase.status === "Completed"
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600"
: "bg-slate-100 border-slate-300"
}`}
}`}
>
{fnfCase.status === "Completed" ? (
<CheckCircle2 className="w-6 h-6 text-green-600" />
@ -1407,11 +1411,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TableCell>
{dept.amount ? (
<span
className={`font-semibold tabular-nums ${
dept.duesFlow === "recovery"
className={`font-semibold tabular-nums ${dept.duesFlow === "recovery"
? "text-red-700"
: "text-emerald-700"
}`}
}`}
>
{dept.amount.toLocaleString()}
</span>
@ -1425,7 +1428,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableCell>
{canAnyDepartmentRespond && (
<TableCell>
{canRespondToDepartment(dept.departmentName) ? (
{canRespondToDepartment(dept) ? (
<Button
variant="ghost"
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">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${
(fnfCase.netAmount || 0) < 0
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
: "text-green-600"
}`}
}`}
>
{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</p>
@ -1644,11 +1646,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const path = doc.url;
const fullPath =
path.startsWith("/uploads/") &&
!path.startsWith("/uploads/documents/")
!path.startsWith("/uploads/documents/")
? path.replace(
"/uploads/",
"/uploads/documents/",
)
"/uploads/",
"/uploads/documents/",
)
: path;
setPreviewDocument({
fileName: doc.name,
@ -1678,7 +1680,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Dealer bank accounts for settlement disbursement
</CardDescription>
</div>
<Button
<Button
onClick={() => {
setEditingBank(null);
setIsBankModalOpen(true);
@ -1709,7 +1711,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<p className="text-xs text-slate-500">{bank.branchName}</p>
</div>
</div>
<div className="space-y-3 mb-4">
<div>
<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 className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<Button
variant="ghost"
size="sm"
<Button
variant="ghost"
size="sm"
className="h-8 text-amber-600"
onClick={() => {
setEditingBank(bank);
@ -1742,9 +1744,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Pencil className="w-3 h-3 mr-1" />
Edit
</Button>
<Button
variant="ghost"
size="sm"
<Button
variant="ghost"
size="sm"
className="h-8 text-red-600"
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">
<Building2 className="w-12 h-12 text-slate-300 mx-auto mb-3" />
<p className="text-slate-600">No bank details found</p>
<Button
variant="link"
<Button
variant="link"
onClick={() => setIsBankModalOpen(true)}
>
Add first bank account
@ -1793,17 +1795,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex-1">
<div className="flex items-center justify-between mb-1">
<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.description && !log.newData?.action) ? log.description : (
<>
{getFriendlyActionName(log.newData?.action || log.action)}
{log.newData?.department && (
<span className="text-amber-600 ml-1 font-bold">
- {log.newData.department}
</span>
)}
</>
)}
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />}
{(log.description && !log.newData?.action) ? log.description : (
<>
{getFriendlyActionName(log.newData?.action || log.action)}
{log.newData?.department && (
<span className="text-amber-600 ml-1 font-bold">
- {log.newData.department}
</span>
)}
</>
)}
</p>
<span className="text-xs text-slate-500">
{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">
<Badge variant="outline" className="text-[10px] uppercase">{log.actor?.name || log.userName || 'System'}</Badge>
</div>
{(log.newData?.remarks || log.remarks) && (
<div className="mt-2 p-3 bg-slate-50 border border-slate-200 rounded text-sm text-slate-700">
{log.newData?.remarks || log.remarks}
@ -1968,12 +1970,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Dialog>
{/* Bank Details Modal */}
<BankDetailsModal
isOpen={isBankModalOpen}
<BankDetailsModal
isOpen={isBankModalOpen}
onClose={() => {
setIsBankModalOpen(false);
setEditingBank(null);
}}
}}
onSubmit={handleUpsertBank}
editingBank={editingBank}
isSubmitting={isSubmittingBank}

View File

@ -33,7 +33,7 @@ const getStatusColor = (status: string) => {
};
const getTypeColor = (type: string) => {
return type === 'Resignation'
return type === 'Resignation'
? 'bg-amber-100 text-amber-700 border-amber-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 [loading, setLoading] = useState(true);
const canSendToStakeholders = currentUser &&
const canSendToStakeholders = currentUser &&
['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role);
useEffect(() => {
@ -118,7 +118,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Newly created</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Clearance</CardDescription>
@ -130,7 +130,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Department / legal stage</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Finance Approval</CardDescription>
@ -142,7 +142,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Ready for finance review</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>Completed</CardDescription>
@ -154,7 +154,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<p className="text-slate-600">Finalized</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-3">
<CardDescription>All Cases</CardDescription>
@ -184,7 +184,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<TabsTrigger value="finance">Finance Approval</TabsTrigger>
<TabsTrigger value="completed">Completed</TabsTrigger>
</TabsList>
{/* Initiated Tab */}
<TabsContent value="initiated" className="mt-6">
<div className="space-y-4">
@ -243,7 +243,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div>
</div>
<div className="flex items-center gap-2 ml-4">
{canSendToStakeholders && (
{/* {canSendToStakeholders && (
<Button
size="sm"
variant="outline"
@ -253,9 +253,9 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Send className="w-4 h-4 mr-2" />
Send to Stakeholders
</Button>
)}
<Button
size="sm"
)} */}
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(fnfCase.id)}
>
@ -275,7 +275,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)}
</div>
</TabsContent>
{/* All Cases Tab */}
<TabsContent value="all" className="mt-6">
<div className="space-y-4">
@ -284,18 +284,16 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6">
<div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1">
<div className={`p-3 rounded-lg ${
fnfCase.status === 'Initiated' ? 'bg-blue-100' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
'bg-green-100'
}`}>
<IndianRupee className={`w-6 h-6 ${
fnfCase.status === 'Initiated' ? 'text-blue-600' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600'
}`} />
<div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-blue-100' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
'bg-green-100'
}`}>
<IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-blue-600' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600'
}`} />
</div>
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
@ -328,9 +326,9 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div>
</div>
<div className="flex items-center gap-2 ml-4">
{canSendToStakeholders && fnfCase.status === 'Initiated' && (
<Button
size="sm"
{/* {canSendToStakeholders && fnfCase.status === 'Initiated' && (
<Button
size="sm"
variant="outline"
className="text-blue-600 border-blue-300 hover:bg-blue-50"
onClick={() => handleSendToStakeholders(fnfCase.id)}
@ -338,9 +336,9 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Send className="w-4 h-4 mr-2" />
Send to Stakeholders
</Button>
)}
<Button
size="sm"
)} */}
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(fnfCase.id)}
>
@ -354,7 +352,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
))}
</div>
</TabsContent>
{/* Clearance Tab */}
<TabsContent value="clearance" className="mt-6">
<div className="space-y-4">
@ -395,8 +393,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div>
</div>
</div>
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(fnfCase.id)}
className="ml-4"
@ -416,7 +414,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)}
</div>
</TabsContent>
{/* Finance Approval Tab */}
<TabsContent value="finance" className="mt-6">
<div className="space-y-4">
@ -459,8 +457,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div>
</div>
</div>
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(fnfCase.id)}
className="ml-4"
@ -480,7 +478,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
)}
</div>
</TabsContent>
{/* Completed Tab */}
<TabsContent value="completed" className="mt-6">
<div className="space-y-4">
@ -519,8 +517,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
</div>
</div>
</div>
<Button
size="sm"
<Button
size="sm"
variant="outline"
onClick={() => onViewDetails(fnfCase.id)}
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);
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 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;
if (selectedStage?.startsWith('EOR: ')) {
const eorItem = selectedStage.replace('EOR: ', '');
@ -499,241 +503,6 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</DialogContent>
</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.isExternal = false;
const response = await onboardingService.getUsers(reqParams);
if (Array.isArray(response)) setUsers(response);
else if (response && Array.isArray(response.data)) setUsers(response.data);

View File

@ -455,17 +455,18 @@ export function FDDApplicationDetails() {
</div>
{/* 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">
<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>
</CardHeader>
<CardContent className="p-6 space-y-4">
<div className="space-y-1 pb-4 border-b border-slate-50" data-testid="onboarding-fdd-details-target-loc">
<CardContent className="p-5 space-y-4">
<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-sm font-extrabold text-slate-900">{application.city}, {application.state}</p>
</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">
<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>
@ -484,7 +485,7 @@ export function FDDApplicationDetails() {
</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="font-bold text-slate-800">{application.email}</p>
<p className="text-slate-500 font-medium">{application.phone}</p>
@ -492,6 +493,63 @@ export function FDDApplicationDetails() {
</CardContent>
</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">
<h4 className="text-sm font-bold mb-2">Instructions</h4>
<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 () => {
try {
const response = await adminService.getAllUsers();
const response = await adminService.getAllUsers({ isExternal: false });
// Defensive check for array data
const users = (response && response.success && Array.isArray(response.data))
? response.data
@ -261,12 +261,12 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
'Questionnaire Completed': 'bg-cyan-100 text-cyan-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 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 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',
'Payment Pending': 'bg-amber-100 text-amber-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',
'Approved': 'bg-green-100 text-green-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';
};

View File

@ -37,6 +37,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
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('');
// State/District dropdown data
const [states, setStates] = useState<any[]>([]);
@ -139,6 +144,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
return;
}
if (!distance.trim() || !propertyType || !expectedDate) {
toast.error('Please fill all mandatory fields (Distance, Property Type, Date)');
return;
}
try {
setSubmitting(true);
const payload = {
@ -152,8 +162,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
newState,
newDistrictId: selectedDistrictId || null,
newStateId: selectedStateId || null,
distance,
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);
@ -167,6 +183,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
setNewState('');
setNewAddress('');
setReason('');
setDistance('');
setPropertyType('');
setExpectedDate('');
setNewLat('');
setNewLong('');
if (onViewDetails) {
fetchData();
}
} catch (error) {
console.error('Submit relocation error:', error);
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
@ -316,6 +340,43 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
/>
</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 */}
<div className="space-y-2">
<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>
</div>
</div>
<Badge variant="outline" className="border-slate-300 text-slate-700">
Type: {request.relocationType}
</Badge>
<div className="flex flex-wrap gap-2">
<Badge variant="outline" className="border-slate-300 text-slate-700">
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>

View File

@ -4,6 +4,12 @@ import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
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 { User } from '@/lib/mock-data';
import { toast } from 'sonner';
@ -15,6 +21,9 @@ interface RelocationRequestPageProps {
onViewDetails: (id: string) => void;
}
const getApiErrorMessage = (error: any, fallback: string) =>
error?.response?.data?.message || error?.data?.message || error?.message || fallback;
const getStatusColor = (status: string) => {
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';
@ -26,6 +35,32 @@ const getStatusColor = (status: string) => {
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
const [requests, setRequests] = useState<any[]>([]);
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) =>
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
@ -38,8 +73,121 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
useEffect(() => {
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 () => {
try {
setIsLoading(true);
@ -98,6 +246,187 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
Note: Relocation requests are initiated by the dealer.
</span>
</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>
{/* Statistics Cards */}
@ -199,7 +528,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</Badge>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage}
</Badge>
</TableCell>
@ -274,7 +603,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage}
</Badge>
</TableCell>
@ -346,7 +675,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div>
</TableCell>
<TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700">
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
{request.currentStage}
</Badge>
</TableCell>

View File

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