From 624644f8c3000e150d9a9d34e4292a2207f87433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 4 Feb 2026 19:40:21 +0800 Subject: [PATCH 01/54] more admin nation todo items to stage 2 --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4600841e..86700657 100644 --- a/README.md +++ b/README.md @@ -71,10 +71,6 @@ Please read laravel official documents: - change module to support submodule - add edit candidate and edit candidate result permission and update admin admission test candidate permission checking - change SEO from page to layout export props title, desc. and ogImage and change ogImage from public to storage -- add admin nation index -- add admin nation store -- add admin nation update -- add admin nation update status - update candidate store method to support select product and contact stripe - add stripe checkout web hock handle - change quota validity months to inside product and order table @@ -97,6 +93,10 @@ Please read laravel official documents: #### stage 2 third party iq test result +- add admin nation index +- add admin nation store +- add admin nation update +- add admin nation update status - Add admin third party iq test accept list store - Add admin third party iq test accept list index - Add admin third party iq test accept list update name From 0f9c5bb66030929fd645e94564fd7c88b16d75df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 4 Feb 2026 20:42:09 +0800 Subject: [PATCH 02/54] downgrade sass to fix bootstrap warning --- package-lock.json | 545 +++++++++++++++++----------------------------- package.json | 2 +- 2 files changed, 195 insertions(+), 352 deletions(-) diff --git a/package-lock.json b/package-lock.json index 890fe15e..ab2fb672 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ "axios": "^1.11.0", "concurrently": "^9.2.1", "laravel-vite-plugin": "^2.1.0", - "sass": "^1.97.3", + "sass": "1.78.0", "tailwindcss": "^4.1.10", "vite": "^7.3.1" } @@ -1392,302 +1392,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@parcel/watcher": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", - "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "dependencies": { - "detect-libc": "^2.0.3", - "is-glob": "^4.0.3", - "node-addon-api": "^7.0.0", - "picomatch": "^4.0.3" - }, - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.5.6", - "@parcel/watcher-darwin-arm64": "2.5.6", - "@parcel/watcher-darwin-x64": "2.5.6", - "@parcel/watcher-freebsd-x64": "2.5.6", - "@parcel/watcher-linux-arm-glibc": "2.5.6", - "@parcel/watcher-linux-arm-musl": "2.5.6", - "@parcel/watcher-linux-arm64-glibc": "2.5.6", - "@parcel/watcher-linux-arm64-musl": "2.5.6", - "@parcel/watcher-linux-x64-glibc": "2.5.6", - "@parcel/watcher-linux-x64-musl": "2.5.6", - "@parcel/watcher-win32-arm64": "2.5.6", - "@parcel/watcher-win32-ia32": "2.5.6", - "@parcel/watcher-win32-x64": "2.5.6" - } - }, - "node_modules/@parcel/watcher-android-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", - "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", - "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", - "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", - "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", - "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", - "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", - "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", - "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", - "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", - "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", - "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", - "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/watcher-win32-x64": { - "version": "2.5.6", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", - "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2492,6 +2196,20 @@ "dev": true, "license": "MIT" }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/aria-query": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", @@ -2537,6 +2255,19 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/blurhash": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/blurhash/-/blurhash-2.0.5.tgz", @@ -2578,6 +2309,19 @@ ], "license": "MIT" }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -2678,19 +2422,28 @@ } }, "node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "devOptional": true, "license": "MIT", "dependencies": { - "readdirp": "^4.0.1" + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" }, "engines": { - "node": ">= 14.16.0" + "node": ">= 8.10.0" }, "funding": { "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, "node_modules/ckeditor5": { @@ -3123,21 +2876,17 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "devOptional": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "dependencies": { + "to-regex-range": "^5.0.1" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=8" } }, "node_modules/flag-icons": { @@ -3258,6 +3007,19 @@ "node": ">= 0.4" } }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3562,18 +3324,31 @@ } }, "node_modules/immutable": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.4.tgz", - "integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==", + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "devOptional": true, "license": "MIT" }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "devOptional": true, "license": "MIT", - "optional": true, "engines": { "node": ">=0.10.0" } @@ -3592,8 +3367,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "devOptional": true, "license": "MIT", - "optional": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -3601,6 +3376,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -4794,12 +4579,15 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/node-addon-api": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", - "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "devOptional": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=0.10.0" + } }, "node_modules/object-inspect": { "version": "1.13.4", @@ -4830,12 +4618,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "devOptional": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -4901,17 +4690,16 @@ } }, "node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "devOptional": true, "license": "MIT", - "engines": { - "node": ">= 14.18.0" + "dependencies": { + "picomatch": "^2.2.1" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "engines": { + "node": ">=8.10.0" } }, "node_modules/rehype-dom-parse": { @@ -5113,14 +4901,14 @@ } }, "node_modules/sass": { - "version": "1.97.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.97.3.tgz", - "integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", + "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "devOptional": true, "license": "MIT", "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { @@ -5128,9 +4916,6 @@ }, "engines": { "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" } }, "node_modules/shell-quote": { @@ -5367,6 +5152,48 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -5634,14 +5461,30 @@ "picomatch": "^2.3.1" } }, - "node_modules/vite-plugin-full-reload/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "node_modules/vite/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", "engines": { - "node": ">=8.6" + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" diff --git a/package.json b/package.json index 37a89283..bcade997 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "axios": "^1.11.0", "concurrently": "^9.2.1", "laravel-vite-plugin": "^2.1.0", - "sass": "^1.97.3", + "sass": "1.78.0", "tailwindcss": "^4.1.10", "vite": "^7.3.1" }, From 05602a2413b37d4e4192d761e58dbdf51000b89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Thu, 5 Feb 2026 16:16:27 +0800 Subject: [PATCH 03/54] update compoer --- composer.lock | 132 +++++++++++++++++++++++++------------------------- 1 file changed, 67 insertions(+), 65 deletions(-) diff --git a/composer.lock b/composer.lock index b71ad591..7278294a 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "brick/math", - "version": "0.14.5", + "version": "0.14.6", "source": { "type": "git", "url": "https://github.com/brick/math.git", - "reference": "618a8077b3c326045e10d5788ed713b341fcfe40" + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/brick/math/zipball/618a8077b3c326045e10d5788ed713b341fcfe40", - "reference": "618a8077b3c326045e10d5788ed713b341fcfe40", + "url": "https://api.github.com/repos/brick/math/zipball/32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", + "reference": "32498d5e1897e7642c0b961ace2df6d7dc9a3bc3", "shasum": "" }, "require": { @@ -56,7 +56,7 @@ ], "support": { "issues": "https://github.com/brick/math/issues", - "source": "https://github.com/brick/math/tree/0.14.5" + "source": "https://github.com/brick/math/tree/0.14.6" }, "funding": [ { @@ -64,7 +64,7 @@ "type": "github" } ], - "time": "2026-02-03T18:06:51+00:00" + "time": "2026-02-05T07:59:58+00:00" }, { "name": "carbonphp/carbon-doctrine-types", @@ -1344,16 +1344,16 @@ }, { "name": "laravel/framework", - "version": "v12.49.0", + "version": "v12.50.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5" + "reference": "174ffed91d794a35a541a5eb7c3785a02a34aaba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/4bde4530545111d8bdd1de6f545fa8824039fcb5", - "reference": "4bde4530545111d8bdd1de6f545fa8824039fcb5", + "url": "https://api.github.com/repos/laravel/framework/zipball/174ffed91d794a35a541a5eb7c3785a02a34aaba", + "reference": "174ffed91d794a35a541a5eb7c3785a02a34aaba", "shasum": "" }, "require": { @@ -1562,39 +1562,39 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-28T03:40:49+00:00" + "time": "2026-02-04T18:34:13+00:00" }, { "name": "laravel/mcp", - "version": "v0.5.3", + "version": "v0.5.4", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "39b9791b989927642137dd5b55dde0529f1614f9" + "reference": "4f97e50a8e9c60d91aafb472c9b593b1d3181adb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/39b9791b989927642137dd5b55dde0529f1614f9", - "reference": "39b9791b989927642137dd5b55dde0529f1614f9", + "url": "https://api.github.com/repos/laravel/mcp/zipball/4f97e50a8e9c60d91aafb472c9b593b1d3181adb", + "reference": "4f97e50a8e9c60d91aafb472c9b593b1d3181adb", "shasum": "" }, "require": { "ext-json": "*", "ext-mbstring": "*", - "illuminate/console": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/container": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/contracts": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/http": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/json-schema": "^12.41.1", - "illuminate/routing": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/support": "^10.49.0|^11.45.3|^12.41.1", - "illuminate/validation": "^10.49.0|^11.45.3|^12.41.1", - "php": "^8.1" + "illuminate/console": "^11.45.3|^12.41.1|^13.0", + "illuminate/container": "^11.45.3|^12.41.1|^13.0", + "illuminate/contracts": "^11.45.3|^12.41.1|^13.0", + "illuminate/http": "^11.45.3|^12.41.1|^13.0", + "illuminate/json-schema": "^12.41.1|^13.0", + "illuminate/routing": "^11.45.3|^12.41.1|^13.0", + "illuminate/support": "^11.45.3|^12.41.1|^13.0", + "illuminate/validation": "^11.45.3|^12.41.1|^13.0", + "php": "^8.2" }, "require-dev": { "laravel/pint": "^1.20", - "orchestra/testbench": "^8.36|^9.15|^10.8", - "pestphp/pest": "^2.36.0|^3.8.4|^4.1.0", + "orchestra/testbench": "^9.15|^10.8|^11.0", + "pestphp/pest": "^3.8.5|^4.3.2", "phpstan/phpstan": "^2.1.27", "rector/rector": "^2.2.4" }, @@ -1635,34 +1635,34 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-01-26T10:25:21+00:00" + "time": "2026-02-04T15:10:07+00:00" }, { "name": "laravel/prompts", - "version": "v0.3.11", + "version": "v0.3.12", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217" + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", - "reference": "dd2a2ed95acacbcccd32fd98dee4c946ae7a7217", + "url": "https://api.github.com/repos/laravel/prompts/zipball/4861ded9003b7f8a158176a0b7666f74ee761be8", + "reference": "4861ded9003b7f8a158176a0b7666f74ee761be8", "shasum": "" }, "require": { "composer-runtime-api": "^2.2", "ext-mbstring": "*", "php": "^8.1", - "symfony/console": "^6.2|^7.0" + "symfony/console": "^6.2|^7.0|^8.0" }, "conflict": { "illuminate/console": ">=10.17.0 <10.25.0", "laravel/framework": ">=10.17.0 <10.25.0" }, "require-dev": { - "illuminate/collections": "^10.0|^11.0|^12.0", + "illuminate/collections": "^10.0|^11.0|^12.0|^13.0", "mockery/mockery": "^1.5", "pestphp/pest": "^2.3|^3.4|^4.0", "phpstan/phpstan": "^1.12.28", @@ -1692,9 +1692,9 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.11" + "source": "https://github.com/laravel/prompts/tree/v0.3.12" }, - "time": "2026-01-27T02:55:06+00:00" + "time": "2026-02-03T06:57:26+00:00" }, { "name": "laravel/sanctum", @@ -1761,27 +1761,27 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.8", + "version": "v2.0.9", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b" + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7581a4407012f5f53365e11bafc520fd7f36bc9b", - "reference": "7581a4407012f5f53365e11bafc520fd7f36bc9b", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/8f631589ab07b7b52fead814965f5a800459cb3e", + "reference": "8f631589ab07b7b52fead814965f5a800459cb3e", "shasum": "" }, "require": { "php": "^8.1" }, "require-dev": { - "illuminate/support": "^10.0|^11.0|^12.0", + "illuminate/support": "^10.0|^11.0|^12.0|^13.0", "nesbot/carbon": "^2.67|^3.0", "pestphp/pest": "^2.36|^3.0|^4.0", "phpstan/phpstan": "^2.0", - "symfony/var-dumper": "^6.2.0|^7.0.0" + "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0" }, "type": "library", "extra": { @@ -1818,7 +1818,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-01-08T16:22:46+00:00" + "time": "2026-02-03T06:55:34+00:00" }, { "name": "laravel/tinker", @@ -6422,7 +6422,7 @@ "source": { "type": "git", "url": "https://github.com/tszyuloveyou/ziggy", - "reference": "69c0deb0d0b5f1ab0ae7c51e40b29e83b8209eba" + "reference": "adb128808bf750c0312245718a861e58f5acadb4" }, "require": { "ext-json": "*", @@ -6479,7 +6479,7 @@ "routes", "ziggy" ], - "time": "2025-06-02T13:35:24+00:00" + "time": "2026-02-05T08:09:35+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -6999,37 +6999,38 @@ }, { "name": "laravel/pail", - "version": "v1.2.4", + "version": "v1.2.5", "source": { "type": "git", "url": "https://github.com/laravel/pail.git", - "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30" + "reference": "fdb73f5eacf03db576c710d5a00101ba185f2254" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pail/zipball/49f92285ff5d6fc09816e976a004f8dec6a0ea30", - "reference": "49f92285ff5d6fc09816e976a004f8dec6a0ea30", + "url": "https://api.github.com/repos/laravel/pail/zipball/fdb73f5eacf03db576c710d5a00101ba185f2254", + "reference": "fdb73f5eacf03db576c710d5a00101ba185f2254", "shasum": "" }, "require": { "ext-mbstring": "*", - "illuminate/console": "^10.24|^11.0|^12.0", - "illuminate/contracts": "^10.24|^11.0|^12.0", - "illuminate/log": "^10.24|^11.0|^12.0", - "illuminate/process": "^10.24|^11.0|^12.0", - "illuminate/support": "^10.24|^11.0|^12.0", + "illuminate/console": "^10.24|^11.0|^12.0|^13.0", + "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0", + "illuminate/log": "^10.24|^11.0|^12.0|^13.0", + "illuminate/process": "^10.24|^11.0|^12.0|^13.0", + "illuminate/support": "^10.24|^11.0|^12.0|^13.0", "nunomaduro/termwind": "^1.15|^2.0", "php": "^8.2", - "symfony/console": "^6.0|^7.0" + "symfony/console": "^6.0|^7.0|^8.0" }, "require-dev": { - "laravel/framework": "^10.24|^11.0|^12.0", + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", "laravel/pint": "^1.13", - "orchestra/testbench-core": "^8.13|^9.17|^10.8", + "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0", "pestphp/pest": "^2.20|^3.0|^4.0", "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0", "phpstan/phpstan": "^1.12.27", - "symfony/var-dumper": "^6.3|^7.0" + "symfony/var-dumper": "^6.3|^7.0|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" }, "type": "library", "extra": { @@ -7074,7 +7075,7 @@ "issues": "https://github.com/laravel/pail/issues", "source": "https://github.com/laravel/pail" }, - "time": "2025-11-20T16:29:35+00:00" + "time": "2026-02-04T15:10:32+00:00" }, { "name": "laravel/pint", @@ -7976,16 +7977,16 @@ }, { "name": "phpunit/phpunit", - "version": "11.5.50", + "version": "11.5.51", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3" + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", - "reference": "fdfc727f0fcacfeb8fcb30c7e5da173125b58be3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ad14159f92910b0f0e3928c13e9b2077529de091", + "reference": "ad14159f92910b0f0e3928c13e9b2077529de091", "shasum": "" }, "require": { @@ -8000,7 +8001,7 @@ "phar-io/version": "^3.2.1", "php": ">=8.2", "phpunit/php-code-coverage": "^11.0.12", - "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-file-iterator": "^5.1.1", "phpunit/php-invoker": "^5.0.1", "phpunit/php-text-template": "^4.0.1", "phpunit/php-timer": "^7.0.1", @@ -8012,6 +8013,7 @@ "sebastian/exporter": "^6.3.2", "sebastian/global-state": "^7.0.2", "sebastian/object-enumerator": "^6.0.1", + "sebastian/recursion-context": "^6.0.3", "sebastian/type": "^5.1.3", "sebastian/version": "^5.0.2", "staabm/side-effects-detector": "^1.0.5" @@ -8057,7 +8059,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.50" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.51" }, "funding": [ { @@ -8081,7 +8083,7 @@ "type": "tidelift" } ], - "time": "2026-01-27T05:59:18+00:00" + "time": "2026-02-05T07:59:30+00:00" }, { "name": "sebastian/cli-parser", From a5058875d1c791280600da0c5903d17f9791d44e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Thu, 5 Feb 2026 16:17:49 +0800 Subject: [PATCH 04/54] remove unused parameter from inertia middleware --- app/Http/Middleware/HandleInertiaRequests.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php index 4f35c1a3..633d7975 100644 --- a/app/Http/Middleware/HandleInertiaRequests.php +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -37,7 +37,6 @@ public function share(Request $request): array return null; }, - 'ziggy' => new Ziggy, 'navigationItems' => $navigationItems, 'navigationNodes' => $navigationNodes, 'flash' => [ From f1da7430415d441ed5b772fdd9cee36ea122afae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Fri, 6 Feb 2026 01:27:47 +0800 Subject: [PATCH 05/54] remove unused library tailwind --- documents/mac.md | 1 - documents/windows.md | 1 - package-lock.json | 598 +---------------------------------------- package.json | 2 - resources/css/app.scss | 6 - tailwind.config.js | 20 -- vite.config.js | 2 - 7 files changed, 6 insertions(+), 624 deletions(-) delete mode 100644 tailwind.config.js diff --git a/documents/mac.md b/documents/mac.md index edaeb57b..0e2ebd0a 100644 --- a/documents/mac.md +++ b/documents/mac.md @@ -121,7 +121,6 @@ brew install composer - Svelte Snippets (JakobKruse.svelte-kit-snippets) - svelte-format (melihaltintas.svelte-format) - Thunder Client (rangav.vscode-thunder-client) -- Tailwind CSS IntelliSense (bradlc.vscode-tailwindcss) ### Setup Inertia.js from Laravel & PHP Essentials diff --git a/documents/windows.md b/documents/windows.md index 944dc8a2..c5d2748c 100644 --- a/documents/windows.md +++ b/documents/windows.md @@ -186,7 +186,6 @@ nvm install --lts - Svelte Snippets (JakobKruse.svelte-kit-snippets) - svelte-format (melihaltintas.svelte-format) - Thunder Client (rangav.vscode-thunder-client) -- Tailwind CSS IntelliSense (bradlc.vscode-tailwindcss) ### Setup Inertia.js from Laravel & PHP Essentials diff --git a/package-lock.json b/package-lock.json index ab2fb672..d69e1be7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,10 @@ "svelte-dnd-action": "^0.9.69" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.18", "axios": "^1.11.0", "concurrently": "^9.2.1", "laravel-vite-plugin": "^2.1.0", "sass": "1.78.0", - "tailwindcss": "^4.1.10", "vite": "^7.3.1" } }, @@ -1785,278 +1783,6 @@ "svelte": "^4.0.0 || ^5.0.0 || ^5.0.0-next.0" } }, - "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", - "jiti": "^2.6.1", - "lightningcss": "1.30.2", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" - } - }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" - ], - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", - "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@tailwindcss/vite": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.18.tgz", - "integrity": "sha512-jVA+/UpKL1vRLg6Hkao5jldawNmRo7mQYrZtNHMIVpLfLhDml5nMRUo/8MwoX2vNXvnaXNNMedrMfMugAVX1nA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "tailwindcss": "4.1.18" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7" - } - }, "node_modules/@types/color-convert": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.4.tgz", @@ -2673,16 +2399,6 @@ "node": ">=6" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/devalue": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz", @@ -2723,20 +2439,6 @@ "dev": true, "license": "MIT" }, - "node_modules/enhanced-resolve": { - "version": "5.19.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.19.0.tgz", - "integrity": "sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -3032,13 +2734,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true, - "license": "ISC" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3407,20 +3102,10 @@ "@types/estree": "^1.0.6" } }, - "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", - "devOptional": true, - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, "node_modules/laravel-precognition": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.1.tgz", - "integrity": "sha512-BYaDUjEclKbxuQTG9yZnRjjD7ag1Xh+8hGOXmcrw91/vpbIOJ8cSFADTI0kvwetIr3AM1vOJzYwaoA9+KHhLwA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/laravel-precognition/-/laravel-precognition-1.0.2.tgz", + "integrity": "sha512-0H08JDdMWONrL/N314fvsO3FATJwGGlFKGkMF3nNmizVFJaWs17816iM+sX7Rp8d5hUjYCx6WLfsehSKfaTxjg==", "license": "MIT", "dependencies": { "axios": "^1.4.0", @@ -3447,256 +3132,6 @@ "vite": "^7.0.0" } }, - "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", - "devOptional": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", - "cpu": [ - "arm" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", - "cpu": [ - "arm64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", - "cpu": [ - "x64" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -5081,9 +4516,9 @@ } }, "node_modules/svelte": { - "version": "5.49.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz", - "integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==", + "version": "5.49.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.2.tgz", + "integrity": "sha512-PYLwnngYzyhKzqDlGVlCH4z+NVI8mC0/bTv15vw25CcdOhxENsOHIbQ36oj5DIf3oBazM+STbCAvaskpxtBmWA==", "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -5115,27 +4550,6 @@ "svelte": ">=3.23.0 || ^5.0.0-next.0" } }, - "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index bcade997..ca9bc458 100644 --- a/package.json +++ b/package.json @@ -6,12 +6,10 @@ "dev": "vite && vite --ssr" }, "devDependencies": { - "@tailwindcss/vite": "^4.1.18", "axios": "^1.11.0", "concurrently": "^9.2.1", "laravel-vite-plugin": "^2.1.0", "sass": "1.78.0", - "tailwindcss": "^4.1.10", "vite": "^7.3.1" }, "dependencies": { diff --git a/resources/css/app.scss b/resources/css/app.scss index b348c8ee..94ebe059 100644 --- a/resources/css/app.scss +++ b/resources/css/app.scss @@ -5,12 +5,6 @@ $bootstrap-icons-font-src: url("../../node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff2") format("woff2"), url("../../node_modules/bootstrap-icons/font/fonts/bootstrap-icons.woff") format("woff"); @import "../../node_modules/bootstrap-icons/font/bootstrap-icons.scss"; -/* -@tailwind base; -@tailwind components; -@tailwind utilities; -*/ - .draggable { cursor: move; } diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index ce0c57fc..00000000 --- a/tailwind.config.js +++ /dev/null @@ -1,20 +0,0 @@ -import defaultTheme from 'tailwindcss/defaultTheme'; - -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', - './storage/framework/views/*.php', - './resources/**/*.blade.php', - './resources/**/*.js', - './resources/**/*.vue', - ], - theme: { - extend: { - fontFamily: { - sans: ['Figtree', ...defaultTheme.fontFamily.sans], - }, - }, - }, - plugins: [], -}; diff --git a/vite.config.js b/vite.config.js index b37360cd..7ee86737 100644 --- a/vite.config.js +++ b/vite.config.js @@ -2,7 +2,6 @@ import { defineConfig } from 'vite'; import laravel from 'laravel-vite-plugin'; import { svelte } from '@sveltejs/vite-plugin-svelte'; import path from 'path'; -import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [ @@ -12,7 +11,6 @@ export default defineConfig({ refresh: true, }), svelte(), - tailwindcss(), ], resolve: { alias: { From cd6cf355f4aa5bca7f731934b167b680640452fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Fri, 6 Feb 2026 02:58:37 +0800 Subject: [PATCH 06/54] remove unused code and add dark mode --- resources/js/Pages/Layouts/App.svelte | 83 ++++++++++++++++++--------- resources/js/app.js | 3 +- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/resources/js/Pages/Layouts/App.svelte b/resources/js/Pages/Layouts/App.svelte index a243db5a..611db7a5 100644 --- a/resources/js/Pages/Layouts/App.svelte +++ b/resources/js/Pages/Layouts/App.svelte @@ -1,13 +1,28 @@
- - - Mensa + + {#if route().current().startsWith('admin.')} + + {/if} + + Mensa + - +
From 2022c21d4eccd3c5553c07a29a5dd61430c4d956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Fri, 6 Feb 2026 20:53:59 +0800 Subject: [PATCH 16/54] fix missing super administrator role --- database/seeders/SuperAdministratorSeeder.php | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/database/seeders/SuperAdministratorSeeder.php b/database/seeders/SuperAdministratorSeeder.php index cc0da1ad..85a29e21 100644 --- a/database/seeders/SuperAdministratorSeeder.php +++ b/database/seeders/SuperAdministratorSeeder.php @@ -2,6 +2,7 @@ namespace Database\Seeders; +use App\Models\TeamRole; use App\Models\User; use Illuminate\Database\Seeder; @@ -17,16 +18,22 @@ class SuperAdministratorSeeder extends Seeder { public function run(): void { - $user = User::create([ - 'username' => 'superAdmin', - 'password' => 'password', - 'family_name' => 'Lam', - 'middle_name' => '', - 'given_name' => 'Mak', - 'gender_id' => 2, - 'passport_type_id' => 1, - 'passport_number' => '350321096003237001', - 'birthday' => '0960-03-23', + $user = User::firstOrCreate( + ['username' => 'superAdmin'], + [ + 'password' => 'password', + 'family_name' => 'Lam', + 'middle_name' => '', + 'given_name' => 'Mak', + 'gender_id' => 2, + 'passport_type_id' => 1, + 'passport_number' => '350321096003237001', + 'birthday' => '0960-03-23', + ] + ); + TeamRole::firstOrCreate([ + 'name' => 'Super Administrator', + 'guard_name' => 'web', ]); $user->assignRole('Super Administrator'); } From 93b9f8d7f11316a1b8f97ce7a0348a2d7419bd32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:07:27 +0800 Subject: [PATCH 17/54] move updateAddress function from AdmissionTest Controller to Address model for share to user and admin user controller --- .../Admin/AdmissionTest/Controller.php | 35 +------------------ app/Models/Address.php | 34 ++++++++++++++++-- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/app/Http/Controllers/Admin/AdmissionTest/Controller.php b/app/Http/Controllers/Admin/AdmissionTest/Controller.php index 25305d53..b9f74469 100644 --- a/app/Http/Controllers/Admin/AdmissionTest/Controller.php +++ b/app/Http/Controllers/Admin/AdmissionTest/Controller.php @@ -225,39 +225,6 @@ public function show(AdmissionTest $admissionTest) ); } - private function updateAddress(Address $address, string $newAddress, int $newDistrictID): Address - { - $addressModel = $address; - if ( - $newAddress != $address->value || - $newDistrictID != $address->district_id - ) { - $addressModel = Address::firstWhere([ - 'district_id' => $newDistrictID, - 'value' => $newAddress, - ]); - if ($address->admissionTests()->count() == 1) { - if ($addressModel) { - $address->delete(); - } else { - $address->update([ - 'district_id' => $newDistrictID, - 'value' => $newAddress, - ]); - $addressModel = $address; - } - } - if (! $addressModel) { - $addressModel = Address::create([ - 'district_id' => $newDistrictID, - 'value' => $newAddress, - ]); - } - } - - return $addressModel; - } - private function updateLocation(Location $location, string $newLocationName): Location { $newLocation = $location; @@ -295,7 +262,7 @@ public function update(TestRequest $request, AdmissionTest $admissionTest) 'location' => $admissionTest->location->name, 'address' => "{$admissionTest->address->value}, {$admissionTest->address->district->name}, {$admissionTest->address->district->area->name}", ]; - $address = $this->updateAddress($admissionTest->address, $request->address, $request->district_id); + $address = $admissionTest->address->updateAddress($request->district_id, $request->address); $location = $this->updateLocation($admissionTest->location, $request->location); $admissionTest->update([ 'type_id' => $request->type_id, diff --git a/app/Models/Address.php b/app/Models/Address.php index 0f004922..7a391f15 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -24,8 +24,38 @@ public function admissionTests() return $this->hasMany(AdmissionTest::class); } - public function members() + public function user() { - return $this->hasMany(Member::class); + return $this->hasMany(User::class); + } + + public function updateAddress($districtID, $value) + { + if ($districtID == $this->district_id && $value == $this->value) { + return $this; + } + $address = Address::firstWhere([ + 'district_id' => $districtID, + 'value' => $value, + ]); + if ($this->user()->count() + $this->admissionTests()->count() == 1) { + if ($address) { + $this->delete(); + } else { + $this->update([ + 'district_id' => $districtID, + 'value' => $value, + ]); + + return $this; + } + } elseif (! $address) { + $address = Address::create([ + 'district_id' => $districtID, + 'value' => $value, + ]); + } + + return $address; } } From a749cd6c991b7d0e0cd1902b67c50628921c05db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:15:59 +0800 Subject: [PATCH 18/54] update user profile for add address --- app/Http/Controllers/UserController.php | 31 ++++- resources/js/Pages/User/Profile.svelte | 170 +++++++++++++++++++----- 2 files changed, 163 insertions(+), 38 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 130225f0..df4754ce 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -129,18 +129,20 @@ public function show(Request $request) { $user = $request->user(); $user->load([ - 'admissionTests', 'emails.lastVerification' => function ($query) { + 'member', 'admissionTests', + 'emails.lastVerification' => function ($query) { $query->select(['contact_id', 'verified_at', 'expired_at']); }, 'mobiles.lastVerification' => function ($query) { $query->select(['contact_id', 'verified_at', 'expired_at']); - }, + }, 'address' ]); - $user->emails->append('is_verified'); - $user->mobiles->append('is_verified'); $user->makeHidden([ 'roles', 'permissions', 'synced_to_stripe', - 'created_at', 'updated_at', 'member', + 'created_at', 'updated_at', 'address_id', ]); + $user->member?->makeHidden(['user_id', 'created_at', 'updated_at']); + $user->emails->append('is_verified'); + $user->mobiles->append('is_verified'); $user->emails->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); $user->mobiles->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); $user->admissionTests->makeHidden([ @@ -150,6 +152,7 @@ public function show(Request $request) foreach ($user->admissionTests as $test) { $test->pivot->makeHidden('user_id', 'test_id'); } + $user->address?->makeHidden(['id', 'created_at', 'updated_at']); return Inertia::render('User/Profile') ->with('user', $user) @@ -165,6 +168,24 @@ public function show(Request $request) 'maxBirthday', now() ->subYears(2) ->format('Y-m-d') + )->with( + 'districts', function() { + $areas = Area::with([ + 'districts' => function ($query) { + $query->orderBy('display_order'); + }, + ])->orderBy('display_order') + ->get(); + $districts = []; + foreach ($areas as $area) { + $districts[$area->name] = []; + foreach ($area->districts as $district) { + $districts[$area->name][$district->id] = $district->name; + } + } + + return $districts; + } ); } diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index 4b4861b3..cec40584 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -8,9 +8,14 @@ import { alert } from '@/Pages/Components/Modals/Alert.svelte'; import { formatToDate } from '@/timeZoneDatetime'; - let { user: initUser, genders, passportTypes, maxBirthday } = $props(); + let { user: initUser, genders, passportTypes, maxBirthday, districts: areaDistricts } = $props(); let user = $state({ + id: initUser.id, + memberNumber: initUser.member?.number, username: initUser.username, + prefixName: initUser.member?.prefix_name, + nickname: initUser.member?.nickname, + suffixName: initUser.member?.suffix_name, familyName: initUser.family_name, middleName: initUser.middle_name, givenName: initUser.given_name, @@ -18,6 +23,8 @@ passportNumber: initUser.passport_number, genderID: initUser.gender_id, birthday: formatToDate(initUser.birthday), + districtID: initUser.address?.district_id , + address: initUser.address?.value, }); let inputs = $state({}); let editing = $state(false); @@ -29,8 +36,22 @@ newPassword: '', gender: '', birthday: '', + district: '', + address: '', }); + let usernameValue = $state(user.username); + let newPasswordValue = $state(''); + let confirmNewPasswordValue = $state(''); + let showPassportNumber = $state(false); + let districtValue = $state(`${user.districtID}`); + let districts = {}; + for(let [area, object] of Object.entries(areaDistricts)) { + for(let [key, value] of Object.entries(object)) { + districts[key] = value; + } + } + function hasError() { for(let [key, feedback] of Object.entries(feedbacks)) { if(feedback != 'Looks good!') { @@ -80,6 +101,14 @@ } else if(inputs.birthday.validity.rangeOverflow) { feedbacks.birthday = `The birthday not be greater than ${birthday.max} characters.`; } + if(inputs.district.value) { + if(inputs.address.validity.valueMissing) { + feedbacks.address = 'The address field is required when district is present.'; + } else if(inputs.address.validity.tooLong) { + feedbacks.mobile = `The address must not be greater than ${inputs.address.maxLength} characters.`; + } + } + return !hasError(); } @@ -88,8 +117,13 @@ inputs.password.value = ''; inputs.newPassword.value = ''; inputs.confirmNewPassword.value = ''; - inputs.gender.value = user.gender; + inputs.gender.value = genders[user.genderID]; inputs.birthday.value = user.birthday; + inputs.district.value = user.districtID; + inputs.address.value = user.address; + for(let key in feedbacks) { + feedbacks[key] = ''; + } } function successCallback(response) { @@ -98,6 +132,8 @@ user.username = response.data.username; user.genderID = response.data.gender_id; user.birthday = formatToDate(response.data.birthday); + user.districtID = response.data.district_id; + user.address = response.data.address; editing = false; resetInputValues(); submitting = false; @@ -108,8 +144,6 @@ if(error.status == 422) { for(let key in error.response.data.errors) { let value = error.response.data.errors[key]; - let feedback; - let input; switch(key) { case 'username': feedbacks.username = value; @@ -126,6 +160,12 @@ case 'birthday': feedbacks.birthday = value; break; + case 'district_id': + feedbacks.district = value; + break; + case 'address': + feedbacks.address = value; + break; default: alert(`Undefine Feedback Key: ${key}\nMessage: ${message}`); break; @@ -159,6 +199,10 @@ data['new_password'] = inputs.newPassword.value; data['new_password_confirmation'] = inputs.confirmNewPassword.value; } + if (inputs.district.value) { + data['district_id'] = inputs.district.value; + data['address'] = inputs.address.value; + } post( route('profile.update'), successCallback, @@ -174,6 +218,7 @@ function cancel(event) { event.preventDefault(); + resetInputValues(); editing = false; } @@ -190,7 +235,7 @@
-
+

Profile + - - + + - + - - + + + + + + + + + {#each Object.entries(areaDistricts) as [area, object]} + + {#each Object.entries(object) as [key, value]} + + {/each} + + {/each} + + + + + + +

@@ -334,4 +438,4 @@ {/if}
-
\ No newline at end of file + From 2b96023e16d3feec5bce9f1ffe53c95d413cdc89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:17:42 +0800 Subject: [PATCH 19/54] update user update profile back end form validation and update logic for add address --- app/Http/Controllers/UserController.php | 18 +++++++++++++++++- app/Http/Requests/User/UpdateRequest.php | 10 +++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index df4754ce..63ce3772 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -207,10 +207,26 @@ public function update(UpdateRequest $request) if ($request->new_password) { $update['password'] = $request->new_password; } + if ($user->address) { + if ($request->district_id) { + $update['address_id'] = $user->address->updateAddress($request->district_id, $request->address)->id; + } else { + $user->address->delete(); + $update['address_id'] = null; + } + } elseif ($request->district_id) { + $address = Address::firstOrCreate([ + 'district_id' => $request->district_id, + 'value' => $request->address, + ]); + $update['address_id'] = $address->id; + } $user->update($update); - $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $unsetKeys = ['password', 'new_password', 'new_password_confirmation', 'address_id']; $return = array_diff_key($update, array_flip($unsetKeys)); $return['gender'] = $request->gender; + $return['district_id'] = $request->district_id; + $return['address'] = $request->address; $return['success'] = 'The profile update success!'; DB::commit(); diff --git a/app/Http/Requests/User/UpdateRequest.php b/app/Http/Requests/User/UpdateRequest.php index 954efd5c..166e19e2 100644 --- a/app/Http/Requests/User/UpdateRequest.php +++ b/app/Http/Requests/User/UpdateRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\User; +use App\Models\District; use App\Models\User; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -28,11 +29,18 @@ public function rules(): array 'new_password' => 'nullable|string|min:8|max:16|confirmed', 'gender' => 'required|string|max:255', 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), + 'district_id' => 'nullable|integer|exists:'.District::class.',id', + 'address' => 'required_with:district_id|string|max:255', ]; } public function messages(): array { - return ['password.required' => 'The password field is required when you change the username or password.']; + return [ + 'password.required' => 'The password field is required when you change the username or password.', + 'district_id.integer' => 'The district field must be an integer.', + 'district_id.exists' => 'The selected district is invalid.', + 'address.required_with' => 'The address field is required when district is present.', + ]; } } From 7014e1755ed5d66a2bd2bcfcc056c50edbb3777f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:18:21 +0800 Subject: [PATCH 20/54] update user update profile tests for add address --- tests/Feature/User/UpdateTest.php | 158 ++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 6 deletions(-) diff --git a/tests/Feature/User/UpdateTest.php b/tests/Feature/User/UpdateTest.php index 2c11bd44..2565a1b3 100644 --- a/tests/Feature/User/UpdateTest.php +++ b/tests/Feature/User/UpdateTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\User; +use App\Models\Address; +use App\Models\District; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -61,7 +63,7 @@ public function test_username_too_long() $response->assertInvalid(['username' => 'The username field must not be greater than 16 characters.']); } - public function test_with_change_username_missing_password() + public function test_missing_password_when_username_present() { $data = $this->happyCase; $data['username'] = 'testing2'; @@ -69,7 +71,7 @@ public function test_with_change_username_missing_password() $response->assertInvalid(['password' => 'The password field is required when you change the username or password.']); } - public function test_with_change_password_missing_password() + public function test_missing_password_when_new_password_present() { $data = $this->happyCase; $data['new_password'] = '98765432'; @@ -184,7 +186,51 @@ public function test_birthday_too_close() $response->assertInvalid(['birthday' => "The birthday field must be a date before or equal to $beforeTwoYear."]); } - public function test_without_change_username_and_new_password_happy_case() + public function test_district_id_is_not_integer() + { + $data = $this->happyCase; + $data['district_id'] = 'abc'; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The district field must be an integer.']); + } + + public function test_district_id_not_exists() + { + $data = $this->happyCase; + $data['district_id'] = 0; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The selected district is invalid.']); + } + + public function test_address_required_when_district_id_present() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field is required when district is present.']); + } + + public function test_address_not_string() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = ['123 Street']; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field must be a string.']); + } + + public function test_address_too_long() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = str_repeat('a', 256); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); + } + + public function test_without_change_username_and_new_password_address_happy_case() { $data = $this->happyCase; $response = $this->actingAs($this->user)->put(route('profile.update'), $data); @@ -195,7 +241,7 @@ public function test_without_change_username_and_new_password_happy_case() $response->assertJson($expect); } - public function test_with_change_username_without_new_password_happy_case() + public function test_with_change_username_without_new_password_and_address_happy_case() { $data = $this->happyCase; $data['username'] = 'testing2'; @@ -208,7 +254,7 @@ public function test_with_change_username_without_new_password_happy_case() $response->assertJson($expect); } - public function test_with_new_password_without_change_username_happy_case() + public function test_with_new_password_without_change_username_and_address_happy_case() { $data = $this->happyCase; $data['password'] = '12345678'; @@ -222,7 +268,107 @@ public function test_with_new_password_without_change_username_happy_case() $response->assertJson($expect); } - public function test_with_change_username_and_new_password_happy_case() + public function test_with_change_address_when_before_user_has_no_address_and_without_change_username_and_new_password_happy_case() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + } + + public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password_happy_case() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + $this->assertFalse( + Address::where('district_id', $address->district_id) + ->where('value', $address->value) + ->exists() + ); + } + + public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_ithout_change_username_and_new_password_happy_case() + { + $data = $this->happyCase; + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['address'] = null; + $expect['district_id'] = null; + $expect['success'] = 'The profile update success!'; + $response->assertJson($expect); + $this->assertFalse( + Address::where('district_id', $address->district_id) + ->where('value', $address->value) + ->exists() + ); + $this->assertNull($this->user->fresh()->address_id); + } + + public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_other_object_using_and_without_change_username_and_new_password_happy_case() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + User::factory()->state(['address_id' => $address->id])->create(); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + $this->assertTrue( + Address::where('district_id', $address->district_id) + ->where('value', $address->value) + ->exists() + ); + $this->assertNotEquals($address->id, $this->user->fresh()->address_id); + } + + public function test_with_change_username_and_new_password_and_without_address_happy_case() { $data = $this->happyCase; $data['username'] = 'testing2'; From 405f5491b040e8035fa99d12bd45f08a893cf8ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:19:11 +0800 Subject: [PATCH 21/54] fix register tests missing add address too long test care --- tests/Feature/User/RegisterTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/Feature/User/RegisterTest.php b/tests/Feature/User/RegisterTest.php index d6416c6e..93171a9c 100644 --- a/tests/Feature/User/RegisterTest.php +++ b/tests/Feature/User/RegisterTest.php @@ -345,7 +345,7 @@ public function test_district_id_not_exist() $response->assertInvalid(['district_id' => 'The selected district is invalid.']); } - public function test_address_required_with_district_id() + public function test_missing_address_when_district_id_present() { $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; @@ -362,6 +362,15 @@ public function test_address_not_string() $response->assertInvalid(['address' => 'The address field must be a string.']); } + public function test_address_too_long() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = str_repeat('a', 256); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); + } + public function test_without_middle_name_and_mobile_and_email_and_address_happy_case() { $data = $this->happyCase; From 56a1163052979d5d8ed8603f041821fda60fc5a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sat, 7 Feb 2026 19:26:28 +0800 Subject: [PATCH 22/54] add auto add required mark css and fix some labels missing "for" attribute --- resources/css/app.scss | 12 +++++ .../Admin/AdmissionTest/Orders/Create.svelte | 14 +++--- .../Admin/AdmissionTest/Orders/Index.svelte | 6 +-- .../js/Pages/Admin/CustomWebPages/Form.svelte | 14 +++--- resources/js/Pages/Admin/Users/Show.svelte | 48 +++++++++---------- resources/js/Pages/User/Register.svelte | 32 ++++++------- 6 files changed, 69 insertions(+), 57 deletions(-) diff --git a/resources/css/app.scss b/resources/css/app.scss index 94ebe059..c411b7c2 100644 --- a/resources/css/app.scss +++ b/resources/css/app.scss @@ -18,3 +18,15 @@ aside nav > ul > li { width: max-content; min-width: 100%; } + +.form-floating:has([required]:empty) label:after { + content: " *"; + color: var(--bs-danger-text-emphasis); +} + +label:has(+ input:required:enabled:not([hidden])):after, +label:has(+ select:required:enabled:not([hidden])):after, +label:has(+ textarea:required:enabled:not([hidden])):after { + content: ' *'; + color: var(--bs-danger-text-emphasis); +} diff --git a/resources/js/Pages/Admin/AdmissionTest/Orders/Create.svelte b/resources/js/Pages/Admin/AdmissionTest/Orders/Create.svelte index d5e00101..9ee5053f 100644 --- a/resources/js/Pages/Admin/AdmissionTest/Orders/Create.svelte +++ b/resources/js/Pages/Admin/AdmissionTest/Orders/Create.svelte @@ -57,7 +57,7 @@ }, (new Date).addMinute().startOfMinute() - (new Date) ); - + function hasError() { for(let [key, feedback] of Object.entries(feedbacks)) { if(feedback != 'Looks good!') { @@ -184,7 +184,7 @@ creating = false; submitting = false; } - + function create(event) { event.preventDefault(); if(! submitting) { @@ -367,9 +367,9 @@ - - - + - + @@ -123,4 +123,4 @@ {/if} - \ No newline at end of file + diff --git a/resources/js/Pages/Admin/CustomWebPages/Form.svelte b/resources/js/Pages/Admin/CustomWebPages/Form.svelte index 5e826a4a..9a973748 100644 --- a/resources/js/Pages/Admin/CustomWebPages/Form.svelte +++ b/resources/js/Pages/Admin/CustomWebPages/Form.svelte @@ -142,7 +142,7 @@ - + https://{import.meta.env.VITE_APP_URL}/ - + - + - + - - Content + - \ No newline at end of file + diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index 61cd94e4..b0f55ace 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -27,7 +27,7 @@ user.defaultEmail = row.id; } } - + for(let row of initUser.mobiles) { if(row.is_default) { user.defaultMobile = row.id; @@ -57,7 +57,7 @@ inputs.givenName.value = user.givenName; inputs.passportType.value = user.passportTypeID; inputs.passportNumber.value = user.passportNumber; - inputs.gender.value = user.genderID; + inputs.gender.value = genders[user.genderID]; inputs.birthday.value = user.birthday; for(let key in feedbacks) { feedbacks[key] = ''; @@ -177,7 +177,7 @@ submitting = false; updating = false; } - + function update(event) { event.preventDefault(); if(submitting == '') { @@ -265,7 +265,7 @@

Info - {#if + {#if auth.user.permissions.includes('Edit:User') || auth.user.roles.includes('Super Administrator') } @@ -279,20 +279,20 @@ {/if}

- - + + - + ******** - {#if + {#if auth.user.permissions.includes('Edit:User') || auth.user.roles.includes('Super Administrator') } @@ -314,38 +314,37 @@ - - + + - - + + - - + + - - + {value} {/each} + - - + + - - + + - + - - + + @@ -395,4 +395,4 @@ - \ No newline at end of file + diff --git a/resources/js/Pages/User/Register.svelte b/resources/js/Pages/User/Register.svelte index 9265fbf8..f42b92dd 100644 --- a/resources/js/Pages/User/Register.svelte +++ b/resources/js/Pages/User/Register.svelte @@ -251,28 +251,28 @@

Register

- + - + - + - + - + - + - + - + - + - + - + - + - + - + Date: Sat, 7 Feb 2026 21:45:34 +0800 Subject: [PATCH 23/54] fix some table missing unique attribute --- database/migrations/2024_09_15_082949_create_genders_table.php | 2 +- .../2024_09_15_083156_create_passport_types_table.php | 2 +- database/migrations/2024_12_29_192521_create_teams_table.php | 2 +- .../migrations/2025_03_06_221826_create_site_pages_table.php | 2 +- .../migrations/2025_03_06_221841_create_site_contents_table.php | 1 + .../2025_05_27_211341_create_other_payment_gateways_table.php | 2 +- .../2026_01_28_134847_create_national_mensas_table.php | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/database/migrations/2024_09_15_082949_create_genders_table.php b/database/migrations/2024_09_15_082949_create_genders_table.php index 8a2fa046..b2e87096 100644 --- a/database/migrations/2024_09_15_082949_create_genders_table.php +++ b/database/migrations/2024_09_15_082949_create_genders_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('genders', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->timestamps(); }); } diff --git a/database/migrations/2024_09_15_083156_create_passport_types_table.php b/database/migrations/2024_09_15_083156_create_passport_types_table.php index 9a0be87c..dbbf427f 100644 --- a/database/migrations/2024_09_15_083156_create_passport_types_table.php +++ b/database/migrations/2024_09_15_083156_create_passport_types_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('passport_types', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->timestamps(); }); } diff --git a/database/migrations/2024_12_29_192521_create_teams_table.php b/database/migrations/2024_12_29_192521_create_teams_table.php index c49d1c17..0553c9b0 100644 --- a/database/migrations/2024_12_29_192521_create_teams_table.php +++ b/database/migrations/2024_12_29_192521_create_teams_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('teams', function (Blueprint $table) { $table->id(); - $table->string('name', 170); + $table->string('name', 170)->unique(); $table->unsignedBigInteger('type_id'); $table->unsignedBigInteger('display_order')->default(0); $table->timestamps(); diff --git a/database/migrations/2025_03_06_221826_create_site_pages_table.php b/database/migrations/2025_03_06_221826_create_site_pages_table.php index 79586fd4..bd60fccd 100644 --- a/database/migrations/2025_03_06_221826_create_site_pages_table.php +++ b/database/migrations/2025_03_06_221826_create_site_pages_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('site_pages', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->timestamps(); }); } diff --git a/database/migrations/2025_03_06_221841_create_site_contents_table.php b/database/migrations/2025_03_06_221841_create_site_contents_table.php index 839a66a5..b0077513 100644 --- a/database/migrations/2025_03_06_221841_create_site_contents_table.php +++ b/database/migrations/2025_03_06_221841_create_site_contents_table.php @@ -17,6 +17,7 @@ public function up(): void $table->string('name'); $table->text('content')->nullable(); $table->timestamps(); + $table->unique(['page_id', 'name']); }); } diff --git a/database/migrations/2025_05_27_211341_create_other_payment_gateways_table.php b/database/migrations/2025_05_27_211341_create_other_payment_gateways_table.php index abc157eb..436491fb 100644 --- a/database/migrations/2025_05_27_211341_create_other_payment_gateways_table.php +++ b/database/migrations/2025_05_27_211341_create_other_payment_gateways_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('other_payment_gateways', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->boolean('is_active')->default(0); $table->unsignedBigInteger('display_order')->default(0); $table->timestamps(); diff --git a/database/migrations/2026_01_28_134847_create_national_mensas_table.php b/database/migrations/2026_01_28_134847_create_national_mensas_table.php index a8b6ff59..d21c42d2 100644 --- a/database/migrations/2026_01_28_134847_create_national_mensas_table.php +++ b/database/migrations/2026_01_28_134847_create_national_mensas_table.php @@ -13,7 +13,7 @@ public function up(): void { Schema::create('national_mensas', function (Blueprint $table) { $table->id(); - $table->string('name'); + $table->string('name')->unique(); $table->string('url', 1855); $table->boolean('is_active'); $table->timestamps(); From 401229f3f0eb7deecb3f7fe744daf8d1cf3e536b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sun, 8 Feb 2026 00:59:36 +0800 Subject: [PATCH 24/54] fix user tests same function name wrong --- tests/Feature/User/UpdateTest.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/Feature/User/UpdateTest.php b/tests/Feature/User/UpdateTest.php index 2565a1b3..ecf2a7e8 100644 --- a/tests/Feature/User/UpdateTest.php +++ b/tests/Feature/User/UpdateTest.php @@ -230,7 +230,7 @@ public function test_address_too_long() $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); } - public function test_without_change_username_and_new_password_address_happy_case() + public function test_without_change_username_and_new_password_and_address_happy_case() { $data = $this->happyCase; $response = $this->actingAs($this->user)->put(route('profile.update'), $data); @@ -268,7 +268,7 @@ public function test_with_new_password_without_change_username_and_address_happy $response->assertJson($expect); } - public function test_with_change_address_when_before_user_has_no_address_and_without_change_username_and_new_password_happy_case() + public function test_with_change_address_when_before_user_have_no_address_and_without_change_username_and_new_password_happy_case() { $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; @@ -312,9 +312,10 @@ public function test_with_change_address_when_before_user_has_address_and_the_us ->where('value', $address->value) ->exists() ); + $this->assertEquals(1,Address::count()); } - public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_ithout_change_username_and_new_password_happy_case() + public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password_happy_case() { $data = $this->happyCase; $address = Address::create([ @@ -330,12 +331,8 @@ public function test_without_address_when_before_user_has_address_and_the_user_a $expect['district_id'] = null; $expect['success'] = 'The profile update success!'; $response->assertJson($expect); - $this->assertFalse( - Address::where('district_id', $address->district_id) - ->where('value', $address->value) - ->exists() - ); $this->assertNull($this->user->fresh()->address_id); + $this->assertEquals(0,Address::count()); } public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_other_object_using_and_without_change_username_and_new_password_happy_case() @@ -366,6 +363,7 @@ public function test_with_change_address_when_before_user_has_address_and_the_us ->exists() ); $this->assertNotEquals($address->id, $this->user->fresh()->address_id); + $this->assertEquals(2,Address::count()); } public function test_with_change_username_and_new_password_and_without_address_happy_case() From 4f364ba188290a65afbf49d24ea02f49ff1c4dc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sun, 8 Feb 2026 01:01:26 +0800 Subject: [PATCH 25/54] fix user profile when district_is null districtValue is not '' --- resources/js/Pages/User/Profile.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index cec40584..1774acb4 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -44,7 +44,7 @@ let newPasswordValue = $state(''); let confirmNewPasswordValue = $state(''); let showPassportNumber = $state(false); - let districtValue = $state(`${user.districtID}`); + let districtValue = $state(`${user.districtID ?? ''}`); let districts = {}; for(let [area, object] of Object.entries(areaDistricts)) { for(let [key, value] of Object.entries(object)) { From 709b8af0dd774644427869ee6a9bd3bfa59ee48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sun, 8 Feb 2026 01:17:33 +0800 Subject: [PATCH 26/54] update admin user show for add address --- app/Http/Controllers/Admin/UserController.php | 27 ++++- resources/js/Pages/Admin/Users/Show.svelte | 113 ++++++++++++++++-- 2 files changed, 124 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 3f487bfd..bad0c72d 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -5,6 +5,8 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Admin\User\ResetPasswordRequest; use App\Http\Requests\Admin\User\UpdateRequest; +use App\Models\Address; +use App\Models\Area; use App\Models\Gender; use App\Models\PassportType; use App\Models\ResetPasswordLog; @@ -124,16 +126,18 @@ public function index(Request $request) public function show(User $user) { $user->load([ - 'emails.lastVerification' => function ($query) { + 'member', 'emails.lastVerification' => function ($query) { $query->select(['contact_id', 'verified_at', 'expired_at']); }, 'mobiles.lastVerification' => function ($query) { $query->select(['contact_id', 'verified_at', 'expired_at']); - }, + }, 'address' ]); + $user->member?->makeHidden(['user_id', 'created_at', 'updated_at']); $user->emails->append('is_verified'); $user->emails->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); $user->mobiles->append('is_verified'); $user->mobiles->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); + $user->address?->makeHidden(['id', 'created_at', 'updated_at']); return Inertia::render('Admin/Users/Show') ->with('user', $user) @@ -149,6 +153,24 @@ public function show(User $user) 'maxBirthday', now() ->subYears(2) ->format('Y-m-d') + )->with( + 'districts', function() { + $areas = Area::with([ + 'districts' => function ($query) { + $query->orderBy('display_order'); + }, + ])->orderBy('display_order') + ->get(); + $districts = []; + foreach ($areas as $area) { + $districts[$area->name] = []; + foreach ($area->districts as $district) { + $districts[$area->name][$district->id] = $district->name; + } + } + + return $districts; + } ); } @@ -167,7 +189,6 @@ public function update(UpdateRequest $request, User $user) 'birthday' => $request->birthday, ]; $user->update($return); - unset($return['gender_id']); $return['gender'] = $gender->name; $return['success'] = 'The user data update success!'; DB::commit(); diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index b0f55ace..36d4bb6f 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -7,10 +7,14 @@ import { Button, Spinner, Col, Row, Label, Input } from '@sveltestrap/sveltestrap'; import { formatToDate } from '@/timeZoneDatetime'; - let {auth, user: initUser, passportTypes, genders, maxBirthday} = $props(); + let {auth, user: initUser, passportTypes, genders, maxBirthday, districts: areaDistricts} = $props(); let user = $state({ id: initUser.id, + memberNumber: initUser.member?.number, username: initUser.username, + prefixName: initUser.member?.prefix_name, + nickname: initUser.member?.nickname, + suffixName: initUser.member?.suffix_name, familyName: initUser.family_name, middleName: initUser.middle_name, givenName: initUser.given_name, @@ -18,6 +22,8 @@ passportNumber: initUser.passport_number, genderID: initUser.gender_id, birthday: formatToDate(initUser.birthday), + districtID: initUser.address?.district_id , + address: initUser.address?.value, defaultEmail: null, defaultMobile: null }); @@ -48,8 +54,18 @@ passportNumber: '', gender: '', birthday: '', + district: '', + address: '', }); + let districtValue = $state(`${user.districtID ?? ''}`); + let districts = {}; + for(let [area, object] of Object.entries(areaDistricts)) { + for(let [key, value] of Object.entries(object)) { + districts[key] = value; + } + } + function resetInputValues() { inputs.username.value = user.username; inputs.familyName.value = user.familyName; @@ -59,6 +75,8 @@ inputs.passportNumber.value = user.passportNumber; inputs.gender.value = genders[user.genderID]; inputs.birthday.value = user.birthday; + inputs.district.value = user.districtID; + inputs.address.value = user.address; for(let key in feedbacks) { feedbacks[key] = ''; } @@ -117,6 +135,14 @@ } else if(inputs.birthday.validity.rangeOverflow) { feedbacks.birthday = `The birthday field must be a date before or equal to ${inputs.birthday.max}.`; } + if(inputs.district.value) { + if(inputs.address.validity.valueMissing) { + feedbacks.address = 'The address field is required when district is present.'; + } else if(inputs.address.validity.tooLong) { + feedbacks.mobile = `The address must not be greater than ${inputs.address.maxLength} characters.`; + } + } + return ! hasError(); } @@ -131,6 +157,8 @@ user.passportNumber = response.data.passport_number; user.genderID = response.data.gender_id; user.birthday = response.data.birthday; + user.districtID = response.data.district_id; + user.address = response.data.address; editing = false; resetInputValues(); submitting = false; @@ -141,8 +169,6 @@ if(error.status == 422) { for(let key in error.response.data.errors) { let value = error.response.data.errors[key]; - let feedback; - let input; switch(key) { case 'username': feedbacks.username = value;; @@ -168,6 +194,12 @@ case 'birthday': feedbacks.birthday = value; break; + case 'district_id': + feedbacks.district = value; + break; + case 'address': + feedbacks.address = value; + break; default: alert(`Undefine Feedback Key: ${key}\nMessage: ${message}`); break; @@ -186,6 +218,20 @@ if(submitting == 'update'+submitAt) { if(validation()) { updating = true; + let data = { + username: inputs.username.value, + family_name: inputs.familyName.value, + middle_name: inputs.middleName.value, + given_name: inputs.givenName.value, + passport_type_id: inputs.passportType.value, + passport_number: inputs.passportNumber.value, + gender: inputs.gender.value, + birthday: inputs.birthday.value, + } + if (inputs.district.value) { + data['district_id'] = inputs.district.value; + data['address'] = inputs.address.value; + } post( route( 'admin.users.update', @@ -193,16 +239,7 @@ ), updateSuccessCallback, updateFailCallback, - 'put', { - username: inputs.username.value, - family_name: inputs.familyName.value, - middle_name: inputs.middleName.value, - given_name: inputs.givenName.value, - passport_type_id: inputs.passportType.value, - passport_number: inputs.passportNumber.value, - gender: inputs.gender.value, - birthday: inputs.birthday.value, - } + 'put', data ); } else { submitting = false; @@ -278,6 +315,15 @@ {/if} + +
User ID:
+
{user.id}
+ + +
Member Number:
+
{user.memberNumber}
+ + + {#if user.memberNumber} + +
Prefix Name:
+
{user.prefixName ?? "\u00A0"}
+ + +
Nickname:
+
{user.nickname ?? "\u00A0"}
+ + +
Suffix Name:
+
{user.suffixName ?? "\u00A0"}
+ + {/if} + + + + + + {#each Object.entries(areaDistricts) as [area, object]} + + {#each Object.entries(object) as [key, value]} + + {/each} + + {/each} + + + + + + + + Date: Sun, 8 Feb 2026 01:19:45 +0800 Subject: [PATCH 27/54] update admin user update back end form validation and update logic for add address --- app/Http/Controllers/Admin/UserController.php | 17 +++++++++++++++++ app/Http/Requests/Admin/User/UpdateRequest.php | 6 ++++++ 2 files changed, 23 insertions(+) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index bad0c72d..70db691a 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -188,7 +188,24 @@ public function update(UpdateRequest $request, User $user) 'gender_id' => $gender->id, 'birthday' => $request->birthday, ]; + if ($user->address) { + if ($request->district_id) { + $return['address_id'] = $user->address->updateAddress($request->district_id, $request->address)->id; + } else { + $user->address->delete(); + $return['address_id'] = null; + } + } elseif ($request->district_id) { + $address = Address::firstOrCreate([ + 'district_id' => $request->district_id, + 'value' => $request->address, + ]); + $return['address_id'] = $address->id; + } $user->update($return); + unset($return['address_id']); + $return['district_id'] = $request->district_id; + $return['address'] = $request->address; $return['gender'] = $gender->name; $return['success'] = 'The user data update success!'; DB::commit(); diff --git a/app/Http/Requests/Admin/User/UpdateRequest.php b/app/Http/Requests/Admin/User/UpdateRequest.php index eeec189a..f6f58701 100644 --- a/app/Http/Requests/Admin/User/UpdateRequest.php +++ b/app/Http/Requests/Admin/User/UpdateRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests\Admin\User; +use App\Models\District; use App\Models\PassportType; use App\Models\User; use Illuminate\Foundation\Http\FormRequest; @@ -31,6 +32,8 @@ public function rules(): array 'passport_number' => 'required|regex:/^[A-Z0-9]+$/|min:8|max:18', 'gender' => 'required|string|max:255', 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), + 'district_id' => 'nullable|integer|exists:'.District::class.',id', + 'address' => 'required_with:district_id|string|max:255', ]; } @@ -39,6 +42,9 @@ public function messages(): array return [ 'passport_type_id.required' => 'The passport type field is required.', 'passport_type_id.exists' => 'The selected passport type is invalid.', + 'district_id.integer' => 'The district field must be an integer.', + 'district_id.exists' => 'The selected district is invalid.', + 'address.required_with' => 'The address field is required when district is present.', ]; } } From 8753fcfabf71ba1ac43299da2e2cc0acc8cf18da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sun, 8 Feb 2026 01:20:37 +0800 Subject: [PATCH 28/54] update admin update user update tests for add address --- tests/Feature/Admin/Users/UpdateTest.php | 189 ++++++++++++++++++++++- 1 file changed, 187 insertions(+), 2 deletions(-) diff --git a/tests/Feature/Admin/Users/UpdateTest.php b/tests/Feature/Admin/Users/UpdateTest.php index c0b0f09e..bf4a0b5e 100644 --- a/tests/Feature/Admin/Users/UpdateTest.php +++ b/tests/Feature/Admin/Users/UpdateTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Admin\Users; +use App\Models\Address; +use App\Models\District; use App\Models\Gender; use App\Models\ModulePermission; use App\Models\User; @@ -435,7 +437,66 @@ public function test_birthday_too_close() $response->assertInvalid(['birthday' => "The birthday field must be a date before or equal to $beforeTwoYear."]); } - public function test_happy_case_without_middle_name() + public function test_district_id_is_not_integer() + { + $data = $this->happyCase; + $data['district_id'] = 'abc'; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['district_id' => 'The district field must be an integer.']); + } + + public function test_district_id_is_not_exist() + { + $data = $this->happyCase; + $data['district_id'] = 0; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['district_id' => 'The selected district is invalid.']); + } + + public function test_address_required_when_district_id_present() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['address' => 'The address field is required when district is present.']); + } + + public function test_address_too_long() + { + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = str_repeat('a', 256); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); + } + + public function test_happy_case_without_middle_name_and_address() { $data = $this->happyCase; $response = $this->actingAs($this->user) @@ -460,7 +521,7 @@ public function test_happy_case_without_middle_name() $this->assertEquals($data['birthday'], $user->birthday->format('Y-m-d')); } - public function test_happy_case_with_middle_name() + public function test_happy_case_with_middle_name_and_without_address() { $data = $this->happyCase; $data['middle_name'] = 'intelligent'; @@ -485,4 +546,128 @@ public function test_happy_case_with_middle_name() $this->assertEquals($data['gender'], $user->gender->name); $this->assertEquals($data['birthday'], $user->birthday->format('Y-m-d')); } + + public function test_with_change_address_when_before_user_have_no_address_and_without_middle_name_happy_case() + { + $data = $this->happyCase; + unset($data['middle_name']); + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The user data update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + } + + public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name_happy_case() + { + $data = $this->happyCase; + unset($data['middle_name']); + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The user data update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + $this->assertFalse( + Address::where('district_id', $address->district_id) + ->where('value', $address->value) + ->exists() + ); + $this->assertEquals(1,Address::count()); + } + + public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name_happy_case() + { + $data = $this->happyCase; + unset($data['middle_name']); + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The user data update success!'; + $response->assertJson($expect); + $this->assertNull($this->user->fresh()->address_id); + $this->assertEquals(0,Address::count()); + } + + public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_other_object_using_and_without_middle_name_happy_case() + { + $data = $this->happyCase; + unset($data['middle_name']); + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => District::inRandomOrder()->first()->id, + 'value' => '456 Street', + ]); + User::factory()->state(['address_id' => $address->id])->create(); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The user data update success!'; + $response->assertJson($expect); + $this->assertTrue( + Address::where('district_id', $data['district_id']) + ->where('value', $data['address']) + ->exists() + ); + $this->assertTrue( + Address::where('district_id', $address->district_id) + ->where('value', $address->value) + ->exists() + ); + $this->assertNotEquals($address->id, $this->user->fresh()->address_id); + $this->assertEquals(2,Address::count()); + } } From 81e9c516cec5e5a8271c07e1ab8b02a5439c2107 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Sun, 8 Feb 2026 21:56:02 +0800 Subject: [PATCH 29/54] add active member and has in progress membership order must type address rule for user update and update tests --- app/Http/Controllers/Admin/UserController.php | 1 + app/Http/Controllers/UserController.php | 1 + .../Requests/Admin/User/UpdateRequest.php | 15 +++- app/Http/Requests/User/UpdateRequest.php | 16 +++- app/Models/Member.php | 1 + resources/js/Pages/Admin/Users/Show.svelte | 24 ++++-- resources/js/Pages/User/Profile.svelte | 26 ++++--- tests/Feature/Admin/Users/UpdateTest.php | 74 ++++++++++++++++++- tests/Feature/User/UpdateTest.php | 71 ++++++++++++++++++ 9 files changed, 207 insertions(+), 22 deletions(-) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index 70db691a..aacd600a 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -133,6 +133,7 @@ public function show(User $user) }, 'address' ]); $user->member?->makeHidden(['user_id', 'created_at', 'updated_at']); + $user->member?->append('is_active'); $user->emails->append('is_verified'); $user->emails->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); $user->mobiles->append('is_verified'); diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 63ce3772..553e5a77 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -141,6 +141,7 @@ public function show(Request $request) 'created_at', 'updated_at', 'address_id', ]); $user->member?->makeHidden(['user_id', 'created_at', 'updated_at']); + $user->member?->append('is_active'); $user->emails->append('is_verified'); $user->mobiles->append('is_verified'); $user->emails->makeHidden(['user_id', 'type', 'created_at', 'lastVerification']); diff --git a/app/Http/Requests/Admin/User/UpdateRequest.php b/app/Http/Requests/Admin/User/UpdateRequest.php index f6f58701..6e579682 100644 --- a/app/Http/Requests/Admin/User/UpdateRequest.php +++ b/app/Http/Requests/Admin/User/UpdateRequest.php @@ -18,6 +18,15 @@ public function authorize(): bool public function rules(): array { $user = $this->route('user'); + $districtUtility = 'nullable'; + $addressUtility = 'required_with:district_id'; + if( + $this->user()->member?->isActive || + $this->user()->member?->orders()->where('expired_at', '>', now())->exists() + ) { + $districtUtility = 'required'; + $addressUtility = 'required'; + } return [ 'username' => [ @@ -32,8 +41,8 @@ public function rules(): array 'passport_number' => 'required|regex:/^[A-Z0-9]+$/|min:8|max:18', 'gender' => 'required|string|max:255', 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), - 'district_id' => 'nullable|integer|exists:'.District::class.',id', - 'address' => 'required_with:district_id|string|max:255', + 'district_id' => $districtUtility.'|integer|exists:'.District::class.',id', + 'address' => $addressUtility.'|string|max:255', ]; } @@ -42,9 +51,11 @@ public function messages(): array return [ 'passport_type_id.required' => 'The passport type field is required.', 'passport_type_id.exists' => 'The selected passport type is invalid.', + 'district_id.required' => 'The district field is required when user is an active member or has membership order in progress.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', 'address.required_with' => 'The address field is required when district is present.', + 'address.required' => 'The address field is required when user is an active member or has membership order in progress.', ]; } } diff --git a/app/Http/Requests/User/UpdateRequest.php b/app/Http/Requests/User/UpdateRequest.php index 166e19e2..2b043ab0 100644 --- a/app/Http/Requests/User/UpdateRequest.php +++ b/app/Http/Requests/User/UpdateRequest.php @@ -16,6 +16,16 @@ public function authorize(): bool public function rules(): array { + $districtUtility = 'nullable'; + $addressUtility = 'required_with:district_id'; + if( + $this->user()->member?->isActive || + $this->user()->member?->orders()->where('expired_at', '>', now())->exists() + ) { + $districtUtility = 'required'; + $addressUtility = 'required'; + } + return [ 'username' => [ 'required', 'string', 'min:8', 'max:16', @@ -29,8 +39,8 @@ public function rules(): array 'new_password' => 'nullable|string|min:8|max:16|confirmed', 'gender' => 'required|string|max:255', 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), - 'district_id' => 'nullable|integer|exists:'.District::class.',id', - 'address' => 'required_with:district_id|string|max:255', + 'district_id' => $districtUtility.'|integer|exists:'.District::class.',id', + 'address' => $addressUtility.'|string|max:255', ]; } @@ -38,8 +48,10 @@ public function messages(): array { return [ 'password.required' => 'The password field is required when you change the username or password.', + 'district_id.required' => 'The district field is required when you are an active member or have membership order in progress.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', + 'address.required' => 'The address field is required when you are an active member or have membership order in progress.', 'address.required_with' => 'The address field is required when district is present.', ]; } diff --git a/app/Models/Member.php b/app/Models/Member.php index fa41de91..94732c7b 100644 --- a/app/Models/Member.php +++ b/app/Models/Member.php @@ -67,6 +67,7 @@ function ($query) use ($thisYear) { } )->exists() || $this->transfers() ->where('is_accepted', true) + ->whereIn('type', ['in', 'guest']) ->where( function ($query) use ($thisYear) { $query->whereNull('membership_ended_in') diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index 36d4bb6f..4ce3bc32 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -11,6 +11,7 @@ let user = $state({ id: initUser.id, memberNumber: initUser.member?.number, + isActiveMember: initUser.member?.is_active, username: initUser.username, prefixName: initUser.member?.prefix_name, nickname: initUser.member?.nickname, @@ -58,7 +59,7 @@ address: '', }); - let districtValue = $state(`${user.districtID ?? ''}`); + let districtValue = $state(user.districtID ?? ''); let districts = {}; for(let [area, object] of Object.entries(areaDistricts)) { for(let [key, value] of Object.entries(object)) { @@ -135,11 +136,16 @@ } else if(inputs.birthday.validity.rangeOverflow) { feedbacks.birthday = `The birthday field must be a date before or equal to ${inputs.birthday.max}.`; } - if(inputs.district.value) { + if(user.isActiveMember || inputs.district.value) { + if (inputs.district.validity.valueMissing) { + feedbacks.district = 'The district field is required when user is an active member.'; + } if(inputs.address.validity.valueMissing) { - feedbacks.address = 'The address field is required when district is present.'; + feedbacks.address = user.isActiveMember ? + 'The address field is required when user is an active member.' : + 'The address field is required when district is present.'; } else if(inputs.address.validity.tooLong) { - feedbacks.mobile = `The address must not be greater than ${inputs.address.maxLength} characters.`; + feedbacks.address = `The address must not be greater than ${inputs.address.maxLength} characters.`; } } @@ -157,7 +163,7 @@ user.passportNumber = response.data.passport_number; user.genderID = response.data.gender_id; user.birthday = response.data.birthday; - user.districtID = response.data.district_id; + user.districtID = response.data.district_id ?? ''; user.address = response.data.address; editing = false; resetInputValues(); @@ -451,7 +457,8 @@ - @@ -468,8 +475,9 @@ - diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index 1774acb4..cb0fd428 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -12,6 +12,7 @@ let user = $state({ id: initUser.id, memberNumber: initUser.member?.number, + isActiveMember: initUser.member?.is_active, username: initUser.username, prefixName: initUser.member?.prefix_name, nickname: initUser.member?.nickname, @@ -44,7 +45,7 @@ let newPasswordValue = $state(''); let confirmNewPasswordValue = $state(''); let showPassportNumber = $state(false); - let districtValue = $state(`${user.districtID ?? ''}`); + let districtValue = $state(user.districtID ?? ''); let districts = {}; for(let [area, object] of Object.entries(areaDistricts)) { for(let [key, value] of Object.entries(object)) { @@ -101,11 +102,16 @@ } else if(inputs.birthday.validity.rangeOverflow) { feedbacks.birthday = `The birthday not be greater than ${birthday.max} characters.`; } - if(inputs.district.value) { + if(user.isActiveMember || inputs.district.value) { + if (inputs.district.validity.valueMissing) { + feedbacks.district = 'The district field is required when you are an active member.'; + } if(inputs.address.validity.valueMissing) { - feedbacks.address = 'The address field is required when district is present.'; + feedbacks.address = user.isActiveMember ? + 'The address field is required when you are an active member.' : + 'The address field is required when district is present.'; } else if(inputs.address.validity.tooLong) { - feedbacks.mobile = `The address must not be greater than ${inputs.address.maxLength} characters.`; + feedbacks.address = `The address must not be greater than ${inputs.address.maxLength} characters.`; } } @@ -128,11 +134,11 @@ function successCallback(response) { alert(response.data.success); - genders[response.data.gender_id] = response.data.gender + genders[response.data.gender_id] = response.data.gender; user.username = response.data.username; user.genderID = response.data.gender_id; user.birthday = formatToDate(response.data.birthday); - user.districtID = response.data.district_id; + user.districtID = response.data.district_id ?? ''; user.address = response.data.address; editing = false; resetInputValues(); @@ -366,7 +372,8 @@ - @@ -383,8 +390,9 @@ - diff --git a/tests/Feature/Admin/Users/UpdateTest.php b/tests/Feature/Admin/Users/UpdateTest.php index bf4a0b5e..86d0ae86 100644 --- a/tests/Feature/Admin/Users/UpdateTest.php +++ b/tests/Feature/Admin/Users/UpdateTest.php @@ -6,6 +6,7 @@ use App\Models\District; use App\Models\Gender; use App\Models\ModulePermission; +use App\Models\OtherPaymentGateway; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -17,7 +18,7 @@ class UpdateTest extends TestCase private $user; private $happyCase = [ - 'username' => '87654321', + 'username' => '12345678', 'family_name' => 'LEE', 'given_name' => 'Chi Nan', 'passport_type_id' => 2, @@ -437,6 +438,42 @@ public function test_birthday_too_close() $response->assertInvalid(['birthday' => "The birthday field must be a date before or equal to $beforeTwoYear."]); } + public function test_missing_district_id_when_user_is_not_active_member() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'expired_at' => now(), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The district field is required when you are an active member or have membership order in progress.']); + } + + public function test_missing_district_id_when_user_has_membership_order_in_progress() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'pending', + 'from_year' => now()->year, + 'expired_at' => now()->addMinutes(30), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The district field is required when you are an active member or have membership order in progress.']); + } + public function test_district_id_is_not_integer() { $data = $this->happyCase; @@ -467,6 +504,41 @@ public function test_district_id_is_not_exist() $response->assertInvalid(['district_id' => 'The selected district is invalid.']); } + public function test_missing_address_when_user_is_active_member() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field is required when you are an active member or have membership order in progress.']); + } + + public function test_missing_address_when_user_has_membership_order_in_progress() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'pending', + 'from_year' => now()->year, + 'expired_at' => now()->addMinutes(30), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field is required when you are an active member or have membership order in progress.']); + } + public function test_address_required_when_district_id_present() { $data = $this->happyCase; diff --git a/tests/Feature/User/UpdateTest.php b/tests/Feature/User/UpdateTest.php index ecf2a7e8..6e8686ea 100644 --- a/tests/Feature/User/UpdateTest.php +++ b/tests/Feature/User/UpdateTest.php @@ -4,6 +4,7 @@ use App\Models\Address; use App\Models\District; +use App\Models\OtherPaymentGateway; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Tests\TestCase; @@ -186,6 +187,41 @@ public function test_birthday_too_close() $response->assertInvalid(['birthday' => "The birthday field must be a date before or equal to $beforeTwoYear."]); } + public function test_missing_district_id_when_user_is_active_member() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The district field is required when you are an active member or have membership order in progress.']); + } + + public function test_missing_district_id_when_user_has_membership_order_in_progress() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'pending', + 'from_year' => now()->year, + 'expired_at' => now()->addMinutes(30), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['address'] = '123 Street'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['district_id' => 'The district field is required when you are an active member or have membership order in progress.']); + } + public function test_district_id_is_not_integer() { $data = $this->happyCase; @@ -204,6 +240,41 @@ public function test_district_id_not_exists() $response->assertInvalid(['district_id' => 'The selected district is invalid.']); } + public function test_missing_address_when_user_is_active_member() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field is required when you are an active member or have membership order in progress.']); + } + + public function test_missing_address_when_user_has_membership_order_in_progress() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'pending', + 'from_year' => now()->year, + 'expired_at' => now()->addMinutes(30), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['address' => 'The address field is required when you are an active member or have membership order in progress.']); + } + public function test_address_required_when_district_id_present() { $data = $this->happyCase; From 58346c614faa82bedbc994c79d604890392b1359 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Mon, 9 Feb 2026 21:33:07 +0800 Subject: [PATCH 30/54] move the validation logic form user controller login and update method to form validation class and fix missing incorrect password test --- app/Http/Controllers/UserController.php | 39 ++++-------------------- app/Http/Requests/User/LoginRequest.php | 26 ++++++++++++++++ app/Http/Requests/User/UpdateRequest.php | 5 +-- tests/Feature/User/RegisterTest.php | 2 +- tests/Feature/User/UpdateTest.php | 8 +++++ 5 files changed, 44 insertions(+), 36 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 553e5a77..90b582f7 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -193,11 +193,6 @@ public function show(Request $request) public function update(UpdateRequest $request) { $user = $request->user(); - if ($request->password != '' && ! $user->checkPassword($request->password)) { - return response([ - 'errors' => ['password' => 'The provided password is incorrect.'], - ], 422); - } DB::beginTransaction(); $gender = $user->gender->updateName($request->gender); $update = [ @@ -244,35 +239,13 @@ public function logout() public function login(LoginRequest $request) { - $user = User::with([ - 'loginLogs' => function ($query) { - $query->where('status', false) - ->where('created_at', '>=', now()->subDay()); - }, - ])->firstWhere('username', $request->username); - if ($user) { - if ($user->loginLogs->count() >= 10) { - $firstInRangeLoginFailedTime = $user->loginLogs[0]['created_at']; - - abort(429, "Too many failed login attempts. Please try again later than $firstInRangeLoginFailedTime."); - } - $log = ['user_id' => $user->id]; - if ($user->checkPassword($request->password)) { - $log['status'] = true; - UserLoginLog::create($log); - $user->loginLogs() - ->where('status', false) - ->delete(); - Auth::login($user, $request->remember_me); - - return redirect()->intended(route('profile.show')); - } - UserLoginLog::create($log); - } + $request->user->loginLogs() + ->where('status', false) + ->delete(); + $request->user->loginLogs()->create(['status' => true]); + Auth::login($request->user, $request->remember_me); - return response([ - 'errors' => ['failed' => 'The provided username or password is incorrect.'], - ], 422); + return redirect()->intended(route('profile.show')); } public function forgetPassword() diff --git a/app/Http/Requests/User/LoginRequest.php b/app/Http/Requests/User/LoginRequest.php index 54fb8a49..bfe8ecdc 100644 --- a/app/Http/Requests/User/LoginRequest.php +++ b/app/Http/Requests/User/LoginRequest.php @@ -2,6 +2,8 @@ namespace App\Http\Requests\User; +use App\Models\User; +use Illuminate\Contracts\Validation\Validator; use Illuminate\Foundation\Http\FormRequest; class LoginRequest extends FormRequest @@ -19,4 +21,28 @@ public function rules(): array 'remember_me' => 'sometimes|boolean', ]; } + + public function withValidator(Validator $validator) + { + $validator->after(function ($validator) { + $user = User::with([ + 'loginLogs' => function ($query) { + $query->where('status', false) + ->where('created_at', '>=', now()->subDay()); + }, + ])->firstWhere('username', $this->username); + if (! $user || ! $user->checkPassword($this->password)) { + if($user && ! $user->checkPassword($this->password)) { + $user->loginLogs()->create(); + } + $validator->errors()->add('failed', 'The provided username or password is incorrect.'); + } elseif ($user->loginLogs->count() >= 10) { + $firstInRangeLoginFailedTime = $user->loginLogs[0]['created_at']; + + abort(429, "Too many failed login attempts. Please try again later than $firstInRangeLoginFailedTime."); + } else { + $this->merge(['user' => $user]); + } + }); + } } diff --git a/app/Http/Requests/User/UpdateRequest.php b/app/Http/Requests/User/UpdateRequest.php index 2b043ab0..5898f29b 100644 --- a/app/Http/Requests/User/UpdateRequest.php +++ b/app/Http/Requests/User/UpdateRequest.php @@ -33,8 +33,8 @@ public function rules(): array ->ignore($this->user()), ], 'password' => [ - Rule::requiredIf($this->username != $this->user()->username || $this->new_password), - 'string', 'min:8', 'max:16', + 'bail', Rule::requiredIf($this->username != $this->user()->username || $this->new_password), + 'string', 'min:8', 'max:16', 'current_password:web', ], 'new_password' => 'nullable|string|min:8|max:16|confirmed', 'gender' => 'required|string|max:255', @@ -48,6 +48,7 @@ public function messages(): array { return [ 'password.required' => 'The password field is required when you change the username or password.', + 'password.current_password' => 'The provided password is incorrect.', 'district_id.required' => 'The district field is required when you are an active member or have membership order in progress.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', diff --git a/tests/Feature/User/RegisterTest.php b/tests/Feature/User/RegisterTest.php index 93171a9c..00236b4f 100644 --- a/tests/Feature/User/RegisterTest.php +++ b/tests/Feature/User/RegisterTest.php @@ -367,7 +367,7 @@ public function test_address_too_long() $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; $data['address'] = str_repeat('a', 256); - $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response = $this->post(route('register'), $data); $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); } diff --git a/tests/Feature/User/UpdateTest.php b/tests/Feature/User/UpdateTest.php index 6e8686ea..9ec8070b 100644 --- a/tests/Feature/User/UpdateTest.php +++ b/tests/Feature/User/UpdateTest.php @@ -106,6 +106,14 @@ public function test_password_too_long() $response->assertInvalid(['password' => 'The password field must not be greater than 16 characters.']); } + public function test_password_incorrect() + { + $data = $this->happyCase; + $data['password'] = 'wrong_password'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['password' => 'The provided password is incorrect.']); + } + public function test_new_password_is_not_string() { $data = $this->happyCase; From 6879b3b8aab9376465aa1a10ca786746e772e0a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Mon, 9 Feb 2026 22:31:39 +0800 Subject: [PATCH 31/54] fix removed field "login_ip" missing update UserLoginLog model --- app/Models/UserLoginLog.php | 1 - 1 file changed, 1 deletion(-) diff --git a/app/Models/UserLoginLog.php b/app/Models/UserLoginLog.php index 31c3a599..76cbe1ac 100644 --- a/app/Models/UserLoginLog.php +++ b/app/Models/UserLoginLog.php @@ -11,7 +11,6 @@ class UserLoginLog extends Authenticatable protected $fillable = [ 'user_id', - 'login_ip', 'status', ]; From b2bdf554d297f54a32313f252ed9c12251c2e41e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Tue, 10 Feb 2026 01:00:24 +0800 Subject: [PATCH 32/54] fix missing update usar admin index label missing "for" --- resources/js/Pages/Admin/Users/Index.svelte | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/resources/js/Pages/Admin/Users/Index.svelte b/resources/js/Pages/Admin/Users/Index.svelte index 69829d23..0c7c633e 100644 --- a/resources/js/Pages/Admin/Users/Index.svelte +++ b/resources/js/Pages/Admin/Users/Index.svelte @@ -89,7 +89,7 @@
- + - + - + - + - + - + - + - + - + Show + class="btn btn-primary">Show {/each} @@ -240,4 +240,4 @@ {/if} - \ No newline at end of file + From c96794cb49580cb29c3bccaec0ea8d5091f1354a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Tue, 10 Feb 2026 11:53:19 +0800 Subject: [PATCH 33/54] add member data edit and update function to inside user edit and update, update tests --- app/Http/Controllers/Admin/UserController.php | 20 +- app/Http/Controllers/UserController.php | 10 + .../Requests/Admin/User/UpdateRequest.php | 24 +- app/Http/Requests/User/UpdateRequest.php | 24 +- resources/js/Pages/Admin/Users/Show.svelte | 57 ++++- resources/js/Pages/User/Profile.svelte | 82 +++++-- tests/Feature/Admin/Users/UpdateTest.php | 230 +++++++++++++++++- tests/Feature/User/UpdateTest.php | 177 +++++++++++++- 8 files changed, 570 insertions(+), 54 deletions(-) diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php index aacd600a..b235b80b 100644 --- a/app/Http/Controllers/Admin/UserController.php +++ b/app/Http/Controllers/Admin/UserController.php @@ -191,17 +191,19 @@ public function update(UpdateRequest $request, User $user) ]; if ($user->address) { if ($request->district_id) { - $return['address_id'] = $user->address->updateAddress($request->district_id, $request->address)->id; + $return['address_id'] = $user->address->updateAddress( + $request->district_id, + $request->address + )->id; } else { $user->address->delete(); $return['address_id'] = null; } } elseif ($request->district_id) { - $address = Address::firstOrCreate([ + $return['address_id'] = Address::firstOrCreate([ 'district_id' => $request->district_id, 'value' => $request->address, - ]); - $return['address_id'] = $address->id; + ])->id; } $user->update($return); unset($return['address_id']); @@ -209,6 +211,16 @@ public function update(UpdateRequest $request, User $user) $return['address'] = $request->address; $return['gender'] = $gender->name; $return['success'] = 'The user data update success!'; + if($user->member) { + $user->member->update([ + 'prefix_name' => $request->prefix_name, + 'nickname' => $request->nickname, + 'suffix_name' => $request->suffix_name, + ]); + $return['prefix_name'] = $user->member->prefix_name; + $return['nickname'] = $user->member->nickname; + $return['suffix_name'] = $user->member->suffix_name; + } DB::commit(); return $return; diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 90b582f7..6156c8ce 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -224,6 +224,16 @@ public function update(UpdateRequest $request) $return['district_id'] = $request->district_id; $return['address'] = $request->address; $return['success'] = 'The profile update success!'; + if ($user->member) { + $user->member->update([ + 'prefix_name' => $request->prefix_name, + 'nickname' => $request->nickname, + 'suffix_name' => $request->suffix_name, + ]); + $return['prefix_name'] = $user->member->prefix_name; + $return['nickname'] = $user->member->nickname; + $return['suffix_name'] = $user->member->suffix_name; + } DB::commit(); return $return; diff --git a/app/Http/Requests/Admin/User/UpdateRequest.php b/app/Http/Requests/Admin/User/UpdateRequest.php index 6e579682..cd52ebfb 100644 --- a/app/Http/Requests/Admin/User/UpdateRequest.php +++ b/app/Http/Requests/Admin/User/UpdateRequest.php @@ -20,15 +20,23 @@ public function rules(): array $user = $this->route('user'); $districtUtility = 'nullable'; $addressUtility = 'required_with:district_id'; - if( - $this->user()->member?->isActive || - $this->user()->member?->orders()->where('expired_at', '>', now())->exists() - ) { - $districtUtility = 'required'; - $addressUtility = 'required'; + $return = []; + if ($this->route('user')->member) { + $return = array_merge($return, [ + 'prefix_name' => 'nullable|string|max:255', + 'nickname' => 'nullable|string|max:255', + 'suffix_name' => 'nullable|string|max:255', + ]); + if( + $this->user()->member->isActive || + $this->user()->member->orders()->where('expired_at', '>', now())->exists() + ) { + $districtUtility = 'required'; + $addressUtility = 'required'; + } } - return [ + return array_merge($return, [ 'username' => [ 'required', 'string', 'min:8', 'max:16', Rule::unique(User::class, 'username') @@ -43,7 +51,7 @@ public function rules(): array 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), 'district_id' => $districtUtility.'|integer|exists:'.District::class.',id', 'address' => $addressUtility.'|string|max:255', - ]; + ]); } public function messages(): array diff --git a/app/Http/Requests/User/UpdateRequest.php b/app/Http/Requests/User/UpdateRequest.php index 5898f29b..5d80fa81 100644 --- a/app/Http/Requests/User/UpdateRequest.php +++ b/app/Http/Requests/User/UpdateRequest.php @@ -18,15 +18,23 @@ public function rules(): array { $districtUtility = 'nullable'; $addressUtility = 'required_with:district_id'; - if( - $this->user()->member?->isActive || - $this->user()->member?->orders()->where('expired_at', '>', now())->exists() - ) { - $districtUtility = 'required'; - $addressUtility = 'required'; + $return = []; + if ($this->user()->member) { + $return = array_merge($return, [ + 'prefix_name' => 'nullable|string|max:255', + 'nickname' => 'nullable|string|max:255', + 'suffix_name' => 'nullable|string|max:255', + ]); + if( + $this->user()->member->isActive || + $this->user()->member->orders()->where('expired_at', '>', now())->exists() + ) { + $districtUtility = 'required'; + $addressUtility = 'required'; + } } - return [ + return array_merge($return,[ 'username' => [ 'required', 'string', 'min:8', 'max:16', Rule::unique(User::class, 'username') @@ -41,7 +49,7 @@ public function rules(): array 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), 'district_id' => $districtUtility.'|integer|exists:'.District::class.',id', 'address' => $addressUtility.'|string|max:255', - ]; + ]); } public function messages(): array diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index 4ce3bc32..e8cbc19c 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -48,6 +48,9 @@ let resettingPassword = $state(false); let feedbacks = $state({ username: '', + prefixName: '', + nickname: '', + suffixName: '', familyName: '', middleName: '', givenName: '', @@ -69,6 +72,9 @@ function resetInputValues() { inputs.username.value = user.username; + inputs.prefixName.value = user.prefixName; + inputs.nickname.value = user.nickname; + inputs.suffixName.value = user.suffixName; inputs.familyName.value = user.familyName; inputs.middleName.value = user.middleName; inputs.givenName.value = user.givenName; @@ -103,6 +109,17 @@ } else if(inputs.username.validity.tooLong) { feedbacks.username = `The username field must not be greater than ${inputs.username.maxLength} characters.`; } + if (user.memberNumber) { + if(inputs.prefixName.validity.tooLong) { + feedbacks.prefixName = `The prefix name must not be greater than ${inputs.prefixName.maxLength} characters.`; + } + if(inputs.nickname.validity.tooLong) { + feedbacks.nickname = `The nickname must not be greater than ${inputs.nickname.maxLength} characters.`; + } + if(inputs.suffixName.validity.tooLong) { + feedbacks.suffixName = `The suffix name must not be greater than ${inputs.suffixName.maxLength} characters.`; + } + } if(inputs.familyName.validity.valueMissing) { feedbacks.familyName = 'The family name field is required.'; } else if(inputs.familyName.validity.tooLong) { @@ -156,6 +173,11 @@ alert(response.data.success); genders[response.data.gender_id] = response.data.gender; user.username = response.data.username; + if (user.memberNumber) { + user.prefixName = response.data.prefix_name; + user.nickname = response.data.nickname; + user.suffixName = response.data.suffix_name; + } user.familyName = response.data.family_name; user.middleName = response.data.middle_name; user.givenName = response.data.given_name; @@ -179,6 +201,15 @@ case 'username': feedbacks.username = value;; break; + case 'prefix_name': + feedbacks.prefixName = value; + break; + case 'nickname': + feedbacks.nickname = value; + break; + case 'suffix_name': + feedbacks.suffixName = value; + break; case 'family_name': feedbacks.familyName = value; break; @@ -234,6 +265,11 @@ gender: inputs.gender.value, birthday: inputs.birthday.value, } + if (user.memberNumber) { + data['prefix_name'] = inputs.prefixName.value; + data['nickname'] = inputs.nickname.value; + data['suffix_name'] = inputs.suffixName.value; + } if (inputs.district.value) { data['district_id'] = inputs.district.value; data['address'] = inputs.address.value; @@ -367,15 +403,30 @@ {#if user.memberNumber} -
Prefix Name:
+ +
{user.prefixName ?? "\u00A0"}
-
Nickname:
+ +
{user.nickname ?? "\u00A0"}
-
Suffix Name:
+ +
{user.suffixName ?? "\u00A0"}
{/if} diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index cb0fd428..dd69ae29 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -35,6 +35,9 @@ username: '', password: '', newPassword: '', + prefixName: '', + nickname: '', + suffixName: '', gender: '', birthday: '', district: '', @@ -53,6 +56,20 @@ } } + function resetInputValues() { + inputs.username.value = user.username; + inputs.password.value = ''; + inputs.newPassword.value = ''; + inputs.confirmNewPassword.value = ''; + inputs.gender.value = genders[user.genderID]; + inputs.birthday.value = user.birthday; + inputs.district.value = user.districtID; + inputs.address.value = user.address; + for(let key in feedbacks) { + feedbacks[key] = ''; + } + } + function hasError() { for(let [key, feedback] of Object.entries(feedbacks)) { if(feedback != 'Looks good!') { @@ -92,6 +109,17 @@ feedbacks.newPassword = 'The new password confirmation does not match.'; } } + if (user.memberNumber) { + if(inputs.prefixName.validity.tooLong) { + feedbacks.prefixName = `The prefix name must not be greater than ${inputs.prefixName.maxLength} characters.`; + } + if(inputs.nickname.validity.tooLong) { + feedbacks.nickname = `The nickname must not be greater than ${inputs.nickname.maxLength} characters.`; + } + if(inputs.suffixName.validity.tooLong) { + feedbacks.suffixName = `The suffix name must not be greater than ${inputs.suffixName.maxLength} characters.`; + } + } if(inputs.gender.validity.valueMissing) { feedbacks.gender = 'The gender field is required.'; } else if(inputs.gender.validity.tooLong) { @@ -118,24 +146,15 @@ return !hasError(); } - function resetInputValues() { - inputs.username.value = user.username; - inputs.password.value = ''; - inputs.newPassword.value = ''; - inputs.confirmNewPassword.value = ''; - inputs.gender.value = genders[user.genderID]; - inputs.birthday.value = user.birthday; - inputs.district.value = user.districtID; - inputs.address.value = user.address; - for(let key in feedbacks) { - feedbacks[key] = ''; - } - } - function successCallback(response) { alert(response.data.success); genders[response.data.gender_id] = response.data.gender; user.username = response.data.username; + if (user.memberNumber) { + user.prefixName = response.data.prefix_name; + user.nickname = response.data.nickname; + user.suffixName = response.data.suffix_name; + } user.genderID = response.data.gender_id; user.birthday = formatToDate(response.data.birthday); user.districtID = response.data.district_id ?? ''; @@ -157,6 +176,15 @@ case 'password': feedbacks.password = value; break; + case 'prefix_name': + feedbacks.prefixName = value; + break; + case 'nickname': + feedbacks.nickname = value; + break; + case 'suffix_name': + feedbacks.suffixName = value; + break; case 'new_password': feedbacks.newPassword = value; break; @@ -205,6 +233,11 @@ data['new_password'] = inputs.newPassword.value; data['new_password_confirmation'] = inputs.confirmNewPassword.value; } + if (user.memberNumber) { + data['prefix_name'] = inputs.prefixName.value; + data['nickname'] = inputs.nickname.value; + data['suffix_name'] = inputs.suffixName.value; + } if (inputs.district.value) { data['district_id'] = inputs.district.value; data['address'] = inputs.address.value; @@ -310,15 +343,30 @@ {#if user.memberNumber} -
Prefix Name:
+ +
{user.prefixName ?? "\u00A0"}
-
Nickname:
+ +
{user.nickname ?? "\u00A0"}
-
Suffix Name:
+ +
{user.suffixName ?? "\u00A0"}
{/if} diff --git a/tests/Feature/Admin/Users/UpdateTest.php b/tests/Feature/Admin/Users/UpdateTest.php index 86d0ae86..ae922f78 100644 --- a/tests/Feature/Admin/Users/UpdateTest.php +++ b/tests/Feature/Admin/Users/UpdateTest.php @@ -5,6 +5,7 @@ use App\Models\Address; use App\Models\District; use App\Models\Gender; +use App\Models\Member; use App\Models\ModulePermission; use App\Models\OtherPaymentGateway; use App\Models\User; @@ -157,6 +158,96 @@ public function test_username_is_used() $response->assertInvalid(['username' => 'The username has already been taken.']); } + public function test_prefix_name_is_not_string_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['prefix_name'] = ['Mr.']; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['prefix_name' => 'The prefix name field must be a string.']); + } + + public function test_prefix_name_too_long_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['prefix_name'] = str_repeat('a', 256); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['prefix_name' => 'The prefix name field must not be greater than 255 characters.']); + } + + public function test_nickname_is_not_string_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['nickname'] = ['Diamond']; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['nickname' => 'The nickname field must be a string.']); + } + + public function test_nickname_too_long_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['nickname'] = str_repeat('a', 256); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['nickname' => 'The nickname field must not be greater than 255 characters.']); + } + + public function test_suffix_name_is_not_string_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['suffix_name'] = ['Jr.']; + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['suffix_name' => 'The suffix name field must be a string.']); + } + + public function test_suffix_name_too_long_when_user_is_member() + { + $this->user->member()->create(); + $data = $this->happyCase; + $data['suffix_name'] = str_repeat('a', 256); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertInvalid(['suffix_name' => 'The suffix name field must not be greater than 255 characters.']); + } + public function test_missing_family_name() { $data = $this->happyCase; @@ -568,7 +659,7 @@ public function test_address_too_long() $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); } - public function test_happy_case_without_middle_name_and_address() + public function test_happy_case_without_middle_name_and_address_when_user_is_not_member() { $data = $this->happyCase; $response = $this->actingAs($this->user) @@ -593,7 +684,7 @@ public function test_happy_case_without_middle_name_and_address() $this->assertEquals($data['birthday'], $user->birthday->format('Y-m-d')); } - public function test_happy_case_with_middle_name_and_without_address() + public function test_happy_case_with_middle_name_and_without_address_when_user_is_not_member() { $data = $this->happyCase; $data['middle_name'] = 'intelligent'; @@ -619,7 +710,7 @@ public function test_happy_case_with_middle_name_and_without_address() $this->assertEquals($data['birthday'], $user->birthday->format('Y-m-d')); } - public function test_with_change_address_when_before_user_have_no_address_and_without_middle_name_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_member_and_before_have_no_address_and_without_middle_name() { $data = $this->happyCase; unset($data['middle_name']); @@ -644,7 +735,7 @@ public function test_with_change_address_when_before_user_have_no_address_and_wi ); } - public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_member_and_before_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name() { $data = $this->happyCase; unset($data['middle_name']); @@ -680,7 +771,7 @@ public function test_with_change_address_when_before_user_has_address_and_the_us $this->assertEquals(1,Address::count()); } - public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name_happy_case() + public function test_happy_case_without_address_when_user_is_not_member_and_before_has_address_and_the_user_address_have_no_other_object_using_and_without_middle_name() { $data = $this->happyCase; unset($data['middle_name']); @@ -705,7 +796,7 @@ public function test_without_address_when_before_user_has_address_and_the_user_a $this->assertEquals(0,Address::count()); } - public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_other_object_using_and_without_middle_name_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_member_and_before_has_address_and_the_user_address_have_other_object_using_and_without_middle_name() { $data = $this->happyCase; unset($data['middle_name']); @@ -742,4 +833,131 @@ public function test_with_change_address_when_before_user_has_address_and_the_us $this->assertNotEquals($address->id, $this->user->fresh()->address_id); $this->assertEquals(2,Address::count()); } + + public function test_happy_case_without_change_middle_name_and_address_and_member_extends_data_when_user_is_active_member_and_before_member_data_is_null() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'expired_at' => now()->addYear(), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $data['success'] = 'The user data update success!'; + $data['middle_name'] = null; + $data['prefix_name'] = null; + $data['nickname'] = null; + $data['suffix_name'] = null; + $response->assertJson($data); + $user = User::firstWhere('id', $this->user->id); + $member = Member::firstWhere('user_id', $this->user->id); + $this->assertNull($user->fresh()->middle_name); + $this->assertNull($member->prefix_name); + $this->assertNull($member->nickname); + $this->assertNull($member->suffix_name); + } + + public function test_happy_case_with_change_member_data_and_without_change_middle_name_and_address_when_user_is_active_member_and_before_member_data_is_null() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'expired_at' => now()->addYear(), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['prefix_name'] = 'Mr.'; + $data['nickname'] = 'Diamond'; + $data['suffix_name'] = 'Jr.'; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $data['success'] = 'The user data update success!'; + $response->assertJson($data); + $member = Member::firstWhere('user_id', $this->user->id); + $this->assertEquals($data['prefix_name'], $member->prefix_name); + $this->assertEquals($data['nickname'], $member->nickname); + $this->assertEquals($data['suffix_name'], $member->suffix_name); + } + + public function test_happy_case_without_change_middle_name_and_address_and_member_extends_data_when_user_is_active_member_and_before_member_data_is_not_null() + { + $member = $this->user->member()->create([ + 'prefix_name' => 'Mr.', + 'nickname' => 'Diamond', + 'suffix_name' => 'Jr.', + ]); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'expired_at' => now()->addYear(), + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user) + ->putJson( + route( + 'admin.users.update', + ['user' => $this->user] + ), $data + ); + $response->assertSuccessful(); + $data['success'] = 'The user data update success!'; + $data['middle_name'] = null; + $data['prefix_name'] = null; + $data['nickname'] = null; + $data['suffix_name'] = null; + $response->assertJson($data); + $user = User::firstWhere('id', $this->user->id); + $member = Member::firstWhere('user_id', $this->user->id); + $this->assertNull($user->fresh()->middle_name); + $this->assertNull($member->prefix_name); + $this->assertNull($member->nickname); + $this->assertNull($member->suffix_name); + } } diff --git a/tests/Feature/User/UpdateTest.php b/tests/Feature/User/UpdateTest.php index 9ec8070b..40a57b7e 100644 --- a/tests/Feature/User/UpdateTest.php +++ b/tests/Feature/User/UpdateTest.php @@ -4,6 +4,7 @@ use App\Models\Address; use App\Models\District; +use App\Models\Member; use App\Models\OtherPaymentGateway; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -154,6 +155,60 @@ public function test_confirm_new_password_not_match() $response->assertInvalid(['new_password' => 'The new password field confirmation does not match.']); } + public function test_prefix_name_is_not_string() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['prefix_name'] = ['Mr.']; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['prefix_name' => 'The prefix name field must be a string.']); + } + + public function test_prefix_name_too_long() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['prefix_name'] = str_repeat('a', 256); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['prefix_name' => 'The prefix name field must not be greater than 255 characters.']); + } + + public function test_nickname_is_not_string() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['nickname'] = ['Tester']; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['nickname' => 'The nickname field must be a string.']); + } + + public function test_nickname_too_long() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['nickname'] = str_repeat('a', 256); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['nickname' => 'The nickname field must not be greater than 255 characters.']); + } + + public function test_suffix_name_is_not_string() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['suffix_name'] = ['Jr.']; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['suffix_name' => 'The suffix name field must be a string.']); + } + + public function test_suffix_name_too_long() + { + $member = $this->user->member()->create(); + $data = $this->happyCase; + $data['suffix_name'] = str_repeat('a', 256); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertInvalid(['suffix_name' => 'The suffix name field must not be greater than 255 characters.']); + } + public function test_missing_gender() { $data = $this->happyCase; @@ -309,7 +364,7 @@ public function test_address_too_long() $response->assertInvalid(['address' => 'The address field must not be greater than 255 characters.']); } - public function test_without_change_username_and_new_password_and_address_happy_case() + public function test_happy_case_without_change_username_and_new_password_and_address_when_user_is_not_active_member() { $data = $this->happyCase; $response = $this->actingAs($this->user)->put(route('profile.update'), $data); @@ -320,7 +375,7 @@ public function test_without_change_username_and_new_password_and_address_happy_ $response->assertJson($expect); } - public function test_with_change_username_without_new_password_and_address_happy_case() + public function test_happy_case_with_change_username_without_new_password_and_address_when_user_is_not_active_member() { $data = $this->happyCase; $data['username'] = 'testing2'; @@ -333,7 +388,7 @@ public function test_with_change_username_without_new_password_and_address_happy $response->assertJson($expect); } - public function test_with_new_password_without_change_username_and_address_happy_case() + public function test_happy_case_with_new_password_without_change_username_and_address_when_user_is_not_active_member() { $data = $this->happyCase; $data['password'] = '12345678'; @@ -347,7 +402,7 @@ public function test_with_new_password_without_change_username_and_address_happy $response->assertJson($expect); } - public function test_with_change_address_when_before_user_have_no_address_and_without_change_username_and_new_password_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_active_member_and_before_have_no_address_and_without_change_username_and_new_password() { $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; @@ -365,7 +420,7 @@ public function test_with_change_address_when_before_user_have_no_address_and_wi ); } - public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_active_member_and_before_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password() { $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; @@ -394,7 +449,7 @@ public function test_with_change_address_when_before_user_has_address_and_the_us $this->assertEquals(1,Address::count()); } - public function test_without_address_when_before_user_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password_happy_case() + public function test_happy_case_without_address_when_user_is_not_active_member_and_before_has_address_and_the_user_address_have_no_other_object_using_and_without_change_username_and_new_password() { $data = $this->happyCase; $address = Address::create([ @@ -414,7 +469,7 @@ public function test_without_address_when_before_user_has_address_and_the_user_a $this->assertEquals(0,Address::count()); } - public function test_with_change_address_when_before_user_has_address_and_the_user_address_have_other_object_using_and_without_change_username_and_new_password_happy_case() + public function test_happy_case_with_change_address_when_user_is_not_active_member_and_before_has_address_and_the_user_address_have_other_object_using_and_without_change_username_and_new_password() { $data = $this->happyCase; $data['district_id'] = District::inRandomOrder()->first()->id; @@ -445,7 +500,7 @@ public function test_with_change_address_when_before_user_has_address_and_the_us $this->assertEquals(2,Address::count()); } - public function test_with_change_username_and_new_password_and_without_address_happy_case() + public function test_happy_case_with_change_username_and_new_password_and_without_address_when_user_is_not_active_member() { $data = $this->happyCase; $data['username'] = 'testing2'; @@ -459,4 +514,110 @@ public function test_with_change_username_and_new_password_and_without_address_h $expect['success'] = 'The profile update success!'; $response->assertJson($expect); } + + public function test_happy_case_without_change_username_and_new_password_and_address_and_member_data_when_user_is_active_member_and_before_member_data_is_null() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $expect['prefix_name'] = null; + $expect['nickname'] = null; + $expect['suffix_name'] = null; + $response->assertJson($expect); + $member = Member::find($member->user_id); + $this->assertNull($member->prefix_name); + $this->assertNull($member->nickname); + $this->assertNull($member->suffix_name); + } + + public function test_happy_case_without_change_username_and_new_password_and_address_and_with_member_data_when_user_is_active_member_and_before_member_data_is_null() + { + $member = $this->user->member()->create(); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $data['prefix_name'] = 'Mr.'; + $data['nickname'] = 'Tester'; + $data['suffix_name'] = 'Jr.'; + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $response->assertJson($expect); + $member = Member::find($member->user_id); + $this->assertEquals($data['prefix_name'], $member->prefix_name); + $this->assertEquals($data['nickname'], $member->nickname); + $this->assertEquals($data['suffix_name'], $member->suffix_name); + } + + public function test_happy_case_without_change_username_and_new_password_and_address_and_member_data_when_user_is_active_member_and_before_member_data_is_not_null() + { + $member = $this->user->member()->create([ + 'prefix_name' => 'Mr.', + 'nickname' => 'Tester', + 'suffix_name' => 'Jr.', + ]); + $member->orders()->create([ + 'user_id' => $this->user->id, + 'price' => 200, + 'status' => 'succeeded', + 'from_year' => now()->year, + 'gateway_type' => OtherPaymentGateway::class, + 'gateway_id' => OtherPaymentGateway::inRandomOrder()->first()->id, + ]); + $data = $this->happyCase; + $data['district_id'] = District::inRandomOrder()->first()->id; + $data['address'] = '123 Street'; + $address = Address::create([ + 'district_id' => $data['district_id'], + 'value' => $data['address'], + ]); + $this->user->update(['address_id' => $address->id]); + $response = $this->actingAs($this->user)->put(route('profile.update'), $data); + $response->assertSuccessful(); + $unsetKeys = ['password', 'new_password', 'new_password_confirmation']; + $expect = array_diff_key($data, array_flip($unsetKeys)); + $expect['success'] = 'The profile update success!'; + $expect['prefix_name'] = null; + $expect['nickname'] = null; + $expect['suffix_name'] = null; + $response->assertJson($expect); + $member = Member::find($member->user_id); + $this->assertNull($member->prefix_name); + $this->assertNull($member->nickname); + $this->assertNull($member->suffix_name); + } } From 75145f3c72f369fdf6c8499fccbf09e87b48da6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Tue, 10 Feb 2026 22:28:42 +0800 Subject: [PATCH 34/54] fix missing hidden show member data div when editing --- resources/js/Pages/Admin/Users/Show.svelte | 26 +++++++++++----------- resources/js/Pages/User/Profile.svelte | 6 ++--- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index e8cbc19c..d28bd798 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -341,7 +341,7 @@
- +

Info {#if @@ -374,7 +374,7 @@ valid={feedbacks.username == 'Looks good!'} invalid={feedbacks.username != '' && feedbacks.username != 'Looks good!' } feedback={feedbacks.username} bind:inner={inputs.username} /> - + @@ -409,7 +409,7 @@ valid={feedbacks.prefixName == 'Looks good!'} invalid={feedbacks.prefixName != '' && feedbacks.prefixName != 'Looks good!' } feedback={feedbacks.prefixName} bind:inner={inputs.prefixName} /> -
{user.prefixName ?? "\u00A0"}
+ @@ -418,7 +418,7 @@ valid={feedbacks.nickname == 'Looks good!'} invalid={feedbacks.nickname != '' && feedbacks.nickname != 'Looks good!' } feedback={feedbacks.nickname} bind:inner={inputs.nickname} /> -
{user.nickname ?? "\u00A0"}
+ @@ -427,7 +427,7 @@ valid={feedbacks.suffixName == 'Looks good!'} invalid={feedbacks.suffixName != '' && feedbacks.suffixName != 'Looks good!' } feedback={feedbacks.suffixName} bind:inner={inputs.suffixName} /> -
{user.suffixName ?? "\u00A0"}
+ {/if} @@ -438,7 +438,7 @@ valid={feedbacks.familyName == 'Looks good!'} invalid={feedbacks.familyName != '' && feedbacks.familyName != 'Looks good!' } feedback={feedbacks.familyName} bind:inner={inputs.familyName} /> - + @@ -448,7 +448,7 @@ valid={feedbacks.middleName == 'Looks good!'} invalid={feedbacks.middleName != '' && feedbacks.middleName != 'Looks good!' } feedback={feedbacks.middleName} bind:inner={inputs.middleName} /> - + @@ -458,7 +458,7 @@ valid={feedbacks.givenName == 'Looks good!'} invalid={feedbacks.givenName != '' && feedbacks.givenName != 'Looks good!' } feedback={feedbacks.givenName} bind:inner={inputs.givenName} /> - + @@ -468,10 +468,10 @@ invalid={feedbacks.passportType != '' && feedbacks.passportType != 'Looks good!' } feedback={feedbacks.passportType} bind:inner={inputs.passportType}> {#each Object.entries(passportTypes) as [key, value]} - + {/each} - + @@ -481,7 +481,7 @@ valid={feedbacks.passportNumber == 'Looks good!'} invalid={feedbacks.passportNumber != '' && feedbacks.passportNumber != 'Looks good!' } feedback={feedbacks.passportNumber} bind:inner={inputs.passportNumber} /> - + @@ -492,7 +492,7 @@ valid={feedbacks.gender == 'Looks good!'} invalid={feedbacks.gender != '' && feedbacks.gender != 'Looks good!' } feedback={feedbacks.gender} bind:inner={inputs.gender} /> - + @@ -503,7 +503,7 @@ valid={feedbacks.birthday == 'Looks good!'} invalid={feedbacks.birthday != '' && feedbacks.birthday != 'Looks good!' } feedback={feedbacks.birthday} bind:inner={inputs.birthday} /> - + diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index dd69ae29..ed33879a 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -349,7 +349,7 @@ feedback={feedbacks.prefixName} valid={feedbacks.prefixName == 'Looks good!'} invalid={feedbacks.prefixName != '' && feedbacks.prefixName != 'Looks good!'} bind:inner={inputs.prefixName} /> -
{user.prefixName ?? "\u00A0"}
+ @@ -358,7 +358,7 @@ feedback={feedbacks.nickname} valid={feedbacks.nickname == 'Looks good!'} invalid={feedbacks.nickname != '' && feedbacks.nickname != 'Looks good!'} bind:inner={inputs.nickname} /> -
{user.nickname ?? "\u00A0"}
+ @@ -367,7 +367,7 @@ feedback={feedbacks.suffixName} valid={feedbacks.suffixName == 'Looks good!'} invalid={feedbacks.suffixName != '' && feedbacks.suffixName != 'Looks good!'} bind:inner={inputs.suffixName} /> -
{user.suffixName ?? "\u00A0"}
+ {/if} From e3b6b8cf1fbcaaf2228f9c843a452c703fcf3593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Tue, 10 Feb 2026 22:58:29 +0800 Subject: [PATCH 35/54] fix passport format fail message missing format info --- app/Http/Requests/Admin/User/UpdateRequest.php | 1 + app/Http/Requests/User/RegisterRequest.php | 1 + resources/js/Pages/Admin/Users/Show.svelte | 6 ++++-- resources/js/Pages/User/Register.svelte | 4 +++- tests/Feature/Admin/Users/UpdateTest.php | 2 +- tests/Feature/User/RegisterTest.php | 2 +- 6 files changed, 11 insertions(+), 5 deletions(-) diff --git a/app/Http/Requests/Admin/User/UpdateRequest.php b/app/Http/Requests/Admin/User/UpdateRequest.php index cd52ebfb..102fce16 100644 --- a/app/Http/Requests/Admin/User/UpdateRequest.php +++ b/app/Http/Requests/Admin/User/UpdateRequest.php @@ -59,6 +59,7 @@ public function messages(): array return [ 'passport_type_id.required' => 'The passport type field is required.', 'passport_type_id.exists' => 'The selected passport type is invalid.', + 'passport_number.regex' => 'The passport number field format is invalid. It should only contain uppercase letters and numbers.', 'district_id.required' => 'The district field is required when user is an active member or has membership order in progress.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', diff --git a/app/Http/Requests/User/RegisterRequest.php b/app/Http/Requests/User/RegisterRequest.php index 18892d96..a172eec0 100644 --- a/app/Http/Requests/User/RegisterRequest.php +++ b/app/Http/Requests/User/RegisterRequest.php @@ -38,6 +38,7 @@ public function messages(): array return [ 'passport_type_id.required' => 'The passport type field is required.', 'passport_type_id.exists' => 'The selected passport type is invalid.', + 'passport_number.regex' => 'The passport number field format is invalid. It should only contain uppercase letters and numbers.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', 'address.required_with' => 'The address field is required when district is present.', diff --git a/resources/js/Pages/Admin/Users/Show.svelte b/resources/js/Pages/Admin/Users/Show.svelte index d28bd798..9f91dc4c 100644 --- a/resources/js/Pages/Admin/Users/Show.svelte +++ b/resources/js/Pages/Admin/Users/Show.svelte @@ -142,6 +142,8 @@ feedbacks.passportNumber = `The passport number must be at least ${inputs.passportNumber.minLength} characters.`; } else if(inputs.passportNumber.validity.tooLong) { feedbacks.passportNumber = `The passport number not be greater than ${inputs.passportNumber.maxLength} characters.`; + } else if(inputs.passportNumber.validity.patternMismatch) { + feedbacks.passportNumber = 'The passport number format is invalid. It should only contain uppercase letters and numbers.'; } if(inputs.gender.validity.valueMissing) { feedbacks.gender = 'The gender field is required.'; @@ -475,8 +477,8 @@ - diff --git a/tests/Feature/Admin/Users/UpdateTest.php b/tests/Feature/Admin/Users/UpdateTest.php index ae922f78..0eb8d942 100644 --- a/tests/Feature/Admin/Users/UpdateTest.php +++ b/tests/Feature/Admin/Users/UpdateTest.php @@ -427,7 +427,7 @@ public function test_passport_number_format_not_match() ['user' => $this->user] ), $data ); - $response->assertInvalid(['passport_number' => 'The passport number field format is invalid.']); + $response->assertInvalid(['passport_number' => 'The passport number field format is invalid. It should only contain uppercase letters and numbers.']); } public function test_passport_number_too_short() diff --git a/tests/Feature/User/RegisterTest.php b/tests/Feature/User/RegisterTest.php index 00236b4f..058e3c36 100644 --- a/tests/Feature/User/RegisterTest.php +++ b/tests/Feature/User/RegisterTest.php @@ -235,7 +235,7 @@ public function test_passport_number_format_not_match() $data = $this->happyCase; $data['passport_number'] = '1234567$'; $response = $this->post(route('register'), $data); - $response->assertInvalid(['passport_number' => 'The passport number field format is invalid.']); + $response->assertInvalid(['passport_number' => 'The passport number field format is invalid. It should only contain uppercase letters and numbers.']); } public function test_passport_number_too_short() From d6076b08381df4471ee02c98d2f7eeae50689918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 14:32:27 +0800 Subject: [PATCH 36/54] rename prior_evidence_resulis table is_pass column name to is_accepted --- app/Models/PriorEvidenceResult.php | 6 +++++- app/Models/User.php | 8 ++++---- ...6_02_02_124628_create_prior_evidence_results_table.php | 2 +- tests/Feature/Models/User/MembershipQualificationTest.php | 4 ++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/app/Models/PriorEvidenceResult.php b/app/Models/PriorEvidenceResult.php index e528f81b..97f030f4 100644 --- a/app/Models/PriorEvidenceResult.php +++ b/app/Models/PriorEvidenceResult.php @@ -19,7 +19,11 @@ class PriorEvidenceResult extends Model 'taken_on', 'score', 'percent_of_group', - 'is_pass', + 'is_accepted', + ]; + + protected $casts = [ + 'is_accepted' => 'boolean', ]; public function order() diff --git a/app/Models/User.php b/app/Models/User.php index d8412854..c51a8a9f 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -399,10 +399,10 @@ public function priorEvidenceOrders() return $this->hasMany(PriorEvidenceOrder::class); } - public function passedPriorEvidence() + public function acceptedPriorEvidence() { return $this->hasOneThrough(PriorEvidenceResult::class, PriorEvidenceOrder::class, 'user_id', 'order_id', 'id', 'id') - ->where('is_pass', true); + ->where('is_accepted', true); } public function hasQualificationOfMembership(): Attribute @@ -412,7 +412,7 @@ public function hasQualificationOfMembership(): Attribute return Attribute::make( get: function (mixed $value, array $attributes) use ($user) { return $user->member || $user->passedAdmissionTest || - $user->passedPriorEvidence || $user->memberTransfers() + $user->acceptedPriorEvidence || $user->memberTransfers() ->where('is_accepted', true) ->exists(); } @@ -429,7 +429,7 @@ public function hasSamePassportAlreadyQualificationOfMembership(): Attribute function ($query) { $query->has('member') ->orHas('passedAdmissionTest') - ->orHas('passedPriorEvidence') + ->orHas('acceptedPriorEvidence') ->orWhereHas( 'memberTransfers', function ($query) { $query->where('is_accepted', true); diff --git a/database/migrations/2026_02_02_124628_create_prior_evidence_results_table.php b/database/migrations/2026_02_02_124628_create_prior_evidence_results_table.php index 9b1fb43e..2df118e6 100644 --- a/database/migrations/2026_02_02_124628_create_prior_evidence_results_table.php +++ b/database/migrations/2026_02_02_124628_create_prior_evidence_results_table.php @@ -17,7 +17,7 @@ public function up(): void $table->date('taken_on'); $table->string('score'); $table->decimal('percent_of_group', 4, 2)->nullable(); - $table->boolean('is_pass')->nullable(); + $table->boolean('is_accepted')->nullable(); $table->timestamps(); }); } diff --git a/tests/Feature/Models/User/MembershipQualificationTest.php b/tests/Feature/Models/User/MembershipQualificationTest.php index 77062e4b..8bac281d 100644 --- a/tests/Feature/Models/User/MembershipQualificationTest.php +++ b/tests/Feature/Models/User/MembershipQualificationTest.php @@ -123,7 +123,7 @@ public function test_user_only_has_failed_prior_evidence_result() 'taken_on' => fake()->date(), 'score' => 100, 'percent_of_group' => 50, - 'is_pass' => false, + 'is_accepted' => false, ]); $this->assertFalse($this->user->hasQualificationOfMembership); } @@ -142,7 +142,7 @@ public function test_user_only_has_passed_prior_evidence_result() 'taken_on' => fake()->date(), 'score' => 131, 'percent_of_group' => 2, - 'is_pass' => true, + 'is_accepted' => true, ]); $this->assertTrue($this->user->hasQualificationOfMembership); } From 8bd407e43255bcb2f446622c12322cc6e229ed86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 14:45:16 +0800 Subject: [PATCH 37/54] change qualifying_test_details.score to nullable --- .../2026_02_03_135315_create_qualifying_test_details_table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2026_02_03_135315_create_qualifying_test_details_table.php b/database/migrations/2026_02_03_135315_create_qualifying_test_details_table.php index a6e1620e..e9b3e421 100644 --- a/database/migrations/2026_02_03_135315_create_qualifying_test_details_table.php +++ b/database/migrations/2026_02_03_135315_create_qualifying_test_details_table.php @@ -16,7 +16,7 @@ public function up(): void $table->unsignedBigInteger('test_id'); $table->date('taken_from')->nullable(); $table->date('taken_to')->nullable(); - $table->string('score'); + $table->string('score')->nullable(); $table->timestamps(); }); } From ca9ece77750a55d98657cc9ff56ebad5e83ff25f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 14:46:27 +0800 Subject: [PATCH 38/54] add is_refunded column to prior_evidence_orders table and update model --- app/Models/PriorEvidenceOrder.php | 1 + .../2026_02_02_122523_create_prior_evidence_orders_table.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/Models/PriorEvidenceOrder.php b/app/Models/PriorEvidenceOrder.php index c2d39fc8..87990525 100644 --- a/app/Models/PriorEvidenceOrder.php +++ b/app/Models/PriorEvidenceOrder.php @@ -20,6 +20,7 @@ class PriorEvidenceOrder extends Model 'gateway_id', 'reference_number', 'gateway_payment_fee', + 'is_refunded', ]; public function user() diff --git a/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php b/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php index 30b98bb2..ddf8b9af 100644 --- a/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php +++ b/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php @@ -26,6 +26,7 @@ public function up(): void $table->unsignedBigInteger('gateway_id'); $table->string('reference_number')->nullable(); $table->decimal('gateway_payment_fee', $priceDigits, $priceDecimal)->unsigned()->nullable(); + $table->boolean('is_refunded')->default(false); $table->timestamps(); }); } From 9900ea096a37f9e0b1a014da2b90a9df2e40a319 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 14:54:21 +0800 Subject: [PATCH 39/54] add refunded_quota column to admission_test_orders table, update model and update user model hasUnusedQuotaAdmissionTestOrder relatiom method logic --- app/Models/AdmissionTestOrder.php | 1 + app/Models/User.php | 2 +- .../2025_09_05_015134_create_admission_test_orders_table.php | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Models/AdmissionTestOrder.php b/app/Models/AdmissionTestOrder.php index 5cba4d15..b92d4a50 100644 --- a/app/Models/AdmissionTestOrder.php +++ b/app/Models/AdmissionTestOrder.php @@ -23,6 +23,7 @@ class AdmissionTestOrder extends Model 'gateway_id', 'reference_number', 'gateway_payment_fee', + 'returned_quota', ]; protected $casts = [ diff --git a/app/Models/User.php b/app/Models/User.php index c51a8a9f..5afb701c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -364,7 +364,7 @@ public function hasUnusedQuotaAdmissionTestOrder() ->where('status', 'succeeded') ->whereHas( 'attendedTests', null, '<', - DB::raw("$orderTable.quota") + DB::raw("$orderTable.quota - $orderTable.returned_quota") ); $quotaValidityMonths = config('app.admissionTestQuotaValidityMonths'); if ($quotaValidityMonths) { diff --git a/database/migrations/2025_09_05_015134_create_admission_test_orders_table.php b/database/migrations/2025_09_05_015134_create_admission_test_orders_table.php index 6c18135d..5122fe6c 100644 --- a/database/migrations/2025_09_05_015134_create_admission_test_orders_table.php +++ b/database/migrations/2025_09_05_015134_create_admission_test_orders_table.php @@ -29,6 +29,7 @@ public function up(): void $table->unsignedBigInteger('gateway_id'); $table->string('reference_number')->nullable(); $table->decimal('gateway_payment_fee', $priceDigits, $priceDecimal)->unsigned()->nullable(); + $table->unsignedTinyInteger('returned_quota')->default(0); $table->timestamps(); }); } From 8b540d5c4cf571e67364676711103d8b79d623f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 15:31:54 +0800 Subject: [PATCH 40/54] fix prior_evidence_orders.is_returned wrony type to is_refunded --- app/Models/PriorEvidenceOrder.php | 2 +- .../2026_02_02_122523_create_prior_evidence_orders_table.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Models/PriorEvidenceOrder.php b/app/Models/PriorEvidenceOrder.php index 87990525..8aa7337c 100644 --- a/app/Models/PriorEvidenceOrder.php +++ b/app/Models/PriorEvidenceOrder.php @@ -20,7 +20,7 @@ class PriorEvidenceOrder extends Model 'gateway_id', 'reference_number', 'gateway_payment_fee', - 'is_refunded', + 'is_returned', ]; public function user() diff --git a/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php b/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php index ddf8b9af..686dc79a 100644 --- a/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php +++ b/database/migrations/2026_02_02_122523_create_prior_evidence_orders_table.php @@ -26,7 +26,7 @@ public function up(): void $table->unsignedBigInteger('gateway_id'); $table->string('reference_number')->nullable(); $table->decimal('gateway_payment_fee', $priceDigits, $priceDecimal)->unsigned()->nullable(); - $table->boolean('is_refunded')->default(false); + $table->boolean('is_returned')->default(false); $table->timestamps(); }); } From 105dff3cae477f81bde9c9e802d8a0672ea30ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 19:39:15 +0800 Subject: [PATCH 41/54] add canEditPassportInformation method to user model and add tests --- app/Models/User.php | 33 +++ .../User/CanEditPassportInformationTest.php | 240 ++++++++++++++++++ 2 files changed, 273 insertions(+) create mode 100644 tests/Feature/Models/User/CanEditPassportInformationTest.php diff --git a/app/Models/User.php b/app/Models/User.php index 5afb701c..7acc9216 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -445,4 +445,37 @@ public function address() { return $this->belongsTo(Address::class); } + + public function canEditPassportInformation(): Attribute + { + $user = $this; + + return Attribute::make( + get: function (mixed $value, array $attributes) use ($user) { + return ! $user->lastAttendedAdmissionTest && // proctor will update on admission test + ! ( // avoid user update overwrite proctor updated data + $user->futureAdmissionTest && + $user->futureAdmissionTest->testing_at <= now()->addHours(2) + ) && + ! ( + $user->lastAdmissionTest && + $user->lastAdmissionTest->expect_end_at > now()->subHour() + ) && + ! $user->memberTransfers() + ->where('is_accepted', true) + ->orWhereNull('is_accepted') + ->exists() && + ! $user->priorEvidenceOrders() + ->where('is_returned', false) + ->whereIn('status', ['succeeded', 'partial funded', 'full refunded']) + ->whereDoesntHave( + 'result', function ($query) { + $query->whereNot('is_accepted', true) + ->whereNotNull('is_accepted'); + } + )->exists() && + ! $user->member; + } + ); + } } diff --git a/tests/Feature/Models/User/CanEditPassportInformationTest.php b/tests/Feature/Models/User/CanEditPassportInformationTest.php new file mode 100644 index 00000000..d4ffac00 --- /dev/null +++ b/tests/Feature/Models/User/CanEditPassportInformationTest.php @@ -0,0 +1,240 @@ +user = User::factory()->create(); + } + + public function test_user_can_edit_passport_information_when_user_have_no_any_admission_test_and_prior_evidence_order_and_member_transfer() + { + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_has_future_admission_test_before_more_than_2_hour() + { + $this->user->admissionTests()->create([ + 'testing_at' => now()->addHours(3)->addSecond(), + 'expect_end_at' => now()->addHours(4)->addSecond(), + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_has_future_admission_test_before_less_than_2_hour() + { + $this->user->admissionTests()->create([ + 'testing_at' => now()->addHours(1), + 'expect_end_at' => now()->addHours(2), + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_only_has_absent_admission_test_after_less_than_expected_end_time_1_hour() + { + $this->user->admissionTests()->create([ + 'testing_at' => now()->subHours(2)->subMinutes(30), + 'expect_end_at' => now()->subMinutes(30), + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_absent_admission_test_after_than_expected_end_time_1_hour() + { + $this->user->admissionTests()->create([ + 'testing_at' => now()->subHours(3), + 'expect_end_at' => now()->subHour(), + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_in_progress_prior_evidence_order() + { + $this->user->priorEvidenceOrders()->create([ + 'status' => 'pending', + 'expired_at' => now()->addHour(), + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_expired_prior_evidence_order() + { + $this->user->priorEvidenceOrders()->create([ + 'status' => fake()->randomElement(['pending', 'canceled', 'failed']), + 'expired_at' => now()->subSecond(), + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_has_prior_evidence_order_with_succeeded_status_and_no_result_and_does_not_refund_and_return() + { + $this->user->priorEvidenceOrders()->create([ + 'status' => 'succeeded', + 'expired_at' => now()->addHour(), + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_has_prior_evidence_order_with_succeeded_status_and_no_result_and_the_order_returned() + { + $this->user->priorEvidenceOrders()->create([ + 'status' => 'full_refunded', + 'expired_at' => now()->subSecond(), + 'is_returned' => true, + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_has_prior_evidence_order_with_succeeded_status_and_has_result_but_in_progress_and_does_not_refund_and_return() + { + $order = $this->user->priorEvidenceOrders()->create([ + 'status' => 'succeeded', + 'expired_at' => now()->subSecond(), + ]); + + $order->result()->create([ + 'test_id' => QualifyingTest::inRandomOrder()->first()->id ?? QualifyingTest::create(['name' => fake()->word()])->id, + 'test_on' => fake()->date(), + 'score' => fake()->numberBetween(0, 180), + 'percent_of_group' => fake()->randomFloat(2, 0, 99.99), + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_has_prior_evidence_order_with_succeeded_status_and_has_result_but_in_progress_and_the_order_returned() + { + $order = $this->user->priorEvidenceOrders()->create([ + 'status' => 'full_refunded', + 'expired_at' => now()->subSecond(), + 'is_returned' => true, + ]); + + $order->result()->create([ + 'test_id' => QualifyingTest::inRandomOrder()->first()->id ?? QualifyingTest::create(['name' => fake()->word()])->id, + 'test_on' => fake()->date(), + 'score' => fake()->numberBetween(0, 180), + 'percent_of_group' => fake()->randomFloat(2, 0, 99.99), + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_rejected_prior_evidence() + { + $order = $this->user->priorEvidenceOrders()->create([ + 'status' => 'succeeded', + 'expired_at' => now()->subSecond(), + ]); + + $order->result()->create([ + 'test_id' => QualifyingTest::inRandomOrder()->first()->id ?? QualifyingTest::create(['name' => fake()->word()])->id, + 'test_on' => fake()->date(), + 'score' => fake()->numberBetween(0, 180), + 'percent_of_group' => fake()->randomFloat(2, 0, 99.99), + 'is_accepted' => false, + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_only_has_accepted_prior_evidence_and_have_no_return_the_order() + { + $order = $this->user->priorEvidenceOrders()->create([ + 'status' => 'succeeded', + 'expired_at' => now()->subSecond(), + ]); + + $order->result()->create([ + 'test_id' => QualifyingTest::inRandomOrder()->first()->id ?? QualifyingTest::create(['name' => fake()->word()])->id, + 'test_on' => fake()->date(), + 'score' => fake()->numberBetween(0, 180), + 'percent_of_group' => fake()->randomFloat(2, 0, 99.99), + 'is_accepted' => true, + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_accepted_prior_evidence_and_the_order_is_returned() + { + $order = $this->user->priorEvidenceOrders()->create([ + 'status' => 'full_refunded', + 'expired_at' => now()->subSecond(), + 'is_returned' => true, + ]); + + $order->result()->create([ + 'test_id' => QualifyingTest::inRandomOrder()->first()->id ?? QualifyingTest::create(['name' => fake()->word()])->id, + 'test_on' => fake()->date(), + 'score' => fake()->numberBetween(0, 180), + 'percent_of_group' => fake()->randomFloat(2, 0, 99.99), + 'is_accepted' => true, + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_has_member_transfer_in_progress() + { + $this->user->memberTransfers()->create([ + 'type' => fake()->randomElement(['in', 'guest', 'out']), + 'national_mensa_id' => NationalMensa::inRandomOrder()->first()->id, + 'membership_number' => fake()->numberBetween(1, 1000000), + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_can_edit_passport_information_when_user_only_has_rejected_member_transfer() + { + $this->user->memberTransfers()->create([ + 'type' => fake()->randomElement(['in', 'guest', 'out']), + 'national_mensa_id' => NationalMensa::inRandomOrder()->first()->id, + 'membership_number' => fake()->numberBetween(1, 1000000), + 'is_accepted' => false, + ]); + + $this->assertTrue($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_only_has_accepted_member_transfer() + { + $this->user->memberTransfers()->create([ + 'type' => fake()->randomElement(['in', 'guest', 'out']), + 'national_mensa_id' => NationalMensa::inRandomOrder()->first()->id, + 'membership_number' => fake()->numberBetween(1, 1000000), + 'is_accepted' => true, + ]); + + $this->assertFalse($this->user->canEditPassportInformation); + } + + public function test_user_cannot_edit_passport_information_when_user_is_member() + { + $this->user->member()->create(); + + $this->assertFalse($this->user->canEditPassportInformation); + } +} From d4e371c0ec7b8e7be5b2d476cf5ebbd8866094c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=83=E6=A2=93=E6=A6=86?= Date: Wed, 11 Feb 2026 19:43:44 +0800 Subject: [PATCH 42/54] change user can update passport information on profile if user match requestment --- app/Http/Controllers/UserController.php | 22 +- app/Http/Requests/User/UpdateRequest.php | 23 +- resources/js/Pages/User/Profile.svelte | 198 +++++++++-- tests/Feature/User/UpdateTest.php | 406 +++++++++++++++++------ 4 files changed, 499 insertions(+), 150 deletions(-) diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index 6156c8ce..7f0428b5 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -140,6 +140,7 @@ public function show(Request $request) 'roles', 'permissions', 'synced_to_stripe', 'created_at', 'updated_at', 'address_id', ]); + $user->append('can_edit_passport_information'); $user->member?->makeHidden(['user_id', 'created_at', 'updated_at']); $user->member?->append('is_active'); $user->emails->append('is_verified'); @@ -194,12 +195,17 @@ public function update(UpdateRequest $request) { $user = $request->user(); DB::beginTransaction(); - $gender = $user->gender->updateName($request->gender); - $update = [ - 'username' => $request->username, - 'gender_id' => $gender->id, - 'birthday' => $request->birthday, - ]; + $update = ['username' => $request->username]; + if($user->canEditPassportInformation) { + $gender = $user->gender->updateName($request->gender); + $update['family_name'] = $request->family_name; + $update['middle_name'] = $request->middle_name; + $update['given_name'] = $request->given_name; + $update['passport_type_id'] = $request->passport_type_id; + $update['passport_number'] = $request->passport_number; + $update['gender_id'] = $gender->id; + $update['birthday'] = $request->birthday; + } if ($request->new_password) { $update['password'] = $request->new_password; } @@ -220,7 +226,9 @@ public function update(UpdateRequest $request) $user->update($update); $unsetKeys = ['password', 'new_password', 'new_password_confirmation', 'address_id']; $return = array_diff_key($update, array_flip($unsetKeys)); - $return['gender'] = $request->gender; + if($user->canEditPassportInformation) { + $return['gender'] = $request->gender; + } $return['district_id'] = $request->district_id; $return['address'] = $request->address; $return['success'] = 'The profile update success!'; diff --git a/app/Http/Requests/User/UpdateRequest.php b/app/Http/Requests/User/UpdateRequest.php index 5d80fa81..982b41f1 100644 --- a/app/Http/Requests/User/UpdateRequest.php +++ b/app/Http/Requests/User/UpdateRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests\User; use App\Models\District; +use App\Models\PassportType; use App\Models\User; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -19,7 +20,24 @@ public function rules(): array $districtUtility = 'nullable'; $addressUtility = 'required_with:district_id'; $return = []; - if ($this->user()->member) { + if($this->user()->canEditPassportInformation) { + $return = array_merge($return, [ + 'family_name' => 'required|string|max:255', + 'given_name' => 'required|string|max:255', + 'middle_name' => 'nullable|string|max:255', + 'passport_type_id' => 'required|integer|exists:'.PassportType::class.',id', + 'passport_number' => 'required|regex:/^[A-Z0-9]+$/|min:8|max:18', + 'gender' => 'required|string|max:255', + 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), + ]); + } elseif( + $this->hasAny([ + 'family_name', 'given_name', 'middle_name', + 'passport_type_id', 'passport_number', 'gender', 'birthday' + ]) + ) { + abort(409, 'You cannot update passport information, please read the instructions on the profile page.'); + } elseif ($this->user()->member) { $return = array_merge($return, [ 'prefix_name' => 'nullable|string|max:255', 'nickname' => 'nullable|string|max:255', @@ -45,8 +63,6 @@ public function rules(): array 'string', 'min:8', 'max:16', 'current_password:web', ], 'new_password' => 'nullable|string|min:8|max:16|confirmed', - 'gender' => 'required|string|max:255', - 'birthday' => 'required|date|before_or_equal:'.now()->subYears(2)->format('Y-m-d'), 'district_id' => $districtUtility.'|integer|exists:'.District::class.',id', 'address' => $addressUtility.'|string|max:255', ]); @@ -57,6 +73,7 @@ public function messages(): array return [ 'password.required' => 'The password field is required when you change the username or password.', 'password.current_password' => 'The provided password is incorrect.', + 'passport_number.regex' => 'The passport number field format is invalid. It should only contain uppercase letters and numbers.', 'district_id.required' => 'The district field is required when you are an active member or have membership order in progress.', 'district_id.integer' => 'The district field must be an integer.', 'district_id.exists' => 'The selected district is invalid.', diff --git a/resources/js/Pages/User/Profile.svelte b/resources/js/Pages/User/Profile.svelte index ed33879a..d7b90f6a 100644 --- a/resources/js/Pages/User/Profile.svelte +++ b/resources/js/Pages/User/Profile.svelte @@ -11,6 +11,7 @@ let { user: initUser, genders, passportTypes, maxBirthday, districts: areaDistricts } = $props(); let user = $state({ id: initUser.id, + canEditPassportInformation: initUser.can_edit_passport_information, memberNumber: initUser.member?.number, isActiveMember: initUser.member?.is_active, username: initUser.username, @@ -38,6 +39,11 @@ prefixName: '', nickname: '', suffixName: '', + familyName: '', + middleName: '', + givenName: '', + passportTypeID: '', + passportNumber: '', gender: '', birthday: '', district: '', @@ -61,8 +67,18 @@ inputs.password.value = ''; inputs.newPassword.value = ''; inputs.confirmNewPassword.value = ''; - inputs.gender.value = genders[user.genderID]; - inputs.birthday.value = user.birthday; + inputs.prefixName.value = user.prefixName; + inputs.nickname.value = user.nickname; + inputs.suffixName.value = user.suffixName; + if (user.canEditPassportInformation) { + inputs.familyName.value = user.familyName; + inputs.middleName.value = user.middleName; + inputs.givenName.value = user.givenName; + inputs.passportTypeID.value = user.passportTypeID; + inputs.passportNumber.value = user.passportNumber; + inputs.gender.value = genders[user.genderID]; + inputs.birthday.value = user.birthday; + } inputs.district.value = user.districtID; inputs.address.value = user.address; for(let key in feedbacks) { @@ -120,15 +136,42 @@ feedbacks.suffixName = `The suffix name must not be greater than ${inputs.suffixName.maxLength} characters.`; } } - if(inputs.gender.validity.valueMissing) { - feedbacks.gender = 'The gender field is required.'; - } else if(inputs.gender.validity.tooLong) { - feedbacks.gender = `The gender not be greater than ${gender.maxLength} characters.`; - } - if(inputs.birthday.validity.valueMissing) { - feedbacks.birthday = 'The birthday field is required.'; - } else if(inputs.birthday.validity.rangeOverflow) { - feedbacks.birthday = `The birthday not be greater than ${birthday.max} characters.`; + if (user.canEditPassportInformation) { + if(inputs.familyName.validity.valueMissing) { + feedbacks.familyName = 'The family name field is required.'; + } else if(inputs.familyName.validity.tooLong) { + feedbacks.familyName = `The family name must not be greater than ${inputs.familyName.maxLength} characters.`; + } + if(inputs.middleName.validity.tooLong) { + feedbacks.middleName = `The middle name must not be greater than ${inputs.middleName.maxLength} characters.`; + } + if(inputs.givenName.validity.valueMissing) { + feedbacks.givenName = 'The given name field is required.'; + } else if(inputs.givenName.validity.tooLong) { + feedbacks.givenName = `The given name must not be greater than ${inputs.givenName.maxLength} characters.`; + } + if(inputs.passportTypeID.validity.valueMissing) { + feedbacks.passportTypeID = 'The passport type field is required.'; + } + if(inputs.passportNumber.validity.valueMissing) { + feedbacks.passportNumber = 'The passport number field is required.'; + } else if(inputs.passportNumber.validity.tooShort) { + feedbacks.passportNumber = `The passport number field must be at least ${inputs.passportNumber.minLength} characters.`; + } else if(inputs.passportNumber.validity.tooLong) { + feedbacks.passportNumber = `The passport number field must not be greater than ${inputs.passportNumber.maxLength} characters.`; + } else if(inputs.passportNumber.validity.patternMismatch) { + feedbacks.passportNumber = 'The passport number field format is invalid. It should only contain uppercase letters and numbers.'; + } + if(inputs.gender.validity.valueMissing) { + feedbacks.gender = 'The gender field is required.'; + } else if(inputs.gender.validity.tooLong) { + feedbacks.gender = `The gender must not be greater than ${inputs.gender.maxLength} characters.`; + } + if(inputs.birthday.validity.valueMissing) { + feedbacks.birthday = 'The birthday field is required.'; + } else if(inputs.birthday.validity.rangeOverflow) { + feedbacks.birthday = `The birthday must not be greater than ${inputs.birthday.max} characters.`; + } } if(user.isActiveMember || inputs.district.value) { if (inputs.district.validity.valueMissing) { @@ -148,15 +191,22 @@ function successCallback(response) { alert(response.data.success); - genders[response.data.gender_id] = response.data.gender; user.username = response.data.username; if (user.memberNumber) { user.prefixName = response.data.prefix_name; user.nickname = response.data.nickname; user.suffixName = response.data.suffix_name; } - user.genderID = response.data.gender_id; - user.birthday = formatToDate(response.data.birthday); + if (user.canEditPassportInformation) { + genders[response.data.gender_id] = response.data.gender; + user.familyName = response.data.family_name; + user.middleName = response.data.middle_name; + user.givenName = response.data.given_name; + user.passportTypeID = response.data.passport_type_id; + user.passportNumber = response.data.passport_number; + user.genderID = response.data.gender_id; + user.birthday = formatToDate(response.data.birthday); + } user.districtID = response.data.district_id ?? ''; user.address = response.data.address; editing = false; @@ -176,6 +226,9 @@ case 'password': feedbacks.password = value; break; + case 'new_password': + feedbacks.newPassword = value; + break; case 'prefix_name': feedbacks.prefixName = value; break; @@ -185,8 +238,20 @@ case 'suffix_name': feedbacks.suffixName = value; break; - case 'new_password': - feedbacks.newPassword = value; + case 'family_name': + feedbacks.familyName = value; + break; + case 'middle_name': + feedbacks.middleName = value; + break; + case 'given_name': + feedbacks.givenName = value; + break; + case 'passport_type_id': + feedbacks.passportTypeID = value; + break; + case 'passport_number': + feedbacks.passportNumber = value; break; case 'gender': feedbacks.gender = value; @@ -206,6 +271,10 @@ } } } + if(error.status == 409) { + alert(error.response.data.message); + user.canEditPassportInformation = false; + } submitting = false; updating = false; } @@ -218,11 +287,7 @@ if(submitting == 'updateProfile'+submitAt) { if(validation()) { updating = true; - let data = { - username: inputs.username.value, - gender: inputs.gender.value, - birthday: inputs.birthday.value, - } + let data = {username: inputs.username.value} if( inputs.newPassword.value || inputs.username.value != user.username @@ -233,6 +298,15 @@ data['new_password'] = inputs.newPassword.value; data['new_password_confirmation'] = inputs.confirmNewPassword.value; } + if (user.canEditPassportInformation) { + data['family_name'] = inputs.familyName.value; + data['middle_name'] = inputs.middleName.value; + data['given_name'] = inputs.givenName.value; + data['passport_type_id'] = inputs.passportTypeID.value; + data['passport_number'] = inputs.passportNumber.value; + data['gender'] = inputs.gender.value; + data['birthday'] = inputs.birthday.value; + } if (user.memberNumber) { data['prefix_name'] = inputs.prefixName.value; data['nickname'] = inputs.nickname.value; @@ -293,6 +367,21 @@
  1. Password only require when you change the username or password
  2. New password and confirm password is not require unless you want to change a new password
  3. +
  4. + The passport information locks +
      +
    • For avoid overwrite proctor passport validated information, you cannot edit the information 2 hours before of the scheduled admission test.
    • +
    • if you a attended admission test, you cannot edit the passport information because it is already verified by proctor.
    • +
    • If you paid prior evidence order, you cannot edit the passport information until we are rejected your evidence because the verification may be in progress.
    • +
    • If we are accepted your prior evidence, you cannot edit the passport information because it is already verified.
    • +
    • If you are in member transfer process, you cannot edit the passport information.
    • +
    • If we are accepted your member transfer, you can not edit the passport information because it is already verified.
    • +
    • If you are member, you cannot edit the passport information because it is already verified.
    • +
    • Otherwise, you can edit the passport information, if you are not in any of the above situations but cannot see the edit input box, please refresh the page, If the problem persists, please contact us.
    • +
    • If you are in any of the above situations and need to change the passport information, please contact us and provide the needed documentation(e.g.: Deed Poll).
    • +
    • If you are active member and need to change the passport information, you must contact us and provide the needed documentation(e.g.: Deed Poll) because we need to update your information to Companies Registry.
    • +
    +
@@ -371,24 +460,59 @@ {/if} -
Family Name:
-
{user.familyName}
+ + + -
Middle Name:
-
{user.middleName ?? "\u00A0"}
+ + + -
Given Name:
-
{user.givenName}
+ + + -
Passport Type:
-
{passportTypes[user.passportTypeID]}
+ + + {#each Object.entries(passportTypes) as [key, value]} + + {/each} + + -
Passport Number:
-
+ + +