diff --git a/utilities/crs_dataloader/ui-mockup/src/api/config.ts b/utilities/crs_dataloader/ui-mockup/src/api/config.ts index 1b3c7e7fc..b72f35c5b 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/config.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/config.ts @@ -53,7 +53,7 @@ export const MDMS_SCHEMAS = { EMPLOYEE_STATUS: 'egov-hrms.EmployeeStatus', EMPLOYEE_TYPE: 'egov-hrms.EmployeeType', ROLES: 'ACCESSCONTROL-ROLES.roles', - PGR_SERVICE_DEFS: 'RAINMAKER_PGR.ServiceDefs', + PGR_SERVICE_DEFS: 'RAINMAKER-PGR.ServiceDefs', TENANT: 'tenant.tenants', }; diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts index c26d51b8f..2ab2313d5 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/boundary.ts @@ -23,7 +23,9 @@ export const boundaryService = { return hierarchies as BoundaryHierarchy[]; }, - // Create a new boundary hierarchy + // Create a new boundary hierarchy. + // Note: the backend returns BoundaryHierarchy as an ARRAY even though the create + // payload sends a single object — unwrap the first element. async createHierarchy( tenantId: string, hierarchyType: string, @@ -38,7 +40,9 @@ export const boundaryService = { }, }); - return response.BoundaryHierarchy as BoundaryHierarchy; + const raw = response.BoundaryHierarchy; + if (Array.isArray(raw)) return raw[0] as BoundaryHierarchy; + return raw as BoundaryHierarchy; }, // Helper to create hierarchy from level names @@ -60,7 +64,12 @@ export const boundaryService = { // Boundary Methods // ============================================ - // Search boundaries + // Search boundaries — returns the hierarchical tree flattened to a list. + // + // Uses /boundary-service/boundary-relationships/_search (not /boundary/_search) because + // the latter searches boundary *entities* by code and doesn't return children; only + // the relationships endpoint walks the hierarchy. Query params (not body) are how this + // endpoint accepts its filters. async searchBoundaries( tenantId: string, options?: { @@ -71,33 +80,45 @@ export const boundaryService = { offset?: number; } ): Promise { - const response = await apiClient.post(ENDPOINTS.BOUNDARY_SEARCH, { - RequestInfo: apiClient.buildRequestInfo(), - Boundary: { - tenantId, - hierarchyType: options?.hierarchyType, - boundaryType: options?.boundaryType, - codes: options?.codes, - limit: options?.limit || 100, - offset: options?.offset || 0, - }, - }); + const qs = new URLSearchParams({ tenantId, includeChildren: 'true' }); + if (options?.hierarchyType) qs.set('hierarchyType', options.hierarchyType); + if (options?.boundaryType) qs.set('boundaryType', options.boundaryType); + if (options?.codes?.length) qs.set('codes', options.codes.join(',')); + + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_RELATIONSHIP_SEARCH}?${qs.toString()}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); // Flatten the nested boundary structure const tenantBoundaries = response.TenantBoundary || []; const boundaries: Boundary[] = []; + const seen = new Set(); - for (const tb of tenantBoundaries as { boundary: Boundary }[]) { - if (tb.boundary) { - this.flattenBoundaries(tb.boundary, boundaries); + for (const tb of tenantBoundaries as { boundary: Boundary | Boundary[]; hierarchyType?: string }[]) { + if (!tb.boundary) continue; + const items = Array.isArray(tb.boundary) ? tb.boundary : [tb.boundary]; + for (const root of items) { + this.flattenBoundaries(root, boundaries, seen, tb.hierarchyType); } } return boundaries; }, - // Helper to flatten nested boundary tree - flattenBoundaries(boundary: Boundary, result: Boundary[]): void { + // Helper to flatten nested boundary tree. Dedupes by code because the relationships + // endpoint duplicates children under their parent in the response payload. + flattenBoundaries( + boundary: Boundary, + result: Boundary[], + seen?: Set, + hierarchyType?: string, + ): void { + const code = boundary.code; + if (seen && code) { + if (seen.has(code)) return; + seen.add(code); + } result.push({ id: boundary.id, tenantId: boundary.tenantId, @@ -105,19 +126,24 @@ export const boundaryService = { name: boundary.name, boundaryType: boundary.boundaryType, parent: boundary.parent, - hierarchyType: boundary.hierarchyType, + hierarchyType: boundary.hierarchyType ?? hierarchyType, latitude: boundary.latitude, longitude: boundary.longitude, }); if (boundary.children) { for (const child of boundary.children) { - this.flattenBoundaries(child, result); + this.flattenBoundaries(child, result, seen, hierarchyType); } } }, - // Create a boundary entity (just the entity, not the relationship) + // Create a boundary entity (just the entity, not the relationship). + // If the backend reports "already exists", verify the entity actually lives in the + // DB before swallowing the error — the boundary service occasionally returns a + // false-positive "already exists" when a prior request sits in its cache layer + // even though the row isn't persisted, which would otherwise make Phase 2 + // silently succeed and leave nothing for Phase 4 to find. async createBoundaryEntity(tenantId: string, code: string): Promise { try { await apiClient.post(ENDPOINTS.BOUNDARY_CREATE, { @@ -130,16 +156,35 @@ export const boundaryService = { }); return true; } catch (error) { - // Check if already exists (which is OK) const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.toLowerCase().includes('already exists') || errorMsg.includes('DUPLICATE')) { - return true; + // Verify it actually exists before swallowing. + const found = await this.boundaryEntityExists(tenantId, code); + if (found) return true; + throw new Error( + `Backend reported boundary entity ${code} already exists, but a search returned nothing. ` + + `Retry may be needed, or a stale cache/Kafka state is masking the real error.` + ); } throw error; } }, - // Create a boundary relationship (parent-child link in hierarchy) + async boundaryEntityExists(tenantId: string, code: string): Promise { + try { + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_SEARCH}?tenantId=${encodeURIComponent(tenantId)}&codes=${encodeURIComponent(code)}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); + return (response.Boundary?.length ?? 0) > 0; + } catch { + return false; + } + }, + + // Create a boundary relationship (parent-child link in hierarchy). + // Same verify-before-swallow pattern as createBoundaryEntity — the backend + // sometimes returns "already exists" for relationships that never persisted. async createBoundaryRelationship( tenantId: string, hierarchyType: string, @@ -165,15 +210,33 @@ export const boundaryService = { await apiClient.post(ENDPOINTS.BOUNDARY_RELATIONSHIP_CREATE, payload); return true; } catch (error) { - // Check if already exists (which is OK) const errorMsg = error instanceof Error ? error.message : String(error); if (errorMsg.toLowerCase().includes('already exists') || errorMsg.includes('DUPLICATE')) { - return true; + const found = await this.boundaryRelationshipExists(tenantId, hierarchyType, code); + if (found) return true; + throw new Error( + `Backend reported relationship ${code} (${hierarchyType}) already exists, ` + + `but a search returned nothing. Likely a stale cache — try again in a few seconds ` + + `or clean up and re-run.` + ); } throw error; } }, + async boundaryRelationshipExists(tenantId: string, hierarchyType: string, code: string): Promise { + try { + const qs = new URLSearchParams({ tenantId, hierarchyType, codes: code, includeChildren: 'false' }); + const response = await apiClient.post( + `${ENDPOINTS.BOUNDARY_RELATIONSHIP_SEARCH}?${qs.toString()}`, + { RequestInfo: apiClient.buildRequestInfo() }, + ); + return (response.TenantBoundary?.length ?? 0) > 0; + } catch { + return false; + } + }, + // Create a single boundary (entity + relationship) async createBoundary(boundary: Boundary): Promise { // Step 1: Create the boundary entity diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts index 709cf372f..d606382c1 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/hrms.ts @@ -101,6 +101,7 @@ export const hrmsService = { mobileNumber: string; emailId?: string; gender?: string; + dob?: number; department: string; designation: string; roles: Role[]; @@ -109,6 +110,9 @@ export const hrmsService = { password?: string; }): Employee { const now = Date.now(); + // HRMS @NotNull on user.dob. Default to 1990-01-01 UTC when the form/sheet + // didn't provide one, since HRMS otherwise rejects the whole create. + const dob = data.dob ?? Date.UTC(1990, 0, 1); const user: EmployeeUser = { userName: data.userName.toLowerCase(), @@ -117,6 +121,7 @@ export const hrmsService = { mobileNumber: data.mobileNumber, emailId: data.emailId, gender: data.gender, + dob, type: 'EMPLOYEE', active: true, tenantId: data.tenantId, diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts index 8d2caaab4..f9db7ce20 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/localization.ts @@ -115,19 +115,19 @@ export const localizationService = { buildComplaintTypeLocalizations( _tenantId: string, serviceCode: string, - serviceName: string, + name: string, locale: string = 'en_IN' ): LocalizationMessage[] { return [ { code: `SERVICEDEFS.${serviceCode}`, - message: serviceName, + message: name, module: 'rainmaker-pgr', locale, }, { code: `SERVICEDEFS.${serviceCode.toUpperCase()}`, - message: serviceName, + message: name, module: 'rainmaker-pgr', locale, }, @@ -201,11 +201,11 @@ export const localizationService = { // Upload localizations for all complaint types async uploadComplaintTypeLocalizations( tenantId: string, - types: { serviceCode: string; serviceName: string }[], + types: { serviceCode: string; name: string }[], locale: string = 'en_IN' ): Promise<{ success: number; failed: number }> { const messages = types.flatMap((t) => - this.buildComplaintTypeLocalizations(tenantId, t.serviceCode, t.serviceName, locale) + this.buildComplaintTypeLocalizations(tenantId, t.serviceCode, t.name, locale) ); return this.upsertMessages(tenantId, locale, messages); }, diff --git a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts index 62a9bbad7..357e88a15 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/services/mdms.ts @@ -102,6 +102,7 @@ export const mdmsService = { return this.create(tenantId, MDMS_SCHEMAS.DESIGNATION, designation.code, { code: designation.code, name: designation.name, + description: designation.description, department: designation.department, active: designation.active, }); @@ -141,7 +142,8 @@ export const mdmsService = { return results.map((r) => ({ serviceCode: r.serviceCode as string, - serviceName: r.serviceName as string, + name: r.name as string, + keywords: (r.keywords as string) || '', department: r.department as string, slaHours: r.slaHours as number, menuPath: r.menuPath as string | undefined, @@ -160,7 +162,8 @@ export const mdmsService = { complaintType.serviceCode, { serviceCode: complaintType.serviceCode, - serviceName: complaintType.serviceName, + name: complaintType.name, + keywords: complaintType.keywords, department: complaintType.department, slaHours: complaintType.slaHours, menuPath: complaintType.menuPath || 'Complaint', @@ -215,8 +218,11 @@ export const mdmsService = { }, async createTenant(stateTenantId: string, tenant: Tenant): Promise { - // Build the full tenant data structure matching MDMS schema + // Build the full tenant data structure matching MDMS schema. + // tenant.tenants schema requires `tenantId` inside data (in addition to the + // Mdms.tenantId wrapper) — it stores the parent/root tenant this city lives under. const tenantData = { + tenantId: stateTenantId, code: tenant.code, name: tenant.name, type: tenant.city?.ulbGrade || 'CITY', diff --git a/utilities/crs_dataloader/ui-mockup/src/api/types.ts b/utilities/crs_dataloader/ui-mockup/src/api/types.ts index 2683effd9..5597ddb14 100644 --- a/utilities/crs_dataloader/ui-mockup/src/api/types.ts +++ b/utilities/crs_dataloader/ui-mockup/src/api/types.ts @@ -137,7 +137,8 @@ export interface Department { export interface Designation { code: string; name: string; - department?: string; + description: string; + department?: string[]; active: boolean; tenantId?: string; } @@ -145,7 +146,8 @@ export interface Designation { // Complaint Type / Service Definition export interface ComplaintType { serviceCode: string; - serviceName: string; + name: string; + keywords: string; department: string; slaHours: number; menuPath?: string; @@ -370,13 +372,15 @@ export interface DepartmentExcelRow { export interface DesignationExcelRow { code: string; name: string; - department?: string; + description: string; + department?: string[]; active: boolean; } export interface ComplaintTypeExcelRow { serviceCode: string; - serviceName: string; + name: string; + keywords: string; department: string; slaHours: number; active: boolean; @@ -389,6 +393,7 @@ export interface EmployeeExcelRow { mobileNumber: string; emailId?: string; gender?: string; + dob?: string; department: string; designation: string; roles: string; // comma-separated diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx index da422a347..c4f23653b 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase3Page.tsx @@ -137,15 +137,18 @@ export default function Phase3Page() { // Create designations if (designations.length > 0) { setProgressMessage('Creating designations...'); - const desigResults = await mdmsService.createDesignations( - state.tenant, - designations.map(d => ({ - code: d.code, - name: d.name, - department: d.department, - active: d.active, - })) - ); + const payload = designations.map(d => ({ + code: d.code, + name: d.name, + description: d.description, + department: d.department, + active: d.active, + })); + // Temporary diagnostic while chasing the "description not found" 400s: + // logs the exact object handed to the service. Remove once the flow is stable. + // eslint-disable-next-line no-console + console.log('[configurator] designation payload ->', JSON.stringify(payload)); + const desigResults = await mdmsService.createDesignations(state.tenant, payload); setCreatedDesigs(desigResults.success.length); // Create localizations for designations @@ -171,7 +174,8 @@ export default function Phase3Page() { state.tenant, complaintTypes.map(ct => ({ serviceCode: ct.serviceCode, - serviceName: ct.serviceName, + name: ct.name, + keywords: ct.keywords, department: ct.department, slaHours: ct.slaHours, active: ct.active, @@ -184,7 +188,7 @@ export default function Phase3Page() { state.tenant, complaintTypes.map(ct => ({ serviceCode: ct.serviceCode, - serviceName: ct.serviceName, + name: ct.name, })), 'en_IN' ); @@ -491,7 +495,7 @@ export default function Phase3Page() { {type.serviceCode} - {type.serviceName} + {type.name} {type.slaHours}h {type.department} @@ -585,7 +589,7 @@ export default function Phase3Page() { ) : ( )} - {type.serviceCode} - {type.serviceName} + {type.serviceCode} - {type.name} ))} {complaintTypes.length > 5 && ( diff --git a/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx b/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx index 01e474625..25e37c542 100644 --- a/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx +++ b/utilities/crs_dataloader/ui-mockup/src/pages/Phase4Page.tsx @@ -243,6 +243,7 @@ export default function Phase4Page() { mobileNumber: emp.mobileNumber, emailId: emp.emailId, gender: emp.gender, + dob: emp.dob ? new Date(emp.dob).getTime() : undefined, department: emp.department, designation: emp.designation, roles, diff --git a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts index 133861601..dfecd8d80 100644 --- a/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts +++ b/utilities/crs_dataloader/ui-mockup/src/utils/excelParser.ts @@ -521,7 +521,9 @@ export function parseDesignationExcel(workbook: XLSX.WorkBook): { jsonData.forEach((row, index) => { const code = String(row['code'] || row['Code'] || row['designationCode'] || '').trim(); const name = String(row['name'] || row['Name'] || row['designationName'] || '').trim(); - const department = String(row['department'] || row['Department'] || '').trim() || undefined; + const description = String(row['description'] || row['Description'] || '').trim() || name; + const deptRaw = String(row['department'] || row['Department'] || '').trim(); + const department = deptRaw ? deptRaw.split(',').map(s => s.trim()).filter(Boolean) : undefined; const activeStr = String(row['active'] || row['Active'] || row['isActive'] || 'true').trim().toLowerCase(); const active = activeStr === 'true' || activeStr === 'yes' || activeStr === '1'; @@ -545,7 +547,7 @@ export function parseDesignationExcel(workbook: XLSX.WorkBook): { return; } - designations.push({ code, name, department, active }); + designations.push({ code, name, description, department, active }); }); return { @@ -597,7 +599,9 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { jsonData.forEach((row, index) => { const serviceCode = String(row['serviceCode'] || row['ServiceCode'] || row['code'] || '').trim(); - const serviceName = String(row['serviceName'] || row['ServiceName'] || row['name'] || '').trim(); + const name = String(row['name'] || row['Name'] || row['serviceName'] || row['ServiceName'] || '').trim(); + const keywordsRaw = String(row['keywords'] || row['Keywords'] || '').trim(); + const keywords = keywordsRaw || name.toLowerCase().replace(/\s+/g, ','); const department = String(row['department'] || row['Department'] || '').trim(); const slaHours = parseInt(String(row['slaHours'] || row['SlaHours'] || row['sla'] || '24'), 10) || 24; const activeStr = String(row['active'] || row['Active'] || row['isActive'] || 'true').trim().toLowerCase(); @@ -613,10 +617,10 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { return; } - if (!serviceName) { + if (!name) { errors.push({ row: index + 2, - field: 'serviceName', + field: 'name', message: 'Service name is required', code: 'REQUIRED_FIELD', }); @@ -633,7 +637,7 @@ export function parseComplaintTypeExcel(workbook: XLSX.WorkBook): { return; } - complaintTypes.push({ serviceCode, serviceName, department, slaHours, active }); + complaintTypes.push({ serviceCode, name, keywords, department, slaHours, active }); }); return { @@ -690,6 +694,7 @@ export function parseEmployeeExcel(workbook: XLSX.WorkBook): { const mobileNumber = String(row['mobileNumber'] || row['MobileNumber'] || row['mobile'] || row['phone'] || '').trim(); const emailId = String(row['emailId'] || row['EmailId'] || row['email'] || '').trim() || undefined; const gender = String(row['gender'] || row['Gender'] || '').trim() || undefined; + const dob = String(row['dob'] || row['DOB'] || row['dateOfBirth'] || '').trim() || undefined; const department = String(row['department'] || row['Department'] || '').trim(); const designation = String(row['designation'] || row['Designation'] || '').trim(); const roles = String(row['roles'] || row['Roles'] || row['role'] || 'EMPLOYEE').trim(); @@ -779,6 +784,7 @@ export function parseEmployeeExcel(workbook: XLSX.WorkBook): { mobileNumber, emailId, gender, + dob, department, designation, roles,