sla feature addd and templates also included in the sed file user deacticated changed to in active
This commit is contained in:
parent
01e22e4aa7
commit
6c7640737e
@ -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),
|
||||
|
||||
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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';
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user