Skip to content

Commit 0b20b56

Browse files
committed
Merge branch 'module/analytics' into develop
2 parents 46bcb98 + fb5bd0b commit 0b20b56

File tree

3 files changed

+342
-6
lines changed

3 files changed

+342
-6
lines changed

client/my-app/src/app/(with-layout)/analytics/page.tsx

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,40 @@
11
import AnalyticsView from "@/components/features/analytics/AnalyticsView";
22
import TimeBasedAlertDistribution from "@/components/features/analytics/chart/TimeBasedAlertDistribution";
33
import AiAccuracyChart from "@/components/features/analytics/chart/AiAccuracyChart";
4+
import EventCount from "@/components/features/analytics/chart/EventCount";
5+
import AlertResolutionRate from "@/components/features/analytics/chart/AlertResolutionRate";
46

57
export default function Analytics() {
68
return (
79
<div className="space-y-6">
10+
11+
{/* Top Section */}
812
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-6">
913
<AnalyticsView />
1014
</div>
1115

12-
13-
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
14-
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6 mt-6">
15-
<TimeBasedAlertDistribution />
16+
{/* Main 2 Columns Layout */}
17+
<div className="grid grid-cols-1 lg:grid-cols-[2fr,1fr] gap-6">
18+
19+
{/* LEFT SIDE */}
20+
<div className="space-y-6">
21+
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
22+
<TimeBasedAlertDistribution />
23+
</div>
24+
25+
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
26+
<AiAccuracyChart />
27+
</div>
28+
</div>
29+
30+
{/* RIGHT SIDE */}
31+
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
32+
<EventCount />
1633
</div>
1734
</div>
1835

19-
2036
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
21-
<AiAccuracyChart />
37+
<AlertResolutionRate />
2238
</div>
2339
</div>
2440
);
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"use client";
2+
3+
import React from "react";
4+
import dynamic from "next/dynamic";
5+
import type { ApexOptions } from "apexcharts";
6+
7+
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
8+
ssr: false,
9+
});
10+
11+
// props เผื่อรองรับ custom height
12+
type Props = {
13+
height?: number;
14+
};
15+
16+
const AXIS_LABEL_STYLE = {
17+
colors: "#6b7280",
18+
fontSize: "12px",
19+
};
20+
21+
22+
const data = [
23+
{ month: "January", total: 65, closed: 80 },
24+
{ month: "February", total: 90, closed: 60 },
25+
{ month: "March", total: 55, closed: 10 },
26+
{ month: "April", total: 78, closed: 50 },
27+
{ month: "May", total: 50, closed: 45 },
28+
{ month: "June", total: 42, closed: 40 },
29+
{ month: "July", total: 67, closed: 20 },
30+
{ month: "August", total: 40, closed: 55 },
31+
{ month: "September", total: 52, closed: 90 },
32+
{ month: "October", total: 64, closed: 78 },
33+
{ month: "November", total: 48, closed: 25 },
34+
{ month: "December", total: 88, closed: 15 },
35+
];
36+
37+
38+
const chartSeries = [
39+
{ name: "Total Alerts", data: data.map((d) => d.total) },
40+
{ name: "Closed Alerts", data: data.map((d) => d.closed) },
41+
];
42+
43+
44+
const chartOptions: ApexOptions = {
45+
chart: {
46+
type: "line",
47+
zoom: { enabled: false },
48+
toolbar: { show: false },
49+
background: "transparent",
50+
parentHeightOffset: 0,
51+
},
52+
53+
stroke: {
54+
curve: "straight",
55+
width: 3,
56+
},
57+
58+
markers: {
59+
size: 5,
60+
strokeWidth: 2,
61+
colors: ["#ffffff"],
62+
strokeColors: ["#3b82f6", "#ef4444"],
63+
hover: { size: 7 },
64+
},
65+
66+
colors: ["#3b82f6", "#ef4444"],
67+
68+
grid: {
69+
borderColor: "#e5e7eb",
70+
strokeDashArray: 4,
71+
xaxis: { lines: { show: true } },
72+
yaxis: { lines: { show: true } },
73+
},
74+
75+
xaxis: {
76+
categories: data.map((d) => d.month),
77+
labels: { style: AXIS_LABEL_STYLE },
78+
axisBorder: { show: false },
79+
axisTicks: { show: false },
80+
},
81+
82+
yaxis: {
83+
min: 0,
84+
max: 100,
85+
tickAmount: 5,
86+
labels: { style: AXIS_LABEL_STYLE },
87+
},
88+
89+
tooltip: {
90+
theme: "light",
91+
shared: true,
92+
intersect: false,
93+
},
94+
95+
legend: {
96+
show: false,
97+
},
98+
};
99+
100+
101+
export default function AlertResolutionRate({ height = 250 }: Props) {
102+
return (
103+
<div className="w-full rounded-3xl bg-white px-6 py-5 shadow-sm border border-[#e5e7f5]">
104+
<div className="mb-3 flex items-center justify-between">
105+
<h2 className="text-lg font-bold text-[var(--color-primary,#111827)]">
106+
Alert Resolution Rate
107+
</h2>
108+
</div>
109+
110+
{/* Chart */}
111+
<div className="w-full">
112+
<ReactApexChart
113+
options={chartOptions}
114+
series={chartSeries}
115+
type="line"
116+
height={height}
117+
/>
118+
</div>
119+
120+
{/* Legend */}
121+
<div className="mt-4 flex flex-wrap items-center justify-center gap-3 text-xs text-slate-600">
122+
<div className="flex items-center gap-2">
123+
<span className="h-2.5 w-2.5 rounded-full bg-[#3b82f6]" />
124+
<span className="whitespace-nowrap">Total Alerts</span>
125+
</div>
126+
<div className="flex items-center gap-2">
127+
<span className="h-2.5 w-2.5 rounded-full bg-[#ef4444]" />
128+
<span className="whitespace-nowrap">Closed Alerts</span>
129+
</div>
130+
</div>
131+
</div>
132+
);
133+
}
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
"use client";
2+
3+
import React from "react";
4+
import dynamic from "next/dynamic";
5+
import type { ApexOptions } from "apexcharts";
6+
7+
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
8+
ssr: false,
9+
});
10+
11+
type EventCountChartProps = {
12+
height?: number;
13+
};
14+
15+
type ApexSeries = NonNullable<ApexOptions["series"]>;
16+
17+
// ===== DATA =====
18+
const EVENTS = [
19+
"Event 1",
20+
"Event 2",
21+
"Event 3",
22+
"Event 4",
23+
"Event 5",
24+
"Event 6",
25+
"Event 7",
26+
"Event 8",
27+
"Event 9",
28+
"Event 10",
29+
"Event 11",
30+
"Event 12",
31+
"Event 13",
32+
"Event 14",
33+
"Event 15",
34+
"Event 16",
35+
];
36+
37+
const EVENT_COUNTS = [
38+
80, 35, 70, 65, 20, 55, 75, 95, 60, 80, 30, 15, 45, 45, 90, 50,
39+
];
40+
41+
// ===== Scroll / height settings =====
42+
const MAX_VISIBLE_EVENTS = 10; // จำนวนแถวที่โชว์ก่อนจะเริ่ม scroll
43+
const ROW_HEIGHT = 38; // px ต่อแถว
44+
const NEED_SCROLL = EVENTS.length > MAX_VISIBLE_EVENTS;
45+
46+
const computedMaxHeight = NEED_SCROLL
47+
? MAX_VISIBLE_EVENTS * ROW_HEIGHT
48+
: undefined;
49+
50+
const fullChartHeight = Math.max(EVENTS.length * ROW_HEIGHT, 120);
51+
52+
const AXIS_LABEL_STYLE = {
53+
colors: "#6b7280",
54+
fontSize: "12px",
55+
};
56+
57+
// ===== MAX_VALUE (อิงจาก data) =====
58+
// หา max จาก data แล้วเผื่อ 10% เพื่อไม่ให้แท่งแตะขอบแกนพอดี
59+
const maxData = Math.max(...EVENT_COUNTS, 0);
60+
61+
// ถ้า maxData = 0 จะกันหาร 0/undefined
62+
const PADDING_RATIO = 1.1; // เผื่อ 10%
63+
const roundTo = 10; // ปัดเป็นเลขกลมตามหน่วยนี้ (10 => 270 -> 280)
64+
const padded = Math.ceil(maxData * PADDING_RATIO || 1);
65+
const MAX_VALUE = Math.max(
66+
padded % roundTo === 0 ? padded : Math.ceil(padded / roundTo) * roundTo,
67+
roundTo
68+
);
69+
const TOTAL_EVENTS = EVENT_COUNTS.reduce((sum, v) => sum + v, 0);
70+
71+
// ===== CHART OPTIONS =====
72+
const chartOptions = (maxValue: number): ApexOptions => ({
73+
chart: {
74+
type: "bar",
75+
stacked: true,
76+
toolbar: { show: false },
77+
background: "transparent",
78+
parentHeightOffset: 0,
79+
},
80+
81+
plotOptions: {
82+
bar: {
83+
horizontal: true,
84+
barHeight: "60%",
85+
borderRadius: 4,
86+
},
87+
},
88+
89+
dataLabels: {
90+
enabled: false,
91+
},
92+
93+
grid: {
94+
borderColor: "#e5e7eb",
95+
strokeDashArray: 4,
96+
xaxis: { lines: { show: true } },
97+
yaxis: { lines: { show: true } },
98+
},
99+
100+
xaxis: {
101+
position: "top",
102+
categories: EVENTS,
103+
min: 0,
104+
max: maxValue, // ใช้ MAX_VALUE ที่คำนวณแล้ว
105+
tickAmount: 10,
106+
labels: {
107+
style: AXIS_LABEL_STYLE,
108+
},
109+
axisBorder: { show: false },
110+
axisTicks: { show: false },
111+
},
112+
113+
yaxis: {
114+
labels: {
115+
style: AXIS_LABEL_STYLE,
116+
},
117+
},
118+
119+
tooltip: {
120+
theme: "light",
121+
y: {
122+
formatter: (val: number) => `${val}`,
123+
},
124+
},
125+
126+
legend: {
127+
show: false,
128+
},
129+
});
130+
131+
// ===== SERIES (ค่าจริง + remaining) =====
132+
const chartSeries: ApexSeries = [
133+
{
134+
name: "Event Count",
135+
data: EVENT_COUNTS,
136+
color: "#a0a1ff",
137+
},
138+
{
139+
name: "Remaining",
140+
data: EVENT_COUNTS.map((v) => Math.max(0, MAX_VALUE - v)),
141+
color: "#f3f4f6",
142+
},
143+
];
144+
145+
export default function EventCountChart({ height = 380 }: EventCountChartProps) {
146+
const chartHeight = NEED_SCROLL ? fullChartHeight : height;
147+
148+
return (
149+
<div className="w-full rounded-3xl bg-white px-6 py-5 shadow-sm border border-[#e5e7f5]">
150+
<h2 className="text-lg font-bold text-[var(--color-primary,#111827)]">
151+
Event Count
152+
</h2>
153+
154+
{/* Scroll Container */}
155+
<div
156+
className="w-full md:overflow-visible overflow-x-auto"
157+
style={{
158+
maxHeight: computedMaxHeight ? `${computedMaxHeight}px` : "auto",
159+
overflowY: NEED_SCROLL ? "auto" : "visible",
160+
}}
161+
>
162+
<div className="min-w-[700px] md:min-w-0">
163+
<ReactApexChart
164+
options={chartOptions(MAX_VALUE)}
165+
series={chartSeries}
166+
type="bar"
167+
height={chartHeight}
168+
/>
169+
</div>
170+
</div>
171+
172+
{/* Legend */}
173+
<div className="mt-4 flex flex-wrap items-center justify-center gap-3 text-xs text-slate-600">
174+
<div className="flex items-center gap-2">
175+
<span className="h-2.5 w-2.5 rounded-full bg-[#a0a1ff]" />
176+
<span className="whitespace-nowrap">Event Count</span>
177+
</div>
178+
<div className="flex items-center gap-2">
179+
{/* <span className="h-2.5 w-2.5 rounded-full bg-[#f3f4f6]" /> */}
180+
<span className="whitespace-nowrap">
181+
Total: {TOTAL_EVENTS.toLocaleString()}
182+
</span>
183+
</div>
184+
</div>
185+
</div>
186+
);
187+
}

0 commit comments

Comments
 (0)