From 4d274f3382a6c8b74f35cf7c3a8b763e53675c60 Mon Sep 17 00:00:00 2001 From: soxzsoxz Date: Sat, 1 Nov 2025 00:33:43 +0700 Subject: [PATCH] front and back --- package-lock.json | 410 +++++++++++++++++++++++++++++- package.json | 12 +- src/App.jsx | 2 +- src/dashboard/Dashboard.css | 412 ++++++++++++++++++++++++++----- src/dashboard/Dashboard.jsx | 256 +++++++++++++++---- src/{form => dashboard}/Form.css | 0 src/dashboard/Form.jsx | 274 ++++++++++++++++++++ src/equipment/AddEq.jsx | 70 +++++- src/equipment/Equipment.css | 5 + src/equipment/Equipment.jsx | 184 ++++++++++---- src/form/Form.jsx | 112 --------- src/lib/supabaseClient.js | 7 + src/report/Report.css | 2 + src/report/Report.jsx | 132 ++++++++-- src/status/Status.css | 158 +++++++----- src/status/Status.jsx | 70 +++++- 16 files changed, 1736 insertions(+), 370 deletions(-) rename src/{form => dashboard}/Form.css (100%) create mode 100644 src/dashboard/Form.jsx delete mode 100644 src/form/Form.jsx create mode 100644 src/lib/supabaseClient.js diff --git a/package-lock.json b/package-lock.json index 4b89eeb..11e23ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,17 +7,21 @@ "": { "name": "codespaces-react", "version": "0.1.0", + "hasInstallScript": true, "dependencies": { + "@supabase/supabase-js": "^2.78.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.9.4", "web-vitals": "^3.1.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.7.0", + "cross-env": "^10.1.0", "jsdom": "^26.1.0", "vite": "^6.3.6", "vitest": "^3.0.7" @@ -469,6 +473,13 @@ "node": ">=18" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -1267,6 +1278,107 @@ "win32" ] }, + "node_modules/@supabase/auth-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.78.0.tgz", + "integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.78.0.tgz", + "integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + } + }, + "node_modules/@supabase/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/@supabase/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/@supabase/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz", + "integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.78.0.tgz", + "integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.78.0.tgz", + "integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==", + "license": "MIT", + "dependencies": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.78.0.tgz", + "integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.78.0", + "@supabase/functions-js": "2.78.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.78.0", + "@supabase/realtime-js": "2.78.0", + "@supabase/storage-js": "2.78.0" + } + }, "node_modules/@testing-library/dom": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz", @@ -1467,6 +1579,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "node_modules/@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -1508,6 +1626,15 @@ "@types/jest": "*" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/yargs": { "version": "17.0.19", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.19.tgz", @@ -1901,6 +2028,39 @@ "node": ">=18" } }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -2585,6 +2745,13 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, "node_modules/jest-diff": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", @@ -2982,6 +3149,16 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -3099,6 +3276,15 @@ "react": "^18.2.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -3272,6 +3458,29 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -3559,6 +3768,12 @@ "node": ">=18" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -3849,6 +4064,22 @@ "node": ">=18" } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -3917,7 +4148,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -4228,6 +4458,12 @@ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true }, + "@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "dev": true + }, "@esbuild/aix-ppc64": { "version": "0.25.0", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", @@ -4636,6 +4872,96 @@ "dev": true, "optional": true }, + "@supabase/auth-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.78.0.tgz", + "integrity": "sha512-cXDtu1U0LeZj/xfnFoV7yCze37TcbNo8FCxy1FpqhMbB9u9QxxDSW6pA5gm/07Ei7m260Lof4CZx67Cu6DPeig==", + "requires": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "@supabase/functions-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.78.0.tgz", + "integrity": "sha512-t1jOvArBsOINyqaRee1xJ3gryXLvkBzqnKfi6q3YRzzhJbGS6eXz0pXR5fqmJeB01fLC+1njpf3YhMszdPEF7g==", + "requires": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "@supabase/node-fetch": { + "version": "2.6.15", + "resolved": "https://registry.npmjs.org/@supabase/node-fetch/-/node-fetch-2.6.15.tgz", + "integrity": "sha512-1ibVeYUacxWYi9i0cf5efil6adJ9WRyZBLivgjs+AUpewx1F3xPi7gLgaASI2SmIQxPoCEjAsLAzKPgMJVgOUQ==", + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, + "@supabase/postgrest-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.78.0.tgz", + "integrity": "sha512-AwhpYlSvJ+PSnPmIK8sHj7NGDyDENYfQGKrMtpVIEzQA2ApUjgpUGxzXWN4Z0wEtLQsvv7g4y9HVad9Hzo1TNA==", + "requires": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "@supabase/realtime-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.78.0.tgz", + "integrity": "sha512-rCs1zmLe7of7hj4s7G9z8rTqzWuNVtmwDr3FiCRCJFawEoa+RQO1xpZGbdeuVvVmKDyVN6b542Okci+117y/LQ==", + "requires": { + "@supabase/node-fetch": "2.6.15", + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + } + }, + "@supabase/storage-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.78.0.tgz", + "integrity": "sha512-n17P0JbjHOlxqJpkaGFOn97i3EusEKPEbWOpuk1r4t00Wg06B8Z4GUiq0O0n1vUpjiMgJUkLIMuBVp+bEgunzQ==", + "requires": { + "@supabase/node-fetch": "2.6.15", + "tslib": "2.8.1" + } + }, + "@supabase/supabase-js": { + "version": "2.78.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.78.0.tgz", + "integrity": "sha512-xYMRNBFmKp2m1gMuwcp/gr/HlfZKqjye1Ib8kJe29XJNsgwsfO/f8skxnWiscFKTlkOKLuBexNgl5L8dzGt6vA==", + "requires": { + "@supabase/auth-js": "2.78.0", + "@supabase/functions-js": "2.78.0", + "@supabase/node-fetch": "2.6.15", + "@supabase/postgrest-js": "2.78.0", + "@supabase/realtime-js": "2.78.0", + "@supabase/storage-js": "2.78.0" + } + }, "@testing-library/dom": { "version": "8.19.1", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.19.1.tgz", @@ -4803,6 +5129,11 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.18.tgz", "integrity": "sha512-DHQpWGjyQKSHj3ebjFI/wRKcqQcdR+MoFBygntYOZytCqNfkd2ZC4ARDJ2DQqhjH5p85Nnd3jhUJIXrszFX/JA==" }, + "@types/phoenix": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz", + "integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A==" + }, "@types/prop-types": { "version": "15.7.5", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", @@ -4844,6 +5175,14 @@ "@types/jest": "*" } }, + "@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "requires": { + "@types/node": "*" + } + }, "@types/yargs": { "version": "17.0.19", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.19.tgz", @@ -5096,6 +5435,27 @@ "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==" }, + "cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "dev": true, + "requires": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5575,6 +5935,12 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==" }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, "jest-diff": { "version": "29.3.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", @@ -5861,6 +6227,12 @@ "entities": "^6.0.0" } }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, "pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -5934,6 +6306,12 @@ "scheduler": "^0.23.0" } }, + "react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "requires": {} + }, "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", @@ -6057,6 +6435,21 @@ "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -6275,6 +6668,11 @@ "punycode": "^2.3.1" } }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, "update-browserslist-db": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", @@ -6401,6 +6799,15 @@ "webidl-conversions": "^7.0.0" } }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, "which-boxed-primitive": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", @@ -6451,7 +6858,6 @@ "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "dev": true, "requires": {} }, "xml-name-validator": { diff --git a/package.json b/package.json index 6e95c36..310aee0 100644 --- a/package.json +++ b/package.json @@ -4,14 +4,15 @@ "private": true, "type": "module", "dependencies": { + "@supabase/supabase-js": "^2.78.0", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^14.4.3", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-icons": "^5.5.0", "react-router-dom": "^7.9.4", - "web-vitals": "^3.1.0", - "react-icons": "^5.0.1" + "web-vitals": "^3.1.0" }, "overrides": { "@svgr/webpack": "^8.0.1", @@ -19,11 +20,11 @@ "postcss": "^8.4.31" }, "scripts": { - "start": "BROWSER=none WDS_SOCKET_PORT=0 vite --port 3000", + "start": "cross-env BROWSER=none WDS_SOCKET_PORT=0 vite --port 3000", "build": "vite build", "preview": "vite preview", "test": "vitest", - "install": "npm install" + "install": "npm install" }, "eslintConfig": { "extends": [ @@ -45,8 +46,9 @@ }, "devDependencies": { "@vitejs/plugin-react": "^4.7.0", + "cross-env": "^10.1.0", "jsdom": "^26.1.0", "vite": "^6.3.6", "vitest": "^3.0.7" } -} \ No newline at end of file +} diff --git a/src/App.jsx b/src/App.jsx index f57b5e1..ff34608 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -3,7 +3,7 @@ import { Routes, Route, Outlet } from "react-router-dom"; // 2. Import หน้าต่าง ๆ import Dashboard from "./dashboard/Dashboard"; -import Form from "./form/Form"; +import Form from "./dashboard/Form.jsx"; import Status from "./status/Status.jsx"; import Login from "./login/Login.jsx"; import Report from "./report/Report.jsx"; diff --git a/src/dashboard/Dashboard.css b/src/dashboard/Dashboard.css index bc2358b..581762f 100644 --- a/src/dashboard/Dashboard.css +++ b/src/dashboard/Dashboard.css @@ -1,75 +1,113 @@ +/* ------------------------------------------------- + dashboard.css – แก้ "ค้นหา" ทับ + สีระดับใน Modal + ------------------------------------------------- */ +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Prompt:wght@400;500;600;700&display=swap'); + .dashboard { - padding: 30px; - background-color: #f4f5f7; + padding: 32px; + background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%); min-height: 100vh; - font-family: "Prompt", sans-serif; + font-family: 'Inter', 'Prompt', sans-serif; + color: #2d3748; } -/* ส่วนหัว */ +/* ---------- ส่วนหัว ---------- */ .header { display: flex; justify-content: space-between; align-items: center; - margin-bottom: 25px; + margin-bottom: 28px; + flex-wrap: wrap; + gap: 12px; } .header h2 { - font-size: 1.4rem; - color: #444; - font-weight: 600; + font-size: 1.5rem; + color: #1a202c; + font-weight: 700; + margin: 0; + letter-spacing: -0.5px; } .btn-new { - background-color: #009fe3; + background: linear-gradient(135deg, #0095e8, #0077b6); color: white; border: none; - padding: 10px 18px; - border-radius: 5px; - font-size: 1rem; + padding: 11px 20px; + border-radius: 8px; + font-size: 0.95rem; + font-weight: 600; cursor: pointer; + box-shadow: 0 3px 6px rgba(0, 149, 232, 0.2); + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 6px; } + .btn-new:hover { - background-color: #007bbf; + transform: translateY(-1px); + box-shadow: 0 5px 12px rgba(0, 149, 232, 0.3); } -/* การ์ดสรุป */ +/* ---------- การ์ดสรุป ---------- */ .cards { display: flex; gap: 20px; - margin-bottom: 25px; + margin-bottom: 30px; + flex-wrap: wrap; } .card { flex: 1; - background-color: white; - border-radius: 10px; - padding: 20px; + min-width: 180px; + background: white; + border-radius: 14px; + padding: 22px; text-align: center; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06); + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 5px; +} + +.card.orange::before { background: linear-gradient(90deg, #ff7b00, #ff9f1c); } +.card.green::before { background: linear-gradient(90deg, #2d6a4f, #4f9a7c); } +.card.purple::before { background: linear-gradient(90deg, #6a4c93, #8a6bb3); } + +.card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); } .card p { - font-size: 1rem; - margin-bottom: 8px; - color: #555; + font-size: 0.95rem; + color: #718096; + margin-bottom: 6px; + font-weight: 500; } .card h3 { - font-size: 1.5rem; + font-size: 1.8rem; font-weight: 700; margin: 0; + color: #2d3748; } -/* สีการ์ด */ -.card.orange { border-top: 6px solid orange; } -.card.green { border-top: 6px solid #32b57b; } -.card.purple { border-top: 6px solid #7b5cc4; } - -/* ตาราง */ +/* ---------- ตาราง ---------- */ .table-container { background-color: white; - border-radius: 10px; - box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1); + border-radius: 14px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); overflow: hidden; } @@ -77,20 +115,45 @@ display: flex; justify-content: space-between; align-items: center; - background-color: #00a6e6; + background: linear-gradient(135deg, #0077b6, #0095e8); color: white; - padding: 15px 20px; + padding: 16px 24px; + flex-wrap: wrap; + gap: 12px; } .table-header h3 { margin: 0; font-size: 1.2rem; + font-weight: 600; +} + +.search-box { + position: relative; + width: 220px; } .search-box input { - padding: 6px 10px; - border-radius: 4px; + padding: 9px 14px 9px 38px; /* เพิ่ม padding ด้านซ้ายเพื่อไม่ให้ทับไอคอน */ + border-radius: 8px; border: none; + width: 100%; + font-size: 0.95rem; + background: rgba(255, 255, 255, 0.95); + color: #2d3748; + box-sizing: border-box; +} + +.search-box::before { + content: ''; + font-family: "Material Icons"; + position: absolute; + left: 12px; + top: 50%; + transform: translateY(-50%); + color: #718096; + font-size: 1.1rem; + pointer-events: none; } /* ตารางข้อมูล */ @@ -99,42 +162,277 @@ table { border-collapse: collapse; } -th, td { - padding: 14px 20px; - border-bottom: 1px solid #eee; +th, +td { + padding: 15px 20px; + border-bottom: 1px solid #e2e8f0; text-align: left; - font-size: 0.95rem; + font-size: 0.94rem; + vertical-align: middle; } th { - background-color: #f8f9fa; + background-color: #f7fafc; font-weight: 600; - color: #555; + color: #4a5568; + font-size: 0.88rem; + text-transform: uppercase; + letter-spacing: 0.5px; } -/* การแบ่งหน้า */ -.pagination { - display: flex; - justify-content: flex-end; +tbody tr { + transition: background-color 0.2s ease; +} + +tbody tr:hover { + background-color: #f8fafc; +} + +/* ---------- เลขที่แจ้งซ่อม ---------- */ +.seq-no { + font-family: 'Courier New', monospace; + font-weight: 700; + color: #1a202c; + letter-spacing: 1.2px; + font-size: 1rem; +} + +/* ---------- ปุ่มจัดการ (เฉพาะช่าง) ---------- */ +.btn-action { + padding: 8px 14px; + border: none; + border-radius: 8px; + font-size: 0.9rem; + cursor: pointer; + font-weight: 600; + min-width: 88px; + transition: all 0.2s ease; + display: inline-flex; align-items: center; - padding: 12px 20px; - gap: 10px; + justify-content: center; + gap: 6px; +} + +.btn-start-repair { + background: linear-gradient(135deg, #f39c12, #e67e22); + color: white; + box-shadow: 0 2px 4px rgba(243, 156, 18, 0.2); +} +.btn-start-repair:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(243, 156, 18, 0.3); +} + +.btn-complete-repair { + background: linear-gradient(135deg, #27ae60, #219653); + color: white; + box-shadow: 0 2px 4px rgba(39, 174, 96, 0.2); +} +.btn-complete-repair:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(39, 174, 96, 0.3); +} + +.status-completed { + display: inline-flex; + align-items: center; + padding: 8px 14px; + font-size: 0.9rem; + font-weight: 600; + color: #155724; + background-color: #d4edda; + border: 1px solid #a2d5c6; + border-radius: 8px; + min-height: 38px; + min-width: 110px; + justify-content: center; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +/* ---------- ปุ่ม “รายละเอียด” ---------- */ +.btn-detail { + background: linear-gradient(135deg, #007bff, #0056b3); + color: white; + border: none; + padding: 8px 14px; + border-radius: 8px; font-size: 0.9rem; - color: #555; + cursor: pointer; + font-weight: 600; + min-width: 88px; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + box-shadow: 0 2px 4px rgba(0, 123, 255, 0.2); +} +.btn-detail:hover { + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3); +} + +/* ---------- Badge ---------- */ +.status-badge, +.level-badge { + padding: 7px 15px; + border-radius: 20px; + font-size: 0.82rem; + font-weight: 600; + text-align: center; + min-width: 92px; + display: inline-block; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.status-badge::before, +.level-badge::before { + content: ''; + position: absolute; + top: 0; left: 0; right: 0; bottom: 0; + background: linear-gradient(135deg, rgba(255,255,255,0.2), transparent); + border-radius: 20px; + pointer-events: none; +} + +.status-badge:hover, +.level-badge:hover { + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15); } -.dots { +/* สถานะ */ +.status-badge.pending { background: #fff3cd; color: #d39e00; border: 1px solid #ffeaa7; } +.status-badge.in-progress { background: #fff8e1; color: #e67e22; border: 1px solid #ffeaa7; } +.status-badge.completed { background: #d4edda; color: #2d6a4f; border: 1px solid #a2d5c6; } + +/* ระดับความสำคัญ */ +.level-badge.high { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } +.level-badge.medium { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } +.level-badge.low { background: #d4edda; color: #155724; border: 1px solid #a2d5c6; } + +/* ---------- จัดตำแหน่งคอลัมน์ “จัดการ” ---------- */ +.action-cell { + text-align: left; + vertical-align: middle; + padding: 12px 20px; + min-height: 56px; +} + +/* ---------- Modal ---------- */ +.modal-overlay { + position: fixed; + top: 0; left: 0; right: 0; bottom: 0; + background-color: rgba(0, 0, 0, 0.55); display: flex; - gap: 5px; + justify-content: center; + align-items: center; + z-index: 1000; + backdrop-filter: blur(4px); } -.dot { - width: 8px; - height: 8px; - border-radius: 50%; - background-color: #ccc; +.modal-content { + background-color: white; + padding: 32px; + border-radius: 16px; + max-width: 540px; + width: 92%; + box-shadow: 0 15px 40px rgba(0, 0, 0, 0.2); + animation: modalFadeIn 0.3s ease; } -.dot.active { - background-color: #00a6e6; +@keyframes modalFadeIn { + from { opacity: 0; transform: scale(0.95); } + to { opacity: 1; transform: scale(1); } +} + +.modal-title { + margin: 0 0 22px 0; + font-size: 1.45rem; + color: #1a202c; + text-align: center; + font-weight: 700; + border-bottom: 2px solid #e2e8f0; + padding-bottom: 14px; } + +.detail-grid { + display: grid; + grid-template-columns: 1fr 2fr; + gap: 16px 22px; + margin-bottom: 28px; + font-size: 1rem; +} + +.detail-item { + display: contents; +} + +.detail-label { + font-weight: 600; + color: #4a5568; + text-align: right; + font-size: 0.95rem; +} + +.detail-value { + color: #2d3748; + word-break: break-word; + font-weight: 500; +} + +/* แก้สีระดับความสำคัญใน Modal – ใช้สีจริงตามระดับ */ +.detail-value.level-สูง { color: #dc3545; font-weight: 700; } +.detail-value.level-ปานกลาง { color: #ffc107; font-weight: 700; } +.detail-value.level-ต่ำ { color: #28a745; font-weight: 700; } + +.btn-close-modal { + background: linear-gradient(135deg, #718096, #5a677a); + color: white; + border: none; + padding: 11px 24px; + border-radius: 10px; + cursor: pointer; + font-size: 1rem; + font-weight: 600; + display: block; + margin: 0 auto; + transition: all 0.2s ease; + box-shadow: 0 3px 6px rgba(113, 128, 150, 0.2); +} +.btn-close-modal:hover { + transform: translateY(-1px); + box-shadow: 0 5px 12px rgba(113, 128, 150, 0.3); +} + +/* แก้สีระดับความสำคัญใน Modal ให้เป็นดำ */ +.detail-grid .detail-item:nth-child(3) .detail-value { + color: #2d3748 !important; + font-weight: 600; +} + +/* ---------- Responsive ---------- */ +@media (max-width: 992px) { + .cards { gap: 16px; } + .table-header { padding: 14px 18px; } + th, td { padding: 12px 16px; } +} + +@media (max-width: 768px) { + .header, .table-header { flex-direction: column; align-items: stretch; } + .search-box { width: 100%; } + .search-box input { width: 100%; } + .cards { flex-direction: column; } + .detail-grid { grid-template-columns: 1fr; gap: 12px; } + .detail-label { text-align: left; font-weight: 700; color: #4a5568; } + .detail-value { margin-left: 8px; } +} + +@media (max-width: 480px) { + .dashboard { padding: 20px; } + .header h2 { font-size: 1.3rem; } + .modal-content { padding: 24px; } +} \ No newline at end of file diff --git a/src/dashboard/Dashboard.jsx b/src/dashboard/Dashboard.jsx index 10620ed..47d1f59 100644 --- a/src/dashboard/Dashboard.jsx +++ b/src/dashboard/Dashboard.jsx @@ -1,80 +1,250 @@ -import React from "react"; -import "./Dashboard.css"; +// src/dashboard/Dashboard.jsx +import React, { useState, useEffect } from "react"; +import "./dashboard.css"; +import Form from "./Form.jsx"; +import { useAuth } from "../context/AuthContext"; +import { supabase } from "../lib/supabaseClient"; + +const TABLE_NAME = "repairs"; const Dashboard = () => { - const data = [ - { id: "001", date: "02/09/2025", name: "ญาดา ปวีณชัย", detail: "หลอดไฟเสีย", status: "ระหว่างดำเนินการ" }, - { id: "002", date: "10/09/2025", name: "พิมพ์ชนก กมล", detail: "เครื่องปรับอากาศในที่ทำงาน", status: "เสร็จสิ้น" }, - ]; + const { role } = useAuth(); + const [showForm, setShowForm] = useState(false); + const [selectedDetail, setSelectedDetail] = useState(null); + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchRepairs(); + }, []); + + const fetchRepairs = async () => { + try { + setLoading(true); + const { data: repairs, error } = await supabase + .from(TABLE_NAME) + .select("*") + .order("day", { ascending: false }); + + if (error) throw error; + + const formatted = repairs.map((r, index) => ({ + id: r.id, + seqNo: String(index + 1).padStart(3, "0"), + date: new Date(r.day).toLocaleDateString("th-TH"), + name: r.name, + topic: r.topic, + type: r.type ?? "-", + info: r.info ?? "-", + call: r.call ?? "-", + place: r.place ?? "-", + status: r.status ?? "รอดำเนินการ", + level: r.level ?? "ปานกลาง", // เพิ่ม level + detailObj: { + topic: r.topic, + type: r.type ?? "-", + info: r.info ?? "-", + place: r.place ?? "-", + call: r.call ?? "-", + level: r.level ?? "ปานกลาง", // เพิ่มใน modal + }, + })); + + setData(formatted); + } catch (err) { + console.error(err); + setError("โหลดข้อมูลล้มเหลว: " + err.message); + } finally { + setLoading(false); + } + }; + + const changeStatus = async (id, newStatus) => { + try { + const { error } = await supabase + .from(TABLE_NAME) + .update({ status: newStatus }) + .eq("id", id); + + if (error) throw error; + + setData(prev => + prev.map(item => + item.id === id ? { ...item, status: newStatus } : item + ) + ); + } catch (err) { + console.error(err); + alert("อัปเดตสถานะล้มเหลว: " + err.message); + } + }; + + const renderActionButton = (item) => { + if (item.status === "รอดำเนินการ") { + return ( + + ); + } + if (item.status === "ระหว่างดำเนินการ") { + return ( + + ); + } + return เสร็จสิ้นแล้ว; + }; + + const waiting = data.filter(i => i.status === "รอดำเนินการ"); + const pending = data.filter(i => i.status === "ระหว่างดำเนินการ"); + const completed = data.filter(i => i.status === "เสร็จสิ้น"); + + const openDetail = (detailObj) => setSelectedDetail(detailObj); + const closeDetail = () => setSelectedDetail(null); - const waitingItems = data.filter((item) => item.status === "รอดำเนินการ"); - const pendingItems = data.filter((item) => item.status === "ระหว่างดำเนินการ"); - const completedItems = data.filter((item) => item.status === "เสร็จสิ้น"); + if (showForm) { + return
setShowForm(false)} onSuccess={fetchRepairs} />; + } + + if (loading) return
กำลังโหลด...
; + if (error) return
{error}
; return (
-

ภาพรวมรายการแจ้งซ่อมทั้งหมดของบริษัท

- +

ภาพรวมรายการแจ้งซ่อมทั้งหมด

+ {role === "user" && ( + + )}
-
-

รอดำเนินการ

-

{waitingItems.length} รายการ

-
-
-

ระหว่างดำเนินการ

-

{pendingItems.length} รายการ

-
-
-

รายการที่เสร็จสิ้น

-

{completedItems.length} รายการ

-
+

รอดำเนินการ

{waiting.length} รายการ

+

ระหว่างดำเนินการ

{pending.length} รายการ

+

เสร็จสิ้น

{completed.length} รายการ

-

รายการซ่อมทั้งหมด

-
- -
+

รายการแจ้งซ่อมทั้งหมด

+
- - + + - - + + + {/* ช่างเห็น "ระดับความสำคัญ" แทน "สถานะ" */} + {role === "technical" ? : } + {role !== "user" && } + {role === "technical" && } - {data.map((item) => ( + {data.map(item => ( - + - - + + + + {role !== "user" && ( + + )} + {role === "technical" && ( + + )} ))}
เลขที่แจ้งซ่อมวันที่แจ้งซ่อมเลขที่วันที่ ชื่อผู้แจ้งรายละเอียดการแจ้งซ่อมสถานะหัวข้อประเภทระดับความสำคัญสถานะรายละเอียดจัดการ
{item.id}{item.seqNo} {item.date} {item.name}{item.detail}{item.status}{item.topic}{item.type} + {role === "technical" ? ( + + {item.level} + + ) : ( + + {item.status} + + )} + + + {renderActionButton(item)}
+
-
- Prev -
- - + {/* Modal – เพิ่มระดับความสำคัญ */} + {selectedDetail && ( +
+
e.stopPropagation()}> +

รายละเอียดการแจ้งซ่อม

+
+
+ หัวข้อ: + {selectedDetail.topic} +
+
+ ประเภท: + {selectedDetail.type} +
+
+ ระดับความสำคัญ: + + {selectedDetail.level} + +
+
+ รายละเอียด: + {selectedDetail.info} +
+
+ สถานที่: + {selectedDetail.place} +
+
+ เบอร์ติดต่อ: + {selectedDetail.call} +
+
+
- Next
-
+ )}
); }; -export default Dashboard; +// ฟังก์ชันช่วยเลือก class ตามสถานะ +const getStatusClass = (status) => { + if (status === "รอดำเนินการ") return "pending"; + if (status === "ระหว่างดำเนินการ") return "in-progress"; + if (status === "เสร็จสิ้น") return "completed"; + return ""; +}; + +// ฟังก์ชันช่วยเลือก class ตามระดับ +const getLevelClass = (level) => { + if (level === "สูง") return "high"; + if (level === "ปานกลาง") return "medium"; + if (level === "ต่ำ") return "low"; + return "medium"; +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/form/Form.css b/src/dashboard/Form.css similarity index 100% rename from src/form/Form.css rename to src/dashboard/Form.css diff --git a/src/dashboard/Form.jsx b/src/dashboard/Form.jsx new file mode 100644 index 0000000..908a6fb --- /dev/null +++ b/src/dashboard/Form.jsx @@ -0,0 +1,274 @@ +// src/dashboard/Form.jsx +import React, { useState, useEffect } from "react"; +import "./Form.css"; +import { supabase } from "../lib/supabaseClient"; + +const Form = ({ onBack, onSuccess }) => { + const [formData, setFormData] = useState({ + topic: "", + asset: "", // เก็บ idd + category: "", + priority: "ปานกลาง", + info: "", + name: "", + department: "", + call: "", + place: "", // จะถูกเติมอัตโนมัติ + }); + + const [equipmentList, setEquipmentList] = useState([]); // เก็บข้อมูลเต็ม + const [loading, setLoading] = useState(false); + const [fetchingEquipment, setFetchingEquipment] = useState(true); + + // ดึงข้อมูลอุปกรณ์ + useEffect(() => { + const fetchEquipment = async () => { + setFetchingEquipment(true); + try { + const { data, error } = await supabase + .from("equipment") + .select("idd, name, type, place") + .order("idd", { ascending: true }); + + if (error) throw error; + + setEquipmentList(data); + } catch (err) { + console.error("Error fetching equipment:", err); + setEquipmentList([]); + } finally { + setFetchingEquipment(false); + } + }; + + fetchEquipment(); + }, []); + + // เมื่อเลือกทรัพย์สิน → เติม place อัตโนมัติ + const handleAssetChange = (e) => { + const selectedIdd = e.target.value; + const selectedEquipment = equipmentList.find((eq) => eq.idd === selectedIdd); + + setFormData((prev) => ({ + ...prev, + asset: selectedIdd, + place: selectedEquipment ? selectedEquipment.place : "", // ถ้าไม่ระบุ → ล้าง + })); + }; + + const handleChange = (e) => { + const { name, value } = e.target; + if (name === "asset") { + handleAssetChange(e); + } else { + setFormData((prev) => ({ ...prev, [name]: value })); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setLoading(true); + + try { + const dataToInsert = { + day: new Date().toISOString().split("T")[0], + topic: formData.topic, + asset_id: formData.asset || null, + type: formData.category || null, + info: formData.info || null, + name: formData.name, + call: formData.call || null, + place: formData.place || null, + status: "รอดำเนินการ", + level: formData.priority, + }; + + const { error } = await supabase.from("repairs").insert(dataToInsert); + if (error) throw error; + + alert("ส่งแบบฟอร์มเรียบร้อยแล้ว!"); + onSuccess?.(); + onBack?.(); + } catch (err) { + console.error("Insert Error:", err); + alert("เกิดข้อผิดพลาด: " + err.message); + } finally { + setLoading(false); + } + }; + + // สร้าง options สำหรับ dropdown + const equipmentOptions = [ + { idd: "", label: "-- ไม่ระบุทรัพย์สิน --" }, + ...equipmentList.map((item) => ({ + idd: item.idd, + label: `${item.idd} - ${item.name} - ${item.type} - ${item.place}`, + })), + ]; + + return ( +
+ +

แจ้งซ่อมใหม่

+

+ กรอกรายละเอียดปัญหาที่ต้องการแจ้งซ่อม +

+ + {/* หัวข้อปัญหา */} +
+ + +
+ + {/* ทรัพย์สินที่เกี่ยวข้อง */} +
+ + + {fetchingEquipment && ( + กำลังโหลดรายการอุปกรณ์... + )} +
+ + {/* หมวดหมู่ + ระดับความสำคัญ */} +
+
+ + +
+
+ + +
+
+ + {/* รายละเอียดปัญหา */} +
+ + +
+ + {/* ชื่อ + แผนก */} +
+
+ + +
+
+ + +
+
+ + {/* ช่องทางติดต่อ + สถานที่ (เติมอัตโนมัติ) */} +
+
+ + +
+
+ + + +
+
+ + {/* ปุ่ม */} +
+ + +
+ +
+ ); +}; + +export default Form; \ No newline at end of file diff --git a/src/equipment/AddEq.jsx b/src/equipment/AddEq.jsx index ad7c97b..053e668 100644 --- a/src/equipment/AddEq.jsx +++ b/src/equipment/AddEq.jsx @@ -1,10 +1,11 @@ import React, { useState } from "react"; +import { supabase } from "../lib/supabaseClient"; import "./AddEq.css"; -const AddEq = () => { +const AddEq = ({ onCancel, onSave }) => { const [formData, setFormData] = useState({ name: "", - code: "", + code: "", // ใช้เป็น idd category: "", location: "", plan: "", @@ -13,15 +14,65 @@ const AddEq = () => { status: "ใช้งาน", }); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const handleChange = (e) => { const { name, value } = e.target; setFormData({ ...formData, [name]: value }); }; - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - console.log("ข้อมูลอุปกรณ์ที่เพิ่ม:", formData); - alert("บันทึกข้อมูลอุปกรณ์เรียบร้อยแล้ว!"); + setLoading(true); + setError(null); + + try { + // สร้าง object ที่จะ insert เฉพาะ field ที่มีในตาราง equipment + const equipmentData = { + idd: formData.code, // code → idd + name: formData.name, + type: formData.category, // category → type + place: formData.location, // location → place + status: formData.status, + }; + + const { error: insertError } = await supabase + .from("equipment") + .insert([equipmentData]); + + if (insertError) throw insertError; + + // เรียก onSave เพื่อส่งข้อมูลกลับ parent (ถ้ามี) + if (onSave) { + onSave({ + id: formData.code, + ...equipmentData, + }); + } + + alert("เพิ่มอุปกรณ์สำเร็จ!"); + + // รีเซ็ตฟอร์ม + setFormData({ + name: "", + code: "", + category: "", + location: "", + plan: "", + purchaseDate: "", + warrantyDate: "", + status: "ใช้งาน", + }); + + if (onCancel) onCancel(); + + } catch (err) { + setError(err.message || "เกิดข้อผิดพลาดในการเพิ่มข้อมูล"); + console.error("Supabase insert error:", err); + } finally { + setLoading(false); + } }; const handleCancel = () => { @@ -35,6 +86,7 @@ const AddEq = () => { warrantyDate: "", status: "ใช้งาน", }); + if (onCancel) onCancel(); }; return ( @@ -43,6 +95,8 @@ const AddEq = () => {

เพิ่มอุปกรณ์ใหม่

+ {error &&
{error}
} +
@@ -150,8 +204,8 @@ const AddEq = () => { -
@@ -159,4 +213,4 @@ const AddEq = () => { ); }; -export default AddEq; +export default AddEq; \ No newline at end of file diff --git a/src/equipment/Equipment.css b/src/equipment/Equipment.css index ebb7ce2..c33127a 100644 --- a/src/equipment/Equipment.css +++ b/src/equipment/Equipment.css @@ -151,3 +151,8 @@ body, html { .dot.active { background-color: #42b0f5; } + +/* เพิ่มสีสถานะ (สีเดิมมีแค่ .yellow) */ +.status-dot.green { background-color: #28a745; } +.status-dot.yellow { background-color: #ffcc00; } +.status-dot.red { background-color: #dc3545; } \ No newline at end of file diff --git a/src/equipment/Equipment.jsx b/src/equipment/Equipment.jsx index 76f7b67..a360d0e 100644 --- a/src/equipment/Equipment.jsx +++ b/src/equipment/Equipment.jsx @@ -1,25 +1,72 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; +import { supabase } from "../lib/supabaseClient"; // ใช้ path เดียวกับ AddEq import "./Equipment.css"; +import AddEq from "./AddEq"; const Equipment = () => { const [searchTerm, setSearchTerm] = useState(""); + const [showAddForm, setShowAddForm] = useState(false); + const [equipmentList, setEquipmentList] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // 🔹 ข้อมูลจำลอง - const equipmentList = [ - { - id: "PC-001", - name: "คอมพิวเตอร์ Dell OptiPlex", - category: "คอมพิวเตอร์", - location: "ชั้น 2 ห้อง 201", - status: "ซ่อมบำรุง", - }, - ]; - - // 🔹 กรองข้อมูลตามการค้นหา - const filteredList = equipmentList.filter((item) => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) + /* ---------- ดึงข้อมูลจาก Supabase ---------- */ + const fetchEquipment = async () => { + setLoading(true); + setError(null); + try { + const { data, error } = await supabase + .from("equipment") + .select("idd, name, type, place, status") + .order("idd", { ascending: true }); + + if (error) throw error; + + const formatted = data.map((item) => ({ + id: item.idd, + name: item.name, + category: item.type, + location: item.place, + status: item.status, + })); + setEquipmentList(formatted); + } catch (err) { + setError("ไม่สามารถดึงข้อมูลได้: " + err.message); + console.error(err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchEquipment(); + }, []); + + /* ---------- กรองตามคำค้นหา ---------- */ + const filteredList = equipmentList.filter( + (item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()) || + item.id.toLowerCase().includes(searchTerm.toLowerCase()) ); + /* ---------- หลังเพิ่มอุปกรณ์สำเร็จ ---------- */ + const handleSaveSuccess = () => { + fetchEquipment(); // รีเฟรชตาราง + setShowAddForm(false); + alert("เพิ่มอุปกรณ์สำเร็จ!"); + }; + + /* ---------- แสดงฟอร์มเพิ่ม ---------- */ + if (showAddForm) { + return ( + setShowAddForm(false)} + onSave={handleSaveSuccess} + /> + ); + } + + /* ---------- UI หลัก ---------- */ return (
@@ -30,47 +77,86 @@ const Equipment = () => {
setSearchTerm(e.target.value)} /> - +
- +
-
- - - - - - - - - - - - - {filteredList.map((item, index) => ( - - - - - - - + {/* Loading */} + {loading && ( +
+ กำลังโหลดข้อมูล... +
+ )} + + {/* Error */} + {error && ( +
+ {error} +
+ )} + + {/* Table */} + {!loading && !error && ( +
+
รหัสชื่อหมวดหมู่สถานที่สถานะการจัดการ
{item.id}{item.name}{item.category}{item.location} - {item.status} - - - -
+ + + + + + + + - ))} - -
รหัสชื่อหมวดหมู่สถานที่สถานะการจัดการ
-
+ + + {filteredList.length === 0 ? ( + + + {searchTerm ? "ไม่พบข้อมูลที่ค้นหา" : "ยังไม่มีข้อมูลอุปกรณ์"} + + + ) : ( + filteredList.map((item) => ( + + {item.id} + {item.name} + {item.category} + {item.location} + + {" "} + {item.status} + + + + + + + )) + )} + + +
+ )}
Prev @@ -85,4 +171,4 @@ const Equipment = () => { ); }; -export default Equipment; +export default Equipment; \ No newline at end of file diff --git a/src/form/Form.jsx b/src/form/Form.jsx deleted file mode 100644 index 544f03f..0000000 --- a/src/form/Form.jsx +++ /dev/null @@ -1,112 +0,0 @@ -import React from "react"; -import "./Form.css"; - -const Form = () => { - const handleBack = () => { - window.history.back(); // กลับไปหน้าก่อนหน้า - }; - - const handleSubmit = (e) => { - e.preventDefault(); - alert("ส่งแบบฟอร์มเรียบร้อยแล้ว!"); - }; - - return ( -
-
-

แจ้งซ่อมใหม่

-

- กรอกรายละเอียดปัญหาที่ต้องการแจ้งซ่อม -

- - {/* หัวข้อปัญหา */} -
- - -
- - {/* ทรัพย์สินที่เกี่ยวข้อง */} -
- - -
- - {/* หมวดหมู่ + ระดับความสำคัญ */} -
-
- - -
-
- - -
-
- - {/* รายละเอียดปัญหา */} -
- - -
- - {/* ข้อมูลผู้แจ้ง */} -
-
- - -
-
- - -
-
- - {/* ช่องทางติดต่อ + สถานที่ */} -
-
- - -
-
- - -
-
- - {/* ปุ่มย้อนกลับและส่ง */} -
- - -
-
-
- ); -}; - -export default Form; diff --git a/src/lib/supabaseClient.js b/src/lib/supabaseClient.js new file mode 100644 index 0000000..d2cdeb5 --- /dev/null +++ b/src/lib/supabaseClient.js @@ -0,0 +1,7 @@ +// src/lib/supabaseClient.js +import { createClient } from '@supabase/supabase-js' + +const supabaseUrl = 'https://uaimvnjkfmanbhimwwdt.supabase.co' // เปลี่ยนเป็น URL ของคุณ +const supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InVhaW12bmprZm1hbmJoaW13d2R0Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjE5MDI0NTQsImV4cCI6MjA3NzQ3ODQ1NH0.k1OnPTTT0aNxt4Iw4gwtGjmYEH_tANEEjV7DOkLHwos' // เปลี่ยนเป็น anon key + +export const supabase = createClient(supabaseUrl, supabaseAnonKey) \ No newline at end of file diff --git a/src/report/Report.css b/src/report/Report.css index 859a0c9..962ffbd 100644 --- a/src/report/Report.css +++ b/src/report/Report.css @@ -208,3 +208,5 @@ body, html { padding: 12px 20px; } } + + diff --git a/src/report/Report.jsx b/src/report/Report.jsx index df2e9d6..2ea7ef0 100644 --- a/src/report/Report.jsx +++ b/src/report/Report.jsx @@ -1,17 +1,86 @@ -import React from "react"; +// src/pages/Report.jsx +import React, { useState, useEffect } from "react"; import "./Report.css"; +import { supabase } from "../lib/supabaseClient"; + +// รายชื่อหมวดหมู่ตามฟอร์ม (ตรงทุกตัวอักษร) +const CATEGORY_LIST = [ + "ฮาร์ดแวร์ (Hardware)", + "ซอฟต์แวร์ (Software)", + "เครือข่าย (Network)", + "สิ่งอำนวยความสะดวก (Facility)", + "เฟอร์นิเจอร์ (Furniture)", + "ระบบไฟฟ้า (Electrical)", + "อื่น ๆ", +]; const Report = () => { - // 🔹 ข้อมูลกราฟแบบธรรมดา - const data = [ - { name: "ฮาร์ดแวร์", value: 1, color: "#007bff" }, - { name: "ซอฟต์แวร์", value: 0, color: "#dee2e6" }, - { name: "เครือข่าย", value: 0, color: "#dee2e6" }, - { name: "สิ่งอำนวยความสะดวก", value: 1, color: "#fd7e14" }, - { name: "เฟอร์นิเจอร์", value: 0, color: "#dee2e6" }, - { name: "ระบบไฟฟ้า", value: 0, color: "#dee2e6" }, - { name: "อื่นๆ", value: 0, color: "#dee2e6" }, - ]; + const [repairs, setRepairs] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchRepairs(); + }, []); + + const fetchRepairs = async () => { + try { + setLoading(true); + const { data, error } = await supabase + .from("repairs") + .select("type, level, status"); + + if (error) throw error; + + setRepairs(data || []); + } catch (err) { + console.error(err); + setError("โหลดข้อมูลล้มเหลว: " + err.message); + } finally { + setLoading(false); + } + }; + + // นับตามหมวดหมู่ (ใช้ชื่อเต็ม) + const categoryCount = CATEGORY_LIST.reduce((acc, cat) => { + acc[cat] = 0; + return acc; + }, {}); + + const levelCount = { สูง: 0, ปานกลาง: 0, ต่ำ: 0 }; + let total = 0; + let pending = 0; + let completed = 0; + + repairs.forEach((item) => { + const type = item.type || "อื่น ๆ"; + const level = item.level || "ปานกลาง"; + const status = item.status || "รอดำเนินการ"; + + // นับหมวดหมู่ – ใช้ชื่อเต็ม + const fullType = CATEGORY_LIST.find(cat => cat.includes(type)) || "อื่น ๆ"; + categoryCount[fullType]++; + + // นับระดับ + if (levelCount[level] !== undefined) levelCount[level]++; + + // นับสถานะ + total++; + if (status === "รอดำเนินการ") pending++; + if (status === "เสร็จสิ้น") completed++; + }); + + const successRate = total > 0 ? Math.round((completed / total) * 100) : 0; + + // สร้างข้อมูลกราฟ – เรียงตาม CATEGORY_LIST + const chartData = CATEGORY_LIST.map((cat) => ({ + name: cat, + value: categoryCount[cat], + color: categoryCount[cat] > 0 ? "#007bff" : "#dee2e6", + })); + + if (loading) return
กำลังโหลด...
; + if (error) return
{error}
; return (
@@ -20,24 +89,24 @@ const Report = () => {

ภาพรวมและการวิเคราะห์ระบบแจ้งซ่อม

- {/* 🔹 สรุปข้อมูล */} + {/* สรุปข้อมูล */}

จำนวนแจ้งซ่อมทั้งหมด

-

1 รายการ

+

{total} รายการ

อัตราความสำเร็จ

-

50%

+

{successRate}%

รอดำเนินการ

-

1 รายการ

+

{pending} รายการ

- {/* 🔹 กราฟแบบ CSS */}
+ {/* กราฟแท่ง */}

กราฟแท่ง

@@ -45,12 +114,15 @@ const Report = () => {
- {data.map((item, index) => ( + {chartData.map((item, index) => (
{item.name}
{item.value > 0 && {item.value}}
@@ -59,16 +131,28 @@ const Report = () => {
- {/* 🔹 ระดับความสำคัญ */} + {/* ระดับความสำคัญ */}

ระดับความสำคัญ

    -
  • เร่งด่วน 0 (0%)
  • -
  • สูง 1 (50%)
  • -
  • ปานกลาง 1 (50%)
  • -
  • ต่ำ 0 (0%)
  • +
  • + + เร่งด่วน 0 (0%) +
  • +
  • + + สูง {levelCount.สูง} ({total > 0 ? Math.round((levelCount.สูง / total) * 100) : 0}%) +
  • +
  • + + ปานกลาง {levelCount.ปานกลาง} ({total > 0 ? Math.round((levelCount.ปานกลาง / total) * 100) : 0}%) +
  • +
  • + + ต่ำ {levelCount.ต่ำ} ({total > 0 ? Math.round((levelCount.ต่ำ / total) * 100) : 0}%) +
@@ -76,4 +160,4 @@ const Report = () => { ); }; -export default Report; +export default Report; \ No newline at end of file diff --git a/src/status/Status.css b/src/status/Status.css index 70a1a0a..2795156 100644 --- a/src/status/Status.css +++ b/src/status/Status.css @@ -1,85 +1,86 @@ +/* Status.css */ .status-container { - padding: 24px; + padding: 30px; background-color: #f4f7fa; - width: 100%; + min-height: 100vh; + font-family: "Prompt", sans-serif; } .summary-cards { display: flex; gap: 20px; - margin-bottom: 24px; + margin-bottom: 30px; + flex-wrap: wrap; } .summary-card { flex: 1; + min-width: 200px; background-color: #ffffff; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); - padding: 20px; + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); + padding: 24px; display: flex; align-items: center; + transition: transform 0.2s; +} + +.summary-card:hover { + transform: translateY(-4px); } .card-info { display: flex; flex-direction: column; - margin-left: 0; /* ลบ margin จากไอคอนที่ถูกลบ */ } .card-title { color: #6c757d; - font-size: 0.9rem; - margin-bottom: 4px; + font-size: 0.95rem; + margin-bottom: 6px; + font-weight: 500; } .card-count { - font-size: 1.75rem; + font-size: 2.1rem; font-weight: 700; - color: #333; - line-height: 1.2; + color: #2c3e50; + line-height: 1; } .card-unit { font-size: 1.1rem; font-weight: normal; color: #555; - margin-left: 5px; -} - -/* กำหนดสีตาม Dashboard.jsx */ -.card-inprogress { - border-left: 4px solid #ff9800; /* สีส้ม */ + margin-left: 6px; } -.card-completed { - border-left: 4px solid #4caf50; /* สีเขียว */ -} - -.card-total { - border-left: 4px solid #9c27b0; /* สีม่วง */ -} +/* สีการ์ด */ +.card-inprogress { border-left: 6px solid #ff9800; } +.card-completed { border-left: 6px solid #4caf50; } +.card-total { border-left: 6px solid #9c27b0; } .table-section { background-color: #ffffff; - border-radius: 8px; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); + border-radius: 12px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); overflow: hidden; } .table-header-bar { - background-color: #34aadc; /* ใช้สีน้ำเงินจากเดิม */ + background: linear-gradient(135deg, #34aadc, #2c8cc7); color: white; - padding: 16px 24px; + padding: 18px 28px; } .table-header-bar h3 { margin: 0; - font-size: 1.25rem; + font-size: 1.3rem; font-weight: 600; } .table-wrapper { - padding: 0 24px 24px; + padding: 0 28px 28px; overflow-x: auto; } @@ -91,40 +92,67 @@ .repair-table th, .repair-table td { - padding: 15px; + padding: 16px 14px; text-align: left; - border-bottom: 1px solid #e0e0e0; + border-bottom: 1px solid #e9ecef; vertical-align: middle; + font-size: 0.95rem; } .repair-table th { - background-color: #f9f9f9; - color: #555; + background-color: #f8f9fa; + color: #495057; font-weight: 600; font-size: 0.9rem; + text-transform: uppercase; + letter-spacing: 0.5px; } .repair-table td { - color: #333; - font-size: 0.95rem; + color: #2c3e50; +} + +.repair-table tbody tr:hover { + background-color: #f8f9fa; +} + +/* เลขที่แจ้งซ่อม */ +.seq-no { + font-family: "Courier New", monospace; + font-weight: 700; + color: #2c3e50; + letter-spacing: 1px; } +/* สถานะ Badge */ .status-badge { - padding: 6px 12px; - border-radius: 16px; + padding: 6px 14px; + border-radius: 20px; font-size: 0.8rem; font-weight: 600; + text-align: center; + min-width: 90px; + display: inline-block; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .status-badge.in-progress { - background-color: #fff8e1; /* สีพื้นหลังอ่อน ๆ */ - color: #ff9800; /* สีส้ม */ + background-color: #fff8e1; + color: #e67e22; + border: 1px solid #ffeaa7; +} + +.status-badge.completed { + background-color: 1px solid #a2d5c6; + background-color: #d4edda; + color: #2d6a4f; } +/* ปุ่ม */ .btn-action { border: none; - padding: 8px 15px; - border-radius: 5px; + padding: 8px 14px; + border-radius: 6px; cursor: pointer; margin-right: 8px; font-size: 0.9rem; @@ -132,11 +160,12 @@ display: inline-flex; align-items: center; gap: 6px; - transition: opacity 0.2s; + transition: all 0.2s; } .btn-action:hover { - opacity: 0.8; + opacity: 0.9; + transform: translateY(-1px); } .btn-details { @@ -145,25 +174,26 @@ } .btn-print { - background-color: #b3e5fc; - color: #01579b; + background-color: #e3f2fd; + color: #1565c0; } .pagination { display: flex; justify-content: flex-end; - padding: 20px 24px; - border-top: 1px solid #e0e0e0; + padding: 20px 28px; + border-top: 1px solid #e9ecef; + gap: 8px; } .btn-page { background-color: #fff; - border: 1px solid #ccc; - padding: 8px 12px; - margin-left: 5px; + border: 1px solid #dee2e6; + padding: 8px 14px; cursor: pointer; - border-radius: 4px; - color: #555; + border-radius: 6px; + color: #495057; + font-size: 0.9rem; } .btn-page.active { @@ -173,7 +203,23 @@ } .btn-page:disabled { - background-color: #f4f4f4; - color: #ccc; + background-color: #f8f9fa; + color: #adb5bd; cursor: not-allowed; + border-color: #dee2e6; +} + +/* Responsive */ +@media (max-width: 768px) { + .summary-cards { + flex-direction: column; + } + .repair-table th, + .repair-table td { + padding: 12px 10px; + font-size: 0.9rem; + } + .table-header-bar h3 { + font-size: 1.1rem; + } } \ No newline at end of file diff --git a/src/status/Status.jsx b/src/status/Status.jsx index dfcba69..93f1c03 100644 --- a/src/status/Status.jsx +++ b/src/status/Status.jsx @@ -1,17 +1,55 @@ -import React from 'react'; -import './Status.css'; +// src/pages/Status.jsx +import React, { useState, useEffect } from "react"; +import "./Status.css"; +import { supabase } from "../lib/supabaseClient"; + +const TABLE_NAME = "repairs"; const Status = () => { - // ข้อมูลตัวอย่างสำหรับตาราง - const data = [ - { id: "003", date: "02/09/2025", name: "ญาดา ปวีณชัย", detail: "หลอดไฟเสีย", status: "ระหว่างดำเนินการ" }, - { id: "002", date: "01/09/2025", name: "สมชาย ใจดี", detail: "แอร์ไม่เย็น", status: "เสร็จสิ้น" }, - { id: "001", date: "30/08/2025", name: "อารีรัตน์ มีสุข", detail: "ประตูฝืด", status: "เสร็จสิ้น" }, - ]; + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetchRepairs(); + }, []); + + const fetchRepairs = async () => { + try { + setLoading(true); + const { data: repairs, error } = await supabase + .from(TABLE_NAME) + .select("*") + .order("day", { ascending: false }); + + if (error) throw error; + + // เพิ่มเลขที่เป็น 001, 002, ... + const formatted = repairs.map((r, index) => ({ + id: r.id, + seqNo: String(index + 1).padStart(3, "0"), // 001, 002, ... + date: new Date(r.day).toLocaleDateString("th-TH"), + name: r.name, + detail: r.topic, + status: r.status ?? "รอดำเนินการ", + })); - // กรองข้อมูลตามสถานะ + setData(formatted); + } catch (err) { + console.error(err); + setError("โหลดข้อมูลล้มเหลว: " + err.message); + } finally { + setLoading(false); + } + }; + + // กรองข้อมูล const pendingItems = data.filter(item => item.status === "ระหว่างดำเนินการ"); const completedItems = data.filter(item => item.status === "เสร็จสิ้น"); + const totalItems = data.length; + + if (loading) return
กำลังโหลด...
; + if (error) return
{error}
; return (
@@ -34,7 +72,7 @@ const Status = () => {
รายการทั้งหมด - {data.length} รายการ + {totalItems} รายการ
@@ -59,11 +97,15 @@ const Status = () => { {pendingItems.map((item) => ( - {item.id} + {item.seqNo} {item.date} {item.name} {item.detail} - {item.status} + + + {item.status} + + @@ -72,7 +114,9 @@ const Status = () => { ))} {pendingItems.length === 0 && ( - ไม่มีรายการที่ค้างอยู่ + + ไม่มีรายการที่ค้างอยู่ + )}