tambah fungsi upload peserta sebagai hadir

This commit is contained in:
Saufi
2026-05-20 20:10:43 +08:00
parent 154b2c650e
commit 7e4bbca2db
9 changed files with 1109 additions and 34 deletions

773
package-lock.json generated Normal file
View File

@@ -0,0 +1,773 @@
{
"name": "ecert",
"lockfileVersion": 3,
"requires": true,
"packages": {
"node_modules/@emnapi/wasi-threads": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@kurkle/color": {
"version": "0.3.4",
"resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz",
"integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==",
"license": "MIT"
},
"node_modules/@oxc-project/types": {
"version": "0.130.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz",
"integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/Boshen"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz",
"integrity": "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
"integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
"dev": true,
"license": "MIT"
},
"node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/bootstrap": {
"version": "5.3.8",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
"integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"license": "MIT",
"peerDependencies": {
"@popperjs/core": "^2.11.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chalk/node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/chart.js": {
"version": "4.5.1",
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz",
"integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==",
"license": "MIT",
"dependencies": {
"@kurkle/color": "^0.3.0"
},
"engines": {
"pnpm": ">=8"
}
},
"node_modules/cliui": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"string-width": "^4.2.0",
"strip-ansi": "^6.0.1",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/concurrently": {
"version": "9.2.1",
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
"integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "4.1.2",
"rxjs": "7.8.2",
"shell-quote": "1.8.3",
"supports-color": "8.1.1",
"tree-kill": "1.2.2",
"yargs": "17.7.2"
},
"bin": {
"conc": "dist/bin/concurrently.js",
"concurrently": "dist/bin/concurrently.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
}
},
"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==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"dev": true,
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true,
"license": "ISC",
"engines": {
"node": "6.* || 8.* || >= 10.*"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jquery": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"license": "MIT"
},
"node_modules/laravel-vite-plugin": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.1.0.tgz",
"integrity": "sha512-Fzocl+X4eQ9jOi0RwdphYRGkUbPJ3ky1pTAST5Ot18cS2gw6d2vldK2eCrlKDVjtibCjCx5qptYDlA0373n7qg==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"tinyglobby": "^0.2.12",
"vite-plugin-full-reload": "^1.1.0"
},
"bin": {
"clean-orphaned-assets": "bin/clean.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"fontaine": "^0.5.0",
"vite": "^8.0.0"
},
"peerDependenciesMeta": {
"fontaine": {
"optional": true
}
}
},
"node_modules/lightningcss": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
"dev": 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.32.0",
"lightningcss-darwin-arm64": "1.32.0",
"lightningcss-darwin-x64": "1.32.0",
"lightningcss-freebsd-x64": "1.32.0",
"lightningcss-linux-arm-gnueabihf": "1.32.0",
"lightningcss-linux-arm64-gnu": "1.32.0",
"lightningcss-linux-arm64-musl": "1.32.0",
"lightningcss-linux-x64-gnu": "1.32.0",
"lightningcss-linux-x64-musl": "1.32.0",
"lightningcss-win32-arm64-msvc": "1.32.0",
"lightningcss-win32-x64-msvc": "1.32.0"
}
},
"node_modules/lightningcss-win32-x64-msvc": {
"version": "1.32.0",
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MPL-2.0",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 12.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/nanoid": {
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8.6"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz",
"integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/rolldown": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz",
"integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@oxc-project/types": "=0.130.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
"rolldown": "bin/cli.mjs"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
"@rolldown/binding-android-arm64": "1.0.1",
"@rolldown/binding-darwin-arm64": "1.0.1",
"@rolldown/binding-darwin-x64": "1.0.1",
"@rolldown/binding-freebsd-x64": "1.0.1",
"@rolldown/binding-linux-arm-gnueabihf": "1.0.1",
"@rolldown/binding-linux-arm64-gnu": "1.0.1",
"@rolldown/binding-linux-arm64-musl": "1.0.1",
"@rolldown/binding-linux-ppc64-gnu": "1.0.1",
"@rolldown/binding-linux-s390x-gnu": "1.0.1",
"@rolldown/binding-linux-x64-gnu": "1.0.1",
"@rolldown/binding-linux-x64-musl": "1.0.1",
"@rolldown/binding-openharmony-arm64": "1.0.1",
"@rolldown/binding-wasm32-wasi": "1.0.1",
"@rolldown/binding-win32-arm64-msvc": "1.0.1",
"@rolldown/binding-win32-x64-msvc": "1.0.1"
}
},
"node_modules/rxjs": {
"version": "7.8.2",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/shell-quote": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
"integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"dev": true,
"license": "MIT",
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
"strip-ansi": "^6.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/tinyglobby": {
"version": "0.2.16",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.4"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"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==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/tinyglobby/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"dev": true,
"license": "MIT",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/vite": {
"version": "8.0.13",
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz",
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"lightningcss": "^1.32.0",
"picomatch": "^4.0.4",
"postcss": "^8.5.14",
"rolldown": "1.0.1",
"tinyglobby": "^0.2.16"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"@vitejs/devtools": "^0.1.18",
"esbuild": "^0.27.0 || ^0.28.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"@vitejs/devtools": {
"optional": true
},
"esbuild": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/vite-plugin-full-reload": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz",
"integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picocolors": "^1.0.0",
"picomatch": "^2.3.1"
}
},
"node_modules/vite/node_modules/picomatch": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
"strip-ansi": "^6.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
}
},
"node_modules/y18n": {
"version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=10"
}
},
"node_modules/yargs": {
"version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cliui": "^8.0.1",
"escalade": "^3.1.1",
"get-caller-file": "^2.0.5",
"require-directory": "^2.1.1",
"string-width": "^4.2.3",
"y18n": "^5.0.5",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": ">=12"
}
},
"node_modules/yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
}
}
}
}

View File

@@ -112,6 +112,58 @@ class ParticipantController extends Controller
return back()->with('success', 'Peserta berjaya ditambah.'); return back()->with('success', 'Peserta berjaya ditambah.');
} }
public function edit(Program $program, ProgramParticipant $pp, Request $request): View
{
if ($pp->program_id !== $program->id) {
abort(403);
}
$pp->load('participant');
$filters = $request->only(['search', 'source', 'status', 'page']);
return view('admin.programs.participants.edit', compact('program', 'pp', 'filters'));
}
public function update(Program $program, ProgramParticipant $pp, Request $request): RedirectResponse
{
if ($pp->program_id !== $program->id) {
abort(403);
}
$request->validate([
'name' => ['required', 'string', 'max:255'],
'no_kp' => ['required', 'string', 'regex:/^\d{12}$/', 'unique:participants,no_kp,' . $pp->participant_id],
'email' => ['nullable', 'email', 'max:255'],
'phone' => ['nullable', 'string', 'max:20'],
'agency' => ['nullable', 'string', 'max:255'],
'session' => ['nullable', 'in:pagi,petang,full_day'],
]);
$pp->load('participant');
DB::transaction(function () use ($pp, $request) {
$pp->participant->update([
'name' => $request->name,
'no_kp' => preg_replace('/[^0-9]/', '', $request->no_kp),
'email' => $request->email ?: null,
'phone' => $request->phone ?: null,
'agency' => $request->agency ?: null,
]);
$pp->update([
'pre_registered_session' => $request->session ?: null,
]);
});
AuditLogService::log('participant.updated', $pp->participant);
$filters = array_filter($request->only(['search', 'source', 'status', 'page']));
$indexUrl = route('admin.programs.participants.index', $program)
. ($filters ? '?' . http_build_query($filters) : '');
return redirect($indexUrl)->with('success', 'Maklumat peserta berjaya dikemaskini.');
}
public function destroy(Program $program, ProgramParticipant $pp): RedirectResponse public function destroy(Program $program, ProgramParticipant $pp): RedirectResponse
{ {
if ($pp->program_id !== $program->id) { if ($pp->program_id !== $program->id) {
@@ -129,7 +181,10 @@ class ParticipantController extends Controller
public function importForm(Program $program): View public function importForm(Program $program): View
{ {
return view('admin.programs.participants.import', compact('program')); $cutoff = $program->checkin_end_at ?? $program->end_date->endOfDay();
$programEnded = now()->gt($cutoff);
return view('admin.programs.participants.import', compact('program', 'programEnded'));
} }
public function import(Program $program, Request $request, ParticipantImportService $importer): RedirectResponse public function import(Program $program, Request $request, ParticipantImportService $importer): RedirectResponse
@@ -137,23 +192,41 @@ class ParticipantController extends Controller
$request->validate([ $request->validate([
'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:5120'], 'csv_file' => ['required', 'file', 'mimes:csv,txt', 'max:5120'],
'session' => ['nullable', 'in:pagi,petang,full_day'], 'session' => ['nullable', 'in:pagi,petang,full_day'],
'mark_attendance' => ['nullable', 'boolean'],
]); ]);
$cutoff = $program->checkin_end_at ?? $program->end_date->endOfDay();
$markAttendance = now()->gt($cutoff) && $request->boolean('mark_attendance');
$result = $importer->import( $result = $importer->import(
$program, $program,
$request->file('csv_file'), $request->file('csv_file'),
$request->input('session', $program->default_staff_session) $request->input('session', $program->default_staff_session),
$markAttendance
); );
AuditLogService::log('participant.imported', $program, [], [ AuditLogService::log('participant.imported', $program, [], [
'success' => $result['success'], 'success' => $result['success'],
'duplicates' => $result['duplicates'], 'duplicates' => $result['duplicates'],
'failed' => $result['failed'], 'failed' => $result['failed'],
'mark_attendance'=> $markAttendance,
]); ]);
return back()->with('import_result', $result); return back()->with('import_result', $result);
} }
public function clearParticipants(Program $program): RedirectResponse
{
$deleted = $program->programParticipants()
->where('status', '!=', 'checked_in')
->whereDoesntHave('attendance')
->delete();
return redirect()
->route('admin.programs.participants.import.form', $program)
->with('success', "{$deleted} rekod peserta (belum hadir) telah dipadam.");
}
public function export(Program $program): \Symfony\Component\HttpFoundation\StreamedResponse public function export(Program $program): \Symfony\Component\HttpFoundation\StreamedResponse
{ {
$headers = [ $headers = [

View File

@@ -2,8 +2,10 @@
namespace App\Services; namespace App\Services;
use App\Models\Attendance;
use App\Models\Participant; use App\Models\Participant;
use App\Models\Program; use App\Models\Program;
use App\Models\ProgramParticipant;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator; use Illuminate\Support\Facades\Validator;
@@ -11,37 +13,54 @@ use League\Csv\Reader;
class ParticipantImportService class ParticipantImportService
{ {
public function import(Program $program, UploadedFile $file, ?string $defaultSession): array public function import(Program $program, UploadedFile $file, ?string $defaultSession, bool $markAttendance = false): array
{ {
$result = ['success' => 0, 'duplicates' => 0, 'failed' => 0, 'errors' => []]; $result = ['success' => 0, 'duplicates' => 0, 'failed' => 0, 'errors' => [], 'all_empty_ic' => false];
$csv = Reader::createFromPath($file->getRealPath(), 'r'); $csv = Reader::createFromPath($file->getRealPath(), 'r');
$csv->setHeaderOffset(0); $csv->setHeaderOffset(0);
// Strip UTF-8 BOM if present (Excel-exported CSV)
$csv->setOutputBOM(''); $csv->setOutputBOM('');
try { try {
$csv->addStreamFilter('convert.iconv.UTF-8/UTF-8'); $csv->addStreamFilter('convert.iconv.UTF-8/UTF-8');
} catch (\Throwable) {} } catch (\Throwable) {}
// Collect all rows first to detect all_empty_ic
$rows = [];
foreach ($csv->getRecords() as $rowNum => $row) { foreach ($csv->getRecords() as $rowNum => $row) {
$row = array_map('trim', $row); $row = array_map('trim', $row);
// Normalise header keys (lowercase, strip BOM)
$row = array_combine( $row = array_combine(
array_map(fn($k) => strtolower(preg_replace('/[\x{FEFF}\s]/u', '', $k)), array_keys($row)), array_map(fn($k) => strtolower(preg_replace('/[\x{FEFF}\s]/u', '', $k)), array_keys($row)),
array_values($row) array_values($row)
); );
$rows[$rowNum] = $row;
}
if (empty($rows)) {
return $result;
}
// If every row has an empty no_kp, offer delete instead
$noKpValues = array_map(
fn($row) => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''),
$rows
);
if (count(array_filter($noKpValues)) === 0) {
$result['all_empty_ic'] = true;
return $result;
}
$session = $defaultSession ?? $program->default_staff_session;
foreach ($rows as $rowNum => $row) {
$data = [ $data = [
'name' => $row['name'] ?? $row['nama'] ?? '', 'name' => $row['name'] ?? $row['nama'] ?? '',
'no_kp' => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''), 'no_kp' => preg_replace('/[^0-9]/', '', $row['no_kp'] ?? $row['nokp'] ?? $row['ic'] ?? ''),
'email' => $row['email'] ?? $row['emel'] ?? null, 'email' => $row['email'] ?? $row['emel'] ?? null,
'phone' => $row['phone'] ?? $row['telefon'] ?? $row['phone'] ?? null, 'phone' => $row['phone'] ?? $row['telefon'] ?? null,
'agency' => $row['agency'] ?? $row['agensi'] ?? $row['jabatan'] ?? null, 'agency' => $row['agency'] ?? $row['agensi'] ?? $row['jabatan'] ?? null,
]; ];
// Validate row
$validator = Validator::make($data, [ $validator = Validator::make($data, [
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'no_kp' => ['required', 'digits:12'], 'no_kp' => ['required', 'digits:12'],
@@ -56,8 +75,7 @@ class ParticipantImportService
} }
try { try {
DB::transaction(function () use ($program, $data, $defaultSession, &$result) { DB::transaction(function () use ($program, $data, $session, $markAttendance, &$result) {
// Find or create participant by no_kp
$participant = Participant::firstOrCreate( $participant = Participant::firstOrCreate(
['no_kp' => $data['no_kp']], ['no_kp' => $data['no_kp']],
[ [
@@ -69,25 +87,36 @@ class ParticipantImportService
] ]
); );
// Check duplicate in this program $pp = $program->programParticipants()
$exists = $program->programParticipants()
->where('participant_id', $participant->id) ->where('participant_id', $participant->id)
->exists(); ->first();
if ($exists) { if ($pp) {
// Participant already registered
if ($markAttendance) {
$this->recordAttendance($program, $participant, $pp, $session);
$result['duplicates']++; $result['duplicates']++;
} else {
$result['duplicates']++;
}
return; return;
} }
$program->programParticipants()->create([ $newStatus = $markAttendance ? 'checked_in' : 'registered';
$pp = $program->programParticipants()->create([
'participant_id' => $participant->id, 'participant_id' => $participant->id,
'registration_source' => 'import', 'registration_source' => 'import',
'is_pre_registered' => true, 'is_pre_registered' => true,
'pre_registered_session' => $defaultSession, 'pre_registered_session' => $session,
'status' => 'registered', 'status' => $newStatus,
'registered_at' => now(), 'registered_at' => now(),
]); ]);
if ($markAttendance) {
$this->recordAttendance($program, $participant, $pp, $session);
}
$result['success']++; $result['success']++;
}); });
} catch (\Throwable $e) { } catch (\Throwable $e) {
@@ -98,4 +127,25 @@ class ParticipantImportService
return $result; return $result;
} }
private function recordAttendance(Program $program, Participant $participant, ProgramParticipant $pp, ?string $session): void
{
$alreadyAttended = Attendance::where('program_id', $program->id)
->where('participant_id', $participant->id)
->exists();
if ($alreadyAttended) {
return;
}
$pp->update(['status' => 'checked_in']);
Attendance::create([
'program_id' => $program->id,
'participant_id' => $participant->id,
'program_participant_id' => $pp->id,
'attendance_source' => 'import',
'attendance_session' => $session ?? 'full_day',
'checked_in_at' => now(),
]);
}
} }

View File

@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
DB::statement("ALTER TABLE attendances MODIFY attendance_source ENUM('pre_registered_staff','walk_in_external','admin_manual','import') NOT NULL");
}
public function down(): void
{
DB::statement("ALTER TABLE attendances MODIFY attendance_source ENUM('pre_registered_staff','walk_in_external','admin_manual') NOT NULL");
}
};

29
src/package-lock.json generated
View File

@@ -1,5 +1,5 @@
{ {
"name": "ecert", "name": "src",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
@@ -16,6 +16,29 @@
"vite": "^8.0.0" "vite": "^8.0.0"
} }
}, },
"node_modules/@emnapi/core": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"@emnapi/wasi-threads": "1.2.1",
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@emnapi/wasi-threads": { "node_modules/@emnapi/wasi-threads": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
@@ -67,7 +90,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
"url": "https://opencollective.com/popperjs" "url": "https://opencollective.com/popperjs"
@@ -920,7 +942,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -1092,7 +1113,6 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -1123,7 +1143,6 @@
"integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"lightningcss": "^1.32.0", "lightningcss": "^1.32.0",
"picomatch": "^4.0.4", "picomatch": "^4.0.4",

View File

@@ -0,0 +1,93 @@
@extends('layouts.admin')
@section('title', 'Edit Peserta — ' . $pp->participant->name)
@section('header', 'Edit Maklumat Peserta')
@section('breadcrumb')
<li class="breadcrumb-item"><a href="{{ route('admin.programs.index') }}">Program</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.show', $program) }}">{{ Str::limit($program->title, 25) }}</a></li>
<li class="breadcrumb-item"><a href="{{ route('admin.programs.participants.index', $program) }}">Peserta</a></li>
<li class="breadcrumb-item active">Edit</li>
@endsection
@section('content')
<div class="row justify-content-center">
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-white border-bottom py-3">
<span class="fw-semibold">
<i class="bi bi-person-gear me-2 text-primary"></i>Kemaskini Maklumat Peserta
</span>
</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.programs.participants.update', [$program, $pp]) }}">
@csrf @method('PUT')
<div class="mb-3">
<label class="form-label fw-medium">Nama Penuh <span class="text-danger">*</span></label>
<input type="text" name="name" class="form-control @error('name') is-invalid @enderror"
value="{{ old('name', $pp->participant->name) }}" required>
@error('name')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">No. Kad Pengenalan <span class="text-danger">*</span></label>
<input type="text" name="no_kp" class="form-control @error('no_kp') is-invalid @enderror"
value="{{ old('no_kp', $pp->participant->no_kp) }}"
placeholder="12 digit tanpa sempang" required>
@error('no_kp')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Emel</label>
<input type="email" name="email" class="form-control @error('email') is-invalid @enderror"
value="{{ old('email', $pp->participant->email) }}"
placeholder="Kosongkan jika tiada">
@error('email')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">No. Telefon</label>
<input type="text" name="phone" class="form-control @error('phone') is-invalid @enderror"
value="{{ old('phone', $pp->participant->phone) }}"
placeholder="Kosongkan jika tiada">
@error('phone')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-3">
<label class="form-label fw-medium">Jabatan / Agensi</label>
<input type="text" name="agency" class="form-control @error('agency') is-invalid @enderror"
value="{{ old('agency', $pp->participant->agency) }}"
placeholder="Kosongkan jika tiada">
@error('agency')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
<div class="mb-4">
<label class="form-label fw-medium">Sesi</label>
<select name="session" class="form-select @error('session') is-invalid @enderror">
<option value=""> Tiada Sesi </option>
<option value="pagi" {{ old('session', $pp->pre_registered_session) === 'pagi' ? 'selected' : '' }}>Pagi</option>
<option value="petang" {{ old('session', $pp->pre_registered_session) === 'petang' ? 'selected' : '' }}>Petang</option>
<option value="full_day" {{ old('session', $pp->pre_registered_session) === 'full_day' ? 'selected' : '' }}>Sehari Penuh</option>
</select>
@error('session')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div>
@foreach($filters as $key => $value)
<input type="hidden" name="{{ $key }}" value="{{ $value }}">
@endforeach
@php $backUrl = route('admin.programs.participants.index', $program) . ($filters ? '?' . http_build_query($filters) : ''); @endphp
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary">
<i class="bi bi-check-lg me-1"></i> Simpan
</button>
<a href="{{ $backUrl }}" class="btn btn-outline-secondary">Batal</a>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View File

@@ -17,6 +17,30 @@
{{-- Import Result --}} {{-- Import Result --}}
@if(session('import_result')) @if(session('import_result'))
@php $r = session('import_result'); @endphp @php $r = session('import_result'); @endphp
{{-- All IC empty offer delete --}}
@if(!empty($r['all_empty_ic']))
<div class="card border-0 shadow-sm mb-4 border-start border-4 border-warning">
<div class="card-body">
<h6 class="fw-semibold mb-2 text-warning">
<i class="bi bi-exclamation-triangle me-2"></i>Semua No. K/P Kosong
</h6>
<p class="small text-muted mb-3">
Fail CSV yang dimuat naik tidak mengandungi sebarang No. K/P yang sah.
Tiada rekod diimport. Adakah anda ingin <strong>memadam semua peserta belum hadir</strong> dalam program ini?
</p>
<form method="POST" action="{{ route('admin.programs.participants.clear', $program) }}">
@csrf @method('DELETE')
<button type="submit" class="btn btn-danger btn-sm"
onclick="return confirm('Padam semua peserta yang belum hadir? Tindakan ini tidak boleh dibatalkan.')">
<i class="bi bi-trash me-1"></i> Padam Peserta Belum Hadir
</button>
<a href="{{ route('admin.programs.participants.import.form', $program) }}"
class="btn btn-outline-secondary btn-sm ms-2">Batal</a>
</form>
</div>
</div>
@else
<div class="card border-0 shadow-sm mb-4 border-start border-4 <div class="card border-0 shadow-sm mb-4 border-start border-4
{{ $r['failed'] > 0 ? 'border-warning' : 'border-success' }}"> {{ $r['failed'] > 0 ? 'border-warning' : 'border-success' }}">
<div class="card-body"> <div class="card-body">
@@ -59,6 +83,7 @@
</div> </div>
</div> </div>
@endif @endif
@endif
{{-- Upload Form --}} {{-- Upload Form --}}
<div class="card border-0 shadow-sm"> <div class="card border-0 shadow-sm">
@@ -79,7 +104,7 @@
@error('csv_file')<div class="invalid-feedback">{{ $message }}</div>@enderror @error('csv_file')<div class="invalid-feedback">{{ $message }}</div>@enderror
</div> </div>
<div class="mb-4"> <div class="mb-3">
<label class="form-label fw-medium">Sesi Default</label> <label class="form-label fw-medium">Sesi Default</label>
<select name="session" class="form-select"> <select name="session" class="form-select">
<option value=""> Ikut Tetapan Program </option> <option value=""> Ikut Tetapan Program </option>
@@ -90,6 +115,24 @@
<div class="form-text">Sesi yang akan digunakan untuk semua peserta dalam fail ini.</div> <div class="form-text">Sesi yang akan digunakan untuk semua peserta dalam fail ini.</div>
</div> </div>
@if($programEnded)
<div class="mb-4 p-3 bg-warning bg-opacity-10 rounded border border-warning-subtle">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="mark_attendance"
value="1" id="markAttendance">
<label class="form-check-label fw-medium" for="markAttendance">
Tandakan sebagai Data Kehadiran
</label>
</div>
<div class="small text-muted mt-1 ms-4">
<i class="bi bi-info-circle me-1"></i>
Tempoh check-in telah tamat pada <strong>{{ ($program->checkin_end_at ?? $program->end_date->endOfDay())->format('d M Y, H:i') }}</strong>.
Jika ditanda, semua peserta dalam fail ini akan direkodkan sebagai <strong>hadir</strong>.
Peserta sedia ada dalam program akan dikemaskini status kehadirannya.
</div>
</div>
@endif
<button type="submit" class="btn btn-primary w-100"> <button type="submit" class="btn btn-primary w-100">
<i class="bi bi-upload me-2"></i>Mula Import <i class="bi bi-upload me-2"></i>Mula Import
</button> </button>

View File

@@ -201,6 +201,10 @@
<i class="bi bi-download"></i> <i class="bi bi-download"></i>
</a> </a>
@endif @endif
<a href="{{ route('admin.programs.participants.edit', [$program, $pp]) . (request()->hasAny(['search','source','status','page']) ? '?' . http_build_query(request()->only(['search','source','status','page'])) : '') }}"
class="btn btn-sm btn-outline-secondary" title="Edit Peserta">
<i class="bi bi-pencil"></i>
</a>
@if($pp->status !== 'checked_in') @if($pp->status !== 'checked_in')
<form method="POST" <form method="POST"
action="{{ route('admin.programs.participants.destroy', [$program, $pp]) }}" action="{{ route('admin.programs.participants.destroy', [$program, $pp]) }}"

View File

@@ -53,9 +53,12 @@ Route::middleware(['auth', 'admin'])->prefix('admin')->name('admin.')->group(fun
Route::get('/', [ParticipantController::class, 'index'])->name('index'); Route::get('/', [ParticipantController::class, 'index'])->name('index');
Route::get('/create', [ParticipantController::class, 'create'])->name('create'); Route::get('/create', [ParticipantController::class, 'create'])->name('create');
Route::post('/', [ParticipantController::class, 'store'])->name('store'); Route::post('/', [ParticipantController::class, 'store'])->name('store');
Route::get('/{pp}/edit', [ParticipantController::class, 'edit'])->name('edit');
Route::put('/{pp}', [ParticipantController::class, 'update'])->name('update');
Route::delete('/{pp}', [ParticipantController::class, 'destroy'])->name('destroy'); Route::delete('/{pp}', [ParticipantController::class, 'destroy'])->name('destroy');
Route::get('/import', [ParticipantController::class, 'importForm'])->name('import.form'); Route::get('/import', [ParticipantController::class, 'importForm'])->name('import.form');
Route::post('/import', [ParticipantController::class, 'import'])->name('import'); Route::post('/import', [ParticipantController::class, 'import'])->name('import');
Route::delete('/clear', [ParticipantController::class, 'clearParticipants'])->name('clear');
Route::get('/export', [ParticipantController::class, 'export'])->name('export'); Route::get('/export', [ParticipantController::class, 'export'])->name('export');
}); });