From 10fb30ad696c3bc6e158c733ea7389ec11debe1b Mon Sep 17 00:00:00 2001 From: Saufi Date: Wed, 24 Jun 2026 20:32:14 +0800 Subject: [PATCH] first commit --- .claude/settings.json | 15 + .dockerignore | 55 + .editorconfig | 18 + .env.docker | 58 + .env.example | 69 + .gitattributes | 11 + .gitignore | 27 + .npmrc | 2 + Dockerfile | 86 + README.md | 282 + app/Http/Controllers/AuditLogController.php | 22 + app/Http/Controllers/Auth/LoginController.php | 69 + .../ChecklistBuilderController.php | 188 + .../ChecklistTemplateController.php | 110 + app/Http/Controllers/CommentController.php | 45 + app/Http/Controllers/ConsultantController.php | 132 + app/Http/Controllers/Controller.php | 11 + .../CorrectionRequestController.php | 71 + app/Http/Controllers/DashboardController.php | 118 + app/Http/Controllers/DataCentreController.php | 134 + app/Http/Controllers/DocumentController.php | 47 + app/Http/Controllers/ReminderController.php | 67 + app/Http/Controllers/ReportController.php | 59 + .../Controllers/ReportingCycleController.php | 88 + app/Http/Controllers/SettingController.php | 42 + app/Http/Controllers/SubmissionController.php | 143 + .../SubmissionReviewController.php | 122 + app/Http/Controllers/UserController.php | 108 + app/Http/Requests/ConsultantRequest.php | 57 + app/Http/Requests/DataCentreRequest.php | 37 + app/Http/Requests/ReportingCycleRequest.php | 40 + app/Models/AppSetting.php | 44 + app/Models/ChecklistFormula.php | 41 + app/Models/ChecklistItem.php | 69 + app/Models/ChecklistScope.php | 38 + app/Models/ChecklistSection.php | 50 + app/Models/ChecklistTemplate.php | 47 + app/Models/Consultant.php | 68 + app/Models/DataCentre.php | 69 + app/Models/DataCentreConsultantAssignment.php | 46 + app/Models/ReportingCycle.php | 77 + app/Models/Submission.php | 152 + app/Models/SubmissionAnswer.php | 38 + app/Models/SubmissionComment.php | 40 + app/Models/SubmissionCorrectionRequest.php | 61 + app/Models/SubmissionDocument.php | 56 + app/Models/SubmissionResult.php | 32 + app/Models/SubmissionStatusHistory.php | 30 + app/Models/User.php | 72 + app/Policies/ChecklistTemplatePolicy.php | 34 + app/Policies/ConsultantPolicy.php | 39 + app/Policies/DataCentrePolicy.php | 46 + app/Policies/ReportingCyclePolicy.php | 34 + app/Policies/SubmissionPolicy.php | 80 + app/Providers/AppServiceProvider.php | 25 + app/Services/AssignmentService.php | 80 + app/Services/DocumentService.php | 67 + app/Services/ExpressionEvaluator.php | 204 + app/Services/FormulaService.php | 100 + app/Services/ReportingCycleService.php | 37 + app/Services/SubmissionWorkflowService.php | 144 + artisan | 18 + bootstrap/app.php | 26 + bootstrap/cache/.gitignore | 2 + bootstrap/providers.php | 7 + composer.json | 88 + composer.lock | 8536 +++++++++++++++++ config/activitylog.php | 73 + config/app.php | 126 + config/auth.php | 117 + config/cache.php | 136 + config/database.php | 184 + config/filesystems.php | 80 + config/logging.php | 132 + config/mail.php | 118 + config/mbip.php | 9 + config/permission.php | 219 + config/queue.php | 129 + config/services.php | 38 + config/session.php | 233 + database/.gitignore | 1 + database/factories/ConsultantFactory.php | 28 + database/factories/DataCentreFactory.php | 25 + database/factories/ReportingCycleFactory.php | 44 + database/factories/UserFactory.php | 45 + .../0001_01_01_000000_create_users_table.php | 49 + .../0001_01_01_000001_create_cache_table.php | 35 + .../0001_01_01_000002_create_jobs_table.php | 59 + ..._06_23_083811_create_permission_tables.php | 137 + ...06_23_083812_create_activity_log_table.php | 23 + ...06_23_090001_add_fields_to_users_table.php | 26 + ..._06_23_090002_create_consultants_table.php | 32 + ...06_23_090003_create_data_centres_table.php | 34 + ...ta_centre_consultant_assignments_table.php | 31 + ...90005_create_checklist_templates_table.php | 28 + ...090006_create_checklist_sections_table.php | 29 + ...3_090007_create_checklist_scopes_table.php | 28 + ...23_090008_create_checklist_items_table.php | 37 + ...090009_create_checklist_formulas_table.php | 32 + ...3_090012_create_reporting_cycles_table.php | 33 + ..._06_23_090013_create_submissions_table.php | 52 + ...090014_create_submission_answers_table.php | 33 + ...090015_create_submission_results_table.php | 28 + ...0016_create_submission_documents_table.php | 32 + ...eate_submission_status_histories_table.php | 28 + ...e_submission_correction_requests_table.php | 36 + ...90019_create_submission_comments_table.php | 31 + ...06_23_090020_create_app_settings_table.php | 27 + database/seeders/AppSettingsSeeder.php | 51 + database/seeders/ChecklistTemplateSeeder.php | 207 + database/seeders/DatabaseSeeder.php | 19 + database/seeders/DemoDataSeeder.php | 73 + .../seeders/RolesAndPermissionsSeeder.php | 80 + database/seeders/UserSeeder.php | 61 + deploy.sh | 111 + docker-compose.override.yml | 10 + docker-compose.yml | 76 + docker/entrypoint.sh | 95 + docker/nginx/default.conf | 78 + docker/nginx/nginx.conf | 48 + docker/php/opcache-dev.ini | 2 + docker/php/php.ini | 61 + docker/php/www.conf | 44 + docker/supervisor/supervisord.conf | 52 + lang/ms/validation.php | 37 + package-lock.json | 1428 +++ package.json | 20 + phpunit.xml | 36 + public/.htaccess | 25 + public/favicon.ico | 0 public/index.php | 20 + public/robots.txt | 2 + resources/js/app.js | 25 + resources/sass/app.scss | 76 + resources/views/audit/index.blade.php | 38 + resources/views/auth/login.blade.php | 51 + .../templates/_formula_modal.blade.php | 23 + .../checklist/templates/_item_modal.blade.php | 49 + .../templates/_section_modal.blade.php | 17 + .../checklist/templates/builder.blade.php | 175 + .../checklist/templates/create.blade.php | 18 + .../views/checklist/templates/index.blade.php | 35 + resources/views/consultants/_form.blade.php | 63 + resources/views/consultants/create.blade.php | 12 + resources/views/consultants/edit.blade.php | 13 + resources/views/consultants/index.blade.php | 55 + resources/views/consultants/show.blade.php | 63 + .../views/dashboard/consultant.blade.php | 72 + resources/views/dashboard/jpp.blade.php | 96 + resources/views/data_centres/_form.blade.php | 40 + resources/views/data_centres/create.blade.php | 9 + resources/views/data_centres/edit.blade.php | 9 + resources/views/data_centres/index.blade.php | 47 + resources/views/data_centres/show.blade.php | 93 + resources/views/layouts/app.blade.php | 75 + resources/views/layouts/flash.blade.php | 25 + resources/views/layouts/sidebar.blade.php | 98 + resources/views/reminders/index.blade.php | 60 + .../views/reporting_cycles/_form.blade.php | 51 + .../views/reporting_cycles/create.blade.php | 9 + .../views/reporting_cycles/edit.blade.php | 14 + .../views/reporting_cycles/index.blade.php | 40 + resources/views/reports/index.blade.php | 69 + resources/views/settings/edit.blade.php | 34 + .../views/submissions/_comments.blade.php | 39 + .../submissions/_corrections_panel.blade.php | 55 + .../submissions/_documents_panel.blade.php | 32 + .../views/submissions/_item_field.blade.php | 46 + .../submissions/_results_panel.blade.php | 17 + .../views/submissions/_review_panel.blade.php | 87 + resources/views/submissions/create.blade.php | 35 + resources/views/submissions/fill.blade.php | 78 + resources/views/submissions/index.blade.php | 57 + resources/views/submissions/show.blade.php | 111 + resources/views/users/_form.blade.php | 17 + resources/views/users/create.blade.php | 7 + resources/views/users/edit.blade.php | 7 + resources/views/users/index.blade.php | 39 + resources/views/welcome.blade.php | 223 + routes/console.php | 8 + routes/web.php | 123 + storage/app/.gitignore | 4 + storage/app/private/.gitignore | 2 + storage/app/public/.gitignore | 2 + storage/framework/.gitignore | 9 + storage/framework/cache/.gitignore | 3 + storage/framework/cache/data/.gitignore | 2 + storage/framework/sessions/.gitignore | 2 + storage/framework/testing/.gitignore | 2 + storage/framework/views/.gitignore | 2 + storage/logs/.gitignore | 2 + tests/Feature/AssignmentTest.php | 74 + tests/Feature/ConsultantManagementTest.php | 65 + tests/Feature/ExampleTest.php | 20 + tests/Feature/FormulaTest.php | 59 + tests/Feature/ReminderTest.php | 64 + tests/Feature/ReportingCycleTest.php | 42 + tests/Feature/SubmissionTest.php | 218 + tests/TestCase.php | 34 + tests/Unit/ExampleTest.php | 16 + vite.config.js | 16 + 201 files changed, 21356 insertions(+) create mode 100644 .claude/settings.json create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.docker create mode 100644 .env.example create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 app/Http/Controllers/AuditLogController.php create mode 100644 app/Http/Controllers/Auth/LoginController.php create mode 100644 app/Http/Controllers/ChecklistBuilderController.php create mode 100644 app/Http/Controllers/ChecklistTemplateController.php create mode 100644 app/Http/Controllers/CommentController.php create mode 100644 app/Http/Controllers/ConsultantController.php create mode 100644 app/Http/Controllers/Controller.php create mode 100644 app/Http/Controllers/CorrectionRequestController.php create mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 app/Http/Controllers/DataCentreController.php create mode 100644 app/Http/Controllers/DocumentController.php create mode 100644 app/Http/Controllers/ReminderController.php create mode 100644 app/Http/Controllers/ReportController.php create mode 100644 app/Http/Controllers/ReportingCycleController.php create mode 100644 app/Http/Controllers/SettingController.php create mode 100644 app/Http/Controllers/SubmissionController.php create mode 100644 app/Http/Controllers/SubmissionReviewController.php create mode 100644 app/Http/Controllers/UserController.php create mode 100644 app/Http/Requests/ConsultantRequest.php create mode 100644 app/Http/Requests/DataCentreRequest.php create mode 100644 app/Http/Requests/ReportingCycleRequest.php create mode 100644 app/Models/AppSetting.php create mode 100644 app/Models/ChecklistFormula.php create mode 100644 app/Models/ChecklistItem.php create mode 100644 app/Models/ChecklistScope.php create mode 100644 app/Models/ChecklistSection.php create mode 100644 app/Models/ChecklistTemplate.php create mode 100644 app/Models/Consultant.php create mode 100644 app/Models/DataCentre.php create mode 100644 app/Models/DataCentreConsultantAssignment.php create mode 100644 app/Models/ReportingCycle.php create mode 100644 app/Models/Submission.php create mode 100644 app/Models/SubmissionAnswer.php create mode 100644 app/Models/SubmissionComment.php create mode 100644 app/Models/SubmissionCorrectionRequest.php create mode 100644 app/Models/SubmissionDocument.php create mode 100644 app/Models/SubmissionResult.php create mode 100644 app/Models/SubmissionStatusHistory.php create mode 100644 app/Models/User.php create mode 100644 app/Policies/ChecklistTemplatePolicy.php create mode 100644 app/Policies/ConsultantPolicy.php create mode 100644 app/Policies/DataCentrePolicy.php create mode 100644 app/Policies/ReportingCyclePolicy.php create mode 100644 app/Policies/SubmissionPolicy.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Services/AssignmentService.php create mode 100644 app/Services/DocumentService.php create mode 100644 app/Services/ExpressionEvaluator.php create mode 100644 app/Services/FormulaService.php create mode 100644 app/Services/ReportingCycleService.php create mode 100644 app/Services/SubmissionWorkflowService.php create mode 100644 artisan create mode 100644 bootstrap/app.php create mode 100644 bootstrap/cache/.gitignore create mode 100644 bootstrap/providers.php create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 config/activitylog.php create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/filesystems.php create mode 100644 config/logging.php create mode 100644 config/mail.php create mode 100644 config/mbip.php create mode 100644 config/permission.php create mode 100644 config/queue.php create mode 100644 config/services.php create mode 100644 config/session.php create mode 100644 database/.gitignore create mode 100644 database/factories/ConsultantFactory.php create mode 100644 database/factories/DataCentreFactory.php create mode 100644 database/factories/ReportingCycleFactory.php create mode 100644 database/factories/UserFactory.php create mode 100644 database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/2026_06_23_083811_create_permission_tables.php create mode 100644 database/migrations/2026_06_23_083812_create_activity_log_table.php create mode 100644 database/migrations/2026_06_23_090001_add_fields_to_users_table.php create mode 100644 database/migrations/2026_06_23_090002_create_consultants_table.php create mode 100644 database/migrations/2026_06_23_090003_create_data_centres_table.php create mode 100644 database/migrations/2026_06_23_090004_create_data_centre_consultant_assignments_table.php create mode 100644 database/migrations/2026_06_23_090005_create_checklist_templates_table.php create mode 100644 database/migrations/2026_06_23_090006_create_checklist_sections_table.php create mode 100644 database/migrations/2026_06_23_090007_create_checklist_scopes_table.php create mode 100644 database/migrations/2026_06_23_090008_create_checklist_items_table.php create mode 100644 database/migrations/2026_06_23_090009_create_checklist_formulas_table.php create mode 100644 database/migrations/2026_06_23_090012_create_reporting_cycles_table.php create mode 100644 database/migrations/2026_06_23_090013_create_submissions_table.php create mode 100644 database/migrations/2026_06_23_090014_create_submission_answers_table.php create mode 100644 database/migrations/2026_06_23_090015_create_submission_results_table.php create mode 100644 database/migrations/2026_06_23_090016_create_submission_documents_table.php create mode 100644 database/migrations/2026_06_23_090017_create_submission_status_histories_table.php create mode 100644 database/migrations/2026_06_23_090018_create_submission_correction_requests_table.php create mode 100644 database/migrations/2026_06_23_090019_create_submission_comments_table.php create mode 100644 database/migrations/2026_06_23_090020_create_app_settings_table.php create mode 100644 database/seeders/AppSettingsSeeder.php create mode 100644 database/seeders/ChecklistTemplateSeeder.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 database/seeders/DemoDataSeeder.php create mode 100644 database/seeders/RolesAndPermissionsSeeder.php create mode 100644 database/seeders/UserSeeder.php create mode 100644 deploy.sh create mode 100644 docker-compose.override.yml create mode 100644 docker-compose.yml create mode 100644 docker/entrypoint.sh create mode 100644 docker/nginx/default.conf create mode 100644 docker/nginx/nginx.conf create mode 100644 docker/php/opcache-dev.ini create mode 100644 docker/php/php.ini create mode 100644 docker/php/www.conf create mode 100644 docker/supervisor/supervisord.conf create mode 100644 lang/ms/validation.php create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 phpunit.xml create mode 100644 public/.htaccess create mode 100644 public/favicon.ico create mode 100644 public/index.php create mode 100644 public/robots.txt create mode 100644 resources/js/app.js create mode 100644 resources/sass/app.scss create mode 100644 resources/views/audit/index.blade.php create mode 100644 resources/views/auth/login.blade.php create mode 100644 resources/views/checklist/templates/_formula_modal.blade.php create mode 100644 resources/views/checklist/templates/_item_modal.blade.php create mode 100644 resources/views/checklist/templates/_section_modal.blade.php create mode 100644 resources/views/checklist/templates/builder.blade.php create mode 100644 resources/views/checklist/templates/create.blade.php create mode 100644 resources/views/checklist/templates/index.blade.php create mode 100644 resources/views/consultants/_form.blade.php create mode 100644 resources/views/consultants/create.blade.php create mode 100644 resources/views/consultants/edit.blade.php create mode 100644 resources/views/consultants/index.blade.php create mode 100644 resources/views/consultants/show.blade.php create mode 100644 resources/views/dashboard/consultant.blade.php create mode 100644 resources/views/dashboard/jpp.blade.php create mode 100644 resources/views/data_centres/_form.blade.php create mode 100644 resources/views/data_centres/create.blade.php create mode 100644 resources/views/data_centres/edit.blade.php create mode 100644 resources/views/data_centres/index.blade.php create mode 100644 resources/views/data_centres/show.blade.php create mode 100644 resources/views/layouts/app.blade.php create mode 100644 resources/views/layouts/flash.blade.php create mode 100644 resources/views/layouts/sidebar.blade.php create mode 100644 resources/views/reminders/index.blade.php create mode 100644 resources/views/reporting_cycles/_form.blade.php create mode 100644 resources/views/reporting_cycles/create.blade.php create mode 100644 resources/views/reporting_cycles/edit.blade.php create mode 100644 resources/views/reporting_cycles/index.blade.php create mode 100644 resources/views/reports/index.blade.php create mode 100644 resources/views/settings/edit.blade.php create mode 100644 resources/views/submissions/_comments.blade.php create mode 100644 resources/views/submissions/_corrections_panel.blade.php create mode 100644 resources/views/submissions/_documents_panel.blade.php create mode 100644 resources/views/submissions/_item_field.blade.php create mode 100644 resources/views/submissions/_results_panel.blade.php create mode 100644 resources/views/submissions/_review_panel.blade.php create mode 100644 resources/views/submissions/create.blade.php create mode 100644 resources/views/submissions/fill.blade.php create mode 100644 resources/views/submissions/index.blade.php create mode 100644 resources/views/submissions/show.blade.php create mode 100644 resources/views/users/_form.blade.php create mode 100644 resources/views/users/create.blade.php create mode 100644 resources/views/users/edit.blade.php create mode 100644 resources/views/users/index.blade.php create mode 100644 resources/views/welcome.blade.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 storage/app/.gitignore create mode 100644 storage/app/private/.gitignore create mode 100644 storage/app/public/.gitignore create mode 100644 storage/framework/.gitignore create mode 100644 storage/framework/cache/.gitignore create mode 100644 storage/framework/cache/data/.gitignore create mode 100644 storage/framework/sessions/.gitignore create mode 100644 storage/framework/testing/.gitignore create mode 100644 storage/framework/views/.gitignore create mode 100644 storage/logs/.gitignore create mode 100644 tests/Feature/AssignmentTest.php create mode 100644 tests/Feature/ConsultantManagementTest.php create mode 100644 tests/Feature/ExampleTest.php create mode 100644 tests/Feature/FormulaTest.php create mode 100644 tests/Feature/ReminderTest.php create mode 100644 tests/Feature/ReportingCycleTest.php create mode 100644 tests/Feature/SubmissionTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/Unit/ExampleTest.php create mode 100644 vite.config.js diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..4e9bf4a --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(php -v)", + "Bash(npm -v)", + "Bash(mysql --version)", + "Bash(php -r ' *)", + "Bash(php datacenter_tmp/artisan --version)", + "Bash(npm run *)", + "Bash(curl -s -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:8741/login)", + "Bash(curl -s -o /dev/null -w \"HTTP %{http_code}\\\\n\" http://127.0.0.1:8741/)", + "Bash(curl -s http://127.0.0.1:8741/login)" + ] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..87cd38f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,55 @@ +# ───────────────────────────────────────────────────────────── +# .dockerignore — kecualikan fail daripada konteks binaan Docker +# ───────────────────────────────────────────────────────────── + +# Kawalan versi +.git +.gitignore +.gitattributes + +# Persekitaran dev tempatan +.env +.env.* +!.env.docker + +# Node modules (dibina semula / pada host) +node_modules + +# Vendor (dipasang dalam image / entrypoint) +vendor + +# Aset binaan dari mesin tempatan (dibina semula oleh deploy.sh) +public/hot + +# Bootstrap cache — mungkin merujuk provider dev sahaja +bootstrap/cache/ + +# Storage (dilekap sebagai volume Docker) +storage/app +storage/logs +storage/framework/cache +storage/framework/sessions +storage/framework/views + +# Fail docker compose (tidak diperlukan dalam image) +docker-compose.yml +docker-compose.override.yml + +# Fail dev / ujian +.phpunit.cache +phpunit.xml +tests/ +.editorconfig + +# Fail IDE / OS +.idea +.vscode +*.code-workspace +.DS_Store +Thumbs.db + +# Log +*.log + +# Cache composer +.composer diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6df8428 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 + +[{compose,docker-compose}.{yml,yaml}] +indent_size = 4 diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..d772cd2 --- /dev/null +++ b/.env.docker @@ -0,0 +1,58 @@ +# ───────────────────────────────────────────────────────────── +# MBIP Pusat Data — Docker Production Environment +# Salin fail ini ke .env sebelum deploy: cp .env.docker .env +# Kemudian isi APP_KEY, DB_USERNAME, DB_PASSWORD, APP_URL. +# ───────────────────────────────────────────────────────────── + +APP_NAME="Sistem Pemantauan Kemampanan Pusat Data MBIP" +APP_ENV=production +APP_KEY= # Jana: php artisan key:generate --show +APP_DEBUG=false +APP_URL=http://your-server-ip-or-domain +APP_TIMEZONE=Asia/Kuala_Lumpur + +APP_LOCALE=ms +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=ms_MY + +# ── Pangkalan data (MySQL luaran pada host/LAN) ────────────── +# host.docker.internal = MySQL berjalan pada host Ubuntu yang sama. +# Tukar kepada IP server MySQL jika berasingan (cth: 172.17.200.16). +DB_CONNECTION=mysql +DB_HOST=host.docker.internal +DB_PORT=3306 +DB_DATABASE=mbip_datacenter +DB_USERNAME=root +DB_PASSWORD= + +# ── Sesi / Cache / Queue ────────────────────────────────────── +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +CACHE_STORE=database +QUEUE_CONNECTION=database +BROADCAST_CONNECTION=log + +# ── Storan fail ─────────────────────────────────────────────── +FILESYSTEM_DISK=local + +# ── Logging ─────────────────────────────────────────────────── +LOG_CHANNEL=stack +LOG_STACK=daily +LOG_LEVEL=error +LOG_DEPRECATIONS_CHANNEL=null + +# ── Mel (kemas kini jika menghantar emel) ──────────────────── +MAIL_MAILER=log +MAIL_FROM_ADDRESS="no-reply@mbip.gov.my" +MAIL_FROM_NAME="${APP_NAME}" + +# ── Tetapan MBIP ────────────────────────────────────────────── +REMINDER_EMAIL_ENABLED=false +MAX_UPLOAD_SIZE_KB=20480 + +# ── Pemetaan port Nginx (host:container) ───────────────────── +NGINX_HTTP_PORT=8010 diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f872831 --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +APP_NAME="Sistem Pemantauan Kemampanan Pusat Data MBIP" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_URL=http://localhost + +APP_LOCALE=ms +APP_FALLBACK_LOCALE=en +APP_FAKER_LOCALE=ms_MY + +APP_MAINTENANCE_DRIVER=file +# APP_MAINTENANCE_STORE=database + +# PHP_CLI_SERVER_WORKERS=4 + +BCRYPT_ROUNDS=12 + +LOG_CHANNEL=stack +LOG_STACK=single +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=mbip_datacenter +DB_USERNAME=root +DB_PASSWORD= + +SESSION_DRIVER=database +SESSION_LIFETIME=120 +SESSION_ENCRYPT=false +SESSION_PATH=/ +SESSION_DOMAIN=null + +BROADCAST_CONNECTION=log +FILESYSTEM_DISK=local +QUEUE_CONNECTION=database + +CACHE_STORE=database +# CACHE_PREFIX= + +MEMCACHED_HOST=127.0.0.1 + +REDIS_CLIENT=phpredis +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +MAIL_MAILER=log +MAIL_SCHEME=null +MAIL_HOST=127.0.0.1 +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_FROM_ADDRESS="hello@example.com" +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= +AWS_USE_PATH_STYLE_ENDPOINT=false + +VITE_APP_NAME="${APP_NAME}" + +# Tetapan MBIP +REMINDER_EMAIL_ENABLED=false +MAX_UPLOAD_SIZE_KB=20480 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..fcb21d3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +* text=auto eol=lf + +*.blade.php diff=html +*.css diff=css +*.html diff=html +*.md diff=markdown +*.php diff=php + +/.github export-ignore +CHANGELOG.md export-ignore +.styleci.yml export-ignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7be55e2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +*.log +.DS_Store +.env +.env.backup +.env.production +.phpactor.json +.phpunit.result.cache +/.codex +/.cursor/ +/.idea +/.nova +/.phpunit.cache +/.vscode +/.zed +/auth.json +/node_modules +/public/build +/public/fonts-manifest.dev.json +/public/hot +/public/storage +/storage/*.key +/storage/pail +/vendor +_ide_helper.php +Homestead.json +Homestead.yaml +Thumbs.db diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..495a6af --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +ignore-scripts=true +audit=true diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..696792b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# ───────────────────────────────────────────────────────────── +# Production image — PHP 8.4 FPM · Debian Bookworm +# Sistem Pemantauan Kemampanan Pusat Data MBIP +# +# Extensions sedia ada dalam php:8.4-fpm-bookworm — JANGAN pasang +# semula (mencetuskan recompile + header hilang): +# ctype · curl · dom · fileinfo · json · openssl · pcre +# posix · readline · session · simplexml · sodium +# tokenizer · xml · xmlreader · xmlwriter · zlib +# ───────────────────────────────────────────────────────────── +FROM php:8.4-fpm-bookworm + +LABEL maintainer="MBIP" +LABEL description="Sistem Pemantauan Kemampanan Pusat Data MBIP — PHP 8.4 FPM" + +ENV TZ=Asia/Kuala_Lumpur + +# ── System libs + timezone ─────────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + tzdata \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ + libwebp-dev \ + libzip-dev \ + libicu-dev \ + libonig-dev \ + default-mysql-client \ + supervisor \ + unzip \ + curl \ + && ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime \ + && echo ${TZ} > /etc/timezone \ + && rm -rf /var/lib/apt/lists/* + +# ── Configure GD ───────────────────────────────────────────── +RUN docker-php-ext-configure gd \ + --with-freetype \ + --with-jpeg \ + --with-webp + +# ── Install extensions ──────────────────────────────────────── +RUN docker-php-ext-install -j$(nproc) \ + bcmath \ + exif \ + gd \ + intl \ + mbstring \ + mysqli \ + opcache \ + pcntl \ + pdo_mysql \ + zip + +# ── Composer ────────────────────────────────────────────────── +COPY --from=composer:2 /usr/bin/composer /usr/bin/composer + +# ── PHP + FPM config ───────────────────────────────────────── +COPY docker/php/php.ini $PHP_INI_DIR/conf.d/99-mbip.ini +COPY docker/php/www.conf /usr/local/etc/php-fpm.d/www.conf + +# ── Supervisor ──────────────────────────────────────────────── +COPY docker/supervisor/supervisord.conf /etc/supervisor/conf.d/supervisord.conf + +# ── Cipta direktori yang diperlukan ────────────────────────── +RUN mkdir -p \ + /var/www/html/bootstrap/cache \ + /var/www/html/storage/framework/cache/data \ + /var/www/html/storage/framework/sessions \ + /var/www/html/storage/framework/testing \ + /var/www/html/storage/framework/views \ + /var/www/html/storage/app/public \ + /var/www/html/storage/app/private/reports \ + /var/www/html/storage/logs \ + /var/log/php-fpm \ + /var/log/supervisor + +# ── Entrypoint ──────────────────────────────────────────────── +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +WORKDIR /var/www/html + +EXPOSE 9000 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..037319f --- /dev/null +++ b/README.md @@ -0,0 +1,282 @@ +# Sistem Pemantauan Kemampanan Pusat Data MBIP + +Aplikasi web untuk **Majlis Bandaraya Iskandar Puteri (MBIP)** bagi memantau kemampanan dan pelaporan karbon pusat data dalam kawasan pentadbiran MBIP. Sistem ini membantu JPP memantau penggunaan karbon, menyemak laporan perunding, merancang program/projek pengurangan karbon, dan menguruskan pelaporan pembaharuan lesen tahunan. + +Dibina dengan **Laravel 13 + MySQL + Blade + Bootstrap 5 + jQuery (Vite)**. + +--- + +## Kandungan +- [Ciri Utama](#ciri-utama) +- [Keperluan Sistem](#keperluan-sistem) +- [Pemasangan](#pemasangan) +- [Akaun Contoh](#akaun-contoh) +- [Peraturan Perniagaan Penting](#peraturan-perniagaan-penting) +- [Senarai Semak Dinamik & Enjin Formula](#senarai-semak-dinamik--enjin-formula) +- [Peranan & Kebenaran](#peranan--kebenaran) +- [Keselamatan & Jejak Audit](#keselamatan--jejak-audit) +- [Ujian](#ujian) +- [Struktur Pangkalan Data](#struktur-pangkalan-data) + +--- + +## Ciri Utama + +1. **Autentikasi & Kawalan Akses** — log masuk/keluar, papan pemuka mengikut peranan, polisi & kebenaran, jejak audit penuh. +2. **Pengurusan Perunding** — JPP mendaftar akaun perunding; satu perunding boleh urus banyak pusat data. +3. **Pengurusan Pusat Data** — maklumat pusat data, perunding semasa, sejarah penugasan. +4. **Kitaran Pelaporan / Pembaharuan Lesen** — tahun pelaporan dikira automatik, tarikh tutup boleh dikonfigur. +5. **Pembina Templat Senarai Semak Dinamik** — JPP boleh tambah/ubah/buang seksyen, skop, item & formula **tanpa perubahan kod**. +6. **Modul Serahan Perunding** — isi senarai semak, muat naik PDF, simpan draf, hantar muktamad, jawab pembetulan. +7. **Muat Naik Dokumen & Versi** — PDF sahaja, hash fail, sejarah versi, muat turun dilindungi polisi. +8. **Aliran Kerja Semakan JPP** — tukar status, tugaskan pegawai, kunci/buka kunci, salinan keras, permohonan pembetulan. +9. **Modul Komen/Perbualan** — perbualan setiap serahan, mesej dalaman/luaran, soft delete dengan rekod audit. +10. **Senarai Peringatan** — pusat data yang belum serah untuk kitaran aktif, sorotan lampau tempoh. +11. **Papan Pemuka & Laporan** — statistik karbon A/B/C/D/E mengikut tahun, trend, pecahan status. +12. **Tetapan & Log Audit** — konfigurasi tarikh tutup lalai, had muat naik, toggle peringatan emel. + +--- + +## Keperluan Sistem + +- PHP >= 8.3 (diuji pada PHP 8.5) +- Composer 2.x +- MySQL 8.x +- Node.js >= 18 & NPM (untuk binaan aset Vite) + +--- + +## Pemasangan + +```bash +# 1. Pasang kebergantungan PHP +composer install + +# 2. Pasang & bina aset frontend (Bootstrap 5 + jQuery via Vite) +npm install +npm run build # atau `npm run dev` semasa pembangunan + +# 3. Fail persekitaran +cp .env.example .env +php artisan key:generate +``` + +Kemas kini tetapan pangkalan data dalam `.env`: + +```dotenv +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=mbip_datacenter +DB_USERNAME=root +DB_PASSWORD=1234 +``` + +```bash +# 4. Cipta pangkalan data (jika belum) +# CREATE DATABASE mbip_datacenter CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +# 5. Migrasi + semai data (peranan, pengguna contoh, templat senarai semak, data demo) +php artisan migrate:fresh --seed + +# 6. Pautan storan (laporan PDF disimpan di storage/app/private) +php artisan storage:link + +# 7. Jalankan pelayan +php artisan serve +``` + +Lawati `http://127.0.0.1:8000` dan log masuk. + +> **Nota muat naik fail:** Laporan PDF disimpan pada disk `local` (`storage/app/private/reports/…`), **di luar** direktori awam. Ia hanya boleh dimuat turun melalui laluan terlindung yang dikuatkuasakan oleh polisi. + +--- + +## Akaun Contoh + +Semua kata laluan: **`password`** + +| Peranan | Emel | +|---|---| +| Super Admin | superadmin@mbip.gov.my | +| JPP Admin | jppadmin@mbip.gov.my | +| Pegawai JPP | pegawai@mbip.gov.my | +| Penolong Pegawai | penolong@mbip.gov.my | +| Kerani | kerani@mbip.gov.my | +| Perunding | perunding@example.com | + +--- + +## Peraturan Perniagaan Penting + +1. Pembaharuan lesen dibuat **pada/sebelum 1 Disember** setiap tahun (tarikh tutup boleh dikonfigur). +2. Tempoh lesen ialah **1 tahun**. +3. Perunding mesti serahkan bacaan/laporan karbon **Januari–Disember** bagi tahun pelaporan. +4. **Tahun pelaporan = Tahun pembaharuan − 2.** Contoh: pembaharuan **2028** ⇒ data pelaporan **2026**. +5. JPP boleh konfigur tarikh tutup tahunan (Tetapan → Kitaran). +6. Perunding mesti isi borang senarai semak **dan** muat naik laporan PDF sebelum pembaharuan diteruskan. +7. Serahan pertama diperlukan sebelum CCC diperoleh; serahan tahunan selepas itu. + +--- + +## Senarai Semak Dinamik & Enjin Formula + +Senarai semak **tidak ditetapkan dalam kod**. Struktur disimpan secara relasi: + +``` +checklist_templates -> checklist_sections -> checklist_scopes -> checklist_items + -> checklist_formulas (A = SKOP1 + SKOP2 + SKOP3, dsb.) +``` + +Setiap item boleh diberi `formula_token` (cth: `SKOP1`, `EE`, `RE`, `D4I`). Formula seksyen merujuk token ini dan token hasil seksyen terdahulu, dinilai mengikut turutan: + +| Token | Formula | Maksud | +|---|---|---| +| A | `SKOP1 + SKOP2 + SKOP3` | Jumlah pelepasan karbon | +| B | `EE + RE` | Mekanisme pengurangan | +| C | `A - B` | Baki karbon | +| D | `A - D4I - D4II` | Bersih karbon | +| E | `D / A * 100` | % pengurangan (dengan perlindungan bahagi sifar) | + +Formula dinilai oleh `App\Services\ExpressionEvaluator` — sebuah **parser aritmetik selamat** (recursive descent, menyokong `+ - * / ( )` dan pemboleh ubah token). Ia **tidak** menggunakan `eval()`, jadi formula yang dikonfigur pengguna JPP adalah selamat. Hasil disimpan dalam `submission_results` untuk pengagregatan papan pemuka. + +JPP boleh ubah semua ini melalui **Pembina Templat Senarai Semak** (menu sisi → Templat Senarai Semak → Bina). + +--- + +## Peranan & Kebenaran + +Menggunakan `spatie/laravel-permission`. Super Admin memintas semua semakan melalui `Gate::before`. + +| Kebenaran | JPP Admin | Pegawai JPP | Penolong | Kerani | Perunding | +|---|:--:|:--:|:--:|:--:|:--:| +| Urus perunding | ✔ | ✔ | | | | +| Urus pusat data | ✔ | ✔ | ✔ | | | +| Urus penugasan | ✔ | ✔ | | | | +| Urus kitaran | ✔ | ✔ | | | | +| Urus templat senarai semak | ✔ | | | | | +| Lihat semua serahan | ✔ | ✔ | ✔ | ✔ | | +| Semak serahan | ✔ | ✔ | ✔ | | | +| Kunci/buka kunci | ✔ | ✔ | | | | +| Tanda salinan keras | ✔ | ✔ | ✔ | ✔ | | +| Urus serahan sendiri | | | | | ✔ | +| Laporan & statistik | ✔ | ✔ | ✔ | | | +| Senarai peringatan | ✔ | ✔ | ✔ | ✔ | | +| Urus pengguna & tetapan | ✔ | | | | | + +Perunding hanya boleh melihat pusat data & serahan yang **ditugaskan kepadanya** (dikuatkuasakan oleh `SubmissionPolicy` & `DataCentrePolicy`). + +--- + +## Keselamatan & Jejak Audit + +- Validasi muat naik **PDF sahaja** (`mimes:pdf` + `mimetypes:application/pdf`) dengan had saiz boleh dikonfigur. +- Fail disimpan di luar laluan awam; muat turun melalui laluan terlindung + polisi. +- Setiap paparan/muat turun/tindakan diberi kuasa melalui polisi. +- Jejak audit (`spatie/laravel-activitylog`) bagi: log masuk, perubahan status, kunci/buka kunci, muat naik dokumen, permohonan pembetulan, penugasan perunding, padam komen. +- Soft delete pada model utama; komen hanya soft delete (rekod audit dikekalkan). +- Perlindungan CSRF, pengesahan sisi pelayan, indeks pangkalan data pada kunci asing/status/kitaran/perunding/pusat data. + +--- + +## Ujian + +Ujian ciri menggunakan SQLite dalam-memori (lihat `phpunit.xml`). + +```bash +php artisan test +``` + +Liputan ujian utama: + +- Perunding tidak boleh akses pusat data perunding lain +- JPP boleh cipta perunding (+ akaun log masuk) +- JPP boleh tugaskan perunding kepada pusat data +- Hanya satu perunding aktif setiap pusat data (sejarah dikekalkan) +- Tahun pelaporan dikira betul daripada tahun pembaharuan +- Perunding boleh hantar senarai semak + PDF (tidak boleh hantar tanpa PDF) +- Perunding tidak boleh edit serahan yang dikunci +- JPP tukar status ke "pembetulan perunding" hanya dengan butiran pembetulan +- Status "selesai" memerlukan tarikh hardcopy dihantar +- Senarai peringatan mengeluarkan perunding selepas serahan +- Pengiraan formula A, B, C, D, E (+ perlindungan bahagi sifar + keutamaan operator) + +--- + +## Struktur Pangkalan Data + +`users`, `roles`/`permissions` (spatie), `consultants`, `data_centres`, `data_centre_consultant_assignments`, `reporting_cycles`, `checklist_templates`, `checklist_sections`, `checklist_scopes`, `checklist_items`, `checklist_formulas`, `submissions`, `submission_answers`, `submission_results`, `submission_documents`, `submission_status_histories`, `submission_correction_requests`, `submission_comments`, `app_settings`, `activity_log`. + +--- + +## Pengezanan Docker (Produksi Ubuntu) + +Setup Docker mengikut konvensyen projek MBIP yang terbukti (rujukan: `spr2026`): +**PHP 8.4 FPM + Nginx 1.27 (Alpine) + Supervisor**, menyambung ke **MySQL luaran** pada host/LAN. + +### Fail berkaitan +``` +Dockerfile # imej PHP 8.4 FPM (Debian Bookworm) +docker-compose.yml # servis app + nginx, volume, network +docker-compose.override.yml # override pembangunan (auto-dimuat) +.dockerignore +.env.docker # templat env produksi +deploy.sh # skrip deploy/kemas kini +docker/ +├── entrypoint.sh # tunggu MySQL, migrate, seed, cache +├── nginx/{nginx.conf,default.conf} +├── php/{php.ini,www.conf,opcache-dev.ini} +└── supervisor/supervisord.conf # php-fpm + queue worker + scheduler +``` + +### Langkah deploy pertama (di server Ubuntu) +```bash +# 1. Klon kod & masuk direktori +git clone mbip-datacenter && cd mbip-datacenter + +# 2. Sediakan env produksi +cp .env.docker .env +php artisan key:generate --show # salin nilai ke APP_KEY dalam .env +nano .env # isi APP_URL, DB_USERNAME, DB_PASSWORD, DB_HOST + +# 3. Pastikan pangkalan data MySQL wujud (pada host/LAN) +# CREATE DATABASE mbip_datacenter CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +# 4. Deploy (bina imej + aset + naik container + migrate + seed automatik) +chmod +x deploy.sh +./deploy.sh --build +``` + +Container `app` (PHP-FPM) hanya didedahkan kepada `nginx` dalaman; Nginx didedahkan pada +`NGINX_HTTP_PORT` (lalai **8010**). Letakkan reverse proxy (Nginx/Apache host atau Cloudflare) +untuk TLS/443 di hadapan port ini. + +### Kemas kini (deploy seterusnya) +```bash +./deploy.sh # git pull + npm build + up + migrate + optimize +./deploy.sh --build # paksa bina semula imej (selepas tukar Dockerfile) +./deploy.sh --fresh # migrate:fresh --seed (AMARAN: kosongkan data) +./deploy.sh --logs # ikut log container +./deploy.sh --down # hentikan container +``` + +### Nota +- **MySQL luaran:** `DB_HOST=host.docker.internal` menyambung ke MySQL pada host Ubuntu yang sama + (dipetakan melalui `extra_hosts: host-gateway`). Tukar kepada IP pelayan MySQL jika berasingan. +- **Aset Vite** dibina pada host oleh `deploy.sh` (`npm run build`) dan dihidang terus oleh Nginx + daripada `./public` (lekapan baca-sahaja) — tiada langkah build aset dalam imej. +- **Laporan PDF** disimpan dalam volume `mbip_dc_storage` (`storage/app/private`), kekal merentas + deploy, di luar laluan awam. +- `entrypoint.sh` automatik menunggu MySQL, jalankan migrasi, semai data kali pertama, dan bina + semula cache pada setiap permulaan container. +- Supervisor menjalankan **php-fpm + queue worker + scheduler** dalam container `app`. + +--- + +## Binaan Aset (Build) + +```bash +npm run build # binaan produksi aset (manifest dalam public/build) +``` + +© Majlis Bandaraya Iskandar Puteri (MBIP). diff --git a/app/Http/Controllers/AuditLogController.php b/app/Http/Controllers/AuditLogController.php new file mode 100644 index 0000000..fffc48d --- /dev/null +++ b/app/Http/Controllers/AuditLogController.php @@ -0,0 +1,22 @@ +user()->can('pengguna.urus') || auth()->user()->hasRole('Super Admin'), 403); + + $logs = Activity::with('causer') + ->when($request->filled('log'), fn ($q) => $q->where('log_name', $request->log)) + ->latest()->paginate(30)->withQueryString(); + + $logNames = Activity::query()->distinct()->pluck('log_name')->filter()->values(); + + return view('audit.index', compact('logs', 'logNames')); + } +} diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php new file mode 100644 index 0000000..f1c1002 --- /dev/null +++ b/app/Http/Controllers/Auth/LoginController.php @@ -0,0 +1,69 @@ +route('dashboard'); + } + + return view('auth.login'); + } + + public function login(Request $request) + { + $credentials = $request->validate([ + 'email' => ['required', 'email'], + 'password' => ['required', 'string'], + ], [], [ + 'email' => 'emel', + 'password' => 'kata laluan', + ]); + + $remember = $request->boolean('remember'); + + if (! Auth::attempt($credentials, $remember)) { + activity('auth')->withProperties(['email' => $request->email])->log('Cubaan log masuk gagal'); + + throw ValidationException::withMessages([ + 'email' => 'Emel atau kata laluan tidak sah.', + ]); + } + + $user = Auth::user(); + + if (! $user->is_active) { + Auth::logout(); + + throw ValidationException::withMessages([ + 'email' => 'Akaun anda telah dinyahaktifkan. Sila hubungi pentadbir.', + ]); + } + + $request->session()->regenerate(); + + activity('auth')->causedBy($user)->log('Pengguna log masuk'); + + return redirect()->intended(route('dashboard')); + } + + public function logout(Request $request) + { + $user = Auth::user(); + activity('auth')->causedBy($user)->log('Pengguna log keluar'); + + Auth::logout(); + $request->session()->invalidate(); + $request->session()->regenerateToken(); + + return redirect()->route('login'); + } +} diff --git a/app/Http/Controllers/ChecklistBuilderController.php b/app/Http/Controllers/ChecklistBuilderController.php new file mode 100644 index 0000000..dcf2c92 --- /dev/null +++ b/app/Http/Controllers/ChecklistBuilderController.php @@ -0,0 +1,188 @@ +validateSection($request); + $template->sections()->create($data); + + return back()->with('success', 'Seksyen ditambah.'); + } + + public function updateSection(Request $request, ChecklistSection $section) + { + $section->update($this->validateSection($request)); + + return back()->with('success', 'Seksyen dikemas kini.'); + } + + public function destroySection(ChecklistSection $section) + { + $section->delete(); + + return back()->with('success', 'Seksyen dipadam.'); + } + + protected function validateSection(Request $request): array + { + $data = $request->validate([ + 'code' => ['required', 'string', 'max:50'], + 'title' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string', 'max:1000'], + 'order' => ['nullable', 'integer', 'min:0'], + 'is_formula_enabled' => ['nullable', 'boolean'], + ]); + $data['is_formula_enabled'] = $request->boolean('is_formula_enabled'); + $data['order'] ??= 0; + + return $data; + } + + // ---------------- Skop ---------------- + public function storeScope(Request $request, ChecklistSection $section) + { + $section->scopes()->create($this->validateScope($request)); + + return back()->with('success', 'Skop ditambah.'); + } + + public function updateScope(Request $request, ChecklistScope $scope) + { + $scope->update($this->validateScope($request)); + + return back()->with('success', 'Skop dikemas kini.'); + } + + public function destroyScope(ChecklistScope $scope) + { + $scope->delete(); + + return back()->with('success', 'Skop dipadam.'); + } + + protected function validateScope(Request $request): array + { + $data = $request->validate([ + 'code' => ['required', 'string', 'max:50'], + 'title' => ['required', 'string', 'max:255'], + 'description' => ['nullable', 'string', 'max:1000'], + 'order' => ['nullable', 'integer', 'min:0'], + ]); + $data['order'] ??= 0; + + return $data; + } + + // ---------------- Item ---------------- + public function storeItem(Request $request, ChecklistSection $section) + { + $data = $this->validateItem($request); + $section->items()->create($data); + + return back()->with('success', 'Item ditambah.'); + } + + public function updateItem(Request $request, ChecklistItem $item) + { + $item->update($this->validateItem($request)); + + return back()->with('success', 'Item dikemas kini.'); + } + + public function destroyItem(ChecklistItem $item) + { + $item->delete(); + + return back()->with('success', 'Item dipadam.'); + } + + protected function validateItem(Request $request): array + { + $data = $request->validate([ + 'checklist_scope_id' => ['nullable', 'exists:checklist_scopes,id'], + 'code' => ['required', 'string', 'max:50'], + 'formula_token' => ['nullable', 'string', 'max:50', 'regex:/^[A-Za-z_][A-Za-z0-9_]*$/'], + 'label' => ['required', 'string', 'max:500'], + 'description' => ['nullable', 'string', 'max:1000'], + 'input_type' => ['required', 'in:'.implode(',', array_keys(ChecklistItem::INPUT_TYPES))], + 'options' => ['nullable', 'string'], + 'unit' => ['nullable', 'string', 'max:50'], + 'is_required' => ['nullable', 'boolean'], + 'is_calculated' => ['nullable', 'boolean'], + 'formula' => ['nullable', 'string', 'max:500'], + 'order' => ['nullable', 'integer', 'min:0'], + ], [], ['formula_token' => 'token formula']); + + $data['is_required'] = $request->boolean('is_required'); + $data['is_calculated'] = $request->boolean('is_calculated'); + $data['order'] ??= 0; + + // Tukar senarai pilihan (satu baris satu pilihan) kepada array. + if (! empty($data['options'])) { + $data['options'] = collect(preg_split('/\r\n|\r|\n/', $data['options'])) + ->map(fn ($v) => trim($v))->filter()->values()->all(); + } else { + $data['options'] = null; + } + + return $data; + } + + // ---------------- Formula ---------------- + public function storeFormula(Request $request, ChecklistTemplate $template) + { + $data = $this->validateFormula($request); + $template->formulas()->create($data); + + return back()->with('success', 'Formula ditambah.'); + } + + public function updateFormula(Request $request, ChecklistFormula $formula) + { + $formula->update($this->validateFormula($request)); + + return back()->with('success', 'Formula dikemas kini.'); + } + + public function destroyFormula(ChecklistFormula $formula) + { + $formula->delete(); + + return back()->with('success', 'Formula dipadam.'); + } + + protected function validateFormula(Request $request): array + { + $data = $request->validate([ + 'checklist_section_id' => ['nullable', 'exists:checklist_sections,id'], + 'result_token' => ['required', 'string', 'max:50', 'regex:/^[A-Za-z_][A-Za-z0-9_]*$/'], + 'label' => ['nullable', 'string', 'max:255'], + 'expression' => ['required', 'string', 'max:500'], + 'unit' => ['nullable', 'string', 'max:50'], + 'guard_div_zero' => ['nullable', 'boolean'], + 'order' => ['nullable', 'integer', 'min:0'], + ], [], ['result_token' => 'token hasil', 'expression' => 'ungkapan']); + + $data['guard_div_zero'] = $request->boolean('guard_div_zero'); + $data['order'] ??= 0; + + return $data; + } +} diff --git a/app/Http/Controllers/ChecklistTemplateController.php b/app/Http/Controllers/ChecklistTemplateController.php new file mode 100644 index 0000000..2edffcb --- /dev/null +++ b/app/Http/Controllers/ChecklistTemplateController.php @@ -0,0 +1,110 @@ +authorize('viewAny', ChecklistTemplate::class); + + $templates = ChecklistTemplate::withCount('sections')->orderByDesc('is_active')->orderBy('name')->get(); + + return view('checklist.templates.index', compact('templates')); + } + + public function create() + { + $this->authorize('create', ChecklistTemplate::class); + + return view('checklist.templates.create', ['template' => new ChecklistTemplate(['version' => '1.0'])]); + } + + public function store(Request $request) + { + $this->authorize('create', ChecklistTemplate::class); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'version' => ['required', 'string', 'max:50'], + 'description' => ['nullable', 'string', 'max:1000'], + 'is_active' => ['nullable', 'boolean'], + ]); + + $template = ChecklistTemplate::create([ + 'name' => $data['name'], + 'version' => $data['version'], + 'description' => $data['description'] ?? null, + 'is_active' => $request->boolean('is_active'), + ]); + + if ($template->is_active) { + ChecklistTemplate::where('id', '!=', $template->id)->update(['is_active' => false]); + } + + return redirect()->route('checklist-templates.builder', $template) + ->with('success', 'Templat dicipta. Sila bina seksyen dan item.'); + } + + /** Skrin pembina templat (seksyen, skop, item, formula). */ + public function builder(ChecklistTemplate $checklistTemplate) + { + $this->authorize('view', $checklistTemplate); + + $checklistTemplate->load([ + 'sections.scopes', + 'sections.items.scope', + 'sections.formula', + 'formulas', + ]); + + return view('checklist.templates.builder', ['template' => $checklistTemplate]); + } + + public function update(Request $request, ChecklistTemplate $checklistTemplate) + { + $this->authorize('update', $checklistTemplate); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'version' => ['required', 'string', 'max:50'], + 'description' => ['nullable', 'string', 'max:1000'], + 'is_active' => ['nullable', 'boolean'], + ]); + + $checklistTemplate->update([ + 'name' => $data['name'], + 'version' => $data['version'], + 'description' => $data['description'] ?? null, + 'is_active' => $request->boolean('is_active'), + ]); + + if ($checklistTemplate->is_active) { + ChecklistTemplate::where('id', '!=', $checklistTemplate->id)->update(['is_active' => false]); + } + + return back()->with('success', 'Templat dikemas kini.'); + } + + public function activate(ChecklistTemplate $checklistTemplate) + { + $this->authorize('update', $checklistTemplate); + + ChecklistTemplate::query()->update(['is_active' => false]); + $checklistTemplate->update(['is_active' => true]); + + return back()->with('success', 'Templat ditetapkan sebagai aktif.'); + } + + public function destroy(ChecklistTemplate $checklistTemplate) + { + $this->authorize('delete', $checklistTemplate); + + $checklistTemplate->delete(); + + return redirect()->route('checklist-templates.index')->with('success', 'Templat dipadam.'); + } +} diff --git a/app/Http/Controllers/CommentController.php b/app/Http/Controllers/CommentController.php new file mode 100644 index 0000000..6c4fce2 --- /dev/null +++ b/app/Http/Controllers/CommentController.php @@ -0,0 +1,45 @@ +authorize('comment', $submission); + + $data = $request->validate([ + 'body' => ['required', 'string', 'max:2000'], + 'is_internal' => ['nullable', 'boolean'], + ], [], ['body' => 'mesej']); + + // Hanya kakitangan JPP boleh hantar komen dalaman. + $isInternal = $request->boolean('is_internal') && auth()->user()->can('serahan.lihat_semua'); + + $submission->comments()->create([ + 'user_id' => auth()->id(), + 'body' => $data['body'], + 'is_internal' => $isInternal, + ]); + + return back()->with('success', 'Mesej dihantar.')->withFragment('komen'); + } + + /** Soft delete — hanya pentadbir (kebenaran komen.padam), dengan rekod audit. */ + public function destroy(SubmissionComment $comment) + { + abort_unless(auth()->user()->can('komen.padam'), 403); + + $comment->update(['deleted_by' => auth()->id()]); + $comment->delete(); + + activity('komen')->performedOn($comment->submission)->causedBy(auth()->user()) + ->withProperties(['comment_id' => $comment->id])->log('Komen dipadam (soft delete)'); + + return back()->with('success', 'Mesej dipadam (rekod audit disimpan).'); + } +} diff --git a/app/Http/Controllers/ConsultantController.php b/app/Http/Controllers/ConsultantController.php new file mode 100644 index 0000000..2c5578d --- /dev/null +++ b/app/Http/Controllers/ConsultantController.php @@ -0,0 +1,132 @@ +authorize('viewAny', Consultant::class); + + $query = Consultant::query()->withCount('dataCentres'); + + if ($search = $request->string('cari')->toString()) { + $query->where(function ($q) use ($search) { + $q->where('nama_perunding', 'like', "%$search%") + ->orWhere('no_pendaftaran_syarikat', 'like', "%$search%") + ->orWhere('emel', 'like', "%$search%"); + }); + } + + if ($request->filled('status')) { + $query->where('aktif', $request->status === 'aktif'); + } + + $consultants = $query->orderBy('nama_perunding')->paginate(15)->withQueryString(); + + return view('consultants.index', compact('consultants')); + } + + public function create() + { + $this->authorize('create', Consultant::class); + + return view('consultants.create', ['consultant' => new Consultant]); + } + + public function store(ConsultantRequest $request) + { + $consultant = DB::transaction(function () use ($request) { + $userId = null; + + if ($request->boolean('buat_akaun')) { + $user = User::create([ + 'name' => $request->user_name, + 'email' => $request->user_email, + 'password' => Hash::make($request->user_password), + 'is_active' => true, + 'email_verified_at' => now(), + ]); + $user->assignRole('Perunding'); + $userId = $user->id; + } + + return Consultant::create([ + 'user_id' => $userId, + 'nama_perunding' => $request->nama_perunding, + 'no_pendaftaran_syarikat' => $request->no_pendaftaran_syarikat, + 'alamat' => $request->alamat, + 'emel' => $request->emel, + 'telefon' => $request->telefon, + 'aktif' => $request->boolean('aktif'), + ]); + }); + + return redirect()->route('consultants.show', $consultant) + ->with('success', 'Perunding berjaya didaftarkan.'); + } + + public function show(Consultant $consultant) + { + $this->authorize('view', $consultant); + + $consultant->load(['user', 'dataCentres', 'assignments.dataCentre', 'assignments.assignedBy']); + + return view('consultants.show', compact('consultant')); + } + + public function edit(Consultant $consultant) + { + $this->authorize('update', $consultant); + + return view('consultants.edit', compact('consultant')); + } + + public function update(ConsultantRequest $request, Consultant $consultant) + { + $this->authorize('update', $consultant); + + DB::transaction(function () use ($request, $consultant) { + $consultant->update([ + 'nama_perunding' => $request->nama_perunding, + 'no_pendaftaran_syarikat' => $request->no_pendaftaran_syarikat, + 'alamat' => $request->alamat, + 'emel' => $request->emel, + 'telefon' => $request->telefon, + 'aktif' => $request->boolean('aktif'), + ]); + + if ($request->boolean('buat_akaun') && ! $consultant->user_id) { + $user = User::create([ + 'name' => $request->user_name, + 'email' => $request->user_email, + 'password' => Hash::make($request->user_password), + 'is_active' => true, + 'email_verified_at' => now(), + ]); + $user->assignRole('Perunding'); + $consultant->update(['user_id' => $user->id]); + } + }); + + return redirect()->route('consultants.show', $consultant) + ->with('success', 'Maklumat perunding dikemas kini.'); + } + + public function destroy(Consultant $consultant) + { + $this->authorize('delete', $consultant); + + $consultant->delete(); + + return redirect()->route('consultants.index') + ->with('success', 'Perunding telah dipadam.'); + } +} diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php new file mode 100644 index 0000000..ef916e5 --- /dev/null +++ b/app/Http/Controllers/Controller.php @@ -0,0 +1,11 @@ +authorize('review', $submission); + + $data = $request->validate([ + 'checklist_item_id' => ['nullable', 'exists:checklist_items,id'], + 'page_reference' => ['required', 'string', 'max:50'], + 'location' => ['required', 'string', 'max:255'], + 'description' => ['required', 'string', 'max:2000'], + ], [], [ + 'page_reference' => 'no muka surat', + 'location' => 'lokasi/seksyen/item', + 'description' => 'penerangan pembetulan', + ]); + + $submission->correctionRequests()->create(array_merge($data, [ + 'status' => 'terbuka', + 'created_by' => auth()->id(), + ])); + + activity('serahan')->performedOn($submission)->causedBy(auth()->user()) + ->log('Permohonan pembetulan dibuat'); + + return back()->with('success', 'Permohonan pembetulan ditambah.'); + } + + /** Perunding jawab permohonan pembetulan. */ + public function respond(Request $request, SubmissionCorrectionRequest $correction) + { + $submission = $correction->submission; + $this->authorize('respondCorrection', $submission); + + $data = $request->validate([ + 'consultant_response' => ['required', 'string', 'max:2000'], + ], [], ['consultant_response' => 'maklum balas']); + + $correction->update([ + 'consultant_response' => $data['consultant_response'], + 'status' => 'dijawab', + 'responded_at' => now(), + 'responded_by' => auth()->id(), + ]); + + return back()->with('success', 'Maklum balas pembetulan dihantar.'); + } + + /** Pegawai tutup permohonan pembetulan. */ + public function resolve(SubmissionCorrectionRequest $correction) + { + $this->authorize('review', $correction->submission); + + $correction->update([ + 'status' => 'selesai', + 'resolved_at' => now(), + 'resolved_by' => auth()->id(), + ]); + + return back()->with('success', 'Permohonan pembetulan ditandakan selesai.'); + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..aaf3ec1 --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,118 @@ +user(); + + if ($user->isPerunding()) { + return $this->consultantDashboard(); + } + + return $this->jppDashboard(); + } + + protected function jppDashboard() + { + $activeCycle = ReportingCycle::aktif()->orderByDesc('renewal_year')->first(); + + $totalDataCentres = DataCentre::aktif()->count(); + $activeConsultants = Consultant::aktif()->count(); + + $submittedCount = 0; + $notSubmittedCount = 0; + $overdueCount = 0; + $statusBreakdown = collect(); + + if ($activeCycle) { + $expected = DataCentre::aktif()->whereNotNull('current_consultant_id')->count(); + $submittedCount = Submission::where('reporting_cycle_id', $activeCycle->id) + ->where('status', '!=', 'draf')->count(); + $notSubmittedCount = max(0, $expected - $submittedCount); + + $statusBreakdown = Submission::where('reporting_cycle_id', $activeCycle->id) + ->select('status', DB::raw('count(*) as jumlah')) + ->groupBy('status')->pluck('jumlah', 'status'); + + if ($activeCycle->isOverdue()) { + $overdueCount = $notSubmittedCount; + } + } + + // Jumlah karbon A,B,C,D,E mengikut tahun pelaporan + $carbonByYear = SubmissionResult::query() + ->join('submissions', 'submissions.id', '=', 'submission_results.submission_id') + ->join('reporting_cycles', 'reporting_cycles.id', '=', 'submissions.reporting_cycle_id') + ->whereNull('submissions.deleted_at') + ->select( + 'reporting_cycles.reporting_year', + 'submission_results.result_token', + DB::raw('SUM(submission_results.value) as jumlah') + ) + ->groupBy('reporting_cycles.reporting_year', 'submission_results.result_token') + ->get() + ->groupBy('reporting_year'); + + // Pusat data tertinggi mengikut anggaran karbon (token A) + $topDataCentres = SubmissionResult::query() + ->where('submission_results.result_token', 'A') + ->join('submissions', 'submissions.id', '=', 'submission_results.submission_id') + ->join('data_centres', 'data_centres.id', '=', 'submissions.data_centre_id') + ->whereNull('submissions.deleted_at') + ->select('data_centres.nama_pusat_data', DB::raw('SUM(submission_results.value) as jumlah')) + ->groupBy('data_centres.nama_pusat_data') + ->orderByDesc('jumlah') + ->limit(10) + ->get(); + + return view('dashboard.jpp', compact( + 'activeCycle', 'totalDataCentres', 'activeConsultants', + 'submittedCount', 'notSubmittedCount', 'overdueCount', + 'statusBreakdown', 'carbonByYear', 'topDataCentres' + )); + } + + protected function consultantDashboard() + { + $consultant = auth()->user()->consultant; + + $dataCentres = collect(); + $submissions = collect(); + $pendingCount = 0; + $correctionCount = 0; + $activeCycle = ReportingCycle::aktif()->orderByDesc('renewal_year')->first(); + + if ($consultant) { + $dataCentres = DataCentre::where('current_consultant_id', $consultant->id)->get(); + + $submissions = Submission::where('consultant_id', $consultant->id) + ->with(['dataCentre', 'reportingCycle']) + ->latest()->get(); + + $correctionCount = $submissions->where('status', 'pembetulan_perunding')->count(); + + if ($activeCycle) { + $submittedDcIds = Submission::where('consultant_id', $consultant->id) + ->where('reporting_cycle_id', $activeCycle->id) + ->where('status', '!=', 'draf') + ->pluck('data_centre_id'); + $pendingCount = $dataCentres->whereNotIn('id', $submittedDcIds)->count(); + } + } + + return view('dashboard.consultant', compact( + 'consultant', 'dataCentres', 'submissions', + 'pendingCount', 'correctionCount', 'activeCycle' + )); + } +} diff --git a/app/Http/Controllers/DataCentreController.php b/app/Http/Controllers/DataCentreController.php new file mode 100644 index 0000000..aeb8a3c --- /dev/null +++ b/app/Http/Controllers/DataCentreController.php @@ -0,0 +1,134 @@ +authorize('viewAny', DataCentre::class); + + $user = auth()->user(); + $query = DataCentre::query()->with('currentConsultant'); + + // Perunding hanya lihat pusat data yang ditugaskan kepadanya. + if ($user->isPerunding() && ! $user->can('pusat_data.urus')) { + $query->where('current_consultant_id', $user->consultant?->id ?? 0); + } + + if ($search = $request->string('cari')->toString()) { + $query->where('nama_pusat_data', 'like', "%$search%") + ->orWhere('tajuk_permohonan', 'like', "%$search%"); + } + + if ($request->filled('status')) { + $query->where('status', $request->status); + } + + $dataCentres = $query->orderBy('nama_pusat_data')->paginate(15)->withQueryString(); + + return view('data_centres.index', compact('dataCentres')); + } + + public function create() + { + $this->authorize('create', DataCentre::class); + + return view('data_centres.create', ['dataCentre' => new DataCentre]); + } + + public function store(DataCentreRequest $request) + { + $dataCentre = DataCentre::create($request->validated()); + + return redirect()->route('data-centres.show', $dataCentre) + ->with('success', 'Pusat data berjaya didaftarkan.'); + } + + public function show(DataCentre $dataCentre) + { + $this->authorize('view', $dataCentre); + + $dataCentre->load([ + 'currentConsultant', + 'assignments.consultant', + 'assignments.assignedBy', + 'submissions.reportingCycle', + ]); + $consultants = Consultant::aktif()->orderBy('nama_perunding')->get(); + + return view('data_centres.show', compact('dataCentre', 'consultants')); + } + + public function edit(DataCentre $dataCentre) + { + $this->authorize('update', $dataCentre); + + return view('data_centres.edit', compact('dataCentre')); + } + + public function update(DataCentreRequest $request, DataCentre $dataCentre) + { + $this->authorize('update', $dataCentre); + + $dataCentre->update($request->validated()); + + return redirect()->route('data-centres.show', $dataCentre) + ->with('success', 'Maklumat pusat data dikemas kini.'); + } + + public function destroy(DataCentre $dataCentre) + { + $this->authorize('delete', $dataCentre); + + $dataCentre->delete(); + + return redirect()->route('data-centres.index') + ->with('success', 'Pusat data telah dipadam.'); + } + + /** Tugaskan / tukar perunding aktif untuk pusat data. */ + public function assign(Request $request, DataCentre $dataCentre) + { + $this->authorize('assignConsultant', $dataCentre); + + $data = $request->validate([ + 'consultant_id' => ['required', 'exists:consultants,id'], + 'start_date' => ['nullable', 'date'], + 'reason' => ['nullable', 'string', 'max:500'], + ], [], ['consultant_id' => 'perunding', 'reason' => 'sebab']); + + $consultant = Consultant::findOrFail($data['consultant_id']); + + $this->assignmentService->assign( + $dataCentre, + $consultant, + auth()->user(), + $data['start_date'] ?? null, + $data['reason'] ?? null + ); + + return redirect()->route('data-centres.show', $dataCentre) + ->with('success', 'Perunding berjaya ditugaskan. Penugasan terdahulu (jika ada) telah ditamatkan.'); + } + + public function endAssignment(Request $request, DataCentre $dataCentre) + { + $this->authorize('assignConsultant', $dataCentre); + + $data = $request->validate(['reason' => ['nullable', 'string', 'max:500']]); + + $this->assignmentService->endActive($dataCentre, auth()->user(), $data['reason'] ?? null); + + return redirect()->route('data-centres.show', $dataCentre) + ->with('success', 'Penugasan perunding aktif telah ditamatkan.'); + } +} diff --git a/app/Http/Controllers/DocumentController.php b/app/Http/Controllers/DocumentController.php new file mode 100644 index 0000000..644f8c0 --- /dev/null +++ b/app/Http/Controllers/DocumentController.php @@ -0,0 +1,47 @@ +authorize('update', $submission); + + $maxKb = (int) AppSetting::get('max_upload_size_kb', config('mbip.max_upload_size_kb', 20480)); + + $request->validate([ + 'report' => ['required', 'file', 'mimes:pdf', 'mimetypes:application/pdf', "max:$maxKb"], + ], [], ['report' => 'laporan PDF']); + + $this->documents->store($submission, $request->file('report'), auth()->user()); + + return back()->with('success', 'Laporan PDF berjaya dimuat naik (versi baharu dicipta).'); + } + + /** Muat turun laporan — dilindungi oleh polisi. */ + public function download(SubmissionDocument $document) + { + $this->authorize('view', $document->submission); + + return $this->documents->download($document); + } + + public function setActive(SubmissionDocument $document) + { + $this->authorize('update', $document->submission); + + $this->documents->setActiveVersion($document->submission, $document); + + return back()->with('success', 'Versi laporan aktif dikemas kini.'); + } +} diff --git a/app/Http/Controllers/ReminderController.php b/app/Http/Controllers/ReminderController.php new file mode 100644 index 0000000..b5ccd83 --- /dev/null +++ b/app/Http/Controllers/ReminderController.php @@ -0,0 +1,67 @@ +user()->can('peringatan.lihat'), 403); + + $cycles = ReportingCycle::orderByDesc('renewal_year')->get(); + $cycleId = $request->input('cycle') ?: ReportingCycle::aktif()->orderByDesc('renewal_year')->value('id'); + $cycle = $cycleId ? ReportingCycle::find($cycleId) : null; + + $rows = collect(); + + if ($cycle) { + // Pusat data aktif yang mempunyai perunding aktif. + $query = DataCentre::query()->aktif() + ->whereNotNull('current_consultant_id') + ->with('currentConsultant'); + + // ID pusat data yang TELAH menghantar (bukan draf) untuk kitaran ini. + $submittedDcIds = Submission::where('reporting_cycle_id', $cycle->id) + ->where('status', '!=', 'draf') + ->pluck('data_centre_id')->all(); + + // Buang yang telah hantar — peraturan: keluar dari senarai peringatan selepas hantar. + $query->whereNotIn('id', $submittedDcIds); + + if ($request->filled('consultant')) { + $query->where('current_consultant_id', $request->consultant); + } + if ($request->filled('data_centre')) { + $query->where('id', $request->data_centre); + } + + $rows = $query->orderBy('nama_pusat_data')->get()->map(function ($dc) use ($cycle) { + // Adakah ada draf separa lengkap? + $draft = Submission::where('reporting_cycle_id', $cycle->id) + ->where('data_centre_id', $dc->id)->first(); + + return (object) [ + 'data_centre' => $dc, + 'consultant' => $dc->currentConsultant, + 'status' => $draft ? $draft->status : 'belum_mula', + 'is_overdue' => $cycle->isOverdue(), + 'submission' => $draft, + ]; + }); + + // Penapis lampau tempoh sahaja. + if ($request->boolean('overdue')) { + $rows = $rows->filter(fn ($r) => $r->is_overdue)->values(); + } + } + + $consultants = \App\Models\Consultant::aktif()->orderBy('nama_perunding')->get(); + + return view('reminders.index', compact('rows', 'cycles', 'cycle', 'consultants')); + } +} diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php new file mode 100644 index 0000000..0cb5de3 --- /dev/null +++ b/app/Http/Controllers/ReportController.php @@ -0,0 +1,59 @@ +user()->can('laporan.lihat'), 403); + + $cycles = ReportingCycle::orderByDesc('renewal_year')->get(); + $cycleId = $request->input('cycle'); + + $resultsQuery = SubmissionResult::query() + ->join('submissions', 'submissions.id', '=', 'submission_results.submission_id') + ->join('data_centres', 'data_centres.id', '=', 'submissions.data_centre_id') + ->join('reporting_cycles', 'reporting_cycles.id', '=', 'submissions.reporting_cycle_id') + ->whereNull('submissions.deleted_at'); + + if ($cycleId) { + $resultsQuery->where('submissions.reporting_cycle_id', $cycleId); + } + + // Jadual hasil karbon mengikut pusat data + kitaran. + $rows = (clone $resultsQuery) + ->select( + 'data_centres.nama_pusat_data', + 'reporting_cycles.reporting_year', + 'submission_results.result_token', + 'submission_results.value' + ) + ->orderBy('reporting_cycles.reporting_year') + ->orderBy('data_centres.nama_pusat_data') + ->get() + ->groupBy(fn ($r) => $r->nama_pusat_data.'|'.$r->reporting_year); + + // Trend jumlah karbon (A) mengikut tahun pelaporan. + $trend = (clone $resultsQuery) + ->where('submission_results.result_token', 'A') + ->select('reporting_cycles.reporting_year', DB::raw('SUM(submission_results.value) as jumlah')) + ->groupBy('reporting_cycles.reporting_year') + ->orderBy('reporting_cycles.reporting_year') + ->pluck('jumlah', 'reporting_year'); + + // Pecahan status serahan. + $statusBreakdown = Submission::query() + ->when($cycleId, fn ($q) => $q->where('reporting_cycle_id', $cycleId)) + ->select('status', DB::raw('count(*) as jumlah')) + ->groupBy('status')->pluck('jumlah', 'status'); + + return view('reports.index', compact('cycles', 'cycleId', 'rows', 'trend', 'statusBreakdown')); + } +} diff --git a/app/Http/Controllers/ReportingCycleController.php b/app/Http/Controllers/ReportingCycleController.php new file mode 100644 index 0000000..978196c --- /dev/null +++ b/app/Http/Controllers/ReportingCycleController.php @@ -0,0 +1,88 @@ +authorize('viewAny', ReportingCycle::class); + + $cycles = ReportingCycle::withCount('submissions') + ->orderByDesc('renewal_year')->paginate(15); + + return view('reporting_cycles.index', compact('cycles')); + } + + public function create() + { + $this->authorize('create', ReportingCycle::class); + + $templates = ChecklistTemplate::orderByDesc('is_active')->get(); + + return view('reporting_cycles.create', [ + 'cycle' => new ReportingCycle(['licence_period_year' => 1, 'status' => 'aktif']), + 'templates' => $templates, + ]); + } + + public function store(ReportingCycleRequest $request) + { + $attrs = $this->service->buildAttributes( + (int) $request->renewal_year, + $request->cut_off_date + ); + + $cycle = ReportingCycle::create(array_merge($request->validated(), $attrs)); + + return redirect()->route('reporting-cycles.index') + ->with('success', "Kitaran pembaharuan {$cycle->renewal_year} (pelaporan {$cycle->reporting_year}) dicipta."); + } + + public function edit(ReportingCycle $reportingCycle) + { + $this->authorize('update', $reportingCycle); + + $templates = ChecklistTemplate::orderByDesc('is_active')->get(); + + return view('reporting_cycles.edit', ['cycle' => $reportingCycle, 'templates' => $templates]); + } + + public function update(ReportingCycleRequest $request, ReportingCycle $reportingCycle) + { + $this->authorize('update', $reportingCycle); + + // Kira semula tahun pelaporan & tempoh jika tahun pembaharuan berubah. + $attrs = $this->service->buildAttributes( + (int) $request->renewal_year, + $request->cut_off_date + ); + // Kekalkan templat pilihan pengguna jika diberi. + $attrs['checklist_template_id'] = $request->checklist_template_id ?? $attrs['checklist_template_id']; + + $reportingCycle->update(array_merge($request->validated(), $attrs)); + + return redirect()->route('reporting-cycles.index') + ->with('success', 'Kitaran pelaporan dikemas kini.'); + } + + public function destroy(ReportingCycle $reportingCycle) + { + $this->authorize('delete', $reportingCycle); + + if ($reportingCycle->submissions()->exists()) { + return back()->with('error', 'Kitaran tidak boleh dipadam kerana telah mempunyai serahan.'); + } + + $reportingCycle->delete(); + + return redirect()->route('reporting-cycles.index')->with('success', 'Kitaran dipadam.'); + } +} diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php new file mode 100644 index 0000000..27ef739 --- /dev/null +++ b/app/Http/Controllers/SettingController.php @@ -0,0 +1,42 @@ +user()->can('tetapan.urus'), 403); + + $settings = AppSetting::orderBy('group')->orderBy('key')->get()->groupBy('group'); + + return view('settings.edit', compact('settings')); + } + + public function update(Request $request) + { + abort_unless(auth()->user()->can('tetapan.urus'), 403); + + $values = $request->input('settings', []); + + // Proses setiap tetapan. Kotak semak (boolean) yang tidak ditanda tidak + // dihantar oleh borang, jadi tetapan boolean dikendalikan secara eksplisit. + foreach (AppSetting::all() as $setting) { + if ($setting->type === 'boolean') { + $value = $request->boolean("settings.{$setting->key}") ? '1' : '0'; + } elseif (array_key_exists($setting->key, $values)) { + $value = $values[$setting->key]; + } else { + continue; + } + AppSetting::set($setting->key, $value); + } + + activity('tetapan')->causedBy(auth()->user())->log('Tetapan aplikasi dikemas kini'); + + return back()->with('success', 'Tetapan aplikasi disimpan.'); + } +} diff --git a/app/Http/Controllers/SubmissionController.php b/app/Http/Controllers/SubmissionController.php new file mode 100644 index 0000000..1d8930e --- /dev/null +++ b/app/Http/Controllers/SubmissionController.php @@ -0,0 +1,143 @@ +authorize('viewAny', Submission::class); + + $user = auth()->user(); + $query = Submission::query()->with(['dataCentre', 'reportingCycle', 'consultant', 'assignedOfficer']); + + // Perunding: hanya serahan sendiri. + if (! $user->can('serahan.lihat_semua')) { + $query->where('consultant_id', $user->consultant?->id ?? 0); + } + + if ($request->filled('cycle')) { + $query->where('reporting_cycle_id', $request->cycle); + } + if ($request->filled('status')) { + $query->where('status', $request->status); + } + if ($request->filled('consultant') && $user->can('serahan.lihat_semua')) { + $query->where('consultant_id', $request->consultant); + } + + $submissions = $query->latest()->paginate(15)->withQueryString(); + $cycles = ReportingCycle::orderByDesc('renewal_year')->get(); + + return view('submissions.index', compact('submissions', 'cycles')); + } + + /** Borang pilih pusat data + kitaran untuk memulakan serahan. */ + public function create(Request $request) + { + $this->authorize('create', Submission::class); + + $consultant = auth()->user()->consultant; + abort_unless($consultant, 403, 'Akaun anda tiada profil perunding.'); + + $dataCentres = DataCentre::where('current_consultant_id', $consultant->id)->aktif()->get(); + $cycles = ReportingCycle::aktif()->orderByDesc('renewal_year')->get(); + + return view('submissions.create', compact('dataCentres', 'cycles')); + } + + public function store(Request $request) + { + $this->authorize('create', Submission::class); + + $consultant = auth()->user()->consultant; + abort_unless($consultant, 403); + + $data = $request->validate([ + 'data_centre_id' => ['required', 'exists:data_centres,id'], + 'reporting_cycle_id' => ['required', 'exists:reporting_cycles,id'], + ], [], ['data_centre_id' => 'pusat data', 'reporting_cycle_id' => 'kitaran pelaporan']); + + $dataCentre = DataCentre::findOrFail($data['data_centre_id']); + + // Pastikan pusat data ini milik perunding. + abort_unless((int) $dataCentre->current_consultant_id === (int) $consultant->id, 403, + 'Anda tidak ditugaskan untuk pusat data ini.'); + + $cycle = ReportingCycle::findOrFail($data['reporting_cycle_id']); + $submission = $this->workflow->findOrCreateDraft($dataCentre, $cycle); + + return redirect()->route('submissions.edit', $submission) + ->with('success', 'Serahan draf disediakan. Sila lengkapkan senarai semak.'); + } + + /** Borang pengisian senarai semak (perunding). */ + public function edit(Submission $submission) + { + $this->authorize('update', $submission); + + $submission->load([ + 'dataCentre', 'reportingCycle', 'checklistTemplate', + 'answers', 'documents', 'correctionRequests', + ]); + + $template = $submission->checklistTemplate; + $template?->load(['sections.scopes', 'sections.items.scope', 'formulas']); + + $answers = $submission->answers->keyBy('checklist_item_id'); + + return view('submissions.fill', compact('submission', 'template', 'answers')); + } + + public function update(Request $request, Submission $submission) + { + $this->authorize('update', $submission); + + $answers = $request->input('answers', []); + $this->workflow->saveAnswers($submission, $answers); + + return back()->with('success', 'Draf senarai semak disimpan.'); + } + + public function submit(Request $request, Submission $submission) + { + $this->authorize('update', $submission); + + // Simpan dahulu jawapan terkini jika dihantar bersama. + if ($request->has('answers')) { + $this->workflow->saveAnswers($submission, $request->input('answers', [])); + } + + $this->workflow->submit($submission, auth()->user()); + + return redirect()->route('submissions.show', $submission) + ->with('success', 'Serahan berjaya dihantar kepada JPP.'); + } + + public function show(Submission $submission) + { + $this->authorize('view', $submission); + + $submission->load([ + 'dataCentre', 'reportingCycle', 'consultant', 'assignedOfficer', + 'checklistTemplate.sections.scopes', 'checklistTemplate.sections.items.scope', + 'answers', 'results', 'documents.uploadedBy', + 'statusHistories.changedBy', 'correctionRequests.item', 'correctionRequests.createdBy', + 'comments.user', + ]); + + $template = $submission->checklistTemplate; + $answers = $submission->answers->keyBy('checklist_item_id'); + $officers = \App\Models\User::role(['Pegawai JPP', 'Penolong Pegawai', 'Kerani', 'JPP Admin'])->get(); + + return view('submissions.show', compact('submission', 'template', 'answers', 'officers')); + } +} diff --git a/app/Http/Controllers/SubmissionReviewController.php b/app/Http/Controllers/SubmissionReviewController.php new file mode 100644 index 0000000..292cfe5 --- /dev/null +++ b/app/Http/Controllers/SubmissionReviewController.php @@ -0,0 +1,122 @@ +authorize('review', $submission); + + $data = $request->validate([ + 'status' => ['required', Rule::in(array_keys(Submission::STATUSES))], + 'notes' => ['nullable', 'string', 'max:1000'], + // Wajib jika status = selesai: tarikh hardcopy dihantar ke Bahagian Kawalan Perancangan. + 'hardcopy_sent_date' => ['nullable', 'required_if:status,selesai', 'date'], + ], [], ['hardcopy_sent_date' => 'tarikh hardcopy dihantar ke Bahagian Kawalan Perancangan']); + + // Status 'pembetulan_perunding' mesti melalui panel permohonan pembetulan. + if ($data['status'] === 'pembetulan_perunding' && ! $submission->correctionRequests()->where('status', 'terbuka')->exists()) { + return back()->with('error', 'Sila tambah sekurang-kurangnya satu permohonan pembetulan (no muka surat, lokasi & penerangan) sebelum menetapkan status Pembetulan Perunding.'); + } + + $extra = []; + if ($data['status'] === 'selesai') { + $extra['hardcopy_sent_date'] = $data['hardcopy_sent_date']; + } + + $this->workflow->changeStatus($submission, $data['status'], auth()->user(), $data['notes'] ?? null, $extra); + + return back()->with('success', 'Status serahan dikemas kini.'); + } + + public function assignOfficer(Request $request, Submission $submission) + { + $this->authorize('review', $submission); + + $data = $request->validate(['assigned_officer_id' => ['nullable', 'exists:users,id']]); + $submission->update(['assigned_officer_id' => $data['assigned_officer_id'] ?? null]); + + activity('serahan')->performedOn($submission)->causedBy(auth()->user()) + ->log('Pegawai bertanggungjawab dikemas kini'); + + return back()->with('success', 'Pegawai bertanggungjawab dikemas kini.'); + } + + public function lock(Request $request, Submission $submission) + { + $this->authorize('lock', $submission); + + $data = $request->validate(['lock_reason' => ['nullable', 'string', 'max:500']]); + $this->workflow->lock($submission, auth()->user(), $data['lock_reason'] ?? null); + + return back()->with('success', 'Serahan telah dikunci. Perunding tidak boleh mengubah sehingga dibuka semula.'); + } + + public function unlock(Request $request, Submission $submission) + { + $this->authorize('lock', $submission); + + $data = $request->validate([ + 'lock_reason' => ['required', 'string', 'max:500'], + ], [], ['lock_reason' => 'sebab buka kunci']); + + $this->workflow->unlock($submission, auth()->user(), $data['lock_reason']); + + return back()->with('success', 'Serahan dibuka semula untuk pembetulan oleh perunding.'); + } + + public function markHardcopy(Request $request, Submission $submission) + { + $this->authorize('markHardcopy', $submission); + + $data = $request->validate([ + 'hardcopy_received' => ['required', 'boolean'], + 'hardcopy_received_date' => ['nullable', 'required_if:hardcopy_received,1', 'date'], + ], [], ['hardcopy_received_date' => 'tarikh terima salinan keras']); + + $submission->update([ + 'hardcopy_received' => $request->boolean('hardcopy_received'), + 'hardcopy_received_date' => $request->boolean('hardcopy_received') ? $data['hardcopy_received_date'] : null, + ]); + + activity('serahan')->performedOn($submission)->causedBy(auth()->user()) + ->log('Status penerimaan salinan keras dikemas kini'); + + return back()->with('success', 'Status salinan keras dikemas kini.'); + } + + public function reviewNote(Request $request, Submission $submission) + { + $this->authorize('review', $submission); + + $data = $request->validate(['review_notes' => ['nullable', 'string', 'max:2000']]); + $submission->update(['review_notes' => $data['review_notes'] ?? null]); + + return back()->with('success', 'Nota semakan disimpan.'); + } + + /** Pegawai mengesahkan / catat nota bagi satu jawapan item. */ + public function reviewAnswer(Request $request, SubmissionAnswer $answer) + { + $this->authorize('review', $answer->submission); + + $data = $request->validate([ + 'validation_status' => ['required', 'in:belum_semak,sah,tidak_sah'], + 'officer_note' => ['nullable', 'string', 'max:1000'], + ]); + + $answer->update($data); + + return back()->with('success', 'Semakan item dikemas kini.'); + } +} diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php new file mode 100644 index 0000000..a3be3e6 --- /dev/null +++ b/app/Http/Controllers/UserController.php @@ -0,0 +1,108 @@ +user()->can('pengguna.urus'), 403); + + $users = User::with('roles') + ->when($request->filled('cari'), fn ($q) => $q->where('name', 'like', '%'.$request->cari.'%')->orWhere('email', 'like', '%'.$request->cari.'%')) + ->orderBy('name')->paginate(15)->withQueryString(); + + return view('users.index', compact('users')); + } + + public function create() + { + abort_unless(auth()->user()->can('pengguna.urus'), 403); + + return view('users.create', ['user' => new User, 'roles' => Role::orderBy('name')->get()]); + } + + public function store(Request $request) + { + abort_unless(auth()->user()->can('pengguna.urus'), 403); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')], + 'phone' => ['nullable', 'string', 'max:50'], + 'jawatan' => ['nullable', 'string', 'max:255'], + 'password' => ['required', 'confirmed', Password::min(8)], + 'role' => ['required', 'exists:roles,name'], + 'is_active' => ['nullable', 'boolean'], + ]); + + $user = User::create([ + 'name' => $data['name'], + 'email' => $data['email'], + 'phone' => $data['phone'] ?? null, + 'jawatan' => $data['jawatan'] ?? null, + 'password' => Hash::make($data['password']), + 'is_active' => $request->boolean('is_active'), + 'email_verified_at' => now(), + ]); + $user->syncRoles([$data['role']]); + + return redirect()->route('users.index')->with('success', 'Pengguna dicipta.'); + } + + public function edit(User $user) + { + abort_unless(auth()->user()->can('pengguna.urus'), 403); + + return view('users.edit', ['user' => $user, 'roles' => Role::orderBy('name')->get()]); + } + + public function update(Request $request, User $user) + { + abort_unless(auth()->user()->can('pengguna.urus'), 403); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:255'], + 'email' => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($user->id)], + 'phone' => ['nullable', 'string', 'max:50'], + 'jawatan' => ['nullable', 'string', 'max:255'], + 'password' => ['nullable', 'confirmed', Password::min(8)], + 'role' => ['required', 'exists:roles,name'], + 'is_active' => ['nullable', 'boolean'], + ]); + + $user->update([ + 'name' => $data['name'], + 'email' => $data['email'], + 'phone' => $data['phone'] ?? null, + 'jawatan' => $data['jawatan'] ?? null, + 'is_active' => $request->boolean('is_active'), + ]); + + if (! empty($data['password'])) { + $user->update(['password' => Hash::make($data['password'])]); + } + + $user->syncRoles([$data['role']]); + + return redirect()->route('users.index')->with('success', 'Pengguna dikemas kini.'); + } + + public function destroy(User $user) + { + abort_unless(auth()->user()->can('pengguna.urus'), 403); + + abort_if($user->id === auth()->id(), 403, 'Anda tidak boleh memadam akaun sendiri.'); + + $user->delete(); + + return redirect()->route('users.index')->with('success', 'Pengguna dipadam.'); + } +} diff --git a/app/Http/Requests/ConsultantRequest.php b/app/Http/Requests/ConsultantRequest.php new file mode 100644 index 0000000..2981686 --- /dev/null +++ b/app/Http/Requests/ConsultantRequest.php @@ -0,0 +1,57 @@ +user()->can('perunding.urus'); + } + + public function rules(): array + { + $consultantId = $this->route('consultant')?->id; + + return [ + 'nama_perunding' => ['required', 'string', 'max:255'], + 'no_pendaftaran_syarikat' => ['nullable', 'string', 'max:100'], + 'alamat' => ['nullable', 'string', 'max:1000'], + 'emel' => ['nullable', 'email', 'max:255'], + 'telefon' => ['nullable', 'string', 'max:50'], + 'aktif' => ['required', 'boolean'], + // Maklumat akaun pengguna perunding + 'buat_akaun' => ['nullable', 'boolean'], + 'user_name' => ['nullable', 'required_if:buat_akaun,1', 'string', 'max:255'], + 'user_email' => [ + 'nullable', 'required_if:buat_akaun,1', 'email', 'max:255', + Rule::unique('users', 'email')->ignore($this->route('consultant')?->user_id), + ], + 'user_password' => ['nullable', 'required_if:buat_akaun,1', 'string', 'min:8'], + ]; + } + + public function attributes(): array + { + return [ + 'nama_perunding' => 'nama perunding', + 'no_pendaftaran_syarikat' => 'no pendaftaran syarikat', + 'emel' => 'emel', + 'telefon' => 'telefon', + 'user_name' => 'nama pengguna', + 'user_email' => 'emel akaun', + 'user_password' => 'kata laluan akaun', + ]; + } + + protected function prepareForValidation(): void + { + $this->merge([ + 'aktif' => $this->boolean('aktif'), + 'buat_akaun' => $this->boolean('buat_akaun'), + ]); + } +} diff --git a/app/Http/Requests/DataCentreRequest.php b/app/Http/Requests/DataCentreRequest.php new file mode 100644 index 0000000..9a54daa --- /dev/null +++ b/app/Http/Requests/DataCentreRequest.php @@ -0,0 +1,37 @@ +user()->can('pusat_data.urus'); + } + + public function rules(): array + { + return [ + 'nama_pusat_data' => ['required', 'string', 'max:255'], + 'tajuk_permohonan' => ['nullable', 'string', 'max:255'], + 'lokasi_pusat_data' => ['nullable', 'string', 'max:255'], + 'alamat' => ['nullable', 'string', 'max:1000'], + 'keluasan_tapak' => ['nullable', 'numeric', 'min:0'], + 'it_load' => ['nullable', 'numeric', 'min:0'], + 'status' => ['required', 'in:aktif,tidak_aktif'], + ]; + } + + public function attributes(): array + { + return [ + 'nama_pusat_data' => 'nama pusat data', + 'tajuk_permohonan' => 'tajuk permohonan', + 'lokasi_pusat_data' => 'lokasi pusat data', + 'keluasan_tapak' => 'keluasan tapak', + 'it_load' => 'kapasiti IT Load', + ]; + } +} diff --git a/app/Http/Requests/ReportingCycleRequest.php b/app/Http/Requests/ReportingCycleRequest.php new file mode 100644 index 0000000..570cd65 --- /dev/null +++ b/app/Http/Requests/ReportingCycleRequest.php @@ -0,0 +1,40 @@ +user()->can('kitaran.urus'); + } + + public function rules(): array + { + $cycleId = $this->route('reporting_cycle')?->id; + + return [ + 'renewal_year' => [ + 'required', 'integer', 'min:2000', 'max:2100', + Rule::unique('reporting_cycles', 'renewal_year')->ignore($cycleId), + ], + 'cut_off_date' => ['nullable', 'date'], + 'licence_period_year' => ['required', 'integer', 'min:1', 'max:10'], + 'checklist_template_id' => ['nullable', 'exists:checklist_templates,id'], + 'status' => ['required', 'in:aktif,tutup'], + 'catatan' => ['nullable', 'string', 'max:1000'], + ]; + } + + public function attributes(): array + { + return [ + 'renewal_year' => 'tahun pembaharuan', + 'cut_off_date' => 'tarikh tutup', + 'licence_period_year' => 'tempoh lesen (tahun)', + ]; + } +} diff --git a/app/Models/AppSetting.php b/app/Models/AppSetting.php new file mode 100644 index 0000000..3fa99ce --- /dev/null +++ b/app/Models/AppSetting.php @@ -0,0 +1,44 @@ + static::where('key', $key)->first()); + + if (! $setting) { + return $default; + } + + return match ($setting->type) { + 'integer' => (int) $setting->value, + 'boolean' => filter_var($setting->value, FILTER_VALIDATE_BOOLEAN), + default => $setting->value, + }; + } + + public static function set(string $key, $value): void + { + static::where('key', $key)->update(['value' => $value]); + Cache::forget("setting.$key"); + } + + protected static function booted(): void + { + static::saved(fn ($m) => Cache::forget("setting.{$m->key}")); + } +} diff --git a/app/Models/ChecklistFormula.php b/app/Models/ChecklistFormula.php new file mode 100644 index 0000000..842d956 --- /dev/null +++ b/app/Models/ChecklistFormula.php @@ -0,0 +1,41 @@ + 'boolean', + 'order' => 'integer', + ]; + } + + public function template(): BelongsTo + { + return $this->belongsTo(ChecklistTemplate::class, 'checklist_template_id'); + } + + public function section(): BelongsTo + { + return $this->belongsTo(ChecklistSection::class, 'checklist_section_id'); + } +} diff --git a/app/Models/ChecklistItem.php b/app/Models/ChecklistItem.php new file mode 100644 index 0000000..15aa86d --- /dev/null +++ b/app/Models/ChecklistItem.php @@ -0,0 +1,69 @@ + 'Nombor', + 'text' => 'Teks', + 'textarea' => 'Teks Panjang', + 'select' => 'Senarai Pilihan', + 'checkbox' => 'Kotak Semak', + 'file_reference' => 'Rujukan Fail', + 'calculated' => 'Dikira Automatik', + ]; + + protected $fillable = [ + 'checklist_section_id', + 'checklist_scope_id', + 'code', + 'formula_token', + 'label', + 'description', + 'input_type', + 'options', + 'unit', + 'is_required', + 'is_calculated', + 'formula', + 'order', + ]; + + protected function casts(): array + { + return [ + 'options' => 'array', + 'is_required' => 'boolean', + 'is_calculated' => 'boolean', + 'order' => 'integer', + ]; + } + + public function section(): BelongsTo + { + return $this->belongsTo(ChecklistSection::class, 'checklist_section_id'); + } + + public function scope(): BelongsTo + { + return $this->belongsTo(ChecklistScope::class, 'checklist_scope_id'); + } + + public function answers(): HasMany + { + return $this->hasMany(SubmissionAnswer::class); + } + + public function isNumeric(): bool + { + return in_array($this->input_type, ['number', 'calculated'], true); + } +} diff --git a/app/Models/ChecklistScope.php b/app/Models/ChecklistScope.php new file mode 100644 index 0000000..bbfea73 --- /dev/null +++ b/app/Models/ChecklistScope.php @@ -0,0 +1,38 @@ + 'integer', + ]; + } + + public function section(): BelongsTo + { + return $this->belongsTo(ChecklistSection::class, 'checklist_section_id'); + } + + public function items(): HasMany + { + return $this->hasMany(ChecklistItem::class)->orderBy('order'); + } +} diff --git a/app/Models/ChecklistSection.php b/app/Models/ChecklistSection.php new file mode 100644 index 0000000..8c579e5 --- /dev/null +++ b/app/Models/ChecklistSection.php @@ -0,0 +1,50 @@ + 'boolean', + 'order' => 'integer', + ]; + } + + public function template(): BelongsTo + { + return $this->belongsTo(ChecklistTemplate::class, 'checklist_template_id'); + } + + public function scopes(): HasMany + { + return $this->hasMany(ChecklistScope::class)->orderBy('order'); + } + + public function items(): HasMany + { + return $this->hasMany(ChecklistItem::class)->orderBy('order'); + } + + public function formula(): HasMany + { + return $this->hasMany(ChecklistFormula::class); + } +} diff --git a/app/Models/ChecklistTemplate.php b/app/Models/ChecklistTemplate.php new file mode 100644 index 0000000..cf28a6b --- /dev/null +++ b/app/Models/ChecklistTemplate.php @@ -0,0 +1,47 @@ + 'boolean', + ]; + } + + public function sections(): HasMany + { + return $this->hasMany(ChecklistSection::class)->orderBy('order'); + } + + public function formulas(): HasMany + { + return $this->hasMany(ChecklistFormula::class)->orderBy('order'); + } + + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + public static function activeTemplate(): ?self + { + return static::active()->latest('id')->first(); + } +} diff --git a/app/Models/Consultant.php b/app/Models/Consultant.php new file mode 100644 index 0000000..e87830c --- /dev/null +++ b/app/Models/Consultant.php @@ -0,0 +1,68 @@ + 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['nama_perunding', 'no_pendaftaran_syarikat', 'emel', 'telefon', 'aktif', 'user_id']) + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->useLogName('perunding'); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + /** Pusat data yang sedang ditugaskan kepada perunding ini. */ + public function dataCentres(): HasMany + { + return $this->hasMany(DataCentre::class, 'current_consultant_id'); + } + + public function assignments(): HasMany + { + return $this->hasMany(DataCentreConsultantAssignment::class); + } + + public function submissions(): HasMany + { + return $this->hasMany(Submission::class); + } + + public function scopeAktif($query) + { + return $query->where('aktif', true); + } +} diff --git a/app/Models/DataCentre.php b/app/Models/DataCentre.php new file mode 100644 index 0000000..2b50cec --- /dev/null +++ b/app/Models/DataCentre.php @@ -0,0 +1,69 @@ + 'decimal:2', + 'it_load' => 'decimal:2', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['nama_pusat_data', 'tajuk_permohonan', 'status', 'current_consultant_id', 'it_load', 'keluasan_tapak']) + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->useLogName('pusat_data'); + } + + public function currentConsultant(): BelongsTo + { + return $this->belongsTo(Consultant::class, 'current_consultant_id'); + } + + public function assignments(): HasMany + { + return $this->hasMany(DataCentreConsultantAssignment::class)->latest('start_date'); + } + + public function activeAssignment() + { + return $this->hasOne(DataCentreConsultantAssignment::class)->where('is_active', true); + } + + public function submissions(): HasMany + { + return $this->hasMany(Submission::class); + } + + public function scopeAktif($query) + { + return $query->where('status', 'aktif'); + } +} diff --git a/app/Models/DataCentreConsultantAssignment.php b/app/Models/DataCentreConsultantAssignment.php new file mode 100644 index 0000000..70311a9 --- /dev/null +++ b/app/Models/DataCentreConsultantAssignment.php @@ -0,0 +1,46 @@ + 'date', + 'end_date' => 'date', + 'is_active' => 'boolean', + ]; + } + + public function dataCentre(): BelongsTo + { + return $this->belongsTo(DataCentre::class); + } + + public function consultant(): BelongsTo + { + return $this->belongsTo(Consultant::class); + } + + public function assignedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_by'); + } +} diff --git a/app/Models/ReportingCycle.php b/app/Models/ReportingCycle.php new file mode 100644 index 0000000..8387bde --- /dev/null +++ b/app/Models/ReportingCycle.php @@ -0,0 +1,77 @@ + 'integer', + 'reporting_year' => 'integer', + 'period_start' => 'date', + 'period_end' => 'date', + 'cut_off_date' => 'date', + 'licence_period_year' => 'integer', + ]; + } + + /** + * Peraturan perniagaan: tahun pelaporan = tahun pembaharuan - 2. + * Cth: pembaharuan 2028 => pelaporan 2026. + */ + public static function reportingYearFor(int $renewalYear): int + { + return $renewalYear - 2; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['renewal_year', 'reporting_year', 'cut_off_date', 'status']) + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->useLogName('kitaran_pelaporan'); + } + + public function checklistTemplate(): BelongsTo + { + return $this->belongsTo(ChecklistTemplate::class); + } + + public function submissions(): HasMany + { + return $this->hasMany(Submission::class); + } + + public function scopeAktif($query) + { + return $query->where('status', 'aktif'); + } + + public function isOverdue(): bool + { + return $this->cut_off_date && now()->startOfDay()->gt($this->cut_off_date); + } +} diff --git a/app/Models/Submission.php b/app/Models/Submission.php new file mode 100644 index 0000000..6b702a9 --- /dev/null +++ b/app/Models/Submission.php @@ -0,0 +1,152 @@ + 'Draf', + 'baru' => 'Baru', + 'dalam_tindakan' => 'Dalam Tindakan', + 'pembetulan_perunding' => 'Pembetulan Perunding', + 'selesai' => 'Selesai', + ]; + + protected $fillable = [ + 'data_centre_id', + 'reporting_cycle_id', + 'consultant_id', + 'checklist_template_id', + 'status', + 'is_locked', + 'locked_by', + 'locked_at', + 'lock_reason', + 'assigned_officer_id', + 'hardcopy_received', + 'hardcopy_received_date', + 'hardcopy_sent_date', + 'submitted_at', + 'submitted_by', + 'review_notes', + ]; + + protected function casts(): array + { + return [ + 'is_locked' => 'boolean', + 'locked_at' => 'datetime', + 'hardcopy_received' => 'boolean', + 'hardcopy_received_date' => 'date', + 'hardcopy_sent_date' => 'date', + 'submitted_at' => 'datetime', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['status', 'is_locked', 'assigned_officer_id', 'hardcopy_received', 'hardcopy_sent_date']) + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->useLogName('serahan'); + } + + public function dataCentre(): BelongsTo + { + return $this->belongsTo(DataCentre::class); + } + + public function reportingCycle(): BelongsTo + { + return $this->belongsTo(ReportingCycle::class); + } + + public function consultant(): BelongsTo + { + return $this->belongsTo(Consultant::class); + } + + public function checklistTemplate(): BelongsTo + { + return $this->belongsTo(ChecklistTemplate::class); + } + + public function assignedOfficer(): BelongsTo + { + return $this->belongsTo(User::class, 'assigned_officer_id'); + } + + public function answers(): HasMany + { + return $this->hasMany(SubmissionAnswer::class); + } + + public function results(): HasMany + { + return $this->hasMany(SubmissionResult::class); + } + + public function documents(): HasMany + { + return $this->hasMany(SubmissionDocument::class)->orderByDesc('version'); + } + + public function activeDocument(): HasOne + { + return $this->hasOne(SubmissionDocument::class)->where('is_active', true); + } + + public function statusHistories(): HasMany + { + return $this->hasMany(SubmissionStatusHistory::class)->latest(); + } + + public function correctionRequests(): HasMany + { + return $this->hasMany(SubmissionCorrectionRequest::class)->latest(); + } + + public function comments(): HasMany + { + return $this->hasMany(SubmissionComment::class)->oldest(); + } + + /** Perunding boleh edit selagi belum dikunci dan belum selesai. */ + public function isEditableByConsultant(): bool + { + return ! $this->is_locked && $this->status !== 'selesai'; + } + + public function isSubmitted(): bool + { + return ! in_array($this->status, ['draf'], true); + } + + public function statusLabel(): string + { + return self::STATUSES[$this->status] ?? $this->status; + } + + public function statusBadgeClass(): string + { + return [ + 'draf' => 'secondary', + 'baru' => 'info', + 'dalam_tindakan' => 'primary', + 'pembetulan_perunding' => 'warning', + 'selesai' => 'success', + ][$this->status] ?? 'secondary'; + } +} diff --git a/app/Models/SubmissionAnswer.php b/app/Models/SubmissionAnswer.php new file mode 100644 index 0000000..0d57fe1 --- /dev/null +++ b/app/Models/SubmissionAnswer.php @@ -0,0 +1,38 @@ +belongsTo(Submission::class); + } + + public function item(): BelongsTo + { + return $this->belongsTo(ChecklistItem::class, 'checklist_item_id'); + } + + public function numericValue(): ?float + { + return is_numeric($this->value) ? (float) $this->value : null; + } +} diff --git a/app/Models/SubmissionComment.php b/app/Models/SubmissionComment.php new file mode 100644 index 0000000..aa84c4e --- /dev/null +++ b/app/Models/SubmissionComment.php @@ -0,0 +1,40 @@ + 'boolean', + ]; + } + + public function submission(): BelongsTo + { + return $this->belongsTo(Submission::class); + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } +} diff --git a/app/Models/SubmissionCorrectionRequest.php b/app/Models/SubmissionCorrectionRequest.php new file mode 100644 index 0000000..83da8e0 --- /dev/null +++ b/app/Models/SubmissionCorrectionRequest.php @@ -0,0 +1,61 @@ + 'Terbuka', + 'dijawab' => 'Dijawab', + 'selesai' => 'Selesai', + ]; + + protected $fillable = [ + 'submission_id', + 'checklist_item_id', + 'page_reference', + 'location', + 'description', + 'status', + 'consultant_response', + 'responded_at', + 'responded_by', + 'created_by', + 'resolved_at', + 'resolved_by', + ]; + + protected function casts(): array + { + return [ + 'responded_at' => 'datetime', + 'resolved_at' => 'datetime', + ]; + } + + public function submission(): BelongsTo + { + return $this->belongsTo(Submission::class); + } + + public function item(): BelongsTo + { + return $this->belongsTo(ChecklistItem::class, 'checklist_item_id'); + } + + public function createdBy(): BelongsTo + { + return $this->belongsTo(User::class, 'created_by'); + } + + public function respondedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'responded_by'); + } +} diff --git a/app/Models/SubmissionDocument.php b/app/Models/SubmissionDocument.php new file mode 100644 index 0000000..3f506ba --- /dev/null +++ b/app/Models/SubmissionDocument.php @@ -0,0 +1,56 @@ + 'boolean', + 'size' => 'integer', + 'version' => 'integer', + ]; + } + + public function submission(): BelongsTo + { + return $this->belongsTo(Submission::class); + } + + public function uploadedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'uploaded_by'); + } + + public function humanSize(): string + { + $bytes = (int) $this->size; + if ($bytes >= 1048576) { + return round($bytes / 1048576, 2).' MB'; + } + if ($bytes >= 1024) { + return round($bytes / 1024, 2).' KB'; + } + + return $bytes.' B'; + } +} diff --git a/app/Models/SubmissionResult.php b/app/Models/SubmissionResult.php new file mode 100644 index 0000000..a1ad927 --- /dev/null +++ b/app/Models/SubmissionResult.php @@ -0,0 +1,32 @@ + 'decimal:4', + ]; + } + + public function submission(): BelongsTo + { + return $this->belongsTo(Submission::class); + } +} diff --git a/app/Models/SubmissionStatusHistory.php b/app/Models/SubmissionStatusHistory.php new file mode 100644 index 0000000..6ad7eee --- /dev/null +++ b/app/Models/SubmissionStatusHistory.php @@ -0,0 +1,30 @@ +belongsTo(Submission::class); + } + + public function changedBy(): BelongsTo + { + return $this->belongsTo(User::class, 'changed_by'); + } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..ecdb969 --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,72 @@ + 'datetime', + 'password' => 'hashed', + 'is_active' => 'boolean', + ]; + } + + public function getActivitylogOptions(): LogOptions + { + return LogOptions::defaults() + ->logOnly(['name', 'email', 'phone', 'jawatan', 'is_active']) + ->logOnlyDirty() + ->dontLogEmptyChanges() + ->useLogName('pengguna'); + } + + public function consultant(): HasOne + { + return $this->hasOne(Consultant::class); + } + + public function assignedSubmissions(): HasMany + { + return $this->hasMany(Submission::class, 'assigned_officer_id'); + } + + /** Adakah pengguna ini seorang perunding. */ + public function isPerunding(): bool + { + return $this->hasRole('Perunding'); + } + + /** Adakah pengguna ini kakitangan JPP (bukan perunding). */ + public function isJpp(): bool + { + return $this->hasAnyRole(['Super Admin', 'JPP Admin', 'Pegawai JPP', 'Penolong Pegawai', 'Kerani']); + } +} diff --git a/app/Policies/ChecklistTemplatePolicy.php b/app/Policies/ChecklistTemplatePolicy.php new file mode 100644 index 0000000..2010c38 --- /dev/null +++ b/app/Policies/ChecklistTemplatePolicy.php @@ -0,0 +1,34 @@ +can('templat.urus'); + } + + public function view(User $user, ChecklistTemplate $template): bool + { + return $user->can('templat.urus'); + } + + public function create(User $user): bool + { + return $user->can('templat.urus'); + } + + public function update(User $user, ChecklistTemplate $template): bool + { + return $user->can('templat.urus'); + } + + public function delete(User $user, ChecklistTemplate $template): bool + { + return $user->can('templat.urus'); + } +} diff --git a/app/Policies/ConsultantPolicy.php b/app/Policies/ConsultantPolicy.php new file mode 100644 index 0000000..7ab7755 --- /dev/null +++ b/app/Policies/ConsultantPolicy.php @@ -0,0 +1,39 @@ +can('perunding.urus'); + } + + public function view(User $user, Consultant $consultant): bool + { + if ($user->can('perunding.urus')) { + return true; + } + + // Perunding boleh lihat profil sendiri. + return $user->consultant && $user->consultant->id === $consultant->id; + } + + public function create(User $user): bool + { + return $user->can('perunding.urus'); + } + + public function update(User $user, Consultant $consultant): bool + { + return $user->can('perunding.urus'); + } + + public function delete(User $user, Consultant $consultant): bool + { + return $user->can('perunding.urus'); + } +} diff --git a/app/Policies/DataCentrePolicy.php b/app/Policies/DataCentrePolicy.php new file mode 100644 index 0000000..7dcb836 --- /dev/null +++ b/app/Policies/DataCentrePolicy.php @@ -0,0 +1,46 @@ +can('pusat_data.urus') || $user->can('serahan.urus_sendiri'); + } + + /** Perunding hanya boleh lihat pusat data yang sedang ditugaskan kepadanya. */ + public function view(User $user, DataCentre $dataCentre): bool + { + if ($user->can('pusat_data.urus')) { + return true; + } + + $consultantId = $user->consultant?->id; + + return $consultantId !== null && (int) $dataCentre->current_consultant_id === (int) $consultantId; + } + + public function create(User $user): bool + { + return $user->can('pusat_data.urus'); + } + + public function update(User $user, DataCentre $dataCentre): bool + { + return $user->can('pusat_data.urus'); + } + + public function delete(User $user, DataCentre $dataCentre): bool + { + return $user->can('pusat_data.urus'); + } + + public function assignConsultant(User $user, DataCentre $dataCentre): bool + { + return $user->can('tugasan.urus'); + } +} diff --git a/app/Policies/ReportingCyclePolicy.php b/app/Policies/ReportingCyclePolicy.php new file mode 100644 index 0000000..7e93b9a --- /dev/null +++ b/app/Policies/ReportingCyclePolicy.php @@ -0,0 +1,34 @@ +can('kitaran.urus') || $user->can('serahan.urus_sendiri'); + } + + public function view(User $user, ReportingCycle $cycle): bool + { + return $user->can('kitaran.urus') || $user->can('serahan.urus_sendiri'); + } + + public function create(User $user): bool + { + return $user->can('kitaran.urus'); + } + + public function update(User $user, ReportingCycle $cycle): bool + { + return $user->can('kitaran.urus'); + } + + public function delete(User $user, ReportingCycle $cycle): bool + { + return $user->can('kitaran.urus'); + } +} diff --git a/app/Policies/SubmissionPolicy.php b/app/Policies/SubmissionPolicy.php new file mode 100644 index 0000000..a249f3f --- /dev/null +++ b/app/Policies/SubmissionPolicy.php @@ -0,0 +1,80 @@ +can('serahan.lihat_semua')) { + return true; + } + + return $this->ownsSubmission($user, $submission); + } + + public function viewAny(User $user): bool + { + return $user->can('serahan.lihat_semua') || $user->can('serahan.urus_sendiri'); + } + + /** Hanya perunding boleh cipta serahan untuk pusat data yang ditugaskan. */ + public function create(User $user): bool + { + return $user->can('serahan.urus_sendiri'); + } + + /** Perunding boleh kemaskini selagi miliknya, belum dikunci & belum selesai. */ + public function update(User $user, Submission $submission): bool + { + if (! $user->can('serahan.urus_sendiri')) { + return false; + } + + return $this->ownsSubmission($user, $submission) && $submission->isEditableByConsultant(); + } + + /** Pegawai JPP menyemak serahan. */ + public function review(User $user, Submission $submission): bool + { + return $user->can('serahan.semak'); + } + + public function lock(User $user, Submission $submission): bool + { + return $user->can('serahan.kunci'); + } + + public function markHardcopy(User $user, Submission $submission): bool + { + return $user->can('serahan.hardcopy'); + } + + /** Perunding boleh jawab permohonan pembetulan untuk serahan miliknya. */ + public function respondCorrection(User $user, Submission $submission): bool + { + return $user->can('serahan.urus_sendiri') + && $this->ownsSubmission($user, $submission) + && $submission->status === 'pembetulan_perunding'; + } + + public function comment(User $user, Submission $submission): bool + { + if (! $user->can('komen.cipta')) { + return false; + } + + return $user->can('serahan.lihat_semua') || $this->ownsSubmission($user, $submission); + } + + protected function ownsSubmission(User $user, Submission $submission): bool + { + $consultantId = $user->consultant?->id; + + return $consultantId !== null && (int) $submission->consultant_id === (int) $consultantId; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..5953992 --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,25 @@ +hasRole('Super Admin') ? true : null; + }); + + Paginator::useBootstrapFive(); + } +} diff --git a/app/Services/AssignmentService.php b/app/Services/AssignmentService.php new file mode 100644 index 0000000..4600d73 --- /dev/null +++ b/app/Services/AssignmentService.php @@ -0,0 +1,80 @@ +assignments() + ->where('is_active', true) + ->get() + ->each(function (DataCentreConsultantAssignment $existing) use ($start) { + $existing->update([ + 'is_active' => false, + 'end_date' => $existing->end_date ?? $start->copy()->subDay(), + ]); + }); + + $assignment = $dataCentre->assignments()->create([ + 'consultant_id' => $consultant->id, + 'start_date' => $start, + 'reason' => $reason, + 'assigned_by' => $officer->id, + 'is_active' => true, + ]); + + $dataCentre->update(['current_consultant_id' => $consultant->id]); + + activity('penugasan') + ->performedOn($dataCentre) + ->causedBy($officer) + ->withProperties([ + 'consultant_id' => $consultant->id, + 'consultant' => $consultant->nama_perunding, + 'reason' => $reason, + ]) + ->log('Perunding ditugaskan kepada pusat data'); + + return $assignment; + }); + } + + /** + * Tamatkan penugasan aktif tanpa menggantikan dengan perunding baharu. + */ + public function endActive(DataCentre $dataCentre, User $officer, ?string $reason = null): void + { + DB::transaction(function () use ($dataCentre, $officer, $reason) { + $dataCentre->assignments()->where('is_active', true)->update([ + 'is_active' => false, + 'end_date' => now(), + 'reason' => $reason, + ]); + $dataCentre->update(['current_consultant_id' => null]); + + activity('penugasan') + ->performedOn($dataCentre) + ->causedBy($officer) + ->log('Penugasan perunding ditamatkan'); + }); + } +} diff --git a/app/Services/DocumentService.php b/app/Services/DocumentService.php new file mode 100644 index 0000000..2ddae59 --- /dev/null +++ b/app/Services/DocumentService.php @@ -0,0 +1,67 @@ +documents()->max('version') + 1; + + $hash = hash_file('sha256', $file->getRealPath()); + $path = $file->storeAs( + "reports/{$submission->id}", + 'v'.$nextVersion.'_'.uniqid().'.pdf', + 'local' + ); + + // Nyahaktifkan versi aktif sebelumnya. + $submission->documents()->where('is_active', true)->update(['is_active' => false]); + + $document = $submission->documents()->create([ + 'original_filename' => $file->getClientOriginalName(), + 'stored_path' => $path, + 'file_hash' => $hash, + 'mime_type' => $file->getClientMimeType(), + 'size' => $file->getSize(), + 'version' => $nextVersion, + 'is_active' => true, + 'uploaded_by' => $user->id, + ]); + + activity('dokumen') + ->performedOn($submission) + ->causedBy($user) + ->withProperties(['version' => $nextVersion, 'filename' => $document->original_filename]) + ->log('Laporan PDF dimuat naik'); + + return $document; + }); + } + + public function setActiveVersion(Submission $submission, SubmissionDocument $document): void + { + $submission->documents()->where('is_active', true)->update(['is_active' => false]); + $document->update(['is_active' => true]); + } + + public function download(SubmissionDocument $document) + { + abort_unless(Storage::disk('local')->exists($document->stored_path), 404, 'Fail laporan tidak dijumpai.'); + + return Storage::disk('local')->download($document->stored_path, $document->original_filename); + } +} diff --git a/app/Services/ExpressionEvaluator.php b/app/Services/ExpressionEvaluator.php new file mode 100644 index 0000000..99dc0ed --- /dev/null +++ b/app/Services/ExpressionEvaluator.php @@ -0,0 +1,204 @@ + */ + protected array $tokens = []; + + protected int $pos = 0; + + /** @var array */ + protected array $variables = []; + + protected bool $guardDivZero = false; + + protected bool $divByZeroHit = false; + + /** + * @param array $variables + */ + public function evaluate(string $expression, array $variables = [], bool $guardDivZero = false): ?float + { + $this->variables = array_map(fn ($v) => $v === null ? 0.0 : (float) $v, $variables); + $this->guardDivZero = $guardDivZero; + $this->divByZeroHit = false; + $this->tokens = $this->tokenize($expression); + $this->pos = 0; + + if (empty($this->tokens)) { + return null; + } + + $result = $this->parseExpression(); + + if ($this->pos < count($this->tokens)) { + throw new RuntimeException('Ungkapan formula tidak sah berhampiran: '.($this->tokens[$this->pos]['value'] ?? '')); + } + + if ($this->guardDivZero && $this->divByZeroHit) { + return 0.0; + } + + return $result; + } + + /** + * @return array + */ + protected function tokenize(string $expr): array + { + $tokens = []; + $len = strlen($expr); + $i = 0; + + while ($i < $len) { + $ch = $expr[$i]; + + if (ctype_space($ch)) { + $i++; + + continue; + } + + if (in_array($ch, ['+', '-', '*', '/', '(', ')'], true)) { + $tokens[] = ['type' => 'op', 'value' => $ch]; + $i++; + + continue; + } + + // Nombor (termasuk perpuluhan) + if (ctype_digit($ch) || ($ch === '.' && $i + 1 < $len && ctype_digit($expr[$i + 1]))) { + $num = ''; + while ($i < $len && (ctype_digit($expr[$i]) || $expr[$i] === '.')) { + $num .= $expr[$i]; + $i++; + } + $tokens[] = ['type' => 'num', 'value' => $num]; + + continue; + } + + // Token / pemboleh ubah: huruf, nombor, underscore (cth: SKOP1, A, D4I) + if (ctype_alpha($ch) || $ch === '_') { + $tok = ''; + while ($i < $len && (ctype_alnum($expr[$i]) || $expr[$i] === '_')) { + $tok .= $expr[$i]; + $i++; + } + $tokens[] = ['type' => 'var', 'value' => $tok]; + + continue; + } + + throw new RuntimeException("Aksara tidak sah dalam formula: '$ch'"); + } + + return $tokens; + } + + protected function peek(): ?array + { + return $this->tokens[$this->pos] ?? null; + } + + protected function consume(): ?array + { + return $this->tokens[$this->pos++] ?? null; + } + + /** expression = term (('+' | '-') term)* */ + protected function parseExpression(): float + { + $value = $this->parseTerm(); + + while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) { + $op = $this->consume()['value']; + $right = $this->parseTerm(); + $value = $op === '+' ? $value + $right : $value - $right; + } + + return $value; + } + + /** term = factor (('*' | '/') factor)* */ + protected function parseTerm(): float + { + $value = $this->parseFactor(); + + while (($t = $this->peek()) && $t['type'] === 'op' && in_array($t['value'], ['*', '/'], true)) { + $op = $this->consume()['value']; + $right = $this->parseFactor(); + + if ($op === '*') { + $value *= $right; + } else { + if ($right == 0.0) { + $this->divByZeroHit = true; + if ($this->guardDivZero) { + $value = 0.0; + + continue; + } + throw new RuntimeException('Pembahagian dengan sifar dalam formula.'); + } + $value /= $right; + } + } + + return $value; + } + + /** factor = number | variable | '(' expression ')' | ('+'|'-') factor */ + protected function parseFactor(): float + { + $t = $this->peek(); + + if ($t === null) { + throw new RuntimeException('Ungkapan formula tidak lengkap.'); + } + + if ($t['type'] === 'op' && $t['value'] === '(') { + $this->consume(); + $value = $this->parseExpression(); + $close = $this->consume(); + if (! $close || $close['value'] !== ')') { + throw new RuntimeException('Kurungan tidak seimbang dalam formula.'); + } + + return $value; + } + + if ($t['type'] === 'op' && in_array($t['value'], ['+', '-'], true)) { + $op = $this->consume()['value']; + $value = $this->parseFactor(); + + return $op === '-' ? -$value : $value; + } + + if ($t['type'] === 'num') { + $this->consume(); + + return (float) $t['value']; + } + + if ($t['type'] === 'var') { + $this->consume(); + + return $this->variables[$t['value']] ?? 0.0; + } + + throw new RuntimeException('Token formula tidak dijangka: '.$t['value']); + } +} diff --git a/app/Services/FormulaService.php b/app/Services/FormulaService.php new file mode 100644 index 0000000..c446e54 --- /dev/null +++ b/app/Services/FormulaService.php @@ -0,0 +1,100 @@ + + */ + public function computeForSubmission(Submission $submission): array + { + $template = $submission->checklistTemplate + ?: ($submission->checklist_template_id ? ChecklistTemplate::find($submission->checklist_template_id) : ChecklistTemplate::activeTemplate()); + + if (! $template) { + return []; + } + + // Bina peta token => nilai daripada jawapan item. + $variables = []; + $answers = $submission->relationLoaded('answers') + ? $submission->answers + : $submission->answers()->with('item')->get(); + + foreach ($answers as $answer) { + $item = $answer->item; + if ($item && $item->formula_token) { + $variables[$item->formula_token] = is_numeric($answer->value) ? (float) $answer->value : 0.0; + } + } + + return $this->compute($template, $variables); + } + + /** + * Kira hasil formula daripada peta pemboleh ubah mentah (berguna untuk ujian). + * + * @param array $variables + * @return array + */ + public function compute(ChecklistTemplate $template, array $variables): array + { + $formulas = $template->relationLoaded('formulas') + ? $template->formulas->sortBy('order') + : $template->formulas()->orderBy('order')->get(); + + $results = []; + + foreach ($formulas as $formula) { + $value = $this->evaluator->evaluate($formula->expression, $variables, $formula->guard_div_zero); + + // Hasil ini menjadi token yang boleh dirujuk oleh formula seterusnya. + $variables[$formula->result_token] = $value ?? 0.0; + + $results[] = [ + 'result_token' => $formula->result_token, + 'label' => $formula->label, + 'value' => $value, + 'unit' => $formula->unit, + ]; + } + + return $results; + } + + /** + * Kira semula dan simpan hasil ke dalam jadual submission_results. + */ + public function recalculateAndStore(Submission $submission): void + { + $results = $this->computeForSubmission($submission); + + foreach ($results as $r) { + $submission->results()->updateOrCreate( + ['result_token' => $r['result_token']], + [ + 'label' => $r['label'], + 'value' => $r['value'], + 'unit' => $r['unit'], + ] + ); + } + } +} diff --git a/app/Services/ReportingCycleService.php b/app/Services/ReportingCycleService.php new file mode 100644 index 0000000..351714f --- /dev/null +++ b/app/Services/ReportingCycleService.php @@ -0,0 +1,37 @@ + pelaporan 2026) + * - tempoh pelaporan: 1 Jan - 31 Dis tahun pelaporan + * - tarikh tutup lalai: hari/bulan yang dikonfigur JPP, tahun (renewal_year - 1) + */ + public function buildAttributes(int $renewalYear, ?string $cutOffDate = null): array + { + $reportingYear = ReportingCycle::reportingYearFor($renewalYear); + + $cutOffDay = (int) AppSetting::get('default_cut_off_day', 1); + $cutOffMonth = (int) AppSetting::get('default_cut_off_month', 12); + + // Pembaharuan dibuat sebelum/pada 1 Disember tahun sebelum pembaharuan. + $defaultCutOff = Carbon::create($renewalYear - 1, $cutOffMonth, $cutOffDay); + + return [ + 'reporting_year' => $reportingYear, + 'period_start' => Carbon::create($reportingYear, 1, 1), + 'period_end' => Carbon::create($reportingYear, 12, 31), + 'cut_off_date' => $cutOffDate ? Carbon::parse($cutOffDate) : $defaultCutOff, + 'checklist_template_id' => ChecklistTemplate::activeTemplate()?->id, + ]; + } +} diff --git a/app/Services/SubmissionWorkflowService.php b/app/Services/SubmissionWorkflowService.php new file mode 100644 index 0000000..a059eea --- /dev/null +++ b/app/Services/SubmissionWorkflowService.php @@ -0,0 +1,144 @@ +current_consultant_id; + + if (! $consultantId) { + throw ValidationException::withMessages([ + 'data_centre_id' => 'Pusat data ini tiada perunding aktif. Sila hubungi JPP.', + ]); + } + + return Submission::firstOrCreate( + [ + 'data_centre_id' => $dataCentre->id, + 'reporting_cycle_id' => $cycle->id, + ], + [ + 'consultant_id' => $consultantId, + 'checklist_template_id' => $cycle->checklist_template_id ?? ChecklistTemplate::activeTemplate()?->id, + 'status' => 'draf', + ] + ); + } + + /** + * Simpan jawapan senarai semak (draf). $answers = [item_id => [value, unit, page_reference, consultant_note]] + */ + public function saveAnswers(Submission $submission, array $answers): void + { + DB::transaction(function () use ($submission, $answers) { + foreach ($answers as $itemId => $data) { + $submission->answers()->updateOrCreate( + ['checklist_item_id' => $itemId], + [ + 'value' => $data['value'] ?? null, + 'unit' => $data['unit'] ?? null, + 'page_reference' => $data['page_reference'] ?? null, + 'consultant_note' => $data['consultant_note'] ?? null, + ] + ); + } + + // Kira semula nilai formula (A, B, C, D, E ...). + $submission->load('answers.item'); + $this->formulaService->recalculateAndStore($submission); + }); + } + + /** + * Hantar serahan secara muktamad (draf -> baru). + */ + public function submit(Submission $submission, User $user): void + { + if ($submission->is_locked) { + throw ValidationException::withMessages(['status' => 'Serahan telah dikunci dan tidak boleh dihantar semula.']); + } + + // Mesti ada sekurang-kurangnya satu laporan PDF aktif sebelum hantar. + if (! $submission->activeDocument()->exists()) { + throw ValidationException::withMessages(['document' => 'Sila muat naik laporan PDF sebelum menghantar serahan.']); + } + + $from = $submission->status; + // Jika sedang dalam pembetulan, hantar semula sebagai 'dalam_tindakan'. + $to = $from === 'pembetulan_perunding' ? 'dalam_tindakan' : 'baru'; + + $this->changeStatus($submission, $to, $user, 'Serahan dihantar oleh perunding.', [ + 'submitted_at' => now(), + 'submitted_by' => $user->id, + ]); + + $this->formulaService->recalculateAndStore($submission); + } + + /** + * Tukar status serahan dengan rekod sejarah & audit. + */ + public function changeStatus(Submission $submission, string $to, User $user, ?string $notes = null, array $extra = []): void + { + DB::transaction(function () use ($submission, $to, $user, $notes, $extra) { + $from = $submission->status; + + $submission->fill(array_merge(['status' => $to], $extra)); + $submission->save(); + + $submission->statusHistories()->create([ + 'from_status' => $from, + 'to_status' => $to, + 'changed_by' => $user->id, + 'notes' => $notes, + ]); + + activity('serahan') + ->performedOn($submission) + ->causedBy($user) + ->withProperties(['from' => $from, 'to' => $to, 'notes' => $notes]) + ->log("Status serahan ditukar daripada {$from} kepada {$to}"); + }); + } + + public function lock(Submission $submission, User $user, ?string $reason = null): void + { + $submission->update([ + 'is_locked' => true, + 'locked_by' => $user->id, + 'locked_at' => now(), + 'lock_reason' => $reason, + ]); + + activity('serahan')->performedOn($submission)->causedBy($user) + ->withProperties(['reason' => $reason])->log('Serahan dikunci'); + } + + public function unlock(Submission $submission, User $user, string $reason): void + { + $submission->update([ + 'is_locked' => false, + 'lock_reason' => $reason, + 'locked_by' => $user->id, + 'locked_at' => now(), + ]); + + activity('serahan')->performedOn($submission)->causedBy($user) + ->withProperties(['reason' => $reason])->log('Serahan dibuka semula'); + } +} diff --git a/artisan b/artisan new file mode 100644 index 0000000..c35e31d --- /dev/null +++ b/artisan @@ -0,0 +1,18 @@ +#!/usr/bin/env php +handleCommand(new ArgvInput); + +exit($status); diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..f8e9437 --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,26 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware): void { + // Alias middleware kebenaran/peranan daripada spatie/laravel-permission. + $middleware->alias([ + 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class, + 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class, + 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions): void { + $exceptions->shouldRenderJsonWhen( + fn (Request $request) => $request->is('api/*'), + ); + })->create(); diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/bootstrap/cache/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..fc94ae6 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,7 @@ +=5.0.0" + }, + "require-dev": { + "doctrine/dbal": "^4.0.0", + "nesbot/carbon": "^2.71.0 || ^3.0.0", + "phpunit/phpunit": "^10.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "KyleKatarn", + "email": "kylekatarnls@gmail.com" + } + ], + "description": "Types to use Carbon in Doctrine", + "keywords": [ + "carbon", + "date", + "datetime", + "doctrine", + "time" + ], + "support": { + "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon", + "type": "open_collective" + }, + { + "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon", + "type": "tidelift" + } + ], + "time": "2024-02-09T16:56:22+00:00" + }, + { + "name": "dflydev/dot-access-data", + "version": "v3.0.3", + "source": { + "type": "git", + "url": "https://github.com/dflydev/dflydev-dot-access-data.git", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^0.12.42", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3", + "scrutinizer/ocular": "1.6.0", + "squizlabs/php_codesniffer": "^3.5", + "vimeo/psalm": "^4.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Dflydev\\DotAccessData\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Dragonfly Development Inc.", + "email": "info@dflydev.com", + "homepage": "http://dflydev.com" + }, + { + "name": "Beau Simensen", + "email": "beau@dflydev.com", + "homepage": "http://beausimensen.com" + }, + { + "name": "Carlos Frutos", + "email": "carlos@kiwing.it", + "homepage": "https://github.com/cfrutos" + }, + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com" + } + ], + "description": "Given a deep data structure, access data by dot notation.", + "homepage": "https://github.com/dflydev/dflydev-dot-access-data", + "keywords": [ + "access", + "data", + "dot", + "notation" + ], + "support": { + "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues", + "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3" + }, + "time": "2024-07-08T12:26:09+00:00" + }, + { + "name": "doctrine/inflector", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/inflector.git", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "doctrine/coding-standard": "^12.0 || ^13.0", + "phpstan/phpstan": "^1.12 || ^2.0", + "phpstan/phpstan-phpunit": "^1.4 || ^2.0", + "phpstan/phpstan-strict-rules": "^1.6 || ^2.0", + "phpunit/phpunit": "^8.5 || ^12.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Inflector\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.", + "homepage": "https://www.doctrine-project.org/projects/inflector.html", + "keywords": [ + "inflection", + "inflector", + "lowercase", + "manipulation", + "php", + "plural", + "singular", + "strings", + "uppercase", + "words" + ], + "support": { + "issues": "https://github.com/doctrine/inflector/issues", + "source": "https://github.com/doctrine/inflector/tree/2.1.0" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector", + "type": "tidelift" + } + ], + "time": "2025-08-10T19:31:58+00:00" + }, + { + "name": "doctrine/lexer", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/doctrine/lexer.git", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "doctrine/coding-standard": "^12", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5", + "psalm/plugin-phpunit": "^0.18.3", + "vimeo/psalm": "^5.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Common\\Lexer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "https://www.doctrine-project.org/projects/lexer.html", + "keywords": [ + "annotations", + "docblock", + "lexer", + "parser", + "php" + ], + "support": { + "issues": "https://github.com/doctrine/lexer/issues", + "source": "https://github.com/doctrine/lexer/tree/3.0.1" + }, + "funding": [ + { + "url": "https://www.doctrine-project.org/sponsorship.html", + "type": "custom" + }, + { + "url": "https://www.patreon.com/phpdoctrine", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer", + "type": "tidelift" + } + ], + "time": "2024-02-05T11:56:58+00:00" + }, + { + "name": "dragonmantank/cron-expression", + "version": "v3.6.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013", + "shasum": "" + }, + "require": { + "php": "^8.2|^8.3|^8.4|^8.5" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.32|^2.1.31", + "phpunit/phpunit": "^8.5.48|^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "time": "2025-10-31T18:51:33+00:00" + }, + { + "name": "egulias/email-validator", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/egulias/EmailValidator.git", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa", + "shasum": "" + }, + "require": { + "doctrine/lexer": "^2.0 || ^3.0", + "php": ">=8.1", + "symfony/polyfill-intl-idn": "^1.26" + }, + "require-dev": { + "phpunit/phpunit": "^10.2", + "vimeo/psalm": "^5.12" + }, + "suggest": { + "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Egulias\\EmailValidator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Eduardo Gulias Davis" + } + ], + "description": "A library for validating emails against several RFCs", + "homepage": "https://github.com/egulias/EmailValidator", + "keywords": [ + "email", + "emailvalidation", + "emailvalidator", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/egulias/EmailValidator/issues", + "source": "https://github.com/egulias/EmailValidator/tree/4.0.4" + }, + "funding": [ + { + "url": "https://github.com/egulias", + "type": "github" + } + ], + "time": "2025-03-06T22:45:56+00:00" + }, + { + "name": "fruitcake/php-cors", + "version": "v1.4.0", + "source": { + "type": "git", + "url": "https://github.com/fruitcake/php-cors.git", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379", + "shasum": "" + }, + "require": { + "php": "^8.1", + "symfony/http-foundation": "^5.4|^6.4|^7.3|^8" + }, + "require-dev": { + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^9", + "squizlabs/php_codesniffer": "^4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Fruitcake\\Cors\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fruitcake", + "homepage": "https://fruitcake.nl" + }, + { + "name": "Barryvdh", + "email": "barryvdh@gmail.com" + } + ], + "description": "Cross-origin resource sharing library for the Symfony HttpFoundation", + "homepage": "https://github.com/fruitcake/php-cors", + "keywords": [ + "cors", + "laravel", + "symfony" + ], + "support": { + "issues": "https://github.com/fruitcake/php-cors/issues", + "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2025-12-03T09:33:47+00:00" + }, + { + "name": "graham-campbell/result-type", + "version": "v1.1.4", + "source": { + "type": "git", + "url": "https://github.com/GrahamCampbell/Result-Type.git", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", + "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "GrahamCampbell\\ResultType\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "An Implementation Of The Result Type", + "keywords": [ + "Graham Campbell", + "GrahamCampbell", + "Result Type", + "Result-Type", + "result" + ], + "support": { + "issues": "https://github.com/GrahamCampbell/Result-Type/issues", + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:43:20+00:00" + }, + { + "name": "guzzlehttp/guzzle", + "version": "7.12.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "d34627490fbc03bf5c5d7cfed81f2faa19519425" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d34627490fbc03bf5c5d7cfed81f2faa19519425", + "reference": "d34627490fbc03bf5c5d7cfed81f2faa19519425", + "shasum": "" + }, + "require": { + "ext-json": "*", + "guzzlehttp/promises": "^2.5", + "guzzlehttp/psr7": "^2.12.1", + "php": "^7.2.5 || ^8.0", + "psr/http-client": "^1.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" + }, + "provide": { + "psr/http-client-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-curl": "*", + "guzzle/client-integration-tests": "3.0.2", + "guzzlehttp/test-server": "^0.5.1", + "php-http/message-factory": "^1.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "psr/log": "^1.1 || ^2.0 || ^3.0" + }, + "suggest": { + "ext-curl": "Required for CURL handler support", + "ext-intl": "Required for Internationalized Domain Name (IDN) support", + "psr/log": "Required for using the Log middleware" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "files": [ + "src/functions_include.php" + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle is a PHP HTTP client library", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "psr-18", + "psr-7", + "rest", + "web service" + ], + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/7.12.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2026-06-18T14:12:49+00:00" + }, + { + "name": "guzzlehttp/promises", + "version": "2.5.0", + "source": { + "type": "git", + "url": "https://github.com/guzzle/promises.git", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e", + "reference": "4360e982f87f5f258bf872d094647791db2f4c8e", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + } + ], + "description": "Guzzle promises library", + "keywords": [ + "promise" + ], + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/2.5.0" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2026-06-02T12:23:43+00:00" + }, + { + "name": "guzzlehttp/psr7", + "version": "2.12.1", + "source": { + "type": "git", + "url": "https://github.com/guzzle/psr7.git", + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "reference": "172ef2f4e9824c1e058b7f30be8ae25a02c0f2b7", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "psr/http-factory": "^1.0", + "psr/http-message": "^1.1 || ^2.0", + "ralouphie/getallheaders": "^3.0", + "symfony/deprecation-contracts": "^2.5 || ^3.0", + "symfony/polyfill-php80": "^1.24" + }, + "provide": { + "psr/http-factory-implementation": "1.0", + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "http-interop/http-factory-tests": "1.1.0", + "jshttp/mime-db": "1.54.0.1", + "phpunit/phpunit": "^8.5.52 || ^9.6.34" + }, + "suggest": { + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://sagikazarmark.hu" + } + ], + "description": "PSR-7 message implementation that also provides common utility methods", + "keywords": [ + "http", + "message", + "psr-7", + "request", + "response", + "stream", + "uri", + "url" + ], + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/2.12.1" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" + } + ], + "time": "2026-06-18T09:49:37+00:00" + }, + { + "name": "guzzlehttp/uri-template", + "version": "v1.0.7", + "source": { + "type": "git", + "url": "https://github.com/guzzle/uri-template.git", + "reference": "7fe811c23a9e3cd712b4389eaeb50b5456d8c529" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7fe811c23a9e3cd712b4389eaeb50b5456d8c529", + "reference": "7fe811c23a9e3cd712b4389eaeb50b5456d8c529", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0", + "symfony/polyfill-php80": "^1.24" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.52 || ^9.6.34", + "uri-template/tests": "1.0.0" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + } + }, + "autoload": { + "psr-4": { + "GuzzleHttp\\UriTemplate\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + } + ], + "description": "A polyfill class for uri_template of PHP", + "keywords": [ + "guzzlehttp", + "uri-template" + ], + "support": { + "issues": "https://github.com/guzzle/uri-template/issues", + "source": "https://github.com/guzzle/uri-template/tree/v1.0.7" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template", + "type": "tidelift" + } + ], + "time": "2026-06-12T21:33:43+00:00" + }, + { + "name": "laravel/framework", + "version": "v13.16.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/framework.git", + "reference": "6135650d69bd9442e470bb1b343422081b076f1e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/framework/zipball/6135650d69bd9442e470bb1b343422081b076f1e", + "reference": "6135650d69bd9442e470bb1b343422081b076f1e", + "shasum": "" + }, + "require": { + "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17", + "composer-runtime-api": "^2.2", + "doctrine/inflector": "^2.0.5", + "dragonmantank/cron-expression": "^3.4", + "egulias/email-validator": "^4.0", + "ext-ctype": "*", + "ext-filter": "*", + "ext-hash": "*", + "ext-mbstring": "*", + "ext-openssl": "*", + "ext-session": "*", + "ext-tokenizer": "*", + "fruitcake/php-cors": "^1.3", + "guzzlehttp/guzzle": "^7.8.2", + "guzzlehttp/promises": "^2.0.3", + "guzzlehttp/uri-template": "^1.0", + "laravel/prompts": "^0.3.0", + "laravel/serializable-closure": "^2.0.10", + "league/commonmark": "^2.8.1", + "league/flysystem": "^3.25.1", + "league/flysystem-local": "^3.25.1", + "league/uri": "^7.5.1", + "monolog/monolog": "^3.0", + "nesbot/carbon": "^3.8.4", + "nunomaduro/termwind": "^2.0", + "php": "^8.3", + "psr/container": "^1.1.1 || ^2.0.1", + "psr/log": "^1.0 || ^2.0 || ^3.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", + "ramsey/uuid": "^4.7", + "symfony/console": "^7.4.0 || ^8.0.0", + "symfony/error-handler": "^7.4.0 || ^8.0.0", + "symfony/finder": "^7.4.0 || ^8.0.0", + "symfony/http-foundation": "^7.4.0 || ^8.0.0", + "symfony/http-kernel": "^7.4.0 || ^8.0.0", + "symfony/mailer": "^7.4.0 || ^8.0.0", + "symfony/mime": "^7.4.0 || ^8.0.0", + "symfony/polyfill-php84": "^1.36", + "symfony/polyfill-php85": "^1.36", + "symfony/polyfill-php86": "^1.36", + "symfony/process": "^7.4.5 || ^8.0.5", + "symfony/routing": "^7.4.0 || ^8.0.0", + "symfony/uid": "^7.4.0 || ^8.0.0", + "symfony/var-dumper": "^7.4.0 || ^8.0.0", + "tijsverkoyen/css-to-inline-styles": "^2.2.5", + "vlucas/phpdotenv": "^5.6.1", + "voku/portable-ascii": "^2.0.2" + }, + "conflict": { + "tightenco/collect": "<5.5.33" + }, + "provide": { + "psr/container-implementation": "1.1 || 2.0", + "psr/log-implementation": "1.0 || 2.0 || 3.0", + "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0" + }, + "replace": { + "illuminate/auth": "self.version", + "illuminate/broadcasting": "self.version", + "illuminate/bus": "self.version", + "illuminate/cache": "self.version", + "illuminate/collections": "self.version", + "illuminate/concurrency": "self.version", + "illuminate/conditionable": "self.version", + "illuminate/config": "self.version", + "illuminate/console": "self.version", + "illuminate/container": "self.version", + "illuminate/contracts": "self.version", + "illuminate/cookie": "self.version", + "illuminate/database": "self.version", + "illuminate/encryption": "self.version", + "illuminate/events": "self.version", + "illuminate/filesystem": "self.version", + "illuminate/hashing": "self.version", + "illuminate/http": "self.version", + "illuminate/json-schema": "self.version", + "illuminate/log": "self.version", + "illuminate/macroable": "self.version", + "illuminate/mail": "self.version", + "illuminate/notifications": "self.version", + "illuminate/pagination": "self.version", + "illuminate/pipeline": "self.version", + "illuminate/process": "self.version", + "illuminate/queue": "self.version", + "illuminate/redis": "self.version", + "illuminate/reflection": "self.version", + "illuminate/routing": "self.version", + "illuminate/session": "self.version", + "illuminate/support": "self.version", + "illuminate/testing": "self.version", + "illuminate/translation": "self.version", + "illuminate/validation": "self.version", + "illuminate/view": "self.version", + "spatie/once": "*" + }, + "require-dev": { + "ably/ably-php": "^1.0", + "aws/aws-sdk-php": "^3.322.9", + "ext-gmp": "*", + "fakerphp/faker": "^1.24", + "guzzlehttp/psr7": "^2.9", + "laravel/pint": "^1.18", + "league/flysystem-aws-s3-v3": "^3.25.1", + "league/flysystem-ftp": "^3.25.1", + "league/flysystem-path-prefixing": "^3.25.1", + "league/flysystem-read-only": "^3.25.1", + "league/flysystem-sftp-v3": "^3.25.1", + "mockery/mockery": "^1.6.10", + "opis/json-schema": "^2.4.1", + "orchestra/testbench-core": "^11.0.0", + "pda/pheanstalk": "^7.0.0 || ^8.0.0", + "php-http/discovery": "^1.15", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", + "predis/predis": "^2.3 || ^3.0", + "rector/rector": "^2.3", + "resend/resend-php": "^1.0", + "symfony/cache": "^7.4.0 || ^8.0.0", + "symfony/http-client": "^7.4.0 || ^8.0.0", + "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0", + "symfony/translation": "^7.4.0 || ^8.0.0" + }, + "suggest": { + "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).", + "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).", + "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).", + "ext-apcu": "Required to use the APC cache driver.", + "ext-fileinfo": "Required to use the Filesystem class.", + "ext-ftp": "Required to use the Flysystem FTP driver.", + "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().", + "ext-memcached": "Required to use the memcache cache driver.", + "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.", + "ext-pdo": "Required to use all database features.", + "ext-posix": "Required to use all features of the queue worker.", + "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).", + "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).", + "filp/whoops": "Required for friendly error pages in development (^2.14.3).", + "laravel/tinker": "Required to use the tinker console command (^2.0).", + "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).", + "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).", + "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).", + "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)", + "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).", + "mockery/mockery": "Required to use mocking (^1.6).", + "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).", + "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", + "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", + "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", + "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", + "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", + "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", + "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", + "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).", + "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).", + "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).", + "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).", + "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).", + "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "13.0.x-dev" + } + }, + "autoload": { + "files": [ + "src/Illuminate/Collections/functions.php", + "src/Illuminate/Collections/helpers.php", + "src/Illuminate/Events/functions.php", + "src/Illuminate/Filesystem/functions.php", + "src/Illuminate/Foundation/helpers.php", + "src/Illuminate/Log/functions.php", + "src/Illuminate/Reflection/helpers.php", + "src/Illuminate/Support/functions.php", + "src/Illuminate/Support/helpers.php" + ], + "psr-4": { + "Illuminate\\": "src/Illuminate/", + "Illuminate\\Support\\": [ + "src/Illuminate/Macroable/", + "src/Illuminate/Collections/", + "src/Illuminate/Conditionable/", + "src/Illuminate/Reflection/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "The Laravel Framework.", + "homepage": "https://laravel.com", + "keywords": [ + "framework", + "laravel" + ], + "support": { + "issues": "https://github.com/laravel/framework/issues", + "source": "https://github.com/laravel/framework" + }, + "time": "2026-06-16T16:07:50+00:00" + }, + { + "name": "laravel/prompts", + "version": "v0.3.18", + "source": { + "type": "git", + "url": "https://github.com/laravel/prompts.git", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72", + "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.2", + "ext-mbstring": "*", + "php": "^8.1", + "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|^13.0", + "mockery/mockery": "^1.5", + "pestphp/pest": "^2.3|^3.4|^4.0", + "phpstan/phpstan": "^1.12.28", + "phpstan/phpstan-mockery": "^1.1.3" + }, + "suggest": { + "ext-pcntl": "Required for the spinner to be animated." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "0.3.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Laravel\\Prompts\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "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.18" + }, + "time": "2026-05-19T00:47:18+00:00" + }, + { + "name": "laravel/serializable-closure", + "version": "v2.0.13", + "source": { + "type": "git", + "url": "https://github.com/laravel/serializable-closure.git", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "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|^8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\SerializableClosure\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "nuno@laravel.com" + } + ], + "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.", + "keywords": [ + "closure", + "laravel", + "serializable" + ], + "support": { + "issues": "https://github.com/laravel/serializable-closure/issues", + "source": "https://github.com/laravel/serializable-closure" + }, + "time": "2026-04-16T14:03:50+00:00" + }, + { + "name": "laravel/tinker", + "version": "v3.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/tinker.git", + "reference": "4faba77764bd33411735936acdf30446d058c78b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b", + "reference": "4faba77764bd33411735936acdf30446d058c78b", + "shasum": "" + }, + "require": { + "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0", + "php": "^8.1", + "psy/psysh": "^0.12.0", + "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0" + }, + "require-dev": { + "mockery/mockery": "~1.3.3|^1.4.2", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.5|^11.5" + }, + "suggest": { + "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)." + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Tinker\\TinkerServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Tinker\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Powerful REPL for the Laravel framework.", + "keywords": [ + "REPL", + "Tinker", + "laravel", + "psysh" + ], + "support": { + "issues": "https://github.com/laravel/tinker/issues", + "source": "https://github.com/laravel/tinker/tree/v3.0.2" + }, + "time": "2026-03-17T14:54:13+00:00" + }, + { + "name": "league/commonmark", + "version": "2.8.2", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/commonmark.git", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", + "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "league/config": "^1.1.1", + "php": "^7.4 || ^8.0", + "psr/event-dispatcher": "^1.0", + "symfony/deprecation-contracts": "^2.1 || ^3.0", + "symfony/polyfill-php80": "^1.16" + }, + "require-dev": { + "cebe/markdown": "^1.0", + "commonmark/cmark": "0.31.1", + "commonmark/commonmark.js": "0.31.1", + "composer/package-versions-deprecated": "^1.8", + "embed/embed": "^4.4", + "erusev/parsedown": "^1.0", + "ext-json": "*", + "github/gfm": "0.29.0", + "michelf/php-markdown": "^1.4 || ^2.0", + "nyholm/psr7": "^1.5", + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0", + "scrutinizer/ocular": "^1.8.1", + "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0", + "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0", + "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0", + "unleashedtech/php-coding-standard": "^3.1.1", + "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0" + }, + "suggest": { + "symfony/yaml": "v2.3+ required if using the Front Matter extension" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.9-dev" + } + }, + "autoload": { + "psr-4": { + "League\\CommonMark\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)", + "homepage": "https://commonmark.thephpleague.com", + "keywords": [ + "commonmark", + "flavored", + "gfm", + "github", + "github-flavored", + "markdown", + "md", + "parser" + ], + "support": { + "docs": "https://commonmark.thephpleague.com/", + "forum": "https://github.com/thephpleague/commonmark/discussions", + "issues": "https://github.com/thephpleague/commonmark/issues", + "rss": "https://github.com/thephpleague/commonmark/releases.atom", + "source": "https://github.com/thephpleague/commonmark" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/commonmark", + "type": "tidelift" + } + ], + "time": "2026-03-19T13:16:38+00:00" + }, + { + "name": "league/config", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/config.git", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3", + "shasum": "" + }, + "require": { + "dflydev/dot-access-data": "^3.0.1", + "nette/schema": "^1.2", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.8.2", + "phpunit/phpunit": "^9.5.5", + "scrutinizer/ocular": "^1.8.1", + "unleashedtech/php-coding-standard": "^3.1", + "vimeo/psalm": "^4.7.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Config\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + } + ], + "description": "Define configuration arrays with strict schemas and access values with dot notation", + "homepage": "https://config.thephpleague.com", + "keywords": [ + "array", + "config", + "configuration", + "dot", + "dot-access", + "nested", + "schema" + ], + "support": { + "docs": "https://config.thephpleague.com/", + "issues": "https://github.com/thephpleague/config/issues", + "rss": "https://github.com/thephpleague/config/releases.atom", + "source": "https://github.com/thephpleague/config" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + } + ], + "time": "2022-12-11T20:36:23+00:00" + }, + { + "name": "league/flysystem", + "version": "3.35.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/d0a405f03d980461e4b6e08cfed2c162650f9f6b", + "reference": "d0a405f03d980461e4b6e08cfed2c162650f9f6b", + "shasum": "" + }, + "require": { + "league/flysystem-local": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "conflict": { + "async-aws/core": "<1.19.0", + "async-aws/s3": "<1.14.0", + "aws/aws-sdk-php": "3.209.31 || 3.210.0", + "guzzlehttp/guzzle": "<7.0", + "guzzlehttp/ringphp": "<1.1.1", + "phpseclib/phpseclib": "3.0.15", + "symfony/http-client": "<5.2" + }, + "require-dev": { + "async-aws/s3": "^1.5 || ^2.0", + "async-aws/simple-s3": "^1.1 || ^2.0", + "aws/aws-sdk-php": "^3.295.10", + "composer/semver": "^3.0", + "ext-fileinfo": "*", + "ext-ftp": "*", + "ext-mongodb": "^1.3|^2", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.5", + "google/cloud-storage": "^1.23", + "guzzlehttp/psr7": "^2.6", + "microsoft/azure-storage-blob": "^1.1", + "mongodb/mongodb": "^1.2|^2", + "phpseclib/phpseclib": "^3.0.36", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.5.11|^10.0", + "sabre/dav": "^4.6.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "File storage abstraction for PHP", + "keywords": [ + "WebDAV", + "aws", + "cloud", + "file", + "files", + "filesystem", + "filesystems", + "ftp", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/3.35.0" + }, + "time": "2026-06-22T20:12:06+00:00" + }, + { + "name": "league/flysystem-local", + "version": "3.31.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-local.git", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "league/flysystem": "^3.0.0", + "league/mime-type-detection": "^1.0.0", + "php": "^8.0.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\Flysystem\\Local\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Local filesystem adapter for Flysystem.", + "keywords": [ + "Flysystem", + "file", + "files", + "filesystem", + "local" + ], + "support": { + "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + }, + "time": "2026-01-23T15:30:45+00:00" + }, + { + "name": "league/mime-type-detection", + "version": "1.16.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/mime-type-detection.git", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9", + "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "phpstan/phpstan": "^0.12.68", + "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "League\\MimeTypeDetection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frankdejonge.nl" + } + ], + "description": "Mime-type detection for Flysystem", + "support": { + "issues": "https://github.com/thephpleague/mime-type-detection/issues", + "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0" + }, + "funding": [ + { + "url": "https://github.com/frankdejonge", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/flysystem", + "type": "tidelift" + } + ], + "time": "2024-09-21T08:32:55+00:00" + }, + { + "name": "league/uri", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri.git", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4", + "reference": "08cf38e3924d4f56238125547b5720496fac8fd4", + "shasum": "" + }, + "require": { + "league/uri-interfaces": "^7.8.1", + "php": "^8.1", + "psr/http-factory": "^1" + }, + "conflict": { + "league/uri-schemes": "^1.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-dom": "to convert the URI into an HTML anchor tag", + "ext-fileinfo": "to create Data URI from file contennts", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "ext-uri": "to use the PHP native URI class", + "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain", + "league/uri-components": "to provide additional tools to manipulate URI objects components", + "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "URI manipulation library", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "URN", + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "middleware", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc2141", + "rfc3986", + "rfc3987", + "rfc6570", + "rfc8141", + "uri", + "uri-template", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-15T20:22:25+00:00" + }, + { + "name": "league/uri-interfaces", + "version": "7.8.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/uri-interfaces.git", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928", + "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "php": "^8.1", + "psr/http-message": "^1.1 || ^2.0" + }, + "suggest": { + "ext-bcmath": "to improve IPV4 host parsing", + "ext-gmp": "to improve IPV4 host parsing", + "ext-intl": "to handle IDN host with the best performance", + "php-64bit": "to improve IPV4 host parsing", + "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification", + "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "7.x-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Uri\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ignace Nyamagana Butera", + "email": "nyamsprod@gmail.com", + "homepage": "https://nyamsprod.com" + } + ], + "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI", + "homepage": "https://uri.thephpleague.com", + "keywords": [ + "data-uri", + "file-uri", + "ftp", + "hostname", + "http", + "https", + "parse_str", + "parse_url", + "psr-7", + "query-string", + "querystring", + "rfc3986", + "rfc3987", + "rfc6570", + "uri", + "url", + "ws" + ], + "support": { + "docs": "https://uri.thephpleague.com", + "forum": "https://thephpleague.slack.com", + "issues": "https://github.com/thephpleague/uri-src/issues", + "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1" + }, + "funding": [ + { + "url": "https://github.com/sponsors/nyamsprod", + "type": "github" + } + ], + "time": "2026-03-08T20:05:35+00:00" + }, + { + "name": "monolog/monolog", + "version": "3.10.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/log": "^2.0 || ^3.0" + }, + "provide": { + "psr/log-implementation": "3.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.0", + "doctrine/couchdb": "~1.0@dev", + "elasticsearch/elasticsearch": "^7 || ^8", + "ext-json": "*", + "graylog2/gelf-php": "^1.4.2 || ^2.0", + "guzzlehttp/guzzle": "^7.4.5", + "guzzlehttp/psr7": "^2.2", + "mongodb/mongodb": "^1.8 || ^2.0", + "php-amqplib/php-amqplib": "~2.4 || ^3", + "php-console/php-console": "^3.1.8", + "phpstan/phpstan": "^2", + "phpstan/phpstan-deprecation-rules": "^2", + "phpstan/phpstan-strict-rules": "^2", + "phpunit/phpunit": "^10.5.17 || ^11.0.7", + "predis/predis": "^1.1 || ^2", + "rollbar/rollbar": "^4.0", + "ruflin/elastica": "^7 || ^8", + "symfony/mailer": "^5.4 || ^6", + "symfony/mime": "^5.4 || ^6" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler", + "ext-mbstring": "Allow to work properly with unicode symbols", + "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", + "ext-openssl": "Required to send log messages using SSL", + "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "https://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "https://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "support": { + "issues": "https://github.com/Seldaek/monolog/issues", + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + }, + "funding": [ + { + "url": "https://github.com/Seldaek", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/monolog/monolog", + "type": "tidelift" + } + ], + "time": "2026-01-02T08:56:05+00:00" + }, + { + "name": "nesbot/carbon", + "version": "3.13.0", + "source": { + "type": "git", + "url": "https://github.com/CarbonPHP/carbon.git", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a", + "reference": "40f6618f052df16b545f626fbf9a878e6497d16a", + "shasum": "" + }, + "require": { + "carbonphp/carbon-doctrine-types": "<100.0", + "ext-json": "*", + "php": "^8.1", + "psr/clock": "^1.0", + "symfony/clock": "^6.3.12 || ^7.0 || ^8.0", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "require-dev": { + "doctrine/dbal": "^3.6.3 || ^4.0", + "doctrine/orm": "^2.15.2 || ^3.0", + "friendsofphp/php-cs-fixer": "^v3.87.1", + "kylekatarnls/multi-tester": "^2.5.3", + "phpmd/phpmd": "^2.15.0", + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^2.1.22", + "phpunit/phpunit": "^10.5.53", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0" + }, + "bin": [ + "bin/carbon" + ], + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Carbon\\Laravel\\ServiceProvider" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev", + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Carbon\\": "src/Carbon/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Brian Nesbitt", + "email": "brian@nesbot.com", + "homepage": "https://markido.com" + }, + { + "name": "kylekatarnls", + "homepage": "https://github.com/kylekatarnls" + } + ], + "description": "An API extension for DateTime that supports 281 different languages.", + "homepage": "https://carbonphp.github.io/carbon/", + "keywords": [ + "date", + "datetime", + "time" + ], + "support": { + "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html", + "issues": "https://github.com/CarbonPHP/carbon/issues", + "source": "https://github.com/CarbonPHP/carbon" + }, + "funding": [ + { + "url": "https://github.com/sponsors/kylekatarnls", + "type": "github" + }, + { + "url": "https://opencollective.com/Carbon#sponsor", + "type": "opencollective" + }, + { + "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme", + "type": "tidelift" + } + ], + "time": "2026-06-18T13:49:15+00:00" + }, + { + "name": "nette/schema", + "version": "v1.3.5", + "source": { + "type": "git", + "url": "https://github.com/nette/schema.git", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002", + "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0", + "php": "8.1 - 8.5" + }, + "require-dev": { + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.6", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1.39@stable", + "tracy/tracy": "^2.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "📐 Nette Schema: validating data structures against a given Schema.", + "homepage": "https://nette.org", + "keywords": [ + "config", + "nette" + ], + "support": { + "issues": "https://github.com/nette/schema/issues", + "source": "https://github.com/nette/schema/tree/v1.3.5" + }, + "time": "2026-02-23T03:47:12+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.4", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/phpstan-rules": "^1.0", + "nette/tester": "^2.5", + "phpstan/extension-installer": "^1.4@stable", + "phpstan/phpstan": "^2.1@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.4" + }, + "time": "2026-05-11T20:49:54+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8", + "reference": "712a31b768f5daea284c2169a7d227031001b9a8", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.4.4 || ^8.0.4" + }, + "require-dev": { + "illuminate/console": "^11.47.0", + "laravel/pint": "^1.27.1", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2", + "phpstan/phpstan": "^1.12.32", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.3.5 || ^8.0.4", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "It's like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2026-02-16T23:10:27+00:00" + }, + { + "name": "phpoption/phpoption", + "version": "1.9.5", + "source": { + "type": "git", + "url": "https://github.com/schmittjoh/php-option.git", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", + "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "shasum": "" + }, + "require": { + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "1.9-dev" + } + }, + "autoload": { + "psr-4": { + "PhpOption\\": "src/PhpOption/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Johannes M. Schmitt", + "email": "schmittjoh@gmail.com", + "homepage": "https://github.com/schmittjoh" + }, + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + } + ], + "description": "Option Type for PHP", + "keywords": [ + "language", + "option", + "php", + "type" + ], + "support": { + "issues": "https://github.com/schmittjoh/php-option/issues", + "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:41:33+00:00" + }, + { + "name": "psr/clock", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/clock.git", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Psr\\Clock\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for reading the clock.", + "homepage": "https://github.com/php-fig/clock", + "keywords": [ + "clock", + "now", + "psr", + "psr-20", + "time" + ], + "support": { + "issues": "https://github.com/php-fig/clock/issues", + "source": "https://github.com/php-fig/clock/tree/1.0.0" + }, + "time": "2022-11-25T14:36:26+00:00" + }, + { + "name": "psr/container", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", + "shasum": "" + }, + "require": { + "php": ">=7.4.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/2.0.2" + }, + "time": "2021-11-05T16:47:00+00:00" + }, + { + "name": "psr/event-dispatcher", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/event-dispatcher.git", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0", + "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0", + "shasum": "" + }, + "require": { + "php": ">=7.2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\EventDispatcher\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Standard interfaces for event handling.", + "keywords": [ + "events", + "psr", + "psr-14" + ], + "support": { + "issues": "https://github.com/php-fig/event-dispatcher/issues", + "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0" + }, + "time": "2019-01-08T18:20:26+00:00" + }, + { + "name": "psr/http-client", + "version": "1.0.3", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-client.git", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90", + "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90", + "shasum": "" + }, + "require": { + "php": "^7.0 || ^8.0", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Client\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP clients", + "homepage": "https://github.com/php-fig/http-client", + "keywords": [ + "http", + "http-client", + "psr", + "psr-18" + ], + "support": { + "source": "https://github.com/php-fig/http-client" + }, + "time": "2023-09-23T14:17:50+00:00" + }, + { + "name": "psr/http-factory", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-factory.git", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "psr/http-message": "^1.0 || ^2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", + "keywords": [ + "factory", + "http", + "message", + "psr", + "psr-17", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-factory" + }, + "time": "2024-04-15T12:06:14+00:00" + }, + { + "name": "psr/http-message", + "version": "2.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/2.0" + }, + "time": "2023-04-04T09:54:51+00:00" + }, + { + "name": "psr/log", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "support": { + "source": "https://github.com/php-fig/log/tree/3.0.2" + }, + "time": "2024-09-11T13:17:53+00:00" + }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, + { + "name": "psy/psysh", + "version": "v0.12.23", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/psysh.git", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-tokenizer": "*", + "nikic/php-parser": "^5.0 || ^4.0", + "php": "^8.0 || ^7.4", + "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4", + "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4" + }, + "conflict": { + "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.2", + "composer/class-map-generator": "^1.6" + }, + "suggest": { + "composer/class-map-generator": "Improved tab completion performance with better class discovery.", + "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)", + "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well." + }, + "bin": [ + "bin/psysh" + ], + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": false, + "forward-command": false + }, + "branch-alias": { + "dev-main": "0.12.x-dev" + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Psy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info" + } + ], + "description": "An interactive shell for modern PHP.", + "homepage": "https://psysh.org", + "keywords": [ + "REPL", + "console", + "interactive", + "shell" + ], + "support": { + "issues": "https://github.com/bobthecow/psysh/issues", + "source": "https://github.com/bobthecow/psysh/tree/v0.12.23" + }, + "time": "2026-05-23T13:41:31+00:00" + }, + { + "name": "ralouphie/getallheaders", + "version": "3.0.3", + "source": { + "type": "git", + "url": "https://github.com/ralouphie/getallheaders.git", + "reference": "120b605dfeb996808c31b6477290a714d356e822" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", + "reference": "120b605dfeb996808c31b6477290a714d356e822", + "shasum": "" + }, + "require": { + "php": ">=5.6" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.1", + "phpunit/phpunit": "^5 || ^6.5" + }, + "type": "library", + "autoload": { + "files": [ + "src/getallheaders.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ralph Khattar", + "email": "ralph.khattar@gmail.com" + } + ], + "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, + "time": "2019-03-08T08:55:37+00:00" + }, + { + "name": "ramsey/collection", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/ramsey/collection.git", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2", + "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "captainhook/plugin-composer": "^5.3", + "ergebnis/composer-normalize": "^2.45", + "fakerphp/faker": "^1.24", + "hamcrest/hamcrest-php": "^2.0", + "jangregor/phpstan-prophecy": "^2.1", + "mockery/mockery": "^1.6", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpspec/prophecy-phpunit": "^2.3", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5", + "ramsey/coding-standard": "^2.3", + "ramsey/conventional-commits": "^1.6", + "roave/security-advisories": "dev-latest" + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + }, + "ramsey/conventional-commits": { + "configFile": "conventional-commits.json" + } + }, + "autoload": { + "psr-4": { + "Ramsey\\Collection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ben Ramsey", + "email": "ben@benramsey.com", + "homepage": "https://benramsey.com" + } + ], + "description": "A PHP library for representing and manipulating collections.", + "keywords": [ + "array", + "collection", + "hash", + "map", + "queue", + "set" + ], + "support": { + "issues": "https://github.com/ramsey/collection/issues", + "source": "https://github.com/ramsey/collection/tree/2.1.1" + }, + "time": "2025-03-22T05:38:12+00:00" + }, + { + "name": "ramsey/uuid", + "version": "4.9.3", + "source": { + "type": "git", + "url": "https://github.com/ramsey/uuid.git", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ramsey/uuid/zipball/1df15849d00943a67d677dc9cfd80795f038c9f8", + "reference": "1df15849d00943a67d677dc9cfd80795f038c9f8", + "shasum": "" + }, + "require": { + "brick/math": ">=0.8.16 <=0.18", + "php": "^8.0", + "ramsey/collection": "^1.2 || ^2.0" + }, + "replace": { + "rhumsaa/uuid": "self.version" + }, + "require-dev": { + "captainhook/captainhook": "^5.25", + "captainhook/plugin-composer": "^5.3", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "ergebnis/composer-normalize": "^2.47", + "mockery/mockery": "^1.6", + "paragonie/random-lib": "^2", + "php-mock/php-mock": "^2.6", + "php-mock/php-mock-mockery": "^1.5", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpbench/phpbench": "^1.2.14", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-mockery": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^9.6", + "slevomat/coding-standard": "^8.18", + "squizlabs/php_codesniffer": "^3.13" + }, + "suggest": { + "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", + "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", + "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", + "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", + "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." + }, + "type": "library", + "extra": { + "captainhook": { + "force-install": true + } + }, + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Ramsey\\Uuid\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", + "keywords": [ + "guid", + "identifier", + "uuid" + ], + "support": { + "issues": "https://github.com/ramsey/uuid/issues", + "source": "https://github.com/ramsey/uuid/tree/4.9.3" + }, + "time": "2026-06-18T03:57:49+00:00" + }, + { + "name": "spatie/laravel-activitylog", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-activitylog.git", + "reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/0e00fe74fd071cc572a045459f6d4c9de33130bd", + "reference": "0e00fe74fd071cc572a045459f6d4c9de33130bd", + "shasum": "" + }, + "require": { + "illuminate/config": "^12.0 || ^13.0", + "illuminate/database": "^12.0 || ^13.0", + "illuminate/support": "^12.0 || ^13.0", + "php": "^8.4", + "spatie/laravel-package-tools": "^1.6.3" + }, + "require-dev": { + "ext-json": "*", + "larastan/larastan": "^3.0", + "laravel/pint": "^1.29", + "orchestra/testbench": "^10.0 || ^11.0", + "pestphp/pest": "^4.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Activitylog\\ActivitylogServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Activitylog\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Sebastian De Deyne", + "email": "sebastian@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + }, + { + "name": "Tom Witkowski", + "email": "dev.gummibeer@gmail.com", + "homepage": "https://gummibeer.de", + "role": "Developer" + } + ], + "description": "A very simple activity logger to monitor the users of your website or application", + "homepage": "https://github.com/spatie/activitylog", + "keywords": [ + "activity", + "laravel", + "log", + "spatie", + "user" + ], + "support": { + "issues": "https://github.com/spatie/laravel-activitylog/issues", + "source": "https://github.com/spatie/laravel-activitylog/tree/5.0.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-03-25T10:04:54+00:00" + }, + { + "name": "spatie/laravel-package-tools", + "version": "1.93.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "d5552849801f2642aea710557463234b59ef65eb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb", + "reference": "d5552849801f2642aea710557463234b59ef65eb", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0", + "php": "^8.1" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0", + "pestphp/pest": "^2.1|^3.1|^4.0", + "phpunit/php-code-coverage": "^10.0|^11.0|^12.0", + "phpunit/phpunit": "^10.5|^11.5|^12.5", + "spatie/pest-plugin-test-time": "^2.2|^3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-19T14:06:37+00:00" + }, + { + "name": "spatie/laravel-permission", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-permission.git", + "reference": "70a6ab04108616b438e0839598f473b513281644" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/70a6ab04108616b438e0839598f473b513281644", + "reference": "70a6ab04108616b438e0839598f473b513281644", + "shasum": "" + }, + "require": { + "illuminate/auth": "^12.0|^13.0", + "illuminate/container": "^12.0|^13.0", + "illuminate/contracts": "^12.0|^13.0", + "illuminate/database": "^12.0|^13.0", + "php": "^8.3", + "spatie/laravel-package-tools": "^1.0" + }, + "require-dev": { + "larastan/larastan": "^3.9", + "laravel/passport": "^13.0", + "laravel/pint": "^1.0", + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.1", + "phpstan/phpstan": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\Permission\\PermissionServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "8.x-dev", + "dev-master": "8.x-dev" + } + }, + "autoload": { + "files": [ + "src/helpers.php" + ], + "psr-4": { + "Spatie\\Permission\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Permission handling for Laravel 12 and up", + "homepage": "https://github.com/spatie/laravel-permission", + "keywords": [ + "acl", + "laravel", + "permission", + "permissions", + "rbac", + "roles", + "security", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-permission/issues", + "source": "https://github.com/spatie/laravel-permission/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2026-05-30T19:30:22+00:00" + }, + { + "name": "symfony/clock", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/clock.git", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6", + "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/clock": "^1.0" + }, + "provide": { + "psr/clock-implementation": "1.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/now.php" + ], + "psr-4": { + "Symfony\\Component\\Clock\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Decouples applications from the system clock", + "homepage": "https://symfony.com", + "keywords": [ + "clock", + "psr20", + "time" + ], + "support": { + "source": "https://github.com/symfony/clock/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/console", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.0", + "symfony/polyfill-php85": "^1.32", + "symfony/service-contracts": "^2.5|^3", + "symfony/string": "^7.4.6|^8.0.6" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/event-dispatcher": "<8.1" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/event-dispatcher": "^8.1", + "symfony/filesystem": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/lock": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Eases the creation of beautiful and testable command line interfaces", + "homepage": "https://symfony.com", + "keywords": [ + "cli", + "command-line", + "console", + "terminal" + ], + "support": { + "source": "https://github.com/symfony/console/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/css-selector", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/css-selector.git", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\CssSelector\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Jean-François Simon", + "email": "jeanfrancois.simon@sensiolabs.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Converts CSS selectors to XPath expressions", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/css-selector/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/deprecation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b", + "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-13T15:52:40+00:00" + }, + { + "name": "symfony/error-handler", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/error-handler.git", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/polyfill-php85": "^1.32", + "symfony/var-dumper": "^7.4|^8.0" + }, + "conflict": { + "symfony/deprecation-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/webpack-encore-bundle": "^1.0|^2.0" + }, + "bin": [ + "Resources/bin/patch-type-declarations" + ], + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\ErrorHandler\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to manage errors and ease debugging PHP code", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/error-handler/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/event-dispatcher-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/security-http": "<7.4", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "psr/event-dispatcher-implementation": "1.0", + "symfony/event-dispatcher-implementation": "2.0|3.0" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/error-handler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/framework-bundle": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/stopwatch": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\EventDispatcher\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/event-dispatcher-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/event-dispatcher-contracts.git", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32", + "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/event-dispatcher": "^1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\EventDispatcher\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to dispatching event", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/finder", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "58d2e767a66052c1487356f953445634a8194c64" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64", + "reference": "58d2e767a66052c1487356f953445634a8194c64", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "require-dev": { + "symfony/filesystem": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/http-foundation", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-foundation.git", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e", + "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/polyfill-mbstring": "^1.1" + }, + "conflict": { + "doctrine/dbal": "<4.3" + }, + "require-dev": { + "doctrine/dbal": "^4.3", + "predis/predis": "^1.1|^2.0", + "symfony/cache": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpFoundation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Defines an object-oriented layer for the HTTP specification", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-foundation/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/http-kernel", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/http-kernel.git", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "psr/log": "^1|^2|^3", + "symfony/deprecation-contracts": "^2.5|^3", + "symfony/error-handler": "^7.4|^8.0", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/polyfill-ctype": "^1.8" + }, + "conflict": { + "symfony/dependency-injection": "<8.1", + "symfony/flex": "<2.10", + "symfony/http-client-contracts": "<2.5", + "symfony/translation-contracts": "<2.5", + "symfony/var-dumper": "<8.1", + "symfony/web-profiler-bundle": "<8.1", + "twig/twig": "<3.21" + }, + "provide": { + "psr/log-implementation": "1.0|2.0|3.0" + }, + "require-dev": { + "psr/cache": "^1.0|^2.0|^3.0", + "symfony/browser-kit": "^7.4|^8.0", + "symfony/clock": "^7.4|^8.0", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/css-selector": "^7.4|^8.0", + "symfony/dependency-injection": "^8.1", + "symfony/dom-crawler": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/rate-limiter": "^7.4|^8.0", + "symfony/routing": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0", + "symfony/stopwatch": "^7.4|^8.0", + "symfony/translation": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3", + "symfony/uid": "^7.4|^8.0", + "symfony/validator": "^7.4|^8.0", + "symfony/var-dumper": "^8.1", + "symfony/var-exporter": "^7.4|^8.0", + "twig/twig": "^3.21" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\HttpKernel\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides a structured process for converting a Request into a Response", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/http-kernel/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T08:46:08+00:00" + }, + { + "name": "symfony/mailer", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mailer.git", + "reference": "9418d772df3a03a142e3bc06f602adb2b8724877" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mailer/zipball/9418d772df3a03a142e3bc06f602adb2b8724877", + "reference": "9418d772df3a03a142e3bc06f602adb2b8724877", + "shasum": "" + }, + "require": { + "egulias/email-validator": "^2.1.10|^3|^4", + "php": ">=8.4.1", + "psr/event-dispatcher": "^1", + "psr/log": "^1|^2|^3", + "symfony/event-dispatcher": "^7.4|^8.0", + "symfony/mime": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3" + }, + "conflict": { + "symfony/http-client-contracts": "<2.5" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/messenger": "^7.4|^8.0", + "symfony/twig-bridge": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mailer\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Helps sending emails", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/mailer/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/mime", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/mime.git", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", + "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-intl-idn": "^1.10", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "egulias/email-validator": "~3.0.0", + "phpdocumentor/reflection-docblock": "<5.2|>=7", + "phpdocumentor/type-resolver": "<1.5.1" + }, + "require-dev": { + "egulias/email-validator": "^2.1.10|^3.1|^4", + "league/html-to-markdown": "^5.0", + "phpdocumentor/reflection-docblock": "^5.2|^6.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/property-access": "^7.4|^8.0", + "symfony/property-info": "^7.4|^8.0", + "symfony/serializer": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Mime\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Allows manipulating MIME messages", + "homepage": "https://symfony.com", + "keywords": [ + "mime", + "mime-type" + ], + "support": { + "source": "https://github.com/symfony/mime/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2", + "reference": "141046a8f9477948ff284fa65be2095baafb94f2", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-ctype": "*" + }, + "suggest": { + "ext-ctype": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Gert de Pagter", + "email": "BackEndTea@gmail.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for ctype functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "ctype", + "polyfill", + "portable" + ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-intl-grapheme", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-grapheme.git", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", + "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's grapheme_* functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "grapheme", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T05:58:03+00:00" + }, + { + "name": "symfony/polyfill-intl-idn", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "dc21118016c039a66235cf93d96b435ffb282412" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", + "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "shasum": "" + }, + "require": { + "php": ">=7.2", + "symfony/polyfill-intl-normalizer": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T15:22:23+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:48:31+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.38.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": ">=7.2" + }, + "provide": { + "ext-mbstring": "*" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-27T06:59:30+00:00" + }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/polyfill-php84", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php84.git", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php84\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T12:51:13+00:00" + }, + { + "name": "symfony/polyfill-php85", + "version": "v1.38.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php85.git", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php85\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-26T02:25:22+00:00" + }, + { + "name": "symfony/polyfill-php86", + "version": "v1.38.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php86.git", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php86\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-25T11:52:35+00:00" + }, + { + "name": "symfony/polyfill-uuid", + "version": "v1.37.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-uuid.git", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "provide": { + "ext-uuid": "*" + }, + "suggest": { + "ext-uuid": "For best performance" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Uuid\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for uuid functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-04-10T16:19:22+00:00" + }, + { + "name": "symfony/process", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5", + "shasum": "" + }, + "require": { + "php": ">=8.4.1" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/routing", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/routing.git", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "require-dev": { + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/expression-language": "^7.4|^8.0", + "symfony/http-foundation": "^7.4|^8.0", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Routing\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Maps an HTTP request to a set of configuration variables", + "homepage": "https://symfony.com", + "keywords": [ + "router", + "routing", + "uri", + "url" + ], + "support": { + "source": "https://github.com/symfony/routing/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/service-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/service-contracts.git", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "psr/container": "^1.1|^2.0", + "symfony/deprecation-contracts": "^2.5|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Service\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to writing services", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-28T09:44:51+00:00" + }, + { + "name": "symfony/string", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/string.git", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-ctype": "^1.8", + "symfony/polyfill-intl-grapheme": "^1.33", + "symfony/polyfill-intl-normalizer": "^1.0", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.5" + }, + "require-dev": { + "symfony/emoji": "^7.4|^8.0", + "symfony/http-client": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/translation-contracts": "^2.5|^3.0", + "symfony/var-exporter": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", + "homepage": "https://symfony.com", + "keywords": [ + "grapheme", + "i18n", + "string", + "unicode", + "utf-8", + "utf8" + ], + "support": { + "source": "https://github.com/symfony/string/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/translation", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation.git", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0", + "symfony/translation-contracts": "^3.6.1" + }, + "conflict": { + "nikic/php-parser": "<5.0", + "symfony/http-client-contracts": "<2.5", + "symfony/service-contracts": "<2.5" + }, + "provide": { + "symfony/translation-implementation": "2.3|3.0" + }, + "require-dev": { + "nikic/php-parser": "^5.0", + "psr/log": "^1|^2|^3", + "symfony/config": "^7.4|^8.0", + "symfony/console": "^7.4|^8.0", + "symfony/dependency-injection": "^7.4|^8.0", + "symfony/finder": "^7.4|^8.0", + "symfony/http-client-contracts": "^2.5|^3.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/intl": "^7.4|^8.0", + "symfony/polyfill-intl-icu": "^1.21", + "symfony/routing": "^7.4|^8.0", + "symfony/service-contracts": "^2.5|^3", + "symfony/yaml": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "files": [ + "Resources/functions.php" + ], + "psr-4": { + "Symfony\\Component\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides tools to internationalize your application", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/translation/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/translation-contracts", + "version": "v3.7.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/translation-contracts.git", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d", + "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.7-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Contracts\\Translation\\": "" + }, + "exclude-from-classmap": [ + "/Test/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Generic abstractions related to translation", + "homepage": "https://symfony.com", + "keywords": [ + "abstractions", + "contracts", + "decoupling", + "interfaces", + "interoperability", + "standards" + ], + "support": { + "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-01-05T13:30:16+00:00" + }, + { + "name": "symfony/uid", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/uid.git", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/uid/zipball/7393f157a55f7e70a4de0334435c55a5a8fe749a", + "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-uuid": "^1.15" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Uid\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Grégoire Pineau", + "email": "lyrixx@lyrixx.info" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides an object-oriented API to generate and represent UIDs", + "homepage": "https://symfony.com", + "keywords": [ + "UID", + "ulid", + "uuid" + ], + "support": { + "source": "https://github.com/symfony/uid/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "symfony/var-dumper", + "version": "v8.1.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", + "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e", + "shasum": "" + }, + "require": { + "php": ">=8.4.1", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.1.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-05-29T05:06:50+00:00" + }, + { + "name": "tijsverkoyen/css-to-inline-styles", + "version": "v2.4.0", + "source": { + "type": "git", + "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41", + "reference": "f0292ccf0ec75843d65027214426b6b163b48b41", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "php": "^7.4 || ^8.0", + "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^8.5.21 || ^9.5.10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "TijsVerkoyen\\CssToInlineStyles\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Tijs Verkoyen", + "email": "css_to_inline_styles@verkoyen.eu", + "role": "Developer" + } + ], + "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.", + "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles", + "support": { + "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues", + "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0" + }, + "time": "2025-12-02T11:56:42+00:00" + }, + { + "name": "vlucas/phpdotenv", + "version": "v5.6.3", + "source": { + "type": "git", + "url": "https://github.com/vlucas/phpdotenv.git", + "reference": "955e7815d677a3eaa7075231212f2110983adecc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc", + "reference": "955e7815d677a3eaa7075231212f2110983adecc", + "shasum": "" + }, + "require": { + "ext-pcre": "*", + "graham-campbell/result-type": "^1.1.4", + "php": "^7.2.5 || ^8.0", + "phpoption/phpoption": "^1.9.5", + "symfony/polyfill-ctype": "^1.26", + "symfony/polyfill-mbstring": "^1.26", + "symfony/polyfill-php80": "^1.26" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.8.2", + "ext-filter": "*", + "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2" + }, + "suggest": { + "ext-filter": "Required to use the boolean validator." + }, + "type": "library", + "extra": { + "bamarni-bin": { + "bin-links": true, + "forward-command": false + }, + "branch-alias": { + "dev-master": "5.6-dev" + } + }, + "autoload": { + "psr-4": { + "Dotenv\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, + { + "name": "Vance Lucas", + "email": "vance@vancelucas.com", + "homepage": "https://github.com/vlucas" + } + ], + "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "keywords": [ + "dotenv", + "env", + "environment" + ], + "support": { + "issues": "https://github.com/vlucas/phpdotenv/issues", + "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv", + "type": "tidelift" + } + ], + "time": "2025-12-27T19:49:13+00:00" + }, + { + "name": "voku/portable-ascii", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/voku/portable-ascii.git", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f", + "reference": "8e1051fe39379367aecf014f41744ce7539a856f", + "shasum": "" + }, + "require": { + "php": ">=7.1.0" + }, + "require-dev": { + "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5" + }, + "suggest": { + "ext-intl": "Use Intl for transliterator_transliterate() support" + }, + "type": "library", + "autoload": { + "psr-4": { + "voku\\": "src/voku/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Lars Moelleken", + "homepage": "https://www.moelleken.org/" + } + ], + "description": "Portable ASCII library - performance optimized (ascii) string functions for php.", + "homepage": "https://github.com/voku/portable-ascii", + "keywords": [ + "ascii", + "clean", + "php" + ], + "support": { + "issues": "https://github.com/voku/portable-ascii/issues", + "source": "https://github.com/voku/portable-ascii/tree/2.1.1" + }, + "funding": [ + { + "url": "https://www.paypal.me/moelleken", + "type": "custom" + }, + { + "url": "https://github.com/voku", + "type": "github" + }, + { + "url": "https://opencollective.com/portable-ascii", + "type": "open_collective" + }, + { + "url": "https://www.patreon.com/voku", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii", + "type": "tidelift" + } + ], + "time": "2026-04-26T05:33:54+00:00" + } + ], + "packages-dev": [ + { + "name": "fakerphp/faker", + "version": "v1.24.1", + "source": { + "type": "git", + "url": "https://github.com/FakerPHP/Faker.git", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0", + "psr/container": "^1.0 || ^2.0", + "symfony/deprecation-contracts": "^2.2 || ^3.0" + }, + "conflict": { + "fzaninotto/faker": "*" + }, + "require-dev": { + "bamarni/composer-bin-plugin": "^1.4.1", + "doctrine/persistence": "^1.3 || ^2.0", + "ext-intl": "*", + "phpunit/phpunit": "^9.5.26", + "symfony/phpunit-bridge": "^5.4.16" + }, + "suggest": { + "doctrine/orm": "Required to use Faker\\ORM\\Doctrine", + "ext-curl": "Required by Faker\\Provider\\Image to download images.", + "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.", + "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.", + "ext-mbstring": "Required for multibyte Unicode string functionality." + }, + "type": "library", + "autoload": { + "psr-4": { + "Faker\\": "src/Faker/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "François Zaninotto" + } + ], + "description": "Faker is a PHP library that generates fake data for you.", + "keywords": [ + "data", + "faker", + "fixtures" + ], + "support": { + "issues": "https://github.com/FakerPHP/Faker/issues", + "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1" + }, + "time": "2024-11-21T13:46:39+00:00" + }, + { + "name": "filp/whoops", + "version": "2.18.4", + "source": { + "type": "git", + "url": "https://github.com/filp/whoops.git", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0", + "psr/log": "^1.0.1 || ^2.0 || ^3.0" + }, + "require-dev": { + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3", + "symfony/var-dumper": "^4.0 || ^5.0" + }, + "suggest": { + "symfony/var-dumper": "Pretty print complex values better with var-dumper available", + "whoops/soap": "Formats errors as SOAP responses" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Whoops\\": "src/Whoops/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Filipe Dobreira", + "homepage": "https://github.com/filp", + "role": "Developer" + } + ], + "description": "php error handling for cool kids", + "homepage": "https://filp.github.io/whoops/", + "keywords": [ + "error", + "exception", + "handling", + "library", + "throwable", + "whoops" + ], + "support": { + "issues": "https://github.com/filp/whoops/issues", + "source": "https://github.com/filp/whoops/tree/2.18.4" + }, + "funding": [ + { + "url": "https://github.com/denis-sokolov", + "type": "github" + } + ], + "time": "2025-08-08T12:00:00+00:00" + }, + { + "name": "hamcrest/hamcrest-php", + "version": "v2.1.1", + "source": { + "type": "git", + "url": "https://github.com/hamcrest/hamcrest-php.git", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487", + "shasum": "" + }, + "require": { + "php": "^7.4|^8.0" + }, + "replace": { + "cordoval/hamcrest-php": "*", + "davedevelopment/hamcrest-php": "*", + "kodova/hamcrest-php": "*" + }, + "require-dev": { + "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0", + "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.1-dev" + } + }, + "autoload": { + "classmap": [ + "hamcrest" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "This is the PHP port of Hamcrest Matchers", + "keywords": [ + "test" + ], + "support": { + "issues": "https://github.com/hamcrest/hamcrest-php/issues", + "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1" + }, + "time": "2025-04-30T06:54:44+00:00" + }, + { + "name": "laravel/agent-detector", + "version": "v2.0.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/agent-detector.git", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/agent-detector/zipball/90694b9256099591cf9e55d08c18ba7a00bf099f", + "reference": "90694b9256099591cf9e55d08c18ba7a00bf099f", + "shasum": "" + }, + "require": { + "php": "^8.2.0" + }, + "require-dev": { + "laravel/pint": "^1.24.0", + "pestphp/pest": "^3.8.5|^4.1.0", + "pestphp/pest-plugin-type-coverage": "^3.0|^4.0.2", + "phpstan/phpstan": "^2.1.26", + "rector/rector": "^2.1.7", + "symfony/var-dumper": "^7.3.3" + }, + "type": "library", + "autoload": { + "files": [ + "src/functions.php" + ], + "psr-4": { + "Laravel\\AgentDetector\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Detect if code is running in an AI agent or automated development environment", + "homepage": "https://github.com/laravel/agent-detector", + "keywords": [ + "Agent", + "ai", + "automation", + "claude", + "cursor", + "detection", + "devin", + "php" + ], + "support": { + "issues": "https://github.com/laravel/agent-detector/issues", + "source": "https://github.com/laravel/agent-detector" + }, + "time": "2026-04-29T18:32:34+00:00" + }, + { + "name": "laravel/pail", + "version": "v1.2.7", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa", + "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "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|^8.0" + }, + "require-dev": { + "laravel/framework": "^10.24|^11.0|^12.0|^13.0", + "laravel/pint": "^1.13", + "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|^8.0", + "symfony/yaml": "^6.3|^7.0|^8.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + }, + "branch-alias": { + "dev-main": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "dev", + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2026-05-20T22:24:57+00:00" + }, + { + "name": "laravel/pao", + "version": "v1.1.2", + "source": { + "type": "git", + "url": "https://github.com/laravel/pao.git", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pao/zipball/41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "reference": "41b3c61ebeddce52a446afe6d21e0b02983fb2f6", + "shasum": "" + }, + "require": { + "laravel/agent-detector": "^2.0.2", + "php": "^8.3" + }, + "conflict": { + "laravel/framework": "<12.0.0", + "nunomaduro/collision": "<8.9.3", + "pestphp/pest": "<4.6.3 || >=6.0.0", + "phpunit/phpunit": "<12.5.23 || >=13.0.0 <13.1.7 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.20.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench": "^10.11.0 || ^11.1.0", + "pestphp/pest": "^4.7.2 || ^5.0.0", + "pestphp/pest-plugin-type-coverage": "^4.0.4 || ^5.0.0", + "phpstan/phpstan": "^2.2.2", + "rector/rector": "^2.4.5", + "symfony/process": "^7.4.8 || ^8.1.0", + "symfony/var-dumper": "^7.4.8 || ^8.1.0" + }, + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Laravel\\Pao\\Drivers\\Pest\\Plugin" + ] + }, + "laravel": { + "providers": [ + "Laravel\\Pao\\Laravel\\ServiceProvider" + ] + } + }, + "autoload": { + "files": [ + "src/Autoload.php" + ], + "psr-4": { + "Laravel\\Pao\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + } + ], + "description": "Agent-optimized output for PHP testing tools", + "keywords": [ + "Agent", + "PHPStan", + "ai", + "dev", + "paratest", + "pest", + "php", + "phpunit", + "rector", + "testing" + ], + "support": { + "issues": "https://github.com/laravel/pao/issues", + "source": "https://github.com/laravel/pao" + }, + "time": "2026-06-22T19:58:00+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.29.3", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.95.8", + "illuminate/view": "^12.62.0", + "larastan/larastan": "^3.10.0", + "laravel-zero/framework": "^12.1.0", + "laravel/agent-detector": "^2.0.2", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.4.0", + "pestphp/pest": "^3.8.6" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "dev", + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2026-06-16T15:34:04+00:00" + }, + { + "name": "mockery/mockery", + "version": "1.6.12", + "source": { + "type": "git", + "url": "https://github.com/mockery/mockery.git", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699", + "shasum": "" + }, + "require": { + "hamcrest/hamcrest-php": "^2.0.1", + "lib-pcre": ">=7.0", + "php": ">=7.3" + }, + "conflict": { + "phpunit/phpunit": "<8.0" + }, + "require-dev": { + "phpunit/phpunit": "^8.5 || ^9.6.17", + "symplify/easy-coding-standard": "^12.1.14" + }, + "type": "library", + "autoload": { + "files": [ + "library/helpers.php", + "library/Mockery.php" + ], + "psr-4": { + "Mockery\\": "library/Mockery" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Pádraic Brady", + "email": "padraic.brady@gmail.com", + "homepage": "https://github.com/padraic", + "role": "Author" + }, + { + "name": "Dave Marshall", + "email": "dave.marshall@atstsolutions.co.uk", + "homepage": "https://davedevelopment.co.uk", + "role": "Developer" + }, + { + "name": "Nathanael Esayeas", + "email": "nathanael.esayeas@protonmail.com", + "homepage": "https://github.com/ghostwriter", + "role": "Lead Developer" + } + ], + "description": "Mockery is a simple yet flexible PHP mock object framework", + "homepage": "https://github.com/mockery/mockery", + "keywords": [ + "BDD", + "TDD", + "library", + "mock", + "mock objects", + "mockery", + "stub", + "test", + "test double", + "testing" + ], + "support": { + "docs": "https://docs.mockery.io/", + "issues": "https://github.com/mockery/mockery/issues", + "rss": "https://github.com/mockery/mockery/releases.atom", + "security": "https://github.com/mockery/mockery/security/advisories", + "source": "https://github.com/mockery/mockery" + }, + "time": "2024-05-16T03:13:13+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nunomaduro/collision", + "version": "v8.9.4", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/collision.git", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", + "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "shasum": "" + }, + "require": { + "filp/whoops": "^2.18.4", + "nunomaduro/termwind": "^2.4.0", + "php": "^8.2.0", + "symfony/console": "^7.4.8 || ^8.0.8" + }, + "conflict": { + "laravel/framework": "<11.48.0 || >=14.0.0", + "phpunit/phpunit": "<11.5.50 || >=14.0.0" + }, + "require-dev": { + "brianium/paratest": "^7.8.5", + "larastan/larastan": "^3.9.6", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0", + "laravel/pint": "^1.29.1", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1", + "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" + ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" + } + }, + "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], + "psr-4": { + "NunoMaduro\\Collision\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2026-04-21T14:04:20+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.7", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "186dab580576598076de6818596d12b61801880e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", + "reference": "186dab580576598076de6818596d12b61801880e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.1.2", + "sebastian/lines-of-code": "^4.0.1", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.28" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-06-01T13:24:19+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.30", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", + "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.7", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.1", + "sebastian/comparator": "^7.1.8", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.2", + "sebastian/exporter": "^7.0.3", + "sebastian/global-state": "^8.0.3", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.4", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.30" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-06-15T13:12:30+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "7c65c1e79836812819705b473a90c12399542485" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", + "reference": "7c65c1e79836812819705b473a90c12399542485", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-05-21T04:45:25+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", + "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.26" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-05-25T13:40:20+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2026-05-20T04:37:17+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", + "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0.1" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.5.28" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2026-06-01T15:10:33+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", + "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.7.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code", + "type": "tidelift" + } + ], + "time": "2026-05-19T16:22:07+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "82ff822c2edc46724be9f7411d3163021f602773" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", + "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2026-05-20T06:45:45+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": "^8.3" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/config/activitylog.php b/config/activitylog.php new file mode 100644 index 0000000..27cd6aa --- /dev/null +++ b/config/activitylog.php @@ -0,0 +1,73 @@ + env('ACTIVITYLOG_ENABLED', true), + + /* + * When the clean command is executed, all recording activities older than + * the number of days specified here will be deleted. + */ + 'clean_after_days' => 365, + + /* + * If no log name is passed to the activity() helper + * we use this default log name. + */ + 'default_log_name' => 'default', + + /* + * You can specify an auth driver here that gets user models. + * If this is null we'll use the current Laravel auth driver. + */ + 'default_auth_driver' => null, + + /* + * If set to true, the subject relationship on activities + * will include soft deleted models. + */ + 'include_soft_deleted_subjects' => false, + + /* + * This model will be used to log activity. + * It should implement the Spatie\Activitylog\Contracts\Activity interface + * and extend Illuminate\Database\Eloquent\Model. + */ + 'activity_model' => Activity::class, + + /* + * These attributes will be excluded from logging for all models. + * Model-specific exclusions via logExcept() are merged with these. + */ + 'default_except_attributes' => [], + + /* + * When enabled, activities are buffered in memory and inserted in a + * single bulk query after the response has been sent to the client. + * This can significantly reduce the number of database queries when + * many activities are logged during a single request. + * + * Only enable this if your application logs a high volume of activities + * per request. Buffered activities will not have an ID until the + * buffer is flushed. + */ + 'buffer' => [ + 'enabled' => env('ACTIVITYLOG_BUFFER_ENABLED', false), + ], + + /* + * These action classes can be overridden to customize how activities + * are logged and cleaned. Your custom classes must extend the originals. + */ + 'actions' => [ + 'log_activity' => LogActivityAction::class, + 'clean_log' => CleanActivityLogAction::class, + ], +]; diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..423eed5 --- /dev/null +++ b/config/app.php @@ -0,0 +1,126 @@ + env('APP_NAME', 'Laravel'), + + /* + |-------------------------------------------------------------------------- + | Application Environment + |-------------------------------------------------------------------------- + | + | This value determines the "environment" your application is currently + | running in. This may determine how you prefer to configure various + | services the application utilizes. Set this in your ".env" file. + | + */ + + 'env' => env('APP_ENV', 'production'), + + /* + |-------------------------------------------------------------------------- + | Application Debug Mode + |-------------------------------------------------------------------------- + | + | When your application is in debug mode, detailed error messages with + | stack traces will be shown on every error that occurs within your + | application. If disabled, a simple generic error page is shown. + | + */ + + 'debug' => (bool) env('APP_DEBUG', false), + + /* + |-------------------------------------------------------------------------- + | Application URL + |-------------------------------------------------------------------------- + | + | This URL is used by the console to properly generate URLs when using + | the Artisan command line tool. You should set this to the root of + | the application so that it's available within Artisan commands. + | + */ + + 'url' => env('APP_URL', 'http://localhost'), + + /* + |-------------------------------------------------------------------------- + | Application Timezone + |-------------------------------------------------------------------------- + | + | Here you may specify the default timezone for your application, which + | will be used by the PHP date and date-time functions. The timezone + | is set to "UTC" by default as it is suitable for most use cases. + | + */ + + 'timezone' => 'UTC', + + /* + |-------------------------------------------------------------------------- + | Application Locale Configuration + |-------------------------------------------------------------------------- + | + | The application locale determines the default locale that will be used + | by Laravel's translation / localization methods. This option can be + | set to any locale for which you plan to have translation strings. + | + */ + + 'locale' => env('APP_LOCALE', 'en'), + + 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), + + 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), + + /* + |-------------------------------------------------------------------------- + | Encryption Key + |-------------------------------------------------------------------------- + | + | This key is utilized by Laravel's encryption services and should be set + | to a random, 32 character string to ensure that all encrypted values + | are secure. You should do this prior to deploying the application. + | + */ + + 'cipher' => 'AES-256-CBC', + + 'key' => env('APP_KEY'), + + 'previous_keys' => [ + ...array_filter( + explode(',', (string) env('APP_PREVIOUS_KEYS', '')) + ), + ], + + /* + |-------------------------------------------------------------------------- + | Maintenance Mode Driver + |-------------------------------------------------------------------------- + | + | These configuration options determine the driver used to determine and + | manage Laravel's "maintenance mode" status. The "cache" driver will + | allow maintenance mode to be controlled across multiple machines. + | + | Supported drivers: "file", "cache" + | + */ + + 'maintenance' => [ + 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'), + 'store' => env('APP_MAINTENANCE_STORE', 'database'), + ], + +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..d7568ff --- /dev/null +++ b/config/auth.php @@ -0,0 +1,117 @@ + [ + 'guard' => env('AUTH_GUARD', 'web'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), + ], + + /* + |-------------------------------------------------------------------------- + | Authentication Guards + |-------------------------------------------------------------------------- + | + | Next, you may define every authentication guard for your application. + | Of course, a great default configuration has been defined for you + | which utilizes session storage plus the Eloquent user provider. + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | Supported: "session" + | + */ + + 'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', + ], + ], + + /* + |-------------------------------------------------------------------------- + | User Providers + |-------------------------------------------------------------------------- + | + | All authentication guards have a user provider, which defines how the + | users are actually retrieved out of your database or other storage + | system used by the application. Typically, Eloquent is utilized. + | + | If you have multiple user tables or models you may configure multiple + | providers to represent the model / table. These providers may then + | be assigned to any extra authentication guards you have defined. + | + | Supported: "database", "eloquent" + | + */ + + 'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => env('AUTH_MODEL', User::class), + ], + + // 'users' => [ + // 'driver' => 'database', + // 'table' => 'users', + // ], + ], + + /* + |-------------------------------------------------------------------------- + | Resetting Passwords + |-------------------------------------------------------------------------- + | + | These configuration options specify the behavior of Laravel's password + | reset functionality, including the table utilized for token storage + | and the user provider that is invoked to actually retrieve users. + | + | The expiry time is the number of minutes that each reset token will be + | considered valid. This security feature keeps tokens short-lived so + | they have less time to be guessed. You may change this as needed. + | + | The throttle setting is the number of seconds a user must wait before + | generating more password reset tokens. This prevents the user from + | quickly generating a very large amount of password reset tokens. + | + */ + + 'passwords' => [ + 'users' => [ + 'provider' => 'users', + 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'), + 'expire' => 60, + 'throttle' => 60, + ], + ], + + /* + |-------------------------------------------------------------------------- + | Password Confirmation Timeout + |-------------------------------------------------------------------------- + | + | Here you may define the number of seconds before a password confirmation + | window expires and users are asked to re-enter their password via the + | confirmation screen. By default, the timeout lasts for three hours. + | + */ + + 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), + +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..d7eec61 --- /dev/null +++ b/config/cache.php @@ -0,0 +1,136 @@ + env('CACHE_STORE', 'database'), + + /* + |-------------------------------------------------------------------------- + | Cache Stores + |-------------------------------------------------------------------------- + | + | Here you may define all of the cache "stores" for your application as + | well as their drivers. You may even define multiple stores for the + | same cache driver to group types of items stored in your caches. + | + | Supported drivers: "array", "database", "file", "memcached", + | "redis", "dynamodb", "storage", "octane", + | "session", "failover", "null" + | + */ + + 'stores' => [ + + 'array' => [ + 'driver' => 'array', + 'serialize' => false, + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_CACHE_CONNECTION'), + 'table' => env('DB_CACHE_TABLE', 'cache'), + 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'), + 'lock_table' => env('DB_CACHE_LOCK_TABLE'), + ], + + 'file' => [ + 'driver' => 'file', + 'path' => storage_path('framework/cache/data'), + 'lock_path' => storage_path('framework/cache/data'), + ], + + 'storage' => [ + 'driver' => 'storage', + 'disk' => env('CACHE_STORAGE_DISK'), + 'path' => env('CACHE_STORAGE_PATH', 'framework/cache/data'), + ], + + 'memcached' => [ + 'driver' => 'memcached', + 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), + 'sasl' => [ + env('MEMCACHED_USERNAME'), + env('MEMCACHED_PASSWORD'), + ], + 'options' => [ + // Memcached::OPT_CONNECT_TIMEOUT => 2000, + ], + 'servers' => [ + [ + 'host' => env('MEMCACHED_HOST', '127.0.0.1'), + 'port' => env('MEMCACHED_PORT', 11211), + 'weight' => 100, + ], + ], + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'), + 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'), + ], + + 'dynamodb' => [ + 'driver' => 'dynamodb', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), + 'endpoint' => env('DYNAMODB_ENDPOINT'), + ], + + 'octane' => [ + 'driver' => 'octane', + ], + + 'failover' => [ + 'driver' => 'failover', + 'stores' => [ + 'database', + 'array', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Cache Key Prefix + |-------------------------------------------------------------------------- + | + | When utilizing the APC, database, memcached, Redis, and DynamoDB cache + | stores, there might be other applications using the same cache. For + | that reason, you may prefix every cache key to avoid collisions. + | + */ + + 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'), + + /* + |-------------------------------------------------------------------------- + | Serializable Classes + |-------------------------------------------------------------------------- + | + | This value determines the classes that can be unserialized from cache + | storage. By default, no PHP classes will be unserialized from your + | cache to prevent gadget chain attacks if your APP_KEY is leaked. + | + */ + + 'serializable_classes' => false, + +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..abbb88e --- /dev/null +++ b/config/database.php @@ -0,0 +1,184 @@ + env('DB_CONNECTION', 'sqlite'), + + /* + |-------------------------------------------------------------------------- + | Database Connections + |-------------------------------------------------------------------------- + | + | Below are all of the database connections defined for your application. + | An example configuration is provided for each database system which + | is supported by Laravel. You're free to add / remove connections. + | + */ + + 'connections' => [ + + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + 'transaction_mode' => 'DEFERRED', + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'mariadb' => [ + 'driver' => 'mariadb', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + Mysql::ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + + 'pgsql' => [ + 'driver' => 'pgsql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '5432'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + 'search_path' => 'public', + 'sslmode' => env('DB_SSLMODE', 'prefer'), + ], + + 'sqlsrv' => [ + 'driver' => 'sqlsrv', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', 'localhost'), + 'port' => env('DB_PORT', '1433'), + 'database' => env('DB_DATABASE', 'laravel'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'charset' => env('DB_CHARSET', 'utf8'), + 'prefix' => '', + 'prefix_indexes' => true, + // 'encrypt' => env('DB_ENCRYPT', 'yes'), + // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Migration Repository Table + |-------------------------------------------------------------------------- + | + | This table keeps track of all the migrations that have already run for + | your application. Using this information, we can determine which of + | the migrations on disk haven't actually been run on the database. + | + */ + + 'migrations' => [ + 'table' => 'migrations', + 'update_date_on_publish' => true, + ], + + /* + |-------------------------------------------------------------------------- + | Redis Databases + |-------------------------------------------------------------------------- + | + | Redis is an open source, fast, and advanced key-value store that also + | provides a richer body of commands than a typical key-value system + | such as Memcached. You may define your connection settings here. + | + */ + + 'redis' => [ + + 'client' => env('REDIS_CLIENT', 'phpredis'), + + 'options' => [ + 'cluster' => env('REDIS_CLUSTER', 'redis'), + 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'), + 'persistent' => env('REDIS_PERSISTENT', false), + ], + + 'default' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_DB', '0'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + 'cache' => [ + 'url' => env('REDIS_URL'), + 'host' => env('REDIS_HOST', '127.0.0.1'), + 'username' => env('REDIS_USERNAME'), + 'password' => env('REDIS_PASSWORD'), + 'port' => env('REDIS_PORT', '6379'), + 'database' => env('REDIS_CACHE_DB', '1'), + 'max_retries' => env('REDIS_MAX_RETRIES', 3), + 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'), + 'backoff_base' => env('REDIS_BACKOFF_BASE', 100), + 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000), + ], + + ], + +]; diff --git a/config/filesystems.php b/config/filesystems.php new file mode 100644 index 0000000..37d8fca --- /dev/null +++ b/config/filesystems.php @@ -0,0 +1,80 @@ + env('FILESYSTEM_DISK', 'local'), + + /* + |-------------------------------------------------------------------------- + | Filesystem Disks + |-------------------------------------------------------------------------- + | + | Below you may configure as many filesystem disks as necessary, and you + | may even configure multiple disks for the same driver. Examples for + | most supported storage drivers are configured here for reference. + | + | Supported drivers: "local", "ftp", "sftp", "s3" + | + */ + + 'disks' => [ + + 'local' => [ + 'driver' => 'local', + 'root' => storage_path('app/private'), + 'serve' => true, + 'throw' => false, + 'report' => false, + ], + + 'public' => [ + 'driver' => 'local', + 'root' => storage_path('app/public'), + 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage', + 'visibility' => 'public', + 'throw' => false, + 'report' => false, + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'url' => env('AWS_URL'), + 'endpoint' => env('AWS_ENDPOINT'), + 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), + 'throw' => false, + 'report' => false, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Symbolic Links + |-------------------------------------------------------------------------- + | + | Here you may configure the symbolic links that will be created when the + | `storage:link` Artisan command is executed. The array keys should be + | the locations of the links and the values should be their targets. + | + */ + + 'links' => [ + public_path('storage') => storage_path('app/public'), + ], + +]; diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..b09cb25 --- /dev/null +++ b/config/logging.php @@ -0,0 +1,132 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => [ + 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + 'trace' => env('LOG_DEPRECATIONS_TRACE', false), + ], + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Laravel + | utilizes the Monolog PHP logging library, which includes a variety + | of powerful log handlers and formatters that you're free to use. + | + | Available drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", "custom", "stack" + | + */ + + 'channels' => [ + + 'stack' => [ + 'driver' => 'stack', + 'channels' => explode(',', (string) env('LOG_STACK', 'single')), + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => env('LOG_DAILY_DAYS', 14), + 'replace_placeholders' => true, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')), + 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'), + 'level' => env('LOG_LEVEL', 'critical'), + 'replace_placeholders' => true, + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class), + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'), + ], + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'handler_with' => [ + 'stream' => 'php://stderr', + ], + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'processors' => [PsrLogMessageProcessor::class], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER), + 'replace_placeholders' => true, + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + 'replace_placeholders' => true, + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + + ], + +]; diff --git a/config/mail.php b/config/mail.php new file mode 100644 index 0000000..e32e88d --- /dev/null +++ b/config/mail.php @@ -0,0 +1,118 @@ + env('MAIL_MAILER', 'log'), + + /* + |-------------------------------------------------------------------------- + | Mailer Configurations + |-------------------------------------------------------------------------- + | + | Here you may configure all of the mailers used by your application plus + | their respective settings. Several examples have been configured for + | you and you are free to add your own as your application requires. + | + | Laravel supports a variety of mail "transport" drivers that can be used + | when delivering an email. You may specify which one you're using for + | your mailers below. You may also add additional mailers if needed. + | + | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2", + | "postmark", "resend", "log", "array", + | "failover", "roundrobin" + | + */ + + 'mailers' => [ + + 'smtp' => [ + 'transport' => 'smtp', + 'scheme' => env('MAIL_SCHEME'), + 'url' => env('MAIL_URL'), + 'host' => env('MAIL_HOST', '127.0.0.1'), + 'port' => env('MAIL_PORT', 2525), + 'username' => env('MAIL_USERNAME'), + 'password' => env('MAIL_PASSWORD'), + 'timeout' => null, + 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)), + ], + + 'ses' => [ + 'transport' => 'ses', + ], + + 'postmark' => [ + 'transport' => 'postmark', + // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'), + // 'client' => [ + // 'timeout' => 5, + // ], + ], + + 'resend' => [ + 'transport' => 'resend', + ], + + 'sendmail' => [ + 'transport' => 'sendmail', + 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'), + ], + + 'log' => [ + 'transport' => 'log', + 'channel' => env('MAIL_LOG_CHANNEL'), + ], + + 'array' => [ + 'transport' => 'array', + ], + + 'failover' => [ + 'transport' => 'failover', + 'mailers' => [ + 'smtp', + 'log', + ], + 'retry_after' => 60, + ], + + 'roundrobin' => [ + 'transport' => 'roundrobin', + 'mailers' => [ + 'ses', + 'postmark', + ], + 'retry_after' => 60, + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Global "From" Address + |-------------------------------------------------------------------------- + | + | You may wish for all emails sent by your application to be sent from + | the same address. Here you may specify a name and address that is + | used globally for all emails that are sent by your application. + | + */ + + 'from' => [ + 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'), + 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')), + ], + +]; diff --git a/config/mbip.php b/config/mbip.php new file mode 100644 index 0000000..146ad7b --- /dev/null +++ b/config/mbip.php @@ -0,0 +1,9 @@ + env('MAX_UPLOAD_SIZE_KB', 20480), + + // Aktifkan penghantaran peringatan emel (perlu konfigurasi SMTP). + 'reminder_email_enabled' => env('REMINDER_EMAIL_ENABLED', false), +]; diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..8f1f452 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,219 @@ + [ + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * Eloquent model should be used to retrieve your permissions. Of course, it + * is often just the "Permission" model but you may use whatever you like. + * + * The model you want to use as a Permission model needs to implement the + * `Spatie\Permission\Contracts\Permission` contract. + */ + + 'permission' => Permission::class, + + /* + * When using the "HasRoles" trait from this package, we need to know which + * Eloquent model should be used to retrieve your roles. Of course, it + * is often just the "Role" model but you may use whatever you like. + * + * The model you want to use as a Role model needs to implement the + * `Spatie\Permission\Contracts\Role` contract. + */ + + 'role' => Role::class, + + /* + * When using the "Teams" feature from this package, we need to know which + * Eloquent model should be used to retrieve your teams. Of course, it + * is often just the "Team" model but you may use whatever you like. + */ + 'team' => null, + + /* + * When using the "HasModels" trait and passing raw IDs to syncModels, + * attachModels, or detachModels, this model class will be used to + * resolve those IDs. If null, defaults to the guard's model. + */ + 'default_model' => null, + ], + + 'table_names' => [ + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'roles' => 'roles', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your permissions. We have chosen a basic + * default value but you may easily change it to any table you like. + */ + + 'permissions' => 'permissions', + + /* + * When using the "HasPermissions" trait from this package, we need to know which + * table should be used to retrieve your models permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_permissions' => 'model_has_permissions', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your models roles. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'model_has_roles' => 'model_has_roles', + + /* + * When using the "HasRoles" trait from this package, we need to know which + * table should be used to retrieve your roles permissions. We have chosen a + * basic default value but you may easily change it to any table you like. + */ + + 'role_has_permissions' => 'role_has_permissions', + ], + + 'column_names' => [ + /* + * Change this if you want to name the related pivots other than defaults + */ + 'role_pivot_key' => null, // default 'role_id', + 'permission_pivot_key' => null, // default 'permission_id', + + /* + * Change this if you want to name the related model primary key other than + * `model_id`. + * + * For example, this would be nice if your primary keys are all UUIDs. In + * that case, name this `model_uuid`. + */ + + 'model_morph_key' => 'model_id', + + /* + * Change this if you want to use the teams feature and your related model's + * foreign key is other than `team_id`. + */ + + 'team_foreign_key' => 'team_id', + ], + + /* + * When set to true, the method for checking permissions will be registered on the gate. + * Set this to false if you want to implement custom logic for checking permissions. + */ + + 'register_permission_check_method' => true, + + /* + * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered + * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated + * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it. + */ + 'register_octane_reset_listener' => false, + + /* + * Events will fire when a role or permission is assigned/unassigned: + * \Spatie\Permission\Events\RoleAttachedEvent + * \Spatie\Permission\Events\RoleDetachedEvent + * \Spatie\Permission\Events\PermissionAttachedEvent + * \Spatie\Permission\Events\PermissionDetachedEvent + * + * To enable, set to true, and then create listeners to watch these events. + */ + 'events_enabled' => false, + + /* + * Teams Feature. + * When set to true the package implements teams using the 'team_foreign_key'. + * If you want the migrations to register the 'team_foreign_key', you must + * set this to true before doing the migration. + * If you already did the migration then you must make a new migration to also + * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions' + * (view the latest version of this package's migration file) + */ + + 'teams' => false, + + /* + * The class to use to resolve the permissions team id + */ + 'team_resolver' => DefaultTeamResolver::class, + + /* + * Passport Client Credentials Grant + * When set to true the package will use Passports Client to check permissions + */ + + 'use_passport_client_credentials' => false, + + /* + * When set to true, the required permission names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_permission_in_exception' => false, + + /* + * When set to true, the required role names are added to exception messages. + * This could be considered an information leak in some contexts, so the default + * setting is false here for optimum safety. + */ + + 'display_role_in_exception' => false, + + /* + * By default wildcard permission lookups are disabled. + * See documentation to understand supported syntax. + */ + + 'enable_wildcard_permission' => false, + + /* + * The class to use for interpreting wildcard permissions. + * If you need to modify delimiters, override the class and specify its name here. + */ + // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class, + + /* Cache-specific settings */ + + 'cache' => [ + + /* + * By default all permissions are cached for 24 hours to speed up performance. + * When permissions or roles are updated the cache is flushed automatically. + */ + + 'expiration_time' => DateInterval::createFromDateString('24 hours'), + + /* + * The cache key used to store all permissions. + */ + + 'key' => 'spatie.permission.cache', + + /* + * You may optionally indicate a specific cache driver to use for permission and + * role caching using any of the `store` drivers listed in the cache.php config + * file. Using 'default' here means to use the `default` set in cache.php. + */ + + 'store' => 'default', + ], +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..79c2c0a --- /dev/null +++ b/config/queue.php @@ -0,0 +1,129 @@ + env('QUEUE_CONNECTION', 'database'), + + /* + |-------------------------------------------------------------------------- + | Queue Connections + |-------------------------------------------------------------------------- + | + | Here you may configure the connection options for every queue backend + | used by your application. An example configuration is provided for + | each backend supported by Laravel. You're also free to add more. + | + | Drivers: "sync", "database", "beanstalkd", "sqs", "redis", + | "deferred", "background", "failover", "null" + | + */ + + 'connections' => [ + + 'sync' => [ + 'driver' => 'sync', + ], + + 'database' => [ + 'driver' => 'database', + 'connection' => env('DB_QUEUE_CONNECTION'), + 'table' => env('DB_QUEUE_TABLE', 'jobs'), + 'queue' => env('DB_QUEUE', 'default'), + 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), + 'after_commit' => false, + ], + + 'beanstalkd' => [ + 'driver' => 'beanstalkd', + 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'), + 'queue' => env('BEANSTALKD_QUEUE', 'default'), + 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90), + 'block_for' => 0, + 'after_commit' => false, + ], + + 'sqs' => [ + 'driver' => 'sqs', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'), + 'queue' => env('SQS_QUEUE', 'default'), + 'suffix' => env('SQS_SUFFIX'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + 'after_commit' => false, + ], + + 'redis' => [ + 'driver' => 'redis', + 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), + 'queue' => env('REDIS_QUEUE', 'default'), + 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), + 'block_for' => null, + 'after_commit' => false, + ], + + 'deferred' => [ + 'driver' => 'deferred', + ], + + 'background' => [ + 'driver' => 'background', + ], + + 'failover' => [ + 'driver' => 'failover', + 'connections' => [ + 'database', + 'deferred', + ], + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Job Batching + |-------------------------------------------------------------------------- + | + | The following options configure the database and table that store job + | batching information. These options can be updated to any database + | connection and table which has been defined by your application. + | + */ + + 'batching' => [ + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'job_batches', + ], + + /* + |-------------------------------------------------------------------------- + | Failed Queue Jobs + |-------------------------------------------------------------------------- + | + | These options configure the behavior of failed queue job logging so you + | can control how and where failed jobs are stored. Laravel ships with + | support for storing failed jobs in a simple file or in a database. + | + | Supported drivers: "database-uuids", "dynamodb", "file", "null" + | + */ + + 'failed' => [ + 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), + 'database' => env('DB_CONNECTION', 'sqlite'), + 'table' => 'failed_jobs', + ], + +]; diff --git a/config/services.php b/config/services.php new file mode 100644 index 0000000..6a90eb8 --- /dev/null +++ b/config/services.php @@ -0,0 +1,38 @@ + [ + 'key' => env('POSTMARK_API_KEY'), + ], + + 'resend' => [ + 'key' => env('RESEND_API_KEY'), + ], + + 'ses' => [ + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), + ], + + 'slack' => [ + 'notifications' => [ + 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'), + 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'), + ], + ], + +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..f574482 --- /dev/null +++ b/config/session.php @@ -0,0 +1,233 @@ + env('SESSION_DRIVER', 'database'), + + /* + |-------------------------------------------------------------------------- + | Session Lifetime + |-------------------------------------------------------------------------- + | + | Here you may specify the number of minutes that you wish the session + | to be allowed to remain idle before it expires. If you want them + | to expire immediately when the browser is closed then you may + | indicate that via the expire_on_close configuration option. + | + */ + + 'lifetime' => (int) env('SESSION_LIFETIME', 120), + + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + + /* + |-------------------------------------------------------------------------- + | Session Encryption + |-------------------------------------------------------------------------- + | + | This option allows you to easily specify that all of your session data + | should be encrypted before it's stored. All encryption is performed + | automatically by Laravel and you may use the session like normal. + | + */ + + 'encrypt' => env('SESSION_ENCRYPT', false), + + /* + |-------------------------------------------------------------------------- + | Session File Location + |-------------------------------------------------------------------------- + | + | When utilizing the "file" session driver, the session files are placed + | on disk. The default storage location is defined here; however, you + | are free to provide another location where they should be stored. + | + */ + + 'files' => storage_path('framework/sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Database Connection + |-------------------------------------------------------------------------- + | + | When using the "database" or "redis" session drivers, you may specify a + | connection that should be used to manage these sessions. This should + | correspond to a connection in your database configuration options. + | + */ + + 'connection' => env('SESSION_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Session Database Table + |-------------------------------------------------------------------------- + | + | When using the "database" session driver, you may specify the table to + | be used to store sessions. Of course, a sensible default is defined + | for you; however, you're welcome to change this to another table. + | + */ + + 'table' => env('SESSION_TABLE', 'sessions'), + + /* + |-------------------------------------------------------------------------- + | Session Cache Store + |-------------------------------------------------------------------------- + | + | When using one of the framework's cache driven session backends, you may + | define the cache store which should be used to store the session data + | between requests. This must match one of your defined cache stores. + | + | Affects: "dynamodb", "memcached", "redis" + | + */ + + 'store' => env('SESSION_STORE'), + + /* + |-------------------------------------------------------------------------- + | Session Sweeping Lottery + |-------------------------------------------------------------------------- + | + | Some session drivers must manually sweep their storage location to get + | rid of old sessions from storage. Here are the chances that it will + | happen on a given request. By default, the odds are 2 out of 100. + | + */ + + 'lottery' => [2, 100], + + /* + |-------------------------------------------------------------------------- + | Session Cookie Name + |-------------------------------------------------------------------------- + | + | Here you may change the name of the session cookie that is created by + | the framework. Typically, you should not need to change this value + | since doing so does not grant a meaningful security improvement. + | + */ + + 'cookie' => env( + 'SESSION_COOKIE', + Str::slug((string) env('APP_NAME', 'laravel')).'-session' + ), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Path + |-------------------------------------------------------------------------- + | + | The session cookie path determines the path for which the cookie will + | be regarded as available. Typically, this will be the root path of + | your application, but you're free to change this when necessary. + | + */ + + 'path' => env('SESSION_PATH', '/'), + + /* + |-------------------------------------------------------------------------- + | Session Cookie Domain + |-------------------------------------------------------------------------- + | + | This value determines the domain and subdomains the session cookie is + | available to. By default, the cookie will be available to the root + | domain without subdomains. Typically, this shouldn't be changed. + | + */ + + 'domain' => env('SESSION_DOMAIN'), + + /* + |-------------------------------------------------------------------------- + | HTTPS Only Cookies + |-------------------------------------------------------------------------- + | + | By setting this option to true, session cookies will only be sent back + | to the server if the browser has a HTTPS connection. This will keep + | the cookie from being sent to you when it can't be done securely. + | + */ + + 'secure' => env('SESSION_SECURE_COOKIE'), + + /* + |-------------------------------------------------------------------------- + | HTTP Access Only + |-------------------------------------------------------------------------- + | + | Setting this value to true will prevent JavaScript from accessing the + | value of the cookie and the cookie will only be accessible through + | the HTTP protocol. It's unlikely you should disable this option. + | + */ + + 'http_only' => env('SESSION_HTTP_ONLY', true), + + /* + |-------------------------------------------------------------------------- + | Same-Site Cookies + |-------------------------------------------------------------------------- + | + | This option determines how your cookies behave when cross-site requests + | take place, and can be used to mitigate CSRF attacks. By default, we + | will set this value to "lax" to permit secure cross-site requests. + | + | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value + | + | Supported: "lax", "strict", "none", null + | + */ + + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + + /* + |-------------------------------------------------------------------------- + | Partitioned Cookies + |-------------------------------------------------------------------------- + | + | Setting this value to true will tie the cookie to the top-level site for + | a cross-site context. Partitioned cookies are accepted by the browser + | when flagged "secure" and the Same-Site attribute is set to "none". + | + */ + + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), + + /* + |-------------------------------------------------------------------------- + | Session Serialization + |-------------------------------------------------------------------------- + | + | This value controls the serialization strategy for session data, which + | is JSON by default. Setting this to "php" allows the storage of PHP + | objects in the session but can make an application vulnerable to + | "gadget chain" serialization attacks if the APP_KEY is leaked. + | + | Supported: "json", "php" + | + */ + + 'serialization' => 'json', + +]; diff --git a/database/.gitignore b/database/.gitignore new file mode 100644 index 0000000..9b19b93 --- /dev/null +++ b/database/.gitignore @@ -0,0 +1 @@ +*.sqlite* diff --git a/database/factories/ConsultantFactory.php b/database/factories/ConsultantFactory.php new file mode 100644 index 0000000..8151b71 --- /dev/null +++ b/database/factories/ConsultantFactory.php @@ -0,0 +1,28 @@ + fake()->company().' Sdn Bhd', + 'no_pendaftaran_syarikat' => fake()->numerify('20########').' ('.fake()->numerify('#######-A').')', + 'alamat' => fake()->address(), + 'emel' => fake()->unique()->safeEmail(), + 'telefon' => fake()->numerify('07-#######'), + 'aktif' => true, + ]; + } + + public function tidakAktif(): static + { + return $this->state(fn () => ['aktif' => false]); + } +} diff --git a/database/factories/DataCentreFactory.php b/database/factories/DataCentreFactory.php new file mode 100644 index 0000000..f513234 --- /dev/null +++ b/database/factories/DataCentreFactory.php @@ -0,0 +1,25 @@ + 'Pusat Data '.fake()->city(), + 'tajuk_permohonan' => 'Permohonan Lesen '.fake()->words(2, true), + 'lokasi_pusat_data' => fake()->city(), + 'alamat' => fake()->address(), + 'keluasan_tapak' => fake()->randomFloat(2, 1, 20), + 'it_load' => fake()->randomFloat(2, 5, 100), + 'status' => 'aktif', + 'current_consultant_id' => null, + ]; + } +} diff --git a/database/factories/ReportingCycleFactory.php b/database/factories/ReportingCycleFactory.php new file mode 100644 index 0000000..0b431d5 --- /dev/null +++ b/database/factories/ReportingCycleFactory.php @@ -0,0 +1,44 @@ +numberBetween(2026, 2035); + $reportingYear = $renewalYear - 2; + + return [ + 'renewal_year' => $renewalYear, + 'reporting_year' => $reportingYear, + 'period_start' => Carbon::create($reportingYear, 1, 1), + 'period_end' => Carbon::create($reportingYear, 12, 31), + 'cut_off_date' => Carbon::create($renewalYear - 1, 12, 1), + 'licence_period_year' => 1, + 'status' => 'aktif', + ]; + } + + public function renewalYear(int $year): static + { + return $this->state(fn () => [ + 'renewal_year' => $year, + 'reporting_year' => $year - 2, + 'period_start' => Carbon::create($year - 2, 1, 1), + 'period_end' => Carbon::create($year - 2, 12, 31), + 'cut_off_date' => Carbon::create($year - 1, 12, 1), + ]); + } + + public function overdue(): static + { + return $this->state(fn () => ['cut_off_date' => now()->subDays(5)]); + } +} diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php new file mode 100644 index 0000000..c4ceb07 --- /dev/null +++ b/database/factories/UserFactory.php @@ -0,0 +1,45 @@ + + */ +class UserFactory extends Factory +{ + /** + * The current password being used by the factory. + */ + protected static ?string $password; + + /** + * Define the model's default state. + * + * @return array + */ + public function definition(): array + { + return [ + 'name' => fake()->name(), + 'email' => fake()->unique()->safeEmail(), + 'email_verified_at' => now(), + 'password' => static::$password ??= Hash::make('password'), + 'remember_token' => Str::random(10), + ]; + } + + /** + * Indicate that the model's email address should be unverified. + */ + public function unverified(): static + { + return $this->state(fn (array $attributes) => [ + 'email_verified_at' => null, + ]); + } +} diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..05fb5d9 --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,49 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password'); + $table->rememberToken(); + $table->timestamps(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('users'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('sessions'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..06dc7a5 --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,35 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->bigInteger('expiration')->index(); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->bigInteger('expiration')->index(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('cache'); + Schema::dropIfExists('cache_locks'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..edac6fe --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,59 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedSmallInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->string('connection'); + $table->string('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + + $table->index(['connection', 'queue', 'failed_at']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('failed_jobs'); + } +}; diff --git a/database/migrations/2026_06_23_083811_create_permission_tables.php b/database/migrations/2026_06_23_083811_create_permission_tables.php new file mode 100644 index 0000000..8986275 --- /dev/null +++ b/database/migrations/2026_06_23_083811_create_permission_tables.php @@ -0,0 +1,137 @@ +id(); // permission id + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + + $table->unique(['name', 'guard_name']); + }); + + /** + * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered. + */ + Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) { + $table->id(); // role id + if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing + $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable(); + $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index'); + } + $table->string('name'); + $table->string('guard_name'); + $table->timestamps(); + if ($teams || config('permission.testing')) { + $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']); + } else { + $table->unique(['name', 'guard_name']); + } + }); + + Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) { + $table->unsignedBigInteger($pivotPermission); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index'); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } else { + $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'], + 'model_has_permissions_permission_model_type_primary'); + } + }); + + Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) { + $table->unsignedBigInteger($pivotRole); + + $table->string('model_type'); + $table->unsignedBigInteger($columnNames['model_morph_key']); + $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index'); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + if ($teams) { + $table->unsignedBigInteger($columnNames['team_foreign_key']); + $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index'); + + $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } else { + $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'], + 'model_has_roles_role_model_type_primary'); + } + }); + + Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) { + $table->unsignedBigInteger($pivotPermission); + $table->unsignedBigInteger($pivotRole); + + $table->foreign($pivotPermission) + ->references('id') // permission id + ->on($tableNames['permissions']) + ->cascadeOnDelete(); + + $table->foreign($pivotRole) + ->references('id') // role id + ->on($tableNames['roles']) + ->cascadeOnDelete(); + + $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary'); + }); + + app('cache') + ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null) + ->forget(config('permission.cache.key')); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + $tableNames = config('permission.table_names'); + + throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.'); + + Schema::dropIfExists($tableNames['role_has_permissions']); + Schema::dropIfExists($tableNames['model_has_roles']); + Schema::dropIfExists($tableNames['model_has_permissions']); + Schema::dropIfExists($tableNames['roles']); + Schema::dropIfExists($tableNames['permissions']); + } +}; diff --git a/database/migrations/2026_06_23_083812_create_activity_log_table.php b/database/migrations/2026_06_23_083812_create_activity_log_table.php new file mode 100644 index 0000000..5c17c24 --- /dev/null +++ b/database/migrations/2026_06_23_083812_create_activity_log_table.php @@ -0,0 +1,23 @@ +id(); + $table->string('log_name')->nullable()->index(); + $table->text('description'); + $table->nullableMorphs('subject', 'subject'); + $table->string('event')->nullable(); + $table->nullableMorphs('causer', 'causer'); + $table->json('attribute_changes')->nullable(); + $table->json('properties')->nullable(); + $table->timestamps(); + }); + } +}; diff --git a/database/migrations/2026_06_23_090001_add_fields_to_users_table.php b/database/migrations/2026_06_23_090001_add_fields_to_users_table.php new file mode 100644 index 0000000..b0bf8af --- /dev/null +++ b/database/migrations/2026_06_23_090001_add_fields_to_users_table.php @@ -0,0 +1,26 @@ +string('phone')->nullable()->after('email'); + $table->string('jawatan')->nullable()->after('phone'); // jawatan rasmi + $table->boolean('is_active')->default(true)->after('jawatan'); + $table->softDeletes(); + $table->index('is_active'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn(['phone', 'jawatan', 'is_active', 'deleted_at']); + }); + } +}; diff --git a/database/migrations/2026_06_23_090002_create_consultants_table.php b/database/migrations/2026_06_23_090002_create_consultants_table.php new file mode 100644 index 0000000..9097c26 --- /dev/null +++ b/database/migrations/2026_06_23_090002_create_consultants_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->string('nama_perunding'); + $table->string('no_pendaftaran_syarikat')->nullable(); + $table->text('alamat')->nullable(); + $table->string('emel')->nullable(); + $table->string('telefon')->nullable(); + $table->boolean('aktif')->default(true); + $table->timestamps(); + $table->softDeletes(); + + $table->index('aktif'); + $table->index('nama_perunding'); + }); + } + + public function down(): void + { + Schema::dropIfExists('consultants'); + } +}; diff --git a/database/migrations/2026_06_23_090003_create_data_centres_table.php b/database/migrations/2026_06_23_090003_create_data_centres_table.php new file mode 100644 index 0000000..8aff217 --- /dev/null +++ b/database/migrations/2026_06_23_090003_create_data_centres_table.php @@ -0,0 +1,34 @@ +id(); + $table->string('nama_pusat_data'); + $table->string('tajuk_permohonan')->nullable(); + $table->string('lokasi_pusat_data')->nullable(); + $table->text('alamat')->nullable(); + $table->decimal('keluasan_tapak', 15, 2)->nullable()->comment('Keluasan tapak (ekar/m2)'); + $table->decimal('it_load', 15, 2)->nullable()->comment('Kapasiti penjanaan elektrik komponen IT / IT Load (MW)'); + $table->string('status')->default('aktif'); // aktif / tidak_aktif + $table->foreignId('current_consultant_id')->nullable()->constrained('consultants')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + + $table->index('status'); + $table->index('current_consultant_id'); + $table->index('nama_pusat_data'); + }); + } + + public function down(): void + { + Schema::dropIfExists('data_centres'); + } +}; diff --git a/database/migrations/2026_06_23_090004_create_data_centre_consultant_assignments_table.php b/database/migrations/2026_06_23_090004_create_data_centre_consultant_assignments_table.php new file mode 100644 index 0000000..3ad116f --- /dev/null +++ b/database/migrations/2026_06_23_090004_create_data_centre_consultant_assignments_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('data_centre_id')->constrained('data_centres')->cascadeOnDelete(); + $table->foreignId('consultant_id')->constrained('consultants')->cascadeOnDelete(); + $table->date('start_date'); + $table->date('end_date')->nullable(); + $table->text('reason')->nullable(); // sebab perubahan + $table->foreignId('assigned_by')->nullable()->constrained('users')->nullOnDelete(); + $table->boolean('is_active')->default(true); + $table->timestamps(); + + $table->index(['data_centre_id', 'is_active'], 'dcca_dc_active_idx'); + $table->index('consultant_id', 'dcca_consultant_idx'); + }); + } + + public function down(): void + { + Schema::dropIfExists('data_centre_consultant_assignments'); + } +}; diff --git a/database/migrations/2026_06_23_090005_create_checklist_templates_table.php b/database/migrations/2026_06_23_090005_create_checklist_templates_table.php new file mode 100644 index 0000000..0d3b080 --- /dev/null +++ b/database/migrations/2026_06_23_090005_create_checklist_templates_table.php @@ -0,0 +1,28 @@ +id(); + $table->string('name'); + $table->string('version')->default('1.0'); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(false); + $table->timestamps(); + $table->softDeletes(); + + $table->index('is_active'); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_templates'); + } +}; diff --git a/database/migrations/2026_06_23_090006_create_checklist_sections_table.php b/database/migrations/2026_06_23_090006_create_checklist_sections_table.php new file mode 100644 index 0000000..e729005 --- /dev/null +++ b/database/migrations/2026_06_23_090006_create_checklist_sections_table.php @@ -0,0 +1,29 @@ +id(); + $table->foreignId('checklist_template_id')->constrained('checklist_templates')->cascadeOnDelete(); + $table->string('code')->comment('Kod seksyen cth: A, B, C, D, E, LAIN'); + $table->string('title'); + $table->text('description')->nullable(); + $table->unsignedInteger('order')->default(0); + $table->boolean('is_formula_enabled')->default(false)->comment('Seksyen menggunakan formula atau tidak'); + $table->timestamps(); + + $table->index(['checklist_template_id', 'order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_sections'); + } +}; diff --git a/database/migrations/2026_06_23_090007_create_checklist_scopes_table.php b/database/migrations/2026_06_23_090007_create_checklist_scopes_table.php new file mode 100644 index 0000000..ec2437c --- /dev/null +++ b/database/migrations/2026_06_23_090007_create_checklist_scopes_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('checklist_section_id')->constrained('checklist_sections')->cascadeOnDelete(); + $table->string('code')->comment('cth: SKOP1, SKOP2, SKOP3'); + $table->string('title'); + $table->text('description')->nullable(); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + + $table->index(['checklist_section_id', 'order']); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_scopes'); + } +}; diff --git a/database/migrations/2026_06_23_090008_create_checklist_items_table.php b/database/migrations/2026_06_23_090008_create_checklist_items_table.php new file mode 100644 index 0000000..2b66cb9 --- /dev/null +++ b/database/migrations/2026_06_23_090008_create_checklist_items_table.php @@ -0,0 +1,37 @@ +id(); + $table->foreignId('checklist_section_id')->constrained('checklist_sections')->cascadeOnDelete(); + $table->foreignId('checklist_scope_id')->nullable()->constrained('checklist_scopes')->nullOnDelete(); + $table->string('code')->comment('Kod rujukan cth: A.1.i, 2(i)'); + $table->string('formula_token')->nullable()->comment('Token unik untuk rujukan formula cth: SKOP1, EE, RE'); + $table->string('label'); + $table->text('description')->nullable(); + $table->string('input_type')->default('number'); // number,text,textarea,select,checkbox,file_reference,calculated + $table->json('options')->nullable()->comment('Pilihan untuk input jenis select/checkbox'); + $table->string('unit')->nullable()->comment('cth: tCO2e, %, tahun'); + $table->boolean('is_required')->default(false); + $table->boolean('is_calculated')->default(false)->comment('Item formula/dikira automatik'); + $table->text('formula')->nullable()->comment('Ungkapan formula jika item dikira'); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + + $table->index(['checklist_section_id', 'order']); + $table->index('formula_token'); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_items'); + } +}; diff --git a/database/migrations/2026_06_23_090009_create_checklist_formulas_table.php b/database/migrations/2026_06_23_090009_create_checklist_formulas_table.php new file mode 100644 index 0000000..fb87af5 --- /dev/null +++ b/database/migrations/2026_06_23_090009_create_checklist_formulas_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('checklist_template_id')->constrained('checklist_templates')->cascadeOnDelete(); + $table->foreignId('checklist_section_id')->nullable()->constrained('checklist_sections')->cascadeOnDelete(); + $table->string('result_token')->comment('Token hasil yang dikira cth: A, B, C, D, E'); + $table->string('label')->nullable(); + $table->text('expression')->comment('Ungkapan RHS cth: SKOP1 + SKOP2 + SKOP3 atau A - B'); + $table->string('unit')->nullable(); + $table->boolean('guard_div_zero')->default(false)->comment('Lindung pembahagian dengan sifar'); + $table->unsignedInteger('order')->default(0); + $table->timestamps(); + + $table->index(['checklist_template_id', 'order']); + $table->index('result_token'); + }); + } + + public function down(): void + { + Schema::dropIfExists('checklist_formulas'); + } +}; diff --git a/database/migrations/2026_06_23_090012_create_reporting_cycles_table.php b/database/migrations/2026_06_23_090012_create_reporting_cycles_table.php new file mode 100644 index 0000000..8c9b39e --- /dev/null +++ b/database/migrations/2026_06_23_090012_create_reporting_cycles_table.php @@ -0,0 +1,33 @@ +id(); + $table->unsignedSmallInteger('renewal_year')->unique()->comment('Tahun pembaharuan lesen'); + $table->unsignedSmallInteger('reporting_year')->comment('Tahun pelaporan = renewal_year - 2'); + $table->date('period_start')->comment('1 Januari tahun pelaporan'); + $table->date('period_end')->comment('31 Disember tahun pelaporan'); + $table->date('cut_off_date')->comment('Tarikh tutup serahan, boleh dikonfigur JPP'); + $table->unsignedSmallInteger('licence_period_year')->default(1); + $table->foreignId('checklist_template_id')->nullable()->constrained('checklist_templates')->nullOnDelete(); + $table->string('status')->default('aktif'); // aktif / tutup + $table->text('catatan')->nullable(); + $table->timestamps(); + + $table->index('status'); + $table->index('reporting_year'); + }); + } + + public function down(): void + { + Schema::dropIfExists('reporting_cycles'); + } +}; diff --git a/database/migrations/2026_06_23_090013_create_submissions_table.php b/database/migrations/2026_06_23_090013_create_submissions_table.php new file mode 100644 index 0000000..3aa5918 --- /dev/null +++ b/database/migrations/2026_06_23_090013_create_submissions_table.php @@ -0,0 +1,52 @@ +id(); + $table->foreignId('data_centre_id')->constrained('data_centres')->cascadeOnDelete(); + $table->foreignId('reporting_cycle_id')->constrained('reporting_cycles')->cascadeOnDelete(); + $table->foreignId('consultant_id')->constrained('consultants')->cascadeOnDelete(); + $table->foreignId('checklist_template_id')->nullable()->constrained('checklist_templates')->nullOnDelete(); + + // draf, baru, dalam_tindakan, pembetulan_perunding, selesai + $table->string('status')->default('draf'); + + $table->boolean('is_locked')->default(false); + $table->foreignId('locked_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('locked_at')->nullable(); + $table->text('lock_reason')->nullable(); + + $table->foreignId('assigned_officer_id')->nullable()->constrained('users')->nullOnDelete(); + + $table->boolean('hardcopy_received')->default(false); + $table->date('hardcopy_received_date')->nullable(); + $table->date('hardcopy_sent_date')->nullable()->comment('Tarikh hardcopy dihantar ke Bahagian Kawalan Perancangan'); + + $table->timestamp('submitted_at')->nullable(); + $table->foreignId('submitted_by')->nullable()->constrained('users')->nullOnDelete(); + + $table->text('review_notes')->nullable(); + + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['data_centre_id', 'reporting_cycle_id'], 'submissions_dc_cycle_unique'); + $table->index('status'); + $table->index('consultant_id'); + $table->index('reporting_cycle_id'); + $table->index('assigned_officer_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('submissions'); + } +}; diff --git a/database/migrations/2026_06_23_090014_create_submission_answers_table.php b/database/migrations/2026_06_23_090014_create_submission_answers_table.php new file mode 100644 index 0000000..47f2cb8 --- /dev/null +++ b/database/migrations/2026_06_23_090014_create_submission_answers_table.php @@ -0,0 +1,33 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->foreignId('checklist_item_id')->constrained('checklist_items')->cascadeOnDelete(); + $table->text('value')->nullable(); + $table->string('unit')->nullable(); + $table->string('page_reference')->nullable()->comment('No muka surat / rujukan dalam laporan PDF'); + $table->text('consultant_note')->nullable(); + $table->text('officer_note')->nullable(); + // belum_semak, sah, tidak_sah + $table->string('validation_status')->default('belum_semak'); + $table->timestamps(); + + $table->unique(['submission_id', 'checklist_item_id'], 'answers_submission_item_unique'); + $table->index('checklist_item_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_answers'); + } +}; diff --git a/database/migrations/2026_06_23_090015_create_submission_results_table.php b/database/migrations/2026_06_23_090015_create_submission_results_table.php new file mode 100644 index 0000000..23e3978 --- /dev/null +++ b/database/migrations/2026_06_23_090015_create_submission_results_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->string('result_token')->comment('A, B, C, D, E'); + $table->string('label')->nullable(); + $table->decimal('value', 20, 4)->nullable(); + $table->string('unit')->nullable(); + $table->timestamps(); + + $table->unique(['submission_id', 'result_token'], 'results_submission_token_unique'); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_results'); + } +}; diff --git a/database/migrations/2026_06_23_090016_create_submission_documents_table.php b/database/migrations/2026_06_23_090016_create_submission_documents_table.php new file mode 100644 index 0000000..3f0f753 --- /dev/null +++ b/database/migrations/2026_06_23_090016_create_submission_documents_table.php @@ -0,0 +1,32 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->string('original_filename'); + $table->string('stored_path'); + $table->string('file_hash', 64)->nullable(); + $table->string('mime_type')->nullable(); + $table->unsignedBigInteger('size')->nullable(); + $table->unsignedInteger('version')->default(1); + $table->boolean('is_active')->default(true); + $table->foreignId('uploaded_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['submission_id', 'is_active']); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_documents'); + } +}; diff --git a/database/migrations/2026_06_23_090017_create_submission_status_histories_table.php b/database/migrations/2026_06_23_090017_create_submission_status_histories_table.php new file mode 100644 index 0000000..46b1da1 --- /dev/null +++ b/database/migrations/2026_06_23_090017_create_submission_status_histories_table.php @@ -0,0 +1,28 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->string('from_status')->nullable(); + $table->string('to_status'); + $table->foreignId('changed_by')->nullable()->constrained('users')->nullOnDelete(); + $table->text('notes')->nullable(); + $table->timestamps(); + + $table->index('submission_id'); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_status_histories'); + } +}; diff --git a/database/migrations/2026_06_23_090018_create_submission_correction_requests_table.php b/database/migrations/2026_06_23_090018_create_submission_correction_requests_table.php new file mode 100644 index 0000000..6994350 --- /dev/null +++ b/database/migrations/2026_06_23_090018_create_submission_correction_requests_table.php @@ -0,0 +1,36 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->foreignId('checklist_item_id')->nullable()->constrained('checklist_items')->nullOnDelete(); + $table->string('page_reference')->nullable()->comment('No muka surat yang perlu dibetulkan'); + $table->string('location')->nullable()->comment('Lokasi / seksyen / item'); + $table->text('description')->comment('Penerangan apa yang perlu dibetulkan'); + // terbuka, dijawab, selesai + $table->string('status')->default('terbuka'); + $table->text('consultant_response')->nullable(); + $table->timestamp('responded_at')->nullable(); + $table->foreignId('responded_by')->nullable()->constrained('users')->nullOnDelete(); + $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamp('resolved_at')->nullable(); + $table->foreignId('resolved_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + + $table->index(['submission_id', 'status']); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_correction_requests'); + } +}; diff --git a/database/migrations/2026_06_23_090019_create_submission_comments_table.php b/database/migrations/2026_06_23_090019_create_submission_comments_table.php new file mode 100644 index 0000000..dc3f043 --- /dev/null +++ b/database/migrations/2026_06_23_090019_create_submission_comments_table.php @@ -0,0 +1,31 @@ +id(); + $table->foreignId('submission_id')->constrained('submissions')->cascadeOnDelete(); + $table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete(); + $table->text('body'); + $table->boolean('is_internal')->default(false)->comment('Mesej dalaman JPP sahaja / luaran kepada perunding'); + $table->string('attachment_path')->nullable(); + $table->string('attachment_name')->nullable(); + $table->foreignId('deleted_by')->nullable()->constrained('users')->nullOnDelete(); + $table->timestamps(); + $table->softDeletes(); + + $table->index(['submission_id', 'created_at']); + }); + } + + public function down(): void + { + Schema::dropIfExists('submission_comments'); + } +}; diff --git a/database/migrations/2026_06_23_090020_create_app_settings_table.php b/database/migrations/2026_06_23_090020_create_app_settings_table.php new file mode 100644 index 0000000..2d9b9a0 --- /dev/null +++ b/database/migrations/2026_06_23_090020_create_app_settings_table.php @@ -0,0 +1,27 @@ +id(); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->string('type')->default('string'); // string, integer, boolean, date + $table->string('group')->default('umum'); + $table->string('label')->nullable(); + $table->text('description')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('app_settings'); + } +}; diff --git a/database/seeders/AppSettingsSeeder.php b/database/seeders/AppSettingsSeeder.php new file mode 100644 index 0000000..aa346a9 --- /dev/null +++ b/database/seeders/AppSettingsSeeder.php @@ -0,0 +1,51 @@ + 'default_cut_off_day', + 'value' => '1', + 'type' => 'integer', + 'group' => 'kitaran', + 'label' => 'Hari tarikh tutup lalai', + 'description' => 'Hari bulan tarikh tutup serahan lalai (cth: 1).', + ], + [ + 'key' => 'default_cut_off_month', + 'value' => '12', + 'type' => 'integer', + 'group' => 'kitaran', + 'label' => 'Bulan tarikh tutup lalai', + 'description' => 'Bulan tarikh tutup serahan lalai (cth: 12 = Disember). Pembaharuan lesen pada/sebelum 1 Disember.', + ], + [ + 'key' => 'reminder_email_enabled', + 'value' => '0', + 'type' => 'boolean', + 'group' => 'peringatan', + 'label' => 'Hantar peringatan emel', + 'description' => 'Aktifkan penghantaran peringatan emel (perlu konfigurasi SMTP).', + ], + [ + 'key' => 'max_upload_size_kb', + 'value' => '20480', + 'type' => 'integer', + 'group' => 'dokumen', + 'label' => 'Saiz maksimum muat naik (KB)', + 'description' => 'Had saiz fail PDF yang boleh dimuat naik dalam KB.', + ], + ]; + + foreach ($settings as $s) { + AppSetting::firstOrCreate(['key' => $s['key']], $s); + } + } +} diff --git a/database/seeders/ChecklistTemplateSeeder.php b/database/seeders/ChecklistTemplateSeeder.php new file mode 100644 index 0000000..3d3402f --- /dev/null +++ b/database/seeders/ChecklistTemplateSeeder.php @@ -0,0 +1,207 @@ + 'Templat Senarai Semak Kemampanan Pusat Data MBIP'], + ['version' => '1.0', 'is_active' => true, 'description' => 'Templat asas pelaporan jejak karbon pusat data MBIP.'] + ); + + // Elak pendua jika seeder dijalankan semula. + if ($template->sections()->exists()) { + return; + } + + $tCO2e = 'tCO2e'; + + // ============================================================ + // SEKSYEN A — Anggaran Jumlah Pelepasan Karbon + // ============================================================ + $secA = $this->section($template, 'A', 'Anggaran Jumlah Pelepasan Karbon Oleh Pusat Data', 1, true, + 'Jumlah Anggaran GHG Inventori Pusat Data sebagai asas / Baseline Jejak Karbon Tahun.'); + + $skop1 = $this->scope($secA, 'SKOP1', 'Skop 1 — Pelepasan Langsung', 1, + 'Pembakaran bahan api fosil ketika operasi (arang batu, LPG, dll); penggunaan bahan api kenderaan (petrol); pelepasan fugitive daripada penyaman udara/HVAC/chiller, SF6 dalam GIS dan sistem pemadaman kebakaran.'); + $this->item($secA, 'A.1.i', 'SKOP1', 'Jumlah pelepasan Skop 1', 'number', $tCO2e, 1, true, $skop1->id, + 'Jumlah keseluruhan pelepasan langsung (Skop 1) dalam tCO2e.'); + + $skop2 = $this->scope($secA, 'SKOP2', 'Skop 2 — Tenaga Dibeli', 2, + 'Pelepasan daripada jumlah penggunaan elektrik dalam bangunan & kemudahan pusat data (mengambil kira operasi dan kapasiti penjanaan elektrik komponen IT); penggunaan kenderaan elektrik ketika operasi.'); + $this->item($secA, 'A.1.ii', 'SKOP2', 'Jumlah pelepasan Skop 2', 'number', $tCO2e, 2, true, $skop2->id, + 'Jumlah keseluruhan pelepasan tenaga dibeli (Skop 2) dalam tCO2e.'); + + $skop3 = $this->scope($secA, 'SKOP3', 'Skop 3 — Pelepasan Tidak Langsung Lain', 3, + 'Sisa pepejal dilupuskan di luar kawasan; pembakaran bahan api kenderaan pekerja pergi/balik kerja; jumlah karbon daripada bahan binaan / embodied carbon.'); + $this->item($secA, 'A.1.iii', 'SKOP3', 'Jumlah pelepasan Skop 3', 'number', $tCO2e, 3, true, $skop3->id, + 'Jumlah keseluruhan pelepasan tidak langsung lain (Skop 3) dalam tCO2e.'); + + ChecklistFormula::create([ + 'checklist_template_id' => $template->id, + 'checklist_section_id' => $secA->id, + 'result_token' => 'A', + 'label' => 'A — Jumlah Pelepasan Karbon (Skop 1 + Skop 2 + Skop 3)', + 'expression' => 'SKOP1 + SKOP2 + SKOP3', + 'unit' => $tCO2e, + 'order' => 1, + ]); + + // ============================================================ + // SEKSYEN B — Mekanisme Pengurangan Karbon + // ============================================================ + $secB = $this->section($template, 'B', 'Mekanisme Pengurangan Karbon', 2, true); + $this->item($secB, '2(i)', 'EE', 'Jumlah karbon penggunaan kecekapan tenaga / EE', 'number', $tCO2e, 1, true, null, + 'Contoh: LED, inverter penghawa dingin, audit tenaga, dll.'); + $this->item($secB, '2(ii)', 'RE', 'Jumlah karbon penggunaan tenaga boleh diperbaharui / RE', 'number', $tCO2e, 2, true, null, + 'Contoh: solar, hidro, biomas, dll.'); + + ChecklistFormula::create([ + 'checklist_template_id' => $template->id, + 'checklist_section_id' => $secB->id, + 'result_token' => 'B', + 'label' => 'B — Jumlah Pengurangan Karbon (2(i) + 2(ii))', + 'expression' => 'EE + RE', + 'unit' => $tCO2e, + 'order' => 2, + ]); + + // ============================================================ + // SEKSYEN C — Anggaran Jumlah Baki Karbon + // ============================================================ + $secC = $this->section($template, 'C', 'Anggaran Jumlah Baki Karbon', 3, true, + 'Jumlah anggaran baki GHG / residual emissions.'); + // Item dikira automatik (tiada input perunding). + $this->item($secC, '3', 'C_RES', 'Jumlah anggaran baki GHG / residual emissions', 'calculated', $tCO2e, 1, false, null, + 'Dikira automatik: C = A - B.', true, 'A - B'); + + ChecklistFormula::create([ + 'checklist_template_id' => $template->id, + 'checklist_section_id' => $secC->id, + 'result_token' => 'C', + 'label' => 'C — Baki Karbon (A - B)', + 'expression' => 'A - B', + 'unit' => $tCO2e, + 'order' => 3, + ]); + + // ============================================================ + // SEKSYEN D — Anggaran Jumlah Bersih Karbon + // ============================================================ + $secD = $this->section($template, 'D', 'Anggaran Jumlah Bersih Karbon', 4, true); + $this->item($secD, '4(i)', 'D4I', 'Jumlah karbon melalui inisiatif (kredit karbon, CSR, mRECs)', 'number', $tCO2e, 1, true, null, + 'Nyatakan tahun inisiatif dalam catatan perunding.'); + $this->item($secD, '4(ii)', 'D4II', 'Jumlah anggaran pengurangan GHG melalui mekanisme dalam Seksyen B', 'number', $tCO2e, 2, true, null); + + ChecklistFormula::create([ + 'checklist_template_id' => $template->id, + 'checklist_section_id' => $secD->id, + 'result_token' => 'D', + 'label' => 'D — Jumlah Bersih Karbon (A - 4(i) - 4(ii))', + 'expression' => 'A - D4I - D4II', + 'unit' => $tCO2e, + 'order' => 4, + ]); + + // ============================================================ + // SEKSYEN E — Peratus Pengurangan GHG + // ============================================================ + $secE = $this->section($template, 'E', 'Peratus Pengurangan GHG Pusat Data', 5, true, + 'E = D / A x 100. Termasuk perlindungan pembahagian dengan sifar.'); + $this->item($secE, '5', 'E_PCT', 'Peratus pengurangan GHG', 'calculated', '%', 1, false, null, + 'Dikira automatik: E = D / A x 100.', true, 'D / A * 100'); + + ChecklistFormula::create([ + 'checklist_template_id' => $template->id, + 'checklist_section_id' => $secE->id, + 'result_token' => 'E', + 'label' => 'E — Peratus Pengurangan GHG (D / A x 100)', + 'expression' => 'D / A * 100', + 'unit' => '%', + 'guard_div_zero' => true, + 'order' => 5, + ]); + + // ============================================================ + // SEKSYEN LAIN-LAIN — Tiada formula + // ============================================================ + $secLain = $this->section($template, 'LAIN', 'Lain-lain Perkara', 6, false); + $this->item($secLain, 'L.1', null, 'Penyertaan sijil bangunan hijau & rating diterima', 'select', null, 1, false, null, + 'Pilih sijil bangunan hijau jika ada.', false, null, + ['GBI', 'GreenRE', 'LEED', 'Greenmark', 'Lain-lain', 'Tiada']); + $this->item($secLain, 'L.2', null, 'Sasaran pengurangan GHG pusat data secara keseluruhan (jika ada)', 'textarea', null, 2, false, null); + $this->item($secLain, 'L.3', null, 'Jumlah pelepasan karbon daripada Business as Usual / BAU untuk 2030', 'number', $tCO2e, 3, false, null); + $this->item($secLain, 'L.4', null, 'Jumlah pengurangan karbon melalui Counter Measure / CM untuk 2030', 'number', $tCO2e, 4, false, null); + } + + protected function section(ChecklistTemplate $t, string $code, string $title, int $order, bool $formula, ?string $desc = null): ChecklistSection + { + return ChecklistSection::create([ + 'checklist_template_id' => $t->id, + 'code' => $code, + 'title' => $title, + 'description' => $desc, + 'order' => $order, + 'is_formula_enabled' => $formula, + ]); + } + + protected function scope(ChecklistSection $s, string $code, string $title, int $order, ?string $desc = null): ChecklistScope + { + return ChecklistScope::create([ + 'checklist_section_id' => $s->id, + 'code' => $code, + 'title' => $title, + 'description' => $desc, + 'order' => $order, + ]); + } + + protected function item( + ChecklistSection $s, + string $code, + ?string $token, + string $label, + string $inputType, + ?string $unit, + int $order, + bool $required, + ?int $scopeId = null, + ?string $desc = null, + bool $calculated = false, + ?string $formula = null, + ?array $options = null, + ): ChecklistItem { + return ChecklistItem::create([ + 'checklist_section_id' => $s->id, + 'checklist_scope_id' => $scopeId, + 'code' => $code, + 'formula_token' => $token, + 'label' => $label, + 'description' => $desc, + 'input_type' => $inputType, + 'options' => $options, + 'unit' => $unit, + 'is_required' => $required, + 'is_calculated' => $calculated, + 'formula' => $formula, + 'order' => $order, + ]); + } +} diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..cc256c1 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,19 @@ +call([ + RolesAndPermissionsSeeder::class, + UserSeeder::class, + AppSettingsSeeder::class, + ChecklistTemplateSeeder::class, + DemoDataSeeder::class, + ]); + } +} diff --git a/database/seeders/DemoDataSeeder.php b/database/seeders/DemoDataSeeder.php new file mode 100644 index 0000000..42e00a9 --- /dev/null +++ b/database/seeders/DemoDataSeeder.php @@ -0,0 +1,73 @@ +first(); + $officer = User::where('email', 'pegawai@mbip.gov.my')->first(); + $template = ChecklistTemplate::activeTemplate(); + + if (! $consultant) { + return; + } + + // Pusat data contoh. + $dataCentres = [ + ['nama_pusat_data' => 'Pusat Data Iskandar 1', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Hyperscale Iskandar', 'lokasi_pusat_data' => 'Nusajaya', 'it_load' => 50.00, 'keluasan_tapak' => 12.50], + ['nama_pusat_data' => 'Pusat Data Tebrau Tech Park', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Tebrau', 'lokasi_pusat_data' => 'Tebrau', 'it_load' => 30.00, 'keluasan_tapak' => 8.20], + ['nama_pusat_data' => 'Pusat Data Pasir Gudang', 'tajuk_permohonan' => 'Permohonan Lesen Pusat Data Industri', 'lokasi_pusat_data' => 'Pasir Gudang', 'it_load' => 20.00, 'keluasan_tapak' => 5.00], + ]; + + foreach ($dataCentres as $data) { + $dc = DataCentre::firstOrCreate( + ['nama_pusat_data' => $data['nama_pusat_data']], + array_merge($data, [ + 'alamat' => $data['lokasi_pusat_data'].', Johor', + 'status' => 'aktif', + ]) + ); + + // Tugaskan perunding aktif jika belum ada. + if (! $dc->current_consultant_id) { + $dc->update(['current_consultant_id' => $consultant->id]); + DataCentreConsultantAssignment::firstOrCreate( + ['data_centre_id' => $dc->id, 'consultant_id' => $consultant->id, 'is_active' => true], + [ + 'start_date' => Carbon::create(2026, 1, 1), + 'reason' => 'Penugasan awal perunding.', + 'assigned_by' => $officer?->id, + ] + ); + } + } + + // Kitaran pelaporan contoh: pembaharuan 2028 => pelaporan 2026. + foreach ([2027, 2028] as $renewalYear) { + $reportingYear = ReportingCycle::reportingYearFor($renewalYear); + ReportingCycle::firstOrCreate( + ['renewal_year' => $renewalYear], + [ + 'reporting_year' => $reportingYear, + 'period_start' => Carbon::create($reportingYear, 1, 1), + 'period_end' => Carbon::create($reportingYear, 12, 31), + 'cut_off_date' => Carbon::create($renewalYear - 1, 12, 1), + 'licence_period_year' => 1, + 'checklist_template_id' => $template?->id, + 'status' => $renewalYear === 2028 ? 'aktif' : 'tutup', + ] + ); + } + } +} diff --git a/database/seeders/RolesAndPermissionsSeeder.php b/database/seeders/RolesAndPermissionsSeeder.php new file mode 100644 index 0000000..0d5abb3 --- /dev/null +++ b/database/seeders/RolesAndPermissionsSeeder.php @@ -0,0 +1,80 @@ + label Bahasa Melayu. */ + public const PERMISSIONS = [ + 'pengguna.urus' => 'Urus pengguna sistem', + 'perunding.urus' => 'Urus perunding', + 'pusat_data.urus' => 'Urus pusat data', + 'tugasan.urus' => 'Urus penugasan perunding', + 'kitaran.urus' => 'Urus kitaran pelaporan/pembaharuan', + 'templat.urus' => 'Urus templat senarai semak', + 'serahan.lihat_semua' => 'Lihat semua serahan', + 'serahan.semak' => 'Semak serahan (status, ulasan, pembetulan, penugasan pegawai)', + 'serahan.kunci' => 'Kunci/buka kunci serahan', + 'serahan.hardcopy' => 'Tanda penerimaan dokumen salinan keras', + 'serahan.urus_sendiri' => 'Urus serahan sendiri (perunding)', + 'laporan.lihat' => 'Lihat laporan & statistik', + 'peringatan.lihat' => 'Lihat senarai peringatan/belum serah', + 'komen.cipta' => 'Hantar komen/perbualan', + 'komen.padam' => 'Padam (soft delete) komen — pentadbir sahaja', + 'tetapan.urus' => 'Urus tetapan aplikasi', + ]; + + public const ROLES = [ + 'Super Admin' => '*', + 'JPP Admin' => [ + 'pengguna.urus', 'perunding.urus', 'pusat_data.urus', 'tugasan.urus', + 'kitaran.urus', 'templat.urus', 'serahan.lihat_semua', 'serahan.semak', + 'serahan.kunci', 'serahan.hardcopy', 'laporan.lihat', 'peringatan.lihat', + 'komen.cipta', 'komen.padam', 'tetapan.urus', + ], + 'Pegawai JPP' => [ + 'perunding.urus', 'pusat_data.urus', 'tugasan.urus', 'kitaran.urus', + 'serahan.lihat_semua', 'serahan.semak', 'serahan.kunci', 'serahan.hardcopy', + 'laporan.lihat', 'peringatan.lihat', 'komen.cipta', + ], + 'Penolong Pegawai' => [ + 'pusat_data.urus', 'serahan.lihat_semua', 'serahan.semak', 'serahan.hardcopy', + 'laporan.lihat', 'peringatan.lihat', 'komen.cipta', + ], + 'Kerani' => [ + 'serahan.lihat_semua', 'serahan.hardcopy', 'peringatan.lihat', 'komen.cipta', + ], + 'Perunding' => [ + 'serahan.urus_sendiri', 'komen.cipta', + ], + ]; + + public function run(): void + { + app(PermissionRegistrar::class)->forgetCachedPermissions(); + + $guard = 'web'; + + foreach (self::PERMISSIONS as $name => $label) { + Permission::firstOrCreate(['name' => $name, 'guard_name' => $guard]); + } + + foreach (self::ROLES as $roleName => $perms) { + $role = Role::firstOrCreate(['name' => $roleName, 'guard_name' => $guard]); + + if ($perms === '*') { + $role->syncPermissions(Permission::where('guard_name', $guard)->get()); + } else { + $role->syncPermissions($perms); + } + } + + app(PermissionRegistrar::class)->forgetCachedPermissions(); + } +} diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php new file mode 100644 index 0000000..ae46550 --- /dev/null +++ b/database/seeders/UserSeeder.php @@ -0,0 +1,61 @@ + 'Super Admin', 'email' => 'superadmin@mbip.gov.my', 'role' => 'Super Admin', 'jawatan' => 'Pentadbir Sistem'], + ['name' => 'JPP Admin', 'email' => 'jppadmin@mbip.gov.my', 'role' => 'JPP Admin', 'jawatan' => 'Pentadbir JPP'], + ['name' => 'Pegawai JPP', 'email' => 'pegawai@mbip.gov.my', 'role' => 'Pegawai JPP', 'jawatan' => 'Pegawai Perancang Bandar'], + ['name' => 'Penolong Pegawai JPP', 'email' => 'penolong@mbip.gov.my', 'role' => 'Penolong Pegawai', 'jawatan' => 'Penolong Pegawai Perancang Bandar'], + ['name' => 'Kerani JPP', 'email' => 'kerani@mbip.gov.my', 'role' => 'Kerani', 'jawatan' => 'Pembantu Tadbir'], + ]; + + foreach ($users as $data) { + $user = User::firstOrCreate( + ['email' => $data['email']], + [ + 'name' => $data['name'], + 'password' => Hash::make('password'), + 'jawatan' => $data['jawatan'], + 'is_active' => true, + 'email_verified_at' => now(), + ] + ); + $user->syncRoles([$data['role']]); + } + + // Akaun perunding contoh + profil perunding berkait. + $perundingUser = User::firstOrCreate( + ['email' => 'perunding@example.com'], + [ + 'name' => 'Perunding Contoh Sdn Bhd', + 'password' => Hash::make('password'), + 'jawatan' => 'Perunding Alam Sekitar', + 'is_active' => true, + 'email_verified_at' => now(), + ] + ); + $perundingUser->syncRoles(['Perunding']); + + Consultant::firstOrCreate( + ['emel' => 'perunding@example.com'], + [ + 'user_id' => $perundingUser->id, + 'nama_perunding' => 'Perunding Contoh Sdn Bhd', + 'no_pendaftaran_syarikat' => '202301000123 (1234567-A)', + 'alamat' => 'No. 1, Jalan Hijau, 81200 Johor Bahru, Johor', + 'telefon' => '07-1234567', + 'aktif' => true, + ] + ); + } +} diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..b9f0f96 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,111 @@ +#!/bin/bash +# ───────────────────────────────────────────────────────────── +# deploy.sh — Skrip deploy produksi (Ubuntu) +# Sistem Pemantauan Kemampanan Pusat Data MBIP +# +# Penggunaan: +# ./deploy.sh # deploy / kemas kini (pull, build, up, migrate) +# ./deploy.sh --build # paksa bina semula imej Docker +# ./deploy.sh --fresh # migrate:fresh --seed (AMARAN: kosongkan data) +# ./deploy.sh --logs # ikut log container +# ./deploy.sh --down # hentikan container +# ───────────────────────────────────────────────────────────── +set -euo pipefail + +cd "$(dirname "$0")" + +# Guna `docker compose` (v2) atau `docker-compose` (v1) — auto kesan. +if docker compose version >/dev/null 2>&1; then + DC="docker compose" +else + DC="docker-compose" +fi + +# Hanya fail produksi (abaikan docker-compose.override.yml untuk dev). +COMPOSE_FILES="-f docker-compose.yml" + +log() { echo -e "\033[0;32m==>\033[0m $*"; } +warn() { echo -e "\033[0;33m[!]\033[0m $*"; } +die() { echo -e "\033[0;31m[x]\033[0m $*" >&2; exit 1; } + +# ── Semakan prasyarat ──────────────────────────────────────── +command -v docker >/dev/null 2>&1 || die "Docker tidak dipasang." + +if [ ! -f .env ]; then + warn ".env tidak dijumpai — menyalin daripada .env.docker" + cp .env.docker .env + die "Sila edit .env (APP_KEY, DB_USERNAME, DB_PASSWORD, APP_URL) kemudian jalankan semula ./deploy.sh" +fi + +# ── Kendalikan flag ────────────────────────────────────────── +case "${1:-}" in + --logs) exec $DC $COMPOSE_FILES logs -f --tail=100 ;; + --down) log "Menghentikan container..."; $DC $COMPOSE_FILES down; exit 0 ;; +esac + +FORCE_BUILD=false +FRESH=false +for arg in "$@"; do + case "$arg" in + --build) FORCE_BUILD=true ;; + --fresh) FRESH=true ;; + esac +done + +# ── 1. Tarik kod terkini (jika repo git) ───────────────────── +if [ -d .git ]; then + log "Menarik kod terkini (git pull)..." + git pull --ff-only || warn "git pull gagal / dilangkau." +fi + +# ── 2. Bina aset frontend pada host (Vite) ─────────────────── +# Nota: nginx melekap ./public (read-only) terus daripada host, +# jadi aset dibina di sini, bukan dalam imej. +if command -v npm >/dev/null 2>&1; then + log "Membina aset frontend (npm ci && npm run build)..." + npm ci --silent || npm install --silent + npm run build +else + warn "npm tidak dijumpai pada host — pastikan public/build sudah dibina." +fi + +# ── 3. Bina / mulakan container ────────────────────────────── +if [ "$FORCE_BUILD" = true ]; then + log "Membina semula imej Docker..." + $DC $COMPOSE_FILES build --pull +fi + +log "Memulakan container..." +$DC $COMPOSE_FILES up -d --remove-orphans + +# ── 4. Tunggu container app sihat ──────────────────────────── +log "Menunggu container app sihat..." +for i in $(seq 1 30); do + if $DC $COMPOSE_FILES exec -T app php-fpm -t >/dev/null 2>&1; then + break + fi + sleep 2 +done + +# ── 5. Migrasi & optimisasi Laravel ────────────────────────── +if [ "$FRESH" = true ]; then + warn "Menjalankan migrate:fresh --seed (semua data akan dikosongkan)..." + $DC $COMPOSE_FILES exec -T --user www-data app php artisan migrate:fresh --seed --force --no-interaction +else + log "Menjalankan migrasi..." + $DC $COMPOSE_FILES exec -T --user www-data app php artisan migrate --force --no-interaction +fi + +log "Memastikan storage symlink..." +$DC $COMPOSE_FILES exec -T --user www-data app php artisan storage:link --force || true + +log "Membina semula cache Laravel..." +$DC $COMPOSE_FILES exec -T --user www-data app php artisan optimize:clear +$DC $COMPOSE_FILES exec -T --user www-data app php artisan optimize + +# ── 6. Status ──────────────────────────────────────────────── +log "Selesai!" +$DC $COMPOSE_FILES ps + +PORT=$(grep -E '^NGINX_HTTP_PORT=' .env | cut -d= -f2 || echo 8010) +log "Aplikasi tersedia pada port ${PORT:-8010} (cth: http://:${PORT:-8010})" diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..f6bbd11 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,10 @@ +# Override pembangunan tempatan — automatik dimuatkan oleh `docker compose`. +# Di server PRODUCTION, jalankan dengan fail ini diabaikan: +# docker compose -f docker-compose.yml up -d +services: + app: + environment: + APP_ENV: local + APP_DEBUG: "true" + volumes: + - ./docker/php/opcache-dev.ini:/usr/local/etc/php/conf.d/zzz-opcache-dev.ini:ro diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..28c38db --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,76 @@ +name: mbip_datacenter + +services: + + # ── PHP 8.4 FPM Application ────────────────────────────────── + app: + build: + context: . + dockerfile: Dockerfile + container_name: mbip_dc_app + restart: unless-stopped + env_file: .env + environment: + DB_HOST: ${DB_HOST:-host.docker.internal} + DB_PORT: ${DB_PORT:-3306} + DB_DATABASE: ${DB_DATABASE:-mbip_datacenter} + DB_USERNAME: ${DB_USERNAME} + DB_PASSWORD: ${DB_PASSWORD} + volumes: + - .:/var/www/html + - /var/www/html/vendor # kekal vendor dari image + - /var/www/html/bootstrap/cache # elak host cache (dev) overwrite + - mbip_dc_storage:/var/www/html/storage/app + - mbip_dc_logs:/var/www/html/storage/logs + networks: + - mbip_dc_internal + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "php-fpm", "-t"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # ── Nginx Web Server ───────────────────────────────────────── + nginx: + image: nginx:1.27-alpine + container_name: mbip_dc_nginx + restart: unless-stopped + ports: + - "${NGINX_HTTP_PORT:-8010}:80" + volumes: + - ./public:/var/www/html/public:ro + - mbip_dc_storage:/var/www/html/storage/app:ro + - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - mbip_dc_nginx_logs:/var/log/nginx + networks: + - mbip_dc_internal + depends_on: + app: + condition: service_healthy + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +# ── Named Volumes ───────────────────────────────────────────── +volumes: + mbip_dc_storage: + driver: local + mbip_dc_logs: + driver: local + mbip_dc_nginx_logs: + driver: local + +# ── Networks ────────────────────────────────────────────────── +networks: + mbip_dc_internal: + driver: bridge + ipam: + config: + - subnet: 172.29.0.0/16 diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..74ebb9c --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,95 @@ +#!/bin/bash +set -e + +# ───────────────────────────────────────────────────────────── +# MBIP Pusat Data — Container Entrypoint +# Dijalankan setiap kali container bermula sebelum PHP-FPM boot +# ───────────────────────────────────────────────────────────── + +DB_HOST="${DB_HOST:-host.docker.internal}" +DB_PORT="${DB_PORT:-3306}" +DB_USER="${DB_USERNAME:-root}" +DB_PASS="${DB_PASSWORD}" +TIMEOUT=60 + +echo "[entrypoint] Memulakan MBIP Pusat Data — PHP $(php -r 'echo PHP_VERSION;')" +echo "[entrypoint] MySQL luaran: ${DB_HOST}:${DB_PORT}" + +# ── 1. Tunggu MySQL luaran sedia ───────────────────────────── +echo "[entrypoint] Menunggu MySQL..." +elapsed=0 +until mysqladmin ping -h "${DB_HOST}" -P "${DB_PORT}" -u "${DB_USER}" -p"${DB_PASS}" \ + --connect-timeout=3 --silent 2>/dev/null; do + if [ "$elapsed" -ge "$TIMEOUT" ]; then + echo "[entrypoint] RALAT: MySQL di ${DB_HOST}:${DB_PORT} tidak bertindak balas dalam ${TIMEOUT}s. Dibatalkan." + exit 1 + fi + echo "[entrypoint] MySQL belum sedia — cuba lagi dalam 3s... (${elapsed}s berlalu)" + sleep 3 + elapsed=$((elapsed + 3)) +done +echo "[entrypoint] MySQL sedia." + +# ── 2. Pasang composer dependencies jika vendor tiada ──────── +if [ ! -f /var/www/html/vendor/autoload.php ]; then + echo "[entrypoint] vendor/ tidak dijumpai — menjalankan composer install..." + composer install --no-dev --no-interaction --no-progress --optimize-autoloader --no-scripts \ + --working-dir=/var/www/html +fi + +# ── 3. Pastikan direktori storage wujud ────────────────────── +mkdir -p \ + /var/www/html/storage/app/public \ + /var/www/html/storage/app/private/reports \ + /var/www/html/storage/framework/cache/data \ + /var/www/html/storage/framework/sessions \ + /var/www/html/storage/framework/testing \ + /var/www/html/storage/framework/views \ + /var/www/html/storage/logs \ + /var/log/php-fpm \ + /var/log/supervisor + +chown -R www-data:www-data /var/www/html/storage +chmod -R 775 /var/www/html/storage + +# ── 4. Bersihkan bootstrap cache lapuk ─────────────────────── +rm -f /var/www/html/bootstrap/cache/*.php +mkdir -p /var/www/html/bootstrap/cache +chown -R www-data:www-data /var/www/html/bootstrap/cache + +# ── 5. Jana APP_KEY jika belum ditetapkan ──────────────────── +if [ -z "${APP_KEY}" ]; then + echo "[entrypoint] APP_KEY belum ditetapkan — menjana..." + php artisan key:generate --force +fi + +# ── 6. Cipta storage symlink ───────────────────────────────── +if [ ! -L /var/www/html/public/storage ]; then + echo "[entrypoint] Mencipta storage symlink..." + php artisan storage:link --force +fi + +# ── 7. Jalankan migrasi pangkalan data ─────────────────────── +echo "[entrypoint] Menjalankan migrasi..." +php artisan migrate --force --no-interaction + +# ── 8. Semai data jika kosong (kali pertama sahaja) ────────── +ROLE_COUNT=$(php artisan tinker --execute="echo \Spatie\Permission\Models\Role::count();" 2>/dev/null | tail -1) +if [ "${ROLE_COUNT}" = "0" ] || [ -z "${ROLE_COUNT}" ]; then + echo "[entrypoint] Pangkalan data kosong — menjalankan seeder..." + php artisan db:seed --force --no-interaction +fi + +# ── 9. Bina semula cache ───────────────────────────────────── +echo "[entrypoint] Membina semula cache..." +su -s /bin/sh www-data -c "php artisan package:discover --ansi" +su -s /bin/sh www-data -c "php artisan optimize:clear" +su -s /bin/sh www-data -c "php artisan config:cache" +su -s /bin/sh www-data -c "php artisan route:cache" +su -s /bin/sh www-data -c "php artisan view:cache" +su -s /bin/sh www-data -c "php artisan event:cache" + +echo "[entrypoint] Persediaan selesai. Memulakan PHP-FPM..." + +# ── 10. Mulakan PHP-FPM + queue worker melalui Supervisor ──── +exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..79d1f03 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,78 @@ +server { + listen 80; + listen [::]:80; + server_name _; + + root /var/www/html/public; + index index.php; + + charset utf-8; + + # ── Laravel health check ────────────────────────────────── + location = /up { + access_log off; + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + } + + # ── Aset statik — cache jangka panjang ─────────────────── + location ~* \.(css|js|ico|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|webp)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + try_files $uri =404; + } + + # ── Fail awam (storage/app/public) ──────────────────────── + # Nota: laporan PDF disimpan di storage/app/private dan HANYA + # boleh dimuat turun melalui laluan Laravel terlindung polisi. + location /storage/ { + alias /var/www/html/storage/app/public/; + expires 30d; + add_header Cache-Control "public"; + access_log off; + try_files $uri =404; + } + + # ── Semua permintaan lain → Laravel ────────────────────── + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + # ── PHP-FPM ─────────────────────────────────────────────── + location ~ \.php$ { + try_files $uri =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass app:9000; + fastcgi_index index.php; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + fastcgi_param PHP_VALUE "upload_max_filesize=25M \n post_max_size=30M"; + include fastcgi_params; + + fastcgi_connect_timeout 60s; + fastcgi_send_timeout 300s; + fastcgi_read_timeout 300s; + + fastcgi_buffers 16 16k; + fastcgi_buffer_size 32k; + } + + # ── Sekat fail tersembunyi/dot ──────────────────────────── + location ~ /\.(?!well-known).* { + deny all; + access_log off; + log_not_found off; + } + + # ── Sekat fail sensitif ─────────────────────────────────── + location ~* \.(env|log|git|gitignore|md|lock|json|yaml|yml)$ { + deny all; + access_log off; + log_not_found off; + } + + error_page 404 /index.php; + error_page 500 502 503 504 /index.php; +} diff --git a/docker/nginx/nginx.conf b/docker/nginx/nginx.conf new file mode 100644 index 0000000..7968c70 --- /dev/null +++ b/docker/nginx/nginx.conf @@ -0,0 +1,48 @@ +user nginx; +worker_processes auto; + +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; + use epoll; + multi_accept on; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + server_tokens off; + + client_max_body_size 30M; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + image/svg+xml + font/woff + font/woff2; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/docker/php/opcache-dev.ini b/docker/php/opcache-dev.ini new file mode 100644 index 0000000..c35bd78 --- /dev/null +++ b/docker/php/opcache-dev.ini @@ -0,0 +1,2 @@ +opcache.enable = 0 +opcache.enable_cli = 0 diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..7353dad --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,61 @@ +; ───────────────────────────────────────────────────────────── +; MBIP Pusat Data — PHP 8.4 Custom Configuration +; Digunakan untuk CLI dan FPM +; ───────────────────────────────────────────────────────────── + +[PHP] +; ── Zon masa ───────────────────────────────────────────────── +date.timezone = Asia/Kuala_Lumpur + +; ── Memori ─────────────────────────────────────────────────── +memory_limit = 256M +max_execution_time = 300 +max_input_time = 300 +max_input_vars = 5000 +default_socket_timeout = 60 + +; ── Had muat naik (laporan PDF, maks ~20MB) ────────────────── +file_uploads = On +upload_max_filesize = 25M +post_max_size = 30M +max_file_uploads = 10 + +; ── Pelaporan ralat (produksi — log sahaja) ────────────────── +display_errors = Off +display_startup_errors = Off +log_errors = On +error_log = /var/www/html/storage/logs/php_errors.log +error_reporting = E_ALL & ~E_DEPRECATED & ~E_STRICT + +; ── Keselamatan ────────────────────────────────────────────── +expose_php = Off +allow_url_fopen = Off +allow_url_include = Off +session.use_strict_mode = 1 +session.cookie_httponly = 1 +session.cookie_samesite = "Lax" + +; ── Output buffering ───────────────────────────────────────── +output_buffering = 4096 + +; ── OPcache ────────────────────────────────────────────────── +[opcache] +opcache.enable = 1 +opcache.enable_cli = 1 +opcache.memory_consumption = 128 +opcache.interned_strings_buffer = 16 +opcache.max_accelerated_files = 20000 +opcache.validate_timestamps = 1 +opcache.revalidate_freq = 0 +opcache.save_comments = 1 +opcache.fast_shutdown = 1 +opcache.jit_buffer_size = 64M +opcache.jit = tracing + +; ── Realpath cache ─────────────────────────────────────────── +realpath_cache_size = 4096K +realpath_cache_ttl = 600 + +; ── MySQLi / PDO ───────────────────────────────────────────── +[MySQLi] +mysqli.default_port = 3306 diff --git a/docker/php/www.conf b/docker/php/www.conf new file mode 100644 index 0000000..fe4a2e6 --- /dev/null +++ b/docker/php/www.conf @@ -0,0 +1,44 @@ +; ───────────────────────────────────────────────────────────── +; PHP-FPM Pool Configuration — MBIP Pusat Data Production +; ───────────────────────────────────────────────────────────── + +[www] + +; ── Pemilik proses ─────────────────────────────────────────── +user = www-data +group = www-data + +; ── Dengar pada TCP (Nginx sambung melalui nama servis "app") ─ +listen = 0.0.0.0:9000 + +; ── Pengurus proses ────────────────────────────────────────── +pm = dynamic + +; (RAM - overhead OS) / ~20MB setiap proses PHP +pm.max_children = 20 +pm.start_servers = 5 +pm.min_spare_servers = 5 +pm.max_spare_servers = 10 + +; Kitar semula worker untuk elak memory leak +pm.max_requests = 500 + +; ── Log permintaan lambat ───────────────────────────────────── +slowlog = /var/log/php-fpm/slow.log +request_slowlog_timeout = 10s +request_terminate_timeout = 300s + +; ── Logging ─────────────────────────────────────────────────── +access.log = /var/log/php-fpm/access.log +access.format = "%R - %u %t \"%m %r%Q%q\" %s %f %{mili}d %{kilo}M %C%%" + +catch_workers_output = yes +decorate_workers_output = no + +; ── Keselamatan ────────────────────────────────────────────── +clear_env = no + +; ── Health check ───────────────────────────────────────────── +ping.path = /ping +ping.response = pong +pm.status_path = /fpm-status diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf new file mode 100644 index 0000000..dee9f5b --- /dev/null +++ b/docker/supervisor/supervisord.conf @@ -0,0 +1,52 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid +loglevel=warn + +[program:php-fpm] +command=php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf +environment=TZ="Asia/Kuala_Lumpur" +autostart=true +autorestart=true +priority=5 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stopasgroup=true +killasgroup=true + +[program:queue-worker] +command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 --memory=128 +directory=/var/www/html +user=www-data +environment=TZ="Asia/Kuala_Lumpur" +autostart=true +autorestart=true +priority=10 +startsecs=5 +startretries=3 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +stopasgroup=true +killasgroup=true +stopsignal=SIGTERM +stopwaitsecs=60 + +[program:scheduler] +command=/bin/sh -c "while true; do php /var/www/html/artisan schedule:run --verbose --no-interaction; sleep 60; done" +directory=/var/www/html +user=www-data +environment=TZ="Asia/Kuala_Lumpur" +autostart=true +autorestart=true +priority=15 +startsecs=0 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/lang/ms/validation.php b/lang/ms/validation.php new file mode 100644 index 0000000..a47891e --- /dev/null +++ b/lang/ms/validation.php @@ -0,0 +1,37 @@ + 'Medan :attribute wajib diisi.', + 'required_if' => 'Medan :attribute wajib diisi apabila :other ialah :value.', + 'email' => 'Medan :attribute mestilah alamat emel yang sah.', + 'unique' => 'Nilai :attribute telah digunakan.', + 'confirmed' => 'Pengesahan :attribute tidak sepadan.', + 'numeric' => 'Medan :attribute mestilah nombor.', + 'integer' => 'Medan :attribute mestilah integer.', + 'boolean' => 'Medan :attribute mesti benar atau salah.', + 'date' => 'Medan :attribute bukan tarikh yang sah.', + 'exists' => ':attribute yang dipilih tidak sah.', + 'in' => ':attribute yang dipilih tidak sah.', + 'regex' => 'Format :attribute tidak sah.', + 'min' => [ + 'string' => 'Medan :attribute mestilah sekurang-kurangnya :min aksara.', + 'numeric' => 'Medan :attribute mestilah sekurang-kurangnya :min.', + ], + 'max' => [ + 'string' => 'Medan :attribute tidak boleh melebihi :max aksara.', + 'numeric' => 'Medan :attribute tidak boleh melebihi :max.', + 'file' => 'Saiz fail :attribute tidak boleh melebihi :max kilobait.', + ], + 'mimes' => 'Medan :attribute mestilah fail berjenis: :values.', + 'mimetypes' => 'Medan :attribute mestilah fail berjenis: :values.', + 'file' => 'Medan :attribute mestilah sebuah fail.', + + 'attributes' => [ + 'email' => 'emel', + 'password' => 'kata laluan', + 'name' => 'nama', + ], +]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..596a2ca --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1428 @@ +{ + "name": "datacenter", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "jquery": "^3.7.1" + }, + "devDependencies": { + "laravel-vite-plugin": "^3.1", + "sass": "^1.83.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": { + "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/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "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==", + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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" + ], + "dev": true, + "libc": [ + "musl" + ], + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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" + ], + "dev": true, + "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", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", + "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/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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/bootstrap-icons": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz", + "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT" + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "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/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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/immutable": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.7.tgz", + "integrity": "sha512-47Xb+LFbZ/ZIjQMj6Q5J3IfK7PJFuqRdFOC9FpGgRTK6U2dAEVmkR9hp58qU4FpYux5YXpneDwkj2EP6lppzFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "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.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "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/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.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "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/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==", + "dev": true, + "license": "MIT", + "optional": true + }, + "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": "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "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", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/readdirp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", + "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.133.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.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" + } + }, + "node_modules/sass": { + "version": "1.101.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.101.0.tgz", + "integrity": "sha512-OL3GoQyoUdDt843DpVmDO6y2k1sc5IhUDSpu8XucEI+35neq5QivZ1iuegnpraEVTJXlQGK1gl27zKcTLEPbQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^5.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=20.19.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "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/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "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/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", + "optional": true + }, + "node_modules/vite": { + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" + }, + "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-plugin-full-reload/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" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6740c48 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://www.schemastore.org/package.json", + "private": true, + "type": "module", + "scripts": { + "build": "vite build", + "dev": "vite" + }, + "devDependencies": { + "laravel-vite-plugin": "^3.1", + "sass": "^1.83.0", + "vite": "^8.0.0" + }, + "dependencies": { + "@popperjs/core": "^2.11.8", + "bootstrap": "^5.3.3", + "bootstrap-icons": "^1.11.3", + "jquery": "^3.7.1" + } +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e7f0a48 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,36 @@ + + + + + tests/Unit + + + tests/Feature + + + + + app + + + + + + + + + + + + + + + + + + + diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..b574a59 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,25 @@ + + + Options -MultiViews -Indexes + + + RewriteEngine On + + # Handle Authorization Header + RewriteCond %{HTTP:Authorization} . + RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] + + # Handle X-XSRF-Token Header + RewriteCond %{HTTP:x-xsrf-token} . + RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}] + + # Redirect Trailing Slashes If Not A Folder... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_URI} (.+)/$ + RewriteRule ^ %1 [L,R=301] + + # Send Requests To Front Controller... + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ index.php [L] + diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..e69de29 diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ee8f07e --- /dev/null +++ b/public/index.php @@ -0,0 +1,20 @@ +handleRequest(Request::capture()); diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..eb05362 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/resources/js/app.js b/resources/js/app.js new file mode 100644 index 0000000..76a15f2 --- /dev/null +++ b/resources/js/app.js @@ -0,0 +1,25 @@ +import $ from 'jquery'; +window.$ = window.jQuery = $; + +import * as bootstrap from 'bootstrap'; +window.bootstrap = bootstrap; + +// Aktifkan tooltip & auto-dismiss alert +document.addEventListener('DOMContentLoaded', () => { + document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach((el) => new bootstrap.Tooltip(el)); + + setTimeout(() => { + document.querySelectorAll('.alert-auto-dismiss').forEach((el) => { + bootstrap.Alert.getOrCreateInstance(el).close(); + }); + }, 5000); + + // Pengesahan ringkas sebelum hantar borang berbahaya + document.querySelectorAll('form[data-confirm]').forEach((form) => { + form.addEventListener('submit', (e) => { + if (!window.confirm(form.getAttribute('data-confirm'))) { + e.preventDefault(); + } + }); + }); +}); diff --git a/resources/sass/app.scss b/resources/sass/app.scss new file mode 100644 index 0000000..d250590 --- /dev/null +++ b/resources/sass/app.scss @@ -0,0 +1,76 @@ +// Sistem Pemantauan Kemampanan Pusat Data MBIP — gaya aplikasi +$primary: #0a5c36; // hijau kemampanan +$secondary: #5c6b73; + +@import "bootstrap/scss/bootstrap"; +@import "bootstrap-icons/font/bootstrap-icons.css"; + +body { + background-color: #f4f6f8; + font-size: 0.925rem; +} + +.sidebar { + min-height: 100vh; + background: linear-gradient(180deg, #0a5c36 0%, #074026 100%); +} + +.sidebar .nav-link { + color: rgba(255, 255, 255, 0.85); + border-radius: 0.375rem; + margin-bottom: 2px; + font-weight: 500; +} + +.sidebar .nav-link:hover, +.sidebar .nav-link.active { + background-color: rgba(255, 255, 255, 0.15); + color: #fff; +} + +.sidebar .nav-link i { + width: 1.25rem; + text-align: center; + margin-right: 0.5rem; +} + +.sidebar-brand { + color: #fff; + font-weight: 700; + line-height: 1.2; +} + +.card { + border: none; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08); +} + +.stat-card .stat-value { + font-size: 1.75rem; + font-weight: 700; +} + +.table thead th { + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.03em; + color: #6c757d; +} + +.checklist-section-header { + background-color: #e9f3ee; + border-left: 4px solid $primary; +} + +.chat-message { + border-radius: 0.5rem; + padding: 0.5rem 0.75rem; + margin-bottom: 0.5rem; + background: #fff; + border: 1px solid #e3e7ea; +} + +.chat-message.internal { + background: #fff8e1; + border-color: #ffe082; +} diff --git a/resources/views/audit/index.blade.php b/resources/views/audit/index.blade.php new file mode 100644 index 0000000..434bc87 --- /dev/null +++ b/resources/views/audit/index.blade.php @@ -0,0 +1,38 @@ +@extends('layouts.app') +@section('title', 'Log Audit') +@section('heading', 'Log Audit') +@section('page-title', 'Log Audit / Jejak Aktiviti') + +@section('content') +
+
+
+ +
+
+
+
+ +
+ + + + @forelse ($logs as $log) + + + + + + + + @empty + + @endforelse + +
MasaLogAktivitiOlehSubjek
{{ $log->created_at->format('d/m/Y H:i:s') }}{{ $log->log_name }}{{ $log->description }}{{ $log->causer?->name ?? 'Sistem' }}{{ class_basename($log->subject_type) }} #{{ $log->subject_id }}
Tiada log.
+
+
{{ $logs->links() }}
+@endsection diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 0000000..1eb89ad --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,51 @@ + + + + + + Log Masuk — Pemantauan Kemampanan Pusat Data MBIP + @vite(['resources/sass/app.scss', 'resources/js/app.js']) + + +
+
+
+
+ +
MBIP
+

Sistem Pemantauan Kemampanan Pusat Data

+
+
+
+
Log Masuk
+ + @if ($errors->any()) +
{{ $errors->first() }}
+ @endif + +
+ @csrf +
+ + +
+
+ + +
+
+ + +
+ +
+
+
+

© {{ date('Y') }} Majlis Bandaraya Iskandar Puteri

+
+
+
+ + diff --git a/resources/views/checklist/templates/_formula_modal.blade.php b/resources/views/checklist/templates/_formula_modal.blade.php new file mode 100644 index 0000000..2cc4073 --- /dev/null +++ b/resources/views/checklist/templates/_formula_modal.blade.php @@ -0,0 +1,23 @@ + diff --git a/resources/views/checklist/templates/_item_modal.blade.php b/resources/views/checklist/templates/_item_modal.blade.php new file mode 100644 index 0000000..c082ba3 --- /dev/null +++ b/resources/views/checklist/templates/_item_modal.blade.php @@ -0,0 +1,49 @@ +@php + $isEdit = (bool) $item; + $modalId = $isEdit ? 'editItem'.$item->id : 'addItem'.$section->id; + $action = $isEdit ? route('checklist.items.update', $item) : route('checklist.items.store', $section); + $optsText = $isEdit && is_array($item->options) ? implode("\n", $item->options) : ''; +@endphp + diff --git a/resources/views/checklist/templates/_section_modal.blade.php b/resources/views/checklist/templates/_section_modal.blade.php new file mode 100644 index 0000000..1e1f6a7 --- /dev/null +++ b/resources/views/checklist/templates/_section_modal.blade.php @@ -0,0 +1,17 @@ +{{-- Modal edit seksyen --}} + diff --git a/resources/views/checklist/templates/builder.blade.php b/resources/views/checklist/templates/builder.blade.php new file mode 100644 index 0000000..2d63a27 --- /dev/null +++ b/resources/views/checklist/templates/builder.blade.php @@ -0,0 +1,175 @@ +@extends('layouts.app') +@section('title', 'Pembina Templat') +@section('heading', 'Templat Senarai Semak') +@section('page-title', 'Pembina: '.$template->name) +@section('subtitle', 'Versi '.$template->version.($template->is_active ? ' • Aktif' : '')) + +@section('actions') + + Kembali +@endsection + +@section('content') +@php($inputTypes = App\Models\ChecklistItem::INPUT_TYPES) + +
+
+ {{-- ============ SEKSYEN ============ --}} + @foreach ($template->sections as $section) +
+
+
+ {{ $section->code }} + {{ $section->title }} + @if($section->is_formula_enabled)Formula@endif +
+
+ +
@csrf @method('DELETE')
+
+
+
+ @if($section->description)

{{ $section->description }}

@endif + + {{-- Skop --}} + @if($section->scopes->isNotEmpty()) +
+
Skop
+ @foreach ($section->scopes as $scope) +
+ {{ $scope->code }} {{ $scope->title }} +
@csrf @method('DELETE')
+
+ @endforeach +
+ @endif + + {{-- Borang tambah skop --}} +
+ Tambah skop +
+ @csrf +
+
+
+
+
+
+ + {{-- Item --}} +
Item
+
+ + + + @forelse ($section->items as $item) + + + + + + + + + + + @empty + + @endforelse + +
KodLabelSkopTokenJenisUnitWajib
{{ $item->code }}{{ $item->label }}@if($item->is_calculated)dikira@endif@if($item->scope){{ $item->scope->code }}@else— Tiada —@endif{{ $item->formula_token ?: '—' }}{{ $inputTypes[$item->input_type] ?? $item->input_type }}{{ $item->unit ?: '—' }}{!! $item->is_required ? '' : '—' !!} + +
@csrf @method('DELETE')
+
Tiada item.
+
+ + {{-- Borang tambah item --}} + +
+
+ + {{-- Modal edit seksyen --}} + @include('checklist.templates._section_modal', ['section' => $section, 'mode' => 'edit']) + {{-- Modal tambah item --}} + @include('checklist.templates._item_modal', ['section' => $section, 'item' => null, 'inputTypes' => $inputTypes]) + {{-- Modal edit item --}} + @foreach ($section->items as $item) + @include('checklist.templates._item_modal', ['section' => $section, 'item' => $item, 'inputTypes' => $inputTypes]) + @endforeach + @endforeach + + {{-- Tambah seksyen --}} +
+
+
Tambah Seksyen Baharu
+
+ @csrf +
+
+
+
+
+
+
+
+
+ + {{-- ============ FORMULA ============ --}} +
+
+
Formula Seksyen
+
+

Token tersedia: formula_token setiap item & result_token formula sebelumnya. Cth: SKOP1 + SKOP2 + SKOP3, A - B, D / A * 100.

+ @forelse ($template->formulas as $f) +
+
+ {{ $f->result_token }} = {{ $f->expression }} +
+ +
@csrf @method('DELETE')
+
+
+
{{ $f->label }} @if($f->unit)({{ $f->unit }})@endif @if($f->guard_div_zero)guard ÷0@endif
+
+ @include('checklist.templates._formula_modal', ['template' => $template, 'formula' => $f]) + @empty +

Tiada formula.

+ @endforelse + +
+
Tambah Formula
+
+ @csrf +
+
+
+
+
+
+
+
+ + +
+
+
+
+
+ +{{-- Modal tetapan templat --}} + +@endsection diff --git a/resources/views/checklist/templates/create.blade.php b/resources/views/checklist/templates/create.blade.php new file mode 100644 index 0000000..550415d --- /dev/null +++ b/resources/views/checklist/templates/create.blade.php @@ -0,0 +1,18 @@ +@extends('layouts.app') +@section('title', 'Templat Baharu') +@section('heading', 'Templat Senarai Semak') +@section('page-title', 'Cipta Templat Baharu') +@section('content') +
+
+ @csrf +
+
+
+
+
+
+
Batal
+
+
+@endsection diff --git a/resources/views/checklist/templates/index.blade.php b/resources/views/checklist/templates/index.blade.php new file mode 100644 index 0000000..a67e2ed --- /dev/null +++ b/resources/views/checklist/templates/index.blade.php @@ -0,0 +1,35 @@ +@extends('layouts.app') +@section('title', 'Templat Senarai Semak') +@section('heading', 'Templat Senarai Semak') +@section('page-title', 'Templat Senarai Semak') +@section('subtitle', 'Reka bentuk borang senarai semak secara dinamik — JPP boleh ubah seksyen, item & formula tanpa perubahan kod.') + +@section('actions') + Templat Baharu +@endsection + +@section('content') +
+ + + + @forelse ($templates as $t) + + + + + + + + @empty + + @endforelse + +
Nama TemplatVersiSeksyenStatus
{{ $t->name }}{{ $t->version }}{{ $t->sections_count }}@if($t->is_active)Aktif@elseTidak Aktif@endif + Bina + @unless($t->is_active) +
@csrf
+ @endunless +
Tiada templat.
+
+@endsection diff --git a/resources/views/consultants/_form.blade.php b/resources/views/consultants/_form.blade.php new file mode 100644 index 0000000..104de51 --- /dev/null +++ b/resources/views/consultants/_form.blade.php @@ -0,0 +1,63 @@ +@csrf +
+
+ + + @error('nama_perunding')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +@if (!$consultant->user_id) +
+
+ + +
+
+
+ + + @error('user_name')
{{ $message }}
@enderror +
+
+ + + @error('user_email')
{{ $message }}
@enderror +
+
+ + + @error('user_password')
{{ $message }}
@enderror +
+
+@else +
Akaun log masuk telah dipautkan.
+@endif + +
+ + Batal +
diff --git a/resources/views/consultants/create.blade.php b/resources/views/consultants/create.blade.php new file mode 100644 index 0000000..981f927 --- /dev/null +++ b/resources/views/consultants/create.blade.php @@ -0,0 +1,12 @@ +@extends('layouts.app') +@section('title', 'Daftar Perunding') +@section('heading', 'Perunding') +@section('page-title', 'Daftar Perunding Baharu') + +@section('content') +
+
+ @include('consultants._form') +
+
+@endsection diff --git a/resources/views/consultants/edit.blade.php b/resources/views/consultants/edit.blade.php new file mode 100644 index 0000000..76eaedd --- /dev/null +++ b/resources/views/consultants/edit.blade.php @@ -0,0 +1,13 @@ +@extends('layouts.app') +@section('title', 'Kemaskini Perunding') +@section('heading', 'Perunding') +@section('page-title', 'Kemaskini Perunding') + +@section('content') +
+
+ @method('PUT') + @include('consultants._form') +
+
+@endsection diff --git a/resources/views/consultants/index.blade.php b/resources/views/consultants/index.blade.php new file mode 100644 index 0000000..ed07b42 --- /dev/null +++ b/resources/views/consultants/index.blade.php @@ -0,0 +1,55 @@ +@extends('layouts.app') +@section('title', 'Senarai Perunding') +@section('heading', 'Perunding') +@section('page-title', 'Senarai Perunding') + +@section('actions') + @can('create', App\Models\Consultant::class) + Daftar Perunding + @endcan +@endsection + +@section('content') +
+
+
+
+ +
+
+ +
+
+
+ +
+ + + + @forelse ($consultants as $c) + + + + + + + + + @empty + + @endforelse + +
Nama PerundingNo. PendaftaranEmelPusat DataStatus
{{ $c->nama_perunding }}{{ $c->no_pendaftaran_syarikat ?: '—' }}{{ $c->emel ?: '—' }}{{ $c->data_centres_count }} + {{ $c->aktif ? 'Aktif' : 'Tidak Aktif' }} + + +
Tiada perunding dijumpai.
+
+ {{ $consultants->links() }} +
+
+@endsection diff --git a/resources/views/consultants/show.blade.php b/resources/views/consultants/show.blade.php new file mode 100644 index 0000000..fcab477 --- /dev/null +++ b/resources/views/consultants/show.blade.php @@ -0,0 +1,63 @@ +@extends('layouts.app') +@section('title', 'Maklumat Perunding') +@section('heading', 'Perunding') +@section('page-title', $consultant->nama_perunding) + +@section('actions') + @can('update', $consultant) + Kemaskini + @endcan +@endsection + +@section('content') +
+
+
+
Maklumat Perunding
+
+
Status
{{ $consultant->aktif ? 'Aktif':'Tidak Aktif' }}
+
No. Pendaftaran
{{ $consultant->no_pendaftaran_syarikat ?: '—' }}
+
Emel
{{ $consultant->emel ?: '—' }}
+
Telefon
{{ $consultant->telefon ?: '—' }}
+
Alamat
{{ $consultant->alamat ?: '—' }}
+
Akaun
{{ $consultant->user?->email ?? 'Tiada akaun' }}
+
+
+
+ +
+
Pusat Data Ditugaskan (Aktif)
+
+ + + @forelse ($consultant->dataCentres as $dc) + + @empty + + @endforelse + +
Pusat DataLokasiStatus
{{ $dc->nama_pusat_data }}{{ $dc->lokasi_pusat_data }}{{ $dc->status }}
Tiada pusat data aktif.
+
+ +
Sejarah Penugasan
+
+ + + @forelse ($consultant->assignments as $a) + + + + + + + + + @empty + + @endforelse + +
Pusat DataMulaTamatSebabOlehStatus
{{ $a->dataCentre->nama_pusat_data ?? '—' }}{{ $a->start_date?->format('d/m/Y') }}{{ $a->end_date?->format('d/m/Y') ?: '—' }}{{ $a->reason ?: '—' }}{{ $a->assignedBy->name ?? '—' }}{{ $a->is_active ? 'Aktif':'Tamat' }}
Tiada rekod.
+
+
+
+@endsection diff --git a/resources/views/dashboard/consultant.blade.php b/resources/views/dashboard/consultant.blade.php new file mode 100644 index 0000000..c2d37a8 --- /dev/null +++ b/resources/views/dashboard/consultant.blade.php @@ -0,0 +1,72 @@ +@extends('layouts.app') +@section('title', 'Papan Pemuka Perunding') +@section('heading', 'Papan Pemuka') +@section('page-title', 'Papan Pemuka Perunding') +@section('subtitle', $consultant?->nama_perunding) + +@section('actions') + Mula Serahan Baharu +@endsection + +@section('content') +@unless ($consultant) +
Akaun anda belum dipautkan kepada profil perunding. Sila hubungi JPP.
+@else +
+ @foreach ([ + ['Pusat Data Ditugaskan', $dataCentres->count(), 'hdd-rack', 'primary'], + ['Serahan Tertunggak', $pendingCount, 'hourglass-split', 'warning'], + ['Permohonan Pembetulan', $correctionCount, 'pencil-square', 'danger'], + ['Jumlah Serahan', $submissions->count(), 'file-earmark-text', 'info'], + ] as [$label, $value, $icon, $color]) +
+
+
+
{{ $label }}
{{ $value }}
+ +
+
+
+ @endforeach +
+ +@if ($activeCycle) +
+ +
Kitaran aktif: Pembaharuan {{ $activeCycle->renewal_year }} / Pelaporan {{ $activeCycle->reporting_year }}. + Tarikh tutup serahan: {{ $activeCycle->cut_off_date?->format('d/m/Y') }} + @if ($activeCycle->isOverdue()) Lampau Tempoh @endif +
+
+@endif + +
+
Serahan Saya
+
+
+ + + + @forelse ($submissions as $s) + + + + + + + + @empty + + @endforelse + +
Pusat DataKitaranStatusDikemaskini
{{ $s->dataCentre->nama_pusat_data }}Pembaharuan {{ $s->reportingCycle->renewal_year }} / Pelaporan {{ $s->reportingCycle->reporting_year }}{{ $s->statusLabel() }}{{ $s->updated_at->format('d/m/Y H:i') }} + @if ($s->isEditableByConsultant()) + Isi + @endif + Lihat +
Tiada serahan lagi.
+
+
+
+@endunless +@endsection diff --git a/resources/views/dashboard/jpp.blade.php b/resources/views/dashboard/jpp.blade.php new file mode 100644 index 0000000..8693ad6 --- /dev/null +++ b/resources/views/dashboard/jpp.blade.php @@ -0,0 +1,96 @@ +@extends('layouts.app') +@section('title', 'Papan Pemuka JPP') +@section('heading', 'Papan Pemuka') +@section('page-title', 'Papan Pemuka JPP') +@section('subtitle', $activeCycle ? "Kitaran aktif: Pembaharuan {$activeCycle->renewal_year} / Pelaporan {$activeCycle->reporting_year}" : 'Tiada kitaran aktif') + +@section('content') +
+ @foreach ([ + ['Pusat Data Aktif', $totalDataCentres, 'hdd-rack', 'primary'], + ['Perunding Aktif', $activeConsultants, 'people', 'info'], + ['Telah Serah (kitaran aktif)', $submittedCount, 'check2-circle', 'success'], + ['Belum Serah', $notSubmittedCount, 'hourglass-split', 'warning'], + ['Lampau Tempoh', $overdueCount, 'exclamation-octagon', 'danger'], + ] as [$label, $value, $icon, $color]) +
+
+
+
+
+
{{ $label }}
+
{{ number_format($value) }}
+
+ +
+
+
+
+ @endforeach +
+ +
+
+
+
Pecahan Status Serahan (Kitaran Aktif)
+
+ @forelse (\App\Models\Submission::STATUSES as $key => $label) + @php($count = $statusBreakdown[$key] ?? 0) +
+ {{ $label }} + {{ $count }} +
+ @empty +

Tiada data.

+ @endforelse +
+
+
+ +
+
+
Pusat Data Tertinggi Mengikut Anggaran Karbon (A)
+
+ + + + @forelse ($topDataCentres as $row) + + @empty + + @endforelse + +
Pusat DataJumlah A (tCO2e)
{{ $row->nama_pusat_data }}{{ number_format($row->jumlah, 2) }}
Tiada data karbon lagi.
+
+
+
+ +
+
+
Jumlah Anggaran Karbon (A, B, C, D, E) Mengikut Tahun Pelaporan
+
+
+ + + + @forelse ($carbonByYear as $year => $tokens) + @php($map = $tokens->keyBy('result_token')) + + + + + + + + + @empty + + @endforelse + +
Tahun PelaporanA — PelepasanB — PenguranganC — BakiD — BersihE — % Pengurangan
{{ $year }}{{ number_format($map['A']->jumlah ?? 0, 2) }}{{ number_format($map['B']->jumlah ?? 0, 2) }}{{ number_format($map['C']->jumlah ?? 0, 2) }}{{ number_format($map['D']->jumlah ?? 0, 2) }}{{ number_format($map['E']->jumlah ?? 0, 2) }}%
Tiada data karbon lagi.
+
+
+
+
+
+@endsection diff --git a/resources/views/data_centres/_form.blade.php b/resources/views/data_centres/_form.blade.php new file mode 100644 index 0000000..0103628 --- /dev/null +++ b/resources/views/data_centres/_form.blade.php @@ -0,0 +1,40 @@ +@csrf +
+
+ + + @error('nama_pusat_data')
{{ $message }}
@enderror +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
Kapasiti penjanaan elektrik komponen IT.
+
+
+ + +
+
+
+ + Batal +
diff --git a/resources/views/data_centres/create.blade.php b/resources/views/data_centres/create.blade.php new file mode 100644 index 0000000..0aab1c8 --- /dev/null +++ b/resources/views/data_centres/create.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') +@section('title', 'Daftar Pusat Data') +@section('heading', 'Pusat Data') +@section('page-title', 'Daftar Pusat Data Baharu') +@section('content') +
+
@include('data_centres._form')
+
+@endsection diff --git a/resources/views/data_centres/edit.blade.php b/resources/views/data_centres/edit.blade.php new file mode 100644 index 0000000..8ad93fb --- /dev/null +++ b/resources/views/data_centres/edit.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') +@section('title', 'Kemaskini Pusat Data') +@section('heading', 'Pusat Data') +@section('page-title', 'Kemaskini Pusat Data') +@section('content') +
+
@method('PUT')@include('data_centres._form')
+
+@endsection diff --git a/resources/views/data_centres/index.blade.php b/resources/views/data_centres/index.blade.php new file mode 100644 index 0000000..b588f41 --- /dev/null +++ b/resources/views/data_centres/index.blade.php @@ -0,0 +1,47 @@ +@extends('layouts.app') +@section('title', 'Senarai Pusat Data') +@section('heading', 'Pusat Data') +@section('page-title', 'Senarai Pusat Data') + +@section('actions') + @can('create', App\Models\DataCentre::class) + Daftar Pusat Data + @endcan +@endsection + +@section('content') +
+
+
+
+ +
+
+
+ +
+ + + + @forelse ($dataCentres as $dc) + + + + + + + + + @empty + + @endforelse + +
Nama Pusat DataLokasiIT Load (MW)Perunding AktifStatus
{{ $dc->nama_pusat_data }}
{{ $dc->tajuk_permohonan }}
{{ $dc->lokasi_pusat_data ?: '—' }}{{ $dc->it_load ? number_format($dc->it_load,2) : '—' }}{{ $dc->currentConsultant?->nama_perunding ?? '—' }}{{ $dc->status==='aktif'?'Aktif':'Tidak Aktif' }}Lihat
Tiada pusat data dijumpai.
+
+ {{ $dataCentres->links() }} +
+@endsection diff --git a/resources/views/data_centres/show.blade.php b/resources/views/data_centres/show.blade.php new file mode 100644 index 0000000..e47afe1 --- /dev/null +++ b/resources/views/data_centres/show.blade.php @@ -0,0 +1,93 @@ +@extends('layouts.app') +@section('title', 'Maklumat Pusat Data') +@section('heading', 'Pusat Data') +@section('page-title', $dataCentre->nama_pusat_data) +@section('subtitle', $dataCentre->tajuk_permohonan) + +@section('actions') + @can('update', $dataCentre) + Kemaskini + @endcan +@endsection + +@section('content') +
+
+
+
Maklumat Pusat Data
+
+
Status
{{ $dataCentre->status }}
+
Lokasi
{{ $dataCentre->lokasi_pusat_data ?: '—' }}
+
Alamat
{{ $dataCentre->alamat ?: '—' }}
+
Keluasan Tapak
{{ $dataCentre->keluasan_tapak ? number_format($dataCentre->keluasan_tapak,2).' ekar' : '—' }}
+
IT Load
{{ $dataCentre->it_load ? number_format($dataCentre->it_load,2).' MW' : '—' }}
+
Perunding Aktif
{{ $dataCentre->currentConsultant?->nama_perunding ?? 'Tiada' }}
+
+
+ + @can('assignConsultant', $dataCentre) +
Tukar / Tugaskan Perunding
+
+ @csrf +
+ + +
+
+
+ +
+ @if ($dataCentre->current_consultant_id) +
+ @csrf + +
+ @endif +
+ @endcan +
+ +
+
Sejarah Penugasan Perunding
+
+ + + @forelse ($dataCentre->assignments as $a) + + + + + + + + @empty + + @endforelse + +
PerundingMulaTamatSebabStatus
{{ $a->consultant->nama_perunding ?? '—' }}{{ $a->start_date?->format('d/m/Y') }}{{ $a->end_date?->format('d/m/Y') ?: '—' }}{{ $a->reason ?: '—' }}{{ $a->is_active?'Aktif':'Tamat' }}
Tiada rekod penugasan.
+
+ +
Serahan Pusat Data Ini
+
+ + + @forelse ($dataCentre->submissions as $s) + + + + + + @empty + + @endforelse + +
KitaranStatus
Pembaharuan {{ $s->reportingCycle->renewal_year }} / Pelaporan {{ $s->reportingCycle->reporting_year }}{{ $s->statusLabel() }}Lihat
Tiada serahan.
+
+
+
+@endsection diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php new file mode 100644 index 0000000..9e515ff --- /dev/null +++ b/resources/views/layouts/app.blade.php @@ -0,0 +1,75 @@ + + + + + + + @yield('title', 'Pemantauan Kemampanan Pusat Data') — MBIP + @vite(['resources/sass/app.scss', 'resources/js/app.js']) + + +
+ {{-- Sidebar --}} + + + {{-- Offcanvas sidebar untuk mobil --}} + + +
+ {{-- Bar atas --}} +
+
+ + @yield('heading', 'Papan Pemuka') +
+ +
+ +
+ @include('layouts.flash') + +
+
+

@yield('page-title', View::yieldContent('heading'))

+ @hasSection('subtitle')@yield('subtitle')@endif +
+
@yield('actions')
+
+ + @yield('content') +
+
+
+@stack('scripts') + + diff --git a/resources/views/layouts/flash.blade.php b/resources/views/layouts/flash.blade.php new file mode 100644 index 0000000..030b788 --- /dev/null +++ b/resources/views/layouts/flash.blade.php @@ -0,0 +1,25 @@ +@if (session('success')) + +@endif + +@if (session('error')) + +@endif + +@if ($errors->any()) + +@endif diff --git a/resources/views/layouts/sidebar.blade.php b/resources/views/layouts/sidebar.blade.php new file mode 100644 index 0000000..8e915c2 --- /dev/null +++ b/resources/views/layouts/sidebar.blade.php @@ -0,0 +1,98 @@ +@php($u = auth()->user()) + + MBIP +
Pemantauan Kemampanan Pusat Data
+
+ + diff --git a/resources/views/reminders/index.blade.php b/resources/views/reminders/index.blade.php new file mode 100644 index 0000000..6b4bd10 --- /dev/null +++ b/resources/views/reminders/index.blade.php @@ -0,0 +1,60 @@ +@extends('layouts.app') +@section('title', 'Senarai Peringatan') +@section('heading', 'Peringatan') +@section('page-title', 'Senarai Peringatan / Belum Serah') +@section('subtitle', 'Pusat data aktif dengan perunding aktif yang belum menghantar serahan untuk kitaran dipilih.') + +@section('content') +
+
+
+ +
+
+ +
+
+
+
+
+
+
+ +@if ($cycle) +
+ Kitaran: Pelaporan {{ $cycle->reporting_year }} • Tarikh tutup: {{ $cycle->cut_off_date?->format('d/m/Y') }} + {{ $rows->count() }} pusat data belum serah @if($cycle->isOverdue())Lampau Tempoh@endif +
+@endif + +
+ + + + @forelse ($rows as $row) + + + + + + + + @empty + + @endforelse + +
Pusat DataPerundingEmelStatus
{{ $row->data_centre->nama_pusat_data }}{{ $row->consultant?->nama_perunding ?? '—' }}{{ $row->consultant?->emel ?? '—' }} + @if($row->status==='belum_mula')Belum Mula + @elseDraf@endif + @if($row->is_overdue)Lampau@endif + + @if($row->submission)Lihat@endif + Pusat Data +
Tiada pusat data tertunggak — semua telah menghantar. 🎉
+
+@endsection diff --git a/resources/views/reporting_cycles/_form.blade.php b/resources/views/reporting_cycles/_form.blade.php new file mode 100644 index 0000000..7ee3e26 --- /dev/null +++ b/resources/views/reporting_cycles/_form.blade.php @@ -0,0 +1,51 @@ +@csrf +
+
+ + + @error('renewal_year')
{{ $message }}
@enderror +
Tahun pelaporan (auto): {{ $cycle->renewal_year ? $cycle->renewal_year - 2 : '—' }}
+
+
+ + +
Kosongkan untuk guna tetapan lalai JPP.
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + Batal +
+ +@push('scripts') + +@endpush diff --git a/resources/views/reporting_cycles/create.blade.php b/resources/views/reporting_cycles/create.blade.php new file mode 100644 index 0000000..917d424 --- /dev/null +++ b/resources/views/reporting_cycles/create.blade.php @@ -0,0 +1,9 @@ +@extends('layouts.app') +@section('title', 'Cipta Kitaran') +@section('heading', 'Kitaran Pelaporan') +@section('page-title', 'Cipta Kitaran Pelaporan') +@section('content') +
+
@include('reporting_cycles._form')
+
+@endsection diff --git a/resources/views/reporting_cycles/edit.blade.php b/resources/views/reporting_cycles/edit.blade.php new file mode 100644 index 0000000..49672c3 --- /dev/null +++ b/resources/views/reporting_cycles/edit.blade.php @@ -0,0 +1,14 @@ +@extends('layouts.app') +@section('title', 'Kemaskini Kitaran') +@section('heading', 'Kitaran Pelaporan') +@section('page-title', 'Kemaskini Kitaran Pelaporan') +@section('content') +
+
@method('PUT')@include('reporting_cycles._form')
+
+ +
+ @csrf @method('DELETE') + +
+@endsection diff --git a/resources/views/reporting_cycles/index.blade.php b/resources/views/reporting_cycles/index.blade.php new file mode 100644 index 0000000..894ab8f --- /dev/null +++ b/resources/views/reporting_cycles/index.blade.php @@ -0,0 +1,40 @@ +@extends('layouts.app') +@section('title', 'Kitaran Pelaporan') +@section('heading', 'Kitaran Pelaporan') +@section('page-title', 'Kitaran Pelaporan / Pembaharuan Lesen') + +@section('actions') + Cipta Kitaran +@endsection + +@section('content') +
+ Peraturan: Tahun pelaporan = Tahun pembaharuan − 2. Contoh: pembaharuan 2028 ⇒ pelaporan 2026 (data Jan–Dis 2026). Pembaharuan lesen dibuat pada/sebelum tarikh tutup (lalai 1 Disember). +
+
+
+ + + + @forelse ($cycles as $c) + + + + + + + + + + + @empty + + @endforelse + +
Tahun PembaharuanTahun PelaporanTempoh PelaporanTarikh TutupTempoh LesenSerahanStatus
{{ $c->renewal_year }}{{ $c->reporting_year }}{{ $c->period_start?->format('d/m/Y') }} – {{ $c->period_end?->format('d/m/Y') }}{{ $c->cut_off_date?->format('d/m/Y') }} @if($c->isOverdue())Lampau@endif{{ $c->licence_period_year }} tahun{{ $c->submissions_count }}{{ $c->status==='aktif'?'Aktif':'Tutup' }} + +
Tiada kitaran lagi.
+
+
+
{{ $cycles->links() }}
+@endsection diff --git a/resources/views/reports/index.blade.php b/resources/views/reports/index.blade.php new file mode 100644 index 0000000..48a54ce --- /dev/null +++ b/resources/views/reports/index.blade.php @@ -0,0 +1,69 @@ +@extends('layouts.app') +@section('title', 'Laporan & Statistik') +@section('heading', 'Laporan') +@section('page-title', 'Laporan & Statistik Karbon') + +@section('content') +
+
+
+ +
+
+
+
+ +
+
+
Trend Jumlah Pelepasan (A) Mengikut Tahun Pelaporan
+
+ + + @forelse ($trend as $year => $jumlah) + + @empty + + @endforelse + +
TahunJumlah A (tCO2e)
{{ $year }}{{ number_format($jumlah,2) }}
Tiada data.
+
+
+
+
Pecahan Status Serahan
+
+ @foreach (App\Models\Submission::STATUSES as $k => $v) +
{{ $v }}{{ $statusBreakdown[$k] ?? 0 }}
+ @endforeach +
+
+
+
+ +
Hasil Karbon Mengikut Pusat Data & Tahun Pelaporan
+
+ + + + @forelse ($rows as $key => $group) + @php($map = $group->keyBy('result_token')) + @php([$nama, $tahun] = explode('|', $key)) + + + + + + + + + + @empty + + @endforelse + +
Pusat DataTahunABCDE (%)
{{ $nama }}{{ $tahun }}{{ number_format(optional($map->get('A'))->value ?? 0, 2) }}{{ number_format(optional($map->get('B'))->value ?? 0, 2) }}{{ number_format(optional($map->get('C'))->value ?? 0, 2) }}{{ number_format(optional($map->get('D'))->value ?? 0, 2) }}{{ number_format(optional($map->get('E'))->value ?? 0, 2) }}
Tiada data karbon.
+
+
+@endsection diff --git a/resources/views/settings/edit.blade.php b/resources/views/settings/edit.blade.php new file mode 100644 index 0000000..b97a321 --- /dev/null +++ b/resources/views/settings/edit.blade.php @@ -0,0 +1,34 @@ +@extends('layouts.app') +@section('title', 'Tetapan') +@section('heading', 'Tetapan') +@section('page-title', 'Tetapan Aplikasi') + +@section('content') +
+ @csrf @method('PUT') + @foreach ($settings as $group => $items) +
+
{{ $group }}
+
+ @foreach ($items as $s) +
+
{{ $s->description }}
+
+ @if($s->type === 'boolean') +
value)>
+ @elseif($s->type === 'integer') + + @elseif($s->type === 'date') + + @else + + @endif +
+
+ @endforeach +
+
+ @endforeach + +
+@endsection diff --git a/resources/views/submissions/_comments.blade.php b/resources/views/submissions/_comments.blade.php new file mode 100644 index 0000000..e18e8a4 --- /dev/null +++ b/resources/views/submissions/_comments.blade.php @@ -0,0 +1,39 @@ +@php($isJpp = auth()->user()->can('serahan.lihat_semua')) +
+
Perbualan / Komen
+
+
+ @forelse ($submission->comments as $comment) + {{-- Komen dalaman hanya kelihatan kepada kakitangan JPP --}} + @if ($comment->is_internal && ! $isJpp) @continue @endif +
+
+ {{ $comment->user?->name ?? 'Pengguna' }} + @if($comment->is_internal)Dalaman@endif + + {{ $comment->created_at->format('d/m/Y H:i') }} +
+
{!! nl2br(e($comment->body)) !!}
+ @can('komen.padam') +
@csrf @method('DELETE')
+ @endcan +
+ @empty +

Tiada mesej lagi.

+ @endforelse +
+ + @can('comment', $submission) +
+ @csrf + +
+ @if($isJpp) +
+ @else@endif + +
+
+ @endcan +
+
diff --git a/resources/views/submissions/_corrections_panel.blade.php b/resources/views/submissions/_corrections_panel.blade.php new file mode 100644 index 0000000..10d95e8 --- /dev/null +++ b/resources/views/submissions/_corrections_panel.blade.php @@ -0,0 +1,55 @@ +@php($canReview = auth()->user()->can('review', $submission)) +@php($canRespond = auth()->user()->can('respondCorrection', $submission)) +
+
Permohonan Pembetulan
+
+ @forelse ($submission->correctionRequests as $cr) +
+
+ m/s {{ $cr->page_reference }} — {{ $cr->location }} + {{ App\Models\SubmissionCorrectionRequest::STATUSES[$cr->status] ?? $cr->status }} +
+
{{ $cr->description }}
+
Oleh {{ $cr->createdBy?->name }} • {{ $cr->created_at->format('d/m/Y') }}
+ + @if($cr->consultant_response) +
Maklum balas perunding: {{ $cr->consultant_response }}
+ @endif + + @if($canRespond && $cr->status !== 'selesai') +
+ @csrf + + +
+ @endif + + @if($canReview && $cr->status !== 'selesai') +
@csrf
+ @endif +
+ @empty +

Tiada permohonan pembetulan.

+ @endforelse + + @if($canReview) +
+
+ @csrf + +
+
+
+
+ + + +
+ @endif +
+
diff --git a/resources/views/submissions/_documents_panel.blade.php b/resources/views/submissions/_documents_panel.blade.php new file mode 100644 index 0000000..8bcae67 --- /dev/null +++ b/resources/views/submissions/_documents_panel.blade.php @@ -0,0 +1,32 @@ +
+
Laporan PDF
+
+ @if ($submission->isEditableByConsultant() && auth()->user()->can('update', $submission)) +
+ @csrf + + +
PDF sahaja. Versi lama dikekalkan.
+
+ @endif + + @forelse ($submission->documents as $doc) +
+
+ v{{ $doc->version }} + @if($doc->is_active)Aktif@endif +
{{ $doc->original_filename }}
+
{{ $doc->humanSize() }} • {{ $doc->created_at->format('d/m/Y H:i') }}
+
+
+ + @if (!$doc->is_active && auth()->user()->can('update', $submission) && $submission->isEditableByConsultant()) +
@csrf
+ @endif +
+
+ @empty +

Tiada laporan dimuat naik.

+ @endforelse +
+
diff --git a/resources/views/submissions/_item_field.blade.php b/resources/views/submissions/_item_field.blade.php new file mode 100644 index 0000000..b1ba3c5 --- /dev/null +++ b/resources/views/submissions/_item_field.blade.php @@ -0,0 +1,46 @@ +@php + $val = old("answers.{$item->id}.value", $answer->value ?? ''); + $page = old("answers.{$item->id}.page_reference", $answer->page_reference ?? ''); + $note = old("answers.{$item->id}.consultant_note", $answer->consultant_note ?? ''); + $disabled = (!$editable || $item->is_calculated) ? 'disabled' : ''; +@endphp +
+ + @if($item->description)
{{ $item->description }}
@endif + +
+
+ @switch($item->input_type) + @case('textarea') + + @break + @case('select') + + @break + @case('checkbox') +
+ @break + @case('number') + @case('calculated') + + @break + @default + + @endswitch +
+
+ +
+
+ +
+
+
diff --git a/resources/views/submissions/_results_panel.blade.php b/resources/views/submissions/_results_panel.blade.php new file mode 100644 index 0000000..234ae2c --- /dev/null +++ b/resources/views/submissions/_results_panel.blade.php @@ -0,0 +1,17 @@ +
+
Hasil Formula (dikira automatik)
+
+ + + @forelse ($submission->results as $r) + + + + + @empty + + @endforelse + +
{{ $r->result_token }} {{ $r->label }}{{ $r->value !== null ? number_format($r->value, 2) : '—' }} {{ $r->unit }}
Simpan draf untuk mengira formula.
+
+
diff --git a/resources/views/submissions/_review_panel.blade.php b/resources/views/submissions/_review_panel.blade.php new file mode 100644 index 0000000..b5bea1e --- /dev/null +++ b/resources/views/submissions/_review_panel.blade.php @@ -0,0 +1,87 @@ +@php($canReview = auth()->user()->can('review', $submission)) +
+
Tindakan JPP
+
+ @if($canReview) + {{-- Tukar status --}} +
+ @csrf + + + + + +
Status "Pembetulan Perunding" perlu ada permohonan pembetulan terbuka.
+
+ + {{-- Pegawai bertanggungjawab --}} +
+ @csrf + +
+ + +
+
+ + {{-- Nota semakan keseluruhan --}} +
+ @csrf + + + +
+ @endif + + {{-- Salinan keras --}} + @can('markHardcopy', $submission) +
+ @csrf + +
hardcopy_received)>
+ + +
+ @endcan + + {{-- Kunci / buka kunci --}} + @can('lock', $submission) +
+ @if(!$submission->is_locked) +
+ @csrf + + +
+ @else +
+ @csrf + @if($submission->lock_reason)
Sebab kunci: {{ $submission->lock_reason }}
@endif + + +
+ @endif + @endcan +
+
+ +@push('scripts') + +@endpush diff --git a/resources/views/submissions/create.blade.php b/resources/views/submissions/create.blade.php new file mode 100644 index 0000000..996f9e7 --- /dev/null +++ b/resources/views/submissions/create.blade.php @@ -0,0 +1,35 @@ +@extends('layouts.app') +@section('title', 'Mula Serahan') +@section('heading', 'Serahan') +@section('page-title', 'Mula Serahan Baharu') + +@section('content') +
+ @if ($dataCentres->isEmpty()) +
Anda tiada pusat data aktif yang ditugaskan. Sila hubungi JPP.
+ @elseif ($cycles->isEmpty()) +
Tiada kitaran pelaporan aktif buat masa ini.
+ @else +
+ @csrf +
+
+ + +
+
+ + +
+
+
+
+ @endif +
+@endsection diff --git a/resources/views/submissions/fill.blade.php b/resources/views/submissions/fill.blade.php new file mode 100644 index 0000000..e132584 --- /dev/null +++ b/resources/views/submissions/fill.blade.php @@ -0,0 +1,78 @@ +@extends('layouts.app') +@section('title', 'Borang Senarai Semak') +@section('heading', 'Serahan') +@section('page-title', 'Borang Senarai Semak') +@section('subtitle', $submission->dataCentre->nama_pusat_data.' • Pelaporan '.$submission->reportingCycle->reporting_year) + +@section('actions') + Lihat Ringkasan +@endsection + +@section('content') +@if (!$submission->isEditableByConsultant()) +
Serahan ini telah dikunci / selesai dan tidak boleh diubah.
+@endif + +@if ($submission->correctionRequests->where('status','!=','selesai')->count()) +
+ Permohonan pembetulan daripada JPP: +
    + @foreach ($submission->correctionRequests->where('status','!=','selesai') as $cr) +
  • [m/s {{ $cr->page_reference }}] {{ $cr->location }} — {{ $cr->description }} + jawab di sini
  • + @endforeach +
+
+@endif + +
+
+
+ @csrf @method('PUT') + + @forelse ($template->sections as $section) +
+
{{ $section->code }} {{ $section->title }}
+
+ @if($section->description)

{{ $section->description }}

@endif + + @php($renderItem = function($item) use ($answers) { + return [$item, $answers[$item->id] ?? null]; + }) + + {{-- Item terus di bawah seksyen (tanpa skop) --}} + @foreach ($section->items->whereNull('checklist_scope_id') as $item) + @include('submissions._item_field', ['item' => $item, 'answer' => $answers[$item->id] ?? null, 'editable' => $submission->isEditableByConsultant()]) + @endforeach + + {{-- Item dikumpul mengikut skop --}} + @foreach ($section->scopes as $scope) +
+
{{ $scope->code }} {{ $scope->title }}
+ @if($scope->description)

{{ $scope->description }}

@endif + @foreach ($scope->items as $item) + @include('submissions._item_field', ['item' => $item, 'answer' => $answers[$item->id] ?? null, 'editable' => $submission->isEditableByConsultant()]) + @endforeach +
+ @endforeach +
+
+ @empty +
Templat senarai semak tidak mempunyai seksyen.
+ @endforelse + + @if ($submission->isEditableByConsultant()) +
+ + +
+ @endif +
+
+ +
+ @include('submissions._documents_panel', ['submission' => $submission]) + @include('submissions._results_panel', ['submission' => $submission]) +
+
+@endsection diff --git a/resources/views/submissions/index.blade.php b/resources/views/submissions/index.blade.php new file mode 100644 index 0000000..3d31936 --- /dev/null +++ b/resources/views/submissions/index.blade.php @@ -0,0 +1,57 @@ +@extends('layouts.app') +@section('title', 'Senarai Serahan') +@section('heading', 'Serahan') +@section('page-title', 'Senarai Serahan') + +@section('actions') + @can('create', App\Models\Submission::class) + Mula Serahan + @endcan +@endsection + +@section('content') +
+
+
+ +
+
+ +
+
+
+ +
+ + + + @forelse ($submissions as $s) + + + + + + + + + + @empty + + @endforelse + +
Pusat DataKitaranPerundingStatusPegawaiDihantar
{{ $s->dataCentre->nama_pusat_data }}Pemb. {{ $s->reportingCycle->renewal_year }} / Pel. {{ $s->reportingCycle->reporting_year }}{{ $s->consultant->nama_perunding }}{{ $s->statusLabel() }} @if($s->is_locked)@endif{{ $s->assignedOfficer?->name ?? '—' }}{{ $s->submitted_at?->format('d/m/Y') ?? '—' }} + @if ($s->isEditableByConsultant() && auth()->user()->can('update', $s)) + Isi + @endif + Lihat +
Tiada serahan.
+
+ {{ $submissions->links() }} +
+@endsection diff --git a/resources/views/submissions/show.blade.php b/resources/views/submissions/show.blade.php new file mode 100644 index 0000000..6d2a5f6 --- /dev/null +++ b/resources/views/submissions/show.blade.php @@ -0,0 +1,111 @@ +@extends('layouts.app') +@section('title', 'Butiran Serahan') +@section('heading', 'Serahan') +@section('page-title', $submission->dataCentre->nama_pusat_data) +@section('subtitle', 'Pelaporan '.$submission->reportingCycle->reporting_year.' • Pembaharuan '.$submission->reportingCycle->renewal_year) + +@section('actions') + @if ($submission->isEditableByConsultant() && auth()->user()->can('update', $submission)) + Isi / Kemaskini + @endif +@endsection + +@section('content') +@php($canReview = auth()->user()->can('review', $submission)) +
+ {{ $submission->statusLabel() }} + @if($submission->is_locked) Dikunci@endif + Perunding: {{ $submission->consultant->nama_perunding }} + @if($submission->submitted_at)• Dihantar: {{ $submission->submitted_at->format('d/m/Y H:i') }}@endif +
+ +
+
+ {{-- ===== Jawapan senarai semak ===== --}} + @foreach ($template?->sections ?? [] as $section) +
+
{{ $section->code }} {{ $section->title }}
+
+ + @if($canReview)@endif + + @foreach ($section->items as $item) + @php($a = $answers[$item->id] ?? null) + + + + + + @if($canReview) + + @endif + + @endforeach + +
ItemNilaim/sCatatan PerundingSemakan
{{ $item->code }} {{ $item->label }}{{ $a?->value !== null && $a?->value !== '' ? $a->value : '—' }} {{ $item->unit }}{{ $a?->page_reference ?: '—' }}{{ $a?->consultant_note ?: '—' }} + @if($a) + + @else — @endif +
+
+
+ @endforeach + + @include('submissions._comments', ['submission' => $submission]) +
+ + {{-- ===== Panel sisi ===== --}} +
+ @include('submissions._results_panel', ['submission' => $submission]) + @include('submissions._documents_panel', ['submission' => $submission]) + + @if($canReview || auth()->user()->can('markHardcopy', $submission)) + @include('submissions._review_panel', ['submission' => $submission, 'officers' => $officers]) + @endif + + @include('submissions._corrections_panel', ['submission' => $submission]) + + {{-- Sejarah status --}} +
Sejarah Status
+
    + @forelse ($submission->statusHistories as $h) +
  • + {{ App\Models\Submission::STATUSES[$h->from_status] ?? $h->from_status ?? '—' }} → + {{ App\Models\Submission::STATUSES[$h->to_status] ?? $h->to_status }} +
    {{ $h->changedBy?->name }} • {{ $h->created_at->format('d/m/Y H:i') }}
    + @if($h->notes)
    {{ $h->notes }}
    @endif +
  • + @empty +
  • Tiada sejarah.
  • + @endforelse +
+
+
+
+ +{{-- Modal semakan item (pegawai) --}} +@if($canReview) + @foreach ($submission->answers as $a) + + @endforeach +@endif +@endsection diff --git a/resources/views/users/_form.blade.php b/resources/views/users/_form.blade.php new file mode 100644 index 0000000..5e8898f --- /dev/null +++ b/resources/views/users/_form.blade.php @@ -0,0 +1,17 @@ +@csrf +
+
@error('name')
{{ $message }}
@enderror
+
@error('email')
{{ $message }}
@enderror
+
+
+
+ +
+
is_active ?? true))>
+
@error('password')
{{ $message }}
@enderror
+
+
+
Batal
diff --git a/resources/views/users/create.blade.php b/resources/views/users/create.blade.php new file mode 100644 index 0000000..d61c8ac --- /dev/null +++ b/resources/views/users/create.blade.php @@ -0,0 +1,7 @@ +@extends('layouts.app') +@section('title', 'Tambah Pengguna') +@section('heading', 'Pengguna') +@section('page-title', 'Tambah Pengguna') +@section('content') +
@include('users._form')
+@endsection diff --git a/resources/views/users/edit.blade.php b/resources/views/users/edit.blade.php new file mode 100644 index 0000000..98614d2 --- /dev/null +++ b/resources/views/users/edit.blade.php @@ -0,0 +1,7 @@ +@extends('layouts.app') +@section('title', 'Kemaskini Pengguna') +@section('heading', 'Pengguna') +@section('page-title', 'Kemaskini Pengguna') +@section('content') +
@method('PUT')@include('users._form')
+@endsection diff --git a/resources/views/users/index.blade.php b/resources/views/users/index.blade.php new file mode 100644 index 0000000..165eb22 --- /dev/null +++ b/resources/views/users/index.blade.php @@ -0,0 +1,39 @@ +@extends('layouts.app') +@section('title', 'Pengurusan Pengguna') +@section('heading', 'Pengguna') +@section('page-title', 'Pengurusan Pengguna') + +@section('actions') + Tambah Pengguna +@endsection + +@section('content') +
+
+
+ + + + @forelse ($users as $u) + + + + + + + + + @empty + + @endforelse + +
NamaEmelJawatanPerananStatus
{{ $u->name }}{{ $u->email }}{{ $u->jawatan ?: '—' }}@foreach ($u->roles as $r){{ $r->name }}@endforeach{{ $u->is_active?'Aktif':'Tidak Aktif' }} + + @if($u->id !== auth()->id()) +
@csrf @method('DELETE')
+ @endif +
Tiada pengguna.
+
+ {{ $users->links() }} +
+@endsection diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php new file mode 100644 index 0000000..26e294a --- /dev/null +++ b/resources/views/welcome.blade.php @@ -0,0 +1,223 @@ + + + + + + + {{ config('app.name', 'Laravel') }} + + @fonts + + + @if (file_exists(public_path('build/manifest.json')) || file_exists(public_path('hot'))) + @vite(['resources/css/app.css', 'resources/js/app.js']) + @else + + @endif + + +
+ @if (Route::has('login')) + + @endif +
+
+
+
+

Let's get started

+

With so many options available to you,
we suggest you start with the following:

+ + + +

+ v{{ app()->version() }} + + View changelog + + + + +

+
+
+ {{-- Laravel Logo --}} + + + + + + + + + + + {{-- 13 --}} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + @if (Route::has('login')) + + @endif + + diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..3c9adf1 --- /dev/null +++ b/routes/console.php @@ -0,0 +1,8 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..6c24ab4 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,123 @@ + redirect()->route('dashboard')); + +// ---------------- Autentikasi ---------------- +Route::middleware('guest')->group(function () { + Route::get('login', [LoginController::class, 'showLoginForm'])->name('login'); + Route::post('login', [LoginController::class, 'login']); +}); +Route::post('logout', [LoginController::class, 'logout'])->middleware('auth')->name('logout'); + +// ---------------- Laluan yang memerlukan log masuk ---------------- +Route::middleware('auth')->group(function () { + Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); + + // Perunding + Route::resource('consultants', ConsultantController::class); + + // Pusat data + penugasan perunding + Route::resource('data-centres', DataCentreController::class); + Route::post('data-centres/{dataCentre}/assign', [DataCentreController::class, 'assign']) + ->name('data-centres.assign'); + Route::post('data-centres/{dataCentre}/end-assignment', [DataCentreController::class, 'endAssignment']) + ->name('data-centres.end-assignment'); + + // Kitaran pelaporan / pembaharuan lesen + Route::resource('reporting-cycles', ReportingCycleController::class)->except(['show']); + + // ----- Pembina templat senarai semak (kebenaran templat.urus) ----- + Route::middleware('permission:templat.urus')->group(function () { + Route::get('checklist-templates', [ChecklistTemplateController::class, 'index'])->name('checklist-templates.index'); + Route::get('checklist-templates/create', [ChecklistTemplateController::class, 'create'])->name('checklist-templates.create'); + Route::post('checklist-templates', [ChecklistTemplateController::class, 'store'])->name('checklist-templates.store'); + Route::get('checklist-templates/{checklistTemplate}/builder', [ChecklistTemplateController::class, 'builder'])->name('checklist-templates.builder'); + Route::put('checklist-templates/{checklistTemplate}', [ChecklistTemplateController::class, 'update'])->name('checklist-templates.update'); + Route::post('checklist-templates/{checklistTemplate}/activate', [ChecklistTemplateController::class, 'activate'])->name('checklist-templates.activate'); + Route::delete('checklist-templates/{checklistTemplate}', [ChecklistTemplateController::class, 'destroy'])->name('checklist-templates.destroy'); + + // Seksyen + Route::post('checklist-templates/{template}/sections', [ChecklistBuilderController::class, 'storeSection'])->name('checklist.sections.store'); + Route::put('checklist-sections/{section}', [ChecklistBuilderController::class, 'updateSection'])->name('checklist.sections.update'); + Route::delete('checklist-sections/{section}', [ChecklistBuilderController::class, 'destroySection'])->name('checklist.sections.destroy'); + // Skop + Route::post('checklist-sections/{section}/scopes', [ChecklistBuilderController::class, 'storeScope'])->name('checklist.scopes.store'); + Route::put('checklist-scopes/{scope}', [ChecklistBuilderController::class, 'updateScope'])->name('checklist.scopes.update'); + Route::delete('checklist-scopes/{scope}', [ChecklistBuilderController::class, 'destroyScope'])->name('checklist.scopes.destroy'); + // Item + Route::post('checklist-sections/{section}/items', [ChecklistBuilderController::class, 'storeItem'])->name('checklist.items.store'); + Route::put('checklist-items/{item}', [ChecklistBuilderController::class, 'updateItem'])->name('checklist.items.update'); + Route::delete('checklist-items/{item}', [ChecklistBuilderController::class, 'destroyItem'])->name('checklist.items.destroy'); + // Formula + Route::post('checklist-templates/{template}/formulas', [ChecklistBuilderController::class, 'storeFormula'])->name('checklist.formulas.store'); + Route::put('checklist-formulas/{formula}', [ChecklistBuilderController::class, 'updateFormula'])->name('checklist.formulas.update'); + Route::delete('checklist-formulas/{formula}', [ChecklistBuilderController::class, 'destroyFormula'])->name('checklist.formulas.destroy'); + }); + + // ----- Serahan (perunding & semakan JPP) ----- + Route::get('submissions', [SubmissionController::class, 'index'])->name('submissions.index'); + Route::get('submissions/create', [SubmissionController::class, 'create'])->name('submissions.create'); + Route::post('submissions', [SubmissionController::class, 'store'])->name('submissions.store'); + Route::get('submissions/{submission}', [SubmissionController::class, 'show'])->name('submissions.show'); + Route::get('submissions/{submission}/edit', [SubmissionController::class, 'edit'])->name('submissions.edit'); + Route::put('submissions/{submission}', [SubmissionController::class, 'update'])->name('submissions.update'); + Route::post('submissions/{submission}/submit', [SubmissionController::class, 'submit'])->name('submissions.submit'); + + // Dokumen laporan PDF + Route::post('submissions/{submission}/documents', [DocumentController::class, 'store'])->name('documents.store'); + Route::get('documents/{document}/download', [DocumentController::class, 'download'])->name('documents.download'); + Route::post('documents/{document}/active', [DocumentController::class, 'setActive'])->name('documents.active'); + + // Tindakan semakan JPP + Route::post('submissions/{submission}/status', [SubmissionReviewController::class, 'updateStatus'])->name('submissions.status'); + Route::post('submissions/{submission}/assign-officer', [SubmissionReviewController::class, 'assignOfficer'])->name('submissions.assign-officer'); + Route::post('submissions/{submission}/lock', [SubmissionReviewController::class, 'lock'])->name('submissions.lock'); + Route::post('submissions/{submission}/unlock', [SubmissionReviewController::class, 'unlock'])->name('submissions.unlock'); + Route::post('submissions/{submission}/hardcopy', [SubmissionReviewController::class, 'markHardcopy'])->name('submissions.hardcopy'); + Route::post('submissions/{submission}/review-note', [SubmissionReviewController::class, 'reviewNote'])->name('submissions.review-note'); + Route::post('answers/{answer}/review', [SubmissionReviewController::class, 'reviewAnswer'])->name('answers.review'); + + // Permohonan pembetulan + Route::post('submissions/{submission}/corrections', [CorrectionRequestController::class, 'store'])->name('corrections.store'); + Route::post('corrections/{correction}/respond', [CorrectionRequestController::class, 'respond'])->name('corrections.respond'); + Route::post('corrections/{correction}/resolve', [CorrectionRequestController::class, 'resolve'])->name('corrections.resolve'); + + // Komen / perbualan + Route::post('submissions/{submission}/comments', [CommentController::class, 'store'])->name('comments.store'); + Route::delete('comments/{comment}', [CommentController::class, 'destroy'])->name('comments.destroy'); + + // Senarai peringatan / belum serah + Route::get('reminders', [ReminderController::class, 'index'])->name('reminders.index'); + + // Laporan & statistik + Route::get('reports', [ReportController::class, 'index'])->name('reports.index'); + + // Pengurusan pengguna + Route::resource('users', UserController::class)->except(['show']); + + // Tetapan aplikasi + Route::get('settings', [SettingController::class, 'edit'])->name('settings.edit'); + Route::put('settings', [SettingController::class, 'update'])->name('settings.update'); + + // Log audit + Route::get('audit-log', [AuditLogController::class, 'index'])->name('audit.index'); +}); diff --git a/storage/app/.gitignore b/storage/app/.gitignore new file mode 100644 index 0000000..fedb287 --- /dev/null +++ b/storage/app/.gitignore @@ -0,0 +1,4 @@ +* +!private/ +!public/ +!.gitignore diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/private/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/app/public/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore new file mode 100644 index 0000000..05c4471 --- /dev/null +++ b/storage/framework/.gitignore @@ -0,0 +1,9 @@ +compiled.php +config.php +down +events.scanned.php +maintenance.php +routes.php +routes.scanned.php +schedule-* +services.json diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore new file mode 100644 index 0000000..01e4a6c --- /dev/null +++ b/storage/framework/cache/.gitignore @@ -0,0 +1,3 @@ +* +!data/ +!.gitignore diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/cache/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/sessions/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/testing/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/framework/views/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/storage/logs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/tests/Feature/AssignmentTest.php b/tests/Feature/AssignmentTest.php new file mode 100644 index 0000000..c83b2c2 --- /dev/null +++ b/tests/Feature/AssignmentTest.php @@ -0,0 +1,74 @@ +seedRolesAndPermissions(); + } + + public function test_jpp_can_assign_consultant_to_data_centre(): void + { + $jpp = $this->makeUserWithRole('Pegawai JPP'); + $dc = DataCentre::factory()->create(); + $consultant = Consultant::factory()->create(); + + $this->actingAs($jpp)->post(route('data-centres.assign', $dc), [ + 'consultant_id' => $consultant->id, + 'start_date' => now()->toDateString(), + 'reason' => 'Penugasan awal', + ])->assertRedirect(); + + $dc->refresh(); + $this->assertEquals($consultant->id, $dc->current_consultant_id); + $this->assertDatabaseHas('data_centre_consultant_assignments', [ + 'data_centre_id' => $dc->id, + 'consultant_id' => $consultant->id, + 'is_active' => true, + ]); + } + + public function test_only_one_active_consultant_per_data_centre(): void + { + $jpp = $this->makeUserWithRole('Pegawai JPP'); + $dc = DataCentre::factory()->create(); + $first = Consultant::factory()->create(); + $second = Consultant::factory()->create(); + + $this->actingAs($jpp)->post(route('data-centres.assign', $dc), [ + 'consultant_id' => $first->id, + ]); + + // Tugaskan perunding kedua — penugasan pertama mesti ditamatkan. + $this->actingAs($jpp)->post(route('data-centres.assign', $dc), [ + 'consultant_id' => $second->id, + 'reason' => 'Tukar perunding', + ]); + + $dc->refresh(); + $this->assertEquals($second->id, $dc->current_consultant_id); + + // Hanya satu penugasan aktif. + $activeCount = $dc->assignments()->where('is_active', true)->count(); + $this->assertEquals(1, $activeCount); + + // Penugasan pertama ditamatkan (ada end_date). + $this->assertDatabaseHas('data_centre_consultant_assignments', [ + 'data_centre_id' => $dc->id, + 'consultant_id' => $first->id, + 'is_active' => false, + ]); + // Sejarah dikekalkan: dua rekod penugasan. + $this->assertEquals(2, $dc->assignments()->count()); + } +} diff --git a/tests/Feature/ConsultantManagementTest.php b/tests/Feature/ConsultantManagementTest.php new file mode 100644 index 0000000..136c82d --- /dev/null +++ b/tests/Feature/ConsultantManagementTest.php @@ -0,0 +1,65 @@ +seedRolesAndPermissions(); + } + + public function test_jpp_can_create_consultant(): void + { + $jpp = $this->makeUserWithRole('JPP Admin'); + + $response = $this->actingAs($jpp)->post(route('consultants.store'), [ + 'nama_perunding' => 'Perunding Hijau Sdn Bhd', + 'no_pendaftaran_syarikat' => '202301000999', + 'emel' => 'hijau@example.com', + 'telefon' => '07-9998888', + 'aktif' => 1, + ]); + + $this->assertDatabaseHas('consultants', ['nama_perunding' => 'Perunding Hijau Sdn Bhd']); + $response->assertRedirect(); + } + + public function test_jpp_can_create_consultant_with_login_account(): void + { + $jpp = $this->makeUserWithRole('JPP Admin'); + + $this->actingAs($jpp)->post(route('consultants.store'), [ + 'nama_perunding' => 'Perunding Akaun Sdn Bhd', + 'aktif' => 1, + 'buat_akaun' => 1, + 'user_name' => 'Perunding Akaun', + 'user_email' => 'akaun@example.com', + 'user_password' => 'rahsia12345', + ]); + + $this->assertDatabaseHas('users', ['email' => 'akaun@example.com']); + $consultant = Consultant::where('nama_perunding', 'Perunding Akaun Sdn Bhd')->first(); + $this->assertNotNull($consultant->user_id); + $this->assertTrue($consultant->user->hasRole('Perunding')); + } + + public function test_perunding_cannot_create_consultant(): void + { + [$user] = $this->makeConsultantUser(); + + $this->actingAs($user)->post(route('consultants.store'), [ + 'nama_perunding' => 'Tidak Dibenarkan', + 'aktif' => 1, + ])->assertForbidden(); + + $this->assertDatabaseMissing('consultants', ['nama_perunding' => 'Tidak Dibenarkan']); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php new file mode 100644 index 0000000..eb6ee5d --- /dev/null +++ b/tests/Feature/ExampleTest.php @@ -0,0 +1,20 @@ +get('/'); + + $response->assertRedirect(route('dashboard')); + } +} diff --git a/tests/Feature/FormulaTest.php b/tests/Feature/FormulaTest.php new file mode 100644 index 0000000..ecb62bd --- /dev/null +++ b/tests/Feature/FormulaTest.php @@ -0,0 +1,59 @@ +seed(ChecklistTemplateSeeder::class); + $template = ChecklistTemplate::activeTemplate(); + $service = app(FormulaService::class); + + $vars = ['SKOP1' => 100, 'SKOP2' => 200, 'SKOP3' => 50, 'EE' => 30, 'RE' => 20, 'D4I' => 10, 'D4II' => 15]; + $results = collect($service->compute($template, $vars))->keyBy('result_token'); + + $this->assertEquals(350, $results['A']['value']); // 100 + 200 + 50 + $this->assertEquals(50, $results['B']['value']); // 30 + 20 + $this->assertEquals(300, $results['C']['value']); // A - B + $this->assertEquals(325, $results['D']['value']); // A - 10 - 15 + $this->assertEqualsWithDelta(92.857, $results['E']['value'], 0.01); // D / A * 100 + } + + public function test_division_by_zero_is_guarded(): void + { + $this->seed(ChecklistTemplateSeeder::class); + $template = ChecklistTemplate::activeTemplate(); + $service = app(FormulaService::class); + + // Semua sifar => A = 0, formula E (D/A*100) mesti mengembalikan 0, bukan ralat. + $vars = ['SKOP1' => 0, 'SKOP2' => 0, 'SKOP3' => 0, 'EE' => 0, 'RE' => 0, 'D4I' => 0, 'D4II' => 0]; + $results = collect($service->compute($template, $vars))->keyBy('result_token'); + + $this->assertEquals(0, $results['E']['value']); + } + + public function test_expression_evaluator_respects_operator_precedence(): void + { + $eval = app(ExpressionEvaluator::class); + + $this->assertEquals(14, $eval->evaluate('2 + 3 * 4')); + $this->assertEquals(20, $eval->evaluate('(2 + 3) * 4')); + $this->assertEquals(5, $eval->evaluate('A + B', ['A' => 2, 'B' => 3])); + } + + public function test_expression_evaluator_rejects_invalid_characters(): void + { + $this->expectException(\RuntimeException::class); + app(ExpressionEvaluator::class)->evaluate('A; DROP TABLE', ['A' => 1]); + } +} diff --git a/tests/Feature/ReminderTest.php b/tests/Feature/ReminderTest.php new file mode 100644 index 0000000..54a175b --- /dev/null +++ b/tests/Feature/ReminderTest.php @@ -0,0 +1,64 @@ +seedRolesAndPermissions(); + $this->seed(ChecklistTemplateSeeder::class); + } + + public function test_reminder_list_removes_consultant_after_submission(): void + { + $jpp = $this->makeUserWithRole('Pegawai JPP'); + [$cUser, $consultant] = $this->makeConsultantUser(); + $cycle = ReportingCycle::factory()->create(); + $dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]); + + // Pada mulanya pusat data muncul dalam senarai peringatan. + $response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id])); + $response->assertOk(); + $response->assertSee($dc->nama_pusat_data); + + // Perunding hantar serahan. + $submission = Submission::create([ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $consultant->id, + 'status' => 'baru', + 'submitted_at' => now(), + ]); + + // Selepas hantar, pusat data dikeluarkan daripada senarai peringatan. + $response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id])); + $response->assertOk(); + $response->assertDontSee($dc->nama_pusat_data); + } + + public function test_draft_submission_still_appears_in_reminder_list(): void + { + $jpp = $this->makeUserWithRole('Pegawai JPP'); + [$cUser, $consultant] = $this->makeConsultantUser(); + $cycle = ReportingCycle::factory()->create(); + $dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]); + + // Hanya draf (belum hantar) — masih dalam senarai peringatan. + app(SubmissionWorkflowService::class)->findOrCreateDraft($dc, $cycle); + + $response = $this->actingAs($jpp)->get(route('reminders.index', ['cycle' => $cycle->id])); + $response->assertSee($dc->nama_pusat_data); + } +} diff --git a/tests/Feature/ReportingCycleTest.php b/tests/Feature/ReportingCycleTest.php new file mode 100644 index 0000000..840760d --- /dev/null +++ b/tests/Feature/ReportingCycleTest.php @@ -0,0 +1,42 @@ +seedRolesAndPermissions(); + } + + public function test_reporting_year_is_calculated_from_renewal_year(): void + { + // Peraturan: pembaharuan 2028 => pelaporan 2026. + $this->assertEquals(2026, ReportingCycle::reportingYearFor(2028)); + $this->assertEquals(2025, ReportingCycle::reportingYearFor(2027)); + } + + public function test_jpp_can_create_cycle_with_calculated_reporting_year(): void + { + $jpp = $this->makeUserWithRole('JPP Admin'); + + $this->actingAs($jpp)->post(route('reporting-cycles.store'), [ + 'renewal_year' => 2028, + 'licence_period_year' => 1, + 'status' => 'aktif', + ])->assertRedirect(); + + $cycle = ReportingCycle::where('renewal_year', 2028)->first(); + $this->assertNotNull($cycle); + $this->assertEquals(2026, $cycle->reporting_year); + $this->assertEquals('2026-01-01', $cycle->period_start->toDateString()); + $this->assertEquals('2026-12-31', $cycle->period_end->toDateString()); + } +} diff --git a/tests/Feature/SubmissionTest.php b/tests/Feature/SubmissionTest.php new file mode 100644 index 0000000..ab9f3bc --- /dev/null +++ b/tests/Feature/SubmissionTest.php @@ -0,0 +1,218 @@ +seedRolesAndPermissions(); + $this->seed(ChecklistTemplateSeeder::class); + } + + /** Cipta perunding+akaun, pusat data yang ditugaskan & kitaran aktif. */ + protected function scenario(): array + { + [$user, $consultant] = $this->makeConsultantUser(); + $cycle = ReportingCycle::factory()->create(); + $dc = DataCentre::factory()->create(['current_consultant_id' => $consultant->id]); + + return [$user, $consultant, $dc, $cycle]; + } + + public function test_consultant_can_submit_checklist_and_pdf(): void + { + Storage::fake('local'); + [$user, $consultant, $dc, $cycle] = $this->scenario(); + + // Cipta draf serahan. + $this->actingAs($user)->post(route('submissions.store'), [ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + ])->assertRedirect(); + + $submission = Submission::first(); + $this->assertEquals('draf', $submission->status); + + // Simpan jawapan. + $skop1 = \App\Models\ChecklistItem::where('formula_token', 'SKOP1')->first(); + $this->actingAs($user)->put(route('submissions.update', $submission), [ + 'answers' => [$skop1->id => ['value' => '123', 'page_reference' => '12']], + ])->assertRedirect(); + + $this->assertDatabaseHas('submission_answers', [ + 'submission_id' => $submission->id, + 'checklist_item_id' => $skop1->id, + 'value' => '123', + 'page_reference' => '12', + ]); + + // Muat naik laporan PDF. + $this->actingAs($user)->post(route('documents.store', $submission), [ + 'report' => UploadedFile::fake()->create('laporan.pdf', 500, 'application/pdf'), + ])->assertRedirect(); + + $this->assertDatabaseHas('submission_documents', [ + 'submission_id' => $submission->id, + 'version' => 1, + 'is_active' => true, + ]); + + // Hantar serahan. + $this->actingAs($user)->post(route('submissions.submit', $submission))->assertRedirect(); + + $submission->refresh(); + $this->assertEquals('baru', $submission->status); + $this->assertNotNull($submission->submitted_at); + } + + public function test_submission_cannot_be_submitted_without_pdf(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + $submission = app(SubmissionWorkflowService::class)->findOrCreateDraft($dc, $cycle); + + $this->actingAs($user)->post(route('submissions.submit', $submission)) + ->assertSessionHasErrors('document'); + + $submission->refresh(); + $this->assertEquals('draf', $submission->status); + } + + public function test_consultant_cannot_edit_locked_submission(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + $submission = Submission::create([ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $consultant->id, + 'status' => 'baru', + 'is_locked' => true, + ]); + + $this->actingAs($user)->get(route('submissions.edit', $submission))->assertForbidden(); + + $this->actingAs($user)->put(route('submissions.update', $submission), [ + 'answers' => [], + ])->assertForbidden(); + } + + public function test_consultant_cannot_access_other_consultant_data_centre(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + + // Pusat data milik perunding lain. + $otherConsultant = Consultant::factory()->create(); + $otherDc = DataCentre::factory()->create(['current_consultant_id' => $otherConsultant->id]); + + $this->actingAs($user)->get(route('data-centres.show', $otherDc))->assertForbidden(); + + // Serahan milik perunding lain. + $otherSubmission = Submission::create([ + 'data_centre_id' => $otherDc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $otherConsultant->id, + 'status' => 'baru', + ]); + $this->actingAs($user)->get(route('submissions.show', $otherSubmission))->assertForbidden(); + + // Tetapi pusat data sendiri boleh diakses. + $this->actingAs($user)->get(route('data-centres.show', $dc))->assertOk(); + } + + public function test_jpp_can_change_status_to_pembetulan_only_with_correction_details(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + $jpp = $this->makeUserWithRole('Pegawai JPP'); + + $submission = Submission::create([ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $consultant->id, + 'status' => 'dalam_tindakan', + ]); + + // Tanpa permohonan pembetulan terbuka — status tidak boleh ditukar. + $this->actingAs($jpp)->post(route('submissions.status', $submission), [ + 'status' => 'pembetulan_perunding', + ])->assertSessionHas('error'); + + $submission->refresh(); + $this->assertEquals('dalam_tindakan', $submission->status); + + // Tambah permohonan pembetulan (no m/s, lokasi, penerangan wajib). + $this->actingAs($jpp)->post(route('corrections.store', $submission), [ + 'page_reference' => '8', + 'location' => 'Seksyen A / Skop 1', + 'description' => 'Nilai Skop 1 tidak konsisten dengan lampiran.', + ])->assertRedirect(); + + // Kini status boleh ditukar. + $this->actingAs($jpp)->post(route('submissions.status', $submission), [ + 'status' => 'pembetulan_perunding', + ])->assertRedirect(); + + $submission->refresh(); + $this->assertEquals('pembetulan_perunding', $submission->status); + } + + public function test_status_selesai_requires_hardcopy_sent_date(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + $jpp = $this->makeUserWithRole('Pegawai JPP'); + + $submission = Submission::create([ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $consultant->id, + 'status' => 'dalam_tindakan', + ]); + + $this->actingAs($jpp)->post(route('submissions.status', $submission), [ + 'status' => 'selesai', + ])->assertSessionHasErrors('hardcopy_sent_date'); + + $this->actingAs($jpp)->post(route('submissions.status', $submission), [ + 'status' => 'selesai', + 'hardcopy_sent_date' => now()->toDateString(), + ])->assertRedirect(); + + $submission->refresh(); + $this->assertEquals('selesai', $submission->status); + $this->assertNotNull($submission->hardcopy_sent_date); + } + + public function test_locked_submission_can_be_unlocked_by_officer_with_reason(): void + { + [$user, $consultant, $dc, $cycle] = $this->scenario(); + $jpp = $this->makeUserWithRole('Pegawai JPP'); + + $submission = Submission::create([ + 'data_centre_id' => $dc->id, + 'reporting_cycle_id' => $cycle->id, + 'consultant_id' => $consultant->id, + 'status' => 'dalam_tindakan', + 'is_locked' => true, + ]); + + $this->actingAs($jpp)->post(route('submissions.unlock', $submission), [ + 'lock_reason' => 'Dibuka untuk pembetulan tambahan.', + ])->assertRedirect(); + + $submission->refresh(); + $this->assertFalse($submission->is_locked); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..f76d5d7 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,34 @@ +seed(RolesAndPermissionsSeeder::class); + } + + protected function makeUserWithRole(string $role, array $attributes = []): User + { + $user = User::factory()->create($attributes); + $user->assignRole($role); + + return $user; + } + + /** Cipta pengguna perunding + profil perunding yang berkait. */ + protected function makeConsultantUser(array $consultantAttributes = []): array + { + $user = $this->makeUserWithRole('Perunding'); + $consultant = Consultant::factory()->create(array_merge(['user_id' => $user->id], $consultantAttributes)); + + return [$user, $consultant]; + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php new file mode 100644 index 0000000..5773b0c --- /dev/null +++ b/tests/Unit/ExampleTest.php @@ -0,0 +1,16 @@ +assertTrue(true); + } +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..fdf6c41 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite'; +import laravel from 'laravel-vite-plugin'; + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/sass/app.scss', 'resources/js/app.js'], + refresh: true, + }), + ], + server: { + watch: { + ignored: ['**/storage/framework/views/**'], + }, + }, +});