Skip to content

Commit 380c55f

Browse files
committed
Merge branch 'develop'
2 parents ce3b81f + 920c486 commit 380c55f

38 files changed

+2736
-321
lines changed

client/my-app/package-lock.json

Lines changed: 46 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

client/my-app/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@radix-ui/react-slot": "^1.2.3",
2121
"@radix-ui/react-switch": "^1.2.6",
2222
"@radix-ui/react-tabs": "^1.1.13",
23+
"@radix-ui/react-tooltip": "^1.2.8",
2324
"apexcharts": "^5.3.4",
2425
"class-variance-authority": "^0.7.1",
2526
"clsx": "^2.1.1",
@@ -30,7 +31,8 @@
3031
"react": "19.1.0",
3132
"react-apexcharts": "^1.7.0",
3233
"react-dom": "19.1.0",
33-
"tailwind-merge": "^3.3.1"
34+
"tailwind-merge": "^3.3.1",
35+
"tailwindcss-animate": "^1.0.7"
3436
},
3537
"devDependencies": {
3638
"@eslint/eslintrc": "^3",
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"use client";
2+
3+
import AccessControl from "@/app/components/CameraAccess";
4+
5+
export default function Page() {
6+
return (
7+
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
8+
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-6">
9+
<AccessControl />
10+
</div>
11+
</div>
12+
);
13+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export default function Page() {
44
return (
55
<div className="rounded-lg bg-[var(--color-white)] shadow-md p-6">
66
<div className="grid grid-cols-[repeat(auto-fit,minmax(320px,1fr))] gap-6">
7-
7+
88
</div>
99
</div>
1010
);

client/my-app/src/app/components/Alerts/AlertTable.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export default function AlertTable({ alerts }: Props) {
128128
<Table className="table-auto w-full">
129129
<TableHeader>
130130
<TableRow className="border-b border-[var(--color-primary)]">
131-
<TableHead onClick={() => handleSort("severity")} className="cursor-pointer select-none text-[var(--color-primary)]">
131+
<TableHead onClick={() => handleSort("severity")} className="cursor-pointer select-none ]">
132132
<div className="flex items-center justify-between pr-3 border-r border-[var(--color-primary)] w-full">
133133
<span>Severity</span>
134134
{renderSortIcon("severity")}
@@ -223,7 +223,7 @@ export default function AlertTable({ alerts }: Props) {
223223
<>
224224
<button className="inline-flex items-center justify-center gap-2 px-3 py-1 rounded-sm bg-white border border-[var(--color-success)] text-[var(--color-success)] hover:bg-[var(--color-success)] hover:border-[var(--color-success)] hover:text-white transition focus:outline-none focus:ring-2 focus:ring-offset-2">
225225
<CheckCircle2 className="h-4 w-4" aria-hidden="true" />
226-
<span>Resolved</span>
226+
<span>Resolve</span>
227227
</button>
228228
<button className="inline-flex items-center justify-center gap-2 px-3 py-1 rounded-sm bg-white border border-[var(--color-danger)] text-[var(--color-danger)] hover:bg-[var(--color-danger)] hover:border-[var(--color-danger)] hover:text-white transition focus:outline-none focus:ring-2 focus:ring-offset-2">
229229
<XCircle className="h-4 w-4" aria-hidden="true" />

client/my-app/src/app/components/Alerts/Chart/Trends.tsx

Lines changed: 43 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,38 @@
33
import React, { useEffect, useMemo, useState } from "react";
44
import dynamic from "next/dynamic";
55
import type { ApexOptions } from "apexcharts";
6-
76
const ReactApexChart = dynamic(() => import("react-apexcharts"), { ssr: false });
87

98
type Severity = "Critical" | "High" | "Medium" | "Low";
109
type TrendPoint = { severity: Severity; count: number };
1110
type TrendItem = { date: string; trend: TrendPoint[] };
12-
1311
type QuickRange = "3d" | "7d" | "30d";
1412

1513
const SEVERITIES: Severity[] = ["Critical", "High", "Medium", "Low"];
14+
const TZ = "Asia/Bangkok";
1615

17-
function toYMD(d: Date): string {
18-
const y = d.getFullYear();
19-
const m = (`0${d.getMonth() + 1}`).slice(0 - 2);
20-
const dd = (`0${d.getDate()}`).slice(0 - 2);
21-
return `${y}-${m}-${dd}`;
16+
/** format เป็น YYYY-MM-DD ตามโซนเวลาเป้าหมาย (ไม่พึ่ง getFullYear/getMonth) */
17+
function toYMD_TZ(value: number | Date, tz = TZ): string {
18+
const d = typeof value === "number" ? new Date(value) : value;
19+
return new Intl.DateTimeFormat("en-CA", { timeZone: tz }).format(d);
2220
}
2321

24-
function getRangeDates(range: QuickRange): { start: string; end: string } {
25-
const now = new Date(); // ใช้เวลาเครื่อง (Asia/Bangkok ในโปรเจ็กต์คุณอยู่แล้ว)
26-
const end = toYMD(now);
27-
const startDate = new Date(now);
22+
/** คำนวณช่วงวันฝั่ง client เท่านั้น (ป้องกัน SSR/CSR ไม่ตรงกัน) */
23+
function getRangeDatesClient(range: QuickRange, tz = TZ): { start: string; end: string } {
2824
const delta = range === "3d" ? 3 : range === "7d" ? 7 : 30;
29-
startDate.setDate(now.getDate() - (delta - 1)); // รวมวันนี้ด้วย
30-
const start = toYMD(startDate);
25+
const now = Date.now();
26+
const end = toYMD_TZ(now, tz);
27+
const start = toYMD_TZ(now - (delta - 1) * 86400000, tz); // Bangkok ไม่มี DST ใช้ 86400000 ได้
3128
return { start, end };
3229
}
3330

34-
// ไล่เฉดสีจากสีหลัก (โทนเดียวคนละความเข้ม)
31+
/** ไล่เฉดสีจากสีหลัก (โทนเดียวคนละความเข้ม) */
3532
function generateMonochromeShades(base: string): string[] {
36-
// รองรับรูปแบบ #RRGGBB
3733
const hex = base.replace("#", "");
3834
const r = parseInt(hex.slice(0, 2), 16);
3935
const g = parseInt(hex.slice(2, 4), 16);
4036
const b = parseInt(hex.slice(4, 6), 16);
41-
4237
const mix = (t: number) => {
43-
// t = 0 (เข้ม) → 1 (อ่อน)
4438
const bg = 255;
4539
const rr = Math.round(r * (1 - t) + bg * t);
4640
const gg = Math.round(g * (1 - t) + bg * t);
@@ -49,14 +43,22 @@ function generateMonochromeShades(base: string): string[] {
4943
.toString(16)
5044
.padStart(2, "0")}${bb.toString(16).padStart(2, "0")}`.toUpperCase();
5145
};
52-
53-
// 4 เฉดจากเข้ม → อ่อน (ปรับค่าตามรสนิยม)
5446
return [mix(0.15), mix(0.35), mix(0.55), mix(0.78)];
5547
}
5648

49+
/** ย่อ label โดยไม่พึ่ง Date (กัน timezone เพี้ยน) */
50+
function shortLabel(ymd: string): string {
51+
if (!/^\d{4}-\d{2}-\d{2}$/.test(ymd)) return ymd;
52+
const [y, m, d] = ymd.split("-");
53+
const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];
54+
return `${d} ${months[Number(m) - 1]}`;
55+
}
56+
5757
export default function AlertsTrendChart(): React.ReactElement {
5858
const [range, setRange] = useState<QuickRange>("7d");
59-
const [{ start, end }, setDates] = useState(getRangeDates("7d"));
59+
60+
// เริ่มต้นเป็นค่าว่าง → ค่อยคำนวณบน client ใน useEffect (กัน hydration mismatch)
61+
const [{ start, end }, setDates] = useState<{ start: string; end: string }>({ start: "", end: "" });
6062

6163
const [raw, setRaw] = useState<TrendItem[]>([]);
6264
const [series, setSeries] = useState<NonNullable<ApexOptions["series"]>>([]);
@@ -72,19 +74,18 @@ export default function AlertsTrendChart(): React.ReactElement {
7274
return generateMonochromeShades(primary);
7375
}, []);
7476

75-
// เปลี่ยนช่วงวันอัปเดตวันที่
77+
// เปลี่ยนช่วงคำนวณวันที่ฝั่ง client เท่านั้น
7678
useEffect(() => {
77-
setDates(getRangeDates(range));
79+
setDates(getRangeDatesClient(range));
7880
}, [range]);
7981

80-
// ดึงข้อมูล (ลองส่ง start/end ไปด้วย; ถ้าแบ็กเอนด์ยังไม่รับ ก็ยังกรองฝั่ง client ต่อ)
82+
// ดึงข้อมูลเมื่อมี start/end แล้ว
8183
useEffect(() => {
84+
if (!start || !end) return;
8285
(async () => {
8386
try {
84-
const url = `/api/alerts/analytics/trend?start=${encodeURIComponent(
85-
start
86-
)}&end=${encodeURIComponent(end)}`;
87-
const res = await fetch(url);
87+
const url = `/api/alerts/analytics/trend?start=${encodeURIComponent(start)}&end=${encodeURIComponent(end)}`;
88+
const res = await fetch(url, { cache: "no-store" });
8889
const data: TrendItem[] = await res.json();
8990
setRaw(data);
9091
} catch (e) {
@@ -94,12 +95,12 @@ export default function AlertsTrendChart(): React.ReactElement {
9495
})();
9596
}, [start, end]);
9697

97-
// แปลงข้อมูล → categories + series (กรองตามช่วงวันอีกชั้น เผื่อ API ไม่รองรับ)
98+
// map raw → categories + series (กรองซ้ำตามช่วง)
9899
useEffect(() => {
100+
if (!start || !end) return;
101+
99102
const inRange = raw.filter((d) => d.date >= start && d.date <= end);
100-
const sorted = [...inRange].sort(
101-
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()
102-
);
103+
const sorted = [...inRange].sort((a, b) => a.date.localeCompare(b.date));
103104
const cats = sorted.map((d) => d.date);
104105

105106
const getCount = (arr: TrendPoint[], sev: Severity) =>
@@ -117,20 +118,9 @@ export default function AlertsTrendChart(): React.ReactElement {
117118
}, [raw, start, end]);
118119

119120
const options: ApexOptions = {
120-
chart: {
121-
type: "bar",
122-
stacked: true,
123-
toolbar: { show: true },
124-
zoom: { enabled: true },
125-
},
126-
colors: palette, // โทนเดียวต่างเฉด
127-
plotOptions: {
128-
bar: {
129-
horizontal: false,
130-
borderRadius: 6,
131-
columnWidth: "55%",
132-
},
133-
},
121+
chart: { type: "bar", stacked: true, toolbar: { show: true }, zoom: { enabled: true } },
122+
colors: palette,
123+
plotOptions: { bar: { horizontal: false, borderRadius: 6, columnWidth: "55%" } },
134124
dataLabels: { enabled: false },
135125
legend: {
136126
position: "bottom",
@@ -143,35 +133,16 @@ export default function AlertsTrendChart(): React.ReactElement {
143133
categories,
144134
labels: {
145135
style: { colors: Array(categories.length).fill("#6B7280"), fontSize: "12px" },
146-
// ถ้าอยากย่อรูปแบบวันที่: 2025-08-15 → 15 Aug
147-
formatter: (val: string) => {
148-
if (!/^\d{4}-\d{2}-\d{2}$/.test(val)) return val;
149-
const d = new Date(val + "T00:00:00");
150-
return new Intl.DateTimeFormat("en-GB", {
151-
day: "2-digit",
152-
month: "short",
153-
}).format(d);
154-
},
136+
formatter: (val: string) => shortLabel(val),
155137
},
156138
axisBorder: { color: "#E5E7EB" },
157139
axisTicks: { color: "#E5E7EB" },
158140
},
159-
yaxis: {
160-
labels: { style: { colors: ["#6B7280"], fontSize: "12px" } },
161-
},
162-
grid: {
163-
borderColor: "#F3F4F6",
164-
yaxis: { lines: { show: true } },
165-
xaxis: { lines: { show: false } },
166-
},
141+
yaxis: { labels: { style: { colors: ["#6B7280"], fontSize: "12px" } } },
142+
grid: { borderColor: "#F3F4F6", yaxis: { lines: { show: true } }, xaxis: { lines: { show: false } } },
167143
fill: { opacity: 1 },
168144
tooltip: { theme: "light" },
169-
responsive: [
170-
{
171-
breakpoint: 640,
172-
options: { plotOptions: { bar: { columnWidth: "65%" } } },
173-
},
174-
],
145+
responsive: [{ breakpoint: 640, options: { plotOptions: { bar: { columnWidth: "65%" } } } }],
175146
};
176147

177148
return (
@@ -192,11 +163,13 @@ export default function AlertsTrendChart(): React.ReactElement {
192163
</button>
193164
))}
194165

195-
<span className="ml-auto text-xs text-gray-500">
196-
Range: {start}{end}
166+
{/* ใช้ suppressHydrationWarning กันกรณี edge ที่ SSR/CSR ต่างวันขณะเที่ยงคืน */}
167+
<span className="ml-auto text-xs text-gray-500" suppressHydrationWarning>
168+
Range: {start || "—"}{end || "—"}
197169
</span>
198170
</div>
199171

172+
{/* chart แสดงเฉพาะตอน client อยู่แล้วเพราะ react-apexcharts ssr:false */}
200173
<ReactApexChart options={options} series={series} type="bar" height={350} />
201174
</div>
202175
);

0 commit comments

Comments
 (0)