trying differnt appraoch to align location and user herarchy

This commit is contained in:
laxman h 2026-03-25 20:25:52 +05:30
parent af479db2f0
commit 73cf4fdfac
11 changed files with 2094 additions and 749 deletions

1816
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -31,6 +31,7 @@ import { FinancePaymentDetailsPage } from './components/applications/FinancePaym
import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage';
import { MasterPage } from './components/applications/MasterPage';
import { UserManagementPage } from './components/admin/UserManagementPage';
import { ApprovalPoliciesPage } from './components/admin/ApprovalPoliciesPage';
import { ConstitutionalChangePage } from './components/applications/ConstitutionalChangePage';
import { ConstitutionalChangeDetails } from './components/applications/ConstitutionalChangeDetails';
import { RelocationRequestPage } from './components/applications/RelocationRequestPage';
@ -144,6 +145,7 @@ export default function App() {
'/dealer-constitutional': 'Dealer Constitutional Change',
'/dealer-relocation': 'Dealer Relocation Requests',
'/questionnaire-builder': 'Questionnaire Builder',
'/approval-policies': 'Approval Policies',
};
return titles[pathname] || 'Dashboard';
};
@ -226,6 +228,11 @@ export default function App() {
{/* Other Modules */}
<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="/questions" element={<QuestionnaireList />} />
<Route path="/questionnaire-builder" element={<QuestionnaireBuilder />} />

View File

@ -13,15 +13,16 @@ export const API = {
updateRole: (id: string, data: any) => client.put(`/admin/roles/${id}`, data),
getZones: () => client.get('/master/zones'),
createZone: (data: any) => client.post('/master/zones', data),
updateZone: (id: string, data: any) => client.put(`/master/zones/${id}`, data),
createRegion: (data: any) => client.post('/master/regions', data),
updateRegion: (id: string, data: any) => client.put(`/master/regions/${id}`, data),
getRegions: () => client.get('/master/regions'),
getOutlets: () => client.get('/master/outlets'),
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
getStates: (zoneId?: string) => client.get('/master/states', { zoneId }),
getDistricts: (stateId?: string) => client.get('/master/districts', { stateId }),
getAreas: (districtId?: string) => client.get('/master/areas', { districtId }),
getStates: (zoneId?: string) => client.get('/master/states', { params: { zoneId } }),
getDistricts: (stateId?: string) => client.get('/master/districts', { params: { stateId } }),
getAreas: (districtId?: string) => client.get('/master/areas', { params: { districtId } }),
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
createArea: (data: any) => client.post('/master/areas', data),
getAreaManagers: () => client.get('/master/area-managers'),
@ -61,6 +62,9 @@ export const API = {
getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`),
updateRecommendation: (data: any) => client.post('/assessment/recommendation', 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
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),

View 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>
);
}

View File

@ -27,6 +27,19 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, Di
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select';
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 [roles, setRoles] = useState<any[]>([]);
const [zones, setZones] = useState<any[]>([]);
@ -89,7 +102,7 @@ export function UserManagementPage() {
useEffect(() => {
if (formData.zoneId) {
masterService.getStates(formData.zoneId).then((res: any) => {
if (res.success) setStates(res.states);
if (res.success) setStates(normalizeList(res, 'states'));
});
} else {
setStates([]);
@ -100,7 +113,7 @@ export function UserManagementPage() {
useEffect(() => {
if (formData.stateId) {
masterService.getDistricts(formData.stateId).then((res: any) => {
if (res.success) setDistricts(res.districts);
if (res.success) setDistricts(normalizeList(res, 'districts'));
});
} else {
setDistricts([]);
@ -111,7 +124,7 @@ export function UserManagementPage() {
useEffect(() => {
if (formData.districtId) {
masterService.getAreas(formData.districtId).then((res: any) => {
if (res.success) setAreas(res.areas);
if (res.success) setAreas(normalizeList(res, 'areas'));
});
} else {
setAreas([]);
@ -119,6 +132,15 @@ export function UserManagementPage() {
}, [formData.districtId]);
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);
setFormData({
fullName: user.fullName || '',
@ -130,11 +152,11 @@ export function UserManagementPage() {
department: user.department || '',
designation: user.designation || '',
employeeId: user.employeeId || '',
zoneId: user.zoneId || '',
regionId: user.regionId || '',
stateId: user.stateId || '',
districtId: user.districtId || '',
areaId: user.areaId || ''
zoneId: zoneId || '',
regionId: regionId || '',
stateId: stateId || '',
districtId: districtId || '',
areaId: areaId || ''
});
setShowUserModal(true);
};
@ -146,31 +168,26 @@ export function UserManagementPage() {
}
try {
const userLocationId = formData.areaId || formData.districtId || formData.stateId || formData.regionId || formData.zoneId || null;
const submitData = {
...formData,
locationId: userLocationId
};
if (editingUser) {
const res = await adminService.updateUser(editingUser.id, formData);
const res = await adminService.updateUser(editingUser.id, submitData);
if (res.success) {
setShowUserModal(false);
fetchData();
}
} else {
const res = await adminService.createUser(formData);
const res = await adminService.createUser(submitData);
if (res.success) {
toast.success('User created successfully');
setFormData({
fullName: '',
email: '',
roleCode: '',
status: 'active',
isActive: true,
mobileNumber: '',
department: '',
designation: '',
employeeId: '',
zoneId: '',
regionId: '',
stateId: '',
districtId: '',
areaId: ''
fullName: '', email: '', roleCode: '', status: 'active', isActive: true,
mobileNumber: '', department: '', designation: '', employeeId: '',
zoneId: '', regionId: '', stateId: '', districtId: '', areaId: ''
});
setShowUserModal(false);
fetchData();
@ -324,11 +341,11 @@ export function UserManagementPage() {
<div className="space-y-1">
<div className="flex items-center gap-2">
<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>
</div>
<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>
</TableCell>
@ -486,7 +503,7 @@ export function UserManagementPage() {
<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 gap-2">
<Label htmlFor="zoneId">Zone</Label>
<Label htmlFor="zoneId">Zone (Top Level)</Label>
<Select
value={formData.zoneId}
onValueChange={(val) => setFormData({ ...formData, zoneId: val, regionId: '', stateId: '', districtId: '', areaId: '' })}
@ -496,7 +513,7 @@ export function UserManagementPage() {
</SelectTrigger>
<SelectContent>
{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>
</Select>
@ -512,8 +529,8 @@ export function UserManagementPage() {
<SelectValue placeholder="Select Region" />
</SelectTrigger>
<SelectContent>
{regions.filter(r => r.zoneId === formData.zoneId).map(region => (
<SelectItem key={region.id} value={region.id}>{region.regionName}</SelectItem>
{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.name || region.regionName}</SelectItem>
))}
</SelectContent>
</Select>
@ -529,8 +546,8 @@ export function UserManagementPage() {
<SelectValue placeholder="Select State" />
</SelectTrigger>
<SelectContent>
{states.filter(s => s.zoneId === formData.zoneId).map(state => (
<SelectItem key={state.id} value={state.id}>{state.stateName}</SelectItem>
{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.name || state.stateName}</SelectItem>
))}
</SelectContent>
</Select>
@ -546,8 +563,8 @@ export function UserManagementPage() {
<SelectValue placeholder="Select District" />
</SelectTrigger>
<SelectContent>
{districts.map(district => (
<SelectItem key={district.id} value={district.id}>{district.districtName}</SelectItem>
{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.name || district.districtName}</SelectItem>
))}
</SelectContent>
</Select>
@ -563,8 +580,8 @@ export function UserManagementPage() {
<SelectValue placeholder="Select Area" />
</SelectTrigger>
<SelectContent>
{areas.map(area => (
<SelectItem key={area.id} value={area.id}>{area.areaName}</SelectItem>
{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.name || area.areaName}</SelectItem>
))}
</SelectContent>
</Select>

View File

@ -1,5 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
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 { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@ -30,7 +32,8 @@ import {
Play,
UserCog,
CheckCircle,
XCircle
XCircle,
SlidersHorizontal
} from 'lucide-react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
@ -53,6 +56,24 @@ const getBody = (res: ApiResponse | unknown): ApiResponse => {
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
// Unused Location interface removed
@ -230,6 +251,7 @@ interface UserAssignment {
name: string;
role: string;
roleCode: string;
locationId?: string;
region: string;
regionId: string;
zone: string;
@ -383,6 +405,16 @@ export function MasterPage() {
const bodyAreas = getBody(areasRes);
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) {
setRoles((bodyRoles.roles || bodyRoles.data || []).map((r: any) => ({
id: r.id,
@ -394,25 +426,40 @@ export function MasterPage() {
if (bodyZones?.success) {
const rawZones = bodyZones.zones || bodyZones.data || [];
setZones(rawZones.map((z: any) => ({
id: z.id,
code: z.zoneCode,
name: z.zoneName,
description: z.description || '',
states: Array.isArray(z.states) ? z.states.map((s: any) => s.stateName) : [],
zmCount: Array.isArray(z.managers) ? z.managers.length : 0,
zbh: {
name: z.zonalBusinessHead?.fullName || 'Not Assigned',
email: z.zonalBusinessHead?.email || '',
phone: z.zonalBusinessHead?.mobileNumber || ''
},
zonalManagers: Array.isArray(z.managers) ? z.managers.map((m: any) => ({
name: m.user?.fullName || 'Unknown',
email: m.user?.email || '',
phone: m.user?.mobileNumber || '',
districts: []
})) : []
})));
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,
code: z.name ? z.name.substring(0, 3).toUpperCase() : 'ZON',
name: z.name || z.zoneName,
description: z.description || '',
states: Array.isArray(z.children) ? z.children.filter((child: any) => child.type === 'state').map((c: any) => c.name) : [],
zmCount: zmUsers.length,
zbh: {
name: zbhUser.fullName || 'Not Assigned',
email: zbhUser.email || '',
phone: zbhUser.mobileNumber || ''
},
zonalManagers: zmUsers.map((m: any) => ({
name: m.fullName || 'Unknown',
email: m.email || '',
phone: m.mobileNumber || '',
districts: []
}))
};
}));
}
if (bodyPerms?.success) {
@ -420,86 +467,178 @@ export function MasterPage() {
}
if (bodyRegions?.success) {
setRegionalOffices((bodyRegions.regions || bodyRegions.data || []).map((r: any) => ({
id: r.id,
code: r.regionCode,
name: r.regionName,
zoneId: r.zoneId,
zoneName: r.zone?.zoneName || 'Unknown',
states: Array.isArray(r.states) ? r.states.map((s: any) => s.stateName) : [],
cities: [],
regionalOfficerCount: 0,
asmCount: 0,
status: (r.isActive !== false) ? 'Active' : 'Inactive',
regionalManager: r.regionalManager ? {
id: r.regionalManager.id,
name: r.regionalManager.fullName,
email: r.regionalManager.email,
phone: r.regionalManager.mobileNumber
} : undefined
})));
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,
code: r.name ? r.name.substring(0, 3).toUpperCase() : 'REG',
name: r.name || r.regionName,
zoneId: zoneParent?.id || r.zoneId,
zoneName: zoneParent?.name || r.zone?.zoneName || 'Unknown',
states: Array.isArray(r.children) ? r.children.filter((c: any) => c.type === 'state').map((c: any) => c.name) : [],
cities: [],
regionalOfficerCount: rmUser.id ? 1 : 0,
asmCount: asmUsers.length,
status: (r.isActive !== false) ? 'Active' : 'Inactive',
regionalManager: rmUser.id ? {
id: rmUser.id,
name: rmUser.fullName,
email: rmUser.email,
phone: rmUser.mobileNumber
} : undefined
};
}));
}
if (bodyUsers?.success) {
const users = bodyUsers.users || bodyUsers.data || [];
setUserAssignedData(users.map((u: any) => ({
id: u.id,
name: u.fullName,
role: u.role?.roleName || u.roleCode || 'User',
roleCode: u.roleCode || '',
region: u.region?.regionName || 'Not Assigned',
regionId: u.regionId || '',
zone: u.zone?.zoneName || 'Not Assigned',
zoneId: u.zoneId || '',
email: u.email,
phone: u.mobileNumber || 'N/A',
status: (u.isActive !== false) ? 'Active' : 'Inactive',
employeeId: u.employeeId || '',
asmCode: u.asmCode || '',
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
})));
setUserAssignedData(users.map((u: any) => {
const zone = getAncestorByType(u.location, 'zone');
const region = getAncestorByType(u.location, 'region');
return {
id: u.id,
name: u.fullName,
role: u.role?.roleName || u.roleCode || 'User',
roleCode: u.roleCode || '',
locationId: u.location?.id || '',
region: region?.name || 'Not Assigned',
regionId: region?.id || '',
zone: zone?.name || 'Not Assigned',
zoneId: zone?.id || '',
email: u.email,
phone: u.mobileNumber || 'N/A',
status: (u.isActive !== false) ? 'Active' : 'Inactive',
employeeId: u.employeeId || '',
asmCode: u.asmCode || '',
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
};
}));
// Populate Zonal Manager Mappings from user assignments
const zmUsers = users.filter((u: any) => u.roleCode === 'ZM' || u.role?.roleName === 'Zonal Manager');
setZonalManagerMappings(zmUsers.map((u: any) => ({
const globalZmUsers = users.filter((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,
name: u.fullName,
code: u.employeeId || 'N/A',
email: u.email,
phone: u.mobileNumber || 'N/A',
zoneId: u.zoneId || '',
zoneName: u.zone?.zoneName || 'Not Assigned',
regionId: u.regionId || '',
regionName: u.region?.regionName || 'Not Assigned',
zoneId: u.location?.type === 'zone' ? u.location?.id : '',
zoneName: u.location?.type === 'zone' ? u.location?.name : 'Not Assigned',
regionId: u.location?.type === 'region' ? u.location?.id : '',
regionName: u.location?.type === 'region' ? u.location?.name : 'Not Assigned',
districts: u.districts || [],
status: (u.isActive !== false) ? 'Active' : 'Inactive'
})));
}
if (bodyStates?.success) setAllStates(bodyStates.states || bodyStates.data || []);
if (bodyDistricts?.success) setAllDistricts(bodyDistricts.districts || bodyDistricts.data || []);
if (bodyAreas?.success) setAllAreas(bodyAreas.areas || bodyAreas.data || []);
let parsedStates: any[] = [];
if (bodyStates?.success) {
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 (bodyAsm?.success) {
const rawAsms = bodyAsm.data || bodyAsm.users || [];
setAsms(rawAsms.map((u: any) => ({
id: u.id,
code: u.employeeId || 'N/A',
asmCode: u.asmCode || '',
employeeId: u.employeeId || '',
name: u.fullName,
zoneId: u.zoneId || '',
regionId: u.regionId || '',
zoneName: u.zone?.zoneName || 'Unassigned',
regionName: u.region?.regionName || 'Unassigned',
areasManaged: u.areaManagers ? u.areaManagers.map((am: any) => am.area?.areaName).filter(Boolean) : [],
districtNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName).filter(Boolean))) : [],
stateNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.state?.stateName).filter(Boolean))) : [],
email: u.email,
phone: u.mobileNumber,
status: (u.isActive !== false) ? 'Active' : 'Inactive'
})));
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,
code: u.employeeId || 'N/A',
asmCode: u.asmCode || assignmentAsmCode || '',
employeeId: u.employeeId || '',
name: u.fullName,
zoneId: u.zoneId || zone?.id || '',
regionId: u.regionId || region?.id || '',
zoneName: u.zone?.zoneName || zone?.name || 'Unassigned',
regionName: u.region?.regionName || region?.name || 'Unassigned',
areasManaged: mergedAreaNames.length > 0
? mergedAreaNames
: (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,
phone: u.mobileNumber,
status: (u.isActive !== false) ? 'Active' : 'Inactive'
};
}));
}
if (bodySla?.success) {
@ -760,10 +899,10 @@ export function MasterPage() {
if (editingZoneId) {
// Update existing zone
const updateData = {
zoneName,
name: zoneName,
description: zoneDescription,
zonalBusinessHeadId: zbhId || null,
stateIds // Send IDs to backend
childrenIds: stateIds
};
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');
}
} else {
// Create new zone (Mock implementation for now as create is not fully hooked up in this task)
// Build zonal managers array
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,
// Create new zone
const createData = {
name: zoneName,
states: selectedZoneStates,
zmCount: zonalManagersCount,
description: zoneDescription,
zbh: {
name: zbhName,
email: zbhEmail,
phone: zbhPhone
},
zonalManagers
zonalBusinessHeadId: zbhId || null,
childrenIds: stateIds
};
// Add to zones array
setZones([...zones, newZone]);
toast.success('Zone saved successfully!');
setShowZoneDialog(false);
const res = (await masterService.createZone(createData)) as { success: boolean; message?: string };
if (res.success) {
toast.success('Zone created successfully');
setShowZoneDialog(false);
fetchInitialData();
} else {
toast.error(res.message || 'Failed to create zone');
}
}
// Reset form (common)
@ -865,11 +991,10 @@ export function MasterPage() {
.map(s => s.id);
const regionData = {
zoneId: selectedRegionZone, // This is expected to be ID now
regionCode,
regionName,
parentIds: [selectedRegionZone], // This is expected to be array of IDs now
name: regionName,
description: regionDescription,
stateIds,
childrenIds: stateIds,
regionalManagerId: regionalManagerId || null
};
@ -951,7 +1076,8 @@ export function MasterPage() {
const handleSaveASM = async () => {
try {
if (!asmManagerId) {
const targetAsmUserId = asmManagerId || editingASMId || '';
if (!targetAsmUserId) {
toast.error('Please select an Area Sales Manager');
return;
}
@ -960,55 +1086,56 @@ export function MasterPage() {
toast.error('Please select at least one district');
return;
}
if (!selectedASMRegion && !selectedASMZone) {
toast.error('Please select zone and region');
return;
}
// Map selected district names to IDs
const selectedDistrictIds = allDistricts
.filter((d: District) => selectedASMDistricts.includes(d.districtName))
.map((d: District) => d.id);
if (selectedDistrictIds.length === 0) {
// Fallback if we can't match names (e.g. mock data mismatch)
// 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 selectedStateIds = allStates
.filter((s: State) => selectedASMStates.includes(s.stateName))
.map((s: State) => s.id);
const targetAreas = allAreas.filter((a: Area) => a.districtId && selectedDistrictIds.includes(a.districtId));
if (targetAreas.length === 0) {
console.warn('Debugging ASM Save:');
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.');
}
toast.error('No areas found for selected districts');
return;
}
// Assign manager to all found areas and update ASM Code
// We process each area update
const updatePromises = targetAreas.map((area: Area) => {
console.log(`Updating area ${area.id} with managerId ${asmManagerId} and asmCode ${asmCode || 'null'}`);
return ((masterService as unknown) as { updateArea: (id: string, data: { managerId: string; asmCode: string | null }) => Promise<void> }).updateArea(area.id, {
managerId: asmManagerId,
asmCode: asmCode || null
});
const asmAssignments = targetAreas.map((area: Area, index: number) => ({
roleCode: 'ASM',
locationId: area.id,
managerCode: asmCode || null,
isPrimary: index === 0,
isActive: asmStatus === 'active'
}));
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);
setEditingASMId(null);
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) => {
setSelectedRoleForEdit(role);
@ -1105,7 +1257,7 @@ export function MasterPage() {
</div>
) : (
<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">
<Globe className="w-4 h-4" />
Organisation
@ -1126,6 +1278,10 @@ export function MasterPage() {
<MapPin className="w-4 h-4" />
Locations
</TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3">
<SlidersHorizontal className="w-4 h-4" />
Approval Policies
</TabsTrigger>
</TabsList>
{/* Regional Hierarchy Tab */}
@ -1915,7 +2071,7 @@ export function MasterPage() {
<div className="flex gap-2 justify-end">
<Button variant="outline" size="sm" onClick={() => {
setLocationState(area.stateId || area.district?.stateId || '');
setLocationDistrict(area.district?.districtName || '');
setLocationDistrict(area.districtId || '');
setLocationCity(area.areaName);
setLocationPincode(area.pincode || '');
setLocationStatus(area.isActive ? 'active' : 'inactive');
@ -1957,6 +2113,11 @@ export function MasterPage() {
</Card>
</TabsContent>
{/* Approval Policies Tab */}
<TabsContent value="approvals" className="space-y-4">
<ApprovalPoliciesPage />
</TabsContent>
</Tabs>
)}
@ -2001,7 +2162,18 @@ export function MasterPage() {
<SelectValue placeholder="Select Manager" className="truncate" />
</SelectTrigger>
<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}>
{user.name} ({user.email})
</SelectItem>
@ -2639,7 +2811,13 @@ export function MasterPage() {
<SelectValue placeholder="Select a Zonal Business Head" className="truncate" />
</SelectTrigger>
<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}>
{user.name} ({user.email})
</SelectItem>
@ -3050,9 +3228,7 @@ export function MasterPage() {
// Note: STATE_DISTRICTS_MAP is hardcoded map.
// If state names match, this works.
// Use allDistricts or dynamic mapping
const stateDistricts = allDistricts
.filter(d => d.state?.stateName === state)
.map(d => d.districtName);
const stateDistricts = getDistrictsForSelectedState(state);
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">
{selectedASMStates.map((state) => {
// Try to get districts from backend data first
let districts = allDistricts
.filter((d) => d.state?.stateName === state)
.map((d) => d.districtName);
let districts = getDistrictsForSelectedState(state);
// No districts available for this state
if (districts.length === 0) return null;
@ -3133,7 +3307,9 @@ export function MasterPage() {
<div className="flex items-center justify-between mb-2">
<Label>Area Sales Manager <span className="text-red-500">*</span></Label>
</div>
<Select value={asmManagerId} onValueChange={(value) => {
<Select
value={asmManagerId}
onValueChange={(value) => {
setAsmManagerId(value);
const selectedUser = userAssignedData.find(u => u.id === value);
if (selectedUser) {
@ -3141,13 +3317,15 @@ export function MasterPage() {
setAsmCode(selectedUser.asmCode || '');
setAsmEmployeeId(selectedUser.employeeId || '');
}
}}>
}}
disabled={!!editingASMId}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select ASM User" className="truncate" />
</SelectTrigger>
<SelectContent className="max-h-60">
{userAssignedData.length > 0 ? (
userAssignedData.map((user) => (
{filteredASMUsers.length > 0 ? (
filteredASMUsers.map((user) => (
<SelectItem key={user.id} value={user.id}>
<div className="flex flex-col text-left">
<span className="font-medium">{user.name}</span>
@ -3161,7 +3339,9 @@ export function MasterPage() {
</SelectContent>
</Select>
<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 className="grid grid-cols-2 gap-4">
@ -3217,6 +3397,7 @@ export function MasterPage() {
setAsmManagerId('');
setAsmCode('');
setAsmName('');
setEditingASMId(null);
}}>
Cancel
</Button>

View File

@ -95,7 +95,6 @@ export function Sidebar({ onLogout }: SidebarProps) {
menuItems.push({ id: 'users', label: 'User Management', icon: Users });
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
}
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
if (searchQuery.trim()) {

View File

@ -19,8 +19,8 @@ interface ApplicationFormPageProps {
export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps) {
const [formData, setFormData] = useState({
country: 'India', // Default to India
state: '',
district: '',
stateId: '',
districtId: '',
name: '',
interestedCity: '',
email: '',
@ -66,7 +66,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
const handleStateChange = async (selectedState: any) => {
if (!selectedState) return;
setFormData(prev => ({ ...prev, state: selectedState.stateName, district: '' }));
setFormData(prev => ({ ...prev, stateId: selectedState.id, districtId: '' }));
setDistricts([]);
setFetchingDistricts(true);
@ -89,7 +89,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
e.preventDefault();
// 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.ownRoyalEnfield || !formData.age || !formData.education ||
!formData.companyName || !formData.source || !formData.existingDealer ||
@ -110,17 +110,23 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
}
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
const payload = {
applicantName: formData.name,
email: formData.email,
phone: formData.mobile,
state: formData.state,
state: stateName,
city: formData.interestedCity, // Or district?
district: formData.district, // Crucial for auto-assignment
preferredLocation: `${formData.interestedCity}, ${formData.state}`,
district: districtName, // Backward compatibility
preferredLocation: `${formData.interestedCity}, ${stateName}`,
businessType: 'Dealership', // Default or derived
locationType: 'Urban', // Default or need field
locationType: 'district',
locationId: formData.districtId,
address: formData.address, // Need backend support?
pincode: formData.pincode, // Need backend support?
age: formData.age,
@ -141,7 +147,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
// Reset form
setFormData({
country: '', state: '', district: '', name: '', interestedCity: '',
country: 'India', stateId: '', districtId: '', name: '', interestedCity: '',
email: '', pincode: '', mobile: '', ownRoyalEnfield: '', royalEnfieldModel: '',
age: '', education: '', companyName: '', source: '', existingDealer: '',
description: '', address: '', acceptTerms: false
@ -390,9 +396,9 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
State <span className="text-amber-500">*</span>
</Label>
<Select
value={formData.state}
value={formData.stateId}
onValueChange={(value) => {
const selectedState = states.find((s: any) => s.stateName === value);
const selectedState = states.find((s: any) => s.id === value);
handleStateChange(selectedState);
}}
disabled={fetchingStates}
@ -402,7 +408,7 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
{states.map((state: any) => (
<SelectItem key={state.id} value={state.stateName}>
<SelectItem key={state.id} value={state.id}>
{state.stateName}
</SelectItem>
))}
@ -415,16 +421,16 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
District <span className="text-amber-500">*</span>
</Label>
<Select
value={formData.district}
onValueChange={(value) => setFormData({ ...formData, district: value })}
disabled={!formData.state || fetchingDistricts}
value={formData.districtId}
onValueChange={(value) => setFormData({ ...formData, districtId: value })}
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">
<SelectValue placeholder={fetchingDistricts ? "Loading districts..." : "Select district"} />
</SelectTrigger>
<SelectContent className="bg-slate-800 border-slate-700 text-white h-64">
{districts.map((district: any) => (
<SelectItem key={district.id} value={district.districtName}>
<SelectItem key={district.id} value={district.id}>
{district.districtName}
</SelectItem>
))}

View File

@ -190,21 +190,21 @@ export const mockUsers: User[] = [
id: '5',
name: 'Meera Iyer',
email: 'ddlead@royalenfield.com',
password: 'password',
password: 'Admin@123',
role: 'DD Lead',
},
{
id: '13',
name: 'Rahul Verma',
email: 'finance@royalenfield.com',
password: 'password',
password: 'Admin@123',
role: 'Finance',
},
{
id: '14',
name: 'Amit Sharma',
email: 'dealer@royalenfield.com',
password: 'password',
password: 'Admin@123',
role: 'Dealer',
},
{

View 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 };
}
}
};

View File

@ -24,6 +24,10 @@ export const masterService = {
const response = await API.updateZone(id, data);
return response.data;
},
createZone: async (data: any) => {
const response = await API.createZone(data);
return response.data;
},
createRegion: async (data: any) => {
const response = await API.createRegion(data);
return response.data;