first
This commit is contained in:
27
docker-compose.yml
Normal file
27
docker-compose.yml
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/php-apache/Dockerfile
|
||||||
|
container_name: myansuran-app
|
||||||
|
ports:
|
||||||
|
- "8002:80"
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./src:/var/www/html
|
||||||
|
- ./docker/php/custom.ini:/usr/local/etc/php/conf.d/99-custom.ini
|
||||||
|
environment:
|
||||||
|
APACHE_DOCUMENT_ROOT: /var/www/html/public
|
||||||
|
APP_ENV: local
|
||||||
|
APP_DEBUG: "true"
|
||||||
|
REDIS_HOST: shared-redis
|
||||||
|
REDIS_PORT: 6379
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
networks:
|
||||||
|
- backend_net
|
||||||
|
|
||||||
|
networks:
|
||||||
|
backend_net:
|
||||||
|
name: backend_net
|
||||||
|
driver: bridge
|
||||||
25
docker/php-apache/Dockerfile
Normal file
25
docker/php-apache/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM php:8.3-apache
|
||||||
|
|
||||||
|
ENV APACHE_DOCUMENT_ROOT=/var/www/html/public
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
git \
|
||||||
|
unzip \
|
||||||
|
libzip-dev \
|
||||||
|
libpng-dev \
|
||||||
|
libjpeg62-turbo-dev \
|
||||||
|
libfreetype6-dev \
|
||||||
|
libicu-dev \
|
||||||
|
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
|
||||||
|
&& docker-php-ext-install pdo_mysql gd intl zip \
|
||||||
|
&& pecl install redis \
|
||||||
|
&& docker-php-ext-enable redis \
|
||||||
|
&& a2enmod rewrite headers \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
COPY docker/php-apache/vhost.conf /etc/apache2/sites-available/000-default.conf
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
RUN chown -R www-data:www-data /var/www/html
|
||||||
13
docker/php-apache/vhost.conf
Normal file
13
docker/php-apache/vhost.conf
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<VirtualHost *:80>
|
||||||
|
ServerAdmin webmaster@localhost
|
||||||
|
DocumentRoot /var/www/html/public
|
||||||
|
|
||||||
|
<Directory /var/www/html/public>
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
Options -Indexes
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||||
|
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||||
|
</VirtualHost>
|
||||||
4
docker/php/custom.ini
Normal file
4
docker/php/custom.ini
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
display_errors = On
|
||||||
|
display_startup_errors = Off
|
||||||
|
log_errors = On
|
||||||
|
error_reporting = E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED
|
||||||
10
src/.claude/settings.local.json
Normal file
10
src/.claude/settings.local.json
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(Get-ChildItem -Path \"c:\\\\Users\\\\User\\\\Aplikasi\\\\e-ansuran\\\\src\" -Force)",
|
||||||
|
"Bash(Select-Object Name, Mode)",
|
||||||
|
"Bash(Format-Table -AutoSize)",
|
||||||
|
"Bash(php artisan *)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/.editorconfig
Normal file
18
src/.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.yaml]
|
||||||
|
indent_size = 4
|
||||||
65
src/.env-lama.bak
Normal file
65
src/.env-lama.bak
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=base64:WPTBFLwpIonnG1OvemNGo1Pu1E2sLbvqLxPvJHHw+N0=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
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=jpph_najib
|
||||||
|
DB_USERNAME=root
|
||||||
|
DB_PASSWORD=1234
|
||||||
|
|
||||||
|
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}"
|
||||||
65
src/.env.example
Normal file
65
src/.env.example
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
APP_NAME=Laravel
|
||||||
|
APP_ENV=local
|
||||||
|
APP_KEY=
|
||||||
|
APP_DEBUG=true
|
||||||
|
APP_URL=http://localhost
|
||||||
|
|
||||||
|
APP_LOCALE=en
|
||||||
|
APP_FALLBACK_LOCALE=en
|
||||||
|
APP_FAKER_LOCALE=en_US
|
||||||
|
|
||||||
|
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=sqlite
|
||||||
|
# DB_HOST=127.0.0.1
|
||||||
|
# DB_PORT=3306
|
||||||
|
# DB_DATABASE=laravel
|
||||||
|
# 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}"
|
||||||
11
src/.gitattributes
vendored
Normal file
11
src/.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
|
||||||
24
src/.gitignore
vendored
Normal file
24
src/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.backup
|
||||||
|
.env.production
|
||||||
|
.phpactor.json
|
||||||
|
.phpunit.result.cache
|
||||||
|
/.fleet
|
||||||
|
/.idea
|
||||||
|
/.nova
|
||||||
|
/.phpunit.cache
|
||||||
|
/.vscode
|
||||||
|
/.zed
|
||||||
|
/auth.json
|
||||||
|
/node_modules
|
||||||
|
/public/build
|
||||||
|
/public/hot
|
||||||
|
/public/storage
|
||||||
|
/storage/*.key
|
||||||
|
/storage/pail
|
||||||
|
/vendor
|
||||||
|
Homestead.json
|
||||||
|
Homestead.yaml
|
||||||
|
Thumbs.db
|
||||||
2
src/.npmrc
Normal file
2
src/.npmrc
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ignore-scripts=true
|
||||||
|
audit=true
|
||||||
669
src/PROMPT_MYANSURAN_TERKINI.txt
Normal file
669
src/PROMPT_MYANSURAN_TERKINI.txt
Normal file
@@ -0,0 +1,669 @@
|
|||||||
|
PROMPT PENUH MYANSURAN VERSI TERKINI
|
||||||
|
|
||||||
|
Anda ialah pembangun Laravel berpengalaman. Saya mahu anda menyambung,
|
||||||
|
menyelenggara dan menambah baik sistem web lengkap bernama MyAnsuran untuk
|
||||||
|
demo KIK. Sistem ini telah dibangunkan menggunakan Laravel, Blade, Bootstrap,
|
||||||
|
jQuery dan MySQL.
|
||||||
|
|
||||||
|
Jangan hasilkan cadangan umum sahaja. Jangan hasilkan skeleton kosong.
|
||||||
|
Teruskan pembangunan berdasarkan sistem sedia ada. Jika terdapat maklumat
|
||||||
|
tidak lengkap, buat andaian munasabah, teruskan pembangunan dan catat andaian
|
||||||
|
penting secara ringkas.
|
||||||
|
|
||||||
|
Sistem perlu kekal moden, kemas, profesional dan sesuai untuk portal kerajaan
|
||||||
|
atau pihak berkuasa tempatan. Semua role perlu boleh login, dashboard boleh
|
||||||
|
dipaparkan, menu utama boleh dibuka dan flow utama boleh didemo.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
NAMA SISTEM
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
MyAnsuran
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
STACK SISTEM
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. Laravel
|
||||||
|
2. Blade
|
||||||
|
3. Bootstrap
|
||||||
|
4. jQuery
|
||||||
|
5. MySQL
|
||||||
|
6. Laravel Authentication
|
||||||
|
7. Role-Based Access Control
|
||||||
|
8. Maatwebsite Excel untuk import dan export Excel
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
ROLE SISTEM
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. Admin
|
||||||
|
2. Staff
|
||||||
|
3. Pengguna Awam
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
OBJEKTIF SISTEM
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. Pengguna awam boleh membuat permohonan bayaran cukai harta secara ansuran.
|
||||||
|
2. Pengguna awam hanya memasukkan No_Akaun_Tunggakan untuk semakan.
|
||||||
|
3. Sistem memaparkan maklumat akaun cukai harta jika akaun wujud.
|
||||||
|
4. Sistem memaparkan status terkini akaun berdasarkan permohonan ansuran.
|
||||||
|
5. Pengguna boleh memilih tempoh ansuran antara 2 hingga 6 bulan.
|
||||||
|
6. Staff menyemak permohonan dan memasukkan jadual ansuran secara manual.
|
||||||
|
7. Staff memasukkan amaun ansuran, ulasan semakan dan tindakan status.
|
||||||
|
8. Staff boleh merekod bayaran manual dengan nombor resit.
|
||||||
|
9. Staff boleh kemaskini bayaran secara pukal melalui Excel atau CSV.
|
||||||
|
10. Admin boleh mengurus pengguna, data harta, import cukai harta, laporan dan audit trail.
|
||||||
|
11. Sistem menyokong import Excel berbeza untuk Admin dan Staff.
|
||||||
|
12. Sistem menyediakan flow demo lengkap untuk KIK.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
PERATURAN UTAMA SISTEM ANSURAN
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Sistem MyAnsuran TIDAK menjana jadual ansuran secara automatik.
|
||||||
|
|
||||||
|
Sistem hanya:
|
||||||
|
1. Menyemak No_Akaun_Tunggakan.
|
||||||
|
2. Memaparkan maklumat akaun tunggakan jika wujud.
|
||||||
|
3. Membenarkan pengguna memilih tempoh ansuran 2 hingga 6 bulan.
|
||||||
|
4. Membenarkan pengguna mengisi borang permohonan ansuran.
|
||||||
|
5. Membenarkan staff menyemak permohonan.
|
||||||
|
6. Membenarkan staff memasukkan amaun ansuran secara manual.
|
||||||
|
7. Menyimpan jadual ansuran berdasarkan input staff.
|
||||||
|
8. Memaparkan jadual ansuran kepada pengguna selepas staff sahkan.
|
||||||
|
|
||||||
|
Jangan bina fungsi auto-generate jadual ansuran.
|
||||||
|
Jangan paksa sistem membahagikan tunggakan secara automatik.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
STATUS AKAUN / PERMOHONAN
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Gunakan status berikut:
|
||||||
|
|
||||||
|
1. diproses
|
||||||
|
- Permohonan baharu.
|
||||||
|
- Staff perlu membuat semakan dan proses kelulusan.
|
||||||
|
- Badge oren.
|
||||||
|
|
||||||
|
2. aktif
|
||||||
|
- Permohonan diterima.
|
||||||
|
- Akaun berada dalam tempoh bayaran ansuran.
|
||||||
|
- Jadual ansuran manual telah disahkan.
|
||||||
|
- Badge hijau.
|
||||||
|
|
||||||
|
3. selesai
|
||||||
|
- Semua bayaran ansuran telah lengkap.
|
||||||
|
- Akaun tidak lagi dianggap akaun aktif.
|
||||||
|
- Badge gelap atau putih mengikut UI.
|
||||||
|
|
||||||
|
4. gagal
|
||||||
|
- Akaun disahkan gagal oleh staff.
|
||||||
|
- Badge merah.
|
||||||
|
|
||||||
|
5. batal
|
||||||
|
- Akaun dibatalkan oleh staff.
|
||||||
|
- Badge merah.
|
||||||
|
|
||||||
|
Peraturan penting:
|
||||||
|
1. Staff hanya boleh menyemak akaun berstatus diproses dan aktif.
|
||||||
|
2. Akaun batal, gagal dan selesai tidak dipaparkan di dashboard semakan staff.
|
||||||
|
3. Akaun batal, gagal dan selesai masih wujud dalam sejarah, database, laporan dan import Excel.
|
||||||
|
4. Pengguna tidak boleh meneruskan permohonan baharu jika No_Akaun_Tunggakan sudah mempunyai akaun berstatus diproses, aktif atau selesai.
|
||||||
|
5. Pengguna boleh melihat status terkini akaun semasa semakan No_Akaun_Tunggakan.
|
||||||
|
6. Jika akaun tiada dalam database, pengguna tidak boleh meneruskan permohonan.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL PENGGUNA AWAM
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Halaman utama pengguna awam:
|
||||||
|
1. Dashboard pengguna.
|
||||||
|
2. Permohonan Saya.
|
||||||
|
3. Permohonan Baharu.
|
||||||
|
4. Profil Pengguna.
|
||||||
|
5. Notifikasi.
|
||||||
|
6. Logout.
|
||||||
|
|
||||||
|
Flow permohonan baharu adalah page-to-page / step wizard:
|
||||||
|
|
||||||
|
Langkah 1:
|
||||||
|
Semakan No_Akaun_Tunggakan
|
||||||
|
|
||||||
|
1. Pengguna hanya boleh memasukkan No_Akaun_Tunggakan.
|
||||||
|
2. Sistem mencari akaun dalam table property_accounts.
|
||||||
|
3. Jika akaun tidak wujud, paparkan mesej:
|
||||||
|
Nombor akaun tunggakan tidak dijumpai. Sila semak semula nombor akaun anda atau hubungi pihak MBIP.
|
||||||
|
4. Jika akaun wujud, sistem paparkan:
|
||||||
|
- No_Akaun
|
||||||
|
- No_Akaun_Tunggakan
|
||||||
|
- Status Akaun Terkini
|
||||||
|
- No Permohonan terkini jika ada
|
||||||
|
- Pemilik
|
||||||
|
- Tunggakan
|
||||||
|
- Taman
|
||||||
|
- Rupacara
|
||||||
|
- Kadar
|
||||||
|
5. Jika status akaun ialah diproses, aktif atau selesai, butang Seterusnya mesti disabled.
|
||||||
|
6. Jika status akaun ialah gagal atau batal, pengguna boleh meneruskan permohonan baharu.
|
||||||
|
7. Jika belum ada permohonan ansuran, pengguna boleh meneruskan permohonan.
|
||||||
|
|
||||||
|
Langkah 2:
|
||||||
|
Borang Permohonan Bayaran Cukai Harta Secara Ansuran
|
||||||
|
|
||||||
|
Field penting:
|
||||||
|
1. No_Akaun - autofill dan readonly.
|
||||||
|
2. Status Akaun Terkini - autofill dan readonly.
|
||||||
|
3. Pemilik - autofill dan readonly.
|
||||||
|
4. Rupacara - autofill dan readonly.
|
||||||
|
5. No_Lot - autofill dan readonly.
|
||||||
|
6. No_DHM - pengguna boleh isi atau kemaskini.
|
||||||
|
7. No_Syarikat - pengguna boleh isi atau kemaskini.
|
||||||
|
8. Alamat_Harta - autofill dan readonly.
|
||||||
|
9. Alamat_Harta disusun berdasarkan No_Bgnn, Nama_Jalan daripada import Excel.
|
||||||
|
10. Kategori Harta:
|
||||||
|
- Kediaman
|
||||||
|
- Industri
|
||||||
|
- Perdagangan
|
||||||
|
11. Tempoh Ansuran:
|
||||||
|
- 2 bulan
|
||||||
|
- 3 bulan
|
||||||
|
- 4 bulan
|
||||||
|
- 5 bulan
|
||||||
|
- 6 bulan
|
||||||
|
12. Identification Type:
|
||||||
|
- Army/Police ID
|
||||||
|
- MyKad
|
||||||
|
- National ID
|
||||||
|
13. Identification Number.
|
||||||
|
14. Alamat_Surat_Menyurat.
|
||||||
|
15. Mobile_No dengan kod negara.
|
||||||
|
16. Emel.
|
||||||
|
17. Alamat_Tempat_Bekerja.
|
||||||
|
18. Pekerjaan.
|
||||||
|
19. Pendapatan_Sebulan.
|
||||||
|
|
||||||
|
Langkah 3:
|
||||||
|
Teks Perakuan Pemohon
|
||||||
|
|
||||||
|
Paparkan teks perakuan:
|
||||||
|
|
||||||
|
Saya yang bertandatangan dibawah bersetuju dan berjanji akan menjelaskan
|
||||||
|
bayaran cukai harta mengikut jadual ansuran yang telah diluluskan sebagaimana
|
||||||
|
di bawah. Sekiranya saya gagal maka pihak Majlis Bandaraya Iskandar Puteri
|
||||||
|
berhak membatalkan permohonan ini dan mengambil TINDAKAN SITAAN /
|
||||||
|
UNDANG-UNDANG ke atas SAYA / PREMIS tanpa apa-apa notis lagi.
|
||||||
|
|
||||||
|
Field perakuan:
|
||||||
|
1. Nama Perakuan.
|
||||||
|
2. Jenis ID Perakuan.
|
||||||
|
3. No ID Perakuan.
|
||||||
|
4. Tarikh Perakuan.
|
||||||
|
5. Upload / capture salinan IC.
|
||||||
|
6. Tick perakuan.
|
||||||
|
|
||||||
|
Permohonan tidak boleh dihantar jika tick perakuan tidak ditanda.
|
||||||
|
|
||||||
|
Server-side juga wajib menyemak semula:
|
||||||
|
1. Akaun mesti wujud.
|
||||||
|
2. Akaun tidak boleh ada permohonan terkini berstatus diproses, aktif atau selesai.
|
||||||
|
3. Jika status blocked, sistem redirect balik dengan mesej ralat.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Menu sidebar Staff:
|
||||||
|
1. Dashboard Staff.
|
||||||
|
2. Permohonan.
|
||||||
|
3. Kemaskini Bayaran Pukal.
|
||||||
|
4. Import Excel.
|
||||||
|
|
||||||
|
Dashboard Staff:
|
||||||
|
1. Jumlah permohonan baharu.
|
||||||
|
2. Jumlah permohonan diproses.
|
||||||
|
3. Jumlah permohonan diterima.
|
||||||
|
4. Jumlah permohonan ditolak.
|
||||||
|
5. Tugasan belum selesai.
|
||||||
|
6. Senarai permohonan terkini.
|
||||||
|
|
||||||
|
Staff hanya melihat permohonan diproses dan aktif untuk semakan.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
DETAIL PERMOHONAN STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Staff perlu melihat:
|
||||||
|
|
||||||
|
Maklumat permohonan:
|
||||||
|
1. No Permohonan.
|
||||||
|
2. No_Akaun.
|
||||||
|
3. No_Akaun_Tunggakan.
|
||||||
|
4. Kategori Harta.
|
||||||
|
5. Tempoh Ansuran.
|
||||||
|
6. No_Lot.
|
||||||
|
7. No_DHM.
|
||||||
|
8. No_Syarikat.
|
||||||
|
9. Jumlah Tunggakan.
|
||||||
|
10. Cukai Harta.
|
||||||
|
11. Cukai Harta / 2.
|
||||||
|
12. Jumlah Ansuran.
|
||||||
|
13. Jumlah Jadual Semasa.
|
||||||
|
|
||||||
|
Maklumat pemohon:
|
||||||
|
1. Nama Pemohon.
|
||||||
|
2. Identification Type.
|
||||||
|
3. Identification Number.
|
||||||
|
4. No Telefon / Contact Number.
|
||||||
|
5. Emel.
|
||||||
|
6. Alamat Surat Menyurat.
|
||||||
|
7. Alamat Harta.
|
||||||
|
8. Alamat Tempat Bekerja.
|
||||||
|
9. Pekerjaan.
|
||||||
|
10. Pendapatan Sebulan.
|
||||||
|
|
||||||
|
Maklumat akaun dan sejarah:
|
||||||
|
1. Pemilik.
|
||||||
|
2. Bandar / Taman.
|
||||||
|
3. Rupacara.
|
||||||
|
4. Kadar.
|
||||||
|
5. Cukai Harta.
|
||||||
|
6. Sejarah tindakan.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
LOGIK JUMLAH ANSURAN STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Pada kad Input Manual Ansuran Oleh Staff:
|
||||||
|
|
||||||
|
1. Staff boleh edit Jumlah Tunggakan.
|
||||||
|
2. Staff boleh edit Cukai Harta / 2.
|
||||||
|
3. Sistem autofill Cukai Harta / 2 berdasarkan Cukai Harta.
|
||||||
|
4. Staff masih boleh ubah nilai Cukai Harta / 2 jika perlu.
|
||||||
|
5. Formula paparan:
|
||||||
|
Jumlah Ansuran = Jumlah Tunggakan + (Cukai Harta / 2)
|
||||||
|
6. Jika Cukai Harta / 2 berubah, Jumlah Tunggakan diselaraskan mengikut logic UI sedia ada.
|
||||||
|
7. Staff mengisi jadual ansuran dalam bentuk table.
|
||||||
|
8. Jadual ansuran mesti berdasarkan tempoh ansuran yang dipilih pengguna.
|
||||||
|
9. Sistem membandingkan jumlah jadual dengan Jumlah Ansuran.
|
||||||
|
10. Jika jumlah tidak sama, sistem beri amaran.
|
||||||
|
11. Staff boleh simpan draf.
|
||||||
|
12. Staff hanya boleh sahkan aktif jika jumlah lengkap dan sah.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
TINDAKAN STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Pilihan tindakan staff:
|
||||||
|
1. Akaun Diproses.
|
||||||
|
2. Akaun Aktif (permohonan diterima).
|
||||||
|
3. Akaun Batal.
|
||||||
|
4. Akaun Gagal.
|
||||||
|
5. Akaun Selesai.
|
||||||
|
6. Tiada Tindakan.
|
||||||
|
|
||||||
|
Peraturan:
|
||||||
|
1. Akaun Aktif mengesahkan permohonan dan jadual ansuran.
|
||||||
|
2. Akaun Batal menukar status kepada batal.
|
||||||
|
3. Akaun Gagal menukar status kepada gagal.
|
||||||
|
4. Akaun Selesai hanya dibenarkan selepas semua ansuran dibayar.
|
||||||
|
5. Tiada Tindakan tidak mengubah status tetapi menyimpan ulasan/sejarah.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
REKOD BAYARAN MANUAL STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Pada detail permohonan staff:
|
||||||
|
|
||||||
|
1. Staff boleh merekod bayaran manual kerana sistem tiada API payment.
|
||||||
|
2. Staff pilih ansuran yang belum bayar atau lewat.
|
||||||
|
3. Staff wajib isi No. Resit.
|
||||||
|
4. Staff boleh isi No. Transaksi.
|
||||||
|
5. Staff isi Amaun Bayaran.
|
||||||
|
6. Staff isi Tarikh Bayaran.
|
||||||
|
7. Staff isi Catatan.
|
||||||
|
8. Amaun bayaran mesti sama dengan amaun jadual ansuran.
|
||||||
|
9. Sistem cipta rekod dalam payments.
|
||||||
|
10. Sistem kemaskini installment_schedules kepada dibayar.
|
||||||
|
11. Sistem rekod application history dan audit trail.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL KEMASKINI BAYARAN PUKAL STAFF
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Route:
|
||||||
|
GET /staff/bulk-payments
|
||||||
|
POST /staff/bulk-payments
|
||||||
|
GET /staff/bulk-payments/template
|
||||||
|
|
||||||
|
Tujuan:
|
||||||
|
Staff boleh kemaskini bayaran banyak akaun melalui Excel/CSV tanpa buka
|
||||||
|
satu-satu detail permohonan.
|
||||||
|
|
||||||
|
Halaman ini mesti ada:
|
||||||
|
1. Kad Akaun Aktif.
|
||||||
|
2. Kad Ansuran Dalam Excel.
|
||||||
|
3. Kad Jumlah Belum Direkod.
|
||||||
|
4. Butang Download Template Excel.
|
||||||
|
5. Form upload Excel/CSV.
|
||||||
|
6. Ringkasan proses terakhir.
|
||||||
|
7. Jadual ansuran akaun aktif.
|
||||||
|
|
||||||
|
Template Excel perlu mengandungi:
|
||||||
|
1. Installment_Schedule_ID.
|
||||||
|
2. No_Akaun.
|
||||||
|
3. No_Akaun_Tunggakan.
|
||||||
|
4. No_Permohonan.
|
||||||
|
5. Nama_Pemohon.
|
||||||
|
6. Ansuran_Ke.
|
||||||
|
7. Tarikh_Jadual.
|
||||||
|
8. Amaun_Jadual.
|
||||||
|
9. Status_Bayaran.
|
||||||
|
10. No_Resit.
|
||||||
|
11. No_Transaksi.
|
||||||
|
12. Tarikh_Bayaran.
|
||||||
|
13. Amaun_Bayaran.
|
||||||
|
14. Catatan.
|
||||||
|
|
||||||
|
Peraturan penting:
|
||||||
|
1. Template memaparkan semua ansuran untuk akaun berstatus aktif.
|
||||||
|
2. Ansuran belum_bayar dan lewat dipaparkan untuk direkod sebagai bayaran baharu.
|
||||||
|
3. Ansuran dibayar turut dipaparkan dengan transaksi bayaran sedia ada.
|
||||||
|
4. Jika ansuran sudah dibayar, Excel mesti memaparkan No_Resit, No_Transaksi,
|
||||||
|
Tarikh_Bayaran, Amaun_Bayaran dan Catatan sedia ada.
|
||||||
|
5. Upload Excel boleh mencipta bayaran baharu untuk ansuran belum_bayar/lewat.
|
||||||
|
6. Upload Excel boleh mengemaskini transaksi bayaran sedia ada untuk ansuran dibayar.
|
||||||
|
7. Sistem tidak boleh cipta duplicate payment untuk ansuran yang sudah dibayar.
|
||||||
|
8. No_Transaksi yang telah digunakan oleh bayaran lain mesti disekat.
|
||||||
|
9. Setiap import perlu direkod dalam audit trail.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL ADMIN
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Menu Admin:
|
||||||
|
1. Dashboard.
|
||||||
|
2. Pengurusan Pengguna.
|
||||||
|
3. Pengurusan Peranan.
|
||||||
|
4. Permohonan.
|
||||||
|
5. Akaun Ansuran.
|
||||||
|
6. Bayaran.
|
||||||
|
7. Tugasan.
|
||||||
|
8. Notifikasi & Emel.
|
||||||
|
9. Data Harta.
|
||||||
|
10. Import Excel.
|
||||||
|
11. Tetapan Sistem.
|
||||||
|
12. Laporan.
|
||||||
|
13. Audit Trail.
|
||||||
|
|
||||||
|
Admin boleh:
|
||||||
|
1. Mengurus pengguna.
|
||||||
|
2. Mengurus peranan.
|
||||||
|
3. Melihat semua permohonan.
|
||||||
|
4. Melihat akaun ansuran.
|
||||||
|
5. Melihat semua bayaran.
|
||||||
|
6. Melihat data harta.
|
||||||
|
7. Import data cukai harta.
|
||||||
|
8. Jana laporan.
|
||||||
|
9. Semak audit trail.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL IMPORT EXCEL
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Terdapat dua jenis import:
|
||||||
|
|
||||||
|
1. Import Staff
|
||||||
|
- Untuk data tunggakan.
|
||||||
|
- Route: /staff/import-excel.
|
||||||
|
- Staff import data tunggakan berdasarkan No_Akaun_Tunggakan.
|
||||||
|
- Import staff tidak boleh overwrite cukai_harta jika fail tiada kolum Cukai_Harta.
|
||||||
|
|
||||||
|
2. Import Admin
|
||||||
|
- Untuk data cukai harta.
|
||||||
|
- Route: /admin/import-excel.
|
||||||
|
- Hanya Admin boleh import data cukai harta.
|
||||||
|
- Admin import data seperti No_Akaun, No_Bgnn, Nama_Jalan, Kadar, Cukai_Harta.
|
||||||
|
- Import admin tidak boleh overwrite No_Akaun_Tunggakan jika fail cukai harta tiada kolum tersebut.
|
||||||
|
- Import admin tidak boleh overwrite pemilik dengan Tidak dinyatakan jika data pemilik sedia ada wujud.
|
||||||
|
|
||||||
|
Kolum import tunggakan Staff:
|
||||||
|
1. No_Akaun.
|
||||||
|
2. Pemilik.
|
||||||
|
3. Warganegara.
|
||||||
|
4. Pelanggan.
|
||||||
|
5. Status.
|
||||||
|
6. Bandar.
|
||||||
|
7. Taman.
|
||||||
|
8. Rupacara.
|
||||||
|
9. Kadar.
|
||||||
|
10. Aktif.
|
||||||
|
11. Tunggakan.
|
||||||
|
12. No_Akaun_Tunggakan.
|
||||||
|
|
||||||
|
Kolum import cukai harta Admin:
|
||||||
|
1. No_Akaun.
|
||||||
|
2. No_Lot.
|
||||||
|
3. No_DHM.
|
||||||
|
4. No_Bgnn.
|
||||||
|
5. Nama_Jalan.
|
||||||
|
6. Rupacara.
|
||||||
|
7. Kadar.
|
||||||
|
8. Cukai_Harta.
|
||||||
|
|
||||||
|
Logik import:
|
||||||
|
1. Jika No_Akaun atau No_Akaun_Tunggakan wujud, update rekod.
|
||||||
|
2. Jika tidak wujud, tambah rekod baharu.
|
||||||
|
3. Untuk import staff, akaun yang tiada dalam import semasa ditanda Tiada Dalam Import Terkini.
|
||||||
|
4. Jangan padam rekod lama secara automatik.
|
||||||
|
5. Simpan import batch.
|
||||||
|
6. Papar statistik import.
|
||||||
|
7. Rekod audit trail.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
MODUL BAYARAN DUMMY JOMPAY PENGGUNA
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Pengguna boleh bayar secara dummy JomPAY.
|
||||||
|
|
||||||
|
Maklumat tetap:
|
||||||
|
Biller Code: 37317
|
||||||
|
|
||||||
|
REF 1:
|
||||||
|
No_Akaun
|
||||||
|
|
||||||
|
REF 2:
|
||||||
|
Emel atau Mobile_No
|
||||||
|
|
||||||
|
Flow:
|
||||||
|
1. Pengguna pilih ansuran belum bayar.
|
||||||
|
2. Sistem papar flow JomPAY dummy.
|
||||||
|
3. Pengguna masukkan amaun menggunakan pad nombor jQuery.
|
||||||
|
4. Pengguna tambah ke bakul.
|
||||||
|
5. Pengguna klik Bayar.
|
||||||
|
6. Sistem set status success untuk demo.
|
||||||
|
7. Sistem cipta rekod payment.
|
||||||
|
8. Sistem kemaskini jadual ansuran kepada dibayar.
|
||||||
|
9. Sistem papar resit dummy.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
PRINT BORANG PERMOHONAN
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Borang print perlu mengandungi:
|
||||||
|
1. Tajuk rasmi:
|
||||||
|
BORANG PERMOHONAN BAYARAN CUKAI HARTA SECARA ANSURAN
|
||||||
|
2. Maklumat akaun.
|
||||||
|
3. Maklumat pemohon.
|
||||||
|
4. Maklumat permohonan.
|
||||||
|
5. Jadual ansuran manual.
|
||||||
|
6. Perakuan pemohon.
|
||||||
|
7. Ulasan staff / pegawai penyemak.
|
||||||
|
8. Ruangan tandatangan.
|
||||||
|
|
||||||
|
Ulasan staff wajib dipaparkan dalam print.
|
||||||
|
Jika tiada ulasan, paparkan:
|
||||||
|
Tiada ulasan direkodkan.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
DATABASE UTAMA
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Table utama:
|
||||||
|
1. users.
|
||||||
|
2. roles.
|
||||||
|
3. role_permissions.
|
||||||
|
4. user_profiles.
|
||||||
|
5. departments atau units.
|
||||||
|
6. property_accounts.
|
||||||
|
7. import_batches.
|
||||||
|
8. installment_applications.
|
||||||
|
9. installment_schedules.
|
||||||
|
10. payments.
|
||||||
|
11. notifications.
|
||||||
|
12. email_logs.
|
||||||
|
13. tasks.
|
||||||
|
14. application_histories.
|
||||||
|
15. system_settings.
|
||||||
|
16. audit_trails.
|
||||||
|
|
||||||
|
Relationship penting:
|
||||||
|
1. User belongsTo Role.
|
||||||
|
2. User hasOne UserProfile.
|
||||||
|
3. User belongsTo Department.
|
||||||
|
4. PropertyAccount hasMany InstallmentApplications.
|
||||||
|
5. InstallmentApplication belongsTo User.
|
||||||
|
6. InstallmentApplication belongsTo PropertyAccount.
|
||||||
|
7. InstallmentApplication hasMany InstallmentSchedules.
|
||||||
|
8. InstallmentApplication hasMany Payments.
|
||||||
|
9. InstallmentApplication hasMany ApplicationHistories.
|
||||||
|
10. InstallmentApplication hasMany Tasks.
|
||||||
|
11. Payment belongsTo InstallmentApplication.
|
||||||
|
12. Payment belongsTo InstallmentSchedule.
|
||||||
|
13. Notification belongsTo User.
|
||||||
|
14. Task belongsTo User sebagai assigned staff.
|
||||||
|
15. AuditTrail belongsTo User.
|
||||||
|
16. ImportBatch belongsTo User sebagai uploaded_by.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
SERVICE CLASS PENTING
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. InstallmentManualScheduleService
|
||||||
|
- Simpan jadual ansuran manual.
|
||||||
|
- Validasi jumlah ansuran.
|
||||||
|
- Confirm jadual ansuran.
|
||||||
|
|
||||||
|
2. WorkflowStatusService
|
||||||
|
- Mengurus status diproses, aktif, batal, gagal, selesai.
|
||||||
|
- Rekod application history.
|
||||||
|
|
||||||
|
3. NotificationService
|
||||||
|
- Mencipta notifikasi sistem.
|
||||||
|
- Mencipta dummy email log.
|
||||||
|
|
||||||
|
4. PropertyAccountImportService
|
||||||
|
- Mengurus import staff dan admin.
|
||||||
|
- Menambah dan update property_accounts.
|
||||||
|
|
||||||
|
5. BulkPaymentImportService
|
||||||
|
- Menjana template Excel bayaran pukal.
|
||||||
|
- Memproses upload bayaran pukal.
|
||||||
|
- Mencipta bayaran baharu.
|
||||||
|
- Mengemaskini transaksi bayaran sedia ada untuk ansuran dibayar.
|
||||||
|
|
||||||
|
6. AuditTrailService
|
||||||
|
- Merekod tindakan penting.
|
||||||
|
|
||||||
|
7. ReportService
|
||||||
|
- Menjana ringkasan laporan admin.
|
||||||
|
|
||||||
|
8. ApplicationInformationExcelService
|
||||||
|
- Menyimpan maklumat permohonan pengguna ke Excel dalaman.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
DATA DEMO DAN SEEDER
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Seeder menyediakan:
|
||||||
|
1. Role Admin, Staff dan Pengguna Awam.
|
||||||
|
2. User demo:
|
||||||
|
- admin@myansuran.test
|
||||||
|
- staff@myansuran.test
|
||||||
|
- user@myansuran.test
|
||||||
|
3. Password demo:
|
||||||
|
- password
|
||||||
|
4. Profil pengguna awam demo.
|
||||||
|
5. System settings demo.
|
||||||
|
|
||||||
|
Nota penting:
|
||||||
|
1. Akaun property dummy telah dibersihkan daripada database local/demo sebenar.
|
||||||
|
2. Fixture akaun dummy hanya dikekalkan untuk environment testing supaya automated test mempunyai data kawalan.
|
||||||
|
3. Jangan padam data import sebenar semasa menjalankan pembaikan.
|
||||||
|
4. Elakkan migrate:fresh pada database kerja sebenar jika data import pengguna perlu dikekalkan.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
UI / UX
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
1. Gunakan Bootstrap layout kemas.
|
||||||
|
2. Gaya portal kerajaan moden.
|
||||||
|
3. Warna utama navy, biru dan hijau lembut.
|
||||||
|
4. Sidebar konsisten untuk Admin dan Staff.
|
||||||
|
5. Navbar atas untuk Pengguna Awam.
|
||||||
|
6. Dashboard mempunyai kad statistik.
|
||||||
|
7. Table perlu kemas dan responsive.
|
||||||
|
8. Badge status jelas.
|
||||||
|
9. Alert mesej berjaya/gagal jelas.
|
||||||
|
10. Halaman print mesra A4.
|
||||||
|
11. Jangan biarkan UI kosong.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
VERIFIKASI WAJIB SELEPAS PERUBAHAN
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Jalankan:
|
||||||
|
php artisan test
|
||||||
|
|
||||||
|
Jika sesuai, jalankan juga:
|
||||||
|
php -l pada fail PHP yang diubah.
|
||||||
|
|
||||||
|
Jangan jalankan migrate:fresh pada database kerja sebenar kecuali diminta,
|
||||||
|
kerana ia boleh memadam data import sebenar.
|
||||||
|
|
||||||
|
Flow yang perlu diuji:
|
||||||
|
1. Login Admin.
|
||||||
|
2. Login Staff.
|
||||||
|
3. Login Pengguna Awam.
|
||||||
|
4. Semakan No_Akaun_Tunggakan wujud.
|
||||||
|
5. Semakan No_Akaun_Tunggakan tidak wujud.
|
||||||
|
6. Semakan akaun diproses, aktif dan selesai tidak boleh teruskan.
|
||||||
|
7. Pengguna hantar permohonan baharu untuk akaun yang dibenarkan.
|
||||||
|
8. Staff semak permohonan.
|
||||||
|
9. Staff input jadual ansuran manual.
|
||||||
|
10. Staff sahkan akaun aktif.
|
||||||
|
11. Staff rekod bayaran manual.
|
||||||
|
12. Staff download template bayaran pukal.
|
||||||
|
13. Staff upload bayaran pukal untuk ansuran belum bayar.
|
||||||
|
14. Staff upload bayaran pukal untuk kemaskini transaksi ansuran dibayar.
|
||||||
|
15. Import Excel Staff untuk data tunggakan.
|
||||||
|
16. Import Excel Admin untuk data cukai harta.
|
||||||
|
17. Admin semak laporan.
|
||||||
|
18. Admin semak audit trail.
|
||||||
|
|
||||||
|
==================================================
|
||||||
|
FORMAT LAPORAN AKHIR
|
||||||
|
==================================================
|
||||||
|
|
||||||
|
Apabila selesai membuat perubahan, laporkan:
|
||||||
|
|
||||||
|
1. Modul yang dikemaskini.
|
||||||
|
2. Fail utama yang diubah.
|
||||||
|
3. Flow yang berjaya diuji.
|
||||||
|
4. Command verifikasi yang dijalankan.
|
||||||
|
5. Limitasi kecil jika ada.
|
||||||
|
6. Sebarang andaian penting.
|
||||||
|
|
||||||
|
Teruskan dengan kod sebenar. Jangan berhenti pada cadangan.
|
||||||
144
src/README.md
Normal file
144
src/README.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
# MyAnsuran
|
||||||
|
|
||||||
|
Sistem demo Laravel untuk permohonan bayaran cukai harta secara ansuran bagi pembentangan KIK. Aplikasi ini dibina menggunakan Laravel, Blade, Bootstrap, jQuery dan Maatwebsite Excel, dengan aliran penuh untuk `Admin`, `Staff`, dan `Pengguna Awam`.
|
||||||
|
|
||||||
|
## Ciri utama
|
||||||
|
|
||||||
|
- Pendaftaran dan login berasaskan role.
|
||||||
|
- Pengguna awam boleh:
|
||||||
|
- lengkapkan profil
|
||||||
|
- semak `No_Akaun_Tunggakan`
|
||||||
|
- hantar permohonan ansuran 2 hingga 6 bulan
|
||||||
|
- lihat jadual ansuran selepas staff sahkan
|
||||||
|
- buat bayaran dummy JomPAY
|
||||||
|
- Staff boleh:
|
||||||
|
- semak permohonan
|
||||||
|
- lihat maklumat lengkap pemohon
|
||||||
|
- masukkan amaun ansuran secara manual
|
||||||
|
- rekod ulasan staff
|
||||||
|
- cetak borang permohonan rasmi
|
||||||
|
- import data akaun harta melalui Excel/CSV
|
||||||
|
- Admin boleh:
|
||||||
|
- pantau dashboard penuh
|
||||||
|
- urus pengguna, peranan dan data harta
|
||||||
|
- lihat bayaran, tugasan, notifikasi, laporan dan audit trail
|
||||||
|
|
||||||
|
## Teknologi
|
||||||
|
|
||||||
|
- Laravel 13
|
||||||
|
- Blade
|
||||||
|
- Bootstrap 5
|
||||||
|
- jQuery
|
||||||
|
- SQLite untuk demo tempatan
|
||||||
|
- Struktur migrasi serasi untuk MySQL
|
||||||
|
- Maatwebsite Excel
|
||||||
|
|
||||||
|
## Setup cepat
|
||||||
|
|
||||||
|
```bash
|
||||||
|
composer install
|
||||||
|
php artisan key:generate
|
||||||
|
php artisan migrate:fresh --seed --force
|
||||||
|
php artisan serve
|
||||||
|
```
|
||||||
|
|
||||||
|
Aplikasi akan tersedia di:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Akaun demo
|
||||||
|
|
||||||
|
- Admin: `admin@myansuran.test`
|
||||||
|
- Staff: `staff@myansuran.test`
|
||||||
|
- Pengguna Awam: `user@myansuran.test`
|
||||||
|
- Kata laluan semua akaun: `password`
|
||||||
|
|
||||||
|
## Flow demo disyorkan
|
||||||
|
|
||||||
|
### 1. Pengguna awam
|
||||||
|
|
||||||
|
1. Log masuk sebagai `user@myansuran.test`
|
||||||
|
2. Buka profil dan semak maklumat pemohon
|
||||||
|
3. Pergi ke `Permohonan Baharu`
|
||||||
|
4. Semak `No_Akaun_Tunggakan`
|
||||||
|
5. Hantar permohonan ansuran
|
||||||
|
6. Lihat permohonan dan statusnya
|
||||||
|
7. Selepas staff sahkan, buka jadual ansuran
|
||||||
|
8. Klik bayaran dummy JomPAY dan simpan resit
|
||||||
|
|
||||||
|
### 2. Staff
|
||||||
|
|
||||||
|
1. Log masuk sebagai `staff@myansuran.test`
|
||||||
|
2. Buka `Permohonan`
|
||||||
|
3. Pilih permohonan untuk disemak
|
||||||
|
4. Semak seksyen `Maklumat Pemohon`
|
||||||
|
5. Masukkan amaun ansuran manual bagi setiap bulan
|
||||||
|
6. Isi `Ulasan Staff / Pegawai Penyemak`
|
||||||
|
7. Pilih tindakan `Permohonan Diterima` atau `Permohonan Ditolak`
|
||||||
|
8. Cetak borang permohonan
|
||||||
|
9. Buka modul `Import Excel` untuk demo import akaun harta
|
||||||
|
|
||||||
|
### 3. Admin
|
||||||
|
|
||||||
|
1. Log masuk sebagai `admin@myansuran.test`
|
||||||
|
2. Buka dashboard admin
|
||||||
|
3. Semak modul:
|
||||||
|
- Pengurusan Pengguna
|
||||||
|
- Pengurusan Peranan
|
||||||
|
- Permohonan
|
||||||
|
- Akaun Ansuran
|
||||||
|
- Bayaran
|
||||||
|
- Tugasan
|
||||||
|
- Notifikasi & Emel
|
||||||
|
- Data Harta
|
||||||
|
- Tetapan Sistem
|
||||||
|
- Laporan
|
||||||
|
- Audit Trail
|
||||||
|
|
||||||
|
## Data demo yang tersedia
|
||||||
|
|
||||||
|
- 10 akaun harta dengan kategori `Kediaman`, `Industri`, `Perdagangan`
|
||||||
|
- permohonan demo dengan status `diproses`, `aktif`, `gagal`, `selesai`
|
||||||
|
- jadual ansuran manual
|
||||||
|
- bayaran dummy
|
||||||
|
- notifikasi sistem
|
||||||
|
- email log dummy
|
||||||
|
- tugasan staff
|
||||||
|
- audit trail
|
||||||
|
|
||||||
|
## Verifikasi
|
||||||
|
|
||||||
|
Command utama yang telah disahkan:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php artisan migrate:fresh --seed --force
|
||||||
|
php artisan route:list
|
||||||
|
php artisan test
|
||||||
|
```
|
||||||
|
|
||||||
|
Feature tests semasa meliputi:
|
||||||
|
|
||||||
|
- login semua role
|
||||||
|
- hantar permohonan pengguna awam
|
||||||
|
- sahkan permohonan oleh staff dengan jadual manual
|
||||||
|
- validasi mismatch jumlah jadual ansuran
|
||||||
|
- print borang staff dengan maklumat pemohon dan ulasan
|
||||||
|
- pembayaran dummy JomPAY
|
||||||
|
- import data akaun melalui service
|
||||||
|
- akses modul pemantauan admin
|
||||||
|
|
||||||
|
## Limitasi semasa
|
||||||
|
|
||||||
|
- JomPAY masih dummy/mock
|
||||||
|
- email masih dummy log
|
||||||
|
- PDF sebenar belum diintegrasi, cetakan guna HTML print-friendly
|
||||||
|
- SMS/WhatsApp belum diintegrasi
|
||||||
|
|
||||||
|
## Lokasi penting
|
||||||
|
|
||||||
|
- Routes: [routes/web.php](/C:/Users/User/Desktop/KIK/kik-app/routes/web.php)
|
||||||
|
- Services: [app/Services](/C:/Users/User/Desktop/KIK/kik-app/app/Services)
|
||||||
|
- Seeders: [database/seeders/DatabaseSeeder.php](/C:/Users/User/Desktop/KIK/kik-app/database/seeders/DatabaseSeeder.php)
|
||||||
|
- Tests: [tests/Feature/MyAnsuranFlowTest.php](/C:/Users/User/Desktop/KIK/kik-app/tests/Feature/MyAnsuranFlowTest.php)
|
||||||
41
src/app/Enums/ApplicationStatus.php
Normal file
41
src/app/Enums/ApplicationStatus.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum ApplicationStatus: string
|
||||||
|
{
|
||||||
|
case PROCESSING = 'diproses';
|
||||||
|
case ACTIVE = 'aktif';
|
||||||
|
case FAILED = 'gagal';
|
||||||
|
case CANCELLED = 'batal';
|
||||||
|
case COMPLETED = 'selesai';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::PROCESSING => 'Diproses',
|
||||||
|
self::ACTIVE => 'Aktif',
|
||||||
|
self::FAILED => 'Gagal',
|
||||||
|
self::CANCELLED => 'Batal',
|
||||||
|
self::COMPLETED => 'Selesai',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function badgeClass(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::PROCESSING => 'warning',
|
||||||
|
self::ACTIVE => 'success',
|
||||||
|
self::FAILED, self::CANCELLED => 'danger',
|
||||||
|
self::COMPLETED => 'dark',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function reviewableValues(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
self::PROCESSING->value,
|
||||||
|
self::ACTIVE->value,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/app/Enums/IdentificationType.php
Normal file
10
src/app/Enums/IdentificationType.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum IdentificationType: string
|
||||||
|
{
|
||||||
|
case ARMY_POLICE = 'Army/Police ID';
|
||||||
|
case MYKAD = 'MyKad';
|
||||||
|
case NATIONAL_ID = 'National ID';
|
||||||
|
}
|
||||||
10
src/app/Enums/PaymentStatus.php
Normal file
10
src/app/Enums/PaymentStatus.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PaymentStatus: string
|
||||||
|
{
|
||||||
|
case PENDING = 'pending';
|
||||||
|
case SUCCESS = 'success';
|
||||||
|
case FAILED = 'failed';
|
||||||
|
}
|
||||||
18
src/app/Enums/PropertyCategory.php
Normal file
18
src/app/Enums/PropertyCategory.php
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum PropertyCategory: string
|
||||||
|
{
|
||||||
|
case RESIDENTIAL = 'Kediaman';
|
||||||
|
case INDUSTRIAL = 'Industri';
|
||||||
|
case COMMERCIAL = 'Perdagangan';
|
||||||
|
|
||||||
|
public function guidanceRate(): int
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::RESIDENTIAL => 30,
|
||||||
|
self::INDUSTRIAL, self::COMMERCIAL => 50,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/app/Enums/SchedulePaymentStatus.php
Normal file
11
src/app/Enums/SchedulePaymentStatus.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum SchedulePaymentStatus: string
|
||||||
|
{
|
||||||
|
case UNPAID = 'belum_bayar';
|
||||||
|
case PAID = 'dibayar';
|
||||||
|
case LATE = 'lewat';
|
||||||
|
case COMPLETED = 'selesai';
|
||||||
|
}
|
||||||
10
src/app/Enums/TaskStatus.php
Normal file
10
src/app/Enums/TaskStatus.php
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum TaskStatus: string
|
||||||
|
{
|
||||||
|
case OPEN = 'belum_selesai';
|
||||||
|
case COMPLETED = 'selesai';
|
||||||
|
case OVERDUE = 'tertunggak';
|
||||||
|
}
|
||||||
28
src/app/Enums/UserRole.php
Normal file
28
src/app/Enums/UserRole.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Enums;
|
||||||
|
|
||||||
|
enum UserRole: string
|
||||||
|
{
|
||||||
|
case ADMIN = 'admin';
|
||||||
|
case STAFF = 'staff';
|
||||||
|
case PUBLIC = 'public';
|
||||||
|
|
||||||
|
public function label(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::ADMIN => 'Admin',
|
||||||
|
self::STAFF => 'Staff',
|
||||||
|
self::PUBLIC => 'Pengguna Awam',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboardRoute(): string
|
||||||
|
{
|
||||||
|
return match ($this) {
|
||||||
|
self::ADMIN => 'admin.dashboard',
|
||||||
|
self::STAFF => 'staff.dashboard',
|
||||||
|
self::PUBLIC => 'portal.dashboard',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/Exports/SimpleArrayExport.php
Normal file
26
src/app/Exports/SimpleArrayExport.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exports;
|
||||||
|
|
||||||
|
use Maatwebsite\Excel\Concerns\FromArray;
|
||||||
|
use Maatwebsite\Excel\Concerns\ShouldAutoSize;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||||
|
|
||||||
|
class SimpleArrayExport implements FromArray, WithHeadings, ShouldAutoSize
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected array $headings,
|
||||||
|
protected array $rows,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function array(): array
|
||||||
|
{
|
||||||
|
return $this->rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function headings(): array
|
||||||
|
{
|
||||||
|
return $this->headings;
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/app/Http/Controllers/Admin/ApplicationController.php
Normal file
73
src/app/Http/Controllers/Admin/ApplicationController.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Exports\SimpleArrayExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ApplicationController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$applications = InstallmentApplication::with('user.profile', 'propertyAccount', 'reviewer')
|
||||||
|
->when($request->filled('status'), fn ($query) => $query->where('status', $request->string('status')))
|
||||||
|
->when($request->filled('search'), function ($query) use ($request) {
|
||||||
|
$search = $request->string('search');
|
||||||
|
$query->where(function ($query) use ($search) {
|
||||||
|
$query->where('application_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('no_akaun_tunggakan', 'like', "%{$search}%")
|
||||||
|
->orWhere('status', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('user.profile', fn ($userQuery) => $userQuery->where('public_name', 'like', "%{$search}%"))
|
||||||
|
->orWhereHas('propertyAccount', function ($propertyQuery) use ($search) {
|
||||||
|
$propertyQuery->where('bandar', 'like', "%{$search}%")
|
||||||
|
->orWhere('taman', 'like', "%{$search}%")
|
||||||
|
->orWhere('pemilik', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('date_from'), fn ($query) => $query->whereDate('created_at', '>=', $request->date('date_from')))
|
||||||
|
->when($request->filled('date_to'), fn ($query) => $query->whereDate('created_at', '<=', $request->date('date_to')))
|
||||||
|
->latest()
|
||||||
|
->paginate(12);
|
||||||
|
|
||||||
|
return view('admin.applications-index', compact('applications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(InstallmentApplication $application): View
|
||||||
|
{
|
||||||
|
$application->load('user.profile', 'propertyAccount', 'schedules', 'payments.schedule', 'payments.user.profile', 'histories.user', 'reviewer', 'assignedStaff');
|
||||||
|
|
||||||
|
return view('admin.application-show', compact('application'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print(InstallmentApplication $application): View
|
||||||
|
{
|
||||||
|
$application->load('user.profile', 'propertyAccount', 'schedules', 'payments.schedule', 'payments.user.profile', 'reviewer');
|
||||||
|
|
||||||
|
return view('print.application', compact('application'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export()
|
||||||
|
{
|
||||||
|
$rows = InstallmentApplication::with('user.profile')->get()->map(fn ($application) => [
|
||||||
|
$application->application_no,
|
||||||
|
$application->user->displayName(),
|
||||||
|
$application->no_akaun_tunggakan,
|
||||||
|
$application->category,
|
||||||
|
$application->installment_months,
|
||||||
|
$application->status,
|
||||||
|
])->toArray();
|
||||||
|
|
||||||
|
return Excel::download(
|
||||||
|
new SimpleArrayExport(
|
||||||
|
['No Permohonan', 'Pemohon', 'No Akaun Tunggakan', 'Kategori', 'Tempoh', 'Status'],
|
||||||
|
$rows
|
||||||
|
),
|
||||||
|
'permohonan-admin.xlsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/Http/Controllers/Admin/AuditTrailController.php
Normal file
17
src/app/Http/Controllers/Admin/AuditTrailController.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AuditTrail;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuditTrailController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.audit-trail', [
|
||||||
|
'audits' => AuditTrail::with('user', 'application')->latest()->paginate(20),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/Http/Controllers/Admin/DashboardController.php
Normal file
26
src/app/Http/Controllers/Admin/DashboardController.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\SystemNotification;
|
||||||
|
use App\Services\ReportService;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected ReportService $reportService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.dashboard', [
|
||||||
|
'summary' => $this->reportService->dashboardSummary(),
|
||||||
|
'latestApplications' => InstallmentApplication::with('user.profile', 'propertyAccount')->latest()->take(8)->get(),
|
||||||
|
'latestNotifications' => SystemNotification::with('user')->latest()->take(6)->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
51
src/app/Http/Controllers/Admin/ModuleController.php
Normal file
51
src/app/Http/Controllers/Admin/ModuleController.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\EmailLog;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\SystemNotification;
|
||||||
|
use App\Models\Task;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ModuleController extends Controller
|
||||||
|
{
|
||||||
|
public function roles(): View
|
||||||
|
{
|
||||||
|
return view('admin.roles-index', [
|
||||||
|
'roles' => Role::with('permissions')->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function installmentAccounts(): View
|
||||||
|
{
|
||||||
|
return view('admin.installment-accounts-index', [
|
||||||
|
'applications' => InstallmentApplication::with('user.profile', 'propertyAccount', 'schedules', 'payments')->where('status', 'aktif')->latest()->paginate(12),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payments(): View
|
||||||
|
{
|
||||||
|
return view('admin.payments-index', [
|
||||||
|
'payments' => Payment::with('user.profile', 'application.propertyAccount', 'schedule')->latest()->paginate(15),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tasks(): View
|
||||||
|
{
|
||||||
|
return view('admin.tasks-index', [
|
||||||
|
'tasks' => Task::with('application', 'assignee', 'assigner')->latest()->paginate(15),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function communications(): View
|
||||||
|
{
|
||||||
|
return view('admin.communications-index', [
|
||||||
|
'notifications' => SystemNotification::with('user')->latest()->take(20)->get(),
|
||||||
|
'emailLogs' => EmailLog::with('user', 'application')->latest()->take(20)->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
src/app/Http/Controllers/Admin/PropertyAccountController.php
Normal file
62
src/app/Http/Controllers/Admin/PropertyAccountController.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StorePropertyAccountRequest;
|
||||||
|
use App\Models\PropertyAccount;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PropertyAccountController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$accounts = PropertyAccount::query()
|
||||||
|
->when($request->filled('search'), function ($query) use ($request) {
|
||||||
|
$search = $request->string('search');
|
||||||
|
$query->where('no_akaun', 'like', "%{$search}%")
|
||||||
|
->orWhere('no_akaun_tunggakan', 'like', "%{$search}%")
|
||||||
|
->orWhere('no_bgnn', 'like', "%{$search}%")
|
||||||
|
->orWhere('nama_jalan', 'like', "%{$search}%")
|
||||||
|
->orWhere('pemilik', 'like', "%{$search}%")
|
||||||
|
->orWhere('bandar', 'like', "%{$search}%");
|
||||||
|
})
|
||||||
|
->latest()
|
||||||
|
->paginate(15);
|
||||||
|
|
||||||
|
return view('admin.property-accounts-index', compact('accounts'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('admin.property-account-form', ['account' => new PropertyAccount()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StorePropertyAccountRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
PropertyAccount::create($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('admin.property-accounts.index')->with('success', 'Data harta berjaya ditambah.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(PropertyAccount $propertyAccount): View
|
||||||
|
{
|
||||||
|
return view('admin.property-account-form', ['account' => $propertyAccount]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(StorePropertyAccountRequest $request, PropertyAccount $propertyAccount): RedirectResponse
|
||||||
|
{
|
||||||
|
$propertyAccount->update($request->validated());
|
||||||
|
|
||||||
|
return redirect()->route('admin.property-accounts.index')->with('success', 'Data harta berjaya dikemaskini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(PropertyAccount $propertyAccount): RedirectResponse
|
||||||
|
{
|
||||||
|
$propertyAccount->delete();
|
||||||
|
|
||||||
|
return back()->with('success', 'Data harta berjaya dipadam.');
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/app/Http/Controllers/Admin/ReportController.php
Normal file
52
src/app/Http/Controllers/Admin/ReportController.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Exports\SimpleArrayExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\ReportService;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ReportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected ReportService $reportService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.reports', [
|
||||||
|
'summary' => $this->reportService->dashboardSummary(),
|
||||||
|
'monthlyApplications' => $this->reportService->monthlyApplications(),
|
||||||
|
'monthlyPayments' => $this->reportService->monthlyPayments(),
|
||||||
|
'categoryBreakdown' => $this->reportService->categoryBreakdown(),
|
||||||
|
'areaBreakdown' => $this->reportService->areaBreakdown(),
|
||||||
|
'staffPerformance' => $this->reportService->staffPerformance(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportApplications()
|
||||||
|
{
|
||||||
|
$rows = $this->reportService->monthlyApplications()->map(fn ($row) => [$row->month, $row->total])->toArray();
|
||||||
|
|
||||||
|
return Excel::download(new SimpleArrayExport(['Bulan', 'Jumlah Permohonan'], $rows), 'laporan-permohonan.xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function exportPayments()
|
||||||
|
{
|
||||||
|
$rows = $this->reportService->monthlyPayments()->map(fn ($row) => [$row->month, $row->total])->toArray();
|
||||||
|
|
||||||
|
return Excel::download(new SimpleArrayExport(['Bulan', 'Jumlah Bayaran'], $rows), 'laporan-bayaran.xlsx');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print(): View
|
||||||
|
{
|
||||||
|
return view('exports.report-print', [
|
||||||
|
'summary' => $this->reportService->dashboardSummary(),
|
||||||
|
'monthlyApplications' => $this->reportService->monthlyApplications(),
|
||||||
|
'monthlyPayments' => $this->reportService->monthlyPayments(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/Http/Controllers/Admin/SettingController.php
Normal file
28
src/app/Http/Controllers/Admin/SettingController.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\UpdateSystemSettingRequest;
|
||||||
|
use App\Models\SystemSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class SettingController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.settings', [
|
||||||
|
'settings' => SystemSetting::orderBy('group')->orderBy('label')->get()->groupBy('group'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateSystemSettingRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
foreach ($request->input('settings', []) as $key => $value) {
|
||||||
|
SystemSetting::where('key', $key)->update(['value' => $value]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', 'Tetapan sistem berjaya dikemaskini.');
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/app/Http/Controllers/Admin/UserController.php
Normal file
90
src/app/Http/Controllers/Admin/UserController.php
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\ManageUserRequest;
|
||||||
|
use App\Models\Department;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\AuditTrailService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class UserController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
return view('admin.users-index', [
|
||||||
|
'users' => User::with('role', 'department', 'profile')->latest()->paginate(12),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
return view('admin.user-form', [
|
||||||
|
'user' => new User(),
|
||||||
|
'roles' => Role::all(),
|
||||||
|
'departments' => Department::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(ManageUserRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = User::create([
|
||||||
|
...$request->safe()->except('password', 'is_active'),
|
||||||
|
'password' => Hash::make($request->string('password')),
|
||||||
|
'is_active' => $request->boolean('is_active', true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->auditTrailService->log('Pengurusan Pengguna', 'tambah_pengguna', 'Admin menambah pengguna baharu.', $request->user(), [], $user->toArray());
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.index')->with('success', 'Pengguna baharu berjaya ditambah.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit(User $user): View
|
||||||
|
{
|
||||||
|
return view('admin.user-form', [
|
||||||
|
'user' => $user,
|
||||||
|
'roles' => Role::all(),
|
||||||
|
'departments' => Department::all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(ManageUserRequest $request, User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
$old = $user->toArray();
|
||||||
|
$payload = $request->safe()->except('password', 'is_active');
|
||||||
|
$payload['is_active'] = $request->boolean('is_active');
|
||||||
|
|
||||||
|
if ($request->filled('password')) {
|
||||||
|
$payload['password'] = Hash::make($request->string('password'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->update($payload);
|
||||||
|
|
||||||
|
$this->auditTrailService->log('Pengurusan Pengguna', 'kemaskini_pengguna', 'Admin mengemaskini maklumat pengguna.', $request->user(), $old, $user->fresh()->toArray());
|
||||||
|
|
||||||
|
return redirect()->route('admin.users.index')->with('success', 'Maklumat pengguna berjaya dikemaskini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resetPassword(User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
$user->update(['password' => Hash::make('password')]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Kata laluan pengguna telah ditetapkan semula kepada "password".');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toggle(User $user): RedirectResponse
|
||||||
|
{
|
||||||
|
$user->update(['is_active' => ! $user->is_active]);
|
||||||
|
|
||||||
|
return back()->with('success', 'Status akaun pengguna berjaya dikemaskini.');
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/app/Http/Controllers/Auth/AuthController.php
Normal file
114
src/app/Http/Controllers/Auth/AuthController.php
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Auth;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\LoginRequest;
|
||||||
|
use App\Http\Requests\RegisterRequest;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\AuditTrailService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\Auth;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class AuthController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showLogin(): View
|
||||||
|
{
|
||||||
|
return view('auth.login');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function showRegister(): View
|
||||||
|
{
|
||||||
|
return view('auth.register');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function register(RegisterRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$publicRole = Role::where('slug', UserRole::PUBLIC->value)->firstOrFail();
|
||||||
|
$email = $request->string('email')->toString();
|
||||||
|
|
||||||
|
$user = User::create([
|
||||||
|
'name' => str($email)->before('@')->headline()->toString(),
|
||||||
|
'email' => $email,
|
||||||
|
'password' => Hash::make($request->string('password')->toString()),
|
||||||
|
'role_id' => $publicRole->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Auth::login($user);
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Authentication',
|
||||||
|
'register',
|
||||||
|
'Pengguna awam mendaftar akaun baharu.',
|
||||||
|
$user,
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('portal.profile.edit')
|
||||||
|
->with('success', 'Pendaftaran berjaya. Sila lengkapkan profil anda.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function login(LoginRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$matchedUser = User::where('email', $request->string('email'))->first();
|
||||||
|
|
||||||
|
if ($matchedUser && ! $matchedUser->is_active) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['email' => 'Akaun ini telah dinyahaktifkan. Sila hubungi pentadbir sistem.'])
|
||||||
|
->onlyInput('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
$credentials = $request->safe()->only('email', 'password');
|
||||||
|
|
||||||
|
if (! Auth::attempt($credentials, $request->boolean('remember'))) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['email' => 'Emel atau kata laluan tidak sah.'])
|
||||||
|
->onlyInput('email');
|
||||||
|
}
|
||||||
|
|
||||||
|
$request->session()->regenerate();
|
||||||
|
|
||||||
|
$user = $request->user();
|
||||||
|
$user->forceFill(['last_login_at' => now()])->save();
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Authentication',
|
||||||
|
'login',
|
||||||
|
'Pengguna berjaya log masuk ke sistem.',
|
||||||
|
$user,
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()->route($user->role->slug === UserRole::ADMIN->value
|
||||||
|
? 'admin.dashboard'
|
||||||
|
: ($user->role->slug === UserRole::STAFF->value ? 'staff.dashboard' : 'portal.dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logout(): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = request()->user();
|
||||||
|
|
||||||
|
if ($user) {
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Authentication',
|
||||||
|
'logout',
|
||||||
|
'Pengguna log keluar daripada sistem.',
|
||||||
|
$user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Auth::logout();
|
||||||
|
request()->session()->invalidate();
|
||||||
|
request()->session()->regenerateToken();
|
||||||
|
|
||||||
|
return redirect()->route('login')->with('success', 'Anda telah berjaya log keluar.');
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/app/Http/Controllers/Controller.php
Normal file
8
src/app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
abstract class Controller
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
245
src/app/Http/Controllers/Portal/ApplicationController.php
Normal file
245
src/app/Http/Controllers/Portal/ApplicationController.php
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Portal;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use App\Enums\PropertyCategory;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\LookupPropertyAccountRequest;
|
||||||
|
use App\Http\Requests\StoreInstallmentApplicationRequest;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\PropertyAccount;
|
||||||
|
use App\Models\Task;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\ApplicationInformationExcelService;
|
||||||
|
use App\Services\AuditTrailService;
|
||||||
|
use App\Services\NotificationService;
|
||||||
|
use App\Services\WorkflowStatusService;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ApplicationController extends Controller
|
||||||
|
{
|
||||||
|
protected const BLOCKED_REAPPLICATION_STATUSES = [
|
||||||
|
'diproses',
|
||||||
|
'aktif',
|
||||||
|
'selesai',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected WorkflowStatusService $workflowStatusService,
|
||||||
|
protected NotificationService $notificationService,
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
protected ApplicationInformationExcelService $applicationInformationExcelService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$applications = auth()->user()
|
||||||
|
->applications()
|
||||||
|
->with('propertyAccount', 'schedules', 'payments')
|
||||||
|
->latest()
|
||||||
|
->paginate(10);
|
||||||
|
|
||||||
|
return view('portal.applications-index', compact('applications'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(): View
|
||||||
|
{
|
||||||
|
$oldAccount = old('property_account_id') ? PropertyAccount::find(old('property_account_id')) : null;
|
||||||
|
$oldApplication = $oldAccount ? $this->latestAccountApplication($oldAccount) : null;
|
||||||
|
|
||||||
|
return view('portal.application-form', [
|
||||||
|
'categories' => PropertyCategory::cases(),
|
||||||
|
'months' => range(2, 6),
|
||||||
|
'oldAccount' => $oldAccount,
|
||||||
|
'oldAccountStatus' => $this->accountStatusPayload($oldApplication),
|
||||||
|
'oldPropertyAddress' => $oldAccount ? $this->formattedPropertyAddress($oldAccount) : null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function lookup(LookupPropertyAccountRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$accountNumber = trim($request->string('no_akaun_tunggakan')->toString());
|
||||||
|
|
||||||
|
$account = PropertyAccount::query()
|
||||||
|
->where('no_akaun_tunggakan', $accountNumber)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $account) {
|
||||||
|
return response()->json([
|
||||||
|
'message' => 'Nombor akaun tunggakan tidak dijumpai. Sila semak semula nombor akaun anda atau hubungi pihak MBIP.',
|
||||||
|
], 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$latestApplication = $this->latestAccountApplication($account);
|
||||||
|
$accountStatus = $this->accountStatusPayload($latestApplication);
|
||||||
|
|
||||||
|
return response()->json([
|
||||||
|
'id' => $account->id,
|
||||||
|
'no_akaun' => $account->no_akaun,
|
||||||
|
'no_akaun_tunggakan' => $account->no_akaun_tunggakan,
|
||||||
|
'account_status' => $accountStatus,
|
||||||
|
'can_continue' => ! $accountStatus['is_blocked'],
|
||||||
|
'block_message' => $accountStatus['block_message'],
|
||||||
|
'pemilik' => $account->pemilik,
|
||||||
|
'taman' => $account->taman,
|
||||||
|
'rupacara' => $account->rupacara,
|
||||||
|
'kadar' => $account->kadar,
|
||||||
|
'tunggakan' => number_format((float) $account->tunggakan, 2),
|
||||||
|
'no_lot' => $account->no_lot,
|
||||||
|
'no_dhm' => $account->no_dhm,
|
||||||
|
'no_bgnn' => $account->no_bgnn,
|
||||||
|
'nama_jalan' => $account->nama_jalan,
|
||||||
|
'no_syarikat' => $account->no_syarikat,
|
||||||
|
'alamat_harta' => $this->formattedPropertyAddress($account),
|
||||||
|
'bandar' => $account->bandar,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreInstallmentApplicationRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user()->loadMissing('profile');
|
||||||
|
$account = PropertyAccount::findOrFail($request->integer('property_account_id'));
|
||||||
|
$latestApplication = $this->latestAccountApplication($account);
|
||||||
|
$accountStatus = $this->accountStatusPayload($latestApplication);
|
||||||
|
|
||||||
|
if ($accountStatus['is_blocked']) {
|
||||||
|
return back()
|
||||||
|
->withErrors(['no_akaun_tunggakan' => $accountStatus['block_message']])
|
||||||
|
->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignedStaff = User::whereHas('role', fn ($query) => $query->where('slug', 'staff'))->first();
|
||||||
|
|
||||||
|
$payload = $request->safe()->except('ic_attachment');
|
||||||
|
$payload['application_no'] = 'MYA-'.now()->format('Ymd').'-'.str_pad((string) (InstallmentApplication::count() + 1), 4, '0', STR_PAD_LEFT);
|
||||||
|
$payload['user_id'] = $user->id;
|
||||||
|
$payload['property_account_id'] = $account->id;
|
||||||
|
$payload['assigned_staff_id'] = $assignedStaff?->id;
|
||||||
|
$payload['no_akaun'] = $account->no_akaun;
|
||||||
|
$payload['total_arrears'] = $account->tunggakan;
|
||||||
|
$payload['no_lot'] = $account->no_lot;
|
||||||
|
$payload['no_dhm'] = $request->filled('no_dhm') ? $request->string('no_dhm')->trim()->toString() : $account->no_dhm;
|
||||||
|
$payload['alamat_harta'] = $this->formattedPropertyAddress($account);
|
||||||
|
$payload['no_syarikat'] = $request->filled('no_syarikat') ? $request->string('no_syarikat')->trim()->toString() : $account->no_syarikat;
|
||||||
|
|
||||||
|
if ($request->hasFile('ic_attachment')) {
|
||||||
|
$payload['ic_attachment_path'] = $request->file('ic_attachment')->store('ic-attachments');
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($payload, $user, $account, $assignedStaff): void {
|
||||||
|
$application = InstallmentApplication::create($payload);
|
||||||
|
|
||||||
|
Task::create([
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'assigned_to' => $assignedStaff?->id,
|
||||||
|
'assigned_by' => $user->id,
|
||||||
|
'title' => 'Semak permohonan '.$application->application_no,
|
||||||
|
'description' => 'Permohonan baharu memerlukan semakan pegawai.',
|
||||||
|
'priority' => 'tinggi',
|
||||||
|
'status' => 'belum_selesai',
|
||||||
|
'due_at' => now()->addDays(2),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->workflowStatusService->recordHistory(
|
||||||
|
$application,
|
||||||
|
$user,
|
||||||
|
'permohonan_dihantar',
|
||||||
|
null,
|
||||||
|
$application->status,
|
||||||
|
'Permohonan ansuran dihantar oleh pengguna awam.',
|
||||||
|
['property_account' => $account->no_akaun_tunggakan],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationService->createNotification(
|
||||||
|
$user,
|
||||||
|
'Permohonan diterima',
|
||||||
|
'Permohonan '.$application->application_no.' telah berjaya dihantar dan sedang diproses.',
|
||||||
|
route('portal.applications.show', $application),
|
||||||
|
'application',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Permohonan Ansuran',
|
||||||
|
'hantar_permohonan',
|
||||||
|
'Pengguna menghantar permohonan bayaran ansuran.',
|
||||||
|
$user,
|
||||||
|
[],
|
||||||
|
$application->toArray(),
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->applicationInformationExcelService->sync();
|
||||||
|
|
||||||
|
return redirect()->route('portal.applications.index')->with('success', 'Permohonan bayaran ansuran berjaya dihantar.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function formattedPropertyAddress(PropertyAccount $account): string
|
||||||
|
{
|
||||||
|
$importedAddress = collect([$account->no_bgnn, $account->nama_jalan])
|
||||||
|
->filter(fn ($value) => filled($value))
|
||||||
|
->implode(', ');
|
||||||
|
|
||||||
|
$areaAddress = collect([$account->taman, $account->bandar])
|
||||||
|
->filter(fn ($value) => filled($value))
|
||||||
|
->implode(', ');
|
||||||
|
|
||||||
|
return $importedAddress ?: ((string) $account->alamat_harta ?: $areaAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function latestAccountApplication(PropertyAccount $account): ?InstallmentApplication
|
||||||
|
{
|
||||||
|
return InstallmentApplication::query()
|
||||||
|
->where(function ($query) use ($account) {
|
||||||
|
$query->where('property_account_id', $account->id)
|
||||||
|
->orWhere('no_akaun', $account->no_akaun)
|
||||||
|
->orWhere('no_akaun_tunggakan', $account->no_akaun_tunggakan);
|
||||||
|
})
|
||||||
|
->latest('updated_at')
|
||||||
|
->latest('id')
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function accountStatusPayload(?InstallmentApplication $application): array
|
||||||
|
{
|
||||||
|
if (! $application) {
|
||||||
|
return [
|
||||||
|
'value' => null,
|
||||||
|
'label' => 'Belum Ada Permohonan Ansuran',
|
||||||
|
'badge_class' => 'secondary',
|
||||||
|
'application_no' => null,
|
||||||
|
'is_blocked' => false,
|
||||||
|
'block_message' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = ApplicationStatus::tryFrom($application->status);
|
||||||
|
$isBlocked = in_array($application->status, self::BLOCKED_REAPPLICATION_STATUSES, true);
|
||||||
|
$label = $status ? 'Akaun '.$status->label() : str($application->status)->headline()->toString();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'value' => $application->status,
|
||||||
|
'label' => $label,
|
||||||
|
'badge_class' => $status?->badgeClass() ?? 'secondary',
|
||||||
|
'application_no' => $application->application_no,
|
||||||
|
'is_blocked' => $isBlocked,
|
||||||
|
'block_message' => $isBlocked
|
||||||
|
? 'Permohonan baharu tidak boleh diteruskan kerana No_Akaun ini sedang berstatus '.$label.'.'
|
||||||
|
: null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(InstallmentApplication $application): View
|
||||||
|
{
|
||||||
|
abort_unless($application->user_id === auth()->id(), 403);
|
||||||
|
|
||||||
|
$application->load('propertyAccount', 'schedules', 'payments', 'histories.user', 'reviewer', 'assignedStaff');
|
||||||
|
|
||||||
|
return view('portal.application-show', compact('application'));
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/app/Http/Controllers/Portal/DashboardController.php
Normal file
47
src/app/Http/Controllers/Portal/DashboardController.php
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Portal;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\PropertyAccount;
|
||||||
|
use App\Models\SystemSetting;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function landing(): View|RedirectResponse
|
||||||
|
{
|
||||||
|
if (auth()->check()) {
|
||||||
|
return redirect()->route(auth()->user()->role->slug === 'admin'
|
||||||
|
? 'admin.dashboard'
|
||||||
|
: (auth()->user()->role->slug === 'staff' ? 'staff.dashboard' : 'portal.dashboard'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return view('portal.landing', [
|
||||||
|
'stats' => [
|
||||||
|
'applications' => InstallmentApplication::count(),
|
||||||
|
'property_accounts' => PropertyAccount::count(),
|
||||||
|
'active_cases' => InstallmentApplication::where('status', 'aktif')->count(),
|
||||||
|
],
|
||||||
|
'settings' => SystemSetting::all()->keyBy('key'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$user = auth()->user();
|
||||||
|
$applications = $user->applications()->latest()->with('propertyAccount', 'schedules')->get();
|
||||||
|
|
||||||
|
return view('portal.dashboard', [
|
||||||
|
'applications' => $applications,
|
||||||
|
'stats' => [
|
||||||
|
'total' => $applications->count(),
|
||||||
|
'processing' => $applications->where('status', 'diproses')->count(),
|
||||||
|
'active' => $applications->where('status', 'aktif')->count(),
|
||||||
|
'completed' => $applications->where('status', 'selesai')->count(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
101
src/app/Http/Controllers/Portal/PaymentController.php
Normal file
101
src/app/Http/Controllers/Portal/PaymentController.php
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Portal;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\StorePaymentRequest;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\InstallmentSchedule;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Services\AuditTrailService;
|
||||||
|
use App\Services\WorkflowStatusService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PaymentController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected WorkflowStatusService $workflowStatusService,
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create(InstallmentApplication $application): View
|
||||||
|
{
|
||||||
|
abort_unless($application->user_id === auth()->id(), 403);
|
||||||
|
|
||||||
|
$schedule = $application->schedules()->find(request('schedule'));
|
||||||
|
|
||||||
|
return view('portal.payment', [
|
||||||
|
'application' => $application->load('propertyAccount', 'user.profile'),
|
||||||
|
'schedule' => $schedule,
|
||||||
|
'defaultAmount' => $schedule?->amount ?? $application->schedule_total ?? $application->total_arrears,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StorePaymentRequest $request, InstallmentApplication $application): RedirectResponse
|
||||||
|
{
|
||||||
|
abort_unless($application->user_id === $request->user()->id, 403);
|
||||||
|
|
||||||
|
$schedule = $application->schedules()->find($request->input('schedule_id'));
|
||||||
|
|
||||||
|
$payment = Payment::create([
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'installment_schedule_id' => $schedule?->id,
|
||||||
|
'user_id' => $request->user()->id,
|
||||||
|
'transaction_no' => 'TRX-'.str()->upper(str()->random(10)),
|
||||||
|
'receipt_no' => 'RCPT-'.now()->format('YmdHis'),
|
||||||
|
'biller_code' => $request->string('biller_code'),
|
||||||
|
'ref_1' => $request->string('ref_1'),
|
||||||
|
'ref_2' => $request->string('ref_2'),
|
||||||
|
'amount' => $request->input('amount'),
|
||||||
|
'status' => 'success',
|
||||||
|
'payload' => [
|
||||||
|
'channel' => 'JomPAY Dummy',
|
||||||
|
'payer_name' => $request->user()->displayName(),
|
||||||
|
],
|
||||||
|
'paid_at' => now(),
|
||||||
|
'remarks' => 'Pembayaran demo JomPAY berjaya.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($schedule) {
|
||||||
|
$schedule->update([
|
||||||
|
'payment_status' => 'dibayar',
|
||||||
|
'paid_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->workflowStatusService->recordHistory(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
'bayaran_dibuat',
|
||||||
|
$application->status,
|
||||||
|
$application->status,
|
||||||
|
'Pembayaran dummy JomPAY direkodkan.',
|
||||||
|
['payment_id' => $payment->id],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->workflowStatusService->markCompletedIfPaid($application->refresh()->load('schedules'), $request->user());
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Pembayaran',
|
||||||
|
'bayaran_dummy',
|
||||||
|
'Pengguna membuat pembayaran dummy melalui JomPAY.',
|
||||||
|
$request->user(),
|
||||||
|
[],
|
||||||
|
$payment->toArray(),
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
|
||||||
|
return redirect()->route('portal.payments.receipt', $payment)->with('success', 'Bayaran dummy berjaya direkodkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function receipt(Payment $payment): View
|
||||||
|
{
|
||||||
|
abort_unless($payment->user_id === auth()->id(), 403);
|
||||||
|
|
||||||
|
return view('portal.receipt', [
|
||||||
|
'payment' => $payment->load('application.propertyAccount', 'user.profile', 'schedule'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/app/Http/Controllers/Portal/ProfileController.php
Normal file
31
src/app/Http/Controllers/Portal/ProfileController.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Portal;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\UpdateProfileRequest;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class ProfileController extends Controller
|
||||||
|
{
|
||||||
|
public function edit(): View
|
||||||
|
{
|
||||||
|
return view('portal.profile', [
|
||||||
|
'user' => auth()->user()->load('profile'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateProfileRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
$user->update($request->safe()->only('name', 'email'));
|
||||||
|
$user->profile()->updateOrCreate(
|
||||||
|
['user_id' => $user->id],
|
||||||
|
$request->safe()->except('name', 'email'),
|
||||||
|
);
|
||||||
|
|
||||||
|
return back()->with('success', 'Maklumat profil berjaya dikemaskini.');
|
||||||
|
}
|
||||||
|
}
|
||||||
85
src/app/Http/Controllers/PropertyAccountImportController.php
Normal file
85
src/app/Http/Controllers/PropertyAccountImportController.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\ImportPropertyAccountRequest;
|
||||||
|
use App\Models\ImportBatch;
|
||||||
|
use App\Services\PropertyAccountImportService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class PropertyAccountImportController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected PropertyAccountImportService $importService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$prefix = request()->routeIs('admin.*') ? 'admin' : 'staff';
|
||||||
|
$importType = $prefix === 'admin' ? 'tax' : 'arrears';
|
||||||
|
|
||||||
|
return view('shared.import-excel', [
|
||||||
|
'batches' => ImportBatch::with('uploader')->where('import_type', $importType)->latest()->paginate(10),
|
||||||
|
'prefix' => $prefix,
|
||||||
|
'importType' => $importType,
|
||||||
|
'pageTitle' => $importType === 'tax' ? 'Import Data Cukai Harta' : 'Import Data Akaun Tunggakan',
|
||||||
|
'description' => $importType === 'tax'
|
||||||
|
? 'Admin memuat naik data cukai harta seperti No_Bgnn, Nama_Jalan, Kadar dan Cukai_Harta.'
|
||||||
|
: 'Staff memuat naik data akaun tunggakan untuk semakan permohonan ansuran.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(ImportPropertyAccountRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$mode = $request->routeIs('admin.*') ? 'tax' : 'arrears';
|
||||||
|
$batch = $this->importService->import($request->file('file'), $request->user(), $mode);
|
||||||
|
|
||||||
|
return back()->with('success', 'Import selesai diproses untuk batch #'.$batch->id.'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadTemplate(Request $request): Response
|
||||||
|
{
|
||||||
|
$type = $request->query('type') === 'tax' ? 'tax' : 'arrears';
|
||||||
|
$filename = $type === 'tax' ? 'template-cukai-harta.csv' : 'template-akaun-tunggakan.csv';
|
||||||
|
$columns = $type === 'tax'
|
||||||
|
? [
|
||||||
|
'No_Akaun',
|
||||||
|
'No_Akaun_Tunggakan',
|
||||||
|
'No_Bgnn',
|
||||||
|
'Nama_Jalan',
|
||||||
|
'Pemilik',
|
||||||
|
'Bandar',
|
||||||
|
'Taman',
|
||||||
|
'Rupacara',
|
||||||
|
'Kadar',
|
||||||
|
'Cukai_Harta',
|
||||||
|
]
|
||||||
|
: [
|
||||||
|
'No_Akaun',
|
||||||
|
'Pemilik',
|
||||||
|
'Warganegara',
|
||||||
|
'Pelanggan',
|
||||||
|
'Status',
|
||||||
|
'Bandar',
|
||||||
|
'Taman',
|
||||||
|
'Rupacara',
|
||||||
|
'Kadar',
|
||||||
|
'Aktif',
|
||||||
|
'Tunggakan',
|
||||||
|
'No_Akaun_Tunggakan',
|
||||||
|
];
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Content-Type' => 'text/csv',
|
||||||
|
'Content-Disposition' => 'attachment; filename="'.$filename.'"',
|
||||||
|
];
|
||||||
|
|
||||||
|
$content = implode(',', $columns).PHP_EOL;
|
||||||
|
|
||||||
|
return response($content, 200, $headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
395
src/app/Http/Controllers/Staff/ApplicationController.php
Normal file
395
src/app/Http/Controllers/Staff/ApplicationController.php
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Staff;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use App\Enums\PropertyCategory;
|
||||||
|
use App\Exports\SimpleArrayExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\ReviewInstallmentApplicationRequest;
|
||||||
|
use App\Http\Requests\StoreManualPaymentRequest;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Services\AuditTrailService;
|
||||||
|
use App\Services\InstallmentManualScheduleService;
|
||||||
|
use App\Services\NotificationService;
|
||||||
|
use App\Services\WorkflowStatusService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ApplicationController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected InstallmentManualScheduleService $manualScheduleService,
|
||||||
|
protected WorkflowStatusService $workflowStatusService,
|
||||||
|
protected NotificationService $notificationService,
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(Request $request): View
|
||||||
|
{
|
||||||
|
$applications = InstallmentApplication::with('user.profile', 'propertyAccount', 'assignedStaff', 'reviewer')
|
||||||
|
->reviewableForStaff()
|
||||||
|
->when(
|
||||||
|
in_array((string) $request->string('status'), ApplicationStatus::reviewableValues(), true),
|
||||||
|
fn ($query) => $query->where('status', $request->string('status')->toString())
|
||||||
|
)
|
||||||
|
->when($request->filled('search'), function ($query) use ($request) {
|
||||||
|
$search = $request->string('search');
|
||||||
|
$query->where(function ($query) use ($search) {
|
||||||
|
$query->where('application_no', 'like', "%{$search}%")
|
||||||
|
->orWhere('no_akaun_tunggakan', 'like', "%{$search}%")
|
||||||
|
->orWhereHas('user.profile', fn ($userQuery) => $userQuery->where('public_name', 'like', "%{$search}%"))
|
||||||
|
->orWhereHas('propertyAccount', function ($propertyQuery) use ($search) {
|
||||||
|
$propertyQuery->where('bandar', 'like', "%{$search}%")
|
||||||
|
->orWhere('taman', 'like', "%{$search}%")
|
||||||
|
->orWhere('pemilik', 'like', "%{$search}%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
->when($request->filled('date_from'), fn ($query) => $query->whereDate('created_at', '>=', $request->date('date_from')))
|
||||||
|
->when($request->filled('date_to'), fn ($query) => $query->whereDate('created_at', '<=', $request->date('date_to')))
|
||||||
|
->latest()
|
||||||
|
->paginate(12);
|
||||||
|
|
||||||
|
return view('staff.applications-index', [
|
||||||
|
'applications' => $applications,
|
||||||
|
'allowedStatuses' => ApplicationStatus::reviewableValues(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(InstallmentApplication $application): View|RedirectResponse
|
||||||
|
{
|
||||||
|
if ($redirect = $this->redirectIfNotReviewable($application)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
$application->load([
|
||||||
|
'user.profile',
|
||||||
|
'propertyAccount',
|
||||||
|
'schedules',
|
||||||
|
'histories.user',
|
||||||
|
'payments.schedule',
|
||||||
|
'payments.user.profile',
|
||||||
|
'tasks.assignee',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$existingSchedules = $application->schedules->sortBy('installment_no')->values();
|
||||||
|
$scheduleRows = collect(range(1, $application->installment_months))
|
||||||
|
->map(function (int $number) use ($existingSchedules) {
|
||||||
|
$existing = $existingSchedules->firstWhere('installment_no', $number);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'installment_no' => $number,
|
||||||
|
'due_date' => $existing?->due_date?->format('Y-m-d') ?? now()->startOfMonth()->addMonths($number)->format('Y-m-d'),
|
||||||
|
'amount' => $existing?->amount,
|
||||||
|
'notes' => $existing?->notes,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
return view('staff.application-show', [
|
||||||
|
'application' => $application,
|
||||||
|
'scheduleRows' => $scheduleRows,
|
||||||
|
'guidance' => PropertyCategory::tryFrom($application->category),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function review(ReviewInstallmentApplicationRequest $request, InstallmentApplication $application): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($redirect = $this->redirectIfNotReviewable($application)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
$action = $request->input('action');
|
||||||
|
$result = [
|
||||||
|
'warning' => null,
|
||||||
|
'message' => 'Jadual ansuran tidak diubah untuk tindakan ini.',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($request->filled('total_arrears')) {
|
||||||
|
$application->update([
|
||||||
|
'total_arrears' => round((float) $request->input('total_arrears'), 2),
|
||||||
|
'property_tax_half_amount' => $request->filled('property_tax_half_amount')
|
||||||
|
? round((float) $request->input('property_tax_half_amount'), 2)
|
||||||
|
: $application->property_tax_half_amount,
|
||||||
|
]);
|
||||||
|
$application->refresh()->loadMissing('propertyAccount');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($action === 'processing') {
|
||||||
|
if ($application->status === ApplicationStatus::PROCESSING->value) {
|
||||||
|
$result = $this->manualScheduleService->saveDraft($application, $request->input('schedules', []), $request->user());
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = $this->workflowStatusService->applyProcessing(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
} elseif ($action === 'active') {
|
||||||
|
$this->manualScheduleService->saveDraft($application, $request->input('schedules', []), $request->user());
|
||||||
|
$result = $this->manualScheduleService->confirm($application, $request->input('schedules', []), $request->user());
|
||||||
|
|
||||||
|
$application = $this->workflowStatusService->applyAccepted(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationService->notifyDecision(
|
||||||
|
$application->user,
|
||||||
|
$application,
|
||||||
|
'Permohonan diterima',
|
||||||
|
'Permohonan '.$application->application_no.' telah diterima dan jadual ansuran manual telah disahkan.',
|
||||||
|
);
|
||||||
|
} elseif ($action === 'cancel') {
|
||||||
|
$application = $this->workflowStatusService->applyCancelled(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationService->notifyDecision(
|
||||||
|
$application->user,
|
||||||
|
$application,
|
||||||
|
'Akaun dibatalkan',
|
||||||
|
'Akaun ansuran '.$application->application_no.' telah dibatalkan oleh staff. Sila semak ulasan pegawai.',
|
||||||
|
);
|
||||||
|
} elseif ($action === 'failed') {
|
||||||
|
$application = $this->workflowStatusService->applyFailed(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationService->notifyDecision(
|
||||||
|
$application->user,
|
||||||
|
$application,
|
||||||
|
'Akaun gagal',
|
||||||
|
'Akaun ansuran '.$application->application_no.' telah disahkan gagal oleh staff. Sila semak ulasan pegawai.',
|
||||||
|
);
|
||||||
|
} elseif ($action === 'complete') {
|
||||||
|
if (! $this->allInstallmentsPaid($application)) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'action' => 'Akaun hanya boleh disahkan selesai selepas semua ansuran direkodkan sebagai dibayar.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$application = $this->workflowStatusService->applyCompleted(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->notificationService->notifyDecision(
|
||||||
|
$application->user,
|
||||||
|
$application,
|
||||||
|
'Akaun ansuran selesai',
|
||||||
|
'Akaun ansuran '.$application->application_no.' telah disahkan selesai.',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$result = $this->manualScheduleService->saveDraft($application, $request->input('schedules', []), $request->user());
|
||||||
|
|
||||||
|
$application = $this->workflowStatusService->applyNoAction(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
$request->input('staff_remarks'),
|
||||||
|
['schedule_result' => $result],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Semakan Staff',
|
||||||
|
'semak_permohonan',
|
||||||
|
'Staff menyemak permohonan dan mengemaskini jadual ansuran manual.',
|
||||||
|
$request->user(),
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'application_id' => $application->id,
|
||||||
|
'action' => $action,
|
||||||
|
'schedule_result' => $result,
|
||||||
|
],
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
|
||||||
|
$message = match ($action) {
|
||||||
|
'processing' => 'Akaun berjaya ditetapkan sebagai diproses.',
|
||||||
|
'active' => 'Akaun berjaya disahkan aktif.',
|
||||||
|
'cancel' => 'Akaun berjaya dibatalkan.',
|
||||||
|
'failed' => 'Akaun berjaya disahkan gagal.',
|
||||||
|
'complete' => 'Akaun berjaya disahkan selesai.',
|
||||||
|
default => 'Tiada perubahan status dibuat. Semakan draf berjaya disimpan.',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (in_array($action, ['cancel', 'failed', 'complete'], true)) {
|
||||||
|
return redirect()->route('staff.applications.index')->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function storeManualPayment(StoreManualPaymentRequest $request, InstallmentApplication $application): RedirectResponse
|
||||||
|
{
|
||||||
|
if ($redirect = $this->redirectIfNotReviewable($application)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($application->status !== ApplicationStatus::ACTIVE->value) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'manual_payment' => 'Bayaran manual hanya boleh direkodkan untuk akaun berstatus aktif.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = $application->schedules()
|
||||||
|
->whereKey($request->integer('installment_schedule_id'))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if (! $schedule) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'installment_schedule_id' => 'Ansuran yang dipilih tidak sah untuk permohonan ini.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($schedule->payment_status === 'dibayar') {
|
||||||
|
return back()->withErrors([
|
||||||
|
'installment_schedule_id' => 'Ansuran ini telah direkodkan sebagai dibayar.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs((float) $request->input('amount') - (float) $schedule->amount) > 0.009) {
|
||||||
|
return back()->withErrors([
|
||||||
|
'amount' => 'Amaun bayaran manual mesti sama dengan amaun ansuran yang dipilih.',
|
||||||
|
])->withInput();
|
||||||
|
}
|
||||||
|
|
||||||
|
$payment = Payment::create([
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'installment_schedule_id' => $schedule->id,
|
||||||
|
'user_id' => $application->user_id,
|
||||||
|
'transaction_no' => $request->filled('transaction_no')
|
||||||
|
? $request->string('transaction_no')->trim()->toString()
|
||||||
|
: 'MANUAL-'.now()->format('YmdHis').'-'.str()->upper(str()->random(4)),
|
||||||
|
'receipt_no' => $request->string('receipt_no')->trim()->toString(),
|
||||||
|
'biller_code' => '37317',
|
||||||
|
'ref_1' => $application->no_akaun,
|
||||||
|
'ref_2' => $application->email ?: $application->mobile_no,
|
||||||
|
'amount' => $request->input('amount'),
|
||||||
|
'status' => 'success',
|
||||||
|
'payload' => [
|
||||||
|
'channel' => 'Manual Staff Entry',
|
||||||
|
'recorded_by' => $request->user()->displayName(),
|
||||||
|
],
|
||||||
|
'paid_at' => $request->date('paid_at'),
|
||||||
|
'remarks' => $request->filled('remarks')
|
||||||
|
? $request->string('remarks')->trim()->toString()
|
||||||
|
: 'Bayaran direkodkan secara manual oleh staff.',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$schedule->update([
|
||||||
|
'payment_status' => 'dibayar',
|
||||||
|
'paid_at' => $request->date('paid_at'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->workflowStatusService->recordHistory(
|
||||||
|
$application,
|
||||||
|
$request->user(),
|
||||||
|
'bayaran_manual_direkodkan',
|
||||||
|
$application->status,
|
||||||
|
$application->status,
|
||||||
|
'Staff merekodkan bayaran manual dengan no. resit '.$payment->receipt_no.'.',
|
||||||
|
[
|
||||||
|
'payment_id' => $payment->id,
|
||||||
|
'receipt_no' => $payment->receipt_no,
|
||||||
|
'installment_schedule_id' => $schedule->id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->workflowStatusService->markCompletedIfPaid($application->refresh()->load('schedules'), $request->user());
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Pembayaran Manual',
|
||||||
|
'rekod_bayaran_manual',
|
||||||
|
'Staff merekodkan bayaran manual tanpa integrasi API.',
|
||||||
|
$request->user(),
|
||||||
|
[],
|
||||||
|
$payment->toArray(),
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
|
||||||
|
return back()->with('success', 'Bayaran manual berjaya direkodkan.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print(InstallmentApplication $application): View|RedirectResponse
|
||||||
|
{
|
||||||
|
if ($redirect = $this->redirectIfNotReviewable($application)) {
|
||||||
|
return $redirect;
|
||||||
|
}
|
||||||
|
|
||||||
|
$application->update(['printed_at' => now()]);
|
||||||
|
$application->load('user.profile', 'propertyAccount', 'schedules', 'payments.schedule', 'payments.user.profile', 'reviewer');
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Print Borang',
|
||||||
|
'cetak_borang',
|
||||||
|
'Borang permohonan ansuran dicetak oleh staff.',
|
||||||
|
auth()->user(),
|
||||||
|
[],
|
||||||
|
['printed_at' => now()->toDateTimeString()],
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
|
||||||
|
return view('print.application', compact('application'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export(): \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||||
|
{
|
||||||
|
$rows = InstallmentApplication::with('user.profile', 'propertyAccount')
|
||||||
|
->reviewableForStaff()
|
||||||
|
->get()
|
||||||
|
->map(fn ($application) => [
|
||||||
|
$application->application_no,
|
||||||
|
$application->no_akaun_tunggakan,
|
||||||
|
$application->user->displayName(),
|
||||||
|
$application->category,
|
||||||
|
$application->installment_months,
|
||||||
|
$application->total_arrears,
|
||||||
|
$application->status,
|
||||||
|
])
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return Excel::download(
|
||||||
|
new SimpleArrayExport(
|
||||||
|
['No Permohonan', 'No Akaun Tunggakan', 'Pemohon', 'Kategori', 'Tempoh Ansuran', 'Jumlah Tunggakan', 'Status'],
|
||||||
|
$rows
|
||||||
|
),
|
||||||
|
'permohonan-staff.xlsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function redirectIfNotReviewable(InstallmentApplication $application): ?RedirectResponse
|
||||||
|
{
|
||||||
|
if ($application->canBeReviewedByStaff()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('staff.applications.index')
|
||||||
|
->with('error', 'Hanya akaun berstatus diproses dan aktif boleh disemak oleh staff.');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function allInstallmentsPaid(InstallmentApplication $application): bool
|
||||||
|
{
|
||||||
|
return $application->schedules()->exists()
|
||||||
|
&& $application->schedules()
|
||||||
|
->whereNotIn('payment_status', ['dibayar', 'selesai'])
|
||||||
|
->doesntExist();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/app/Http/Controllers/Staff/BulkPaymentController.php
Normal file
58
src/app/Http/Controllers/Staff/BulkPaymentController.php
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Staff;
|
||||||
|
|
||||||
|
use App\Exports\SimpleArrayExport;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\ImportBulkPaymentRequest;
|
||||||
|
use App\Services\BulkPaymentImportService;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
class BulkPaymentController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected BulkPaymentImportService $bulkPaymentImportService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$activeSchedules = $this->bulkPaymentImportService->activePaymentSchedules();
|
||||||
|
$unpaidSchedules = $activeSchedules->whereIn('payment_status', ['belum_bayar', 'lewat']);
|
||||||
|
$paidSchedules = $activeSchedules->whereIn('payment_status', ['dibayar', 'selesai']);
|
||||||
|
|
||||||
|
return view('staff.bulk-payments-index', [
|
||||||
|
'activeSchedules' => $activeSchedules,
|
||||||
|
'activeAccountCount' => $activeSchedules->pluck('installment_application_id')->unique()->count(),
|
||||||
|
'unpaidScheduleCount' => $unpaidSchedules->count(),
|
||||||
|
'paidScheduleCount' => $paidSchedules->count(),
|
||||||
|
'totalUnpaidAmount' => $unpaidSchedules->sum(fn ($schedule) => (float) $schedule->amount),
|
||||||
|
'importResult' => session('bulk_payment_result'),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function downloadTemplate(): BinaryFileResponse
|
||||||
|
{
|
||||||
|
return Excel::download(
|
||||||
|
new SimpleArrayExport(
|
||||||
|
BulkPaymentImportService::HEADINGS,
|
||||||
|
$this->bulkPaymentImportService->templateRows(),
|
||||||
|
),
|
||||||
|
'template-kemaskini-bayaran-pukal.xlsx'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(ImportBulkPaymentRequest $request): RedirectResponse
|
||||||
|
{
|
||||||
|
$result = $this->bulkPaymentImportService->import($request->file('file'), $request->user());
|
||||||
|
$message = 'Fail bayaran pukal selesai diproses. Berjaya: '.$result['success'].', gagal: '.$result['failed'].', diabaikan: '.$result['skipped'].'.';
|
||||||
|
|
||||||
|
return redirect()
|
||||||
|
->route('staff.bulk-payments.index')
|
||||||
|
->with($result['failed'] > 0 ? 'warning' : 'success', $message)
|
||||||
|
->with('bulk_payment_result', $result);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
src/app/Http/Controllers/Staff/DashboardController.php
Normal file
33
src/app/Http/Controllers/Staff/DashboardController.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Staff;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\Task;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
class DashboardController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$applications = InstallmentApplication::with('user.profile', 'propertyAccount')
|
||||||
|
->reviewableForStaff()
|
||||||
|
->latest()
|
||||||
|
->take(8)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return view('staff.dashboard', [
|
||||||
|
'applications' => $applications,
|
||||||
|
'stats' => [
|
||||||
|
'reviewable' => InstallmentApplication::reviewableForStaff()->count(),
|
||||||
|
'processing' => InstallmentApplication::where('status', ApplicationStatus::PROCESSING->value)->count(),
|
||||||
|
'active' => InstallmentApplication::where('status', ApplicationStatus::ACTIVE->value)->count(),
|
||||||
|
'pending_tasks' => Task::where('status', 'belum_selesai')
|
||||||
|
->whereHas('application', fn ($query) => $query->reviewableForStaff())
|
||||||
|
->count(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/Http/Middleware/EnsureProfileCompleted.php
Normal file
23
src/app/Http/Middleware/EnsureProfileCompleted.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureProfileCompleted
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if ($user?->isPublicUser() && (! $user->profile?->identification_number || ! $user->profile?->mobile_no || ! $user->profile?->correspondence_address)) {
|
||||||
|
return redirect()
|
||||||
|
->route('portal.profile.edit')
|
||||||
|
->with('warning', 'Sila lengkapkan profil sebelum membuat permohonan ansuran.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/app/Http/Middleware/EnsureRole.php
Normal file
21
src/app/Http/Middleware/EnsureRole.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Middleware;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class EnsureRole
|
||||||
|
{
|
||||||
|
public function handle(Request $request, Closure $next, string ...$roles): Response
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
if (! $user || ! in_array($user->role?->slug, $roles, true)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
}
|
||||||
40
src/app/Http/Requests/ImportBulkPaymentRequest.php
Normal file
40
src/app/Http/Requests/ImportBulkPaymentRequest.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ImportBulkPaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'file' => [
|
||||||
|
'required',
|
||||||
|
'file',
|
||||||
|
'max:10240',
|
||||||
|
function (string $attribute, mixed $value, \Closure $fail): void {
|
||||||
|
$extension = strtolower((string) $value?->getClientOriginalExtension());
|
||||||
|
|
||||||
|
if (! in_array($extension, ['xlsx', 'xls', 'csv'], true)) {
|
||||||
|
$fail('Format fail mestilah xlsx, xls atau csv.');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'file.required' => 'Sila pilih fail Excel/CSV bayaran pukal untuk diproses.',
|
||||||
|
'file.mimes' => 'Format fail mestilah xlsx, xls atau csv.',
|
||||||
|
'file.max' => 'Saiz fail terlalu besar. Sila muat naik fail tidak melebihi 10MB.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/Http/Requests/ImportPropertyAccountRequest.php
Normal file
20
src/app/Http/Requests/ImportPropertyAccountRequest.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class ImportPropertyAccountRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'file' => ['required', 'file', 'mimes:xlsx,xls,csv', 'max:10240'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/app/Http/Requests/LoginRequest.php
Normal file
21
src/app/Http/Requests/LoginRequest.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class LoginRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'password' => ['required', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/app/Http/Requests/LookupPropertyAccountRequest.php
Normal file
20
src/app/Http/Requests/LookupPropertyAccountRequest.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class LookupPropertyAccountRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no_akaun_tunggakan' => ['required', 'string', 'max:255'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/Http/Requests/ManageUserRequest.php
Normal file
28
src/app/Http/Requests/ManageUserRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class ManageUserRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$userId = $this->route('user')?->id;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($userId)],
|
||||||
|
'role_id' => ['required', 'exists:roles,id'],
|
||||||
|
'department_id' => ['nullable', 'exists:departments,id'],
|
||||||
|
'is_active' => ['nullable', 'boolean'],
|
||||||
|
'password' => [$userId ? 'nullable' : 'required', 'string', 'min:8'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/app/Http/Requests/RegisterRequest.php
Normal file
21
src/app/Http/Requests/RegisterRequest.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class RegisterRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email' => ['required', 'email', 'max:255', 'unique:users,email'],
|
||||||
|
'password' => ['required', 'string', 'min:8', 'confirmed'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class ReviewInstallmentApplicationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'action' => ['required', 'in:processing,active,cancel,failed,complete,none'],
|
||||||
|
'total_arrears' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'property_tax_half_amount' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'staff_remarks' => ['nullable', 'string'],
|
||||||
|
'schedules' => ['array'],
|
||||||
|
'schedules.*.due_date' => ['nullable', 'date'],
|
||||||
|
'schedules.*.amount' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'schedules.*.notes' => ['nullable', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator): void {
|
||||||
|
if (in_array($this->input('action'), ['cancel', 'failed'], true) && blank($this->input('staff_remarks'))) {
|
||||||
|
$validator->errors()->add('staff_remarks', 'Ulasan staff wajib diisi apabila akaun dibatalkan atau disahkan gagal.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
49
src/app/Http/Requests/StoreInstallmentApplicationRequest.php
Normal file
49
src/app/Http/Requests/StoreInstallmentApplicationRequest.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreInstallmentApplicationRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'property_account_id' => ['required', 'exists:property_accounts,id'],
|
||||||
|
'no_akaun_tunggakan' => ['required', 'string'],
|
||||||
|
'category' => ['required', 'string'],
|
||||||
|
'installment_months' => ['required', 'integer', 'min:2', 'max:6'],
|
||||||
|
'identification_type' => ['required', 'string'],
|
||||||
|
'identification_number' => ['required', 'string'],
|
||||||
|
'no_lot' => ['nullable', 'string', 'max:255'],
|
||||||
|
'no_dhm' => ['nullable', 'string', 'max:255'],
|
||||||
|
'no_syarikat' => ['nullable', 'string'],
|
||||||
|
'alamat_harta' => ['required', 'string'],
|
||||||
|
'alamat_surat_menyurat' => ['required', 'string'],
|
||||||
|
'country_code' => ['required', 'string'],
|
||||||
|
'mobile_no' => ['required', 'string'],
|
||||||
|
'email' => ['required', 'email'],
|
||||||
|
'alamat_tempat_bekerja' => ['nullable', 'string'],
|
||||||
|
'pekerjaan' => ['nullable', 'string'],
|
||||||
|
'pendapatan_sebulan' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'declaration_name' => ['required', 'string', 'max:255'],
|
||||||
|
'declaration_identification_type' => ['required', 'string'],
|
||||||
|
'declaration_identification_number' => ['required', 'string'],
|
||||||
|
'declaration_date' => ['required', 'date'],
|
||||||
|
'declaration_accepted' => ['accepted'],
|
||||||
|
'ic_attachment' => ['nullable', 'file', 'mimes:jpg,jpeg,png,pdf', 'max:4096'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'declaration_accepted.accepted' => 'Sila tandakan perakuan pemohon sebelum menghantar permohonan.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
34
src/app/Http/Requests/StoreManualPaymentRequest.php
Normal file
34
src/app/Http/Requests/StoreManualPaymentRequest.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class StoreManualPaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'installment_schedule_id' => ['required', 'integer', 'exists:installment_schedules,id'],
|
||||||
|
'receipt_no' => ['required', 'string', 'max:255'],
|
||||||
|
'transaction_no' => ['nullable', 'string', 'max:255', Rule::unique('payments', 'transaction_no')],
|
||||||
|
'amount' => ['required', 'numeric', 'min:0.01'],
|
||||||
|
'paid_at' => ['required', 'date'],
|
||||||
|
'remarks' => ['nullable', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'receipt_no.required' => 'No. resit wajib diisi untuk rekod bayaran manual.',
|
||||||
|
'installment_schedule_id.required' => 'Sila pilih ansuran yang ingin direkodkan.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/Http/Requests/StorePaymentRequest.php
Normal file
23
src/app/Http/Requests/StorePaymentRequest.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StorePaymentRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'biller_code' => ['required', 'string'],
|
||||||
|
'ref_1' => ['required', 'string'],
|
||||||
|
'ref_2' => ['required', 'string'],
|
||||||
|
'amount' => ['required', 'numeric', 'min:1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/Http/Requests/StorePropertyAccountRequest.php
Normal file
32
src/app/Http/Requests/StorePropertyAccountRequest.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StorePropertyAccountRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'no_akaun' => ['required', 'string', 'max:255'],
|
||||||
|
'no_akaun_tunggakan' => ['required', 'string', 'max:255'],
|
||||||
|
'no_bgnn' => ['nullable', 'string', 'max:255'],
|
||||||
|
'nama_jalan' => ['nullable', 'string', 'max:255'],
|
||||||
|
'pemilik' => ['required', 'string', 'max:255'],
|
||||||
|
'bandar' => ['nullable', 'string', 'max:255'],
|
||||||
|
'taman' => ['nullable', 'string', 'max:255'],
|
||||||
|
'rupacara' => ['nullable', 'string', 'max:255'],
|
||||||
|
'kadar' => ['nullable', 'string', 'max:255'],
|
||||||
|
'cukai_harta' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'tunggakan' => ['required', 'numeric', 'min:0'],
|
||||||
|
'category' => ['nullable', 'string', 'max:255'],
|
||||||
|
'alamat_harta' => ['nullable', 'string', 'max:500'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/app/Http/Requests/UpdateProfileRequest.php
Normal file
31
src/app/Http/Requests/UpdateProfileRequest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateProfileRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['required', 'string', 'max:255'],
|
||||||
|
'email' => ['required', 'email', Rule::unique('users', 'email')->ignore($this->user()?->id)],
|
||||||
|
'public_name' => ['required', 'string', 'max:255'],
|
||||||
|
'identification_type' => ['required', 'string'],
|
||||||
|
'identification_number' => ['required', 'string', 'max:100'],
|
||||||
|
'country_code' => ['required', 'string', 'max:10'],
|
||||||
|
'mobile_no' => ['required', 'string', 'max:30'],
|
||||||
|
'correspondence_address' => ['required', 'string', 'max:500'],
|
||||||
|
'workplace_address' => ['nullable', 'string', 'max:500'],
|
||||||
|
'occupation' => ['nullable', 'string', 'max:255'],
|
||||||
|
'monthly_income' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/app/Http/Requests/UpdateSystemSettingRequest.php
Normal file
21
src/app/Http/Requests/UpdateSystemSettingRequest.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateSystemSettingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'settings' => ['required', 'array'],
|
||||||
|
'settings.*' => ['nullable', 'string'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/app/Imports/PropertyAccountsImport.php
Normal file
17
src/app/Imports/PropertyAccountsImport.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Imports;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Maatwebsite\Excel\Concerns\ToCollection;
|
||||||
|
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||||
|
|
||||||
|
class PropertyAccountsImport implements ToCollection, WithHeadingRow
|
||||||
|
{
|
||||||
|
public Collection $rows;
|
||||||
|
|
||||||
|
public function collection(Collection $collection): void
|
||||||
|
{
|
||||||
|
$this->rows = $collection;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/app/Models/ApplicationHistory.php
Normal file
39
src/app/Models/ApplicationHistory.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class ApplicationHistory extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'installment_application_id',
|
||||||
|
'user_id',
|
||||||
|
'action',
|
||||||
|
'old_status',
|
||||||
|
'new_status',
|
||||||
|
'remarks',
|
||||||
|
'metadata',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
src/app/Models/AuditTrail.php
Normal file
42
src/app/Models/AuditTrail.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class AuditTrail extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'installment_application_id',
|
||||||
|
'action',
|
||||||
|
'module',
|
||||||
|
'description',
|
||||||
|
'old_values',
|
||||||
|
'new_values',
|
||||||
|
'ip_address',
|
||||||
|
'user_agent',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'old_values' => 'array',
|
||||||
|
'new_values' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/app/Models/Department.php
Normal file
23
src/app/Models/Department.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Department extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'code',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function users(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/app/Models/EmailLog.php
Normal file
39
src/app/Models/EmailLog.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class EmailLog extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'installment_application_id',
|
||||||
|
'recipient_email',
|
||||||
|
'subject',
|
||||||
|
'body',
|
||||||
|
'status',
|
||||||
|
'sent_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sent_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/Models/ImportBatch.php
Normal file
43
src/app/Models/ImportBatch.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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 ImportBatch extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'filename',
|
||||||
|
'stored_path',
|
||||||
|
'import_type',
|
||||||
|
'uploaded_by',
|
||||||
|
'total_processed',
|
||||||
|
'total_new',
|
||||||
|
'total_updated',
|
||||||
|
'total_missing',
|
||||||
|
'total_failed',
|
||||||
|
'summary',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'summary' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uploader(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploaded_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function propertyAccounts(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(PropertyAccount::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
125
src/app/Models/InstallmentApplication.php
Normal file
125
src/app/Models/InstallmentApplication.php
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class InstallmentApplication extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'property_account_id',
|
||||||
|
'assigned_staff_id',
|
||||||
|
'application_no',
|
||||||
|
'no_akaun',
|
||||||
|
'no_akaun_tunggakan',
|
||||||
|
'category',
|
||||||
|
'installment_months',
|
||||||
|
'total_arrears',
|
||||||
|
'property_tax_half_amount',
|
||||||
|
'status',
|
||||||
|
'schedule_status',
|
||||||
|
'schedule_total',
|
||||||
|
'staff_remarks',
|
||||||
|
'reviewed_by',
|
||||||
|
'reviewed_at',
|
||||||
|
'approved_at',
|
||||||
|
'rejected_at',
|
||||||
|
'rejection_reason',
|
||||||
|
'identification_type',
|
||||||
|
'identification_number',
|
||||||
|
'no_syarikat',
|
||||||
|
'jenis_lot',
|
||||||
|
'no_lot',
|
||||||
|
'no_dhm',
|
||||||
|
'jenis_dhm',
|
||||||
|
'alamat_harta',
|
||||||
|
'alamat_surat_menyurat',
|
||||||
|
'country_code',
|
||||||
|
'mobile_no',
|
||||||
|
'email',
|
||||||
|
'alamat_tempat_bekerja',
|
||||||
|
'pekerjaan',
|
||||||
|
'pendapatan_sebulan',
|
||||||
|
'declaration_name',
|
||||||
|
'declaration_identification_type',
|
||||||
|
'declaration_identification_number',
|
||||||
|
'declaration_date',
|
||||||
|
'declaration_accepted',
|
||||||
|
'ic_attachment_path',
|
||||||
|
'printed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'total_arrears' => 'decimal:2',
|
||||||
|
'property_tax_half_amount' => 'decimal:2',
|
||||||
|
'schedule_total' => 'decimal:2',
|
||||||
|
'pendapatan_sebulan' => 'decimal:2',
|
||||||
|
'declaration_accepted' => 'boolean',
|
||||||
|
'reviewed_at' => 'datetime',
|
||||||
|
'approved_at' => 'datetime',
|
||||||
|
'rejected_at' => 'datetime',
|
||||||
|
'printed_at' => 'datetime',
|
||||||
|
'declaration_date' => 'date',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function propertyAccount(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(PropertyAccount::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignedStaff(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'assigned_staff_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviewer(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'reviewed_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function schedules(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstallmentSchedule::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Payment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function histories(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ApplicationHistory::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tasks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Task::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function scopeReviewableForStaff(Builder $query): Builder
|
||||||
|
{
|
||||||
|
return $query->whereIn('status', ApplicationStatus::reviewableValues());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canBeReviewedByStaff(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->status, ApplicationStatus::reviewableValues(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
50
src/app/Models/InstallmentSchedule.php
Normal file
50
src/app/Models/InstallmentSchedule.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 InstallmentSchedule extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'installment_application_id',
|
||||||
|
'installment_no',
|
||||||
|
'due_date',
|
||||||
|
'amount',
|
||||||
|
'payment_status',
|
||||||
|
'paid_at',
|
||||||
|
'notes',
|
||||||
|
'is_confirmed',
|
||||||
|
'created_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
'due_date' => 'date',
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
'is_confirmed' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Payment::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/app/Models/Payment.php
Normal file
52
src/app/Models/Payment.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Payment extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'installment_application_id',
|
||||||
|
'installment_schedule_id',
|
||||||
|
'user_id',
|
||||||
|
'transaction_no',
|
||||||
|
'receipt_no',
|
||||||
|
'biller_code',
|
||||||
|
'ref_1',
|
||||||
|
'ref_2',
|
||||||
|
'amount',
|
||||||
|
'status',
|
||||||
|
'payload',
|
||||||
|
'paid_at',
|
||||||
|
'remarks',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'amount' => 'decimal:2',
|
||||||
|
'payload' => 'array',
|
||||||
|
'paid_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function schedule(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentSchedule::class, 'installment_schedule_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/app/Models/PropertyAccount.php
Normal file
60
src/app/Models/PropertyAccount.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?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 PropertyAccount extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'no_akaun',
|
||||||
|
'no_akaun_tunggakan',
|
||||||
|
'no_bgnn',
|
||||||
|
'nama_jalan',
|
||||||
|
'pemilik',
|
||||||
|
'warganegara',
|
||||||
|
'pelanggan',
|
||||||
|
'status',
|
||||||
|
'bandar',
|
||||||
|
'taman',
|
||||||
|
'rupacara',
|
||||||
|
'kadar',
|
||||||
|
'cukai_harta',
|
||||||
|
'aktif',
|
||||||
|
'tunggakan',
|
||||||
|
'jenis_lot',
|
||||||
|
'no_lot',
|
||||||
|
'jenis_dhm',
|
||||||
|
'no_dhm',
|
||||||
|
'no_syarikat',
|
||||||
|
'alamat_harta',
|
||||||
|
'category',
|
||||||
|
'status_data',
|
||||||
|
'last_imported_at',
|
||||||
|
'import_batch_id',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tunggakan' => 'decimal:2',
|
||||||
|
'cukai_harta' => 'decimal:2',
|
||||||
|
'last_imported_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function importBatch(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ImportBatch::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstallmentApplication::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/app/Models/Role.php
Normal file
28
src/app/Models/Role.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
class Role extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'slug',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function users(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function permissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(RolePermission::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/app/Models/RolePermission.php
Normal file
30
src/app/Models/RolePermission.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 RolePermission extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'role_id',
|
||||||
|
'module',
|
||||||
|
'permissions',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'permissions' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Role::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/app/Models/SystemNotification.php
Normal file
37
src/app/Models/SystemNotification.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class SystemNotification extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'notifications';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'type',
|
||||||
|
'title',
|
||||||
|
'message',
|
||||||
|
'action_url',
|
||||||
|
'is_read',
|
||||||
|
'read_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_read' => 'boolean',
|
||||||
|
'read_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/app/Models/SystemSetting.php
Normal file
19
src/app/Models/SystemSetting.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class SystemSetting extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'group',
|
||||||
|
'key',
|
||||||
|
'label',
|
||||||
|
'value',
|
||||||
|
'description',
|
||||||
|
];
|
||||||
|
}
|
||||||
47
src/app/Models/Task.php
Normal file
47
src/app/Models/Task.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\BelongsTo;
|
||||||
|
|
||||||
|
class Task extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'installment_application_id',
|
||||||
|
'assigned_to',
|
||||||
|
'assigned_by',
|
||||||
|
'title',
|
||||||
|
'description',
|
||||||
|
'priority',
|
||||||
|
'status',
|
||||||
|
'due_at',
|
||||||
|
'completed_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'due_at' => 'datetime',
|
||||||
|
'completed_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function application(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(InstallmentApplication::class, 'installment_application_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignee(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'assigned_to');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assigner(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'assigned_by');
|
||||||
|
}
|
||||||
|
}
|
||||||
118
src/app/Models/User.php
Normal file
118
src/app/Models/User.php
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Enums\UserRole;
|
||||||
|
use Database\Factories\UserFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||||
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
|
class User extends Authenticatable
|
||||||
|
{
|
||||||
|
/** @use HasFactory<UserFactory> */
|
||||||
|
use HasFactory, Notifiable;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'name',
|
||||||
|
'email',
|
||||||
|
'password',
|
||||||
|
'role_id',
|
||||||
|
'department_id',
|
||||||
|
'is_active',
|
||||||
|
'last_login_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'password',
|
||||||
|
'remember_token',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the attributes that should be cast.
|
||||||
|
*
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'email_verified_at' => 'datetime',
|
||||||
|
'password' => 'hashed',
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'last_login_at' => 'datetime',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function role(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Role::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function department(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Department::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profile(): HasOne
|
||||||
|
{
|
||||||
|
return $this->hasOne(UserProfile::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstallmentApplication::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assignedApplications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstallmentApplication::class, 'assigned_staff_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function reviewedApplications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(InstallmentApplication::class, 'reviewed_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function payments(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Payment::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifications(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(SystemNotification::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function tasks(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(Task::class, 'assigned_to');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function auditTrails(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(AuditTrail::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAdmin(): bool
|
||||||
|
{
|
||||||
|
return $this->role?->slug === UserRole::ADMIN->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isStaff(): bool
|
||||||
|
{
|
||||||
|
return $this->role?->slug === UserRole::STAFF->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPublicUser(): bool
|
||||||
|
{
|
||||||
|
return $this->role?->slug === UserRole::PUBLIC->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function displayName(): string
|
||||||
|
{
|
||||||
|
return $this->profile?->public_name ?: $this->name;
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/app/Models/UserProfile.php
Normal file
37
src/app/Models/UserProfile.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class UserProfile extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'user_id',
|
||||||
|
'public_name',
|
||||||
|
'identification_type',
|
||||||
|
'identification_number',
|
||||||
|
'country_code',
|
||||||
|
'mobile_no',
|
||||||
|
'correspondence_address',
|
||||||
|
'workplace_address',
|
||||||
|
'occupation',
|
||||||
|
'monthly_income',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'monthly_income' => 'decimal:2',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/app/Providers/AppServiceProvider.php
Normal file
25
src/app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Pagination\Paginator;
|
||||||
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
|
||||||
|
class AppServiceProvider extends ServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap any application services.
|
||||||
|
*/
|
||||||
|
public function boot(): void
|
||||||
|
{
|
||||||
|
Paginator::useBootstrapFive();
|
||||||
|
}
|
||||||
|
}
|
||||||
89
src/app/Services/ApplicationInformationExcelService.php
Normal file
89
src/app/Services/ApplicationInformationExcelService.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Exports\SimpleArrayExport;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class ApplicationInformationExcelService
|
||||||
|
{
|
||||||
|
public const EXPORT_PATH = 'exports/permohonan-maklumat-pengguna.xlsx';
|
||||||
|
|
||||||
|
public function sync(): string
|
||||||
|
{
|
||||||
|
Storage::disk('local')->makeDirectory('exports');
|
||||||
|
|
||||||
|
Excel::store(
|
||||||
|
new SimpleArrayExport($this->headings(), $this->rows()),
|
||||||
|
self::EXPORT_PATH,
|
||||||
|
'local',
|
||||||
|
);
|
||||||
|
|
||||||
|
return self::EXPORT_PATH;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function headings(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'No Permohonan',
|
||||||
|
'Tarikh Permohonan',
|
||||||
|
'Nama Pemohon',
|
||||||
|
'Identification Type',
|
||||||
|
'Identification Number',
|
||||||
|
'No Telefon',
|
||||||
|
'Emel',
|
||||||
|
'Alamat Surat Menyurat',
|
||||||
|
'No Akaun',
|
||||||
|
'No Akaun Tunggakan',
|
||||||
|
'Pemilik',
|
||||||
|
'Rupacara',
|
||||||
|
'No Lot',
|
||||||
|
'No DHM',
|
||||||
|
'No Syarikat',
|
||||||
|
'Alamat Harta',
|
||||||
|
'Kategori Harta',
|
||||||
|
'Tempoh Ansuran',
|
||||||
|
'Jumlah Tunggakan',
|
||||||
|
'Alamat Tempat Bekerja',
|
||||||
|
'Pekerjaan',
|
||||||
|
'Pendapatan Sebulan',
|
||||||
|
'Status',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rows(): array
|
||||||
|
{
|
||||||
|
return InstallmentApplication::query()
|
||||||
|
->with(['user.profile', 'propertyAccount'])
|
||||||
|
->oldest('created_at')
|
||||||
|
->get()
|
||||||
|
->map(fn (InstallmentApplication $application): array => [
|
||||||
|
$application->application_no,
|
||||||
|
optional($application->created_at)->format('d/m/Y h:i A'),
|
||||||
|
$application->user?->displayName(),
|
||||||
|
$application->identification_type,
|
||||||
|
$application->identification_number,
|
||||||
|
trim($application->country_code.' '.$application->mobile_no),
|
||||||
|
$application->email,
|
||||||
|
$application->alamat_surat_menyurat,
|
||||||
|
$application->no_akaun,
|
||||||
|
$application->no_akaun_tunggakan,
|
||||||
|
$application->propertyAccount?->pemilik,
|
||||||
|
$application->propertyAccount?->rupacara,
|
||||||
|
$application->no_lot,
|
||||||
|
$application->no_dhm,
|
||||||
|
$application->no_syarikat,
|
||||||
|
$application->alamat_harta,
|
||||||
|
$application->category,
|
||||||
|
$application->installment_months.' bulan',
|
||||||
|
(float) $application->total_arrears,
|
||||||
|
$application->alamat_tempat_bekerja,
|
||||||
|
$application->pekerjaan,
|
||||||
|
$application->pendapatan_sebulan !== null ? (float) $application->pendapatan_sebulan : null,
|
||||||
|
$application->status,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/app/Services/AuditTrailService.php
Normal file
32
src/app/Services/AuditTrailService.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\AuditTrail;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class AuditTrailService
|
||||||
|
{
|
||||||
|
public function log(
|
||||||
|
string $module,
|
||||||
|
string $action,
|
||||||
|
string $description,
|
||||||
|
?User $user = null,
|
||||||
|
array $oldValues = [],
|
||||||
|
array $newValues = [],
|
||||||
|
?InstallmentApplication $application = null,
|
||||||
|
): AuditTrail {
|
||||||
|
return AuditTrail::create([
|
||||||
|
'user_id' => $user?->id,
|
||||||
|
'installment_application_id' => $application?->id,
|
||||||
|
'module' => $module,
|
||||||
|
'action' => $action,
|
||||||
|
'description' => $description,
|
||||||
|
'old_values' => $oldValues ?: null,
|
||||||
|
'new_values' => $newValues ?: null,
|
||||||
|
'ip_address' => request()->ip(),
|
||||||
|
'user_agent' => request()->userAgent(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
387
src/app/Services/BulkPaymentImportService.php
Normal file
387
src/app/Services/BulkPaymentImportService.php
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use App\Imports\PropertyAccountsImport;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\InstallmentSchedule;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||||||
|
use RuntimeException;
|
||||||
|
use Throwable;
|
||||||
|
|
||||||
|
class BulkPaymentImportService
|
||||||
|
{
|
||||||
|
public const HEADINGS = [
|
||||||
|
'Installment_Schedule_ID',
|
||||||
|
'No_Akaun',
|
||||||
|
'No_Akaun_Tunggakan',
|
||||||
|
'No_Permohonan',
|
||||||
|
'Nama_Pemohon',
|
||||||
|
'Ansuran_Ke',
|
||||||
|
'Tarikh_Jadual',
|
||||||
|
'Amaun_Jadual',
|
||||||
|
'Status_Bayaran',
|
||||||
|
'No_Resit',
|
||||||
|
'No_Transaksi',
|
||||||
|
'Tarikh_Bayaran',
|
||||||
|
'Amaun_Bayaran',
|
||||||
|
'Catatan',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected WorkflowStatusService $workflowStatusService,
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activeUnpaidSchedules(): Collection
|
||||||
|
{
|
||||||
|
return $this->activePaymentSchedules()
|
||||||
|
->whereIn('payment_status', ['belum_bayar', 'lewat'])
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activePaidSchedules(): Collection
|
||||||
|
{
|
||||||
|
return $this->activePaymentSchedules()
|
||||||
|
->whereIn('payment_status', ['dibayar', 'selesai'])
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function activePaymentSchedules(): Collection
|
||||||
|
{
|
||||||
|
return InstallmentSchedule::query()
|
||||||
|
->with(['application.user.profile', 'application.propertyAccount'])
|
||||||
|
->with(['payments' => fn ($query) => $query->latest('paid_at')->latest('id')])
|
||||||
|
->whereHas('application', fn ($query) => $query->where('status', ApplicationStatus::ACTIVE->value))
|
||||||
|
->get()
|
||||||
|
->sortBy(fn (InstallmentSchedule $schedule) => sprintf(
|
||||||
|
'%s-%03d',
|
||||||
|
$schedule->application?->no_akaun ?? '',
|
||||||
|
$schedule->installment_no
|
||||||
|
))
|
||||||
|
->values();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function templateRows(): array
|
||||||
|
{
|
||||||
|
return $this->activePaymentSchedules()
|
||||||
|
->map(function (InstallmentSchedule $schedule) {
|
||||||
|
$application = $schedule->application;
|
||||||
|
$payment = $this->latestPaymentForSchedule($schedule);
|
||||||
|
|
||||||
|
return [
|
||||||
|
$schedule->id,
|
||||||
|
$application?->no_akaun,
|
||||||
|
$application?->no_akaun_tunggakan,
|
||||||
|
$application?->application_no,
|
||||||
|
$application?->user?->displayName(),
|
||||||
|
$schedule->installment_no,
|
||||||
|
optional($schedule->due_date)->format('Y-m-d'),
|
||||||
|
number_format((float) $schedule->amount, 2, '.', ''),
|
||||||
|
$schedule->payment_status,
|
||||||
|
$payment?->receipt_no ?? '',
|
||||||
|
$payment?->transaction_no ?? '',
|
||||||
|
optional($payment?->paid_at)->format('Y-m-d H:i') ?? '',
|
||||||
|
$payment ? number_format((float) $payment->amount, 2, '.', '') : '',
|
||||||
|
$payment?->remarks ?? '',
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import(UploadedFile $file, User $staff): array
|
||||||
|
{
|
||||||
|
$storedPath = $file->store('imports/bulk-payments');
|
||||||
|
$rows = Excel::toCollection(new PropertyAccountsImport(), Storage::path($storedPath))->first() ?? collect();
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'processed' => 0,
|
||||||
|
'success' => 0,
|
||||||
|
'skipped' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
'errors' => [],
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
];
|
||||||
|
|
||||||
|
$touchedApplicationIds = [];
|
||||||
|
|
||||||
|
foreach ($rows as $index => $row) {
|
||||||
|
$rowNumber = $index + 2;
|
||||||
|
$payload = $this->normalizeRow($row instanceof Collection ? $row->toArray() : (array) $row);
|
||||||
|
|
||||||
|
if (! $this->hasPaymentIntent($payload)) {
|
||||||
|
$result['skipped']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$result['processed']++;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$payment = DB::transaction(fn () => $this->processRow($payload, $staff, $rowNumber));
|
||||||
|
$result['success']++;
|
||||||
|
$touchedApplicationIds[$payment->installment_application_id] = true;
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$result['failed']++;
|
||||||
|
$result['errors'][] = 'Baris '.$rowNumber.': '.$exception->getMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (array_keys($touchedApplicationIds) as $applicationId) {
|
||||||
|
$application = InstallmentApplication::with('schedules')->find($applicationId);
|
||||||
|
|
||||||
|
if ($application) {
|
||||||
|
$this->workflowStatusService->markCompletedIfPaid($application, $staff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Kemaskini Bayaran Pukal',
|
||||||
|
'import_bayaran_pukal',
|
||||||
|
'Staff memproses kemaskini bayaran ansuran secara pukal melalui Excel/CSV.',
|
||||||
|
$staff,
|
||||||
|
[],
|
||||||
|
$result + ['stored_path' => $storedPath],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function processRow(array $payload, User $staff, int $rowNumber): Payment
|
||||||
|
{
|
||||||
|
$schedule = $this->resolveSchedule($payload);
|
||||||
|
$application = $schedule?->application;
|
||||||
|
|
||||||
|
if (! $schedule || ! $application) {
|
||||||
|
throw new RuntimeException('Jadual ansuran tidak dijumpai untuk baris ini.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($application->status !== ApplicationStatus::ACTIVE->value) {
|
||||||
|
throw new RuntimeException('Hanya akaun berstatus aktif boleh dikemaskini melalui bayaran pukal.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$receiptNo = $this->stringValue($payload, ['no_resit', 'receipt_no', 'resit']);
|
||||||
|
$transactionNo = $this->stringValue($payload, ['no_transaksi', 'transaction_no', 'transaksi']);
|
||||||
|
$amount = $this->decimalValue($payload, ['amaun_bayaran', 'amount', 'jumlah_bayaran']);
|
||||||
|
$paidAt = $this->dateValue($payload, ['tarikh_bayaran', 'paid_at']);
|
||||||
|
|
||||||
|
if ($receiptNo === '') {
|
||||||
|
throw new RuntimeException('No. resit wajib diisi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($amount === null || $amount <= 0) {
|
||||||
|
throw new RuntimeException('Amaun bayaran wajib diisi dan mesti melebihi 0.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $paidAt) {
|
||||||
|
throw new RuntimeException('Tarikh bayaran wajib diisi.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (abs($amount - (float) $schedule->amount) > 0.009) {
|
||||||
|
throw new RuntimeException('Amaun bayaran mesti sama dengan amaun jadual ansuran RM '.number_format((float) $schedule->amount, 2).'.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingPayment = $this->latestPaymentForSchedule($schedule);
|
||||||
|
|
||||||
|
if ($transactionNo !== '' && Payment::where('transaction_no', $transactionNo)
|
||||||
|
->when($existingPayment, fn ($query) => $query->whereKeyNot($existingPayment->id))
|
||||||
|
->exists()) {
|
||||||
|
throw new RuntimeException('No. transaksi telah wujud dalam rekod bayaran.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$paymentPayload = [
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'installment_schedule_id' => $schedule->id,
|
||||||
|
'user_id' => $application->user_id,
|
||||||
|
'transaction_no' => $transactionNo ?: ($existingPayment?->transaction_no ?: 'BULK-'.now()->format('YmdHis').'-'.str()->upper(str()->random(6))),
|
||||||
|
'receipt_no' => $receiptNo,
|
||||||
|
'biller_code' => '37317',
|
||||||
|
'ref_1' => $application->no_akaun,
|
||||||
|
'ref_2' => $application->email ?: $application->mobile_no,
|
||||||
|
'amount' => $amount,
|
||||||
|
'status' => 'success',
|
||||||
|
'payload' => array_merge($existingPayment?->payload ?? [], [
|
||||||
|
'channel' => 'Bulk Manual Staff Entry',
|
||||||
|
'import_row' => $rowNumber,
|
||||||
|
'recorded_by' => $staff->displayName(),
|
||||||
|
'updated_via_bulk_payment' => true,
|
||||||
|
]),
|
||||||
|
'paid_at' => $paidAt,
|
||||||
|
'remarks' => $this->stringValue($payload, ['catatan', 'remarks', 'nota'])
|
||||||
|
?: ($existingPayment?->remarks ?: 'Bayaran direkodkan secara pukal oleh staff.'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$oldPaymentValues = $existingPayment?->toArray() ?? [];
|
||||||
|
$payment = $existingPayment
|
||||||
|
? tap($existingPayment)->update($paymentPayload)->refresh()
|
||||||
|
: Payment::create($paymentPayload);
|
||||||
|
|
||||||
|
$schedule->update([
|
||||||
|
'payment_status' => 'dibayar',
|
||||||
|
'paid_at' => $paidAt,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$historyAction = $oldPaymentValues === [] ? 'bayaran_pukal_direkodkan' : 'transaksi_bayaran_pukal_dikemaskini';
|
||||||
|
$historyRemarks = $oldPaymentValues === []
|
||||||
|
? 'Staff merekodkan bayaran pukal dengan no. resit '.$payment->receipt_no.'.'
|
||||||
|
: 'Staff mengemaskini transaksi bayaran pukal dengan no. resit '.$payment->receipt_no.'.';
|
||||||
|
|
||||||
|
$this->workflowStatusService->recordHistory(
|
||||||
|
$application,
|
||||||
|
$staff,
|
||||||
|
$historyAction,
|
||||||
|
$application->status,
|
||||||
|
$application->status,
|
||||||
|
$historyRemarks,
|
||||||
|
[
|
||||||
|
'payment_id' => $payment->id,
|
||||||
|
'receipt_no' => $payment->receipt_no,
|
||||||
|
'installment_schedule_id' => $schedule->id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Kemaskini Bayaran Pukal',
|
||||||
|
$oldPaymentValues === [] ? 'rekod_bayaran_pukal' : 'kemaskini_transaksi_bayaran_pukal',
|
||||||
|
$oldPaymentValues === []
|
||||||
|
? 'Staff merekodkan bayaran ansuran secara pukal.'
|
||||||
|
: 'Staff mengemaskini transaksi bayaran ansuran yang telah dibayar melalui Excel/CSV.',
|
||||||
|
$staff,
|
||||||
|
$oldPaymentValues,
|
||||||
|
$payment->toArray(),
|
||||||
|
$application,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $payment;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function resolveSchedule(array $payload): ?InstallmentSchedule
|
||||||
|
{
|
||||||
|
$scheduleId = $this->integerValue($payload, ['installment_schedule_id', 'schedule_id', 'id_jadual']);
|
||||||
|
|
||||||
|
if ($scheduleId) {
|
||||||
|
return InstallmentSchedule::with('application')->find($scheduleId);
|
||||||
|
}
|
||||||
|
|
||||||
|
$noAkaun = $this->stringValue($payload, ['no_akaun', 'account_no']);
|
||||||
|
$installmentNo = $this->integerValue($payload, ['ansuran_ke', 'installment_no', 'ansuran']);
|
||||||
|
|
||||||
|
if ($noAkaun === '' || ! $installmentNo) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return InstallmentSchedule::query()
|
||||||
|
->with('application')
|
||||||
|
->where('installment_no', $installmentNo)
|
||||||
|
->whereHas('application', function ($query) use ($noAkaun) {
|
||||||
|
$query->where('status', ApplicationStatus::ACTIVE->value)
|
||||||
|
->where('no_akaun', $noAkaun);
|
||||||
|
})
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function latestPaymentForSchedule(InstallmentSchedule $schedule): ?Payment
|
||||||
|
{
|
||||||
|
if ($schedule->relationLoaded('payments')) {
|
||||||
|
return $schedule->payments->sortByDesc(fn (Payment $payment) => $payment->paid_at?->timestamp ?? $payment->id)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $schedule->payments()->latest('paid_at')->latest('id')->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function hasPaymentIntent(array $payload): bool
|
||||||
|
{
|
||||||
|
return $this->stringValue($payload, ['no_resit', 'receipt_no', 'resit']) !== ''
|
||||||
|
|| $this->stringValue($payload, ['no_transaksi', 'transaction_no', 'transaksi']) !== ''
|
||||||
|
|| $this->stringValue($payload, ['tarikh_bayaran', 'paid_at']) !== ''
|
||||||
|
|| $this->stringValue($payload, ['amaun_bayaran', 'amount', 'jumlah_bayaran']) !== ''
|
||||||
|
|| $this->stringValue($payload, ['catatan', 'remarks', 'nota']) !== '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeRow(array $row): array
|
||||||
|
{
|
||||||
|
return collect($row)->mapWithKeys(function ($value, $key) {
|
||||||
|
$header = preg_replace('/^\xEF\xBB\xBF/u', '', trim((string) $key)) ?? '';
|
||||||
|
|
||||||
|
$normalizedKey = str($header)
|
||||||
|
->lower()
|
||||||
|
->replace([' ', '-', '/', '.'], '_')
|
||||||
|
->replace('__', '_')
|
||||||
|
->trim('_')
|
||||||
|
->value();
|
||||||
|
|
||||||
|
return [$normalizedKey => $value];
|
||||||
|
})->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stringValue(array $payload, array $keys): string
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (! array_key_exists($key, $payload) || $payload[$key] === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $payload[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function integerValue(array $payload, array $keys): ?int
|
||||||
|
{
|
||||||
|
$value = $this->stringValue($payload, $keys);
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return (int) $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
$digits = preg_replace('/[^0-9]/', '', $value);
|
||||||
|
|
||||||
|
return $digits === '' ? null : (int) $digits;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function decimalValue(array $payload, array $keys): ?float
|
||||||
|
{
|
||||||
|
$value = $this->stringValue($payload, $keys);
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = preg_replace('/[^0-9.\-]/', '', str($value)->replace(',', '')->toString());
|
||||||
|
|
||||||
|
return $normalized === '' ? null : (float) $normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function dateValue(array $payload, array $keys): ?Carbon
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (! array_key_exists($key, $payload) || blank($payload[$key])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = $payload[$key];
|
||||||
|
|
||||||
|
if (is_numeric($value)) {
|
||||||
|
return Carbon::instance(ExcelDate::excelToDateTimeObject((float) $value));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Carbon::parse((string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
119
src/app/Services/InstallmentManualScheduleService.php
Normal file
119
src/app/Services/InstallmentManualScheduleService.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\InstallmentSchedule;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
class InstallmentManualScheduleService
|
||||||
|
{
|
||||||
|
public function saveDraft(InstallmentApplication $application, array $schedules, User $actor): array
|
||||||
|
{
|
||||||
|
return $this->persist($application, $schedules, $actor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function confirm(InstallmentApplication $application, array $schedules, User $actor): array
|
||||||
|
{
|
||||||
|
$evaluation = $this->evaluateSchedules($application, $schedules);
|
||||||
|
|
||||||
|
if (! $evaluation['matches_total']) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'schedules' => 'Jumlah ansuran tidak sama dengan Jumlah Tunggakan + (Cukai Harta / 2). Sila semak semula amaun ansuran.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->persist($application, $schedules, $actor, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultDueDate(int $sequence): string
|
||||||
|
{
|
||||||
|
return now()->startOfMonth()->addMonths($sequence)->format('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function persist(InstallmentApplication $application, array $schedules, User $actor, bool $confirm): array
|
||||||
|
{
|
||||||
|
$evaluation = $this->evaluateSchedules($application, $schedules);
|
||||||
|
$normalized = $evaluation['normalized'];
|
||||||
|
$total = $evaluation['total'];
|
||||||
|
$arrears = $evaluation['arrears'];
|
||||||
|
$propertyTaxHalf = $evaluation['property_tax_half'];
|
||||||
|
$targetTotal = $evaluation['target_total'];
|
||||||
|
$matches = $evaluation['matches_total'];
|
||||||
|
|
||||||
|
DB::transaction(function () use ($application, $normalized, $actor, $confirm, $total) {
|
||||||
|
$application->schedules()->delete();
|
||||||
|
|
||||||
|
$normalized->each(function (array $schedule) use ($application, $actor, $confirm): void {
|
||||||
|
InstallmentSchedule::create([
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'installment_no' => $schedule['installment_no'],
|
||||||
|
'due_date' => $schedule['due_date'],
|
||||||
|
'amount' => $schedule['amount'],
|
||||||
|
'payment_status' => 'belum_bayar',
|
||||||
|
'notes' => $schedule['notes'],
|
||||||
|
'is_confirmed' => $confirm,
|
||||||
|
'created_by' => $actor->id,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'schedule_total' => $total,
|
||||||
|
'schedule_status' => $confirm ? 'disahkan' : 'draf',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'total' => number_format($total, 2, '.', ''),
|
||||||
|
'arrears' => number_format($arrears, 2, '.', ''),
|
||||||
|
'property_tax_half' => number_format($propertyTaxHalf, 2, '.', ''),
|
||||||
|
'target_total' => number_format($targetTotal, 2, '.', ''),
|
||||||
|
'matches_total' => $matches,
|
||||||
|
'warning' => $matches ? null : 'Jumlah ansuran tidak sama dengan Jumlah Tunggakan + (Cukai Harta / 2). Sila semak semula amaun ansuran.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function evaluateSchedules(InstallmentApplication $application, array $schedules): array
|
||||||
|
{
|
||||||
|
$normalized = $this->normalizeSchedules($application, $schedules);
|
||||||
|
$total = (float) $normalized->sum('amount');
|
||||||
|
$arrears = (float) $application->total_arrears;
|
||||||
|
$application->loadMissing('propertyAccount');
|
||||||
|
$propertyTaxHalf = $application->property_tax_half_amount !== null
|
||||||
|
? (float) $application->property_tax_half_amount
|
||||||
|
: (float) ($application->propertyAccount?->cukai_harta ?? 0) / 2;
|
||||||
|
$targetTotal = $arrears + $propertyTaxHalf;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'normalized' => $normalized,
|
||||||
|
'total' => $total,
|
||||||
|
'arrears' => $arrears,
|
||||||
|
'property_tax_half' => $propertyTaxHalf,
|
||||||
|
'target_total' => $targetTotal,
|
||||||
|
'matches_total' => abs($total - $targetTotal) < 0.01,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeSchedules(InstallmentApplication $application, array $schedules): Collection
|
||||||
|
{
|
||||||
|
return collect($schedules)
|
||||||
|
->take($application->installment_months)
|
||||||
|
->values()
|
||||||
|
->map(function ($schedule, int $index) {
|
||||||
|
$sequence = $index + 1;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'installment_no' => $sequence,
|
||||||
|
'due_date' => ! empty($schedule['due_date'])
|
||||||
|
? Carbon::parse($schedule['due_date'])->format('Y-m-d')
|
||||||
|
: $this->defaultDueDate($sequence),
|
||||||
|
'amount' => round((float) ($schedule['amount'] ?? 0), 2),
|
||||||
|
'notes' => $schedule['notes'] ?? null,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/app/Services/NotificationService.php
Normal file
43
src/app/Services/NotificationService.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\EmailLog;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\SystemNotification;
|
||||||
|
use App\Models\User;
|
||||||
|
|
||||||
|
class NotificationService
|
||||||
|
{
|
||||||
|
public function createNotification(User $user, string $title, string $message, ?string $actionUrl = null, string $type = 'system'): SystemNotification
|
||||||
|
{
|
||||||
|
return SystemNotification::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'type' => $type,
|
||||||
|
'title' => $title,
|
||||||
|
'message' => $message,
|
||||||
|
'action_url' => $actionUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createDummyEmail(User $user, string $subject, string $body, ?InstallmentApplication $application = null): EmailLog
|
||||||
|
{
|
||||||
|
return EmailLog::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
'installment_application_id' => $application?->id,
|
||||||
|
'recipient_email' => $user->email,
|
||||||
|
'subject' => $subject,
|
||||||
|
'body' => $body,
|
||||||
|
'status' => 'sent',
|
||||||
|
'sent_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function notifyDecision(User $user, InstallmentApplication $application, string $title, string $message): void
|
||||||
|
{
|
||||||
|
$url = route('portal.applications.show', $application);
|
||||||
|
|
||||||
|
$this->createNotification($user, $title, $message, $url, 'application');
|
||||||
|
$this->createDummyEmail($user, $title, $message, $application);
|
||||||
|
}
|
||||||
|
}
|
||||||
265
src/app/Services/PropertyAccountImportService.php
Normal file
265
src/app/Services/PropertyAccountImportService.php
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Imports\PropertyAccountsImport;
|
||||||
|
use App\Models\ImportBatch;
|
||||||
|
use App\Models\PropertyAccount;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Maatwebsite\Excel\Facades\Excel;
|
||||||
|
|
||||||
|
class PropertyAccountImportService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected AuditTrailService $auditTrailService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import(UploadedFile $file, User $user, string $mode = 'arrears'): ImportBatch
|
||||||
|
{
|
||||||
|
$storedPath = $file->store('imports');
|
||||||
|
$rows = Excel::toCollection(new PropertyAccountsImport(), Storage::path($storedPath))->first() ?? collect();
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($file, $storedPath, $rows, $user, $mode) {
|
||||||
|
$batch = ImportBatch::create([
|
||||||
|
'filename' => $file->getClientOriginalName(),
|
||||||
|
'stored_path' => $storedPath,
|
||||||
|
'import_type' => $mode,
|
||||||
|
'uploaded_by' => $user->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$stats = [
|
||||||
|
'processed' => 0,
|
||||||
|
'new' => 0,
|
||||||
|
'updated' => 0,
|
||||||
|
'failed' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
$seenIds = [];
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$stats['processed']++;
|
||||||
|
$payload = $this->mapRow($row instanceof Collection ? $row->toArray() : (array) $row);
|
||||||
|
|
||||||
|
if ($this->rowIsInvalid($payload, $mode)) {
|
||||||
|
$stats['failed']++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->preparePayload($payload, $mode);
|
||||||
|
|
||||||
|
$account = PropertyAccount::query()
|
||||||
|
->where('no_akaun', $payload['no_akaun'])
|
||||||
|
->when(filled($payload['no_akaun_tunggakan'] ?? null), fn ($query) => $query->orWhere('no_akaun_tunggakan', $payload['no_akaun_tunggakan']))
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($account) {
|
||||||
|
if ($mode === 'tax' && blank($payload['no_akaun_tunggakan'] ?? null)) {
|
||||||
|
unset($payload['no_akaun_tunggakan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode === 'tax' && ($payload['pemilik'] ?? null) === 'Tidak dinyatakan') {
|
||||||
|
unset($payload['pemilik']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$account->update(array_merge($payload, [
|
||||||
|
'status_data' => 'Aktif',
|
||||||
|
'last_imported_at' => now(),
|
||||||
|
'import_batch_id' => $batch->id,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$stats['updated']++;
|
||||||
|
$seenIds[] = $account->id;
|
||||||
|
} else {
|
||||||
|
$payload['no_akaun_tunggakan'] = ($payload['no_akaun_tunggakan'] ?? '') ?: $payload['no_akaun'];
|
||||||
|
|
||||||
|
$account = PropertyAccount::create(array_merge($payload, [
|
||||||
|
'status_data' => 'Aktif',
|
||||||
|
'last_imported_at' => now(),
|
||||||
|
'import_batch_id' => $batch->id,
|
||||||
|
]));
|
||||||
|
|
||||||
|
$stats['new']++;
|
||||||
|
$seenIds[] = $account->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$missingCount = 0;
|
||||||
|
|
||||||
|
if ($mode === 'arrears') {
|
||||||
|
$missingCount = PropertyAccount::query()
|
||||||
|
->when($seenIds !== [], fn ($query) => $query->whereNotIn('id', $seenIds))
|
||||||
|
->update(['status_data' => 'Tiada Dalam Import Terkini']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$batch->update([
|
||||||
|
'total_processed' => $stats['processed'],
|
||||||
|
'total_new' => $stats['new'],
|
||||||
|
'total_updated' => $stats['updated'],
|
||||||
|
'total_missing' => $missingCount,
|
||||||
|
'total_failed' => $stats['failed'],
|
||||||
|
'summary' => [
|
||||||
|
'mode' => $mode,
|
||||||
|
'processed_at' => Carbon::now()->toDateTimeString(),
|
||||||
|
'seen_account_ids' => $seenIds,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->auditTrailService->log(
|
||||||
|
'Import Excel',
|
||||||
|
'upload_excel',
|
||||||
|
$mode === 'tax'
|
||||||
|
? 'Fail Excel data cukai harta dimuat naik dan diproses oleh admin.'
|
||||||
|
: 'Fail Excel data akaun tunggakan dimuat naik dan diproses oleh staff.',
|
||||||
|
$user,
|
||||||
|
[],
|
||||||
|
[
|
||||||
|
'batch_id' => $batch->id,
|
||||||
|
'filename' => $batch->filename,
|
||||||
|
'stats' => $batch->summary,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $batch->refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function mapRow(array $row): array
|
||||||
|
{
|
||||||
|
$normalized = collect($row)->mapWithKeys(function ($value, $key) {
|
||||||
|
$key = $this->normalizeHeaderKey($key);
|
||||||
|
|
||||||
|
return [$key => $value];
|
||||||
|
});
|
||||||
|
|
||||||
|
return [
|
||||||
|
'no_akaun' => $this->stringValue($normalized, ['no_akaun', 'no._akaun', 'akaun_no', 'account_no']),
|
||||||
|
'no_akaun_tunggakan' => $this->stringValue($normalized, ['no_akaun_tunggakan', 'no._akaun_tunggakan', 'akaun_tunggakan', 'account_no_tunggakan']),
|
||||||
|
'no_bgnn' => $this->stringValue($normalized, ['no_bgnn', 'no_bangunan', 'no._bgnn', 'no._bangunan']),
|
||||||
|
'nama_jalan' => $this->stringValue($normalized, ['nama_jalan', 'jalan']),
|
||||||
|
'no_lot' => $this->stringValue($normalized, ['no_lot', 'lot_no', 'lot']),
|
||||||
|
'no_dhm' => $this->stringValue($normalized, ['no_dhm', 'dhm_no', 'dhm']),
|
||||||
|
'pemilik' => $this->stringValue($normalized, ['pemilik', 'nama_pemilik']),
|
||||||
|
'warganegara' => $this->stringValue($normalized, ['warganegara']),
|
||||||
|
'pelanggan' => $this->stringValue($normalized, ['pelanggan']),
|
||||||
|
'status' => $this->stringValue($normalized, ['status']),
|
||||||
|
'bandar' => $this->stringValue($normalized, ['bandar']),
|
||||||
|
'taman' => $this->stringValue($normalized, ['taman']),
|
||||||
|
'rupacara' => $this->stringValue($normalized, ['rupacara']),
|
||||||
|
'kadar' => $this->stringValue($normalized, ['kadar']),
|
||||||
|
'cukai_harta' => $this->decimalValue($normalized, ['cukai_harta', 'cukai']),
|
||||||
|
'aktif' => $this->stringValue($normalized, ['aktif']),
|
||||||
|
'tunggakan' => $this->decimalValue($normalized, ['tunggakan']) ?? 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function rowIsInvalid(array $payload, string $mode): bool
|
||||||
|
{
|
||||||
|
if ($mode === 'tax') {
|
||||||
|
return blank($payload['no_akaun']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return blank($payload['no_akaun']) && blank($payload['no_akaun_tunggakan']);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function preparePayload(array $payload, string $mode): array
|
||||||
|
{
|
||||||
|
$payload['no_akaun'] = $payload['no_akaun'] ?: $payload['no_akaun_tunggakan'];
|
||||||
|
|
||||||
|
if ($mode !== 'tax') {
|
||||||
|
$payload['no_akaun_tunggakan'] = $payload['no_akaun_tunggakan'] ?: $payload['no_akaun'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = $this->withoutBlankOptionalPropertyFields($payload);
|
||||||
|
|
||||||
|
if ($mode === 'tax') {
|
||||||
|
$payload['pemilik'] = $payload['pemilik'] ?: 'Tidak dinyatakan';
|
||||||
|
$payload['cukai_harta'] = $payload['cukai_harta'] ?? 0;
|
||||||
|
|
||||||
|
return collect($payload)->only([
|
||||||
|
'no_akaun',
|
||||||
|
'no_akaun_tunggakan',
|
||||||
|
'no_bgnn',
|
||||||
|
'nama_jalan',
|
||||||
|
'no_lot',
|
||||||
|
'no_dhm',
|
||||||
|
'pemilik',
|
||||||
|
'bandar',
|
||||||
|
'taman',
|
||||||
|
'rupacara',
|
||||||
|
'kadar',
|
||||||
|
'cukai_harta',
|
||||||
|
])->toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($payload['cukai_harta'] === null) {
|
||||||
|
unset($payload['cukai_harta']);
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload['no_akaun_tunggakan'] = $payload['no_akaun_tunggakan'] ?: $payload['no_akaun'];
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function withoutBlankOptionalPropertyFields(array $payload): array
|
||||||
|
{
|
||||||
|
foreach (['no_bgnn', 'nama_jalan', 'no_lot', 'no_dhm'] as $field) {
|
||||||
|
if (array_key_exists($field, $payload) && blank($payload[$field])) {
|
||||||
|
unset($payload[$field]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function stringValue(Collection $normalized, array $keys): string
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
$value = $normalized->get($key);
|
||||||
|
|
||||||
|
if ($value === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trim((string) $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function decimalValue(Collection $normalized, array $keys): ?float
|
||||||
|
{
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
if (! $normalized->has($key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$value = trim((string) $normalized->get($key));
|
||||||
|
|
||||||
|
if ($value === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float) str($value)->replace(',', '')->toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function normalizeHeaderKey(mixed $key): string
|
||||||
|
{
|
||||||
|
$header = preg_replace('/^\xEF\xBB\xBF/u', '', trim((string) $key)) ?? '';
|
||||||
|
|
||||||
|
return str($header)
|
||||||
|
->lower()
|
||||||
|
->replace([' ', '-', '/', '.'], '_')
|
||||||
|
->replace('__', '_')
|
||||||
|
->trim('_')
|
||||||
|
->value();
|
||||||
|
}
|
||||||
|
}
|
||||||
81
src/app/Services/ReportService.php
Normal file
81
src/app/Services/ReportService.php
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\Payment;
|
||||||
|
use App\Models\PropertyAccount;
|
||||||
|
use App\Models\Task;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ReportService
|
||||||
|
{
|
||||||
|
protected function monthExpression(string $column): string
|
||||||
|
{
|
||||||
|
return DB::connection()->getDriverName() === 'sqlite'
|
||||||
|
? "strftime('%Y-%m', {$column})"
|
||||||
|
: "DATE_FORMAT({$column}, '%Y-%m')";
|
||||||
|
}
|
||||||
|
|
||||||
|
public function dashboardSummary(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'new_applications' => InstallmentApplication::whereDate('created_at', today())->count(),
|
||||||
|
'approved_applications' => InstallmentApplication::where('status', 'aktif')->count(),
|
||||||
|
'failed_applications' => InstallmentApplication::where('status', 'gagal')->count(),
|
||||||
|
'cancelled_applications' => InstallmentApplication::where('status', 'batal')->count(),
|
||||||
|
'active_accounts' => InstallmentApplication::where('status', 'aktif')->count(),
|
||||||
|
'payments_received' => Payment::where('status', 'success')->sum('amount'),
|
||||||
|
'outstanding_arrears' => PropertyAccount::sum('tunggakan'),
|
||||||
|
'completed_accounts' => InstallmentApplication::where('status', 'selesai')->count(),
|
||||||
|
'public_users' => User::whereHas('role', fn ($query) => $query->where('slug', 'public'))->count(),
|
||||||
|
'open_tasks' => Task::where('status', 'belum_selesai')->count(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function monthlyApplications()
|
||||||
|
{
|
||||||
|
$monthExpression = $this->monthExpression('created_at');
|
||||||
|
|
||||||
|
return InstallmentApplication::selectRaw("{$monthExpression} as month, count(*) as total")
|
||||||
|
->groupBy('month')
|
||||||
|
->orderBy('month')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function monthlyPayments()
|
||||||
|
{
|
||||||
|
$monthExpression = $this->monthExpression('created_at');
|
||||||
|
|
||||||
|
return Payment::where('status', 'success')
|
||||||
|
->selectRaw("{$monthExpression} as month, sum(amount) as total")
|
||||||
|
->groupBy('month')
|
||||||
|
->orderBy('month')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function categoryBreakdown()
|
||||||
|
{
|
||||||
|
return InstallmentApplication::select('category', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('category')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function areaBreakdown()
|
||||||
|
{
|
||||||
|
return PropertyAccount::select('bandar', DB::raw('count(*) as total'))
|
||||||
|
->groupBy('bandar')
|
||||||
|
->orderByDesc('total')
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function staffPerformance()
|
||||||
|
{
|
||||||
|
return User::query()
|
||||||
|
->select('users.id', 'users.name')
|
||||||
|
->withCount(['reviewedApplications as total_reviewed'])
|
||||||
|
->whereHas('role', fn ($query) => $query->where('slug', 'staff'))
|
||||||
|
->get();
|
||||||
|
}
|
||||||
|
}
|
||||||
227
src/app/Services/WorkflowStatusService.php
Normal file
227
src/app/Services/WorkflowStatusService.php
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Enums\ApplicationStatus;
|
||||||
|
use App\Models\ApplicationHistory;
|
||||||
|
use App\Models\InstallmentApplication;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
|
||||||
|
class WorkflowStatusService
|
||||||
|
{
|
||||||
|
public function applyProcessing(InstallmentApplication $application, User $reviewer, ?string $remarks = null, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::PROCESSING->value,
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'approved_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'akaun_diproses',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::PROCESSING->value,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyAccepted(InstallmentApplication $application, User $reviewer, ?string $remarks = null, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::ACTIVE->value,
|
||||||
|
'schedule_status' => 'disahkan',
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'approved_at' => now(),
|
||||||
|
'rejected_at' => null,
|
||||||
|
'rejection_reason' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'permohonan_diterima',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::ACTIVE->value,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyRejected(InstallmentApplication $application, User $reviewer, string $remarks, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::FAILED->value,
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'rejected_at' => now(),
|
||||||
|
'rejection_reason' => $remarks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'permohonan_ditolak',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::FAILED->value,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyFailed(InstallmentApplication $application, User $reviewer, string $remarks, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::FAILED->value,
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
'rejected_at' => now(),
|
||||||
|
'rejection_reason' => $remarks,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'akaun_gagal',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::FAILED->value,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyCancelled(InstallmentApplication $application, User $reviewer, string $remarks, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::CANCELLED->value,
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'akaun_dibatalkan',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::CANCELLED->value,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyCompleted(InstallmentApplication $application, User $reviewer, ?string $remarks = null, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::COMPLETED->value,
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'akaun_ansuran_selesai',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::COMPLETED->value,
|
||||||
|
$remarks ?: 'Akaun disahkan selesai oleh staff.',
|
||||||
|
$metadata + ['completed_at' => Carbon::now()->toDateTimeString()],
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function applyNoAction(InstallmentApplication $application, User $reviewer, ?string $remarks = null, array $metadata = []): InstallmentApplication
|
||||||
|
{
|
||||||
|
$application->update([
|
||||||
|
'staff_remarks' => $remarks,
|
||||||
|
'reviewed_by' => $reviewer->id,
|
||||||
|
'reviewed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$reviewer,
|
||||||
|
'tiada_tindakan',
|
||||||
|
$application->status,
|
||||||
|
$application->status,
|
||||||
|
$remarks,
|
||||||
|
$metadata,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $application->refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markCompletedIfPaid(InstallmentApplication $application, ?User $actor = null): void
|
||||||
|
{
|
||||||
|
if ($application->schedules()->exists()
|
||||||
|
&& $application->schedules()->where('payment_status', '!=', 'dibayar')->where('payment_status', '!=', 'selesai')->doesntExist()) {
|
||||||
|
$oldStatus = $application->status;
|
||||||
|
$application->update([
|
||||||
|
'status' => ApplicationStatus::COMPLETED->value,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->recordHistory(
|
||||||
|
$application,
|
||||||
|
$actor,
|
||||||
|
'akaun_ansuran_selesai',
|
||||||
|
$oldStatus,
|
||||||
|
ApplicationStatus::COMPLETED->value,
|
||||||
|
'Semua ansuran telah dibayar.',
|
||||||
|
['completed_at' => Carbon::now()->toDateTimeString()],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function recordHistory(
|
||||||
|
InstallmentApplication $application,
|
||||||
|
?User $user,
|
||||||
|
string $action,
|
||||||
|
?string $oldStatus,
|
||||||
|
?string $newStatus,
|
||||||
|
?string $remarks = null,
|
||||||
|
array $metadata = [],
|
||||||
|
): ApplicationHistory {
|
||||||
|
return ApplicationHistory::create([
|
||||||
|
'installment_application_id' => $application->id,
|
||||||
|
'user_id' => $user?->id,
|
||||||
|
'action' => $action,
|
||||||
|
'old_status' => $oldStatus,
|
||||||
|
'new_status' => $newStatus,
|
||||||
|
'remarks' => $remarks,
|
||||||
|
'metadata' => $metadata ?: null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/artisan
Normal file
18
src/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);
|
||||||
21
src/bootstrap/app.php
Normal file
21
src/bootstrap/app.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Application;
|
||||||
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
use Illuminate\Foundation\Configuration\Middleware;
|
||||||
|
|
||||||
|
return Application::configure(basePath: dirname(__DIR__))
|
||||||
|
->withRouting(
|
||||||
|
web: __DIR__.'/../routes/web.php',
|
||||||
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
health: '/up',
|
||||||
|
)
|
||||||
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
|
$middleware->alias([
|
||||||
|
'role' => \App\Http\Middleware\EnsureRole::class,
|
||||||
|
'profile.completed' => \App\Http\Middleware\EnsureProfileCompleted::class,
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
|
//
|
||||||
|
})->create();
|
||||||
2
src/bootstrap/cache/.gitignore
vendored
Normal file
2
src/bootstrap/cache/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
||||||
7
src/bootstrap/providers.php
Normal file
7
src/bootstrap/providers.php
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Providers\AppServiceProvider;
|
||||||
|
|
||||||
|
return [
|
||||||
|
AppServiceProvider::class,
|
||||||
|
];
|
||||||
87
src/composer.json
Normal file
87
src/composer.json
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"$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",
|
||||||
|
"maatwebsite/excel": "^3.1"
|
||||||
|
},
|
||||||
|
"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
|
||||||
|
}
|
||||||
8979
src/composer.lock
generated
Normal file
8979
src/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
126
src/config/app.php
Normal file
126
src/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' => env('APP_TIMEZONE', 'Asia/Kuala_Lumpur'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| 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
src/config/auth.php
Normal file
117
src/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),
|
||||||
|
|
||||||
|
];
|
||||||
130
src/config/cache.php
Normal file
130
src/config/cache.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?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", "octane",
|
||||||
|
| "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'),
|
||||||
|
],
|
||||||
|
|
||||||
|
'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
src/config/database.php
Normal file
184
src/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
src/config/filesystems.php
Normal file
80
src/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
src/config/logging.php
Normal file
132
src/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
src/config/mail.php
Normal file
118
src/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')),
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
129
src/config/queue.php
Normal file
129
src/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
src/config/services.php
Normal file
38
src/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
src/config/session.php
Normal file
233
src/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
src/database/.gitignore
vendored
Normal file
1
src/database/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*.sqlite*
|
||||||
45
src/database/factories/UserFactory.php
Normal file
45
src/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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
<?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->unsignedBigInteger('role_id')->nullable()->index();
|
||||||
|
$table->unsignedBigInteger('department_id')->nullable()->index();
|
||||||
|
$table->boolean('is_active')->default(true);
|
||||||
|
$table->timestamp('last_login_at')->nullable();
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user