commit 10fb30ad696c3bc6e158c733ea7389ec11debe1b Author: Saufi Date: Wed Jun 24 20:32:14 2026 +0800 first commit 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/**'], + }, + }, +});