first commit
This commit is contained in:
15
.claude/settings.json
Normal file
15
.claude/settings.json
Normal file
@@ -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)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
55
.dockerignore
Normal file
55
.dockerignore
Normal file
@@ -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
|
||||||
18
.editorconfig
Normal file
18
.editorconfig
Normal file
@@ -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
|
||||||
58
.env.docker
Normal file
58
.env.docker
Normal file
@@ -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
|
||||||
69
.env.example
Normal file
69
.env.example
Normal file
@@ -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
|
||||||
11
.gitattributes
vendored
Normal file
11
.gitattributes
vendored
Normal file
@@ -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
|
||||||
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal file
@@ -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
|
||||||
86
Dockerfile
Normal file
86
Dockerfile
Normal file
@@ -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"]
|
||||||
282
README.md
Normal file
282
README.md
Normal file
@@ -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 <repo> 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).
|
||||||
22
app/Http/Controllers/AuditLogController.php
Normal file
22
app/Http/Controllers/AuditLogController.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
class AuditLogController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Http/Controllers/Auth/LoginController.php
Normal file
69
app/Http/Controllers/Auth/LoginController.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class LoginController extends Controller
|
||||||
|
{
|
||||||
|
public function showLoginForm()
|
||||||
|
{
|
||||||
|
if (Auth::check()) {
|
||||||
|
return redirect()->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
188
app/Http/Controllers/ChecklistBuilderController.php
Normal file
188
app/Http/Controllers/ChecklistBuilderController.php
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ChecklistFormula;
|
||||||
|
use App\Models\ChecklistItem;
|
||||||
|
use App\Models\ChecklistScope;
|
||||||
|
use App\Models\ChecklistSection;
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengurus elemen bersarang templat senarai semak (seksyen, skop, item, formula).
|
||||||
|
* Semua tindakan memerlukan kebenaran 'templat.urus'.
|
||||||
|
*/
|
||||||
|
class ChecklistBuilderController extends Controller
|
||||||
|
{
|
||||||
|
// Kebenaran 'templat.urus' dikuatkuasakan pada peringkat laluan (route group).
|
||||||
|
|
||||||
|
// ---------------- Seksyen ----------------
|
||||||
|
public function storeSection(Request $request, ChecklistTemplate $template)
|
||||||
|
{
|
||||||
|
$data = $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
110
app/Http/Controllers/ChecklistTemplateController.php
Normal file
110
app/Http/Controllers/ChecklistTemplateController.php
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ChecklistTemplateController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
45
app/Http/Controllers/CommentController.php
Normal file
45
app/Http/Controllers/CommentController.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionComment;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CommentController extends Controller
|
||||||
|
{
|
||||||
|
public function store(Request $request, Submission $submission)
|
||||||
|
{
|
||||||
|
$this->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).');
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/Http/Controllers/ConsultantController.php
Normal file
132
app/Http/Controllers/ConsultantController.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ConsultantRequest;
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
class ConsultantController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
11
app/Http/Controllers/Controller.php
Normal file
11
app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
use AuthorizesRequests, ValidatesRequests;
|
||||||
|
}
|
||||||
71
app/Http/Controllers/CorrectionRequestController.php
Normal file
71
app/Http/Controllers/CorrectionRequestController.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionCorrectionRequest;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class CorrectionRequestController extends Controller
|
||||||
|
{
|
||||||
|
/** Pegawai cipta permohonan pembetulan (wajib: no muka surat, lokasi, penerangan). */
|
||||||
|
public function store(Request $request, Submission $submission)
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
118
app/Http/Controllers/DashboardController.php
Normal file
118
app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionResult;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$user = auth()->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'
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
134
app/Http/Controllers/DataCentreController.php
Normal file
134
app/Http/Controllers/DataCentreController.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\DataCentreRequest;
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Services\AssignmentService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DataCentreController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected AssignmentService $assignmentService) {}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Http/Controllers/DocumentController.php
Normal file
47
app/Http/Controllers/DocumentController.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionDocument;
|
||||||
|
use App\Services\DocumentService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class DocumentController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected DocumentService $documents) {}
|
||||||
|
|
||||||
|
/** Muat naik laporan PDF (perunding sahaja, serahan belum dikunci). */
|
||||||
|
public function store(Request $request, Submission $submission)
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Http/Controllers/ReminderController.php
Normal file
67
app/Http/Controllers/ReminderController.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class ReminderController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
59
app/Http/Controllers/ReportController.php
Normal file
59
app/Http/Controllers/ReportController.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionResult;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
88
app/Http/Controllers/ReportingCycleController.php
Normal file
88
app/Http/Controllers/ReportingCycleController.php
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ReportingCycleRequest;
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Services\ReportingCycleService;
|
||||||
|
|
||||||
|
class ReportingCycleController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected ReportingCycleService $service) {}
|
||||||
|
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
42
app/Http/Controllers/SettingController.php
Normal file
42
app/Http/Controllers/SettingController.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SettingController extends Controller
|
||||||
|
{
|
||||||
|
public function edit()
|
||||||
|
{
|
||||||
|
abort_unless(auth()->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
143
app/Http/Controllers/SubmissionController.php
Normal file
143
app/Http/Controllers/SubmissionController.php
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Services\SubmissionWorkflowService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SubmissionController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected SubmissionWorkflowService $workflow) {}
|
||||||
|
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$this->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
122
app/Http/Controllers/SubmissionReviewController.php
Normal file
122
app/Http/Controllers/SubmissionReviewController.php
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionAnswer;
|
||||||
|
use App\Services\SubmissionWorkflowService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class SubmissionReviewController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(protected SubmissionWorkflowService $workflow) {}
|
||||||
|
|
||||||
|
/** Tukar status serahan. Status 'selesai' & 'pembetulan_perunding' ada syarat tambahan. */
|
||||||
|
public function updateStatus(Request $request, Submission $submission)
|
||||||
|
{
|
||||||
|
$this->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
108
app/Http/Controllers/UserController.php
Normal file
108
app/Http/Controllers/UserController.php
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Rules\Password;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
abort_unless(auth()->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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
57
app/Http/Requests/ConsultantRequest.php
Normal file
57
app/Http/Requests/ConsultantRequest.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ConsultantRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->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'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Http/Requests/DataCentreRequest.php
Normal file
37
app/Http/Requests/DataCentreRequest.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class DataCentreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->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',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Http/Requests/ReportingCycleRequest.php
Normal file
40
app/Http/Requests/ReportingCycleRequest.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ReportingCycleRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->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)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
app/Models/AppSetting.php
Normal file
44
app/Models/AppSetting.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Support\Facades\Cache;
|
||||||
|
|
||||||
|
class AppSetting extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'key',
|
||||||
|
'value',
|
||||||
|
'type',
|
||||||
|
'group',
|
||||||
|
'label',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public static function get(string $key, $default = null)
|
||||||
|
{
|
||||||
|
$setting = Cache::remember("setting.$key", 3600, fn () => 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}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/Models/ChecklistFormula.php
Normal file
41
app/Models/ChecklistFormula.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ChecklistFormula extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'checklist_template_id',
|
||||||
|
'checklist_section_id',
|
||||||
|
'result_token',
|
||||||
|
'label',
|
||||||
|
'expression',
|
||||||
|
'unit',
|
||||||
|
'guard_div_zero',
|
||||||
|
'order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'guard_div_zero' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Models/ChecklistItem.php
Normal file
69
app/Models/ChecklistItem.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ChecklistItem extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const INPUT_TYPES = [
|
||||||
|
'number' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Models/ChecklistScope.php
Normal file
38
app/Models/ChecklistScope.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ChecklistScope extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'checklist_section_id',
|
||||||
|
'code',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'order',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function section(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ChecklistSection::class, 'checklist_section_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function items(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ChecklistItem::class)->orderBy('order');
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Models/ChecklistSection.php
Normal file
50
app/Models/ChecklistSection.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class ChecklistSection extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'checklist_template_id',
|
||||||
|
'code',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'order',
|
||||||
|
'is_formula_enabled',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_formula_enabled' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
47
app/Models/ChecklistTemplate.php
Normal file
47
app/Models/ChecklistTemplate.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class ChecklistTemplate extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'version',
|
||||||
|
'description',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_active' => '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();
|
||||||
|
}
|
||||||
|
}
|
||||||
68
app/Models/Consultant.php
Normal file
68
app/Models/Consultant.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
|
||||||
|
class Consultant extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'nama_perunding',
|
||||||
|
'no_pendaftaran_syarikat',
|
||||||
|
'alamat',
|
||||||
|
'emel',
|
||||||
|
'telefon',
|
||||||
|
'aktif',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'aktif' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Models/DataCentre.php
Normal file
69
app/Models/DataCentre.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
|
||||||
|
class DataCentre extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'nama_pusat_data',
|
||||||
|
'tajuk_permohonan',
|
||||||
|
'lokasi_pusat_data',
|
||||||
|
'alamat',
|
||||||
|
'keluasan_tapak',
|
||||||
|
'it_load',
|
||||||
|
'status',
|
||||||
|
'current_consultant_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'keluasan_tapak' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Models/DataCentreConsultantAssignment.php
Normal file
46
app/Models/DataCentreConsultantAssignment.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class DataCentreConsultantAssignment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'data_centre_id',
|
||||||
|
'consultant_id',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'reason',
|
||||||
|
'assigned_by',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'start_date' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
77
app/Models/ReportingCycle.php
Normal file
77
app/Models/ReportingCycle.php
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
|
||||||
|
class ReportingCycle extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, LogsActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'renewal_year',
|
||||||
|
'reporting_year',
|
||||||
|
'period_start',
|
||||||
|
'period_end',
|
||||||
|
'cut_off_date',
|
||||||
|
'licence_period_year',
|
||||||
|
'checklist_template_id',
|
||||||
|
'status',
|
||||||
|
'catatan',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'renewal_year' => '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);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
app/Models/Submission.php
Normal file
152
app/Models/Submission.php
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
|
||||||
|
class Submission extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
|
public const STATUSES = [
|
||||||
|
'draf' => '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';
|
||||||
|
}
|
||||||
|
}
|
||||||
38
app/Models/SubmissionAnswer.php
Normal file
38
app/Models/SubmissionAnswer.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SubmissionAnswer extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'submission_id',
|
||||||
|
'checklist_item_id',
|
||||||
|
'value',
|
||||||
|
'unit',
|
||||||
|
'page_reference',
|
||||||
|
'consultant_note',
|
||||||
|
'officer_note',
|
||||||
|
'validation_status',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function submission(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Models/SubmissionComment.php
Normal file
40
app/Models/SubmissionComment.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class SubmissionComment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory, SoftDeletes;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'submission_id',
|
||||||
|
'user_id',
|
||||||
|
'body',
|
||||||
|
'is_internal',
|
||||||
|
'attachment_path',
|
||||||
|
'attachment_name',
|
||||||
|
'deleted_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_internal' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function submission(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Submission::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
app/Models/SubmissionCorrectionRequest.php
Normal file
61
app/Models/SubmissionCorrectionRequest.php
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SubmissionCorrectionRequest extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
public const STATUSES = [
|
||||||
|
'terbuka' => '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');
|
||||||
|
}
|
||||||
|
}
|
||||||
56
app/Models/SubmissionDocument.php
Normal file
56
app/Models/SubmissionDocument.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SubmissionDocument extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'submission_id',
|
||||||
|
'original_filename',
|
||||||
|
'stored_path',
|
||||||
|
'file_hash',
|
||||||
|
'mime_type',
|
||||||
|
'size',
|
||||||
|
'version',
|
||||||
|
'is_active',
|
||||||
|
'uploaded_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_active' => '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';
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Models/SubmissionResult.php
Normal file
32
app/Models/SubmissionResult.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SubmissionResult extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'submission_id',
|
||||||
|
'result_token',
|
||||||
|
'label',
|
||||||
|
'value',
|
||||||
|
'unit',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'value' => 'decimal:4',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function submission(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Submission::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
app/Models/SubmissionStatusHistory.php
Normal file
30
app/Models/SubmissionStatusHistory.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SubmissionStatusHistory extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'submission_id',
|
||||||
|
'from_status',
|
||||||
|
'to_status',
|
||||||
|
'changed_by',
|
||||||
|
'notes',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function submission(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Submission::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function changedBy(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'changed_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Models/User.php
Normal file
72
app/Models/User.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
use Spatie\Activitylog\Support\LogOptions;
|
||||||
|
use Spatie\Activitylog\Models\Concerns\LogsActivity;
|
||||||
|
use Spatie\Permission\Traits\HasRoles;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
use HasFactory, Notifiable, HasRoles, SoftDeletes, LogsActivity;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'phone',
|
||||||
|
'jawatan',
|
||||||
|
'is_active',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => '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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Policies/ChecklistTemplatePolicy.php
Normal file
34
app/Policies/ChecklistTemplatePolicy.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ChecklistTemplatePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Policies/ConsultantPolicy.php
Normal file
39
app/Policies/ConsultantPolicy.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ConsultantPolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
46
app/Policies/DataCentrePolicy.php
Normal file
46
app/Policies/DataCentrePolicy.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class DataCentrePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
app/Policies/ReportingCyclePolicy.php
Normal file
34
app/Policies/ReportingCyclePolicy.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class ReportingCyclePolicy
|
||||||
|
{
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
return $user->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Policies/SubmissionPolicy.php
Normal file
80
app/Policies/SubmissionPolicy.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class SubmissionPolicy
|
||||||
|
{
|
||||||
|
/** Perunding hanya boleh lihat serahan miliknya; kakitangan JPP lihat semua. */
|
||||||
|
public function view(User $user, Submission $submission): bool
|
||||||
|
{
|
||||||
|
if ($user->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Providers/AppServiceProvider.php
Normal file
25
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
// Super Admin mempunyai akses penuh kepada semua kebenaran.
|
||||||
|
Gate::before(function ($user, $ability) {
|
||||||
|
return $user->hasRole('Super Admin') ? true : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
Paginator::useBootstrapFive();
|
||||||
|
}
|
||||||
|
}
|
||||||
80
app/Services/AssignmentService.php
Normal file
80
app/Services/AssignmentService.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\DataCentreConsultantAssignment;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class AssignmentService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Tugaskan perunding kepada pusat data.
|
||||||
|
*
|
||||||
|
* Peraturan: satu pusat data hanya boleh ada SATU perunding aktif pada satu masa.
|
||||||
|
* Penugasan terdahulu ditamatkan (end_date diisi, is_active=false) dan disimpan
|
||||||
|
* dalam sejarah penugasan.
|
||||||
|
*/
|
||||||
|
public function assign(DataCentre $dataCentre, Consultant $consultant, User $officer, ?string $startDate = null, ?string $reason = null): DataCentreConsultantAssignment
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($dataCentre, $consultant, $officer, $startDate, $reason) {
|
||||||
|
$start = $startDate ? Carbon::parse($startDate) : now();
|
||||||
|
|
||||||
|
// Tamatkan semua penugasan aktif sedia ada untuk pusat data ini.
|
||||||
|
$dataCentre->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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Services/DocumentService.php
Normal file
67
app/Services/DocumentService.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\SubmissionDocument;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
||||||
|
class DocumentService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Simpan laporan PDF baharu untuk serahan dengan versi & hash.
|
||||||
|
* Versi sebelumnya ditanda tidak aktif tetapi dikekalkan (sejarah versi).
|
||||||
|
* Fail disimpan pada disk 'local' (di luar direktori public).
|
||||||
|
*/
|
||||||
|
public function store(Submission $submission, UploadedFile $file, User $user): SubmissionDocument
|
||||||
|
{
|
||||||
|
return DB::transaction(function () use ($submission, $file, $user) {
|
||||||
|
$nextVersion = (int) $submission->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
204
app/Services/ExpressionEvaluator.php
Normal file
204
app/Services/ExpressionEvaluator.php
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Penilai ungkapan aritmetik yang selamat (recursive-descent parser).
|
||||||
|
*
|
||||||
|
* Menyokong: + - * / ( ), nombor perpuluhan, dan pemboleh ubah (token) yang
|
||||||
|
* dipetakan daripada array $variables. Tiada eval()/PHP code dilaksanakan,
|
||||||
|
* jadi selamat untuk formula yang dikonfigur oleh pengguna JPP.
|
||||||
|
*/
|
||||||
|
class ExpressionEvaluator
|
||||||
|
{
|
||||||
|
/** @var array<int, array{type:string, value:string}> */
|
||||||
|
protected array $tokens = [];
|
||||||
|
|
||||||
|
protected int $pos = 0;
|
||||||
|
|
||||||
|
/** @var array<string, float> */
|
||||||
|
protected array $variables = [];
|
||||||
|
|
||||||
|
protected bool $guardDivZero = false;
|
||||||
|
|
||||||
|
protected bool $divByZeroHit = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, float|int|null> $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<int, array{type:string, value:string}>
|
||||||
|
*/
|
||||||
|
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']);
|
||||||
|
}
|
||||||
|
}
|
||||||
100
app/Services/FormulaService.php
Normal file
100
app/Services/FormulaService.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use App\Models\Submission;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengira hasil formula seksyen (A, B, C, D, E ...) berdasarkan jawapan serahan.
|
||||||
|
*
|
||||||
|
* Token tersedia kepada formula:
|
||||||
|
* - formula_token bagi setiap checklist_item (cth: SKOP1, EE, RE, D4I)
|
||||||
|
* - result_token bagi setiap formula yang telah dikira sebelumnya (cth: A, B)
|
||||||
|
*
|
||||||
|
* Formula dinilai mengikut turutan `order` supaya C boleh merujuk A & B, dsb.
|
||||||
|
*/
|
||||||
|
class FormulaService
|
||||||
|
{
|
||||||
|
public function __construct(protected ExpressionEvaluator $evaluator) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kira semua hasil formula bagi satu serahan.
|
||||||
|
*
|
||||||
|
* @return array<int, array{result_token:string,label:?string,value:?float,unit:?string}>
|
||||||
|
*/
|
||||||
|
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<string, float|int|null> $variables
|
||||||
|
* @return array<int, array{result_token:string,label:?string,value:?float,unit:?string}>
|
||||||
|
*/
|
||||||
|
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'],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Services/ReportingCycleService.php
Normal file
37
app/Services/ReportingCycleService.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\AppSetting;
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class ReportingCycleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Bina atribut kitaran daripada tahun pembaharuan.
|
||||||
|
* Peraturan perniagaan:
|
||||||
|
* - reporting_year = renewal_year - 2 (cth: pembaharuan 2028 => 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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
144
app/Services/SubmissionWorkflowService.php
Normal file
144
app/Services/SubmissionWorkflowService.php
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\ChecklistTemplate;
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use App\Models\Submission;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class SubmissionWorkflowService
|
||||||
|
{
|
||||||
|
public function __construct(protected FormulaService $formulaService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dapatkan atau cipta serahan draf untuk pusat data + kitaran tertentu.
|
||||||
|
*/
|
||||||
|
public function findOrCreateDraft(DataCentre $dataCentre, ReportingCycle $cycle): Submission
|
||||||
|
{
|
||||||
|
$consultantId = $dataCentre->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
18
artisan
Normal file
18
artisan
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
|
||||||
|
define('LARAVEL_START', microtime(true));
|
||||||
|
|
||||||
|
// Register the Composer autoloader...
|
||||||
|
require __DIR__.'/vendor/autoload.php';
|
||||||
|
|
||||||
|
// Bootstrap Laravel and handle the command...
|
||||||
|
/** @var Application $app */
|
||||||
|
$app = require_once __DIR__.'/bootstrap/app.php';
|
||||||
|
|
||||||
|
$status = $app->handleCommand(new ArgvInput);
|
||||||
|
|
||||||
|
exit($status);
|
||||||
26
bootstrap/app.php
Normal file
26
bootstrap/app.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->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();
|
||||||
2
bootstrap/cache/.gitignore
vendored
Normal file
2
bootstrap/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
7
bootstrap/providers.php
Normal file
7
bootstrap/providers.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
88
composer.json
Normal file
88
composer.json
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://getcomposer.org/schema.json",
|
||||||
|
"name": "laravel/laravel",
|
||||||
|
"type": "project",
|
||||||
|
"description": "The skeleton application for the Laravel framework.",
|
||||||
|
"keywords": ["laravel", "framework"],
|
||||||
|
"license": "MIT",
|
||||||
|
"require": {
|
||||||
|
"php": "^8.3",
|
||||||
|
"laravel/framework": "^13.8",
|
||||||
|
"laravel/tinker": "^3.0",
|
||||||
|
"spatie/laravel-activitylog": "^5.0",
|
||||||
|
"spatie/laravel-permission": "^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"fakerphp/faker": "^1.23",
|
||||||
|
"laravel/pail": "^1.2.5",
|
||||||
|
"laravel/pao": "^1.0.6",
|
||||||
|
"laravel/pint": "^1.27",
|
||||||
|
"mockery/mockery": "^1.6",
|
||||||
|
"nunomaduro/collision": "^8.6",
|
||||||
|
"phpunit/phpunit": "^12.5.12"
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"App\\": "app/",
|
||||||
|
"Database\\Factories\\": "database/factories/",
|
||||||
|
"Database\\Seeders\\": "database/seeders/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"psr-4": {
|
||||||
|
"Tests\\": "tests/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"setup": [
|
||||||
|
"composer install",
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\"",
|
||||||
|
"@php artisan key:generate",
|
||||||
|
"@php artisan migrate --force",
|
||||||
|
"npm install --ignore-scripts",
|
||||||
|
"npm run build"
|
||||||
|
],
|
||||||
|
"dev": [
|
||||||
|
"Composer\\Config::disableProcessTimeout",
|
||||||
|
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1 --timeout=0\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite --kill-others"
|
||||||
|
],
|
||||||
|
"test": [
|
||||||
|
"@php artisan config:clear --ansi @no_additional_args",
|
||||||
|
"@php artisan test"
|
||||||
|
],
|
||||||
|
"post-autoload-dump": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
|
||||||
|
"@php artisan package:discover --ansi"
|
||||||
|
],
|
||||||
|
"post-update-cmd": [
|
||||||
|
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
|
||||||
|
],
|
||||||
|
"post-root-package-install": [
|
||||||
|
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
|
||||||
|
],
|
||||||
|
"post-create-project-cmd": [
|
||||||
|
"@php artisan key:generate --ansi",
|
||||||
|
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
|
||||||
|
"@php artisan migrate --graceful --ansi"
|
||||||
|
],
|
||||||
|
"pre-package-uninstall": [
|
||||||
|
"Illuminate\\Foundation\\ComposerScripts::prePackageUninstall"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"extra": {
|
||||||
|
"laravel": {
|
||||||
|
"dont-discover": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"optimize-autoloader": true,
|
||||||
|
"preferred-install": "dist",
|
||||||
|
"sort-packages": true,
|
||||||
|
"allow-plugins": {
|
||||||
|
"pestphp/pest-plugin": true,
|
||||||
|
"php-http/discovery": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"prefer-stable": true
|
||||||
|
}
|
||||||
8536
composer.lock
generated
Normal file
8536
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
73
config/activitylog.php
Normal file
73
config/activitylog.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\Activitylog\Actions\CleanActivityLogAction;
|
||||||
|
use Spatie\Activitylog\Actions\LogActivityAction;
|
||||||
|
use Spatie\Activitylog\Models\Activity;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If set to false, no activities will be saved to the database.
|
||||||
|
*/
|
||||||
|
'enabled' => 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,
|
||||||
|
],
|
||||||
|
];
|
||||||
126
config/app.php
Normal file
126
config/app.php
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Application Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is the name of your application, which will be used when the
|
||||||
|
| framework needs to place the application's name in a notification or
|
||||||
|
| other UI elements where an application name needs to be displayed.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'name' => 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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
117
config/auth.php
Normal file
117
config/auth.php
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Authentication Defaults
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default authentication "guard" and password
|
||||||
|
| reset "broker" for your application. You may change these values
|
||||||
|
| as required, but they're a perfect start for most applications.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'defaults' => [
|
||||||
|
'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),
|
||||||
|
|
||||||
|
];
|
||||||
136
config/cache.php
Normal file
136
config/cache.php
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Cache Store
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default cache store that will be used by the
|
||||||
|
| framework. This connection is utilized if another isn't explicitly
|
||||||
|
| specified when running a cache operation inside the application.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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,
|
||||||
|
|
||||||
|
];
|
||||||
184
config/database.php
Normal file
184
config/database.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use Pdo\Mysql;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Database Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify which of the database connections below you wish
|
||||||
|
| to use as your default connection for database operations. This is
|
||||||
|
| the connection which will be utilized unless another connection
|
||||||
|
| is explicitly specified when you execute a query / statement.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
80
config/filesystems.php
Normal file
80
config/filesystems.php
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Filesystem Disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Here you may specify the default filesystem disk that should be used
|
||||||
|
| by the framework. The "local" disk, as well as a variety of cloud
|
||||||
|
| based disks are available to your application for file storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
132
config/logging.php
Normal file
132
config/logging.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Monolog\Handler\NullHandler;
|
||||||
|
use Monolog\Handler\StreamHandler;
|
||||||
|
use Monolog\Handler\SyslogUdpHandler;
|
||||||
|
use Monolog\Processor\PsrLogMessageProcessor;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Log Channel
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option defines the default log channel that is utilized to write
|
||||||
|
| messages to your logs. The value provided here should match one of
|
||||||
|
| the channels present in the list of "channels" configured below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
118
config/mail.php
Normal file
118
config/mail.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Mailer
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option controls the default mailer that is used to send all email
|
||||||
|
| messages unless another mailer is explicitly specified when sending
|
||||||
|
| the message. All additional mailers can be configured within the
|
||||||
|
| "mailers" array. Examples of each type of mailer are provided.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
9
config/mbip.php
Normal file
9
config/mbip.php
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
// Had saiz muat naik fail PDF (KB). Boleh ditindih oleh tetapan dalam pangkalan data.
|
||||||
|
'max_upload_size_kb' => env('MAX_UPLOAD_SIZE_KB', 20480),
|
||||||
|
|
||||||
|
// Aktifkan penghantaran peringatan emel (perlu konfigurasi SMTP).
|
||||||
|
'reminder_email_enabled' => env('REMINDER_EMAIL_ENABLED', false),
|
||||||
|
];
|
||||||
219
config/permission.php
Normal file
219
config/permission.php
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Spatie\Permission\DefaultTeamResolver;
|
||||||
|
use Spatie\Permission\Models\Permission;
|
||||||
|
use Spatie\Permission\Models\Role;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
'models' => [
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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',
|
||||||
|
],
|
||||||
|
];
|
||||||
129
config/queue.php
Normal file
129
config/queue.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Queue Connection Name
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Laravel's queue supports a variety of backends via a single, unified
|
||||||
|
| API, giving you convenient access to each backend using identical
|
||||||
|
| syntax for each. The default queue connection is defined below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'default' => 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',
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
38
config/services.php
Normal file
38
config/services.php
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Third Party Services
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This file is for storing the credentials for third party services such
|
||||||
|
| as Mailgun, Postmark, AWS and more. This file provides the de facto
|
||||||
|
| location for this type of information, allowing packages to have
|
||||||
|
| a conventional file to locate the various service credentials.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'postmark' => [
|
||||||
|
'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'),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
233
config/session.php
Normal file
233
config/session.php
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default Session Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option determines the default session driver that is utilized for
|
||||||
|
| incoming requests. Laravel supports a variety of storage options to
|
||||||
|
| persist session data. Database storage is a great default choice.
|
||||||
|
|
|
||||||
|
| Supported: "file", "cookie", "database", "memcached",
|
||||||
|
| "redis", "dynamodb", "array"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => 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',
|
||||||
|
|
||||||
|
];
|
||||||
1
database/.gitignore
vendored
Normal file
1
database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
28
database/factories/ConsultantFactory.php
Normal file
28
database/factories/ConsultantFactory.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\Consultant;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ConsultantFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = Consultant::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nama_perunding' => 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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
database/factories/DataCentreFactory.php
Normal file
25
database/factories/DataCentreFactory.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\DataCentre;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class DataCentreFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = DataCentre::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'nama_pusat_data' => '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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
44
database/factories/ReportingCycleFactory.php
Normal file
44
database/factories/ReportingCycleFactory.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\ReportingCycle;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
|
||||||
|
class ReportingCycleFactory extends Factory
|
||||||
|
{
|
||||||
|
protected $model = ReportingCycle::class;
|
||||||
|
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
$renewalYear = fake()->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)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
database/factories/UserFactory.php
Normal file
45
database/factories/UserFactory.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends Factory<User>
|
||||||
|
*/
|
||||||
|
class UserFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The current password being used by the factory.
|
||||||
|
*/
|
||||||
|
protected static ?string $password;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
49
database/migrations/0001_01_01_000000_create_users_table.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('users', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
35
database/migrations/0001_01_01_000001_create_cache_table.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('cache', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
59
database/migrations/0001_01_01_000002_create_jobs_table.php
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('jobs', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$teams = config('permission.teams');
|
||||||
|
$tableNames = config('permission.table_names');
|
||||||
|
$columnNames = config('permission.column_names');
|
||||||
|
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
|
||||||
|
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
|
||||||
|
|
||||||
|
throw_if(empty($tableNames), 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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['permissions'], static function (Blueprint $table) {
|
||||||
|
$table->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']);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('activity_log', function (Blueprint $table) {
|
||||||
|
$table->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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('users', function (Blueprint $table) {
|
||||||
|
$table->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']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('consultants', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('data_centres', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('data_centre_consultant_assignments', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('checklist_templates', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('checklist_sections', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('checklist_scopes', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('checklist_items', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('checklist_formulas', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('reporting_cycles', function (Blueprint $table) {
|
||||||
|
$table->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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user