@djb25/digit-ui-module-ekyc 1.0.11 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/index.js +1 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.modern.js +1338 -591
  4. package/dist/index.modern.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/Module.js +25 -7
  7. package/src/components/AadhaarVerification.js +415 -0
  8. package/src/components/AddressDetails.js +207 -0
  9. package/src/components/CeoDashboard.js +205 -0
  10. package/src/components/DesktopInbox.js +1 -1
  11. package/src/components/EKYCCard.js +4 -0
  12. package/src/components/MeterDetails.js +372 -0
  13. package/src/components/PropertyInfo.js +303 -0
  14. package/src/components/Review.js +572 -0
  15. package/src/components/analytics/charts/ClusterHeatmap.js +88 -0
  16. package/src/components/analytics/charts/TaskStatusChart.js +92 -0
  17. package/src/components/analytics/components/AnalyticsTable.js +106 -0
  18. package/src/components/analytics/components/DashboardLayout.js +72 -0
  19. package/src/components/analytics/components/EmptyState.js +27 -0
  20. package/src/components/analytics/components/ErrorBoundary.js +27 -0
  21. package/src/components/analytics/components/FilterBar.js +73 -0
  22. package/src/components/analytics/components/NotificationPanel.js +77 -0
  23. package/src/components/analytics/components/SLAWidget.js +56 -0
  24. package/src/components/analytics/components/SkeletonLoader.js +53 -0
  25. package/src/components/analytics/components/SummaryCard.js +74 -0
  26. package/src/components/analytics/components/WorkflowTimeline.js +55 -0
  27. package/src/components/analytics/styles/Dashboard.css +54 -0
  28. package/src/components/analytics/utils/exportUtils.js +64 -0
  29. package/src/components/analytics/utils/filterSerializer.js +50 -0
  30. package/src/config/config.js +1 -1
  31. package/src/pages/citizen/index.js +74 -18
  32. package/src/pages/employee/ConsumerDetails.js +10 -281
  33. package/src/pages/employee/Inbox.js +6 -4
  34. package/src/pages/employee/index.js +44 -8
  35. package/src/pages/employee/AadhaarVerification.js +0 -512
  36. package/src/pages/employee/AddressDetails.js +0 -548
  37. package/src/pages/employee/MeterDetails.js +0 -496
  38. package/src/pages/employee/PropertyInfo.js +0 -489
  39. package/src/pages/employee/Review.js +0 -314
@@ -0,0 +1,572 @@
1
+ import React, { useState } from "react";
2
+ import {
3
+ Card,
4
+ CardHeader,
5
+ CardSubHeader,
6
+ StatusTable,
7
+ Row,
8
+ SubmitBar,
9
+ Loader,
10
+ ActionBar,
11
+ CheckBox,
12
+ LinkButton,
13
+ EditIcon,
14
+ GenericFileIcon,
15
+ } from "@djb25/digit-ui-react-components";
16
+ import { useTranslation } from "react-i18next";
17
+ import { useHistory, useLocation } from "react-router-dom";
18
+
19
+
20
+ // ─── Constants ───────────────────────────────────────────────────────────────
21
+
22
+ const ActionButton = ({ jumpTo, state }) => {
23
+ const history = useHistory();
24
+ function routeTo() {
25
+ history.push(jumpTo, { ...state, isEditing: true });
26
+ }
27
+ return (
28
+ <LinkButton
29
+ label={<EditIcon style={{ width: "20px", height: "20px", fill: "#F47738" }} />}
30
+ style={{ margin: 0, padding: 0 }}
31
+ onClick={routeTo}
32
+ />
33
+ );
34
+ };
35
+
36
+ const checkForNA = (value) => (value !== null && value !== undefined && value !== "" ? value : "N/A");
37
+
38
+ const boolToYesNo = (value, t) => {
39
+ if (value === true || value === "true" || String(value).toLowerCase() === "yes") return t("CORE_COMMON_YES");
40
+ if (value === false || value === "false" || String(value).toLowerCase() === "no") return t("CORE_COMMON_NO");
41
+ if (value === "true") return t("CORE_COMMON_YES");
42
+ if (value === "false") return t("CORE_COMMON_NO");
43
+ return "N/A";
44
+ };
45
+
46
+ /**
47
+ * Robust data extraction for comparison.
48
+ * The API returns { applicationReviewInfo: { newData: { ... }, oldData: { ... } } }
49
+ */
50
+ const extractReviewData = (searchData, flowState) => {
51
+ const rawData = searchData && Object.keys(searchData).length > 0 ? searchData : flowState?.reviewData || {};
52
+
53
+ // Navigate through applicationReviewInfo -> newData/oldData
54
+ const reviewWrapper = rawData?.applicationReviewInfo || rawData?.applicationReview || rawData;
55
+ const applicationData = (Array.isArray(reviewWrapper) ? reviewWrapper[0] : reviewWrapper) || {};
56
+
57
+ return {
58
+ newData: applicationData?.newData || applicationData,
59
+ oldData: applicationData?.oldData || null
60
+ };
61
+ };
62
+
63
+ const ReviewSection = ({ title, fields, newData, oldData, t, jumpTo, state }) => {
64
+ return (
65
+ <div className="review-section-wrapper" style={{ marginBottom: "48px", background: "#fff", borderRadius: "12px", border: "1px solid #EAECF0", overflow: "hidden" }}>
66
+ <div style={{ padding: "20px 24px", background: "#F9FAFB", borderBottom: "1px solid #EAECF0", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
67
+ <CardSubHeader style={{ margin: 0, fontSize: "18px", color: "#101828", fontWeight: "700" }}>{title}</CardSubHeader>
68
+ {jumpTo && <ActionButton jumpTo={jumpTo} state={state} />}
69
+ </div>
70
+
71
+ <div style={{ padding: "0 24px" }}>
72
+ <table style={{ width: "100%", borderCollapse: "collapse", tableLayout: "fixed" }}>
73
+ <thead>
74
+ <tr>
75
+ <th style={{ padding: "16px 0", textAlign: "left", color: "#667085", fontSize: "12px", fontWeight: "600", textTransform: "uppercase", width: "30%" }}>{t("EKYC_FIELD_NAME")}</th>
76
+ <th style={{ padding: "16px 0", textAlign: "left", color: "#667085", fontSize: "12px", fontWeight: "600", textTransform: "uppercase", width: "35%" }}>{t("EKYC_EXISTING_INFORMATION")}</th>
77
+ <th style={{ padding: "16px 0", textAlign: "left", color: "#667085", fontSize: "12px", fontWeight: "600", textTransform: "uppercase", width: "35%" }}>{t("EKYC_PROPOSED_UPDATES")}</th>
78
+ </tr>
79
+ </thead>
80
+ <tbody>
81
+ {fields.map((field, idx) => {
82
+ const valNew = newData?.[field.key];
83
+ const valOld = oldData?.[field.key];
84
+ const isChanged = oldData && String(valNew) !== String(valOld) && valOld !== undefined && valOld !== null;
85
+
86
+ return (
87
+ <tr key={idx} style={{ borderTop: "1px solid #F2F4F7" }}>
88
+ <td style={{ padding: "16px 0", fontSize: "14px", color: "#344054", fontWeight: "500" }}>
89
+ {t(field.label)}
90
+ </td>
91
+ <td style={{ padding: "16px 0", fontSize: "14px", color: "#667085" }}>
92
+ {field.isBool ? boolToYesNo(valOld, t) : checkForNA(valOld)}
93
+ </td>
94
+ <td style={{ padding: "16px 0", fontSize: "14px", color: isChanged ? "#1B8B32" : "#101828", fontWeight: isChanged ? "700" : "400" }}>
95
+ <div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
96
+ {field.isBool ? boolToYesNo(valNew, t) : checkForNA(valNew)}
97
+ {isChanged && (
98
+ <span style={{ background: "#ECFDF3", color: "#067647", padding: "2px 8px", borderRadius: "12px", fontSize: "10px", fontWeight: "600" }}>
99
+ {t("EKYC_CHANGED")}
100
+ </span>
101
+ )}
102
+ </div>
103
+ </td>
104
+ </tr>
105
+ );
106
+ })}
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+ </div>
111
+ );
112
+ };
113
+
114
+ const Review = () => {
115
+
116
+ const { t } = useTranslation();
117
+ const history = useHistory();
118
+ const location = useLocation();
119
+
120
+ const [agree, setAgree] = useState(false);
121
+ const [isSubmitting, setIsSubmitting] = useState(false);
122
+ const [showPreview, setShowPreview] = useState(false);
123
+ const [previewUrl, setPreviewUrl] = useState("");
124
+
125
+ const flowState = location.state || {};
126
+ const { kNumber, kno, edits = {} } = flowState;
127
+ const activeKno = kNumber || kno;
128
+
129
+ const { aadhaarData = {}, addressDetails: editedAddress = {}, propertyDetails: editedProperty = {}, meterDetails: editedMeter = {} } = edits;
130
+
131
+ const tenantId = Digit.ULBService.getCurrentTenantId();
132
+ const workflowMutation = Digit.Hooks.ekyc.useEkycWorkflow(tenantId);
133
+ const updateMutation = Digit.Hooks.ekyc.useEkycUpdate(tenantId);
134
+
135
+ const { data: searchData, isLoading: isSearchLoading } = Digit.Hooks.ekyc.useEkycSearchReview({ kno: activeKno, fetchType: "REVIEW" }, tenantId, {
136
+ enabled: !!activeKno,
137
+ });
138
+
139
+ // ── Data Consolidation ──────────────────────────────────────────────────
140
+ const { newData: apiNewData, oldData: apiOldData } = extractReviewData(searchData, flowState);
141
+
142
+ const prepareConsolidatedData = (data) => {
143
+ if (!data) return null;
144
+ const apiConn = data?.connectionDetails || data || {};
145
+ const apiAddr = data?.addressDetails || data || {};
146
+ const apiProp = data?.propertyInfo || data || {};
147
+ const apiMeter = data?.meterDetails || data || {};
148
+
149
+ return {
150
+ connection: {
151
+ consumerName: apiConn?.consumerName || (apiConn?.firstName ? [apiConn.firstName, apiConn.middleName, apiConn.lastName].filter(Boolean).join(" ") : null),
152
+ address: apiConn?.address || apiConn?.addressRaw,
153
+ connectionType: apiConn?.connectionType || apiConn?.connectionCategory,
154
+ meterNumber: apiConn?.meterNumber || apiConn?.meterNo,
155
+ phoneNumber: apiConn?.phoneNumber || apiConn?.mobileNo || apiConn?.mobileNumber,
156
+ email: apiConn?.email,
157
+ statusflag: apiConn?.statusflag || apiConn?.statusFlag,
158
+ ekycStatus: apiConn?.ekycStatus,
159
+ knumber: apiConn?.knumber || apiConn?.kno,
160
+ },
161
+ address: {
162
+ fullAddress: apiAddr?.fullAddress || apiAddr?.addressRaw,
163
+ flatHouseNumber: apiAddr?.flatHouseNumber || apiAddr?.flatNo,
164
+ buildingTower: apiAddr?.buildingTower || apiAddr?.building,
165
+ landmark: apiAddr?.landmark,
166
+ pinCode: apiAddr?.pinCode || apiAddr?.pincode,
167
+ ward: apiAddr?.ward || apiAddr?.locality,
168
+ assembly: apiAddr?.assembly,
169
+ gpsValid: apiAddr?.gpsValid,
170
+ latitude: apiAddr?.latitude,
171
+ longitude: apiAddr?.longitude,
172
+ mobileNo: apiAddr?.mobileNo || apiAddr?.mobileNumber,
173
+ whatsappNo: apiAddr?.whatsappNo,
174
+ email: apiAddr?.email,
175
+ noOfPerson: apiAddr?.noOfPerson || apiAddr?.noOfPersons,
176
+ knumber: apiAddr?.knumber || apiAddr?.kno,
177
+ doorPhotoFilestoreId: apiAddr?.doorPhotoFilestoreId,
178
+ },
179
+ property: {
180
+ kno: apiProp?.kno,
181
+ pidNumber: apiProp?.pidNumber,
182
+ typeOfConnection: apiProp?.typeOfConnection,
183
+ connectionCategory: apiProp?.connectionCategory,
184
+ userType: apiProp?.userType,
185
+ numberOfFloors: apiProp?.numberOfFloors || apiProp?.noOfFloor,
186
+ tenantName: apiProp?.tenantName,
187
+ tenantMobile: apiProp?.tenantMobile,
188
+ ekycStatus: apiProp?.ekycStatus,
189
+ propertyDocumentFileStoreId: apiProp?.propertyDocumentFileStoreId,
190
+ buildingImageFileStoreId: apiProp?.buildingImageFileStoreId,
191
+ },
192
+ meter: {
193
+ kno: apiMeter?.kno,
194
+ metered: apiMeter?.meterStatus === "METERED" || apiMeter?.metered,
195
+ meterNumber: apiMeter?.meterNumber || apiMeter?.meterNo,
196
+ meterMake: apiMeter?.meterMake,
197
+ meterLocationAddress: apiMeter?.meterLocationAddress,
198
+ meterLatitude: apiMeter?.meterLatitude,
199
+ meterLongitude: apiMeter?.meterLongitude,
200
+ workingStatus: apiMeter?.workingStatus,
201
+ lastBillRaised: apiMeter?.lastBillRaised,
202
+ systemMeterId: apiMeter?.systemMeterId,
203
+ meterPhotoFileStoreId: apiMeter?.meterPhotoFileStoreId,
204
+ }
205
+ };
206
+ };
207
+
208
+ const newDataRaw = prepareConsolidatedData(apiNewData);
209
+ const oldDataRaw = prepareConsolidatedData(apiOldData);
210
+
211
+ // Apply edits to newData if present
212
+ const connectionData = {
213
+ ...newDataRaw?.connection,
214
+ consumerName: aadhaarData?.name || newDataRaw?.connection?.consumerName,
215
+ phoneNumber: aadhaarData?.mobileNumber || newDataRaw?.connection?.phoneNumber,
216
+ knumber: newDataRaw?.connection?.knumber || activeKno,
217
+ };
218
+
219
+ const addressData = {
220
+ ...newDataRaw?.address,
221
+ fullAddress: editedAddress?.fullAddress || newDataRaw?.address?.fullAddress,
222
+ flatHouseNumber: editedAddress?.flatHouseNumber || editedAddress?.flatNo || newDataRaw?.address?.flatHouseNumber,
223
+ buildingTower: editedAddress?.buildingTower || editedAddress?.building || newDataRaw?.address?.buildingTower,
224
+ landmark: editedAddress?.landmark || newDataRaw?.address?.landmark,
225
+ pinCode: editedAddress?.pinCode || editedAddress?.pincode || newDataRaw?.address?.pinCode,
226
+ ward: editedAddress?.ward || newDataRaw?.address?.ward,
227
+ assembly: editedAddress?.assembly || newDataRaw?.address?.assembly,
228
+ gpsValid: editedAddress?.gpsValid !== undefined ? editedAddress.gpsValid : newDataRaw?.address?.gpsValid,
229
+ latitude: editedAddress?.latitude || newDataRaw?.address?.latitude,
230
+ longitude: editedAddress?.longitude || newDataRaw?.address?.longitude,
231
+ mobileNo: editedAddress?.mobileNo || aadhaarData?.mobileNumber || newDataRaw?.address?.mobileNo,
232
+ whatsappNo: editedAddress?.whatsappNo || aadhaarData?.whatsappNumber || newDataRaw?.address?.whatsappNo,
233
+ email: editedAddress?.email || newDataRaw?.address?.email,
234
+ noOfPerson: editedAddress?.noOfPerson || aadhaarData?.noOfPersons || newDataRaw?.address?.noOfPerson,
235
+ knumber: editedAddress?.knumber || newDataRaw?.address?.knumber || activeKno,
236
+ doorPhotoFilestoreId: editedAddress?.doorPhotoFileStoreId || newDataRaw?.address?.doorPhotoFilestoreId,
237
+ };
238
+
239
+ const propertyData = {
240
+ ...newDataRaw?.property,
241
+ kno: newDataRaw?.property?.kno || activeKno,
242
+ pidNumber: editedProperty?.pidNumber || newDataRaw?.property?.pidNumber,
243
+ typeOfConnection: editedProperty?.connectionTypeData?.label || newDataRaw?.property?.typeOfConnection,
244
+ connectionCategory: editedProperty?.connectionCategoryData?.label || newDataRaw?.property?.connectionCategory,
245
+ userType: editedProperty?.userTypeData?.label || newDataRaw?.property?.userType,
246
+ numberOfFloors: editedProperty?.noOfFloorsData?.label || newDataRaw?.property?.numberOfFloors,
247
+ propertyDocumentFileStoreId: editedProperty?.propertyDocumentFileStoreId || newDataRaw?.property?.propertyDocumentFileStoreId,
248
+ };
249
+
250
+ const meterData = {
251
+ ...newDataRaw?.meter,
252
+ kno: editedMeter?.kno || newDataRaw?.meter?.kno || activeKno,
253
+ metered: editedMeter?.meterStatusData?.value === "Metered" || newDataRaw?.meter?.metered,
254
+ meterNumber: newDataRaw?.meter?.meterNumber,
255
+ meterMake: editedMeter?.meterMake || newDataRaw?.meter?.meterMake,
256
+ meterLocationAddress: editedMeter?.meterLocation || newDataRaw?.meter?.meterLocationAddress,
257
+ workingStatus: editedMeter?.workingStatusData?.value === "Working" || newDataRaw?.meter?.workingStatus,
258
+ lastBillRaised: editedMeter?.lastBillRaisedData?.value === "Yes" || newDataRaw?.meter?.lastBillRaised,
259
+ meterPhotoFileStoreId: editedMeter?.meterPhotoFileStoreId || newDataRaw?.meter?.meterPhotoFileStoreId,
260
+ };
261
+
262
+ // ── Section Fields Configuration ─────────────────────────────────────
263
+ const connectionFields = [
264
+ { label: "EKYC_K_NUMBER", key: "knumber" },
265
+ { label: "EKYC_CONSUMER_NAME", key: "consumerName" },
266
+ { label: "EKYC_ADDRESS", key: "address" },
267
+ { label: "EKYC_CONNECTION_TYPE", key: "connectionType" },
268
+ { label: "EKYC_METER_NO", key: "meterNumber" },
269
+ { label: "EKYC_MOBILE_NO", key: "phoneNumber" },
270
+ { label: "EKYC_EMAIL", key: "email" },
271
+ { label: "EKYC_STATUS_FLAG", key: "statusflag" },
272
+ { label: "EKYC_STATUS", key: "ekycStatus" },
273
+ ];
274
+
275
+ const addressFields = [
276
+ { label: "EKYC_FULL_ADDRESS", key: "fullAddress" },
277
+ { label: "EKYC_FLAT_HOUSE_NO", key: "flatHouseNumber" },
278
+ { label: "EKYC_BUILDING_TOWER", key: "buildingTower" },
279
+ { label: "EKYC_LANDMARK", key: "landmark" },
280
+ { label: "EKYC_PINCODE", key: "pinCode" },
281
+ { label: "EKYC_LOCALITY", key: "ward" },
282
+ { label: "EKYC_ASSEMBLY", key: "assembly" },
283
+ { label: "EKYC_GPS_VALID", key: "gpsValid", isBool: true },
284
+ { label: "EKYC_LATITUDE", key: "latitude" },
285
+ { label: "EKYC_LONGITUDE", key: "longitude" },
286
+ { label: "EKYC_MOBILE_NO", key: "mobileNo" },
287
+ { label: "EKYC_WHATSAPP_NO", key: "whatsappNo" },
288
+ { label: "EKYC_EMAIL", key: "email" },
289
+ { label: "EKYC_NO_OF_PERSONS", key: "noOfPerson" },
290
+ { label: "EKYC_K_NUMBER", key: "knumber" },
291
+ ];
292
+
293
+ const propertyFields = [
294
+ { label: "EKYC_CONNECTION_CATEGORY", key: "connectionCategory" },
295
+ { label: "EKYC_PID_NUMBER", key: "pidNumber" },
296
+ { label: "EKYC_TYPE_OF_CONNECTION", key: "typeOfConnection" },
297
+ { label: "EKYC_USER_TYPE", key: "userType" },
298
+ { label: "EKYC_FLOOR_COUNT", key: "numberOfFloors" },
299
+ { label: "EKYC_TENANT_NAME", key: "tenantName" },
300
+ { label: "EKYC_TENANT_MOBILE", key: "tenantMobile" },
301
+ { label: "EKYC_STATUS", key: "ekycStatus" },
302
+ ];
303
+
304
+ const meterFields = [
305
+ { label: "EKYC_METERED", key: "metered", isBool: true },
306
+ { label: "EKYC_METER_NO", key: "meterNumber" },
307
+ { label: "EKYC_METER_MAKE", key: "meterMake" },
308
+ { label: "EKYC_METER_LOCATION_ADDRESS", key: "meterLocationAddress" },
309
+ { label: "EKYC_METER_LATITUDE", key: "meterLatitude" },
310
+ { label: "EKYC_METER_LONGITUDE", key: "meterLongitude" },
311
+ { label: "EKYC_WORKING_STATUS", key: "workingStatus", isBool: true },
312
+ { label: "EKYC_LAST_BILL_RAISED", key: "lastBillRaised", isBool: true },
313
+ { label: "EKYC_SYSTEM_METER_ID", key: "systemMeterId" },
314
+ ];
315
+
316
+ const handleDeclaration = () => setAgree(!agree);
317
+
318
+ const handleReject = async () => {
319
+ const payload = {
320
+ RequestInfo: {
321
+ apiId: "Rainmaker",
322
+ ver: "1.0",
323
+ msgId: "message-id",
324
+ authToken: Digit.UserService.getUser()?.access_token,
325
+ },
326
+ kno: activeKno,
327
+ action: "REJECTED",
328
+ remarks: "Application rejected by reviewer",
329
+ reviewedBy: Digit.UserService.getUser()?.info?.userName,
330
+ role: "ZRO",
331
+ };
332
+ try {
333
+ const result = await workflowMutation.mutateAsync(payload);
334
+ if (result) {
335
+ history.push("/digit-ui/employee/ekyc/response", { success: true, result });
336
+ }
337
+ } catch (err) {
338
+ console.error("Reject Error:", err);
339
+ }
340
+ };
341
+
342
+ const handleApprove = async () => {
343
+ const payload = {
344
+ RequestInfo: {
345
+ apiId: "Rainmaker",
346
+ ver: "1.0",
347
+ msgId: "message-id",
348
+ authToken: Digit.UserService.getUser()?.access_token,
349
+ },
350
+ kno: activeKno,
351
+ action: "APPROVED",
352
+ remarks: "All documents verified",
353
+ reviewedBy: Digit.UserService.getUser()?.info?.userName,
354
+ role: "ZRO",
355
+ };
356
+ try {
357
+ const result = await workflowMutation.mutateAsync(payload);
358
+ if (result) {
359
+ history.push("/digit-ui/employee/ekyc/response", { success: true, result });
360
+ }
361
+ } catch (err) {
362
+ console.error("Approve Error:", err);
363
+ }
364
+ };
365
+
366
+ const handleFinalSubmit = async () => {
367
+ setIsSubmitting(true);
368
+ try {
369
+ const payload = {
370
+ kno: activeKno,
371
+ tenantId: tenantId,
372
+ newData: {
373
+ connectionDetails: connectionData,
374
+ addressDetails: addressData,
375
+ propertyInfo: propertyData,
376
+ meterDetails: meterData,
377
+ },
378
+ };
379
+
380
+ const result = await updateMutation.mutateAsync(payload);
381
+ if (result) {
382
+ history.push("/digit-ui/employee/ekyc/response", { success: true, result });
383
+ }
384
+ } catch (err) {
385
+ console.error("Submit Error:", err);
386
+ } finally {
387
+ setIsSubmitting(false);
388
+ }
389
+ };
390
+
391
+ const handleViewDocument = (fileStoreId) => {
392
+ if (!fileStoreId) return;
393
+ const documentUrl = `https://dev-djb.nitcon.in/filestore/v1/files/id?tenantId=dl.djb&fileStoreId=${fileStoreId}`;
394
+ setPreviewUrl(documentUrl);
395
+ setShowPreview(true);
396
+ };
397
+
398
+ if (isSearchLoading || isSubmitting) return <Loader />;
399
+
400
+ const baseUrl = "/digit-ui/employee/ekyc";
401
+
402
+ return (
403
+ <div className="employeeCard overflow-y-scroll">
404
+ <Card style={{ padding: "32px" }}>
405
+ {/* ── Header ───────────────────────────────────────────────────── */}
406
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "40px", borderBottom: "1px solid #EAECF0", paddingBottom: "20px" }}>
407
+ <CardHeader style={{ margin: 0, fontSize: "28px" }}>{t("EKYC_REVIEW_APPLICATION")}</CardHeader>
408
+ <div style={{
409
+ background: "#F9FAFB", border: "1px solid #EAECF0",
410
+ borderRadius: "24px", padding: "8px 20px",
411
+ fontSize: "14px", color: "#475467", fontWeight: "500"
412
+ }}>
413
+ {t("EKYC_K_NUMBER")}: <span style={{ color: "#101828", fontWeight: "700" }}>{activeKno}</span>
414
+ </div>
415
+ </div>
416
+
417
+ {/* ── 1. Connection Details ────────────────────────────────────── */}
418
+ <ReviewSection
419
+ title={t("EKYC_CONNECTION_DETAILS")}
420
+ fields={connectionFields}
421
+ newData={connectionData}
422
+ oldData={oldDataRaw?.connection}
423
+ t={t}
424
+ jumpTo={`${baseUrl}/consumer-details`}
425
+ state={{ ...flowState, reviewData: searchData, edits }}
426
+ />
427
+
428
+ {/* ── 2. Address Details ──────────────────────────────────────── */}
429
+ <ReviewSection
430
+ title={t("EKYC_ADDRESS_DETAILS")}
431
+ fields={addressFields}
432
+ newData={addressData}
433
+ oldData={oldDataRaw?.address}
434
+ t={t}
435
+ jumpTo={`${baseUrl}/address-details`}
436
+ state={{ ...flowState, reviewData: searchData, edits }}
437
+ />
438
+
439
+ {/* ── 3. Property Info ────────────────────────────────────────── */}
440
+ <ReviewSection
441
+ title={t("EKYC_PROPERTY_INFO")}
442
+ fields={propertyFields}
443
+ newData={propertyData}
444
+ oldData={oldDataRaw?.property}
445
+ t={t}
446
+ jumpTo={`${baseUrl}/property-info`}
447
+ state={{ ...flowState, reviewData: searchData, edits }}
448
+ />
449
+
450
+ {/* ── 4. Meter Details ────────────────────────────────────────── */}
451
+ <ReviewSection
452
+ title={t("EKYC_METER_DETAILS")}
453
+ fields={meterFields}
454
+ newData={meterData}
455
+ oldData={oldDataRaw?.meter}
456
+ t={t}
457
+ jumpTo={`${baseUrl}/meter-details`}
458
+ state={{ ...flowState, reviewData: searchData, edits }}
459
+ />
460
+
461
+ {/* ── 5. Documents ────────────────────────────────────────────── */}
462
+ <div style={{ marginTop: "40px" }}>
463
+ <CardSubHeader style={{ marginBottom: "20px" }}>{t("EKYC_DOCUMENTS")}</CardSubHeader>
464
+ <StatusTable style={{ maxWidth: "600px" }}>
465
+ <Row
466
+ label={t("EKYC_DOOR_PHOTO")}
467
+ text={addressData.doorPhotoFilestoreId ? t("EKYC_PHOTO_AVAILABLE") : t("CS_NA")}
468
+ actionButton={addressData.doorPhotoFilestoreId && (
469
+ <div onClick={() => handleViewDocument(addressData.doorPhotoFilestoreId)}>
470
+ <GenericFileIcon style={{ cursor: "pointer", fill: "#F47738" }} />
471
+ </div>
472
+ )}
473
+ />
474
+ <Row
475
+ label={t("EKYC_METER_PHOTO")}
476
+ text={meterData.meterPhotoFileStoreId ? t("EKYC_PHOTO_AVAILABLE") : t("CS_NA")}
477
+ actionButton={meterData.meterPhotoFileStoreId && (
478
+ <div onClick={() => handleViewDocument(meterData.meterPhotoFileStoreId)}>
479
+ <GenericFileIcon style={{ cursor: "pointer", fill: "#F47738" }} />
480
+ </div>
481
+ )}
482
+ />
483
+ <Row
484
+ label={t("EKYC_BUILDING_IMAGE")}
485
+ text={propertyData.buildingImageFileStoreId ? t("EKYC_PHOTO_AVAILABLE") : t("CS_NA")}
486
+ actionButton={propertyData.buildingImageFileStoreId && (
487
+ <div onClick={() => handleViewDocument(propertyData.buildingImageFileStoreId)}>
488
+ <GenericFileIcon style={{ cursor: "pointer", fill: "#F47738" }} />
489
+ </div>
490
+ )}
491
+ />
492
+ <Row
493
+ label={t("EKYC_PROPERTY_DOCUMENTS")}
494
+ text={propertyData.propertyDocumentFileStoreId ? t("EKYC_DOCS_AVAILABLE") : t("CS_NA")}
495
+ actionButton={propertyData.propertyDocumentFileStoreId && (
496
+ <div onClick={() => handleViewDocument(propertyData.propertyDocumentFileStoreId)}>
497
+ <GenericFileIcon style={{ cursor: "pointer", fill: "#F47738" }} />
498
+ </div>
499
+ )}
500
+ />
501
+ </StatusTable>
502
+ </div>
503
+
504
+ <div style={{ marginTop: "40px", paddingTop: "30px", borderTop: "1px solid #EAECF0" }}>
505
+ <CheckBox
506
+ id="agreeDeclaration"
507
+ name="agreeDeclaration"
508
+ label={<span style={{ fontSize: "16px", color: "#344054" }}>{t("EKYC_FINAL_DECLARATION")}</span>}
509
+ onChange={handleDeclaration}
510
+ checked={agree}
511
+ />
512
+ </div>
513
+ </Card>
514
+
515
+ <ActionBar style={{ position: "static", marginTop: "32px", display: "flex", justifyContent: "flex-end" }}>
516
+ <SubmitBar
517
+ label={t("EKYC_REJECT")}
518
+ onSubmit={handleReject}
519
+ disabled={!agree}
520
+ />
521
+ <SubmitBar
522
+ label={t("EKYC_APPROVE")}
523
+ onSubmit={handleApprove}
524
+ disabled={!agree}
525
+ />
526
+ <SubmitBar label={t("EKYC_SUBMIT_APPLICATION")} onSubmit={handleFinalSubmit} disabled={!agree} />
527
+ </ActionBar>
528
+
529
+ {/* ── Document Preview Modal ────────────────────────────────────── */}
530
+ {showPreview && (
531
+ <div style={{
532
+ position: "fixed", top: 0, left: 0, width: "100%", height: "100%",
533
+ backgroundColor: "rgba(0,0,0,0.7)", display: "flex", justifyContent: "center",
534
+ alignItems: "center", zIndex: 10000
535
+ }}>
536
+ <div style={{
537
+ position: "relative", backgroundColor: "#fff", padding: "30px",
538
+ borderRadius: "12px", maxWidth: "600px", width: "100%", maxHeight: "70vh", overflow: "auto",
539
+ boxShadow: "0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)"
540
+ }}>
541
+ <div
542
+ onClick={() => setShowPreview(false)}
543
+ style={{
544
+ position: "absolute", top: "10px", right: "10px", cursor: "pointer",
545
+ fontWeight: "bold", fontSize: "20px", color: "#333", background: "#eee",
546
+ width: "30px", height: "30px", display: "flex", justifyContent: "center",
547
+ alignItems: "center", borderRadius: "50%"
548
+ }}
549
+ >
550
+ ×
551
+ </div>
552
+ <img
553
+ src={previewUrl}
554
+ alt="Document Preview"
555
+ style={{ maxWidth: "100%", height: "auto", display: "block" }}
556
+ onError={(e) => {
557
+ e.target.style.display = "none";
558
+ e.target.nextSibling.style.display = "block";
559
+ }}
560
+ />
561
+ <div style={{ display: "none", padding: "40px", textAlign: "center" }}>
562
+ <p>{t("EKYC_DOCUMENT_PREVIEW_NOT_AVAILABLE")}</p>
563
+ <LinkButton label={t("EKYC_OPEN_IN_NEW_TAB")} onClick={() => window.open(previewUrl, "_blank")} />
564
+ </div>
565
+ </div>
566
+ </div>
567
+ )}
568
+ </div>
569
+ );
570
+ };
571
+
572
+ export default Review;
@@ -0,0 +1,88 @@
1
+ import React from "react";
2
+ import { useTranslation } from "react-i18next";
3
+
4
+ /**
5
+ * Custom SVG-based Heatmap component for Cluster Workload monitoring.
6
+ */
7
+ const ClusterHeatmap = ({ data, title, onDrillDown }) => {
8
+ const { t } = useTranslation();
9
+
10
+ if (!data || data.length === 0) return null;
11
+
12
+ const getIntensityColor = (score) => {
13
+ if (score > 80) return "#EF4444"; // High - Red
14
+ if (score > 50) return "#F59E0B"; // Medium - Amber
15
+ return "#10B981"; // Low - Emerald
16
+ };
17
+
18
+ return (
19
+ <div className="cluster-heatmap-container glass-card" style={{ padding: "28px" }}>
20
+ {title && <h3 className="chart-title" style={{ marginBottom: "28px", fontSize: "18px", fontWeight: "800", color: "#111827", letterSpacing: "-0.025em" }}>{t(title)}</h3>}
21
+ <div className="heatmap-grid" style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))", gap: "20px" }}>
22
+ {data.map((cluster) => (
23
+ <div
24
+ key={cluster.clusterId}
25
+ className="heatmap-card glass-card"
26
+ onClick={() => onDrillDown?.(cluster)}
27
+ style={{
28
+ padding: "20px",
29
+ background: "#FFFFFF",
30
+ border: `1px solid ${getIntensityColor(cluster.intensityScore)}40`,
31
+ cursor: "pointer",
32
+ position: "relative",
33
+ overflow: "hidden"
34
+ }}
35
+ >
36
+ <div className="intensity-bar" style={{
37
+ position: "absolute",
38
+ top: 0,
39
+ left: 0,
40
+ height: "4px",
41
+ width: "100%",
42
+ background: getIntensityColor(cluster.intensityScore)
43
+ }} />
44
+
45
+ <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", marginBottom: "12px" }}>
46
+ <span style={{ fontWeight: "600", fontSize: "14px", color: "#374151" }}>{cluster.clusterName}</span>
47
+ <span style={{
48
+ fontSize: "12px",
49
+ fontWeight: "700",
50
+ padding: "2px 8px",
51
+ borderRadius: "12px",
52
+ background: `${getIntensityColor(cluster.intensityScore)}20`,
53
+ color: getIntensityColor(cluster.intensityScore)
54
+ }}>
55
+ {cluster.intensityScore}%
56
+ </span>
57
+ </div>
58
+
59
+ <div className="cluster-stats" style={{ display: "flex", flexDirection: "column", gap: "8px" }}>
60
+ <div style={{ display: "flex", justifyContent: "space-between", fontSize: "12px" }}>
61
+ <span style={{ color: "#6B7280" }}>{t("EKYC_PENDING_WORKLOAD")}</span>
62
+ <span style={{ fontWeight: "600", color: "#1F2937" }}>{cluster.pendingWorkload}</span>
63
+ </div>
64
+ <div style={{ display: "flex", justifyContent: "space-between", fontSize: "12px" }}>
65
+ <span style={{ color: "#6B7280" }}>{t("EKYC_ACTIVE_AGENCIES")}</span>
66
+ <span style={{ fontWeight: "600", color: "#1F2937" }}>{cluster.activeAgencies}</span>
67
+ </div>
68
+ </div>
69
+
70
+ <div className="mini-spark" style={{ marginTop: "12px", display: "flex", gap: "2px", alignItems: "flex-end", height: "20px" }}>
71
+ {cluster.wards.map((ward, idx) => (
72
+ <div key={idx} style={{
73
+ flex: 1,
74
+ height: `${Math.min(100, (ward.pendingCount / cluster.pendingWorkload) * 100)}%`,
75
+ background: getIntensityColor(cluster.intensityScore),
76
+ opacity: 0.6,
77
+ borderRadius: "1px"
78
+ }} />
79
+ ))}
80
+ </div>
81
+ </div>
82
+ ))}
83
+ </div>
84
+ </div>
85
+ );
86
+ };
87
+
88
+ export default ClusterHeatmap;