diff --git a/client/my-app/package-lock.json b/client/my-app/package-lock.json index b4119f4c..9b209416 100644 --- a/client/my-app/package-lock.json +++ b/client/my-app/package-lock.json @@ -1958,7 +1958,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.js/-/svg.js-3.2.4.tgz", "integrity": "sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==", "license": "MIT", - "peer": true, "funding": { "type": "github", "url": "https://github.com/sponsors/Fuzzyma" @@ -1982,7 +1981,6 @@ "resolved": "https://registry.npmjs.org/@svgdotjs/svg.select.js/-/svg.select.js-4.0.3.tgz", "integrity": "sha512-qkMgso1sd2hXKd1FZ1weO7ANq12sNmQJeGDjs46QwDVsxSRcHmvWKL2NDF7Yimpwf3sl5esOLkPqtV2bQ3v/Jg==", "license": "MIT", - "peer": true, "engines": { "node": ">= 14.18" }, @@ -2323,7 +2321,6 @@ "integrity": "sha512-WmdoynAX8Stew/36uTSVMcLJJ1KRh6L3IZRx1PZ7qJtBqT3dYTgyDTx8H1qoRghErydW7xw9mSJ3wS//tCRpFA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.0.2" } @@ -2334,7 +2331,6 @@ "integrity": "sha512-i5ZzwYpqjmrKenzkoLM2Ibzt6mAsM7pxB6BCIouEVVmgiqaMj1TjaK7hnA36hbW5aZv20kx7Lw6hWzPWg0Rurw==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.0.0" } @@ -2385,7 +2381,6 @@ "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.39.0", "@typescript-eslint/types": "8.39.0", @@ -2909,7 +2904,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2965,7 +2959,6 @@ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.3.4.tgz", "integrity": "sha512-N0gNh8uLu/BN8N+BCphNK+gZAoSoUtDDn1jFGB+3+EMcv8s6vajuP3W0g4dMLTRp6chFkjMmQK3uD8pz4ISmLA==", "license": "SEE LICENSE IN LICENSE", - "peer": true, "dependencies": { "@svgdotjs/svg.draggable.js": "^3.0.4", "@svgdotjs/svg.filter.js": "^3.0.8", @@ -3865,7 +3858,6 @@ "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", @@ -4040,7 +4032,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6189,7 +6180,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -6212,7 +6202,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.26.0" }, @@ -6919,8 +6908,7 @@ "version": "4.1.11", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tailwindcss-animate": { "version": "1.0.7", @@ -6997,7 +6985,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -7157,7 +7144,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/client/my-app/src/app/(with-layout)/reports/page.tsx b/client/my-app/src/app/(with-layout)/reports/page.tsx index 601fe970..073d1180 100644 --- a/client/my-app/src/app/(with-layout)/reports/page.tsx +++ b/client/my-app/src/app/(with-layout)/reports/page.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import GeneratePDFReports from "@/components/forms/GeneratePDFReports"; import GenerateScheduleReports from "@/components/forms/GenerateScheduleReports"; +import ReportMenu from "@/components/features/reports/ReportMenu"; export default function Reports() { const [open, setOpen] = useState(false); @@ -15,6 +16,7 @@ export default function Reports() { + ); } \ No newline at end of file diff --git a/client/my-app/src/components/features/reports/ReportHistoryTable.tsx b/client/my-app/src/components/features/reports/ReportHistoryTable.tsx new file mode 100644 index 00000000..a88b3ebb --- /dev/null +++ b/client/my-app/src/components/features/reports/ReportHistoryTable.tsx @@ -0,0 +1,295 @@ +import { useState, useEffect } from "react"; +import { ArrowUpDown, ArrowUp, ArrowDown, Download, Info, Clock, FileText } from "lucide-react"; + +type SortKey = "report" | "recipient" | "sentDate" | "format" | "size" | "status"; +type SortOrder = "asc" | "desc" | null; + +type ReportHistory = { + rh_id: number; + rpt_name: string; + rpt_description: string; + sent_date: string; + emails: string; + email_count: string; + file_size: string; + status: string; +}; + +interface IconActionProps { + label: string; + children: React.ReactNode; + variant?: "primary" | "info"; + onClick: () => void; +} + +type StatusType = "Success" | "Processing" | "Failed"; + +interface StatusBadgeProps { + status: StatusType; +} + +const IconAction = ({ label, children, variant = "primary", onClick }: IconActionProps) => { + const palette = + variant === "info" + ? "border-sky-500 text-sky-500 hover:bg-sky-500 focus:ring-sky-500" + : "border-blue-500 text-blue-500 hover:bg-blue-500 focus:ring-blue-500"; + + return ( +
+ +
+ {label} +
+
+ ); +}; + +const StatusBadge = ({ status }: StatusBadgeProps) => { + const statusConfig: Record = { + "Success": "bg-emerald-50 text-emerald-700 border-emerald-400", + "Processing": "bg-blue-50 text-blue-700 border-blue-400", + "Failed": "bg-red-50 text-red-700 border-red-400" + }; + + const config = statusConfig[status] || statusConfig["Processing"]; + + return ( +
+ {status} +
+ ); +}; + +const PDFBadge = () => { + return ( +
+ + PDF +
+ ); +}; + +export default function ReportHistory() { + const [sortKey, setSortKey] = useState(null); + const [sortOrder, setSortOrder] = useState(null); + const [reports, setReports] = useState([]); + + useEffect(() => { + const mockData: ReportHistory[] = [ + { + rh_id: 1, + rpt_name: "รายงานประจำวันระบบบริหารความปลอดภัย", + rpt_description: "สรุปกิจกรรมและเหตุการณ์ความปลอดภัย 18 ตุลาคม 2025", + sent_date: "2025-10-02 16:58", + emails: "admin001@gmail.com, ...", + email_count: "2", + file_size: "2.1 MB", + status: "Success" + }, + { + rh_id: 2, + rpt_name: "รายงานประจำวันระบบบริหารความปลอดภัย", + rpt_description: "สรุปกิจกรรมและเหตุการณ์ความปลอดภัย 18 ตุลาคม 2025", + sent_date: "2025-10-02 16:58", + emails: "admin001@gmail.com, ...", + email_count: "2", + file_size: "2.2 MB", + status: "Processing" + }, + { + rh_id: 3, + rpt_name: "รายงานประจำวันระบบบริหารความปลอดภัย", + rpt_description: "สรุปกิจกรรมและเหตุการณ์ความปลอดภัย 18 ตุลาคม 2025", + sent_date: "2025-10-02 16:58", + emails: "admin001@gmail.com, ...", + email_count: "2", + file_size: "2.3 MB", + status: "Processing" + }, + { + rh_id: 4, + rpt_name: "รายงานประจำวันระบบบริหารความปลอดภัย", + rpt_description: "สรุปกิจกรรมและเหตุการณ์ความปลอดภัย 18 ตุลาคม 2025", + sent_date: "2025-10-01 16:58", + emails: "admin001@gmail.com, ...", + email_count: "2", + file_size: "2.4 MB", + status: "Failed" + }, + { + rh_id: 5, + rpt_name: "รายงานประจำวันระบบบริหารความปลอดภัย", + rpt_description: "สรุปกิจกรรมและเหตุการณ์ความปลอดภัย 18 ตุลาคม 2025", + sent_date: "2025-10-02 16:58", + emails: "admin001@gmail.com, ...", + email_count: "2", + file_size: "2.5 MB", + status: "Success" + } + ]; + + setReports(mockData); + }, []); + + const renderSortIcon = (key: SortKey) => { + if (sortKey !== key || !sortOrder) return ; + if (sortOrder === "asc") return ; + return ; + }; + + const handleSort = (key: SortKey) => { + if (sortKey !== key) { + setSortKey(key); + setSortOrder("asc"); + } else { + if (sortOrder === "asc") setSortOrder("desc"); + else if (sortOrder === "desc") { + setSortKey(null); + setSortOrder(null); + } else setSortOrder("asc"); + } + }; + + const getSortedReports = () => { + if (!sortKey || !sortOrder) return reports; + + return [...reports].sort((a, b) => { + let aValue: string | number = ""; + let bValue: string | number = ""; + + switch (sortKey) { + case "report": + aValue = a.rpt_name; + bValue = b.rpt_name; + break; + case "recipient": + aValue = Number(a.email_count); + bValue = Number(b.email_count); + break; + case "sentDate": + aValue = a.sent_date; + bValue = b.sent_date; + break; + case "format": + aValue = "PDF"; + bValue = "PDF"; + break; + case "size": + aValue = parseFloat(a.file_size); + bValue = parseFloat(b.file_size); + break; + case "status": + aValue = a.status; + bValue = b.status; + break; + } + + if (typeof aValue === "number" && typeof bValue === "number") { + return sortOrder === "asc" ? aValue - bValue : bValue - aValue; + } + + const comparison = String(aValue).localeCompare(String(bValue)); + return sortOrder === "asc" ? comparison : -comparison; + }); + }; + + return ( +
+
+ + + + {[ + { key: "report", label: "Report" }, + { key: "recipient", label: "Recipient" }, + { key: "sentDate", label: "Sent Date" }, + { key: "format", label: "Format" }, + { key: "size", label: "Size" }, + { key: "status", label: "Status" }, + ].map(({ key, label }) => ( + + ))} + + + + + + {getSortedReports().map((report) => ( + + + + + + + + + + + + + + + + ))} + +
handleSort(key as SortKey)} + className="cursor-pointer select-none text-blue-700 py-3 px-4 text-left" + > +
+ {label} + {renderSortIcon(key as SortKey)} +
+
Action
+
+
{report.rpt_name}
+
{report.rpt_description}
+
+
+
+
+ {report.email_count} recipient{Number(report.email_count) > 1 ? "s" : ""} +
+
{report.emails}
+
+
+
+ + {report.sent_date} +
+
+ + +
{report.file_size}
+
+ + +
+ console.log("Download", report.rh_id)} + > + + + + console.log("Info", report.rh_id)} + > + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/client/my-app/src/components/features/reports/ReportHistoryView.tsx b/client/my-app/src/components/features/reports/ReportHistoryView.tsx index 0e4ba98f..69089537 100644 --- a/client/my-app/src/components/features/reports/ReportHistoryView.tsx +++ b/client/my-app/src/components/features/reports/ReportHistoryView.tsx @@ -10,6 +10,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Button } from "@/components/ui/button"; +import ReportHistory from "./ReportHistoryTable"; /* ---------------- Mock Data (จำลองประวัติการส่งรายงาน) ---------------- */ @@ -235,7 +236,7 @@ export default function ReportHistoryView() { - {/* Table or List section can go here */} + ); } \ No newline at end of file diff --git a/client/my-app/src/components/features/reports/ReportMenu.tsx b/client/my-app/src/components/features/reports/ReportMenu.tsx index 8839d56c..9e1525eb 100644 --- a/client/my-app/src/components/features/reports/ReportMenu.tsx +++ b/client/my-app/src/components/features/reports/ReportMenu.tsx @@ -38,8 +38,6 @@ export default function ReportMenu() { - -