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),
|
createRegion: (data: any) => client.post('/master/regions', data),
|
||||||
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
|
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
|
||||||
getRegions: () => client.get('/master/regions'),
|
getRegions: () => client.get('/master/regions'),
|
||||||
getOutlets: () => client.get('/master/outlets'),
|
getOutlets: () => client.get('/outlets'),
|
||||||
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
||||||
getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params),
|
getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params),
|
||||||
getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params),
|
getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params),
|
||||||
|
|||||||
@ -480,8 +480,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
toast.success('Payable item added');
|
toast.success('Payable item added');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to add payable item');
|
toast.error(error.response?.data?.message || 'Failed to add payable item');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -521,8 +521,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
});
|
});
|
||||||
toast.success('Changes saved');
|
toast.success('Changes saved');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to update item');
|
toast.error(error.response?.data?.message || 'Failed to update item');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -536,8 +536,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
toast.info('Payable item removed');
|
toast.info('Payable item removed');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to delete item');
|
toast.error(error.response?.data?.message || 'Failed to delete item');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -566,8 +566,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
toast.success('Receivable item added');
|
toast.success('Receivable item added');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to add receivable item');
|
toast.error(error.response?.data?.message || 'Failed to add receivable item');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -607,8 +607,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
});
|
});
|
||||||
toast.success('Changes saved');
|
toast.success('Changes saved');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to update item');
|
toast.error(error.response?.data?.message || 'Failed to update item');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -619,8 +619,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
setReceivableItems(receivableItems.filter(item => item.id !== id));
|
setReceivableItems(receivableItems.filter(item => item.id !== id));
|
||||||
toast.info('Receivable item removed');
|
toast.info('Receivable item removed');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to delete item');
|
toast.error(error.response?.data?.message || 'Failed to delete item');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -649,8 +649,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
toast.success('Deduction item added');
|
toast.success('Deduction item added');
|
||||||
fetchFnFDetails();
|
fetchFnFDetails();
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to add deduction item');
|
toast.error(error.response?.data?.message || 'Failed to add deduction item');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -690,8 +690,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
|
|||||||
});
|
});
|
||||||
toast.success('Changes saved');
|
toast.success('Changes saved');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
toast.error('Failed to update item');
|
toast.error(error.response?.data?.message || 'Failed to update item');
|
||||||
fetchFnFDetails(false);
|
fetchFnFDetails(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
} from "@/components/ui/card";
|
} from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Plus, Pencil, Trash2, Building2, CreditCard, Landmark } from "lucide-react";
|
import { Plus, Pencil, Trash2, Building2, Landmark } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -42,7 +42,7 @@ import {
|
|||||||
import { Progress } from "@/components/ui/progress";
|
import { Progress } from "@/components/ui/progress";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { settlementService } from "@/services/settlement.service";
|
|
||||||
import { API } from "@/api/API";
|
import { API } from "@/api/API";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { DocumentPreviewModal } from "@/components/ui/DocumentPreviewModal";
|
import { DocumentPreviewModal } from "@/components/ui/DocumentPreviewModal";
|
||||||
@ -253,8 +253,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
|
|
||||||
const duesFlow =
|
const duesFlow =
|
||||||
netAmount > 0 ? ("payable" as const)
|
netAmount > 0 ? ("payable" as const)
|
||||||
: netAmount < 0 ? ("recovery" as const)
|
: netAmount < 0 ? ("recovery" as const)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: c?.id || `dept-${deptName}`,
|
id: c?.id || `dept-${deptName}`,
|
||||||
@ -429,22 +429,30 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
currentUser.role,
|
currentUser.role,
|
||||||
);
|
);
|
||||||
|
|
||||||
const canRespondToDepartment = (departmentName: string) => {
|
const canRespondToDepartment = (dept: any) => {
|
||||||
|
if (!fnfCase || !dept) return false;
|
||||||
const role = String(currentUser?.role || "").toLowerCase();
|
const role = String(currentUser?.role || "").toLowerCase();
|
||||||
if (!role) return false;
|
if (!role) return false;
|
||||||
|
|
||||||
const isGlobalResponder =
|
// 1. If any user (including Admin) has already responded, hide the button to prevent double-submission
|
||||||
role.includes("super admin") ||
|
const hasAlreadyResponded = dept.status !== "Pending";
|
||||||
role.includes("finance") ||
|
if (hasAlreadyResponded) return false;
|
||||||
role.includes("dd admin");
|
|
||||||
if (isGlobalResponder) return true;
|
|
||||||
|
|
||||||
const deptKeyword = departmentName.replace(" Department", "").toLowerCase();
|
// 2. Case Level Closure: Finance Approval or Completed states lock the window
|
||||||
|
const isWindowClosed = ["Finance Approval", "Completed"].includes(fnfCase.status);
|
||||||
|
const hasOverridePower = role.includes("super admin") || role.includes("finance") || role.includes("dd admin");
|
||||||
|
|
||||||
|
if (isWindowClosed && !hasOverridePower) return false;
|
||||||
|
|
||||||
|
// 3. Normal Role Check
|
||||||
|
if (hasOverridePower) return true;
|
||||||
|
|
||||||
|
const deptKeyword = dept.departmentName.replace(" Department", "").toLowerCase();
|
||||||
return role.includes(deptKeyword);
|
return role.includes(deptKeyword);
|
||||||
};
|
};
|
||||||
|
|
||||||
const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) =>
|
const canAnyDepartmentRespond = (fnfCase?.departmentResponses || []).some((dept: any) =>
|
||||||
canRespondToDepartment(dept.departmentName),
|
canRespondToDepartment(dept),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleUpdateClearance = async () => {
|
const handleUpdateClearance = async () => {
|
||||||
@ -463,7 +471,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
formData.append("type", clearanceForm.type);
|
formData.append("type", clearanceForm.type);
|
||||||
if (clearanceFile) formData.append("file", clearanceFile);
|
if (clearanceFile) formData.append("file", clearanceFile);
|
||||||
|
|
||||||
await API.updateFnFClearance(fnfId, selectedDept.clearanceId, formData);
|
const response = await API.updateFnFClearance(fnfId, selectedDept.clearanceId, formData) as any;
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
toast.error(response.data?.message || "Failed to update department clearance");
|
||||||
|
setIsUpdatingClearance(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
toast.success(`Clearance updated for ${selectedDept.departmentName}`);
|
||||||
setShowClearanceDialog(false);
|
setShowClearanceDialog(false);
|
||||||
@ -546,11 +560,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Net Balance Banner */}
|
{/* Net Balance Banner */}
|
||||||
<Card className={`border-none shadow-md bg-gradient-to-r ${
|
<Card className={`border-none shadow-md bg-gradient-to-r ${(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
|
||||||
(fnfCase.totalRecoveryAmount || 0) > (fnfCase.totalPayableAmount || 0)
|
|
||||||
? "from-red-600 to-red-500"
|
? "from-red-600 to-red-500"
|
||||||
: "from-green-600 to-green-500"
|
: "from-green-600 to-green-500"
|
||||||
} text-white`}>
|
} text-white`}>
|
||||||
<CardContent className="p-6 flex items-center justify-between">
|
<CardContent className="p-6 flex items-center justify-between">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<div className="p-3 bg-white/20 rounded-full">
|
<div className="p-3 bg-white/20 rounded-full">
|
||||||
@ -617,7 +630,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
View Work Notes
|
View Work Notes
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{canSendToStakeholders && fnfCase.status === "New" && (
|
{/* {canSendToStakeholders && fnfCase.status === "New" && (
|
||||||
<Button
|
<Button
|
||||||
className="bg-amber-600 hover:bg-blue-700"
|
className="bg-amber-600 hover:bg-blue-700"
|
||||||
onClick={() => setSendStakeholdersDialog(true)}
|
onClick={() => setSendStakeholdersDialog(true)}
|
||||||
@ -625,7 +638,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
Send to Stakeholders
|
Send to Stakeholders
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -762,21 +775,20 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<div className="flex shrink-0 flex-col items-center">
|
<div className="flex shrink-0 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
|
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${responsesReceived === totalDepartments ||
|
||||||
responsesReceived === totalDepartments ||
|
["Finance Approval", "Completed"].includes(
|
||||||
["Finance Approval", "Completed"].includes(
|
fnfCase.status,
|
||||||
fnfCase.status,
|
)
|
||||||
)
|
|
||||||
? "bg-green-100 border-green-600"
|
? "bg-green-100 border-green-600"
|
||||||
: responsesReceived > 0
|
: responsesReceived > 0
|
||||||
? "bg-amber-100 border-amber-600"
|
? "bg-amber-100 border-amber-600"
|
||||||
: "bg-slate-100 border-slate-300"
|
: "bg-slate-100 border-slate-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{responsesReceived === totalDepartments ||
|
{responsesReceived === totalDepartments ||
|
||||||
["Finance Approval", "Completed"].includes(
|
["Finance Approval", "Completed"].includes(
|
||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
) ? (
|
) ? (
|
||||||
<Check className="w-6 h-6 text-green-600" />
|
<Check className="w-6 h-6 text-green-600" />
|
||||||
) : responsesReceived > 0 ? (
|
) : responsesReceived > 0 ? (
|
||||||
<Users className="w-6 h-6 text-amber-600" />
|
<Users className="w-6 h-6 text-amber-600" />
|
||||||
@ -785,14 +797,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-0.5 h-full mt-2 ${
|
className={`w-0.5 h-full mt-2 ${responsesReceived === totalDepartments ||
|
||||||
responsesReceived === totalDepartments ||
|
["Finance Approval", "Completed"].includes(
|
||||||
["Finance Approval", "Completed"].includes(
|
fnfCase.status,
|
||||||
fnfCase.status,
|
)
|
||||||
)
|
|
||||||
? "bg-green-300"
|
? "bg-green-300"
|
||||||
: "bg-slate-200"
|
: "bg-slate-200"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -804,9 +815,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Badge
|
<Badge
|
||||||
className={
|
className={
|
||||||
responsesReceived === totalDepartments ||
|
responsesReceived === totalDepartments ||
|
||||||
["Finance Approval", "Completed"].includes(
|
["Finance Approval", "Completed"].includes(
|
||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
)
|
)
|
||||||
? "bg-green-600"
|
? "bg-green-600"
|
||||||
: responsesReceived > 0
|
: responsesReceived > 0
|
||||||
? "bg-amber-600"
|
? "bg-amber-600"
|
||||||
@ -814,9 +825,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{responsesReceived === totalDepartments ||
|
{responsesReceived === totalDepartments ||
|
||||||
["Finance Approval", "Completed"].includes(
|
["Finance Approval", "Completed"].includes(
|
||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
)
|
)
|
||||||
? "Completed"
|
? "Completed"
|
||||||
: responsesReceived > 0
|
: responsesReceived > 0
|
||||||
? "In Progress"
|
? "In Progress"
|
||||||
@ -834,9 +845,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<Card
|
<Card
|
||||||
className={
|
className={
|
||||||
responsesReceived === totalDepartments ||
|
responsesReceived === totalDepartments ||
|
||||||
["Finance Approval", "Completed"].includes(
|
["Finance Approval", "Completed"].includes(
|
||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
)
|
)
|
||||||
? "bg-green-50 border-green-200"
|
? "bg-green-50 border-green-200"
|
||||||
: "bg-blue-50 border-amber-200"
|
: "bg-blue-50 border-amber-200"
|
||||||
}
|
}
|
||||||
@ -902,13 +913,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<div className="flex shrink-0 flex-col items-center">
|
<div className="flex shrink-0 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
|
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-100 border-green-600"
|
? "bg-green-100 border-green-600"
|
||||||
: fnfCase.status === "Finance Approval"
|
: fnfCase.status === "Finance Approval"
|
||||||
? "bg-amber-100 border-amber-600"
|
? "bg-amber-100 border-amber-600"
|
||||||
: "bg-slate-100 border-slate-300"
|
: "bg-slate-100 border-slate-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{fnfCase.status === "Completed" ? (
|
{fnfCase.status === "Completed" ? (
|
||||||
<Check className="w-6 h-6 text-green-600" />
|
<Check className="w-6 h-6 text-green-600" />
|
||||||
@ -919,11 +929,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-0.5 h-full mt-2 ${
|
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-300"
|
? "bg-green-300"
|
||||||
: "bg-slate-200"
|
: "bg-slate-200"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -957,58 +966,58 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
{["Finance Approval", "Completed"].includes(
|
{["Finance Approval", "Completed"].includes(
|
||||||
fnfCase.status,
|
fnfCase.status,
|
||||||
) && (
|
) && (
|
||||||
<Card
|
<Card
|
||||||
className={
|
className={
|
||||||
fnfCase.status === "Completed"
|
fnfCase.status === "Completed"
|
||||||
? "bg-green-50 border-green-200"
|
? "bg-green-50 border-green-200"
|
||||||
: "bg-blue-50 border-amber-200"
|
: "bg-blue-50 border-amber-200"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="grid grid-cols-3 gap-4">
|
<div className="grid grid-cols-3 gap-4">
|
||||||
<div className="text-center p-3 bg-green-100 rounded-lg">
|
<div className="text-center p-3 bg-green-100 rounded-lg">
|
||||||
<p className="text-xs text-green-700 mb-1">
|
<p className="text-xs text-green-700 mb-1">
|
||||||
Payable Amount
|
Payable Amount
|
||||||
</p>
|
</p>
|
||||||
<p className="text-green-900">
|
<p className="text-green-900">
|
||||||
₹
|
₹
|
||||||
{fnfCase.totalPayableAmount?.toLocaleString() ||
|
{fnfCase.totalPayableAmount?.toLocaleString() ||
|
||||||
"0"}
|
"0"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-red-100 rounded-lg">
|
<div className="text-center p-3 bg-red-100 rounded-lg">
|
||||||
<p className="text-xs text-red-700 mb-1">
|
<p className="text-xs text-red-700 mb-1">
|
||||||
Receivable amount
|
Receivable amount
|
||||||
</p>
|
</p>
|
||||||
<p className="text-red-900">
|
<p className="text-red-900">
|
||||||
₹
|
₹
|
||||||
{fnfCase.totalRecoveryAmount?.toLocaleString() ||
|
{fnfCase.totalRecoveryAmount?.toLocaleString() ||
|
||||||
"0"}
|
"0"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center p-3 bg-amber-100 rounded-lg">
|
<div className="text-center p-3 bg-amber-100 rounded-lg">
|
||||||
<p className="text-xs text-blue-700 mb-1">
|
<p className="text-xs text-blue-700 mb-1">
|
||||||
Net Amount
|
Net Amount
|
||||||
</p>
|
</p>
|
||||||
<p
|
<p
|
||||||
className={
|
className={
|
||||||
(fnfCase.totalRecoveryAmount || 0) >
|
(fnfCase.totalRecoveryAmount || 0) >
|
||||||
(fnfCase.totalPayableAmount || 0)
|
(fnfCase.totalPayableAmount || 0)
|
||||||
? "text-red-900"
|
? "text-red-900"
|
||||||
: "text-green-900"
|
: "text-green-900"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
₹
|
₹
|
||||||
{Math.abs(
|
{Math.abs(
|
||||||
(fnfCase.totalRecoveryAmount || 0) -
|
(fnfCase.totalRecoveryAmount || 0) -
|
||||||
(fnfCase.totalPayableAmount || 0),
|
(fnfCase.totalPayableAmount || 0),
|
||||||
).toLocaleString()}
|
).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</CardContent>
|
||||||
</CardContent>
|
</Card>
|
||||||
</Card>
|
)}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -1016,13 +1025,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<div className="flex shrink-0 flex-col items-center">
|
<div className="flex shrink-0 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
|
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-100 border-green-600"
|
? "bg-green-100 border-green-600"
|
||||||
: fnfCase.status === "Finance Approval"
|
: fnfCase.status === "Finance Approval"
|
||||||
? "bg-amber-100 border-amber-600"
|
? "bg-amber-100 border-amber-600"
|
||||||
: "bg-slate-100 border-slate-300"
|
: "bg-slate-100 border-slate-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{fnfCase.status === "Completed" ? (
|
{fnfCase.status === "Completed" ? (
|
||||||
<Check className="w-6 h-6 text-green-600" />
|
<Check className="w-6 h-6 text-green-600" />
|
||||||
@ -1033,11 +1041,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-0.5 h-full mt-2 ${
|
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-300"
|
? "bg-green-300"
|
||||||
: "bg-slate-200"
|
: "bg-slate-200"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -1095,11 +1102,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<div className="flex shrink-0 flex-col items-center">
|
<div className="flex shrink-0 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
|
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-100 border-green-600"
|
? "bg-green-100 border-green-600"
|
||||||
: "bg-slate-100 border-slate-300"
|
: "bg-slate-100 border-slate-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{fnfCase.status === "Completed" ? (
|
{fnfCase.status === "Completed" ? (
|
||||||
<Check className="w-6 h-6 text-green-600" />
|
<Check className="w-6 h-6 text-green-600" />
|
||||||
@ -1108,11 +1114,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`w-0.5 h-full mt-2 ${
|
className={`w-0.5 h-full mt-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-300"
|
? "bg-green-300"
|
||||||
: "bg-slate-200"
|
: "bg-slate-200"
|
||||||
}`}
|
}`}
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 pb-8">
|
<div className="flex-1 pb-8">
|
||||||
@ -1146,11 +1151,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex gap-4 items-start">
|
<div className="flex gap-4 items-start">
|
||||||
<div className="flex shrink-0 flex-col items-center">
|
<div className="flex shrink-0 flex-col items-center">
|
||||||
<div
|
<div
|
||||||
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${
|
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
|
||||||
fnfCase.status === "Completed"
|
|
||||||
? "bg-green-100 border-green-600"
|
? "bg-green-100 border-green-600"
|
||||||
: "bg-slate-100 border-slate-300"
|
: "bg-slate-100 border-slate-300"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{fnfCase.status === "Completed" ? (
|
{fnfCase.status === "Completed" ? (
|
||||||
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
<CheckCircle2 className="w-6 h-6 text-green-600" />
|
||||||
@ -1407,11 +1411,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
{dept.amount ? (
|
{dept.amount ? (
|
||||||
<span
|
<span
|
||||||
className={`font-semibold tabular-nums ${
|
className={`font-semibold tabular-nums ${dept.duesFlow === "recovery"
|
||||||
dept.duesFlow === "recovery"
|
|
||||||
? "text-red-700"
|
? "text-red-700"
|
||||||
: "text-emerald-700"
|
: "text-emerald-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
₹{dept.amount.toLocaleString()}
|
₹{dept.amount.toLocaleString()}
|
||||||
</span>
|
</span>
|
||||||
@ -1425,7 +1428,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
{canAnyDepartmentRespond && (
|
{canAnyDepartmentRespond && (
|
||||||
<TableCell>
|
<TableCell>
|
||||||
{canRespondToDepartment(dept.departmentName) ? (
|
{canRespondToDepartment(dept) ? (
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
@ -1537,11 +1540,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
|
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
|
||||||
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
|
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
|
||||||
<p
|
<p
|
||||||
className={`text-3xl font-extrabold ${
|
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
|
||||||
(fnfCase.netAmount || 0) < 0
|
|
||||||
? "text-red-600"
|
? "text-red-600"
|
||||||
: "text-green-600"
|
: "text-green-600"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
|
₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
@ -1644,11 +1646,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
const path = doc.url;
|
const path = doc.url;
|
||||||
const fullPath =
|
const fullPath =
|
||||||
path.startsWith("/uploads/") &&
|
path.startsWith("/uploads/") &&
|
||||||
!path.startsWith("/uploads/documents/")
|
!path.startsWith("/uploads/documents/")
|
||||||
? path.replace(
|
? path.replace(
|
||||||
"/uploads/",
|
"/uploads/",
|
||||||
"/uploads/documents/",
|
"/uploads/documents/",
|
||||||
)
|
)
|
||||||
: path;
|
: path;
|
||||||
setPreviewDocument({
|
setPreviewDocument({
|
||||||
fileName: doc.name,
|
fileName: doc.name,
|
||||||
@ -1793,17 +1795,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<p className="font-semibold text-slate-900 flex items-center gap-2">
|
<p className="font-semibold text-slate-900 flex items-center gap-2">
|
||||||
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />}
|
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />}
|
||||||
{(log.description && !log.newData?.action) ? log.description : (
|
{(log.description && !log.newData?.action) ? log.description : (
|
||||||
<>
|
<>
|
||||||
{getFriendlyActionName(log.newData?.action || log.action)}
|
{getFriendlyActionName(log.newData?.action || log.action)}
|
||||||
{log.newData?.department && (
|
{log.newData?.department && (
|
||||||
<span className="text-amber-600 ml-1 font-bold">
|
<span className="text-amber-600 ml-1 font-bold">
|
||||||
- {log.newData.department}
|
- {log.newData.department}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
<span className="text-xs text-slate-500">
|
<span className="text-xs text-slate-500">
|
||||||
{formatDateTime(log.createdAt || log.timestamp)}
|
{formatDateTime(log.createdAt || log.timestamp)}
|
||||||
|
|||||||
@ -243,7 +243,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
{canSendToStakeholders && (
|
{/* {canSendToStakeholders && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -253,7 +253,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
Send to Stakeholders
|
Send to Stakeholders
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -284,18 +284,16 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<CardContent className="pt-6">
|
<CardContent className="pt-6">
|
||||||
<div className="flex items-start justify-between">
|
<div className="flex items-start justify-between">
|
||||||
<div className="flex items-start gap-4 flex-1">
|
<div className="flex items-start gap-4 flex-1">
|
||||||
<div className={`p-3 rounded-lg ${
|
<div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-blue-100' :
|
||||||
fnfCase.status === 'Initiated' ? 'bg-blue-100' :
|
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
|
||||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
|
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
|
||||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
|
'bg-green-100'
|
||||||
'bg-green-100'
|
}`}>
|
||||||
}`}>
|
<IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-blue-600' :
|
||||||
<IndianRupee className={`w-6 h-6 ${
|
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
|
||||||
fnfCase.status === 'Initiated' ? 'text-blue-600' :
|
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
|
||||||
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
|
'text-green-600'
|
||||||
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
|
}`} />
|
||||||
'text-green-600'
|
|
||||||
}`} />
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-3 mb-2">
|
<div className="flex items-center gap-3 mb-2">
|
||||||
@ -328,7 +326,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2 ml-4">
|
<div className="flex items-center gap-2 ml-4">
|
||||||
{canSendToStakeholders && fnfCase.status === 'Initiated' && (
|
{/* {canSendToStakeholders && fnfCase.status === 'Initiated' && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@ -338,7 +336,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
|
|||||||
<Send className="w-4 h-4 mr-2" />
|
<Send className="w-4 h-4 mr-2" />
|
||||||
Send to Stakeholders
|
Send to Stakeholders
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)} */}
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
@ -334,6 +334,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
|||||||
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
|
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
|
||||||
else if (!selectedStage || selectedStage === 'General') {
|
else if (!selectedStage || selectedStage === 'General') {
|
||||||
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
|
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
|
||||||
|
} else if (selectedStage?.toLowerCase().includes('architecture')) {
|
||||||
|
filteredDocs = ['Architecture Blueprint', 'Site Plan', 'Proposed Site City Map', 'Site Readiness Report', 'Architecture Completion Certificate', 'Other'];
|
||||||
|
} else if (selectedStage?.toLowerCase().includes('fdd')) {
|
||||||
|
filteredDocs = ['FDD Final Audit Report', 'Bank Statement', 'Income Tax Returns (ITR)', 'CIBIL Report', 'Other'];
|
||||||
} else filteredDocs = baseDocs;
|
} else filteredDocs = baseDocs;
|
||||||
if (selectedStage?.startsWith('EOR: ')) {
|
if (selectedStage?.startsWith('EOR: ')) {
|
||||||
const eorItem = selectedStage.replace('EOR: ', '');
|
const eorItem = selectedStage.replace('EOR: ', '');
|
||||||
@ -499,241 +503,6 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<Dialog open={showDocumentsModal} onOpenChange={(open) => { setShowDocumentsModal(open); if (!open) setShowUploadForm(false); }}>
|
|
||||||
<DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6">
|
|
||||||
<DialogHeader className="pb-4">
|
|
||||||
<DialogTitle className="text-xl font-bold flex items-center gap-2"><FileText className="w-5 h-5 text-amber-600" />Documents - {selectedStage || 'General'}</DialogTitle>
|
|
||||||
<DialogDescription className="text-slate-500">View and manage documents uploaded for this stage.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{!showUploadForm ? (
|
|
||||||
<div className="flex-1 flex flex-col min-h-0 space-y-4">
|
|
||||||
{getDocumentsForStage(selectedStage || '').length > 0 ? (
|
|
||||||
<div className="flex-1 overflow-auto border rounded-lg border-slate-200">
|
|
||||||
<Table className="w-full table-auto">
|
|
||||||
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
|
|
||||||
<TableRow className="hover:bg-transparent border-b">
|
|
||||||
<TableHead className="w-[45%] min-w-[150px] font-semibold text-slate-900 py-3">Document Name</TableHead>
|
|
||||||
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Type</TableHead>
|
|
||||||
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Upload Date</TableHead>
|
|
||||||
<TableHead className="w-[15%] min-w-[140px] font-semibold text-slate-900 py-3">Uploaded By</TableHead>
|
|
||||||
<TableHead className="text-right w-[10%] min-w-[80px] font-semibold text-slate-900 py-3">Actions</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{getDocumentsForStage(selectedStage || '').map((doc: any) => (
|
|
||||||
<TableRow key={doc.id} className="hover:bg-slate-50/50 transition-colors">
|
|
||||||
<TableCell className="py-3"><div className="flex items-center gap-2 min-w-0"><FileText className="w-4 h-4 text-slate-400 shrink-0" /><span className="truncate font-medium text-slate-700" title={doc.fileName}>{doc.fileName}</span></div></TableCell>
|
|
||||||
<TableCell className="py-3"><Badge variant="outline" className="capitalize whitespace-nowrap font-normal border-slate-200 bg-white">{doc.documentType?.toLowerCase() || 'Other'}</Badge></TableCell>
|
|
||||||
<TableCell className="py-3 whitespace-nowrap text-slate-600">{formatDateTime(doc.createdAt)}</TableCell>
|
|
||||||
<TableCell className="py-3 text-slate-600">{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}</TableCell>
|
|
||||||
<TableCell className="text-right py-3">
|
|
||||||
<div className="flex gap-1 justify-end">
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}><Eye className="w-4 h-4" /></Button>
|
|
||||||
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full" onClick={() => { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }}><Download className="w-4 h-4" /></Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
|
|
||||||
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)}><Upload className="w-5 h-5 mr-3" />Upload Document</Button>
|
|
||||||
<Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)}>Close</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-6 py-4">
|
|
||||||
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-slate-700 font-semibold px-1">Stage context</Label>
|
|
||||||
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}>
|
|
||||||
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select stage" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="null">General / No Stage</SelectItem>
|
|
||||||
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-slate-700 font-semibold px-1">Document Type</Label>
|
|
||||||
<Select value={uploadDocType} onValueChange={setUploadDocType}>
|
|
||||||
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select type" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{(() => {
|
|
||||||
const baseDocs = ['Other'];
|
|
||||||
const stageConfigs = documentConfigs.filter((c: any) => {
|
|
||||||
const cfgStage = c.stageCode?.trim();
|
|
||||||
const selStage = (selectedStage || 'General').trim();
|
|
||||||
if (cfgStage === selStage) return true;
|
|
||||||
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true;
|
|
||||||
if (!selectedStage && cfgStage === 'General') return true;
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
let filteredDocs: string[] = [];
|
|
||||||
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
|
|
||||||
else if (!selectedStage || selectedStage === 'General') {
|
|
||||||
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
|
|
||||||
} else filteredDocs = baseDocs;
|
|
||||||
if (selectedStage?.startsWith('EOR: ')) {
|
|
||||||
const eorItem = selectedStage.replace('EOR: ', '');
|
|
||||||
if (!filteredDocs.includes(eorItem)) filteredDocs = [eorItem, ...filteredDocs];
|
|
||||||
}
|
|
||||||
return Array.from(new Set(filteredDocs)).map((doc, idx) => <SelectItem key={`${doc}-${idx}`} value={doc}>{doc}</SelectItem>);
|
|
||||||
})()}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-slate-700 font-semibold px-1">Select File</Label>
|
|
||||||
<Input type="file" className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer" onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
|
||||||
<Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading}>Cancel</Button>
|
|
||||||
<Button className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading}>
|
|
||||||
{isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
<DocumentPreviewModal isOpen={showPreviewModal} onClose={() => setShowPreviewModal(false)} document={previewDoc} />
|
|
||||||
|
|
||||||
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
|
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
|
|
||||||
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /><div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]"><ShieldCheck className="w-10 h-10 text-amber-500" /></div></div>
|
|
||||||
<div className="p-8 space-y-6 bg-white">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
|
|
||||||
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
|
|
||||||
<Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)}>{rec}</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
|
|
||||||
<Textarea placeholder="Summarize key financial findings or discrepancies..." className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm" value={fddAuditFindings} onChange={(e) => setFddAuditFindings(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100"><Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /><p className="text-[11px] text-amber-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
|
||||||
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd}>Cancel</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500"
|
|
||||||
disabled={isFinalizingFdd || !fddAuditFindings}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setIsFinalizingFdd(true);
|
|
||||||
await onboardingService.submitStageDecision({
|
|
||||||
applicationId: application!.id,
|
|
||||||
stageCode: 'FDD_VERIFICATION',
|
|
||||||
decision: 'Approved',
|
|
||||||
remarks: (currentUser?.role === 'FDD' || currentUser?.roleCode === 'FDD')
|
|
||||||
? `Findings: ${fddAuditFindings}`
|
|
||||||
: `[RECOMMENDATION: ${fddAuditRecommendation}] \nFindings: ${fddAuditFindings}`,
|
|
||||||
nextStatus: 'LOI In Progress',
|
|
||||||
nextProgress: 65
|
|
||||||
});
|
|
||||||
toast.success('FDD Audit finalized and submitted.');
|
|
||||||
setShowFddFinalizeModal(false);
|
|
||||||
fetchApplication();
|
|
||||||
} catch {
|
|
||||||
toast.error('Submission failed');
|
|
||||||
} finally {
|
|
||||||
setIsFinalizingFdd(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFinalizingFdd ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={showFddFlagModal} onOpenChange={setShowFddFlagModal}>
|
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
|
|
||||||
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" /><div className="w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center relative z-10 shadow-[0_0_40px_rgba(220,38,38,0.2)]"><ShieldAlert className="w-10 h-10 text-red-500" /></div></div>
|
|
||||||
<div className="p-8 space-y-6 bg-white text-center">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="text-2xl font-black text-slate-900 tracking-tight">Flag Non-Responsive</DialogTitle>
|
|
||||||
<DialogDescription className="text-slate-500 pt-2 leading-relaxed text-sm font-medium">Are you sure you want to flag this applicant? This will notify the DD Admin that the audit cannot proceed due to applicant's non-cooperation.</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100"><AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" /><p className="text-[11px] text-red-800 text-left font-medium">"Applicant is unresponsive to multiple queries and financial document requests."</p></div>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
|
||||||
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFlagModal(false)} disabled={isFddFlagging}>Go Back</Button>
|
|
||||||
<Button
|
|
||||||
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-red-600"
|
|
||||||
disabled={isFddFlagging}
|
|
||||||
onClick={async () => {
|
|
||||||
try {
|
|
||||||
setIsFddFlagging(true);
|
|
||||||
await onboardingService.submitStageDecision({
|
|
||||||
applicationId: application!.id,
|
|
||||||
stageCode: 'FDD_VERIFICATION',
|
|
||||||
decision: 'Rejected',
|
|
||||||
remarks: 'Applicant is non-responsive to FDD queries.'
|
|
||||||
});
|
|
||||||
toast.error('Applicant flagged as non-responsive.');
|
|
||||||
setShowFddFlagModal(false);
|
|
||||||
fetchApplication();
|
|
||||||
} catch {
|
|
||||||
toast.error('Action failed');
|
|
||||||
} finally {
|
|
||||||
setIsFddFlagging(false);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFddFlagging ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Flag'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
|
|
||||||
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl">
|
|
||||||
<div className="bg-amber-600 p-8 text-white">
|
|
||||||
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div>
|
|
||||||
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
|
|
||||||
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p>
|
|
||||||
</div>
|
|
||||||
<div className="p-8 space-y-6 bg-white">
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution</Label>
|
|
||||||
<Select value={tempFirmType} onValueChange={setTempFirmType}>
|
|
||||||
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500"><SelectValue placeholder="Select Firm Type" /></SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="Proprietorship">Proprietorship</SelectItem>
|
|
||||||
<SelectItem value="Partnership">Partnership</SelectItem>
|
|
||||||
<SelectItem value="Limited Liability partnership">LLP (Limited Liability partnership)</SelectItem>
|
|
||||||
<SelectItem value="Private Limited Company">Private Limited Company</SelectItem>
|
|
||||||
<SelectItem value="Public Limited Company">Public Limited Company</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 pt-2">
|
|
||||||
<Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType}>Cancel</Button>
|
|
||||||
<Button className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType}>{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,6 +146,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
|||||||
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
|
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
reqParams.isExternal = false;
|
||||||
const response = await onboardingService.getUsers(reqParams);
|
const response = await onboardingService.getUsers(reqParams);
|
||||||
if (Array.isArray(response)) setUsers(response);
|
if (Array.isArray(response)) setUsers(response);
|
||||||
else if (response && Array.isArray(response.data)) setUsers(response.data);
|
else if (response && Array.isArray(response.data)) setUsers(response.data);
|
||||||
|
|||||||
@ -455,17 +455,18 @@ export function FDDApplicationDetails() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Column: Applicant Meta & Guidelines */}
|
{/* Right Column: Applicant Meta & Guidelines */}
|
||||||
<div className="space-y-6">
|
{/* Right Column: Applicant Meta & Guidelines */}
|
||||||
|
<div className="space-y-4">
|
||||||
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-profile-card">
|
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-profile-card">
|
||||||
<CardHeader className="border-b border-slate-100 px-6 py-4">
|
<CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
|
||||||
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Applicant Profile</CardTitle>
|
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Applicant Profile</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="p-6 space-y-4">
|
<CardContent className="p-5 space-y-4">
|
||||||
<div className="space-y-1 pb-4 border-b border-slate-50" data-testid="onboarding-fdd-details-target-loc">
|
<div className="space-y-1 pb-3 border-b border-slate-50" data-testid="onboarding-fdd-details-target-loc">
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Target Location</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Target Location</p>
|
||||||
<p className="text-sm font-extrabold text-slate-900">{application.city}, {application.state}</p>
|
<p className="text-sm font-extrabold text-slate-900">{application.city}, {application.state}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4 text-xs" data-testid="onboarding-fdd-details-profile-meta">
|
<div className="grid grid-cols-2 gap-3 text-xs" data-testid="onboarding-fdd-details-profile-meta">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Education</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Education</p>
|
||||||
<p className="font-bold text-slate-800">{application.education || 'N/A'}</p>
|
<p className="font-bold text-slate-800">{application.education || 'N/A'}</p>
|
||||||
@ -484,7 +485,7 @@ export function FDDApplicationDetails() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-1 pt-4 border-t border-slate-50 text-xs" data-testid="onboarding-fdd-details-communication">
|
<div className="space-y-1 pt-3 border-t border-slate-50 text-xs" data-testid="onboarding-fdd-details-communication">
|
||||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Communication</p>
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Communication</p>
|
||||||
<p className="font-bold text-slate-800">{application.email}</p>
|
<p className="font-bold text-slate-800">{application.email}</p>
|
||||||
<p className="text-slate-500 font-medium">{application.phone}</p>
|
<p className="text-slate-500 font-medium">{application.phone}</p>
|
||||||
@ -492,6 +493,63 @@ export function FDDApplicationDetails() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* Statutory Details Card */}
|
||||||
|
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-statutory-card">
|
||||||
|
<CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
|
||||||
|
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Statutory Details</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-5 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-2.5 text-xs">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Constitution Type</p>
|
||||||
|
<p className="font-bold text-slate-800">{application.constitutionType || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">PAN Number</p>
|
||||||
|
<p className="font-bold text-slate-800 uppercase tracking-tight">{application.panNumber || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">GST Number</p>
|
||||||
|
<p className="font-bold text-slate-800 uppercase tracking-tight">{application.gstNumber || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Registered Address</p>
|
||||||
|
<p className="font-medium text-slate-700 leading-relaxed text-[11px]">{application.registeredAddress || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Bank Details Card */}
|
||||||
|
<Card className="border border-slate-200 shadow-sm bg-white" data-testid="onboarding-fdd-details-bank-card">
|
||||||
|
<CardHeader className="border-b border-slate-100 px-6 pt-4 pb-2.5">
|
||||||
|
<CardTitle className="text-xs font-bold uppercase tracking-wider text-slate-500">Bank Details</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-5 space-y-3">
|
||||||
|
<div className="grid grid-cols-1 gap-2.5 text-xs">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Account Holder</p>
|
||||||
|
<p className="font-bold text-slate-800">{application.accountHolderName || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Bank Name</p>
|
||||||
|
<p className="font-bold text-slate-800">{application.bankName || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">Account Number</p>
|
||||||
|
<p className="font-bold text-slate-800 tabular-nums">{application.accountNumber || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider">IFSC Code & Branch</p>
|
||||||
|
<p className="font-bold text-slate-800 uppercase tracking-tight">
|
||||||
|
{application.ifscCode || 'N/A'}
|
||||||
|
{application.branchName && <span className="text-slate-400 font-medium ml-2">— {application.branchName}</span>}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<div className="p-6 bg-slate-900 rounded-lg text-white font-medium" data-testid="onboarding-fdd-details-instructions">
|
<div className="p-6 bg-slate-900 rounded-lg text-white font-medium" data-testid="onboarding-fdd-details-instructions">
|
||||||
<h4 className="text-sm font-bold mb-2">Instructions</h4>
|
<h4 className="text-sm font-bold mb-2">Instructions</h4>
|
||||||
<ul className="text-xs text-slate-300 space-y-2 list-disc pl-4">
|
<ul className="text-xs text-slate-300 space-y-2 list-disc pl-4">
|
||||||
|
|||||||
@ -86,7 +86,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await adminService.getAllUsers();
|
const response = await adminService.getAllUsers({ isExternal: false });
|
||||||
// Defensive check for array data
|
// Defensive check for array data
|
||||||
const users = (response && response.success && Array.isArray(response.data))
|
const users = (response && response.success && Array.isArray(response.data))
|
||||||
? response.data
|
? response.data
|
||||||
@ -261,12 +261,12 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
|
'Questionnaire Pending': 'bg-yellow-100 text-yellow-800',
|
||||||
'Questionnaire Completed': 'bg-cyan-100 text-cyan-800',
|
'Questionnaire Completed': 'bg-cyan-100 text-cyan-800',
|
||||||
'Shortlisted': 'bg-purple-100 text-purple-800',
|
'Shortlisted': 'bg-purple-100 text-purple-800',
|
||||||
'Level 1 Pending': 'bg-orange-100 text-orange-800',
|
'Level 1 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||||
'Level 1 Approved': 'bg-green-100 text-green-800',
|
'Level 1 Approved': 'bg-green-100 text-green-800',
|
||||||
'Level 2 Pending': 'bg-orange-100 text-orange-800',
|
'Level 2 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||||
'Level 2 Approved': 'bg-green-100 text-green-800',
|
'Level 2 Approved': 'bg-green-100 text-green-800',
|
||||||
'Level 2 Recommended': 'bg-teal-100 text-teal-800',
|
'Level 2 Recommended': 'bg-teal-100 text-teal-800',
|
||||||
'Level 3 Pending': 'bg-orange-100 text-orange-800',
|
'Level 3 Interview Pending': 'bg-orange-100 text-orange-800',
|
||||||
'FDD Verification': 'bg-indigo-100 text-indigo-800',
|
'FDD Verification': 'bg-indigo-100 text-indigo-800',
|
||||||
'Payment Pending': 'bg-amber-100 text-amber-800',
|
'Payment Pending': 'bg-amber-100 text-amber-800',
|
||||||
'LOI Issued': 'bg-sky-100 text-sky-800',
|
'LOI Issued': 'bg-sky-100 text-sky-800',
|
||||||
@ -290,7 +290,24 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
|||||||
'Inauguration': 'bg-green-100 text-green-800',
|
'Inauguration': 'bg-green-100 text-green-800',
|
||||||
'Approved': 'bg-green-100 text-green-800',
|
'Approved': 'bg-green-100 text-green-800',
|
||||||
'Rejected': 'bg-red-100 text-red-800',
|
'Rejected': 'bg-red-100 text-red-800',
|
||||||
'Disqualified': 'bg-gray-100 text-gray-800'
|
'Disqualified': 'bg-gray-100 text-gray-800',
|
||||||
|
'In Review': 'bg-slate-100 text-slate-800',
|
||||||
|
'Level 3 Approved': 'bg-green-100 text-green-800',
|
||||||
|
'LOI In Progress': 'bg-sky-50 text-sky-700',
|
||||||
|
'LOI Approved': 'bg-green-100 text-green-800',
|
||||||
|
'Security Details In Progress': 'bg-blue-50 text-blue-700',
|
||||||
|
'Security Details Approved': 'bg-green-100 text-green-800',
|
||||||
|
'Security Details': 'bg-blue-100 text-blue-800',
|
||||||
|
'LOI Issued In Progress': 'bg-sky-50 text-sky-700',
|
||||||
|
'Statutory Work In Progress': 'bg-emerald-50 text-emerald-700',
|
||||||
|
'Statutory Work Completed': 'bg-green-100 text-green-800',
|
||||||
|
'Architecture Work In Progress': 'bg-blue-50 text-blue-700',
|
||||||
|
'Architecture Work Completed': 'bg-green-100 text-green-800',
|
||||||
|
'Dealer Code Generation In Progress': 'bg-purple-50 text-purple-700',
|
||||||
|
'Dealer Code Generated': 'bg-green-100 text-green-800',
|
||||||
|
'LOA Issued': 'bg-pink-100 text-pink-800',
|
||||||
|
'EOR Complete': 'bg-violet-100 text-violet-800',
|
||||||
|
'Onboarded': 'bg-green-200 text-green-900'
|
||||||
};
|
};
|
||||||
return colors[status] || 'bg-gray-100 text-gray-800';
|
return colors[status] || 'bg-gray-100 text-gray-800';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -37,6 +37,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
const [newState, setNewState] = useState('');
|
const [newState, setNewState] = useState('');
|
||||||
const [newAddress, setNewAddress] = useState('');
|
const [newAddress, setNewAddress] = useState('');
|
||||||
const [reason, setReason] = useState('');
|
const [reason, setReason] = useState('');
|
||||||
|
const [distance, setDistance] = useState('');
|
||||||
|
const [propertyType, setPropertyType] = useState('');
|
||||||
|
const [expectedDate, setExpectedDate] = useState('');
|
||||||
|
const [newLat, setNewLat] = useState('');
|
||||||
|
const [newLong, setNewLong] = useState('');
|
||||||
|
|
||||||
// State/District dropdown data
|
// State/District dropdown data
|
||||||
const [states, setStates] = useState<any[]>([]);
|
const [states, setStates] = useState<any[]>([]);
|
||||||
@ -139,6 +144,11 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!distance.trim() || !propertyType || !expectedDate) {
|
||||||
|
toast.error('Please fill all mandatory fields (Distance, Property Type, Date)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const payload = {
|
const payload = {
|
||||||
@ -152,8 +162,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
newState,
|
newState,
|
||||||
newDistrictId: selectedDistrictId || null,
|
newDistrictId: selectedDistrictId || null,
|
||||||
newStateId: selectedStateId || null,
|
newStateId: selectedStateId || null,
|
||||||
|
distance,
|
||||||
reason,
|
reason,
|
||||||
proposedDate: null
|
propertyType,
|
||||||
|
proposedDate: expectedDate,
|
||||||
|
proposedLatitude: newLat ? parseFloat(newLat) : null,
|
||||||
|
proposedLongitude: newLong ? parseFloat(newLong) : null,
|
||||||
|
currentLatitude: selectedOutlet.latitude || null,
|
||||||
|
currentLongitude: selectedOutlet.longitude || null
|
||||||
};
|
};
|
||||||
|
|
||||||
await dealerService.submitRelocationRequest(payload);
|
await dealerService.submitRelocationRequest(payload);
|
||||||
@ -167,6 +183,14 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
setNewState('');
|
setNewState('');
|
||||||
setNewAddress('');
|
setNewAddress('');
|
||||||
setReason('');
|
setReason('');
|
||||||
|
setDistance('');
|
||||||
|
setPropertyType('');
|
||||||
|
setExpectedDate('');
|
||||||
|
setNewLat('');
|
||||||
|
setNewLong('');
|
||||||
|
if (onViewDetails) {
|
||||||
|
fetchData();
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Submit relocation error:', error);
|
console.error('Submit relocation error:', error);
|
||||||
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
|
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
|
||||||
@ -316,6 +340,43 @@ export function DealerRelocationPage({ currentUser, onViewDetails }: DealerReloc
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="distance">Estimated Distance from Current Location (in km) *</Label>
|
||||||
|
<Input
|
||||||
|
id="distance"
|
||||||
|
placeholder="e.g. 5.5 km"
|
||||||
|
value={distance}
|
||||||
|
onChange={(e) => setDistance(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="propertyType">Property Type *</Label>
|
||||||
|
<Select value={propertyType} onValueChange={setPropertyType} required>
|
||||||
|
<SelectTrigger id="propertyType">
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Owned">Owned</SelectItem>
|
||||||
|
<SelectItem value="Leased">Leased</SelectItem>
|
||||||
|
<SelectItem value="Rented">Rented</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="expectedDate">Expected Relocation Date *</Label>
|
||||||
|
<Input
|
||||||
|
id="expectedDate"
|
||||||
|
type="date"
|
||||||
|
value={expectedDate}
|
||||||
|
onChange={(e) => setExpectedDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Reason */}
|
{/* Reason */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="reason">Reason for Relocation *</Label>
|
<Label htmlFor="reason">Reason for Relocation *</Label>
|
||||||
|
|||||||
@ -647,9 +647,26 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
|
|||||||
<p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p>
|
<p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
<div className="flex flex-wrap gap-2">
|
||||||
Type: {request.relocationType}
|
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
||||||
</Badge>
|
Type: {request.relocationType}
|
||||||
|
</Badge>
|
||||||
|
{request.distance && (
|
||||||
|
<Badge variant="outline" className="border-amber-200 bg-amber-50 text-amber-700">
|
||||||
|
Distance: {request.distance}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{request.propertyType && (
|
||||||
|
<Badge variant="outline" className="border-blue-200 bg-blue-50 text-blue-700">
|
||||||
|
Property: {request.propertyType}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{request.expectedRelocationDate && (
|
||||||
|
<Badge variant="outline" className="border-purple-200 bg-purple-50 text-purple-700">
|
||||||
|
Expected Date: {request.expectedRelocationDate}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -4,6 +4,12 @@ import { Badge } from '@/components/ui/badge';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Plus } from 'lucide-react';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { User } from '@/lib/mock-data';
|
import { User } from '@/lib/mock-data';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
@ -15,6 +21,9 @@ interface RelocationRequestPageProps {
|
|||||||
onViewDetails: (id: string) => void;
|
onViewDetails: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getApiErrorMessage = (error: any, fallback: string) =>
|
||||||
|
error?.response?.data?.message || error?.data?.message || error?.message || fallback;
|
||||||
|
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
|
if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
|
||||||
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
|
||||||
@ -27,6 +36,32 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
const [requests, setRequests] = useState<any[]>([]);
|
const [requests, setRequests] = useState<any[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
// Relocation Creation State (for Super Admin)
|
||||||
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
const [outlets, setOutlets] = useState<any[]>([]);
|
||||||
|
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
|
||||||
|
const [newCity, setNewCity] = useState('');
|
||||||
|
const [newState, setNewState] = useState('');
|
||||||
|
const [newAddress, setNewAddress] = useState('');
|
||||||
|
const [reason, setReason] = useState('');
|
||||||
|
const [distance, setDistance] = useState('');
|
||||||
|
const [propertyType, setPropertyType] = useState('');
|
||||||
|
const [expectedDate, setExpectedDate] = useState('');
|
||||||
|
const [newLat, setNewLat] = useState('');
|
||||||
|
const [newLong, setNewLong] = useState('');
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
|
||||||
|
// Master Data
|
||||||
|
const [states, setStates] = useState<any[]>([]);
|
||||||
|
const [districts, setDistricts] = useState<any[]>([]);
|
||||||
|
const [selectedStateId, setSelectedStateId] = useState('');
|
||||||
|
const [selectedDistrictId, setSelectedDistrictId] = useState('');
|
||||||
|
const [masterDataLoading, setMasterDataLoading] = useState(false);
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const isSuperAdmin = currentUser?.role === 'Super Admin' || currentUser?.roleCode === 'Super Admin';
|
||||||
|
|
||||||
|
|
||||||
const isCompletedRequest = (request: any) =>
|
const isCompletedRequest = (request: any) =>
|
||||||
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
|
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
|
||||||
|
|
||||||
@ -38,8 +73,121 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchRequests();
|
fetchRequests();
|
||||||
|
if (isSuperAdmin) {
|
||||||
|
fetchOutlets();
|
||||||
|
fetchMasterData();
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const fetchOutlets = async () => {
|
||||||
|
try {
|
||||||
|
const response = await API.getOutlets() as any;
|
||||||
|
if (response.data.success) {
|
||||||
|
setOutlets(response.data.outlets || response.data.data?.outlets || response.data.data || []);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch outlets error:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchMasterData = async () => {
|
||||||
|
try {
|
||||||
|
setMasterDataLoading(true);
|
||||||
|
const [statesRes, districtsRes] = await Promise.all([
|
||||||
|
API.getStates().catch(() => ({ success: false })) as Promise<any>,
|
||||||
|
API.getDistricts({ limit: 'all' }).catch(() => ({ success: false })) as Promise<any>
|
||||||
|
]);
|
||||||
|
|
||||||
|
const statesData = statesRes?.data?.success ? (statesRes.data.data?.states || statesRes.data.data || []) : [];
|
||||||
|
const districtsData = districtsRes?.data?.success ? (districtsRes.data.data?.districts || districtsRes.data.data || []) : [];
|
||||||
|
|
||||||
|
setStates(statesData);
|
||||||
|
setDistricts(districtsData);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Fetch master data error:', error);
|
||||||
|
} finally {
|
||||||
|
setMasterDataLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleStateChange = (stateId: string) => {
|
||||||
|
setSelectedStateId(stateId);
|
||||||
|
setSelectedDistrictId('');
|
||||||
|
const selectedState = states.find(s => s.id === stateId);
|
||||||
|
if (selectedState) {
|
||||||
|
setNewState(selectedState.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDistrictChange = (districtId: string) => {
|
||||||
|
setSelectedDistrictId(districtId);
|
||||||
|
const selectedDistrict = districts.find(d => d.id === districtId);
|
||||||
|
if (selectedDistrict) {
|
||||||
|
setNewCity(selectedDistrict.name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmitRequest = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedOutlet || !newCity || !newState || !newAddress || !reason || !distance || !propertyType || !expectedDate) {
|
||||||
|
toast.error('Please fill all mandatory fields');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
const payload = {
|
||||||
|
outletId: selectedOutlet.id,
|
||||||
|
relocationType: 'Intercity',
|
||||||
|
currentAddress: selectedOutlet.address || '',
|
||||||
|
currentCity: selectedOutlet.city || '',
|
||||||
|
currentState: selectedOutlet.state || '',
|
||||||
|
newAddress,
|
||||||
|
newCity,
|
||||||
|
newState,
|
||||||
|
newDistrictId: selectedDistrictId || null,
|
||||||
|
newStateId: selectedStateId || null,
|
||||||
|
reason,
|
||||||
|
distance,
|
||||||
|
propertyType,
|
||||||
|
proposedDate: expectedDate,
|
||||||
|
newLatitude: newLat ? parseFloat(newLat) : null,
|
||||||
|
newLongitude: newLong ? parseFloat(newLong) : null,
|
||||||
|
currentLatitude: selectedOutlet.latitude || null,
|
||||||
|
currentLongitude: selectedOutlet.longitude || null
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await API.createRelocationRequest(payload) as any;
|
||||||
|
if (response.data.success) {
|
||||||
|
toast.success(`Relocation request submitted successfully for ${selectedOutlet.name}`);
|
||||||
|
setIsDialogOpen(false);
|
||||||
|
fetchRequests();
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
setSelectedOutlet(null);
|
||||||
|
setNewCity('');
|
||||||
|
setNewState('');
|
||||||
|
setNewAddress('');
|
||||||
|
setReason('');
|
||||||
|
setDistance('');
|
||||||
|
setPropertyType('');
|
||||||
|
setExpectedDate('');
|
||||||
|
setNewLat('');
|
||||||
|
setNewLong('');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Submit relocation error:', error);
|
||||||
|
toast.error(getApiErrorMessage(error, 'Failed to submit relocation request'));
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredDistricts = selectedStateId
|
||||||
|
? districts.filter(d => d.stateId === selectedStateId)
|
||||||
|
: districts;
|
||||||
|
|
||||||
|
|
||||||
const fetchRequests = async () => {
|
const fetchRequests = async () => {
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@ -98,6 +246,187 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
• Note: Relocation requests are initiated by the dealer.
|
• Note: Relocation requests are initiated by the dealer.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{isSuperAdmin && (
|
||||||
|
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button className="bg-amber-600 hover:bg-amber-700 text-white">
|
||||||
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
New Relocation Request
|
||||||
|
</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Submit Relocation Request</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Create a new relocation request on behalf of a dealer
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmitRequest} className="space-y-4">
|
||||||
|
{/* Select Outlet */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="outlet">Select Outlet to Relocate *</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedOutlet?.id}
|
||||||
|
onValueChange={(val) => setSelectedOutlet(outlets.find(o => o.id === val))}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select an outlet" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{outlets.map((outlet) => (
|
||||||
|
<SelectItem key={outlet.id} value={outlet.id}>
|
||||||
|
{outlet.name} ({outlet.code})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedOutlet && (
|
||||||
|
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2 text-sm">
|
||||||
|
<h3 className="text-slate-900 font-medium">Current Location</h3>
|
||||||
|
<p className="text-slate-600">{selectedOutlet.address}, {selectedOutlet.city}, {selectedOutlet.state} - {selectedOutlet.pincode}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* New Location Details - State/District Dropdowns */}
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newState">Proposed State *</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedStateId}
|
||||||
|
onValueChange={handleStateChange}
|
||||||
|
required
|
||||||
|
disabled={masterDataLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="newState">
|
||||||
|
<SelectValue placeholder={masterDataLoading ? "Loading..." : "Select state"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{states.map((state) => (
|
||||||
|
<SelectItem key={state.id} value={state.id}>
|
||||||
|
{state.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newCity">Proposed City/District *</Label>
|
||||||
|
<Select
|
||||||
|
value={selectedDistrictId}
|
||||||
|
onValueChange={handleDistrictChange}
|
||||||
|
required
|
||||||
|
disabled={!selectedStateId || masterDataLoading}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="newCity">
|
||||||
|
<SelectValue placeholder={!selectedStateId ? "Select state first" : masterDataLoading ? "Loading..." : "Select district"} />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{filteredDistricts.map((district) => (
|
||||||
|
<SelectItem key={district.id} value={district.id}>
|
||||||
|
{district.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="newAddress">Proposed Full Address *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="newAddress"
|
||||||
|
placeholder="Enter detailed address of the proposed new location..."
|
||||||
|
value={newAddress}
|
||||||
|
onChange={(e) => setNewAddress(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="distance">Estimated Distance from Current Location (in km) *</Label>
|
||||||
|
<Input
|
||||||
|
id="distance"
|
||||||
|
type="text"
|
||||||
|
placeholder="e.g. 5.5 km"
|
||||||
|
value={distance}
|
||||||
|
onChange={(e) => setDistance(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="propertyType">Property Type *</Label>
|
||||||
|
<Select value={propertyType} onValueChange={setPropertyType} required>
|
||||||
|
<SelectTrigger id="propertyType">
|
||||||
|
<SelectValue placeholder="Select type" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Owned">Owned</SelectItem>
|
||||||
|
<SelectItem value="Leased">Leased</SelectItem>
|
||||||
|
<SelectItem value="Rented">Rented</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="expectedDate">Expected Relocation Date *</Label>
|
||||||
|
<Input
|
||||||
|
id="expectedDate"
|
||||||
|
type="date"
|
||||||
|
value={expectedDate}
|
||||||
|
onChange={(e) => setExpectedDate(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Reason */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="reason">Reason for Relocation *</Label>
|
||||||
|
<Textarea
|
||||||
|
id="reason"
|
||||||
|
placeholder="Why is this relocation requested?"
|
||||||
|
value={reason}
|
||||||
|
onChange={(e) => setReason(e.target.value)}
|
||||||
|
rows={4}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => setIsDialogOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
className="bg-amber-600 hover:bg-amber-700 text-white"
|
||||||
|
disabled={submitting}
|
||||||
|
>
|
||||||
|
{submitting ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||||
|
Submitting...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
'Submit Relocation Request'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Statistics Cards */}
|
{/* Statistics Cards */}
|
||||||
@ -199,7 +528,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
|
||||||
{request.currentStage}
|
{request.currentStage}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -274,7 +603,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
|
||||||
{request.currentStage}
|
{request.currentStage}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -346,7 +675,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant="outline" className="border-slate-300 text-slate-700">
|
<Badge className={`border ${getStatusColor(request.currentStage)}`}>
|
||||||
{request.currentStage}
|
{request.currentStage}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@ -11,9 +11,9 @@ export const adminService = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async getAllUsers() {
|
async getAllUsers(params?: any) {
|
||||||
try {
|
try {
|
||||||
const response = await API.getUsers() as any;
|
const response = await API.getUsers(params) as any;
|
||||||
return response.data;
|
return response.data;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error fetching users:', error);
|
console.error('Error fetching users:', error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user