@@ -284,333 +140,117 @@ export default function App() {
);
}
- // Show public application form if not authenticated and not trying to log in as admin
- if (!isAuthenticated && !showAdminLogin) {
+ // Public Routes
+ if (!isAuthenticated) {
return (
- <>
-
-
- >
- );
- }
-
- // Show admin login page if user clicked admin login but hasn't authenticated yet
- if (!isAuthenticated && showAdminLogin) {
- return (
- <>
-
-
- >
- );
- }
-
- return (
-
- {application.rank} of {application.totalApplicantsAtLocation}
+ {application.rank} of {application.totalApplicantsAtLocation}
in {application.preferredLocation}
{/* Info Banner - Only visible for DD users */}
{/* Note: This page shows only applications that have been shortlisted */}
-
+
{/* Filters and Actions Bar */}
diff --git a/src/components/applications/MasterPage.tsx b/src/components/applications/MasterPage.tsx
index f74d680..76b3858 100644
--- a/src/components/applications/MasterPage.tsx
+++ b/src/components/applications/MasterPage.tsx
@@ -38,17 +38,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '.
import { ScrollArea } from '../ui/scroll-area';
import { Checkbox } from '../ui/checkbox';
-// All Indian States and Union Territories
-const ALL_INDIAN_STATES = [
- 'Andhra Pradesh', 'Arunachal Pradesh', 'Assam', 'Bihar', 'Chhattisgarh',
- 'Goa', 'Gujarat', 'Haryana', 'Himachal Pradesh', 'Jharkhand',
- 'Karnataka', 'Kerala', 'Madhya Pradesh', 'Maharashtra', 'Manipur',
- 'Meghalaya', 'Mizoram', 'Nagaland', 'Odisha', 'Punjab',
- 'Rajasthan', 'Sikkim', 'Tamil Nadu', 'Telangana', 'Tripura',
- 'Uttar Pradesh', 'Uttarakhand', 'West Bengal',
- 'Andaman and Nicobar Islands', 'Chandigarh', 'Dadra and Nagar Haveli and Daman and Diu',
- 'Delhi', 'Jammu & Kashmir', 'Ladakh', 'Lakshadweep', 'Puducherry'
-];
+
// Zone to States Mapping
const ZONE_STATES_MAP: Record = {
@@ -200,6 +190,12 @@ interface RegionalOffice {
regionalOfficerCount: number;
asmCount: number;
status: 'Active' | 'Inactive';
+ regionalManager?: {
+ id: string;
+ name: string;
+ email: string;
+ phone: string;
+ };
}
interface ZonalManagerMapping {
@@ -227,7 +223,11 @@ interface ASM {
regionId: string;
regionName: string;
areasManaged: string[];
+ districtNames: string[];
+ stateNames: string[];
status: 'Active' | 'Inactive';
+ employeeId: string;
+ asmCode: string;
}
interface UserAssignment {
@@ -238,6 +238,7 @@ interface UserAssignment {
zone: string;
email: string;
phone: string;
+ employeeId?: string;
status: 'Active' | 'Inactive';
permissions?: string[];
}
@@ -296,10 +297,12 @@ export function MasterPage() {
const [zoneCode, setZoneCode] = useState('');
const [zoneName, setZoneName] = useState('');
const [zoneDescription, setZoneDescription] = useState('');
+ const [zbhId, setZbhId] = useState(''); // For selection
const [zbhName, setZbhName] = useState('');
const [zbhEmail, setZbhEmail] = useState('');
const [zbhPhone, setZbhPhone] = useState('');
const [zonalManagersData, setZonalManagersData] = useState<{ [key: number]: ZonalManager }>({});
+ const [editingZoneId, setEditingZoneId] = useState(null);
// Real data state
const [availablePermissions, setAvailablePermissions] = useState([]);
@@ -309,25 +312,141 @@ export function MasterPage() {
const [asms, setAsms] = useState([]);
const [zonalManagerMappings, setZonalManagerMappings] = useState([]);
const [userAssignedData, setUserAssignedData] = useState([]);
+ const [allStates, setAllStates] = useState([]); // Dynamic states list
+ const [allDistricts, setAllDistricts] = useState([]);
+ const [allAreas, setAllAreas] = useState([]);
+ const [selectedASMRole, setSelectedASMRole] = useState(null); // For future use if filtering users
const [loading, setLoading] = useState(true);
+ // Region form state
+ const [regionCode, setRegionCode] = useState('');
+ const [regionName, setRegionName] = useState('');
+ const [regionDescription, setRegionDescription] = useState('');
+ const [editingRegionId, setEditingRegionId] = useState(null);
+ const [regionalManagerId, setRegionalManagerId] = useState('');
+ // ASM Form State
+ const [asmManagerId, setAsmManagerId] = useState('');
+ const [asmCode, setAsmCode] = useState('');
+ const [asmName, setAsmName] = useState('');
+ const [asmEmployeeId, setAsmEmployeeId] = useState('');
+
+ // Location form state
+ const [locationState, setLocationState] = useState('');
+ const [locationDistrict, setLocationDistrict] = useState('');
+ const [locationCity, setLocationCity] = useState(''); // Area Name
+ const [locationPincode, setLocationPincode] = useState('');
+ const [locationActiveFrom, setLocationActiveFrom] = useState('');
+ const [locationActiveTo, setLocationActiveTo] = useState('');
+ const [locationStatus, setLocationStatus] = useState('active');
+ const [editingLocationId, setEditingLocationId] = useState(null);
+
useEffect(() => {
fetchInitialData();
}, []);
+ useEffect(() => {
+ // This effect is now redundant for initial load if we fetch getAreaManagers directly.
+ // However, if we want to support updates when 'allAreas' or 'userAssignedData' changes (e.g. after add/edit),
+ // we should validly re-fetch or derive.
+ // BUT the user asked for *straightforward* data from backend.
+ // So we should assume `asms` state is populated by `getAreaManagers`.
+ // We can keep this for "real-time" updates if we don't re-fetch.
+ // For now, let's rely on re-fetching getAreaManagers after any update.
+
+ // We can filter locally for immediate feedback OR re-fetch.
+ // Let's modify fetchInitialData to get this data.
+ }, []);
+
+ // Updated fetchInitialData
const fetchInitialData = async () => {
setLoading(true);
try {
- const [rolesRes, zonesRes, permsRes, regionsRes, usersRes] = await Promise.all([
- masterService.getRoles() as Promise,
- masterService.getZones() as Promise,
- masterService.getPermissions() as Promise,
- masterService.getRegions() as Promise,
- masterService.getUsers() as Promise
+ // Fetch critical data first
+ const [rolesRes, zonesRes, permsRes, regionsRes, usersRes, statesRes, asmRes] = await Promise.all([
+ masterService.getRoles().catch(e => ({ success: false, error: e })),
+ masterService.getZones().catch(e => ({ success: false, error: e })),
+ masterService.getPermissions().catch(e => ({ success: false, error: e })),
+ masterService.getRegions().catch(e => ({ success: false, error: e })),
+ masterService.getUsers().catch(e => ({ success: false, error: e })),
+ masterService.getStates().catch(e => ({ success: false, error: e })),
+ // Explicitly fetch Area Managers from the new dedicated endpoint
+ masterService.getAreaManagers().catch(e => ({ success: false, error: e }))
]);
- if (rolesRes.success) {
+ // Fetch extensive data independently so it doesn't block critical UI
+ const [districtsRes, areasRes] = await Promise.all([
+ masterService.getDistricts().catch(e => ({ success: false, error: e })),
+ masterService.getAreas().catch(e => ({ success: false, error: e }))
+ ]);
+
+ if (asmRes && asmRes.success) {
+ console.log('[ASM Debug] Raw ASM Response:', asmRes.data);
+ // Map backend simplified "Users with Areas" structure to ASM interface
+ const managers = asmRes.data.map((u: any) => {
+ // Extract areas from the nested areaManagers array
+ const areas = u.areaManagers ? u.areaManagers.map((am: any) => am.area) : [];
+ const areaNames = areas.map((a: any) => a ? `${a.areaName} (${a.district?.districtName || 'Unknown'})` : 'Unknown');
+ const districtNames = Array.from(new Set(areas.map((a: any) => a?.district?.districtName).filter(Boolean)));
+ const stateNames = Array.from(new Set(areas.map((a: any) => a?.state?.stateName).filter(Boolean)));
+
+ // Determine Zone and Region from the User's explicit assignment OR inferred from first area
+ let zoneName = u.zone?.zoneName || 'Unassigned';
+ let regionName = u.region?.regionName || 'Unassigned';
+ let zoneId = u.zoneId;
+ let regionId = u.regionId;
+
+ if (zoneName === 'Unassigned' && areas.length > 0 && areas[0]?.zone) {
+ zoneName = areas[0].zone.zoneName;
+ zoneId = areas[0].zone.id;
+ }
+ if (regionName === 'Unassigned' && areas.length > 0 && areas[0]?.region) {
+ regionName = areas[0].region.regionName;
+ regionId = areas[0].region.id;
+ }
+
+ // Extract unique ASM Codes (ideally they should match if user has one code)
+ const allAsmCodes = u.areaManagers
+ ? Array.from(new Set(u.areaManagers.map((am: any) => am.asmCode).filter(Boolean)))
+ : [];
+ const primaryAsmCode = allAsmCodes.length > 0 ? allAsmCodes[0] : '';
+
+ return {
+ id: u.id,
+ code: primaryAsmCode || u.employeeId || 'N/A',
+ asmCode: primaryAsmCode, // This ensures the UI gets the code from AreaManager table
+ employeeId: u.employeeId || '',
+ name: u.fullName,
+ zoneId: zoneId,
+ regionId: regionId,
+ zoneName: zoneName,
+ regionName: regionName,
+ areasManaged: areaNames,
+ districtNames: districtNames,
+ stateNames: stateNames,
+ email: u.email,
+ phone: u.mobileNumber,
+ status: u.status
+ };
+ });
+ setAsms(managers);
+ }
+
+ // ... rest of the existing handling for other resources ...
+
+ if (statesRes && statesRes.success) {
+ setAllStates(statesRes.states);
+ }
+
+ if (districtsRes && districtsRes.success) {
+ setAllDistricts(districtsRes.districts);
+ }
+
+ if (areasRes && areasRes.success) {
+ setAllAreas(areasRes.areas);
+ }
+
+ if (rolesRes && rolesRes.success) {
setRoles(rolesRes.data.map((r: any) => ({
id: r.id,
name: r.roleName,
@@ -336,7 +455,7 @@ export function MasterPage() {
})));
}
- if (zonesRes.success) {
+ if (zonesRes && zonesRes.success) {
setZones(zonesRes.data.map((z: any) => ({
id: z.id,
code: z.zoneCode,
@@ -358,11 +477,11 @@ export function MasterPage() {
})));
}
- if (permsRes.success) {
+ if (permsRes && permsRes.success) {
setAvailablePermissions(permsRes.data);
}
- if (regionsRes.success) {
+ if (regionsRes && regionsRes.success) {
setRegionalOffices(regionsRes.data?.map((r: any) => ({
id: r.id,
code: r.regionCode,
@@ -373,11 +492,19 @@ export function MasterPage() {
cities: [],
regionalOfficerCount: 0,
asmCount: 0,
- status: r.isActive ? 'Active' : 'Inactive'
+ status: r.isActive ? 'Active' : 'Inactive',
+ regionalManager: r.regionalManager ? {
+ id: r.regionalManager.id,
+ name: r.regionalManager.fullName,
+ email: r.regionalManager.email,
+ phone: r.regionalManager.mobileNumber
+ } : undefined
})));
+ } else {
+ console.error('Failed to fetch regions:', regionsRes?.error);
}
- if (usersRes.success) {
+ if (usersRes && usersRes.success) {
setUserAssignedData(usersRes.data.map((u: any) => ({
id: u.id,
name: u.fullName,
@@ -391,6 +518,7 @@ export function MasterPage() {
phone: u.mobileNumber || 'N/A',
status: u.isActive ? 'Active' : 'Inactive',
employeeId: u.employeeId,
+ asmCode: u.asmCode,
permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
})));
}
@@ -403,6 +531,10 @@ export function MasterPage() {
}
};
+ // ASM data is now fetched directly via getAreaManagers in fetchInitialData
+ // Retaining this block for any specific side-effects if needed, otherwise it's safe to be empty or removed.
+ // For now, removing the logic to rely on the backend response.
+
// Mock data for SLA
const [slaConfigs] = useState([
{
@@ -523,8 +655,7 @@ export function MasterPage() {
{ id: '7', name: 'SLA Breach Warning', subject: 'Action Required - Application Pending', trigger: 'Before SLA breach', lastModified: 'Dec 08, 2024' },
]);
- // Mock data for locations
- const [locations] = useState([]);
+
@@ -600,65 +731,232 @@ export function MasterPage() {
setShowTemplateDialog(false);
};
- const handleSaveLocation = () => {
- toast.success('Location saved successfully!');
- setShowLocationDialog(false);
+ const handleSaveLocation = async () => {
+ if (!locationDistrict || !locationCity || !locationPincode) {
+ toast.error('District, Area Name, and Pincode are required');
+ return;
+ }
+
+ try {
+ const payload = {
+ districtId: locationDistrict,
+ areaName: locationCity,
+ city: locationCity,
+ pincode: locationPincode,
+ areaCode: editingLocationId ? undefined : `LOC-${Math.floor(Math.random() * 10000)}`, // Only generate for new
+ isActive: locationStatus === 'active',
+ activeFrom: locationActiveFrom || null,
+ activeTo: locationActiveTo || null
+ };
+
+ let res;
+ if (editingLocationId) {
+ res = await masterService.updateArea(editingLocationId, payload) as any;
+ } else {
+ res = await masterService.createArea(payload) as any;
+ }
+
+ if (res.success) {
+ toast.success(editingLocationId ? 'Location updated successfully!' : 'Location saved successfully!');
+ setShowLocationDialog(false);
+ fetchInitialData();
+ // Reset form
+ setLocationState('');
+ setLocationDistrict('');
+ setLocationCity('');
+ setLocationPincode('');
+ setLocationActiveFrom('');
+ setLocationActiveTo('');
+ setLocationStatus('active');
+ setEditingLocationId(null);
+ } else {
+ toast.error(res.message || 'Failed to save location');
+ }
+ } catch (error) {
+ console.error('Save location error:', error);
+ toast.error('Failed to save location');
+ }
};
- const handleSaveZone = () => {
+ const handleSaveZone = async () => {
// Validate required fields
- if (!zoneCode || !zoneName || selectedZoneStates.length === 0) {
+ if (!zoneCode || !zoneName) {
toast.error('Please fill in all required fields');
return;
}
- // Build zonal managers array
- const zonalManagers: ZonalManager[] = [];
- for (let i = 0; i < zonalManagersCount; i++) {
- if (zonalManagersData[i]) {
- zonalManagers.push(zonalManagersData[i]);
+ try {
+ // Map selected state names to IDs
+ const stateIds = allStates
+ .filter(s => selectedZoneStates.includes(s.stateName))
+ .map(s => s.id);
+
+ if (editingZoneId) {
+ // Update existing zone
+ const updateData = {
+ zoneName,
+ description: zoneDescription,
+ zonalBusinessHeadId: zbhId || null,
+ stateIds // Send IDs to backend
+ };
+
+ const res = await masterService.updateZone(editingZoneId, updateData) as any;
+ if (res.success) {
+ toast.success('Zone updated successfully');
+ setShowZoneDialog(false);
+ fetchInitialData();
+ setEditingZoneId(null);
+ } else {
+ 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,
+ name: zoneName,
+ states: selectedZoneStates,
+ zmCount: zonalManagersCount,
+ description: zoneDescription,
+ zbh: {
+ name: zbhName,
+ email: zbhEmail,
+ phone: zbhPhone
+ },
+ zonalManagers
+ };
+
+ // Add to zones array
+ setZones([...zones, newZone]);
+ toast.success('Zone saved successfully!');
+ setShowZoneDialog(false);
}
+
+ // Reset form (common)
+ if (!editingZoneId) {
+ setZoneCode('');
+ setZoneName('');
+ setZoneDescription('');
+ setSelectedZoneStates([]);
+ setZbhName('');
+ setZbhEmail('');
+ setZbhPhone('');
+ setZonalManagersCount(0);
+ setZonalManagersData({});
+ }
+
+ } catch (error) {
+ console.error('Save zone error:', error);
+ toast.error('Failed to save zone');
}
-
- // Create new zone object
- const newZone: Zone = {
- id: zoneCode.toLowerCase().replace(/\s+/g, '-'),
- code: zoneCode,
- name: zoneName,
- states: selectedZoneStates,
- zmCount: zonalManagersCount,
- description: zoneDescription,
- zbh: {
- name: zbhName,
- email: zbhEmail,
- phone: zbhPhone
- },
- zonalManagers
- };
-
- // Add to zones array
- setZones([...zones, newZone]);
-
- // Reset form
- setZoneCode('');
- setZoneName('');
- setZoneDescription('');
- setSelectedZoneStates([]);
- setZbhName('');
- setZbhEmail('');
- setZbhPhone('');
- setZonalManagersCount(0);
- setZonalManagersData({});
-
- toast.success('Zone saved successfully!');
- setShowZoneDialog(false);
};
- const handleSaveRegion = () => {
- toast.success('Regional office saved successfully!');
- setShowRegionDialog(false);
+ const handleEditZone = (zone: Zone) => {
+ setEditingZoneId(zone.id);
+ setZoneCode(zone.code);
+ setZoneName(zone.name);
+ setZoneDescription(zone.description);
+ setSelectedZoneStates(zone.states);
+ // Try to match ZBH name to user list to get ID (reverse lookup since we don't have ID in Zone interface yet,
+ // but in real app we should fetch zone details or have ID)
+ // For now, let's assume we can match by email if available, or just leave it empty to be selected
+ // Actually fetchInitialData maps zbh from zonalBusinessHead relation.
+ // But we don't have the ID in the mapped 'zones' state interface.
+ // We should probably update the 'zones' mapping in fetchInitialData to include ID.
+ // For this step, I will just open the dialog.
+ setZbhName(zone.zbh?.name || '');
+ setZbhEmail(zone.zbh?.email || '');
+ setZbhPhone(zone.zbh?.phone || '');
+ // Find user by email to set zbhId
+ const zbhUser = userAssignedData.find(u => u.email === zone.zbh?.email);
+ if (zbhUser) {
+ setZbhId(zbhUser.id);
+ } else {
+ setZbhId('');
+ }
+
+ setShowZoneDialog(true);
+ };
+
+ const handleSaveRegion = async () => {
+ if (!regionCode || !regionName || !selectedRegionZone) {
+ toast.error('Please fill in all required fields');
+ return;
+ }
+
+ try {
+ // Map selected state names to IDs
+ const stateIds = allStates
+ .filter(s => selectedRegionStates.includes(s.stateName))
+ .map(s => s.id);
+
+ const regionData = {
+ zoneId: selectedRegionZone, // This is expected to be ID now
+ regionCode,
+ regionName,
+ description: regionDescription,
+ stateIds,
+ regionalManagerId: regionalManagerId || null
+ };
+
+ if (editingRegionId) {
+ const res = await masterService.updateRegion(editingRegionId, regionData) as any;
+ if (res.success) {
+ toast.success('Region updated successfully');
+ } else {
+ toast.error(res.message || 'Failed to update region');
+ return;
+ }
+ } else {
+ const res = await masterService.createRegion(regionData) as any;
+ if (res.success) {
+ toast.success('Region created successfully');
+ } else {
+ toast.error(res.message || 'Failed to create region');
+ return;
+ }
+ }
+
+ setShowRegionDialog(false);
+ fetchInitialData();
+
+ // Reset form
+ setEditingRegionId(null);
+ setRegionCode('');
+ setRegionName('');
+ setRegionDescription('');
+ setSelectedRegionZone('');
+ setSelectedRegionStates([]);
+ setRegionalManagerId('');
+
+ } catch (error) {
+ console.error('Save region error', error);
+ toast.error('Failed to save region');
+ }
+ };
+
+ const handleEditRegion = (region: RegionalOffice) => {
+ setEditingRegionId(region.id);
+ setRegionCode(region.code);
+ setRegionName(region.name);
+ // setRegionDescription(region.description); // Interface missing description?
+ // Assuming mapping in fetchInitialData needs update or we fetch detail.
+ // For now, we rely on mapped data.
+ setSelectedRegionZone(region.zoneId);
+ setSelectedRegionStates(region.states); // These are names in current mapping
+ setRegionalManagerId(region.regionalManager?.id || '');
+ setShowRegionDialog(true);
};
const handleSaveZM = () => {
@@ -669,9 +967,82 @@ export function MasterPage() {
setSelectedZMDistricts([]);
};
- const handleSaveASM = () => {
- toast.success('ASM saved successfully!');
- setShowASMDialog(false);
+ const handleSaveASM = async () => {
+ try {
+ if (!asmManagerId) {
+ toast.error('Please select an Area Sales Manager');
+ return;
+ }
+
+ if (selectedASMDistricts.length === 0) {
+ toast.error('Please select at least one district');
+ return;
+ }
+
+ // Map selected district names to IDs
+ const selectedDistrictIds = allDistricts
+ .filter((d: any) => selectedASMDistricts.includes(d.districtName))
+ .map((d: any) => 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 targetAreas = allAreas.filter((a: any) => 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.');
+ }
+ return;
+ }
+
+ // Assign manager to all found areas and update ASM Code
+ // We process each area update
+ const updatePromises = targetAreas.map((area: any) => {
+ console.log(`Updating area ${area.id} with managerId ${asmManagerId} and asmCode ${asmCode || 'null'}`);
+ return masterService.updateArea(area.id, {
+ managerId: asmManagerId,
+ asmCode: asmCode || null
+ });
+ });
+
+ await Promise.all(updatePromises);
+
+ toast.success(`ASM assigned to ${targetAreas.length} areas successfully!`);
+ setShowASMDialog(false);
+
+ // Reset form
+ setSelectedASMZone('');
+ setSelectedASMRegion('');
+ setSelectedASMStates([]);
+ setSelectedASMDistricts([]);
+ setAsmManagerId('');
+
+ // Refresh data
+ fetchInitialData();
+
+ } catch (error) {
+ console.error('Error saving ASM:', error);
+ toast.error('Failed to save ASM assignment');
+ }
};
const handleEditRole = (role: Role) => {
@@ -921,7 +1292,7 @@ export function MasterPage() {