trying differnt appraoch to align location and user herarchy
This commit is contained in:
parent
af479db2f0
commit
73cf4fdfac
1816
package-lock.json
generated
1816
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -31,6 +31,7 @@ import { FinancePaymentDetailsPage } from './components/applications/FinancePaym
|
|||||||
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
|
||||||
import { MasterPage } from './components/applications/MasterPage';
|
import { MasterPage } from './components/applications/MasterPage';
|
||||||
import { UserManagementPage } from './components/admin/UserManagementPage';
|
import { UserManagementPage } from './components/admin/UserManagementPage';
|
||||||
|
import { ApprovalPoliciesPage } from './components/admin/ApprovalPoliciesPage';
|
||||||
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
|
||||||
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
|
||||||
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
|
||||||
@ -144,6 +145,7 @@ export default function App() {
|
|||||||
'/dealer-constitutional': 'Dealer Constitutional Change',
|
'/dealer-constitutional': 'Dealer Constitutional Change',
|
||||||
'/dealer-relocation': 'Dealer Relocation Requests',
|
'/dealer-relocation': 'Dealer Relocation Requests',
|
||||||
'/questionnaire-builder': 'Questionnaire Builder',
|
'/questionnaire-builder': 'Questionnaire Builder',
|
||||||
|
'/approval-policies': 'Approval Policies',
|
||||||
};
|
};
|
||||||
return titles[pathname] || 'Dashboard';
|
return titles[pathname] || 'Dashboard';
|
||||||
};
|
};
|
||||||
@ -226,6 +228,11 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Other Modules */}
|
{/* Other Modules */}
|
||||||
<Route path="/users" element={<UserManagementPage />} />
|
<Route path="/users" element={<UserManagementPage />} />
|
||||||
|
<Route path="/approval-policies" element={
|
||||||
|
(currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin')
|
||||||
|
? <ApprovalPoliciesPage />
|
||||||
|
: <Navigate to="/dashboard" />
|
||||||
|
} />
|
||||||
<Route path="/master" element={<MasterPage />} />
|
<Route path="/master" element={<MasterPage />} />
|
||||||
<Route path="/questions" element={<QuestionnaireList />} />
|
<Route path="/questions" element={<QuestionnaireList />} />
|
||||||
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />
|
||||||
|
|||||||
@ -13,15 +13,16 @@ export const API = {
|
|||||||
updateRole: (id: string, data: any) => client.put(`/admin/roles/${id}`, data),
|
updateRole: (id: string, data: any) => client.put(`/admin/roles/${id}`, data),
|
||||||
|
|
||||||
getZones: () => client.get('/master/zones'),
|
getZones: () => client.get('/master/zones'),
|
||||||
|
createZone: (data: any) => client.post('/master/zones', data),
|
||||||
updateZone: (id: string, data: any) => client.put(`/master/zones/${id}`, data),
|
updateZone: (id: string, data: any) => client.put(`/master/zones/${id}`, data),
|
||||||
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('/master/outlets'),
|
||||||
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
||||||
getStates: (zoneId?: string) => client.get('/master/states', { zoneId }),
|
getStates: (zoneId?: string) => client.get('/master/states', { params: { zoneId } }),
|
||||||
getDistricts: (stateId?: string) => client.get('/master/districts', { stateId }),
|
getDistricts: (stateId?: string) => client.get('/master/districts', { params: { stateId } }),
|
||||||
getAreas: (districtId?: string) => client.get('/master/areas', { districtId }),
|
getAreas: (districtId?: string) => client.get('/master/areas', { params: { districtId } }),
|
||||||
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
|
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
|
||||||
createArea: (data: any) => client.post('/master/areas', data),
|
createArea: (data: any) => client.post('/master/areas', data),
|
||||||
getAreaManagers: () => client.get('/master/area-managers'),
|
getAreaManagers: () => client.get('/master/area-managers'),
|
||||||
@ -61,6 +62,9 @@ export const API = {
|
|||||||
getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`),
|
getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`),
|
||||||
updateRecommendation: (data: any) => client.post('/assessment/recommendation', data),
|
updateRecommendation: (data: any) => client.post('/assessment/recommendation', data),
|
||||||
updateInterviewDecision: (data: any) => client.post('/assessment/decision', data),
|
updateInterviewDecision: (data: any) => client.post('/assessment/decision', data),
|
||||||
|
getInterviewApprovalStatus: (interviewId: string) => client.get(`/assessment/interviews/${interviewId}/approval-status`),
|
||||||
|
getApprovalPolicies: () => client.get('/assessment/approval-policies'),
|
||||||
|
upsertApprovalPolicy: (stageCode: string, data: any) => client.put(`/assessment/approval-policies/${stageCode}`, data),
|
||||||
|
|
||||||
// Collaboration & Participants
|
// Collaboration & Participants
|
||||||
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
|
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
|
||||||
|
|||||||
336
src/components/admin/ApprovalPoliciesPage.tsx
Normal file
336
src/components/admin/ApprovalPoliciesPage.tsx
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../ui/card';
|
||||||
|
import { Button } from '../ui/button';
|
||||||
|
import { Input } from '../ui/input';
|
||||||
|
import { Label } from '../ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
|
||||||
|
import { Badge } from '../ui/badge';
|
||||||
|
import { RefreshCw, Settings2, Edit2, Save, X } from 'lucide-react';
|
||||||
|
import { approvalPolicyService } from '../../services/approvalPolicy.service';
|
||||||
|
|
||||||
|
type ApprovalMode = 'ALL' | 'MIN_N' | 'ROLE_MANDATORY';
|
||||||
|
|
||||||
|
interface Policy {
|
||||||
|
stageCode: string;
|
||||||
|
minApprovals: number;
|
||||||
|
approvalMode: ApprovalMode;
|
||||||
|
requiredRoles: string[];
|
||||||
|
isActive: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApprovalPoliciesPage() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [policies, setPolicies] = useState<Policy[]>([]);
|
||||||
|
const [editingCode, setEditingCode] = useState<string | null>(null);
|
||||||
|
const [draft, setDraft] = useState<Policy | null>(null);
|
||||||
|
const [creating, setCreating] = useState(false);
|
||||||
|
const [newPolicy, setNewPolicy] = useState<Policy>({
|
||||||
|
stageCode: '',
|
||||||
|
minApprovals: 1,
|
||||||
|
approvalMode: 'MIN_N',
|
||||||
|
requiredRoles: [],
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortedPolicies = useMemo(
|
||||||
|
() => [...policies].sort((a, b) => a.stageCode.localeCompare(b.stageCode)),
|
||||||
|
[policies]
|
||||||
|
);
|
||||||
|
|
||||||
|
const fetchPolicies = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await approvalPolicyService.getPolicies();
|
||||||
|
if (res?.success) setPolicies(res.data || []);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchPolicies();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const startEdit = (policy: Policy) => {
|
||||||
|
setEditingCode(policy.stageCode);
|
||||||
|
setDraft({
|
||||||
|
stageCode: policy.stageCode,
|
||||||
|
minApprovals: policy.minApprovals || 1,
|
||||||
|
approvalMode: policy.approvalMode || 'MIN_N',
|
||||||
|
requiredRoles: Array.isArray(policy.requiredRoles) ? policy.requiredRoles : [],
|
||||||
|
isActive: policy.isActive !== false
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const cancelEdit = () => {
|
||||||
|
setEditingCode(null);
|
||||||
|
setDraft(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveEdit = async () => {
|
||||||
|
if (!draft) return;
|
||||||
|
|
||||||
|
if (draft.approvalMode === 'ROLE_MANDATORY' && draft.requiredRoles.length > 0 && draft.minApprovals > draft.requiredRoles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
minApprovals: Number(draft.minApprovals) || 1,
|
||||||
|
approvalMode: draft.approvalMode,
|
||||||
|
requiredRoles: draft.requiredRoles,
|
||||||
|
isActive: draft.isActive
|
||||||
|
};
|
||||||
|
const res = await approvalPolicyService.savePolicy(draft.stageCode, payload);
|
||||||
|
if (res?.success) {
|
||||||
|
await fetchPolicies();
|
||||||
|
cancelEdit();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetNewPolicy = () => {
|
||||||
|
setNewPolicy({
|
||||||
|
stageCode: '',
|
||||||
|
minApprovals: 1,
|
||||||
|
approvalMode: 'MIN_N',
|
||||||
|
requiredRoles: [],
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createPolicy = async () => {
|
||||||
|
const stageCode = newPolicy.stageCode.trim().toUpperCase().replace(/\s+/g, '_');
|
||||||
|
if (!stageCode) return;
|
||||||
|
|
||||||
|
if (newPolicy.approvalMode === 'ROLE_MANDATORY' && newPolicy.requiredRoles.length > 0 && newPolicy.minApprovals > newPolicy.requiredRoles.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
minApprovals: Number(newPolicy.minApprovals) || 1,
|
||||||
|
approvalMode: newPolicy.approvalMode,
|
||||||
|
requiredRoles: newPolicy.requiredRoles,
|
||||||
|
isActive: newPolicy.isActive
|
||||||
|
};
|
||||||
|
const res = await approvalPolicyService.savePolicy(stageCode, payload);
|
||||||
|
if (res?.success) {
|
||||||
|
await fetchPolicies();
|
||||||
|
resetNewPolicy();
|
||||||
|
setCreating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6 max-w-7xl mx-auto py-6 px-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
|
||||||
|
<Settings2 className="w-6 h-6 text-amber-600" />
|
||||||
|
Approval Policies
|
||||||
|
</h1>
|
||||||
|
<p className="text-slate-500">Configure stage-level approvers, mode, and minimum approvals.</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="outline" onClick={fetchPolicies} disabled={loading}>
|
||||||
|
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
|
||||||
|
Refresh
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card className="border-slate-200 overflow-hidden">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<CardTitle>Configured Stages</CardTitle>
|
||||||
|
{!creating ? (
|
||||||
|
<Button className="bg-amber-600 hover:bg-amber-700" onClick={() => setCreating(true)}>
|
||||||
|
Add New Policy
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => { setCreating(false); resetNewPolicy(); }}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button className="bg-amber-600 hover:bg-amber-700" onClick={createPolicy}>
|
||||||
|
Save New Policy
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{creating && (
|
||||||
|
<div className="border-b bg-amber-50/40 p-4 grid grid-cols-1 md:grid-cols-5 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label>Stage Code</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="e.g. ARCHITECTURE_APPROVAL"
|
||||||
|
value={newPolicy.stageCode}
|
||||||
|
onChange={(e) => setNewPolicy({ ...newPolicy, stageCode: e.target.value })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Approval Mode</Label>
|
||||||
|
<Select
|
||||||
|
value={newPolicy.approvalMode}
|
||||||
|
onValueChange={(val: ApprovalMode) => setNewPolicy({ ...newPolicy, approvalMode: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">ALL</SelectItem>
|
||||||
|
<SelectItem value="MIN_N">MIN_N</SelectItem>
|
||||||
|
<SelectItem value="ROLE_MANDATORY">ROLE_MANDATORY</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Min Approvals</Label>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={newPolicy.minApprovals}
|
||||||
|
onChange={(e) => setNewPolicy({ ...newPolicy, minApprovals: Number(e.target.value || 1) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Required Roles (comma separated)</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="DD Head, NBH"
|
||||||
|
value={newPolicy.requiredRoles.join(', ')}
|
||||||
|
onChange={(e) =>
|
||||||
|
setNewPolicy({
|
||||||
|
...newPolicy,
|
||||||
|
requiredRoles: e.target.value.split(',').map((r) => r.trim()).filter(Boolean)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Status</Label>
|
||||||
|
<Select
|
||||||
|
value={newPolicy.isActive ? 'active' : 'inactive'}
|
||||||
|
onValueChange={(val) => setNewPolicy({ ...newPolicy, isActive: val === 'active' })}
|
||||||
|
>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Table>
|
||||||
|
<TableHeader className="bg-slate-50">
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Stage Code</TableHead>
|
||||||
|
<TableHead>Approval Mode</TableHead>
|
||||||
|
<TableHead>Min Approvals</TableHead>
|
||||||
|
<TableHead>Required Roles</TableHead>
|
||||||
|
<TableHead>Status</TableHead>
|
||||||
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{sortedPolicies.map((policy) => {
|
||||||
|
const isEditing = editingCode === policy.stageCode && draft;
|
||||||
|
return (
|
||||||
|
<TableRow key={policy.stageCode}>
|
||||||
|
<TableCell className="font-medium">{policy.stageCode}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<Select
|
||||||
|
value={draft.approvalMode}
|
||||||
|
onValueChange={(val: ApprovalMode) => setDraft({ ...draft, approvalMode: val })}
|
||||||
|
>
|
||||||
|
<SelectTrigger><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="ALL">ALL</SelectItem>
|
||||||
|
<SelectItem value="MIN_N">MIN_N</SelectItem>
|
||||||
|
<SelectItem value="ROLE_MANDATORY">ROLE_MANDATORY</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Badge variant="outline">{policy.approvalMode}</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
value={draft.minApprovals}
|
||||||
|
onChange={(e) => setDraft({ ...draft, minApprovals: Number(e.target.value || 1) })}
|
||||||
|
className="w-28"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
policy.minApprovals
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="max-w-[420px]">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="space-y-1">
|
||||||
|
<Label>Comma separated roles</Label>
|
||||||
|
<Input
|
||||||
|
value={draft.requiredRoles.join(', ')}
|
||||||
|
onChange={(e) =>
|
||||||
|
setDraft({
|
||||||
|
...draft,
|
||||||
|
requiredRoles: e.target.value
|
||||||
|
.split(',')
|
||||||
|
.map((r) => r.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{(policy.requiredRoles || []).map((role) => (
|
||||||
|
<Badge key={role} variant="secondary">{role}</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{isEditing ? (
|
||||||
|
<Select
|
||||||
|
value={draft.isActive ? 'active' : 'inactive'}
|
||||||
|
onValueChange={(val) => setDraft({ ...draft, isActive: val === 'active' })}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-28"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="active">Active</SelectItem>
|
||||||
|
<SelectItem value="inactive">Inactive</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
) : (
|
||||||
|
<Badge className={policy.isActive ? 'bg-green-600' : 'bg-slate-500'}>
|
||||||
|
{policy.isActive ? 'Active' : 'Inactive'}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
{isEditing ? (
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<Button size="sm" className="bg-amber-600 hover:bg-amber-700" onClick={saveEdit}>
|
||||||
|
<Save className="w-4 h-4 mr-1" /> Save
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" variant="outline" onClick={cancelEdit}>
|
||||||
|
<X className="w-4 h-4 mr-1" /> Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button size="sm" variant="outline" onClick={() => startEdit(policy)}>
|
||||||
|
<Edit2 className="w-4 h-4 mr-1" /> Edit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -27,6 +27,19 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
|
||||||
|
|
||||||
export function UserManagementPage() {
|
export function UserManagementPage() {
|
||||||
|
const getParentIdByType = (location: any, parentType: string): string => {
|
||||||
|
if (!location?.parents || !Array.isArray(location.parents)) return '';
|
||||||
|
const match = location.parents.find((p: any) => p?.type === parentType);
|
||||||
|
return match?.id || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeList = (res: any, preferredKey: string): any[] => {
|
||||||
|
if (!res) return [];
|
||||||
|
if (Array.isArray(res[preferredKey])) return res[preferredKey];
|
||||||
|
if (Array.isArray(res.data)) return res.data;
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
const [users, setUsers] = useState<any[]>([]);
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
const [roles, setRoles] = useState<any[]>([]);
|
const [roles, setRoles] = useState<any[]>([]);
|
||||||
const [zones, setZones] = useState<any[]>([]);
|
const [zones, setZones] = useState<any[]>([]);
|
||||||
@ -89,7 +102,7 @@ export function UserManagementPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.zoneId) {
|
if (formData.zoneId) {
|
||||||
masterService.getStates(formData.zoneId).then((res: any) => {
|
masterService.getStates(formData.zoneId).then((res: any) => {
|
||||||
if (res.success) setStates(res.states);
|
if (res.success) setStates(normalizeList(res, 'states'));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setStates([]);
|
setStates([]);
|
||||||
@ -100,7 +113,7 @@ export function UserManagementPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.stateId) {
|
if (formData.stateId) {
|
||||||
masterService.getDistricts(formData.stateId).then((res: any) => {
|
masterService.getDistricts(formData.stateId).then((res: any) => {
|
||||||
if (res.success) setDistricts(res.districts);
|
if (res.success) setDistricts(normalizeList(res, 'districts'));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setDistricts([]);
|
setDistricts([]);
|
||||||
@ -111,7 +124,7 @@ export function UserManagementPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (formData.districtId) {
|
if (formData.districtId) {
|
||||||
masterService.getAreas(formData.districtId).then((res: any) => {
|
masterService.getAreas(formData.districtId).then((res: any) => {
|
||||||
if (res.success) setAreas(res.areas);
|
if (res.success) setAreas(normalizeList(res, 'areas'));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setAreas([]);
|
setAreas([]);
|
||||||
@ -119,6 +132,15 @@ export function UserManagementPage() {
|
|||||||
}, [formData.districtId]);
|
}, [formData.districtId]);
|
||||||
|
|
||||||
const handleEditUser = (user: any) => {
|
const handleEditUser = (user: any) => {
|
||||||
|
const userLocation = user.location;
|
||||||
|
const userLocationType = userLocation?.type;
|
||||||
|
|
||||||
|
const zoneId = user.zoneId || (userLocationType === 'zone' ? userLocation?.id : getParentIdByType(userLocation, 'zone'));
|
||||||
|
const regionId = user.regionId || (userLocationType === 'region' ? userLocation?.id : getParentIdByType(userLocation, 'region'));
|
||||||
|
const stateId = user.stateId || (userLocationType === 'state' ? userLocation?.id : getParentIdByType(userLocation, 'state'));
|
||||||
|
const districtId = user.districtId || (userLocationType === 'district' ? userLocation?.id : getParentIdByType(userLocation, 'district'));
|
||||||
|
const areaId = user.areaId || (userLocationType === 'area' ? userLocation?.id : '');
|
||||||
|
|
||||||
setEditingUser(user);
|
setEditingUser(user);
|
||||||
setFormData({
|
setFormData({
|
||||||
fullName: user.fullName || '',
|
fullName: user.fullName || '',
|
||||||
@ -130,11 +152,11 @@ export function UserManagementPage() {
|
|||||||
department: user.department || '',
|
department: user.department || '',
|
||||||
designation: user.designation || '',
|
designation: user.designation || '',
|
||||||
employeeId: user.employeeId || '',
|
employeeId: user.employeeId || '',
|
||||||
zoneId: user.zoneId || '',
|
zoneId: zoneId || '',
|
||||||
regionId: user.regionId || '',
|
regionId: regionId || '',
|
||||||
stateId: user.stateId || '',
|
stateId: stateId || '',
|
||||||
districtId: user.districtId || '',
|
districtId: districtId || '',
|
||||||
areaId: user.areaId || ''
|
areaId: areaId || ''
|
||||||
});
|
});
|
||||||
setShowUserModal(true);
|
setShowUserModal(true);
|
||||||
};
|
};
|
||||||
@ -146,31 +168,26 @@ export function UserManagementPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const userLocationId = formData.areaId || formData.districtId || formData.stateId || formData.regionId || formData.zoneId || null;
|
||||||
|
const submitData = {
|
||||||
|
...formData,
|
||||||
|
locationId: userLocationId
|
||||||
|
};
|
||||||
|
|
||||||
if (editingUser) {
|
if (editingUser) {
|
||||||
const res = await adminService.updateUser(editingUser.id, formData);
|
const res = await adminService.updateUser(editingUser.id, submitData);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
setShowUserModal(false);
|
setShowUserModal(false);
|
||||||
fetchData();
|
fetchData();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const res = await adminService.createUser(formData);
|
const res = await adminService.createUser(submitData);
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success('User created successfully');
|
toast.success('User created successfully');
|
||||||
setFormData({
|
setFormData({
|
||||||
fullName: '',
|
fullName: '', email: '', roleCode: '', status: 'active', isActive: true,
|
||||||
email: '',
|
mobileNumber: '', department: '', designation: '', employeeId: '',
|
||||||
roleCode: '',
|
zoneId: '', regionId: '', stateId: '', districtId: '', areaId: ''
|
||||||
status: 'active',
|
|
||||||
isActive: true,
|
|
||||||
mobileNumber: '',
|
|
||||||
department: '',
|
|
||||||
designation: '',
|
|
||||||
employeeId: '',
|
|
||||||
zoneId: '',
|
|
||||||
regionId: '',
|
|
||||||
stateId: '',
|
|
||||||
districtId: '',
|
|
||||||
areaId: ''
|
|
||||||
});
|
});
|
||||||
setShowUserModal(false);
|
setShowUserModal(false);
|
||||||
fetchData();
|
fetchData();
|
||||||
@ -324,11 +341,11 @@ export function UserManagementPage() {
|
|||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
|
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
|
||||||
Zone: {user.zone?.zoneName || 'N/A'}
|
Location: {user.location?.name || 'N/A'}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-slate-500">
|
<div className="text-xs text-slate-500">
|
||||||
Region: {user.region?.regionName || 'N/A'}
|
Type: {user.location?.type ? user.location.type.toUpperCase() : 'N/A'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@ -486,7 +503,7 @@ export function UserManagementPage() {
|
|||||||
<h3 className="text-sm font-semibold text-slate-900 mb-4">Geographical Assignments</h3>
|
<h3 className="text-sm font-semibold text-slate-900 mb-4">Geographical Assignments</h3>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label htmlFor="zoneId">Zone</Label>
|
<Label htmlFor="zoneId">Zone (Top Level)</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.zoneId}
|
value={formData.zoneId}
|
||||||
onValueChange={(val) => setFormData({ ...formData, zoneId: val, regionId: '', stateId: '', districtId: '', areaId: '' })}
|
onValueChange={(val) => setFormData({ ...formData, zoneId: val, regionId: '', stateId: '', districtId: '', areaId: '' })}
|
||||||
@ -496,7 +513,7 @@ export function UserManagementPage() {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{zones.map(zone => (
|
{zones.map(zone => (
|
||||||
<SelectItem key={zone.id} value={zone.id}>{zone.zoneName}</SelectItem>
|
<SelectItem key={zone.id} value={zone.id}>{zone.name || zone.zoneName}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -512,8 +529,8 @@ export function UserManagementPage() {
|
|||||||
<SelectValue placeholder="Select Region" />
|
<SelectValue placeholder="Select Region" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{regions.filter(r => r.zoneId === formData.zoneId).map(region => (
|
{regions.filter(r => (r.parents && r.parents.some((p:any) => p.id === formData.zoneId)) || r.zoneId === formData.zoneId).map(region => (
|
||||||
<SelectItem key={region.id} value={region.id}>{region.regionName}</SelectItem>
|
<SelectItem key={region.id} value={region.id}>{region.name || region.regionName}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -529,8 +546,8 @@ export function UserManagementPage() {
|
|||||||
<SelectValue placeholder="Select State" />
|
<SelectValue placeholder="Select State" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{states.filter(s => s.zoneId === formData.zoneId).map(state => (
|
{states.filter(s => (s.parents && s.parents.some((p:any) => p.id === formData.zoneId)) || s.zoneId === formData.zoneId).map(state => (
|
||||||
<SelectItem key={state.id} value={state.id}>{state.stateName}</SelectItem>
|
<SelectItem key={state.id} value={state.id}>{state.name || state.stateName}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -546,8 +563,8 @@ export function UserManagementPage() {
|
|||||||
<SelectValue placeholder="Select District" />
|
<SelectValue placeholder="Select District" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{districts.map(district => (
|
{districts.filter(d => (d.parents && d.parents.some((p:any) => p.id === formData.stateId)) || d.stateId === formData.stateId).map(district => (
|
||||||
<SelectItem key={district.id} value={district.id}>{district.districtName}</SelectItem>
|
<SelectItem key={district.id} value={district.id}>{district.name || district.districtName}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
@ -563,8 +580,8 @@ export function UserManagementPage() {
|
|||||||
<SelectValue placeholder="Select Area" />
|
<SelectValue placeholder="Select Area" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{areas.map(area => (
|
{areas.filter(a => (a.parents && a.parents.some((p:any) => p.id === formData.districtId)) || a.districtId === formData.districtId).map(area => (
|
||||||
<SelectItem key={area.id} value={area.id}>{area.areaName}</SelectItem>
|
<SelectItem key={area.id} value={area.id}>{area.name || area.areaName}</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { masterService } from '../../services/master.service';
|
import { masterService } from '../../services/master.service';
|
||||||
|
import { adminService } from '../../services/admin.service';
|
||||||
|
import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
|
||||||
import { Badge } from '../ui/badge';
|
import { Badge } from '../ui/badge';
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
@ -30,7 +32,8 @@ import {
|
|||||||
Play,
|
Play,
|
||||||
UserCog,
|
UserCog,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
XCircle
|
XCircle,
|
||||||
|
SlidersHorizontal
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
|
||||||
@ -53,6 +56,24 @@ const getBody = (res: ApiResponse | unknown): ApiResponse => {
|
|||||||
return r;
|
return r;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const flattenLocationParents = (location: any): any[] => {
|
||||||
|
const collected: any[] = [];
|
||||||
|
const walk = (node: any) => {
|
||||||
|
const parents = Array.isArray(node?.parents) ? node.parents : [];
|
||||||
|
for (const parent of parents) {
|
||||||
|
collected.push(parent);
|
||||||
|
walk(parent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
walk(location);
|
||||||
|
return collected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAncestorByType = (location: any, type: string): any | undefined => {
|
||||||
|
if (location?.type === type) return location;
|
||||||
|
return flattenLocationParents(location).find((p: any) => p?.type === type);
|
||||||
|
};
|
||||||
|
|
||||||
// State management interfaces
|
// State management interfaces
|
||||||
|
|
||||||
// Unused Location interface removed
|
// Unused Location interface removed
|
||||||
@ -230,6 +251,7 @@ interface UserAssignment {
|
|||||||
name: string;
|
name: string;
|
||||||
role: string;
|
role: string;
|
||||||
roleCode: string;
|
roleCode: string;
|
||||||
|
locationId?: string;
|
||||||
region: string;
|
region: string;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
zone: string;
|
zone: string;
|
||||||
@ -383,6 +405,16 @@ export function MasterPage() {
|
|||||||
const bodyAreas = getBody(areasRes);
|
const bodyAreas = getBody(areasRes);
|
||||||
const bodySla = getBody(slaRes);
|
const bodySla = getBody(slaRes);
|
||||||
|
|
||||||
|
const users = bodyUsers?.users || bodyUsers?.data || [];
|
||||||
|
const usersByLocation: Record<string, any[]> = {};
|
||||||
|
users.forEach((u: any) => {
|
||||||
|
const locId = u.location?.id || u.locationId;
|
||||||
|
if (locId) {
|
||||||
|
if (!usersByLocation[locId]) usersByLocation[locId] = [];
|
||||||
|
usersByLocation[locId].push(u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (bodyRoles?.success) {
|
if (bodyRoles?.success) {
|
||||||
setRoles((bodyRoles.roles || bodyRoles.data || []).map((r: any) => ({
|
setRoles((bodyRoles.roles || bodyRoles.data || []).map((r: any) => ({
|
||||||
id: r.id,
|
id: r.id,
|
||||||
@ -394,25 +426,40 @@ export function MasterPage() {
|
|||||||
|
|
||||||
if (bodyZones?.success) {
|
if (bodyZones?.success) {
|
||||||
const rawZones = bodyZones.zones || bodyZones.data || [];
|
const rawZones = bodyZones.zones || bodyZones.data || [];
|
||||||
setZones(rawZones.map((z: any) => ({
|
setZones(rawZones.map((z: any) => {
|
||||||
|
const zoneUsers = usersByLocation[z.id] || [];
|
||||||
|
const zbhUser = zoneUsers.find((u: any) => {
|
||||||
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role?.roleName || '').toLowerCase();
|
||||||
|
return code === 'zbh' || name.includes('zonal business head') || code.includes('business head');
|
||||||
|
}) || ({} as any);
|
||||||
|
|
||||||
|
const zmUsers = zoneUsers.filter((u: any) => {
|
||||||
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role?.roleName || '').toLowerCase();
|
||||||
|
return (code === 'zm' || name.includes('zonal manager')) && !name.includes('head');
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
id: z.id,
|
id: z.id,
|
||||||
code: z.zoneCode,
|
code: z.name ? z.name.substring(0, 3).toUpperCase() : 'ZON',
|
||||||
name: z.zoneName,
|
name: z.name || z.zoneName,
|
||||||
description: z.description || '',
|
description: z.description || '',
|
||||||
states: Array.isArray(z.states) ? z.states.map((s: any) => s.stateName) : [],
|
states: Array.isArray(z.children) ? z.children.filter((child: any) => child.type === 'state').map((c: any) => c.name) : [],
|
||||||
zmCount: Array.isArray(z.managers) ? z.managers.length : 0,
|
zmCount: zmUsers.length,
|
||||||
zbh: {
|
zbh: {
|
||||||
name: z.zonalBusinessHead?.fullName || 'Not Assigned',
|
name: zbhUser.fullName || 'Not Assigned',
|
||||||
email: z.zonalBusinessHead?.email || '',
|
email: zbhUser.email || '',
|
||||||
phone: z.zonalBusinessHead?.mobileNumber || ''
|
phone: zbhUser.mobileNumber || ''
|
||||||
},
|
},
|
||||||
zonalManagers: Array.isArray(z.managers) ? z.managers.map((m: any) => ({
|
zonalManagers: zmUsers.map((m: any) => ({
|
||||||
name: m.user?.fullName || 'Unknown',
|
name: m.fullName || 'Unknown',
|
||||||
email: m.user?.email || '',
|
email: m.email || '',
|
||||||
phone: m.user?.mobileNumber || '',
|
phone: m.mobileNumber || '',
|
||||||
districts: []
|
districts: []
|
||||||
})) : []
|
}))
|
||||||
})));
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyPerms?.success) {
|
if (bodyPerms?.success) {
|
||||||
@ -420,86 +467,178 @@ export function MasterPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (bodyRegions?.success) {
|
if (bodyRegions?.success) {
|
||||||
setRegionalOffices((bodyRegions.regions || bodyRegions.data || []).map((r: any) => ({
|
setRegionalOffices((bodyRegions.regions || bodyRegions.data || []).map((r: any) => {
|
||||||
|
const regionUsers = usersByLocation[r.id] || [];
|
||||||
|
const rmUser = regionUsers.find(u => u.roleCode === 'RM' || u.roleCode === 'Regional Manager' || u.role?.roleName === 'Regional Manager') || ({} as any);
|
||||||
|
const asmUsers = regionUsers.filter(u => u.roleCode === 'ASM' || u.roleCode === 'Area Sales Manager' || u.role?.roleName === 'Area Sales Manager');
|
||||||
|
const zoneParent = getAncestorByType(r, 'zone');
|
||||||
|
|
||||||
|
return {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
code: r.regionCode,
|
code: r.name ? r.name.substring(0, 3).toUpperCase() : 'REG',
|
||||||
name: r.regionName,
|
name: r.name || r.regionName,
|
||||||
zoneId: r.zoneId,
|
zoneId: zoneParent?.id || r.zoneId,
|
||||||
zoneName: r.zone?.zoneName || 'Unknown',
|
zoneName: zoneParent?.name || r.zone?.zoneName || 'Unknown',
|
||||||
states: Array.isArray(r.states) ? r.states.map((s: any) => s.stateName) : [],
|
states: Array.isArray(r.children) ? r.children.filter((c: any) => c.type === 'state').map((c: any) => c.name) : [],
|
||||||
cities: [],
|
cities: [],
|
||||||
regionalOfficerCount: 0,
|
regionalOfficerCount: rmUser.id ? 1 : 0,
|
||||||
asmCount: 0,
|
asmCount: asmUsers.length,
|
||||||
status: (r.isActive !== false) ? 'Active' : 'Inactive',
|
status: (r.isActive !== false) ? 'Active' : 'Inactive',
|
||||||
regionalManager: r.regionalManager ? {
|
regionalManager: rmUser.id ? {
|
||||||
id: r.regionalManager.id,
|
id: rmUser.id,
|
||||||
name: r.regionalManager.fullName,
|
name: rmUser.fullName,
|
||||||
email: r.regionalManager.email,
|
email: rmUser.email,
|
||||||
phone: r.regionalManager.mobileNumber
|
phone: rmUser.mobileNumber
|
||||||
} : undefined
|
} : undefined
|
||||||
})));
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyUsers?.success) {
|
if (bodyUsers?.success) {
|
||||||
const users = bodyUsers.users || bodyUsers.data || [];
|
setUserAssignedData(users.map((u: any) => {
|
||||||
setUserAssignedData(users.map((u: any) => ({
|
const zone = getAncestorByType(u.location, 'zone');
|
||||||
|
const region = getAncestorByType(u.location, 'region');
|
||||||
|
|
||||||
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name: u.fullName,
|
name: u.fullName,
|
||||||
role: u.role?.roleName || u.roleCode || 'User',
|
role: u.role?.roleName || u.roleCode || 'User',
|
||||||
roleCode: u.roleCode || '',
|
roleCode: u.roleCode || '',
|
||||||
region: u.region?.regionName || 'Not Assigned',
|
locationId: u.location?.id || '',
|
||||||
regionId: u.regionId || '',
|
region: region?.name || 'Not Assigned',
|
||||||
zone: u.zone?.zoneName || 'Not Assigned',
|
regionId: region?.id || '',
|
||||||
zoneId: u.zoneId || '',
|
zone: zone?.name || 'Not Assigned',
|
||||||
|
zoneId: zone?.id || '',
|
||||||
email: u.email,
|
email: u.email,
|
||||||
phone: u.mobileNumber || 'N/A',
|
phone: u.mobileNumber || 'N/A',
|
||||||
status: (u.isActive !== false) ? 'Active' : 'Inactive',
|
status: (u.isActive !== false) ? 'Active' : 'Inactive',
|
||||||
employeeId: u.employeeId || '',
|
employeeId: u.employeeId || '',
|
||||||
asmCode: u.asmCode || '',
|
asmCode: u.asmCode || '',
|
||||||
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
|
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
|
||||||
})));
|
};
|
||||||
|
}));
|
||||||
|
|
||||||
// Populate Zonal Manager Mappings from user assignments
|
// Populate Zonal Manager Mappings from user assignments
|
||||||
const zmUsers = users.filter((u: any) => u.roleCode === 'ZM' || u.role?.roleName === 'Zonal Manager');
|
const globalZmUsers = users.filter((u: any) => {
|
||||||
setZonalManagerMappings(zmUsers.map((u: any) => ({
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role?.roleName || '').toLowerCase();
|
||||||
|
return code === 'zm' || name.includes('zonal manager') && !name.includes('head');
|
||||||
|
});
|
||||||
|
|
||||||
|
setZonalManagerMappings(globalZmUsers.map((u: any) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
name: u.fullName,
|
name: u.fullName,
|
||||||
code: u.employeeId || 'N/A',
|
code: u.employeeId || 'N/A',
|
||||||
email: u.email,
|
email: u.email,
|
||||||
phone: u.mobileNumber || 'N/A',
|
phone: u.mobileNumber || 'N/A',
|
||||||
zoneId: u.zoneId || '',
|
zoneId: u.location?.type === 'zone' ? u.location?.id : '',
|
||||||
zoneName: u.zone?.zoneName || 'Not Assigned',
|
zoneName: u.location?.type === 'zone' ? u.location?.name : 'Not Assigned',
|
||||||
regionId: u.regionId || '',
|
regionId: u.location?.type === 'region' ? u.location?.id : '',
|
||||||
regionName: u.region?.regionName || 'Not Assigned',
|
regionName: u.location?.type === 'region' ? u.location?.name : 'Not Assigned',
|
||||||
districts: u.districts || [],
|
districts: u.districts || [],
|
||||||
status: (u.isActive !== false) ? 'Active' : 'Inactive'
|
status: (u.isActive !== false) ? 'Active' : 'Inactive'
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodyStates?.success) setAllStates(bodyStates.states || bodyStates.data || []);
|
let parsedStates: any[] = [];
|
||||||
if (bodyDistricts?.success) setAllDistricts(bodyDistricts.districts || bodyDistricts.data || []);
|
if (bodyStates?.success) {
|
||||||
if (bodyAreas?.success) setAllAreas(bodyAreas.areas || bodyAreas.data || []);
|
const rawStates = bodyStates.states || bodyStates.data || [];
|
||||||
|
parsedStates = rawStates.map((s: any) => ({
|
||||||
|
...s,
|
||||||
|
stateName: s.name,
|
||||||
|
zoneId: getAncestorByType(s, 'zone')?.id || s.zoneId
|
||||||
|
}));
|
||||||
|
setAllStates(parsedStates);
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsedDistricts: any[] = [];
|
||||||
|
if (bodyDistricts?.success) {
|
||||||
|
const rawDistricts = bodyDistricts.districts || bodyDistricts.data || [];
|
||||||
|
parsedDistricts = rawDistricts.map((d: any) => ({
|
||||||
|
...d,
|
||||||
|
districtName: d.name,
|
||||||
|
stateId: getAncestorByType(d, 'state')?.id || d.stateId
|
||||||
|
}));
|
||||||
|
setAllDistricts(parsedDistricts);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bodyAreas?.success) {
|
||||||
|
const rawAreas = bodyAreas.areas || bodyAreas.data || [];
|
||||||
|
setAllAreas(rawAreas.map((a: any) => {
|
||||||
|
const districtId = getAncestorByType(a, 'district')?.id || a.districtId;
|
||||||
|
const districtObj = parsedDistricts.find(d => d.id === districtId);
|
||||||
|
const stateObj = parsedStates.find(s => s.id === districtObj?.stateId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...a,
|
||||||
|
areaName: a.name,
|
||||||
|
districtId: districtId,
|
||||||
|
district: {
|
||||||
|
districtName: districtObj?.districtName || 'Unknown',
|
||||||
|
stateId: districtObj?.stateId,
|
||||||
|
state: {
|
||||||
|
stateName: stateObj?.stateName || 'Unknown'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
if (bodyEmail?.success) setEmailTemplates(bodyEmail.data || []);
|
if (bodyEmail?.success) setEmailTemplates(bodyEmail.data || []);
|
||||||
|
|
||||||
if (bodyAsm?.success) {
|
if (bodyAsm?.success) {
|
||||||
const rawAsms = bodyAsm.data || bodyAsm.users || [];
|
const rawAsms = bodyAsm.data || bodyAsm.users || [];
|
||||||
setAsms(rawAsms.map((u: any) => ({
|
setAsms(rawAsms.map((u: any) => {
|
||||||
|
const location = u.location;
|
||||||
|
const zone = getAncestorByType(location, 'zone');
|
||||||
|
const region = getAncestorByType(location, 'region');
|
||||||
|
const district = getAncestorByType(location, 'district');
|
||||||
|
const state = getAncestorByType(location, 'state');
|
||||||
|
const areaNamesFromAreaManagers = Array.isArray(u.areaManagers)
|
||||||
|
? u.areaManagers.map((am: any) => am.area?.areaName || am.area?.name).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const areaNamesFromUserRoles = Array.isArray(u.userRoles)
|
||||||
|
? u.userRoles
|
||||||
|
.filter((ur: any) => ur?.role?.roleCode === 'ASM' && ur?.location?.type === 'area')
|
||||||
|
.map((ur: any) => ur?.location?.name)
|
||||||
|
.filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const mergedAreaNames = Array.from(new Set([...areaNamesFromAreaManagers, ...areaNamesFromUserRoles]));
|
||||||
|
const assignmentAsmCode = Array.isArray(u.userRoles)
|
||||||
|
? (u.userRoles.find((ur: any) =>
|
||||||
|
ur?.role?.roleCode === 'ASM' && (ur?.managerCode || '').trim() !== ''
|
||||||
|
)?.managerCode || '')
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return {
|
||||||
id: u.id,
|
id: u.id,
|
||||||
code: u.employeeId || 'N/A',
|
code: u.employeeId || 'N/A',
|
||||||
asmCode: u.asmCode || '',
|
asmCode: u.asmCode || assignmentAsmCode || '',
|
||||||
employeeId: u.employeeId || '',
|
employeeId: u.employeeId || '',
|
||||||
name: u.fullName,
|
name: u.fullName,
|
||||||
zoneId: u.zoneId || '',
|
zoneId: u.zoneId || zone?.id || '',
|
||||||
regionId: u.regionId || '',
|
regionId: u.regionId || region?.id || '',
|
||||||
zoneName: u.zone?.zoneName || 'Unassigned',
|
zoneName: u.zone?.zoneName || zone?.name || 'Unassigned',
|
||||||
regionName: u.region?.regionName || 'Unassigned',
|
regionName: u.region?.regionName || region?.name || 'Unassigned',
|
||||||
areasManaged: u.areaManagers ? u.areaManagers.map((am: any) => am.area?.areaName).filter(Boolean) : [],
|
areasManaged: mergedAreaNames.length > 0
|
||||||
districtNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName).filter(Boolean))) : [],
|
? mergedAreaNames
|
||||||
stateNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.state?.stateName).filter(Boolean))) : [],
|
: (location?.type === 'area' ? [location?.name] : []),
|
||||||
|
districtNames: u.areaManagers
|
||||||
|
? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName || am.area?.district?.name).filter(Boolean)))
|
||||||
|
: (district?.name ? [district?.name] : []),
|
||||||
|
stateNames: u.areaManagers
|
||||||
|
? Array.from(new Set(u.areaManagers.map((am: any) =>
|
||||||
|
am.area?.state?.stateName ||
|
||||||
|
am.area?.state?.name ||
|
||||||
|
am.area?.district?.state?.stateName ||
|
||||||
|
am.area?.district?.state?.name
|
||||||
|
).filter(Boolean)))
|
||||||
|
: (state?.name ? [state?.name] : []),
|
||||||
email: u.email,
|
email: u.email,
|
||||||
phone: u.mobileNumber,
|
phone: u.mobileNumber,
|
||||||
status: (u.isActive !== false) ? 'Active' : 'Inactive'
|
status: (u.isActive !== false) ? 'Active' : 'Inactive'
|
||||||
})));
|
};
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bodySla?.success) {
|
if (bodySla?.success) {
|
||||||
@ -760,10 +899,10 @@ export function MasterPage() {
|
|||||||
if (editingZoneId) {
|
if (editingZoneId) {
|
||||||
// Update existing zone
|
// Update existing zone
|
||||||
const updateData = {
|
const updateData = {
|
||||||
zoneName,
|
name: zoneName,
|
||||||
description: zoneDescription,
|
description: zoneDescription,
|
||||||
zonalBusinessHeadId: zbhId || null,
|
zonalBusinessHeadId: zbhId || null,
|
||||||
stateIds // Send IDs to backend
|
childrenIds: stateIds
|
||||||
};
|
};
|
||||||
|
|
||||||
const res = (await masterService.updateZone(editingZoneId, updateData)) as { success: boolean; message?: string };
|
const res = (await masterService.updateZone(editingZoneId, updateData)) as { success: boolean; message?: string };
|
||||||
@ -776,35 +915,22 @@ export function MasterPage() {
|
|||||||
toast.error(res.message || 'Failed to update zone');
|
toast.error(res.message || 'Failed to update zone');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Create new zone (Mock implementation for now as create is not fully hooked up in this task)
|
// Create new zone
|
||||||
// Build zonal managers array
|
const createData = {
|
||||||
const zonalManagers: ZonalManager[] = [];
|
|
||||||
for (let i = 0; i < zonalManagersCount; i++) {
|
|
||||||
if (zonalManagersData[i]) {
|
|
||||||
zonalManagers.push(zonalManagersData[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new zone object
|
|
||||||
const newZone: Zone = {
|
|
||||||
id: zoneCode.toLowerCase().replace(/\s+/g, '-'),
|
|
||||||
code: zoneCode,
|
|
||||||
name: zoneName,
|
name: zoneName,
|
||||||
states: selectedZoneStates,
|
|
||||||
zmCount: zonalManagersCount,
|
|
||||||
description: zoneDescription,
|
description: zoneDescription,
|
||||||
zbh: {
|
zonalBusinessHeadId: zbhId || null,
|
||||||
name: zbhName,
|
childrenIds: stateIds
|
||||||
email: zbhEmail,
|
|
||||||
phone: zbhPhone
|
|
||||||
},
|
|
||||||
zonalManagers
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add to zones array
|
const res = (await masterService.createZone(createData)) as { success: boolean; message?: string };
|
||||||
setZones([...zones, newZone]);
|
if (res.success) {
|
||||||
toast.success('Zone saved successfully!');
|
toast.success('Zone created successfully');
|
||||||
setShowZoneDialog(false);
|
setShowZoneDialog(false);
|
||||||
|
fetchInitialData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Failed to create zone');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset form (common)
|
// Reset form (common)
|
||||||
@ -865,11 +991,10 @@ export function MasterPage() {
|
|||||||
.map(s => s.id);
|
.map(s => s.id);
|
||||||
|
|
||||||
const regionData = {
|
const regionData = {
|
||||||
zoneId: selectedRegionZone, // This is expected to be ID now
|
parentIds: [selectedRegionZone], // This is expected to be array of IDs now
|
||||||
regionCode,
|
name: regionName,
|
||||||
regionName,
|
|
||||||
description: regionDescription,
|
description: regionDescription,
|
||||||
stateIds,
|
childrenIds: stateIds,
|
||||||
regionalManagerId: regionalManagerId || null
|
regionalManagerId: regionalManagerId || null
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -951,7 +1076,8 @@ export function MasterPage() {
|
|||||||
|
|
||||||
const handleSaveASM = async () => {
|
const handleSaveASM = async () => {
|
||||||
try {
|
try {
|
||||||
if (!asmManagerId) {
|
const targetAsmUserId = asmManagerId || editingASMId || '';
|
||||||
|
if (!targetAsmUserId) {
|
||||||
toast.error('Please select an Area Sales Manager');
|
toast.error('Please select an Area Sales Manager');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -960,55 +1086,56 @@ export function MasterPage() {
|
|||||||
toast.error('Please select at least one district');
|
toast.error('Please select at least one district');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!selectedASMRegion && !selectedASMZone) {
|
||||||
|
toast.error('Please select zone and region');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Map selected district names to IDs
|
// Map selected district names to IDs
|
||||||
const selectedDistrictIds = allDistricts
|
const selectedDistrictIds = allDistricts
|
||||||
.filter((d: District) => selectedASMDistricts.includes(d.districtName))
|
.filter((d: District) => selectedASMDistricts.includes(d.districtName))
|
||||||
.map((d: District) => d.id);
|
.map((d: District) => d.id);
|
||||||
|
const selectedStateIds = allStates
|
||||||
if (selectedDistrictIds.length === 0) {
|
.filter((s: State) => selectedASMStates.includes(s.stateName))
|
||||||
// Fallback if we can't match names (e.g. mock data mismatch)
|
.map((s: State) => s.id);
|
||||||
// But for now let's hope names match since they come from STATE_DISTRICTS_MAP which aligns with seed?
|
|
||||||
// Actually STATE_DISTRICTS_MAP is hardcoded. backend seed might differ.
|
|
||||||
// We warn if no match.
|
|
||||||
console.warn('No matching district IDs found for names:', selectedASMDistricts);
|
|
||||||
// Attempt to find areas by matching districtName in allAreas if linked?
|
|
||||||
// But let's rely on allDistricts for now.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find all areas in these districts
|
|
||||||
const targetAreas = allAreas.filter((a: Area) => a.districtId && selectedDistrictIds.includes(a.districtId));
|
const targetAreas = allAreas.filter((a: Area) => a.districtId && selectedDistrictIds.includes(a.districtId));
|
||||||
|
|
||||||
if (targetAreas.length === 0) {
|
if (targetAreas.length === 0) {
|
||||||
console.warn('Debugging ASM Save:');
|
toast.error('No areas found for selected districts');
|
||||||
console.warn('Selected District Names:', selectedASMDistricts);
|
|
||||||
console.warn('All Districts Count:', allDistricts.length);
|
|
||||||
console.warn('Matched District IDs:', selectedDistrictIds);
|
|
||||||
console.warn('All Areas Count:', allAreas.length);
|
|
||||||
|
|
||||||
const hasMissingDistricts = selectedASMDistricts.length > selectedDistrictIds.length;
|
|
||||||
|
|
||||||
if (hasMissingDistricts) {
|
|
||||||
toast.error('Some selected districts do not exist in the database. Please create them first from the "Locations" tab.');
|
|
||||||
} else {
|
|
||||||
toast.error('No Areas found in the selected districts. You must create Areas (Locations) in these districts before assigning an ASM.');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assign manager to all found areas and update ASM Code
|
const asmAssignments = targetAreas.map((area: Area, index: number) => ({
|
||||||
// We process each area update
|
roleCode: 'ASM',
|
||||||
const updatePromises = targetAreas.map((area: Area) => {
|
locationId: area.id,
|
||||||
console.log(`Updating area ${area.id} with managerId ${asmManagerId} and asmCode ${asmCode || 'null'}`);
|
managerCode: asmCode || null,
|
||||||
return ((masterService as unknown) as { updateArea: (id: string, data: { managerId: string; asmCode: string | null }) => Promise<void> }).updateArea(area.id, {
|
isPrimary: index === 0,
|
||||||
managerId: asmManagerId,
|
isActive: asmStatus === 'active'
|
||||||
asmCode: asmCode || null
|
}));
|
||||||
});
|
|
||||||
|
if (selectedDistrictIds.length === 0) {
|
||||||
|
toast.error('Selected districts are not mapped in database');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update ASM user mapping itself (not area reporting manager)
|
||||||
|
const updateRes = await adminService.updateUser(targetAsmUserId, {
|
||||||
|
locationId: selectedASMRegion || selectedASMZone || null,
|
||||||
|
status: asmStatus,
|
||||||
|
isActive: asmStatus === 'active',
|
||||||
|
asmCode: asmCode || null,
|
||||||
|
zoneId: selectedASMZone || null,
|
||||||
|
regionId: selectedASMRegion || null,
|
||||||
|
stateIds: selectedStateIds,
|
||||||
|
districtIds: selectedDistrictIds,
|
||||||
|
assignments: asmAssignments
|
||||||
});
|
});
|
||||||
|
|
||||||
await Promise.all(updatePromises);
|
if (!updateRes?.success) {
|
||||||
|
toast.error(updateRes?.message || 'Failed to update ASM mapping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
toast.success(`ASM assigned to ${targetAreas.length} areas successfully!`);
|
toast.success('ASM mapping updated successfully');
|
||||||
setShowASMDialog(false);
|
setShowASMDialog(false);
|
||||||
setEditingASMId(null);
|
setEditingASMId(null);
|
||||||
setAsmStatus('active');
|
setAsmStatus('active');
|
||||||
@ -1028,6 +1155,31 @@ export function MasterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDistrictsForSelectedState = (stateName: string): string[] => {
|
||||||
|
const stateIds = allStates
|
||||||
|
.filter((s) => s.stateName === stateName || s.name === stateName)
|
||||||
|
.map((s) => s.id);
|
||||||
|
|
||||||
|
if (stateIds.length === 0) return [];
|
||||||
|
|
||||||
|
return allDistricts
|
||||||
|
.filter((d) => stateIds.includes(d.stateId || d.state?.id))
|
||||||
|
.map((d) => d.districtName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const filteredASMUsers = userAssignedData.filter((u) => {
|
||||||
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role || '').toLowerCase();
|
||||||
|
const isAsm = code === 'asm' || name.includes('area sales manager') || name.includes('asm');
|
||||||
|
if (!isAsm) return false;
|
||||||
|
|
||||||
|
if (editingASMId && u.id === editingASMId) return true;
|
||||||
|
if (selectedASMRegion && u.regionId) return u.regionId === selectedASMRegion;
|
||||||
|
if (selectedASMZone && u.zoneId) return u.zoneId === selectedASMZone;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const handleEditRole = (role: Role) => {
|
const handleEditRole = (role: Role) => {
|
||||||
setSelectedRoleForEdit(role);
|
setSelectedRoleForEdit(role);
|
||||||
|
|
||||||
@ -1105,7 +1257,7 @@ export function MasterPage() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-5 h-auto">
|
<TabsList className="grid w-full grid-cols-6 h-auto">
|
||||||
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3">
|
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3">
|
||||||
<Globe className="w-4 h-4" />
|
<Globe className="w-4 h-4" />
|
||||||
Organisation
|
Organisation
|
||||||
@ -1126,6 +1278,10 @@ export function MasterPage() {
|
|||||||
<MapPin className="w-4 h-4" />
|
<MapPin className="w-4 h-4" />
|
||||||
Locations
|
Locations
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3">
|
||||||
|
<SlidersHorizontal className="w-4 h-4" />
|
||||||
|
Approval Policies
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
{/* Regional Hierarchy Tab */}
|
{/* Regional Hierarchy Tab */}
|
||||||
@ -1915,7 +2071,7 @@ export function MasterPage() {
|
|||||||
<div className="flex gap-2 justify-end">
|
<div className="flex gap-2 justify-end">
|
||||||
<Button variant="outline" size="sm" onClick={() => {
|
<Button variant="outline" size="sm" onClick={() => {
|
||||||
setLocationState(area.stateId || area.district?.stateId || '');
|
setLocationState(area.stateId || area.district?.stateId || '');
|
||||||
setLocationDistrict(area.district?.districtName || '');
|
setLocationDistrict(area.districtId || '');
|
||||||
setLocationCity(area.areaName);
|
setLocationCity(area.areaName);
|
||||||
setLocationPincode(area.pincode || '');
|
setLocationPincode(area.pincode || '');
|
||||||
setLocationStatus(area.isActive ? 'active' : 'inactive');
|
setLocationStatus(area.isActive ? 'active' : 'inactive');
|
||||||
@ -1957,6 +2113,11 @@ export function MasterPage() {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
{/* Approval Policies Tab */}
|
||||||
|
<TabsContent value="approvals" className="space-y-4">
|
||||||
|
<ApprovalPoliciesPage />
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -2001,7 +2162,18 @@ export function MasterPage() {
|
|||||||
<SelectValue placeholder="Select Manager" className="truncate" />
|
<SelectValue placeholder="Select Manager" className="truncate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-60">
|
<SelectContent className="max-h-60">
|
||||||
{userAssignedData.map((user) => (
|
{userAssignedData
|
||||||
|
.filter(u => {
|
||||||
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role || '').toLowerCase();
|
||||||
|
return (
|
||||||
|
code === 'rm' ||
|
||||||
|
code === 'rbm' ||
|
||||||
|
name.includes('regional manager') ||
|
||||||
|
name.includes('regional business manager')
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map((user) => (
|
||||||
<SelectItem key={user.id} value={user.id}>
|
<SelectItem key={user.id} value={user.id}>
|
||||||
{user.name} ({user.email})
|
{user.name} ({user.email})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -2639,7 +2811,13 @@ export function MasterPage() {
|
|||||||
<SelectValue placeholder="Select a Zonal Business Head" className="truncate" />
|
<SelectValue placeholder="Select a Zonal Business Head" className="truncate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{userAssignedData.map(user => (
|
{userAssignedData
|
||||||
|
.filter(u => {
|
||||||
|
const code = (u.roleCode || '').toLowerCase();
|
||||||
|
const name = (u.role || '').toLowerCase();
|
||||||
|
return code === 'zbh' || name.includes('business head') || name.includes('zbh');
|
||||||
|
})
|
||||||
|
.map(user => (
|
||||||
<SelectItem key={user.id} value={user.id}>
|
<SelectItem key={user.id} value={user.id}>
|
||||||
{user.name} ({user.email})
|
{user.name} ({user.email})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@ -3050,9 +3228,7 @@ export function MasterPage() {
|
|||||||
// Note: STATE_DISTRICTS_MAP is hardcoded map.
|
// Note: STATE_DISTRICTS_MAP is hardcoded map.
|
||||||
// If state names match, this works.
|
// If state names match, this works.
|
||||||
// Use allDistricts or dynamic mapping
|
// Use allDistricts or dynamic mapping
|
||||||
const stateDistricts = allDistricts
|
const stateDistricts = getDistrictsForSelectedState(state);
|
||||||
.filter(d => d.state?.stateName === state)
|
|
||||||
.map(d => d.districtName);
|
|
||||||
|
|
||||||
setSelectedASMDistricts(selectedASMDistricts.filter(d => !stateDistricts.includes(d)));
|
setSelectedASMDistricts(selectedASMDistricts.filter(d => !stateDistricts.includes(d)));
|
||||||
}
|
}
|
||||||
@ -3084,9 +3260,7 @@ export function MasterPage() {
|
|||||||
<div className="mt-2 border rounded-lg p-3 max-h-64 overflow-y-auto bg-slate-50">
|
<div className="mt-2 border rounded-lg p-3 max-h-64 overflow-y-auto bg-slate-50">
|
||||||
{selectedASMStates.map((state) => {
|
{selectedASMStates.map((state) => {
|
||||||
// Try to get districts from backend data first
|
// Try to get districts from backend data first
|
||||||
let districts = allDistricts
|
let districts = getDistrictsForSelectedState(state);
|
||||||
.filter((d) => d.state?.stateName === state)
|
|
||||||
.map((d) => d.districtName);
|
|
||||||
|
|
||||||
// No districts available for this state
|
// No districts available for this state
|
||||||
if (districts.length === 0) return null;
|
if (districts.length === 0) return null;
|
||||||
@ -3133,7 +3307,9 @@ export function MasterPage() {
|
|||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<Label>Area Sales Manager <span className="text-red-500">*</span></Label>
|
<Label>Area Sales Manager <span className="text-red-500">*</span></Label>
|
||||||
</div>
|
</div>
|
||||||
<Select value={asmManagerId} onValueChange={(value) => {
|
<Select
|
||||||
|
value={asmManagerId}
|
||||||
|
onValueChange={(value) => {
|
||||||
setAsmManagerId(value);
|
setAsmManagerId(value);
|
||||||
const selectedUser = userAssignedData.find(u => u.id === value);
|
const selectedUser = userAssignedData.find(u => u.id === value);
|
||||||
if (selectedUser) {
|
if (selectedUser) {
|
||||||
@ -3141,13 +3317,15 @@ export function MasterPage() {
|
|||||||
setAsmCode(selectedUser.asmCode || '');
|
setAsmCode(selectedUser.asmCode || '');
|
||||||
setAsmEmployeeId(selectedUser.employeeId || '');
|
setAsmEmployeeId(selectedUser.employeeId || '');
|
||||||
}
|
}
|
||||||
}}>
|
}}
|
||||||
|
disabled={!!editingASMId}
|
||||||
|
>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select ASM User" className="truncate" />
|
<SelectValue placeholder="Select ASM User" className="truncate" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="max-h-60">
|
<SelectContent className="max-h-60">
|
||||||
{userAssignedData.length > 0 ? (
|
{filteredASMUsers.length > 0 ? (
|
||||||
userAssignedData.map((user) => (
|
filteredASMUsers.map((user) => (
|
||||||
<SelectItem key={user.id} value={user.id}>
|
<SelectItem key={user.id} value={user.id}>
|
||||||
<div className="flex flex-col text-left">
|
<div className="flex flex-col text-left">
|
||||||
<span className="font-medium">{user.name}</span>
|
<span className="font-medium">{user.name}</span>
|
||||||
@ -3161,7 +3339,9 @@ export function MasterPage() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<div className="text-xs text-slate-500 mt-1">
|
<div className="text-xs text-slate-500 mt-1">
|
||||||
Select the user to assign as ASM for the selected districts.
|
{editingASMId
|
||||||
|
? 'Editing existing ASM mapping (ASM user is fixed in edit mode).'
|
||||||
|
: 'Select the user to assign as ASM for the selected districts.'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
@ -3217,6 +3397,7 @@ export function MasterPage() {
|
|||||||
setAsmManagerId('');
|
setAsmManagerId('');
|
||||||
setAsmCode('');
|
setAsmCode('');
|
||||||
setAsmName('');
|
setAsmName('');
|
||||||
|
setEditingASMId(null);
|
||||||
}}>
|
}}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -95,7 +95,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
|
|||||||
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
|
||||||
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSearch = (e: React.FormEvent) => {
|
const handleSearch = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (searchQuery.trim()) {
|
if (searchQuery.trim()) {
|
||||||
|
|||||||
@ -19,8 +19,8 @@ interface ApplicationFormPageProps {
|
|||||||
export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps) {
|
export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps) {
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
country: 'India', // Default to India
|
country: 'India', // Default to India
|
||||||
state: '',
|
stateId: '',
|
||||||
district: '',
|
districtId: '',
|
||||||
name: '',
|
name: '',
|
||||||
interestedCity: '',
|
interestedCity: '',
|
||||||
email: '',
|
email: '',
|
||||||
@ -66,7 +66,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
const handleStateChange = async (selectedState: any) => {
|
const handleStateChange = async (selectedState: any) => {
|
||||||
if (!selectedState) return;
|
if (!selectedState) return;
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, state: selectedState.stateName, district: '' }));
|
setFormData(prev => ({ ...prev, stateId: selectedState.id, districtId: '' }));
|
||||||
setDistricts([]);
|
setDistricts([]);
|
||||||
setFetchingDistricts(true);
|
setFetchingDistricts(true);
|
||||||
|
|
||||||
@ -89,7 +89,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!formData.country || !formData.state || !formData.district || !formData.name ||
|
if (!formData.country || !formData.stateId || !formData.districtId || !formData.name ||
|
||||||
!formData.interestedCity || !formData.email || !formData.pincode || !formData.mobile ||
|
!formData.interestedCity || !formData.email || !formData.pincode || !formData.mobile ||
|
||||||
!formData.ownRoyalEnfield || !formData.age || !formData.education ||
|
!formData.ownRoyalEnfield || !formData.age || !formData.education ||
|
||||||
!formData.companyName || !formData.source || !formData.existingDealer ||
|
!formData.companyName || !formData.source || !formData.existingDealer ||
|
||||||
@ -110,17 +110,23 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const selectedState = states.find((s: any) => s.id === formData.stateId);
|
||||||
|
const selectedDistrict = districts.find((d: any) => d.id === formData.districtId);
|
||||||
|
const stateName = selectedState?.stateName || '';
|
||||||
|
const districtName = selectedDistrict?.districtName || '';
|
||||||
|
|
||||||
// Map form data to backend expected format
|
// Map form data to backend expected format
|
||||||
const payload = {
|
const payload = {
|
||||||
applicantName: formData.name,
|
applicantName: formData.name,
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
phone: formData.mobile,
|
phone: formData.mobile,
|
||||||
state: formData.state,
|
state: stateName,
|
||||||
city: formData.interestedCity, // Or district?
|
city: formData.interestedCity, // Or district?
|
||||||
district: formData.district, // Crucial for auto-assignment
|
district: districtName, // Backward compatibility
|
||||||
preferredLocation: `${formData.interestedCity}, ${formData.state}`,
|
preferredLocation: `${formData.interestedCity}, ${stateName}`,
|
||||||
businessType: 'Dealership', // Default or derived
|
businessType: 'Dealership', // Default or derived
|
||||||
locationType: 'Urban', // Default or need field
|
locationType: 'district',
|
||||||
|
locationId: formData.districtId,
|
||||||
address: formData.address, // Need backend support?
|
address: formData.address, // Need backend support?
|
||||||
pincode: formData.pincode, // Need backend support?
|
pincode: formData.pincode, // Need backend support?
|
||||||
age: formData.age,
|
age: formData.age,
|
||||||
@ -141,7 +147,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
|
|
||||||
// Reset form
|
// Reset form
|
||||||
setFormData({
|
setFormData({
|
||||||
country: '', state: '', district: '', name: '', interestedCity: '',
|
country: 'India', stateId: '', districtId: '', name: '', interestedCity: '',
|
||||||
email: '', pincode: '', mobile: '', ownRoyalEnfield: '', royalEnfieldModel: '',
|
email: '', pincode: '', mobile: '', ownRoyalEnfield: '', royalEnfieldModel: '',
|
||||||
age: '', education: '', companyName: '', source: '', existingDealer: '',
|
age: '', education: '', companyName: '', source: '', existingDealer: '',
|
||||||
description: '', address: '', acceptTerms: false
|
description: '', address: '', acceptTerms: false
|
||||||
@ -390,9 +396,9 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
State <span className="text-amber-500">*</span>
|
State <span className="text-amber-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.state}
|
value={formData.stateId}
|
||||||
onValueChange={(value) => {
|
onValueChange={(value) => {
|
||||||
const selectedState = states.find((s: any) => s.stateName === value);
|
const selectedState = states.find((s: any) => s.id === value);
|
||||||
handleStateChange(selectedState);
|
handleStateChange(selectedState);
|
||||||
}}
|
}}
|
||||||
disabled={fetchingStates}
|
disabled={fetchingStates}
|
||||||
@ -402,7 +408,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
|
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
|
||||||
{states.map((state: any) => (
|
{states.map((state: any) => (
|
||||||
<SelectItem key={state.id} value={state.stateName}>
|
<SelectItem key={state.id} value={state.id}>
|
||||||
{state.stateName}
|
{state.stateName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
@ -415,16 +421,16 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
|
|||||||
District <span className="text-amber-500">*</span>
|
District <span className="text-amber-500">*</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Select
|
<Select
|
||||||
value={formData.district}
|
value={formData.districtId}
|
||||||
onValueChange={(value) => setFormData({ ...formData, district: value })}
|
onValueChange={(value) => setFormData({ ...formData, districtId: value })}
|
||||||
disabled={!formData.state || fetchingDistricts}
|
disabled={!formData.stateId || fetchingDistricts}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20">
|
<SelectTrigger className="bg-slate-800/50 border-slate-600/50 text-white focus:border-amber-500/50 focus:ring-amber-500/20">
|
||||||
<SelectValue placeholder={fetchingDistricts ? "Loading districts..." : "Select district"} />
|
<SelectValue placeholder={fetchingDistricts ? "Loading districts..." : "Select district"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
|
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
|
||||||
{districts.map((district: any) => (
|
{districts.map((district: any) => (
|
||||||
<SelectItem key={district.id} value={district.districtName}>
|
<SelectItem key={district.id} value={district.id}>
|
||||||
{district.districtName}
|
{district.districtName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -190,21 +190,21 @@ export const mockUsers: User[] = [
|
|||||||
id: '5',
|
id: '5',
|
||||||
name: 'Meera Iyer',
|
name: 'Meera Iyer',
|
||||||
email: 'ddlead@royalenfield.com',
|
email: 'ddlead@royalenfield.com',
|
||||||
password: 'password',
|
password: 'Admin@123',
|
||||||
role: 'DD Lead',
|
role: 'DD Lead',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '13',
|
id: '13',
|
||||||
name: 'Rahul Verma',
|
name: 'Rahul Verma',
|
||||||
email: 'finance@royalenfield.com',
|
email: 'finance@royalenfield.com',
|
||||||
password: 'password',
|
password: 'Admin@123',
|
||||||
role: 'Finance',
|
role: 'Finance',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: '14',
|
id: '14',
|
||||||
name: 'Amit Sharma',
|
name: 'Amit Sharma',
|
||||||
email: 'dealer@royalenfield.com',
|
email: 'dealer@royalenfield.com',
|
||||||
password: 'password',
|
password: 'Admin@123',
|
||||||
role: 'Dealer',
|
role: 'Dealer',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
27
src/services/approvalPolicy.service.ts
Normal file
27
src/services/approvalPolicy.service.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import API from '../api/API';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
export const approvalPolicyService = {
|
||||||
|
async getPolicies() {
|
||||||
|
try {
|
||||||
|
const response: any = await API.getApprovalPolicies();
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error?.response?.data?.message || 'Failed to fetch approval policies');
|
||||||
|
return { success: false, data: [] };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async savePolicy(stageCode: string, payload: any) {
|
||||||
|
try {
|
||||||
|
const response: any = await API.upsertApprovalPolicy(stageCode, payload);
|
||||||
|
if (response?.data?.success) {
|
||||||
|
toast.success('Approval policy updated');
|
||||||
|
}
|
||||||
|
return response.data;
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error?.response?.data?.message || 'Failed to update approval policy');
|
||||||
|
return { success: false };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -24,6 +24,10 @@ export const masterService = {
|
|||||||
const response = await API.updateZone(id, data);
|
const response = await API.updateZone(id, data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
createZone: async (data: any) => {
|
||||||
|
const response = await API.createZone(data);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
createRegion: async (data: any) => {
|
createRegion: async (data: any) => {
|
||||||
const response = await API.createRegion(data);
|
const response = await API.createRegion(data);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user