@djb25/digit-ui-module-ekyc 1.0.10 → 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.
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.modern.js +1338 -591
- package/dist/index.modern.js.map +1 -1
- package/package.json +1 -1
- package/src/Module.js +25 -7
- package/src/components/AadhaarVerification.js +415 -0
- package/src/components/AddressDetails.js +207 -0
- package/src/components/CeoDashboard.js +205 -0
- package/src/components/DesktopInbox.js +1 -1
- package/src/components/EKYCCard.js +4 -0
- package/src/components/MeterDetails.js +372 -0
- package/src/components/PropertyInfo.js +303 -0
- package/src/components/Review.js +572 -0
- package/src/components/SearchFormFieldsComponent.js +3 -3
- package/src/components/analytics/charts/ClusterHeatmap.js +88 -0
- package/src/components/analytics/charts/TaskStatusChart.js +92 -0
- package/src/components/analytics/components/AnalyticsTable.js +106 -0
- package/src/components/analytics/components/DashboardLayout.js +72 -0
- package/src/components/analytics/components/EmptyState.js +27 -0
- package/src/components/analytics/components/ErrorBoundary.js +27 -0
- package/src/components/analytics/components/FilterBar.js +73 -0
- package/src/components/analytics/components/NotificationPanel.js +77 -0
- package/src/components/analytics/components/SLAWidget.js +56 -0
- package/src/components/analytics/components/SkeletonLoader.js +53 -0
- package/src/components/analytics/components/SummaryCard.js +74 -0
- package/src/components/analytics/components/WorkflowTimeline.js +55 -0
- package/src/components/analytics/styles/Dashboard.css +54 -0
- package/src/components/analytics/utils/exportUtils.js +64 -0
- package/src/components/analytics/utils/filterSerializer.js +50 -0
- package/src/config/config.js +1 -1
- package/src/pages/citizen/index.js +74 -18
- package/src/pages/employee/ConsumerDetails.js +10 -281
- package/src/pages/employee/Inbox.js +6 -4
- package/src/pages/employee/index.js +44 -8
- package/src/pages/employee/AadhaarVerification.js +0 -512
- package/src/pages/employee/AddressDetails.js +0 -548
- package/src/pages/employee/MeterDetails.js +0 -496
- package/src/pages/employee/PropertyInfo.js +0 -489
- package/src/pages/employee/Review.js +0 -314
|
@@ -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;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useEffect, useRef } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import { Chart, registerables } from "chart.js";
|
|
4
|
+
|
|
5
|
+
Chart.register(...registerables);
|
|
6
|
+
|
|
7
|
+
const TaskStatusChart = ({ data, title }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
const chartRef = useRef(null);
|
|
10
|
+
const chartInstance = useRef(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
if (chartRef.current && data) {
|
|
14
|
+
if (chartInstance.current) chartInstance.current.destroy();
|
|
15
|
+
const ctx = chartRef.current.getContext("2d");
|
|
16
|
+
|
|
17
|
+
// Create vibrant gradients
|
|
18
|
+
const gradientBlue = ctx.createLinearGradient(0, 0, 0, 400);
|
|
19
|
+
gradientBlue.addColorStop(0, '#3B82F6');
|
|
20
|
+
gradientBlue.addColorStop(1, '#2563EB');
|
|
21
|
+
|
|
22
|
+
const gradientAmber = ctx.createLinearGradient(0, 0, 0, 400);
|
|
23
|
+
gradientAmber.addColorStop(0, '#F59E0B');
|
|
24
|
+
gradientAmber.addColorStop(1, '#D97706');
|
|
25
|
+
|
|
26
|
+
const gradientIndigo = ctx.createLinearGradient(0, 0, 0, 400);
|
|
27
|
+
gradientIndigo.addColorStop(0, '#6366F1');
|
|
28
|
+
gradientIndigo.addColorStop(1, '#4F46E5');
|
|
29
|
+
|
|
30
|
+
const gradientEmerald = ctx.createLinearGradient(0, 0, 0, 400);
|
|
31
|
+
gradientEmerald.addColorStop(0, '#10B981');
|
|
32
|
+
gradientEmerald.addColorStop(1, '#059669');
|
|
33
|
+
|
|
34
|
+
const colors = [gradientBlue, gradientAmber, gradientIndigo, gradientEmerald, '#EC4899', '#EF4444'];
|
|
35
|
+
|
|
36
|
+
const labels = data.map(item => t(item.stageName));
|
|
37
|
+
const values = data.map(item => item.count);
|
|
38
|
+
|
|
39
|
+
const ChartConstructor = Chart.Chart || Chart;
|
|
40
|
+
chartInstance.current = new ChartConstructor(ctx, {
|
|
41
|
+
type: "doughnut",
|
|
42
|
+
data: {
|
|
43
|
+
labels,
|
|
44
|
+
datasets: [
|
|
45
|
+
{
|
|
46
|
+
data: values,
|
|
47
|
+
backgroundColor: colors.slice(0, labels.length),
|
|
48
|
+
borderWidth: 2,
|
|
49
|
+
borderColor: "#ffffff",
|
|
50
|
+
hoverOffset: 10
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
options: {
|
|
55
|
+
responsive: true,
|
|
56
|
+
maintainAspectRatio: false,
|
|
57
|
+
cutout: '70%',
|
|
58
|
+
plugins: {
|
|
59
|
+
legend: {
|
|
60
|
+
position: 'right',
|
|
61
|
+
labels: {
|
|
62
|
+
usePointStyle: true,
|
|
63
|
+
padding: 15,
|
|
64
|
+
font: { size: 12 }
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
tooltip: {
|
|
68
|
+
backgroundColor: '#1F2937',
|
|
69
|
+
padding: 10,
|
|
70
|
+
bodyFont: { size: 13 }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return () => {
|
|
78
|
+
if (chartInstance.current) chartInstance.current.destroy();
|
|
79
|
+
};
|
|
80
|
+
}, [data, t]);
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="task-status-chart-container glass-card" style={{ height: "320px", width: "100%", padding: "24px" }}>
|
|
84
|
+
{title && <h3 className="chart-title" style={{ marginBottom: "24px", fontSize: "18px", fontWeight: "700", color: "#111827" }}>{t(title)}</h3>}
|
|
85
|
+
<div style={{ height: "220px" }}>
|
|
86
|
+
<canvas ref={chartRef} />
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default TaskStatusChart;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React, { useState, useMemo } from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
import ExportUtils from "../utils/exportUtils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Enterprise data grid for detailed analytics.
|
|
7
|
+
*/
|
|
8
|
+
const AnalyticsTable = ({ data, columns, title, filename }) => {
|
|
9
|
+
const { t } = useTranslation();
|
|
10
|
+
const [searchTerm, setSearchTerm] = useState("");
|
|
11
|
+
const [sortConfig, setSortConfig] = useState({ key: null, direction: 'asc' });
|
|
12
|
+
|
|
13
|
+
const sortedData = useMemo(() => {
|
|
14
|
+
let items = [...(data || [])];
|
|
15
|
+
|
|
16
|
+
// Filter
|
|
17
|
+
if (searchTerm) {
|
|
18
|
+
items = items.filter(item =>
|
|
19
|
+
Object.values(item).some(val =>
|
|
20
|
+
String(val).toLowerCase().includes(searchTerm.toLowerCase())
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sort
|
|
26
|
+
if (sortConfig.key) {
|
|
27
|
+
items.sort((a, b) => {
|
|
28
|
+
if (a[sortConfig.key] < b[sortConfig.key]) {
|
|
29
|
+
return sortConfig.direction === 'asc' ? -1 : 1;
|
|
30
|
+
}
|
|
31
|
+
if (a[sortConfig.key] > b[sortConfig.key]) {
|
|
32
|
+
return sortConfig.direction === 'asc' ? 1 : -1;
|
|
33
|
+
}
|
|
34
|
+
return 0;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return items;
|
|
38
|
+
}, [data, searchTerm, sortConfig]);
|
|
39
|
+
|
|
40
|
+
const requestSort = (key) => {
|
|
41
|
+
let direction = 'asc';
|
|
42
|
+
if (sortConfig.key === key && sortConfig.direction === 'asc') {
|
|
43
|
+
direction = 'desc';
|
|
44
|
+
}
|
|
45
|
+
setSortConfig({ key, direction });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div className="analytics-table-card glass-card" style={{ padding: "0", overflow: "hidden" }}>
|
|
50
|
+
<div className="table-header" style={{ padding: "28px", borderBottom: "1px solid rgba(229, 231, 235, 0.5)", display: "flex", justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: "16px" }}>
|
|
51
|
+
<h3 style={{ fontSize: "18px", fontWeight: "800", color: "#111827", letterSpacing: "-0.025em" }}>{t(title)}</h3>
|
|
52
|
+
<div style={{ display: "flex", gap: "12px", alignItems: "center" }}>
|
|
53
|
+
<input
|
|
54
|
+
type="text"
|
|
55
|
+
placeholder={t("EKYC_SEARCH_RECORDS")}
|
|
56
|
+
value={searchTerm}
|
|
57
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
58
|
+
style={{ padding: "8px 16px", borderRadius: "8px", border: "1px solid #D1D5DB", outline: "none", fontSize: "14px" }}
|
|
59
|
+
/>
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => ExportUtils.exportToCsv(sortedData, filename, columns)}
|
|
62
|
+
style={{ padding: "10px 20px", borderRadius: "12px", background: "var(--primary-gradient)", color: "#FFF", border: "none", cursor: "pointer", fontWeight: "700", fontSize: "14px", boxShadow: "0 4px 6px rgba(99, 102, 241, 0.2)" }}
|
|
63
|
+
>
|
|
64
|
+
{t("EKYC_EXPORT_CSV")}
|
|
65
|
+
</button>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="table-body" style={{ overflowX: "auto" }}>
|
|
70
|
+
<table style={{ width: "100%", borderCollapse: "collapse", textAlign: "left" }}>
|
|
71
|
+
<thead>
|
|
72
|
+
<tr style={{ background: "#F9FAFB" }}>
|
|
73
|
+
{columns.map(col => (
|
|
74
|
+
<th
|
|
75
|
+
key={col.id}
|
|
76
|
+
onClick={() => requestSort(col.id)}
|
|
77
|
+
style={{ padding: "16px 28px", fontSize: "11px", fontWeight: "700", color: "#6B7280", textTransform: "uppercase", cursor: "pointer", borderBottom: "1px solid rgba(229, 231, 235, 0.5)", letterSpacing: "0.05em" }}
|
|
78
|
+
>
|
|
79
|
+
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
|
80
|
+
{t(col.label)}
|
|
81
|
+
{sortConfig.key === col.id && (<span>{sortConfig.direction === 'asc' ? '↑' : '↓'}</span>)}
|
|
82
|
+
</div>
|
|
83
|
+
</th>
|
|
84
|
+
))}
|
|
85
|
+
</tr>
|
|
86
|
+
</thead>
|
|
87
|
+
<tbody>
|
|
88
|
+
{sortedData.map((row, idx) => (
|
|
89
|
+
<tr key={idx} style={{ borderBottom: "1px solid #F3F4F6", transition: "background 0.2s" }} onMouseOver={(e) => e.currentTarget.style.background = "#F9FAFB"} onMouseOut={(e) => e.currentTarget.style.background = "transparent"}>
|
|
90
|
+
{columns.map(col => (
|
|
91
|
+
<td key={col.id} style={{ padding: "16px 28px", fontSize: "14px", color: "#4B5563", fontWeight: col.id === "agencyName" ? "600" : "400" }}>
|
|
92
|
+
{col.isCurrency ? `₹${new Intl.NumberFormat('en-IN').format(row[col.id])}` :
|
|
93
|
+
col.isPercentage ? `${row[col.id]}%` :
|
|
94
|
+
row[col.id]}
|
|
95
|
+
</td>
|
|
96
|
+
))}
|
|
97
|
+
</tr>
|
|
98
|
+
))}
|
|
99
|
+
</tbody>
|
|
100
|
+
</table>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export default AnalyticsTable;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Main structural shell for the Enterprise Dashboard.
|
|
6
|
+
*/
|
|
7
|
+
const DashboardLayout = ({ header, filters, children, onNotificationClick, activeRole, onRoleChange }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
const roles = ["CEO", "CLUSTER_MANAGER", "AGENCY_SUPERVISOR"];
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<div className="enterprise-dashboard-layout" style={{ background: "linear-gradient(135deg, #F8FAFC 0%, #F1F5F9 100%)", minHeight: "100vh" }}>
|
|
14
|
+
{/* Top Navigation / Header */}
|
|
15
|
+
<div className="glass-card" style={{ padding: "24px 32px", display: "flex", justifyContent: "space-between", alignItems: "center", borderRadius: "0", borderTop: "none", borderLeft: "none", borderRight: "none" }}>
|
|
16
|
+
<div>
|
|
17
|
+
<h1 style={{ fontSize: "28px", fontWeight: "900", color: "#1E293B", letterSpacing: "-0.05em" }}>{t(header)}</h1>
|
|
18
|
+
<p style={{ fontSize: "14px", color: "#64748B", marginTop: "4px", fontWeight: "500" }}>{t("EKYC_DASHBOARD_SUBTITLE_ALT") || "Operational analytics & performance monitoring."}</p>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<div style={{ display: "flex", alignItems: "center", gap: "20px" }}>
|
|
22
|
+
{/* Role Switcher for Simulation */}
|
|
23
|
+
<div style={{ display: "flex", background: "#F3F4F6", padding: "4px", borderRadius: "8px", gap: "4px" }}>
|
|
24
|
+
{roles.map(role => (
|
|
25
|
+
<button
|
|
26
|
+
key={role}
|
|
27
|
+
onClick={() => onRoleChange(role)}
|
|
28
|
+
style={{
|
|
29
|
+
padding: "6px 12px",
|
|
30
|
+
borderRadius: "6px",
|
|
31
|
+
fontSize: "12px",
|
|
32
|
+
fontWeight: "700",
|
|
33
|
+
border: "none",
|
|
34
|
+
cursor: "pointer",
|
|
35
|
+
background: activeRole === role ? "var(--primary-gradient)" : "transparent",
|
|
36
|
+
color: activeRole === role ? "#FFFFFF" : "#64748B",
|
|
37
|
+
boxShadow: activeRole === role ? "0 4px 12px rgba(99, 102, 241, 0.3)" : "none",
|
|
38
|
+
transition: "all 0.3s cubic-bezier(0.4, 0, 0.2, 1)"
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
{t(role)}
|
|
42
|
+
</button>
|
|
43
|
+
))}
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<button
|
|
47
|
+
onClick={onNotificationClick}
|
|
48
|
+
style={{ position: "relative", background: "#F3F4F6", border: "none", padding: "10px", borderRadius: "50%", cursor: "pointer" }}
|
|
49
|
+
>
|
|
50
|
+
<span style={{ fontSize: "20px" }}>🔔</span>
|
|
51
|
+
<span style={{ position: "absolute", top: "0", right: "0", width: "10px", height: "10px", background: "#EF4444", borderRadius: "50%", border: "2px solid #FFF" }} />
|
|
52
|
+
</button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Filter Bar Position */}
|
|
57
|
+
{filters}
|
|
58
|
+
|
|
59
|
+
{/* Main Content Area */}
|
|
60
|
+
<div style={{ padding: "24px", maxWidth: "1600px", margin: "0 auto" }}>
|
|
61
|
+
{children}
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
{/* Footer Branding */}
|
|
65
|
+
<div style={{ padding: "40px 24px", textAlign: "center", color: "#9CA3AF", fontSize: "12px" }}>
|
|
66
|
+
{t("EKYC_POWERED_BY_UPYOG")} | {new Date().getFullYear()} © {t("EKYC_GOVT_DJB")}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export default DashboardLayout;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
const EmptyState = ({ message = "EKYC_NO_DATA_FOUND" }) => {
|
|
5
|
+
const { t } = useTranslation();
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<div className="empty-state-container" style={{
|
|
9
|
+
display: "flex",
|
|
10
|
+
flexDirection: "column",
|
|
11
|
+
alignItems: "center",
|
|
12
|
+
justifyContent: "center",
|
|
13
|
+
padding: "60px 20px",
|
|
14
|
+
background: "#F9FAFB",
|
|
15
|
+
borderRadius: "12px",
|
|
16
|
+
border: "1px dashed #D1D5DB"
|
|
17
|
+
}}>
|
|
18
|
+
<div style={{ fontSize: "48px", marginBottom: "16px" }}>🔍</div>
|
|
19
|
+
<h3 style={{ fontSize: "18px", fontWeight: "600", color: "#374151", marginBottom: "8px" }}>{t("EKYC_EMPTY_STATE_TITLE") || "No Results Found"}</h3>
|
|
20
|
+
<p style={{ color: "#6B7280", textAlign: "center", maxWidth: "300px" }}>
|
|
21
|
+
{t(message) || "Try adjusting your filters or check back later for updated metrics."}
|
|
22
|
+
</p>
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export default EmptyState;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ErrorBoundary extends React.Component {
|
|
5
|
+
constructor(props) {
|
|
6
|
+
super(props);
|
|
7
|
+
this.state = { hasError: false };
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static getDerivedStateFromError(error) {
|
|
11
|
+
return { hasError: true };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
render() {
|
|
15
|
+
if (this.state.hasError) {
|
|
16
|
+
return (
|
|
17
|
+
<div style={{ padding: "20px", background: "#FEF2F2", border: "1px solid #FECACA", borderRadius: "8px", textAlign: "center" }}>
|
|
18
|
+
<h4 style={{ color: "#991B1B", marginBottom: "8px" }}>Widget Error</h4>
|
|
19
|
+
<p style={{ color: "#B91C1C", fontSize: "12px" }}>Failed to render this component.</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return this.props.children;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default ErrorBoundary;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Sticky global filter bar for dashboard parameters.
|
|
6
|
+
*/
|
|
7
|
+
const FilterBar = ({ filters, config, onFilterChange, onReset }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="dashboard-filter-bar glass-card" style={{
|
|
12
|
+
position: "sticky",
|
|
13
|
+
top: "0",
|
|
14
|
+
zIndex: "100",
|
|
15
|
+
padding: "20px 28px",
|
|
16
|
+
display: "flex",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
gap: "24px",
|
|
19
|
+
flexWrap: "wrap",
|
|
20
|
+
borderRadius: "0 0 24px 24px",
|
|
21
|
+
marginTop: "-1px"
|
|
22
|
+
}}>
|
|
23
|
+
<div style={{ display: "flex", alignItems: "center", gap: "8px", borderRight: "1px solid #E5E7EB", paddingRight: "16px" }}>
|
|
24
|
+
<span style={{ fontSize: "14px", fontWeight: "700", color: "#374151" }}>{t("EKYC_GLOBAL_FILTERS")}</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div style={{ display: "flex", gap: "12px", flex: 1, flexWrap: "wrap" }}>
|
|
28
|
+
{config.map((filter) => (
|
|
29
|
+
<div key={filter.id} style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
|
30
|
+
<label style={{ fontSize: "11px", fontWeight: "600", color: "#6B7280", textTransform: "uppercase" }}>{t(filter.label)}</label>
|
|
31
|
+
<select
|
|
32
|
+
value={filters[filter.id] || filter.default}
|
|
33
|
+
onChange={(e) => onFilterChange(filter.id, e.target.value)}
|
|
34
|
+
style={{
|
|
35
|
+
padding: "6px 12px",
|
|
36
|
+
borderRadius: "6px",
|
|
37
|
+
border: "1px solid #D1D5DB",
|
|
38
|
+
background: "#F9FAFB",
|
|
39
|
+
fontSize: "13px",
|
|
40
|
+
outline: "none",
|
|
41
|
+
minWidth: "140px"
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
{filter.options.map(opt => (
|
|
45
|
+
<option key={opt} value={opt}>{opt}</option>
|
|
46
|
+
))}
|
|
47
|
+
</select>
|
|
48
|
+
</div>
|
|
49
|
+
))}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div style={{ display: "flex", gap: "12px" }}>
|
|
53
|
+
<button
|
|
54
|
+
onClick={onReset}
|
|
55
|
+
style={{
|
|
56
|
+
padding: "8px 16px",
|
|
57
|
+
borderRadius: "8px",
|
|
58
|
+
background: "transparent",
|
|
59
|
+
color: "#6B7280",
|
|
60
|
+
border: "1px solid #D1D5DB",
|
|
61
|
+
cursor: "pointer",
|
|
62
|
+
fontSize: "14px",
|
|
63
|
+
fontWeight: "600"
|
|
64
|
+
}}
|
|
65
|
+
>
|
|
66
|
+
{t("EKYC_RESET")}
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default FilterBar;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Drawer-style notification panel for SLA alerts and escalations.
|
|
6
|
+
*/
|
|
7
|
+
const NotificationPanel = ({ notifications, isOpen, onClose }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
if (!isOpen) return null;
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
<div
|
|
15
|
+
onClick={onClose}
|
|
16
|
+
style={{ position: "fixed", top: 0, left: 0, width: "100%", height: "100%", background: "rgba(0,0,0,0.3)", zIndex: 999 }}
|
|
17
|
+
/>
|
|
18
|
+
<div className="notification-panel" style={{
|
|
19
|
+
position: "fixed",
|
|
20
|
+
top: 0,
|
|
21
|
+
right: 0,
|
|
22
|
+
width: "350px",
|
|
23
|
+
height: "100%",
|
|
24
|
+
background: "#FFFFFF",
|
|
25
|
+
zIndex: 1000,
|
|
26
|
+
boxShadow: "-4px 0 15px rgba(0,0,0,0.1)",
|
|
27
|
+
display: "flex",
|
|
28
|
+
flexDirection: "column"
|
|
29
|
+
}}>
|
|
30
|
+
<div style={{ padding: "20px", borderBottom: "1px solid #F3F4F6", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
|
31
|
+
<h3 style={{ fontSize: "18px", fontWeight: "700", color: "#111827" }}>{t("EKYC_ALERTS_CENTER")}</h3>
|
|
32
|
+
<button onClick={onClose} style={{ background: "none", border: "none", fontSize: "24px", cursor: "pointer", color: "#9CA3AF" }}>×</button>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div style={{ flex: 1, overflowY: "auto", padding: "16px" }}>
|
|
36
|
+
{notifications.length === 0 ? (
|
|
37
|
+
<div style={{ textAlign: "center", color: "#9CA3AF", marginTop: "40px" }}>{t("EKYC_NO_NEW_ALERTS")}</div>
|
|
38
|
+
) : (
|
|
39
|
+
notifications.map((alert, idx) => (
|
|
40
|
+
<div key={idx} style={{
|
|
41
|
+
padding: "16px",
|
|
42
|
+
borderRadius: "12px",
|
|
43
|
+
background: alert.priority === "HIGH" ? "#FEF2F2" : "#F9FAFB",
|
|
44
|
+
border: `1px solid ${alert.priority === "HIGH" ? "#FECACA" : "#E5E7EB"}`,
|
|
45
|
+
marginBottom: "12px"
|
|
46
|
+
}}>
|
|
47
|
+
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: "4px" }}>
|
|
48
|
+
<span style={{
|
|
49
|
+
fontSize: "10px",
|
|
50
|
+
fontWeight: "800",
|
|
51
|
+
padding: "2px 6px",
|
|
52
|
+
borderRadius: "4px",
|
|
53
|
+
background: alert.priority === "HIGH" ? "#EF4444" : "#3B82F6",
|
|
54
|
+
color: "#FFFFFF"
|
|
55
|
+
}}>
|
|
56
|
+
{alert.priority}
|
|
57
|
+
</span>
|
|
58
|
+
<span style={{ fontSize: "11px", color: "#9CA3AF" }}>{alert.time}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<h4 style={{ fontSize: "14px", fontWeight: "600", color: "#374151", marginBottom: "4px" }}>{t(alert.title)}</h4>
|
|
61
|
+
<p style={{ fontSize: "13px", color: "#6B7280" }}>{t(alert.message)}</p>
|
|
62
|
+
</div>
|
|
63
|
+
))
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div style={{ padding: "16px", borderTop: "1px solid #F3F4F6" }}>
|
|
68
|
+
<button style={{ width: "100%", padding: "12px", borderRadius: "8px", background: "#F3F4F6", color: "#374151", border: "none", fontWeight: "600", cursor: "pointer" }}>
|
|
69
|
+
{t("EKYC_MARK_ALL_READ")}
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export default NotificationPanel;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useTranslation } from "react-i18next";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Visual widget for SLA and processing performance tracking.
|
|
6
|
+
*/
|
|
7
|
+
const SLAWidget = ({ slaPercentage, avgTime, breachedCount }) => {
|
|
8
|
+
const { t } = useTranslation();
|
|
9
|
+
|
|
10
|
+
const getStatusColor = (pct) => {
|
|
11
|
+
if (pct > 90) return "#10B981";
|
|
12
|
+
if (pct > 75) return "#F59E0B";
|
|
13
|
+
return "#EF4444";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="sla-widget-card glass-card" style={{ padding: "28px" }}>
|
|
18
|
+
<h3 style={{ fontSize: "18px", fontWeight: "800", color: "#111827", marginBottom: "28px", letterSpacing: "-0.025em" }}>{t("EKYC_SLA_PERFORMANCE")}</h3>
|
|
19
|
+
|
|
20
|
+
<div style={{ display: "flex", alignItems: "center", gap: "24px", marginBottom: "24px" }}>
|
|
21
|
+
<div style={{ position: "relative", width: "100px", height: "100px", display: "flex", alignItems: "center", justifyContent: "center" }}>
|
|
22
|
+
<svg width="100" height="100" viewBox="0 0 100 100">
|
|
23
|
+
<circle cx="50" cy="50" r="40" stroke="#F3F4F6" strokeWidth="8" fill="none" />
|
|
24
|
+
<circle
|
|
25
|
+
cx="50" cy="50" r="40"
|
|
26
|
+
stroke={getStatusColor(slaPercentage)}
|
|
27
|
+
strokeWidth="8"
|
|
28
|
+
fill="none"
|
|
29
|
+
strokeDasharray={`${slaPercentage * 2.51} 251`}
|
|
30
|
+
transform="rotate(-90 50 50)"
|
|
31
|
+
strokeLinecap="round"
|
|
32
|
+
/>
|
|
33
|
+
<text x="50" y="55" textAnchor="middle" fontSize="18" fontWeight="700" fill="#111827">{slaPercentage}%</text>
|
|
34
|
+
</svg>
|
|
35
|
+
</div>
|
|
36
|
+
<div>
|
|
37
|
+
<div style={{ fontSize: "14px", color: "#6B7280" }}>{t("EKYC_SLA_COMPLIANCE")}</div>
|
|
38
|
+
<div style={{ fontSize: "20px", fontWeight: "700", color: "#111827" }}>{t("EKYC_OPTIMAL_PERFORMANCE")}</div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "16px", borderTop: "1px solid #F3F4F6", paddingTop: "20px" }}>
|
|
43
|
+
<div>
|
|
44
|
+
<div style={{ fontSize: "12px", color: "#6B7280", marginBottom: "4px" }}>{t("EKYC_AVG_LATENCY")}</div>
|
|
45
|
+
<div style={{ fontSize: "18px", fontWeight: "700", color: "#111827" }}>{avgTime}h</div>
|
|
46
|
+
</div>
|
|
47
|
+
<div>
|
|
48
|
+
<div style={{ fontSize: "12px", color: "#6B7280", marginBottom: "4px" }}>{t("EKYC_BREACH_COUNT")}</div>
|
|
49
|
+
<div style={{ fontSize: "18px", fontWeight: "700", color: "#EF4444" }}>{breachedCount}</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default SLAWidget;
|