@djb25/digit-ui-module-ekyc 1.0.13 → 1.0.14
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.
- package/dist/index.css +54 -1
- package/dist/index.js +1 -1
- package/package.json +34 -29
- package/src/Module.js +2 -5
- package/src/components/CeoDashboard.js +255 -192
- package/src/components/CeoDashboard.jsx +334 -0
- package/src/components/DesktopInbox.js +8 -0
- package/src/components/EKYCCard.js +5 -1
- package/src/components/VendorDetails.jsx +214 -0
- package/src/components/analytics/charts/ExecutiveBarChart.jsx +30 -0
- package/src/components/analytics/charts/ExecutiveLineChart.jsx +159 -0
- package/src/components/analytics/charts/ExecutivePieChart.jsx +24 -0
- package/src/components/analytics/charts/TaskStatusChart.js +1 -3
- package/src/components/analytics/components/DashboardLayout.js +26 -46
- package/src/components/mockData.js +772 -0
- package/src/hook/useInboxTableConfig.js +10 -2
- package/src/pages/employee/Mapping.js +162 -449
- package/src/pages/employee/index.js +19 -2
- package/src/components/analytics/styles/Dashboard.css +0 -54
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import React, { useMemo, useState } from "react";
|
|
2
|
+
import { useHistory, useLocation } from "react-router-dom";
|
|
3
|
+
import {
|
|
4
|
+
FaChartLine,
|
|
5
|
+
FaUsers,
|
|
6
|
+
FaCheckCircle,
|
|
7
|
+
FaClock,
|
|
8
|
+
FaExclamationTriangle,
|
|
9
|
+
FaUserTie,
|
|
10
|
+
FaMapMarkedAlt,
|
|
11
|
+
FaSearch,
|
|
12
|
+
FaArrowRight,
|
|
13
|
+
} from "react-icons/fa";
|
|
14
|
+
import { ekycMockData } from "./mockData";
|
|
15
|
+
import ExecutiveLineChart from "./analytics/charts/ExecutiveLineChart";
|
|
16
|
+
import ExecutiveBarChart from "./analytics/charts/ExecutiveBarChart";
|
|
17
|
+
import ExecutivePieChart from "./analytics/charts/ExecutivePieChart";
|
|
18
|
+
|
|
19
|
+
const CeoDashboard = () => {
|
|
20
|
+
const history = useHistory();
|
|
21
|
+
const location = useLocation();
|
|
22
|
+
const [query, setQuery] = useState("");
|
|
23
|
+
const [statusFilter, setStatusFilter] = useState("All");
|
|
24
|
+
|
|
25
|
+
const basePath = location.pathname.replace(/\/ceo-dashboard$/, "");
|
|
26
|
+
const filters = ["All", "High Priority", "Stable", "Watchlist"];
|
|
27
|
+
|
|
28
|
+
const vendors = useMemo(() => ekycMockData.vendors || [], []);
|
|
29
|
+
const topKpis = useMemo(() => ekycMockData.topKpis || {}, []);
|
|
30
|
+
const dailyTrend = useMemo(() => ekycMockData.dailyTrend || [], []);
|
|
31
|
+
const selfVsAgency = useMemo(() => ekycMockData.selfVsAgency || [], []);
|
|
32
|
+
const pendingVsCompleted = useMemo(() => ekycMockData.pendingVsCompleted || [], []);
|
|
33
|
+
// const rejectionAnalysis = ekycMockData.rejectionAnalysis || [];
|
|
34
|
+
|
|
35
|
+
const filteredVendors = useMemo(() => {
|
|
36
|
+
return vendors
|
|
37
|
+
.filter((vendor) => {
|
|
38
|
+
if (statusFilter === "High Priority") return vendor.successRate < 70;
|
|
39
|
+
if (statusFilter === "Watchlist") return vendor.pending > 35000;
|
|
40
|
+
if (statusFilter === "Stable") return vendor.successRate >= 70;
|
|
41
|
+
return true;
|
|
42
|
+
})
|
|
43
|
+
.filter((vendor) => {
|
|
44
|
+
if (!query) return true;
|
|
45
|
+
const keyword = query.toLowerCase();
|
|
46
|
+
return (
|
|
47
|
+
vendor.name.toLowerCase().includes(keyword) ||
|
|
48
|
+
vendor.jurisdictions.some((zone) => zone.toLowerCase().includes(keyword)) ||
|
|
49
|
+
vendor.zones.some((zone) => zone.location?.toLowerCase().includes(keyword))
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
}, [vendors, query, statusFilter]);
|
|
53
|
+
|
|
54
|
+
const zoneRows = useMemo(() => {
|
|
55
|
+
return vendors
|
|
56
|
+
.flatMap((vendor) => vendor.zones.map((zone) => ({ ...zone, agency: vendor.name })))
|
|
57
|
+
.filter((row) => row.location)
|
|
58
|
+
.sort((a, b) => (b.activeDemand || 0) - (a.activeDemand || 0));
|
|
59
|
+
}, [vendors]);
|
|
60
|
+
|
|
61
|
+
const alerts = [
|
|
62
|
+
"North East District has 22% more pending accounts than last week",
|
|
63
|
+
"Agency 2 has a 12% improvement opportunity in Self eKYC adoption",
|
|
64
|
+
"Urgent: 18 supervisor escalations raised for NWS Bhera Enclave",
|
|
65
|
+
"Risk alert: Rejection trends are rising in Outer North clusters",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const routeToVendor = (vendorId) => {
|
|
69
|
+
history.push(`${basePath}/vendors/${vendorId}`);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className="ekyc-dashboard-wrapper">
|
|
74
|
+
<div className="dashboard-shell">
|
|
75
|
+
<section className="kpi-grid">
|
|
76
|
+
{[
|
|
77
|
+
{
|
|
78
|
+
title: "Total Water Connections",
|
|
79
|
+
value: topKpis.totalWaterConnections,
|
|
80
|
+
icon: <FaUsers />,
|
|
81
|
+
variant: "primary",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
title: "Total eKYC Completed",
|
|
85
|
+
value: topKpis.totalEkycCompleted,
|
|
86
|
+
icon: <FaCheckCircle />,
|
|
87
|
+
variant: "success",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
title: "Total Pending",
|
|
91
|
+
value: topKpis.totalPending,
|
|
92
|
+
icon: <FaClock />,
|
|
93
|
+
variant: "warning",
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: "Total Rejected",
|
|
97
|
+
value: topKpis.totalRejected,
|
|
98
|
+
icon: <FaExclamationTriangle />,
|
|
99
|
+
variant: "danger",
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: "Agency eKYC Completed",
|
|
103
|
+
value: topKpis.agencyEkycCompleted,
|
|
104
|
+
icon: <FaUserTie />,
|
|
105
|
+
variant: "info",
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
title: "Citizen Self eKYC Completed",
|
|
109
|
+
value: topKpis.citizenSelfEkycCompleted,
|
|
110
|
+
icon: <FaUsers />,
|
|
111
|
+
variant: "secondary",
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
title: "Today's eKYC",
|
|
115
|
+
value: topKpis.todaysEkyc,
|
|
116
|
+
icon: <FaChartLine />,
|
|
117
|
+
variant: "muted",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
title: "Success Rate",
|
|
121
|
+
value: `${topKpis.successRate}%`,
|
|
122
|
+
icon: <FaCheckCircle />,
|
|
123
|
+
variant: "success",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
title: "Rejection Percentage",
|
|
127
|
+
value: `${topKpis.rejectionPercentage}%`,
|
|
128
|
+
icon: <FaExclamationTriangle />,
|
|
129
|
+
variant: "danger",
|
|
130
|
+
},
|
|
131
|
+
].map((card) => (
|
|
132
|
+
<article key={card.title} className={`kpi-card card-${card.variant}`}>
|
|
133
|
+
<div className="kpi-icon">{card.icon}</div>
|
|
134
|
+
<div>
|
|
135
|
+
<p className="kpi-title">{card.title}</p>
|
|
136
|
+
<h2>{card.value?.toLocaleString?.() ?? card.value}</h2>
|
|
137
|
+
</div>
|
|
138
|
+
</article>
|
|
139
|
+
))}
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
<section className="vendor-section">
|
|
143
|
+
<div className="section-top">
|
|
144
|
+
<div>
|
|
145
|
+
<h2>Agency Performance Snapshot</h2>
|
|
146
|
+
<p>Search, filter, and navigate into detailed analytics for every assigned vendor.</p>
|
|
147
|
+
</div>
|
|
148
|
+
<div className="vendor-filters">
|
|
149
|
+
<div className="search-box">
|
|
150
|
+
<FaSearch />
|
|
151
|
+
<input type="text" placeholder="Search agency, jurisdiction, location" value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
152
|
+
</div>
|
|
153
|
+
<div className="status-pill-group">
|
|
154
|
+
{filters.map((status) => (
|
|
155
|
+
<button key={status} className={`status-pill ${statusFilter === status ? "active" : ""}`} onClick={() => setStatusFilter(status)}>
|
|
156
|
+
{status}
|
|
157
|
+
</button>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
<div className="vendor-grid">
|
|
164
|
+
{filteredVendors.map((vendor) => (
|
|
165
|
+
<div key={vendor.id} className="vendor-card glass-card" onClick={() => routeToVendor(vendor.id)}>
|
|
166
|
+
<div className="vendor-card-top">
|
|
167
|
+
<div>
|
|
168
|
+
<p className="vendor-chip">{vendor.name}</p>
|
|
169
|
+
<h3>{vendor.assignedConnections.toLocaleString()}</h3>
|
|
170
|
+
<p className="vendor-subtitle">Assigned connections across {vendor.jurisdictions.length} jurisdiction(s)</p>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="vendor-progress-value">{vendor.progress}%</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div className="progress-bar">
|
|
175
|
+
<div className="progress-fill" style={{ width: `${vendor.progress}%` }} />
|
|
176
|
+
</div>
|
|
177
|
+
<div className="vendor-stats-grid">
|
|
178
|
+
<div>
|
|
179
|
+
<strong>{vendor.completedEkyc.toLocaleString()}</strong>
|
|
180
|
+
<span>Agency eKYC</span>
|
|
181
|
+
</div>
|
|
182
|
+
<div>
|
|
183
|
+
<strong>{vendor.selfEkyc.toLocaleString()}</strong>
|
|
184
|
+
<span>Self eKYC</span>
|
|
185
|
+
</div>
|
|
186
|
+
<div>
|
|
187
|
+
<strong>{vendor.pending.toLocaleString()}</strong>
|
|
188
|
+
<span>Pending</span>
|
|
189
|
+
</div>
|
|
190
|
+
<div>
|
|
191
|
+
<strong>{vendor.rejected.toLocaleString()}</strong>
|
|
192
|
+
<span>Rejected</span>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div className="vendor-card-footer">
|
|
196
|
+
<span>{vendor.activeSurveyors} Surveyors</span>
|
|
197
|
+
<span>{vendor.supervisors} Supervisors</span>
|
|
198
|
+
<span>{vendor.jurisdictions.join(", ")}</span>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="vendor-card-action">
|
|
201
|
+
<span>Explore analytics</span>
|
|
202
|
+
<FaArrowRight />
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</section>
|
|
208
|
+
|
|
209
|
+
<section className="analytics-grid">
|
|
210
|
+
<div className="analytics-panel glass-card">
|
|
211
|
+
<div className="section-header">
|
|
212
|
+
<h2>Daily eKYC Trend</h2>
|
|
213
|
+
<span className="badge success">Live index</span>
|
|
214
|
+
</div>
|
|
215
|
+
<ExecutiveLineChart data={dailyTrend} dataKey="completed" name="Completed" />
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
<div className="analytics-panel glass-card">
|
|
219
|
+
<div className="section-header">
|
|
220
|
+
<h2>Self vs Agency eKYC</h2>
|
|
221
|
+
<span className="badge info">Adoption</span>
|
|
222
|
+
</div>
|
|
223
|
+
<ExecutiveBarChart
|
|
224
|
+
data={selfVsAgency}
|
|
225
|
+
categories={[
|
|
226
|
+
{ key: "agency", name: "Agency" },
|
|
227
|
+
{ key: "citizen", name: "Citizen" },
|
|
228
|
+
]}
|
|
229
|
+
/>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div className="analytics-panel glass-card">
|
|
233
|
+
<div className="section-header">
|
|
234
|
+
<h2>Pending vs Completed</h2>
|
|
235
|
+
<span className="badge warning">Operational pulse</span>
|
|
236
|
+
</div>
|
|
237
|
+
<ExecutivePieChart data={pendingVsCompleted} dataKey="completed" labelKey="label" />
|
|
238
|
+
</div>
|
|
239
|
+
</section>
|
|
240
|
+
|
|
241
|
+
<section className="zone-section">
|
|
242
|
+
<div className="section-header">
|
|
243
|
+
<div>
|
|
244
|
+
<h2>
|
|
245
|
+
<FaMapMarkedAlt /> Jurisdiction Performance
|
|
246
|
+
</h2>
|
|
247
|
+
<p>Top zones evaluated from the full agency dataset.</p>
|
|
248
|
+
</div>
|
|
249
|
+
<button className="secondary-button">Export Zone Data</button>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div className="table-wrapper glass-card">
|
|
253
|
+
<table>
|
|
254
|
+
<thead>
|
|
255
|
+
<tr>
|
|
256
|
+
<th>Agency</th>
|
|
257
|
+
<th>Location</th>
|
|
258
|
+
<th>District</th>
|
|
259
|
+
<th>Assigned</th>
|
|
260
|
+
<th>Agency eKYC</th>
|
|
261
|
+
<th>Pending</th>
|
|
262
|
+
<th>Progress</th>
|
|
263
|
+
</tr>
|
|
264
|
+
</thead>
|
|
265
|
+
<tbody>
|
|
266
|
+
{zoneRows.slice(0, 10).map((zone, index) => (
|
|
267
|
+
<tr key={`${zone.agency}-${zone.location}-${index}`}>
|
|
268
|
+
<td>{zone.agency}</td>
|
|
269
|
+
<td>{zone.location}</td>
|
|
270
|
+
<td>{zone.district || zone.cluster}</td>
|
|
271
|
+
<td>{zone.activeDemand?.toLocaleString() ?? "—"}</td>
|
|
272
|
+
<td>{zone.pppZones?.toLocaleString() ?? "—"}</td>
|
|
273
|
+
<td>{zone.inactiveDemand?.toLocaleString() ?? "—"}</td>
|
|
274
|
+
<td>
|
|
275
|
+
<span className="status-badge success">
|
|
276
|
+
{Math.min(100, Math.round(((zone.pppZones || 0) / Math.max(zone.activeDemand || 1, 1)) * 100))}%
|
|
277
|
+
</span>
|
|
278
|
+
</td>
|
|
279
|
+
</tr>
|
|
280
|
+
))}
|
|
281
|
+
</tbody>
|
|
282
|
+
</table>
|
|
283
|
+
</div>
|
|
284
|
+
</section>
|
|
285
|
+
|
|
286
|
+
<section className="bottom-grid">
|
|
287
|
+
<div className="alerts-panel glass-card">
|
|
288
|
+
<div className="section-header">
|
|
289
|
+
<h2>
|
|
290
|
+
<FaExclamationTriangle /> Critical Alerts
|
|
291
|
+
</h2>
|
|
292
|
+
</div>
|
|
293
|
+
<div className="alerts-list">
|
|
294
|
+
{alerts.map((alert) => (
|
|
295
|
+
<div key={alert} className="alert-card">
|
|
296
|
+
<FaExclamationTriangle />
|
|
297
|
+
<p>{alert}</p>
|
|
298
|
+
</div>
|
|
299
|
+
))}
|
|
300
|
+
</div>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div className="efficiency-panel glass-card">
|
|
304
|
+
<div className="section-header">
|
|
305
|
+
<h2>
|
|
306
|
+
<FaUserTie /> Workforce & Efficiency
|
|
307
|
+
</h2>
|
|
308
|
+
</div>
|
|
309
|
+
<div className="efficiency-grid">
|
|
310
|
+
<div className="efficiency-box">
|
|
311
|
+
<h3>48</h3>
|
|
312
|
+
<p>Supervisors Online</p>
|
|
313
|
+
</div>
|
|
314
|
+
<div className="efficiency-box">
|
|
315
|
+
<h3>168</h3>
|
|
316
|
+
<p>Surveyors Active</p>
|
|
317
|
+
</div>
|
|
318
|
+
<div className="efficiency-box">
|
|
319
|
+
<h3>{topKpis.todayEkyc?.toLocaleString()}</h3>
|
|
320
|
+
<p>eKYC Completed Today</p>
|
|
321
|
+
</div>
|
|
322
|
+
<div className="efficiency-box">
|
|
323
|
+
<h3>{topKpis.successRate}%</h3>
|
|
324
|
+
<p>Operational Efficiency</p>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
</section>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
export default CeoDashboard;
|
|
@@ -76,6 +76,14 @@ const DesktopInbox = ({ tableConfig, filterComponent, ...props }) => {
|
|
|
76
76
|
return <span className={`ekyc-status-tag ${status}`}>{t(`${status}`)}</span>;
|
|
77
77
|
},
|
|
78
78
|
},
|
|
79
|
+
{
|
|
80
|
+
Header: t("EKYC_EKYC_STATUS"),
|
|
81
|
+
accessor: "ekycStatus",
|
|
82
|
+
Cell: ({ row }) => {
|
|
83
|
+
const ekycStatus = row.original?.ekycstatus || "NA";
|
|
84
|
+
return <span className={`ekyc-status-tag ${ekycStatus}`}>{t(`${ekycStatus}`)}</span>
|
|
85
|
+
}
|
|
86
|
+
},
|
|
79
87
|
{
|
|
80
88
|
Header: t("EKYC_ACTION"),
|
|
81
89
|
accessor: "status",
|
|
@@ -34,12 +34,16 @@ const EKYCCard = () => {
|
|
|
34
34
|
// },
|
|
35
35
|
{
|
|
36
36
|
label: t("CEO_M.F_DOR_FINANCE_VIEW"),
|
|
37
|
-
link: `/digit-ui/employee/ekyc/ceo-dashboard
|
|
37
|
+
link: `/digit-ui/employee/ekyc/ceo-dashboard`,
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
label: t("EKYC_MAPPING"),
|
|
41
41
|
link: `/digit-ui/employee/ekyc/mapping`,
|
|
42
42
|
},
|
|
43
|
+
{
|
|
44
|
+
label: t("EKYC_ASSIGN"),
|
|
45
|
+
link: `/digit-ui/employee/ekyc/assign`,
|
|
46
|
+
},
|
|
43
47
|
],
|
|
44
48
|
};
|
|
45
49
|
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { useHistory, useParams } from "react-router-dom";
|
|
3
|
+
import { FaArrowLeft, FaUsers, FaCheckCircle, FaClock, FaExclamationTriangle, FaMapMarkedAlt } from "react-icons/fa";
|
|
4
|
+
import { ekycMockData } from "./mockData";
|
|
5
|
+
import ExecutiveLineChart from "./analytics/charts/ExecutiveLineChart";
|
|
6
|
+
import ExecutiveBarChart from "./analytics/charts/ExecutiveBarChart";
|
|
7
|
+
import ExecutivePieChart from "./analytics/charts/ExecutivePieChart";
|
|
8
|
+
|
|
9
|
+
const VendorDetails = () => {
|
|
10
|
+
const history = useHistory();
|
|
11
|
+
const { vendorId } = useParams();
|
|
12
|
+
const vendor = useMemo(() => ekycMockData.vendors.find((item) => item.id === Number(vendorId)), [vendorId]);
|
|
13
|
+
|
|
14
|
+
if (!vendor) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="ekyc-dashboard-wrapper">
|
|
17
|
+
<div className="dashboard-shell">
|
|
18
|
+
<div className="detail-header">
|
|
19
|
+
<button className="back-button" onClick={() => history.goBack()}>
|
|
20
|
+
<FaArrowLeft /> Back
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
<div className="empty-state">
|
|
24
|
+
<h2>Vendor analytics not found</h2>
|
|
25
|
+
<p>The selected vendor does not exist in the current report. Please return to the dashboard.</p>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const zoneRows = vendor.zones.slice(0, 8);
|
|
33
|
+
|
|
34
|
+
const performanceData = vendor.dailyPerformance.map((row) => ({
|
|
35
|
+
day: row.day,
|
|
36
|
+
completed: row.completed,
|
|
37
|
+
pending: row.pending,
|
|
38
|
+
rejected: row.rejected,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="ekyc-dashboard-wrapper">
|
|
43
|
+
<div className="dashboard-shell detail-shell">
|
|
44
|
+
<section className="kpi-grid detail-kpi-grid">
|
|
45
|
+
{[
|
|
46
|
+
{
|
|
47
|
+
label: "Assigned Connections",
|
|
48
|
+
value: vendor.assignedConnections,
|
|
49
|
+
icon: <FaUsers />,
|
|
50
|
+
variant: "primary",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: "Agency eKYC",
|
|
54
|
+
value: vendor.completedEkyc,
|
|
55
|
+
icon: <FaCheckCircle />,
|
|
56
|
+
variant: "success",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
label: "Self eKYC",
|
|
60
|
+
value: vendor.selfEkyc,
|
|
61
|
+
icon: <FaUsers />,
|
|
62
|
+
variant: "info",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
label: "Pending",
|
|
66
|
+
value: vendor.pending,
|
|
67
|
+
icon: <FaClock />,
|
|
68
|
+
variant: "warning",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
label: "Rejected",
|
|
72
|
+
value: vendor.rejected,
|
|
73
|
+
icon: <FaExclamationTriangle />,
|
|
74
|
+
variant: "danger",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
label: "Success Rate",
|
|
78
|
+
value: `${vendor.successRate}%`,
|
|
79
|
+
icon: <FaCheckCircle />,
|
|
80
|
+
variant: "success",
|
|
81
|
+
},
|
|
82
|
+
].map((item) => (
|
|
83
|
+
<article key={item.label} className={`kpi-card card-${item.variant}`}>
|
|
84
|
+
<div className="kpi-icon">{item.icon}</div>
|
|
85
|
+
<div>
|
|
86
|
+
<p className="kpi-title">{item.label}</p>
|
|
87
|
+
<h2>{item.value?.toLocaleString?.() ?? item.value}</h2>
|
|
88
|
+
</div>
|
|
89
|
+
</article>
|
|
90
|
+
))}
|
|
91
|
+
</section>
|
|
92
|
+
|
|
93
|
+
<section className="analytics-grid detail-analytics-grid">
|
|
94
|
+
<div className="analytics-panel glass-card">
|
|
95
|
+
<div className="section-header">
|
|
96
|
+
<h2>Daily Completion Trend</h2>
|
|
97
|
+
<span className="badge success">Performance</span>
|
|
98
|
+
</div>
|
|
99
|
+
<ExecutiveLineChart data={performanceData} dataKey="completed" name="Completed" />
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className="analytics-panel glass-card">
|
|
103
|
+
<div className="section-header">
|
|
104
|
+
<h2>Pending vs Completed</h2>
|
|
105
|
+
<span className="badge warning">Workload</span>
|
|
106
|
+
</div>
|
|
107
|
+
<ExecutiveBarChart
|
|
108
|
+
data={performanceData}
|
|
109
|
+
categories={[
|
|
110
|
+
{ key: "completed", name: "Completed" },
|
|
111
|
+
{ key: "pending", name: "Pending" },
|
|
112
|
+
]}
|
|
113
|
+
xKey="day"
|
|
114
|
+
/>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="analytics-panel glass-card">
|
|
118
|
+
<div className="section-header">
|
|
119
|
+
<h2>Self vs Agency Split</h2>
|
|
120
|
+
<span className="badge info">Adoption</span>
|
|
121
|
+
</div>
|
|
122
|
+
<ExecutivePieChart
|
|
123
|
+
data={[
|
|
124
|
+
{ label: "Agency eKYC", value: vendor.completedEkyc },
|
|
125
|
+
{ label: "Citizen Self eKYC", value: vendor.selfEkyc },
|
|
126
|
+
]}
|
|
127
|
+
dataKey="value"
|
|
128
|
+
labelKey="label"
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<section className="zone-section detail-zone-section">
|
|
134
|
+
<div className="section-header">
|
|
135
|
+
<div>
|
|
136
|
+
<h2>
|
|
137
|
+
<FaMapMarkedAlt /> Zone-wise Jurisdiction Analytics
|
|
138
|
+
</h2>
|
|
139
|
+
<p>Drill into the current operational load and heatmap intensity for this vendor.</p>
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
<div className="table-wrapper glass-card">
|
|
143
|
+
<table>
|
|
144
|
+
<thead>
|
|
145
|
+
<tr>
|
|
146
|
+
<th>Zone</th>
|
|
147
|
+
<th>Cluster</th>
|
|
148
|
+
<th>Assigned</th>
|
|
149
|
+
<th>Agency eKYC</th>
|
|
150
|
+
<th>Pending</th>
|
|
151
|
+
<th>Activity Score</th>
|
|
152
|
+
</tr>
|
|
153
|
+
</thead>
|
|
154
|
+
<tbody>
|
|
155
|
+
{zoneRows.map((row, index) => (
|
|
156
|
+
<tr key={`${row.location}-${index}`}>
|
|
157
|
+
<td>{row.location}</td>
|
|
158
|
+
<td>{row.cluster || row.district}</td>
|
|
159
|
+
<td>{row.activeDemand?.toLocaleString() ?? "—"}</td>
|
|
160
|
+
<td>{row.pppZones?.toLocaleString() ?? "—"}</td>
|
|
161
|
+
<td>{row.inactiveDemand?.toLocaleString() ?? "—"}</td>
|
|
162
|
+
<td>
|
|
163
|
+
<span className={`status-badge ${row.intensityScore > 65 ? "danger" : row.intensityScore > 45 ? "warning" : "success"}`}>
|
|
164
|
+
{row.intensityScore}%
|
|
165
|
+
</span>
|
|
166
|
+
</td>
|
|
167
|
+
</tr>
|
|
168
|
+
))}
|
|
169
|
+
</tbody>
|
|
170
|
+
</table>
|
|
171
|
+
</div>
|
|
172
|
+
</section>
|
|
173
|
+
|
|
174
|
+
<section className="bottom-grid detail-bottom-grid">
|
|
175
|
+
<div className="cards-summary glass-card">
|
|
176
|
+
<h2>Operational Efficiency</h2>
|
|
177
|
+
<div className="efficiency-grid">
|
|
178
|
+
<div className="efficiency-box">
|
|
179
|
+
<h3>{vendor.activeSurveyors}</h3>
|
|
180
|
+
<p>Active Surveyors</p>
|
|
181
|
+
</div>
|
|
182
|
+
<div className="efficiency-box">
|
|
183
|
+
<h3>{vendor.supervisors}</h3>
|
|
184
|
+
<p>Supervisors</p>
|
|
185
|
+
</div>
|
|
186
|
+
<div className="efficiency-box">
|
|
187
|
+
<h3>{vendor.progress}%</h3>
|
|
188
|
+
<p>Execution Rate</p>
|
|
189
|
+
</div>
|
|
190
|
+
<div className="efficiency-box">
|
|
191
|
+
<h3>{vendor.dailyPerformance.slice(-1)[0]?.completed?.toLocaleString()}</h3>
|
|
192
|
+
<p>Latest Day Completed</p>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
|
|
197
|
+
<div className="cards-summary glass-card">
|
|
198
|
+
<h2>Supervisor Pulse</h2>
|
|
199
|
+
<div className="pulse-list">
|
|
200
|
+
{vendor.jurisdictions.map((zone) => (
|
|
201
|
+
<div key={zone} className="pulse-row">
|
|
202
|
+
<span>{zone}</span>
|
|
203
|
+
<span>{Math.max(12, Math.round(Math.random() * 38))} active</span>
|
|
204
|
+
</div>
|
|
205
|
+
))}
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</section>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export default VendorDetails;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { ResponsiveContainer, BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Legend } from "recharts";
|
|
3
|
+
|
|
4
|
+
const ExecutiveBarChart = ({ data = [], categories = [], xKey = "label" }) => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="chart-card" style={{ minHeight: 320 }}>
|
|
7
|
+
<ResponsiveContainer width="100%" height={320}>
|
|
8
|
+
<BarChart data={data} margin={{ top: 12, right: 16, left: 0, bottom: 0 }}>
|
|
9
|
+
<CartesianGrid stroke="rgba(148, 163, 184, 0.12)" strokeDasharray="4 4" />
|
|
10
|
+
<XAxis dataKey={xKey} axisLine={false} tickLine={false} tick={{ fill: "#cbd5e1" }} />
|
|
11
|
+
<YAxis axisLine={false} tickLine={false} tick={{ fill: "#cbd5e1" }} />
|
|
12
|
+
<Tooltip contentStyle={{ background: "#0f172a", border: "none", borderRadius: 14 }} itemStyle={{ color: "#f8fafc" }} />
|
|
13
|
+
<Legend verticalAlign="top" align="right" iconType="circle" wrapperStyle={{ color: "#cbd5e1" }} />
|
|
14
|
+
{categories.map((series, index) => (
|
|
15
|
+
<Bar
|
|
16
|
+
key={series.key}
|
|
17
|
+
dataKey={series.key}
|
|
18
|
+
name={series.name}
|
|
19
|
+
fill={index === 0 ? "#34d399" : "#60a5fa"}
|
|
20
|
+
radius={[12, 12, 0, 0]}
|
|
21
|
+
barSize={24}
|
|
22
|
+
/>
|
|
23
|
+
))}
|
|
24
|
+
</BarChart>
|
|
25
|
+
</ResponsiveContainer>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default ExecutiveBarChart;
|