From d60dd8bf3925f67a2c5e7a78451f146d8dd3600f Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Sun, 6 Sep 2026 12:41:58 +0800 Subject: [PATCH] Explore/ismail (#15) Co-authored-by: ISMAIL MASSERAN Co-authored-by: ISMAIL MASSERAN Co-authored-by: nurafrinaalimi16 Reviewed-on: https://git.koppkb.com/KoPKB/kaunter/pulls/15 --- .env.docker | 54 ++ .env.example | 2 +- .env.staging | 11 +- .envy | 50 ++ .gitea/workflows/deploy-staging.yml | 8 +- .gitignore | 2 +- README.md | 50 ++ REMARK.md | 27 + .../Auth/ImpersonateController.php | 52 ++ app/Http/Controllers/Auth/LoginController.php | 46 +- app/Http/Controllers/CustomersController.php | 120 ++- app/Http/Controllers/GadaianController.php | 154 +++- app/Http/Controllers/KakitanganController.php | 39 +- app/Http/Controllers/KhazanahController.php | 3 + app/Http/Controllers/OperasiController.php | 161 +++- .../PembiayaanOnlineController.php | 842 ++++++++++++++++++ app/Http/Middleware/TrustProxies.php | 2 +- app/Providers/AppServiceProvider.php | 4 +- app/Providers/AuthServiceProvider.php | 4 +- app/User.php | 12 + app/helpers.php | 15 + composer.json | 4 + composer.lock | 75 +- config/api.php | 2 +- config/app.php | 2 + config/mykad-reader.php | 4 +- docker-compose.staging.yml | 17 +- docker/nginx/default.conf | 5 +- docker/php/Dockerfile | 40 +- public/js/mykad-reader-customer.js | 662 ++++++++------ .../views/admin/customer_index.blade.php | 2 +- resources/views/admin/customer_info.blade.php | 2 +- resources/views/admin/poskod.blade.php | 2 +- resources/views/aliran_tunai/index.blade.php | 9 +- resources/views/auth/v2/login.blade.php | 4 +- .../views/callcenter/callnotisjbg2.blade.php | 2 +- resources/views/callcenter/index.blade.php | 2 +- .../views/callcenter/indexnama.blade.php | 2 +- resources/views/callcenter/v1/info.blade.php | 2 +- resources/views/callcenter/v2/index.blade.php | 2 +- resources/views/callcenter/v2/info.blade.php | 2 +- .../views/components/datatable.blade.php | 137 +++ .../views/components/print/footer.blade.php | 44 + .../views/components/print/header.blade.php | 101 +++ .../components/print/watermark.blade.php | 25 + resources/views/customers/v1/index.blade.php | 13 +- resources/views/customers/v1/info.blade.php | 18 +- resources/views/customers/v2/index.blade.php | 82 +- resources/views/customers/v2/info.blade.php | 542 +++++------ .../borang_batch_pembiayaan_online.blade.php | 354 ++++++++ .../dummy/borang_pengesahan_online.blade.php | 322 +++++++ .../views/dummy/maklumat_pelanggan.blade.php | 292 ++++++ resources/views/errors/alert.blade.php | 2 +- resources/views/gadaian/v1/edit.blade.php | 2 +- resources/views/gadaian/v1/index.blade.php | 2 +- resources/views/gadaian/v2/edit.blade.php | 6 +- resources/views/gadaian/v2/index.blade.php | 315 ++++++- .../views/homepage/khazanah/home.blade.php | 6 +- .../views/homepage/operasi/home.blade.php | 17 +- .../views/homepage/penilai/home.blade.php | 2 +- .../views/homepage/teller/home.blade.php | 3 +- .../views/khazanah/agingbulanan.blade.php | 2 +- resources/views/khazanah/bajet.blade.php | 2 +- resources/views/khazanah/gadaian.blade.php | 2 +- resources/views/khazanah/index.blade.php | 2 +- resources/views/khazanah/info.blade.php | 2 +- resources/views/khazanah/kelulusan.blade.php | 4 +- .../views/khazanah/khazanahserahan.blade.php | 2 +- .../khazanah/laporan/agingbulanan.blade.php | 2 +- .../khazanah/laporan/alirantunai.blade.php | 2 +- .../views/khazanah/laporan/ansuran.blade.php | 2 +- .../laporan/batalsuratgadai.blade.php | 2 +- .../khazanah/laporan/datalejer_am.blade.php | 2 +- .../khazanah/laporan/denominasi.blade.php | 2 +- .../views/khazanah/laporan/gadaian.blade.php | 2 +- .../khazanah/laporan/imbangduga.blade.php | 2 +- .../laporan/imbangduga_akru.blade.php | 2 +- .../khazanah/laporan/kawalanstok.blade.php | 2 +- .../khazanah/laporan/marhunharian.blade.php | 2 +- .../laporan/marhunterkumpul.blade.php | 2 +- .../khazanah/laporan/pelangganbaru.blade.php | 2 +- .../khazanah/laporan/penebusan.blade.php | 2 +- .../khazanah/laporan/perbelanjaan.blade.php | 2 +- .../views/khazanah/laporan/petibesi.blade.php | 2 +- .../laporan/ringkasanharian.blade.php | 2 +- .../laporan/ringkasanterkumpul.blade.php | 2 +- .../khazanah/laporan/serahgadai.blade.php | 2 +- .../khazanah/laporan/serahmarhun.blade.php | 2 +- .../laporan/stokauditbersih.blade.php | 2 +- .../laporan/stokauditdetails.blade.php | 2 +- .../khazanah/laporan/tuntutbaki.blade.php | 2 +- .../views/khazanah/laporan/upah.blade.php | 2 +- .../views/khazanah/laporan/wakil.blade.php | 2 +- .../laporanujrah/ansuranujrah.blade.php | 2 +- .../laporanujrah/gadaianujrah.blade.php | 2 +- .../ringkasanharianujrah.blade.php | 2 +- resources/views/khazanah/panjar.blade.php | 4 +- .../views/khazanah/pendahuluan.blade.php | 2 +- .../views/khazanah/pendahuluanserah.blade.php | 2 +- .../khazanah/pendahuluanserahanmain.blade.php | 2 +- .../views/khazanah/permohonanwang.blade.php | 2 +- .../views/khazanah/rekodpermohonan.blade.php | 2 +- resources/views/khazanah/saginfo.blade.php | 2 +- resources/views/khazanah/serahan.blade.php | 2 +- resources/views/khazanah/tebus.blade.php | 2 +- resources/views/khazanah/v1/info.blade.php | 2 +- resources/views/khazanah/v2/info.blade.php | 2 +- resources/views/khazanah/wakil.blade.php | 2 +- resources/views/layouts/app.blade.php | 6 +- .../views/layouts/app_callcenter.blade.php | 3 + resources/views/layouts/app_operasi.blade.php | 3 + .../partials/impersonation_nav.blade.php | 11 + .../views/lelongan/index-khazanah.blade.php | 6 +- resources/views/lelongan/index.blade.php | 4 +- .../views/onetimepassword/home.blade.php | 2 +- .../views/operasi/daftar_kakitangan.blade.php | 146 +-- .../views/operasi/edit_marginmutu.blade.php | 2 +- resources/views/operasi/hargaemas.blade.php | 2 +- resources/views/operasi/kelulusan.blade.php | 4 +- .../views/operasi/komuditi/index.blade.php | 2 +- .../komuditi/rekod_transaksi.blade.php | 2 +- .../pembiayaan_online/operasi/batch.blade.php | 276 ++++++ .../pembiayaan_online/operasi/eod.blade.php | 152 ++++ .../operasi/senarai.blade.php | 102 +++ .../pembiayaan_online/operasi/topup.blade.php | 198 ++++ .../pembiayaan_online/pc/batch.blade.php | 65 ++ .../views/pembiayaan_online/pc/paid.blade.php | 187 ++++ .../pembiayaan_online/pc/senarai.blade.php | 304 +++++++ .../pembiayaan_online/pc/topup.blade.php | 215 +++++ .../penebusan/v1/index-khazanah.blade.php | 2 +- resources/views/penebusan/v1/index.blade.php | 2 +- .../penebusan/v2/index-khazanah.blade.php | 2 +- .../views/penebusan/v2/index.blade-backup.php | 2 +- resources/views/penebusan/v2/index.blade.php | 4 +- .../borang_batch_pembiayaan_online.blade.php | 336 +++++++ .../print/borang_pengesahan_online.blade.php | 302 +++++++ .../views/print/maklumat_pelanggan.blade.php | 260 ++++++ resources/views/print/sag_ujrah_3.blade.php | 118 ++- routes/web.php | 52 ++ 139 files changed, 6788 insertions(+), 867 deletions(-) create mode 100644 .env.docker create mode 100644 .envy create mode 100644 REMARK.md create mode 100644 app/Http/Controllers/Auth/ImpersonateController.php create mode 100644 app/Http/Controllers/PembiayaanOnlineController.php create mode 100644 app/helpers.php create mode 100644 resources/views/components/datatable.blade.php create mode 100644 resources/views/components/print/footer.blade.php create mode 100644 resources/views/components/print/header.blade.php create mode 100644 resources/views/components/print/watermark.blade.php create mode 100644 resources/views/dummy/borang_batch_pembiayaan_online.blade.php create mode 100644 resources/views/dummy/borang_pengesahan_online.blade.php create mode 100644 resources/views/dummy/maklumat_pelanggan.blade.php create mode 100644 resources/views/layouts/partials/impersonation_nav.blade.php create mode 100644 resources/views/pembiayaan_online/operasi/batch.blade.php create mode 100644 resources/views/pembiayaan_online/operasi/eod.blade.php create mode 100644 resources/views/pembiayaan_online/operasi/senarai.blade.php create mode 100644 resources/views/pembiayaan_online/operasi/topup.blade.php create mode 100644 resources/views/pembiayaan_online/pc/batch.blade.php create mode 100644 resources/views/pembiayaan_online/pc/paid.blade.php create mode 100644 resources/views/pembiayaan_online/pc/senarai.blade.php create mode 100644 resources/views/pembiayaan_online/pc/topup.blade.php create mode 100644 resources/views/print/borang_batch_pembiayaan_online.blade.php create mode 100644 resources/views/print/borang_pengesahan_online.blade.php create mode 100644 resources/views/print/maklumat_pelanggan.blade.php diff --git a/.env.docker b/.env.docker new file mode 100644 index 00000000..79c1ea3a --- /dev/null +++ b/.env.docker @@ -0,0 +1,54 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= +APP_DEBUG=true +APP_URL=http://localhost +# API_URL=http://api-annurv2.test +API_URL=http://host.docker.internal:8080 +API_URL_KOOP=http://host.docker.internal:8080 +API_URL_PKB=http://host.docker.internal:8080 +API_ARRAHNBID=http://lelong.test/api/ + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=kaunter_erahn +DB_USERNAME=kaunteruser +DB_PASSWORD=password + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +PUSHER_APP_ID=1132718 +PUSHER_APP_KEY=26b0ec2f297f94b7aa9b +PUSHER_APP_SECRET=421b61fb5a8ecce477a6 + +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +MYKAD_READER_PORT=8085 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED \ No newline at end of file diff --git a/.env.example b/.env.example index 1e819c27..e7821ee4 100644 --- a/.env.example +++ b/.env.example @@ -46,5 +46,5 @@ AWS_BUCKET= MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -MYKAD_READER_PORT=8085 +MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED diff --git a/.env.staging b/.env.staging index 27304237..91b447ec 100644 --- a/.env.staging +++ b/.env.staging @@ -1,15 +1,16 @@ APP_NAME=Laravel APP_ENV=staging +APP_VERSION=v2.2-staging APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= APP_DEBUG=true -APP_URL=http://172.16.6.200:8091 +APP_URL=https://staging.erahn.com.my HTTP_PORT=8091 MYSQL_PUBLISH_PORT=3309 -API_URL=http://172.16.6.200:8090 -API_URL_KOOP=http://172.16.6.200:8090 -API_URL_PKB=http://172.16.6.200:8090 +API_URL=https://api-staging.erahn.com.my +API_URL_KOOP=https://api-staging.erahn.com.my +API_URL_PKB=https://api-staging.erahn.com.my API_ARRAHNBID=http://lelong.test/api/ LOG_CHANNEL=stack @@ -53,5 +54,5 @@ AWS_BUCKET= MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -MYKAD_READER_PORT=8085 +MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED \ No newline at end of file diff --git a/.envy b/.envy new file mode 100644 index 00000000..5c53f303 --- /dev/null +++ b/.envy @@ -0,0 +1,50 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:WRTHcFTq5YARKVdA8YDUzsh9Ue8vD63ivgZgXNNBiGo= +APP_DEBUG=true +APP_URL=http://ujrahkopkb.erahn.com.my +API_URL=http://apiujrah-kopkb.erahn.com.my +#API_ARRAHNBID=http://lelong.test/api/ + +LOG_CHANNEL=stack + +DB_CONNECTION=mysql +DB_HOST=127.0.0.1 +DB_PORT=3306 +DB_DATABASE=kaunter_erahn +DB_USERNAME=spider +DB_PASSWORD=G0dz!ll4 + +BROADCAST_DRIVER=pusher +CACHE_DRIVER=file +QUEUE_CONNECTION=sync +SESSION_DRIVER=file +SESSION_LIFETIME=120 + +REDIS_HOST=127.0.0.1 +REDIS_PASSWORD=null +REDIS_PORT=6379 + +PUSHER_APP_ID=1132718 +PUSHER_APP_KEY=26b0ec2f297f94b7aa9b +PUSHER_APP_SECRET=421b61fb5a8ecce477a6 + +MAIL_MAILER=smtp +MAIL_HOST=smtp.mailtrap.io +MAIL_PORT=2525 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS=null +MAIL_FROM_NAME="${APP_NAME}" + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_DEFAULT_REGION=us-east-1 +AWS_BUCKET= + +MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" +MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +MYKAD_READER_PORT=8085 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 30d130cd..ab79a87f 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -56,11 +56,11 @@ jobs: set -e IMAGE_TAG="${{ inputs.image_tag }}" - mkdir -p "${DEPLOY_DIR}/docker/nginx" + mkdir -p "${DEPLOY_DIR}/docker/nginx" "${DEPLOY_DIR}/public" cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml" cp docker/nginx/default.conf "${DEPLOY_DIR}/docker/nginx/default.conf" - rm -rf "${DEPLOY_DIR}/public" - cp -R public "${DEPLOY_DIR}/public" + find "${DEPLOY_DIR}/public" -mindepth 1 -delete + cp -R public/. "${DEPLOY_DIR}/public/" if [ ! -f "${DEPLOY_DIR}/.env.staging" ]; then if [ -f .env.staging ]; then @@ -78,7 +78,7 @@ jobs: export IMAGE_TAG docker compose --env-file .env.staging -f docker-compose.yml pull app - docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --remove-orphans + docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --force-recreate --remove-orphans app web echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}" diff --git a/.gitignore b/.gitignore index 1ed293c1..80ba177a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,5 @@ Homestead.yaml npm-debug.log yarn-error.log /.vscode -docker-compose.yml docker_src/ +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index f821d23f..ca86015c 100644 --- a/README.md +++ b/README.md @@ -1 +1,51 @@ If you does't know what is this you probably in wrong project + +```sql +--- for all ip +CREATE USER 'kaunteruser'@'%' IDENTIFIED BY 'your_password'; + +--- for specific ip +CREATE USER 'kaunteruser'@'localhost' IDENTIFIED BY 'your_password'; + +GRANT ALL PRIVILEGES ON kaunter_erahn.* TO 'kaunteruser'@'%'; +FLUSH PRIVILEGES; +``` + +# ADDITIONAL NOTE + +## customers/v2/index.blade.php is modal to register customer + +## Min Komoditi = RM5000 + +# System Flowchart + +## Proses Gadaian +1) Pelanggan datang bawa marhun +2) Teller daftar maklumat pelanggan jika pelanggan baharu +3) Serah marhun ke penilai untuk timbang +4) Teller daftar marhun di sistem berdasarkan maklumat dari penilai +5) Penilai print SAG dan serah marhun ke Khazanah (PC/PPC) - __khazanah can still scan still scan though__ +6) Khazanah scan barcode dekat SAG untuk masuk ke dalam peti besi +* table involved - gadai.semakbarcode (default is blank, after is Lulus) +7) DONE + +## Setup new branch command: +1. Duplicate previous branch table - use erahn_{{name}} +```sql +SET FOREIGN_KEY_CHECKS = 0; + +SET SESSION group_concat_max_len = 1000000; + +SELECT GROUP_CONCAT( + CONCAT('TRUNCATE TABLE `', table_name, '`') + SEPARATOR '; ' +) INTO @sql +FROM information_schema.tables +WHERE table_schema = DATABASE(); + +SELECT @sql; --- copy the output and paste it + +SET FOREIGN_KEY_CHECKS = 1; +``` +2. Add new cawangan kod on master_erahn.arrahn_master_db and and kaunter_erahn.cawangan +3. Register new cawangan detail in erahn_{{name}}.cawangan \ No newline at end of file diff --git a/REMARK.md b/REMARK.md new file mode 100644 index 00000000..a80c8c59 --- /dev/null +++ b/REMARK.md @@ -0,0 +1,27 @@ +## CAWANGAN XTRA - X0301 +- WAN NUR FATIN NATASYA BINTI W M AZHARI - PENILAI +- MOHAMAD KHAIRULNIZAN BIN JUSOF - TELLER +- MOHAMAD AZUHAIDIE BIN AHMAD - PPC +- WAN NURUL AMIRA BINTI WAN HARON - PC + +## Akaun +- admin@test.com +- admin2@test.com + +Module Gadai Online: +- Langkah 1 +- Langkah 2 +[x] PC akan tekan submit, then customer akan masuk ke senarai online mengikut batch +[x] PC boleh semak list customer yang pilih gadai online +- Langkah 3 (Operasi) +[x] dapat dalam bentuk list +[x] Apa jadi kalau operasi reject? - patah balik ke pc, untuk pc buat semakan balik +- Langkah 4 +- Langkah 5 +- Langkah 6 +- Langkah 7 +[x] Macam mana PC nak tahu pembayaran berjaya (Tally PAID page) +[x] Kalau online, dia kena upload kat mana (Operasi: PAID + bukti) +- Langkah 8 +[x] PC semak senarai pelanggan gadai tu tally atau tak +[ ] PC tekan butang blast ke (tanda selesai ada; WhatsApp/SMS belum wire) \ No newline at end of file diff --git a/app/Http/Controllers/Auth/ImpersonateController.php b/app/Http/Controllers/Auth/ImpersonateController.php new file mode 100644 index 00000000..dab764ab --- /dev/null +++ b/app/Http/Controllers/Auth/ImpersonateController.php @@ -0,0 +1,52 @@ +middleware('auth'); + } + + public function impersonate(Request $request, $id): RedirectResponse + { + Gate::authorize('impersonate'); + + $impersonator = $request->user(); + $id = (int) $id; + $target = User::findOrFail($id); + + if ($target->id === $request->user()->id) { + return back()->with('error', 'Tidak boleh impersonate diri sendiri.'); + } + + if (method_exists($target, 'canBeImpersonated') && !$target->canBeImpersonated()) { + return back()->with('error', 'Akaun ini tidak boleh di-impersonate.'); + } + + $request->user()->impersonate($target); + + return redirect()->route('home'); + } + + public function leave(Request $request): RedirectResponse + { + if ($request->user() && method_exists($request->user(), 'isImpersonated') && $request->user()->isImpersonated()) { + $impersonated = $request->user(); + $impersonator = method_exists($impersonated, 'impersonator') ? $impersonated->impersonator() : null; + + $request->user()->leaveImpersonation(); + } + + return redirect()->route('home'); + } +} + diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index f29fcc28..b9abb660 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -60,9 +60,7 @@ class LoginController extends Controller 'password' => 'required|string', ]); - $credentials = $request->only('email', 'password'); - - if (Auth::guard('web')->attempt($credentials)) { + if ($this->attemptWebLogin($request)) { $auth_user = Auth::guard('web')->user(); $api_url = config('api.url'); @@ -103,9 +101,7 @@ class LoginController extends Controller 'password' => 'required|string', ]); - $credentials = $request->only('email', 'password'); - - if (Auth::guard('dashboard')->attempt($credentials,true)) { + if ($this->attemptDashboardLogin($request)) { $auth_user = Auth::guard('dashboard')->user(); $user_login['nama'] = $auth_user['name']; @@ -187,4 +183,42 @@ class LoginController extends Controller return response()->json($kakitangan,200); } + + /** + * In local environment, log in by email only (password ignored). + */ + private function attemptWebLogin(Request $request): bool + { + if (app()->environment('local')) { + $user = User::where('email', $request->email)->first(); + if ($user) { + Auth::guard('web')->login($user); + + return true; + } + + return false; + } + + return Auth::guard('web')->attempt($request->only('email', 'password')); + } + + /** + * In local environment, log in by email only (password ignored). + */ + private function attemptDashboardLogin(Request $request): bool + { + if (app()->environment('local')) { + $user = Dashboard::where('email', $request->email)->first(); + if ($user) { + Auth::guard('dashboard')->login($user, true); + + return true; + } + + return false; + } + + return Auth::guard('dashboard')->attempt($request->only('email', 'password'), true); + } } diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php index 40fee0c9..518115cc 100644 --- a/app/Http/Controllers/CustomersController.php +++ b/app/Http/Controllers/CustomersController.php @@ -358,7 +358,7 @@ class CustomersController extends Controller ])->json(); } - $customer_info = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/customers/create/' . $auth_user['cawangan'], [ + $customer_payload = [ 'kodcaw' => $auth_user['cawangan'], 'nopelanggan' => $no_pelanggan, 'nokpbaru' => $nokpbaru, @@ -385,7 +385,14 @@ class CustomersController extends Controller 'nota_pekerjaan' => $request->nota_pekerjaan, 'notaarcc' => $request->notaarcc, 'tujuangadai' => $request->tujuan_gadai, - ]); + ]; + + if ($request->filled('photo_base64')) { + $customer_payload['photo_base64'] = $request->input('photo_base64'); + $customer_payload['photo_mime'] = $request->input('photo_mime') ?: 'image/jpeg'; + } + + $customer_info = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/customers/create/' . $auth_user['cawangan'], $customer_payload); Session::put('customer_info', $customer_info); Session::put('noic', $request->noic); @@ -574,4 +581,113 @@ class CustomersController extends Controller return redirect()->back()->with('error', 'Gagal mengemaskini status anggota. Sila cuba semula.'); } } + + public function printMaklumat(Request $request) + { + $noic = $request->query('noic') ?: Session::get('noic'); + if (empty($noic)) { + return redirect('/pelanggan'); + } + + $auth_user = Auth::user(); + $api_url = config('api.url'); + $verify = ['verify' => config('app.api_verify')]; + $kodcaw = $auth_user['cawangan']; + + $customer = Http::withOptions($verify)->get($api_url . '/api/customers/' . $noic)->json(); + if (!is_array($customer) || (empty($customer['nama']) && empty($customer['nopelanggan']) && empty($customer['kpbaru']) && empty($customer['kplama']))) { + abort(404, 'Pelanggan tidak dijumpai.'); + } + + $cawanganResp = Http::withOptions($verify)->get($api_url . '/api/cawangan/detail/' . $kodcaw)->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $banks = Http::withOptions($verify)->get($api_url . '/api/bank')->json(); + $pekerjaanList = Http::withOptions($verify)->get($api_url . '/api/pekerjaan')->json(); + + $poskodKod = $customer['poskod'] ?? ''; + $poskod = []; + if ($poskodKod !== '' && $poskodKod !== null) { + $poskodResp = Http::withOptions($verify)->get($api_url . '/api/poskod/' . $poskodKod)->json(); + if (is_array($poskodResp)) { + $poskod = $poskodResp; + } + } + + $daerahNama = 'NIL'; + $negeriNama = 'NIL'; + if (!empty($poskod['koddaerah'])) { + $daerahResp = Http::withOptions($verify)->get($api_url . '/api/daerah/' . $poskod['koddaerah'])->json(); + $daerahNama = is_array($daerahResp) ? ($daerahResp['namadaerah'] ?? 'NIL') : 'NIL'; + } + if (!empty($poskod['kodnegeri'])) { + $negeriResp = Http::withOptions($verify)->get($api_url . '/api/negeri/' . $poskod['kodnegeri'])->json(); + $negeriNama = is_array($negeriResp) ? ($negeriResp['negeri'] ?? 'NIL') : 'NIL'; + } + + $bankNama = $this->lookupListValue($banks, 'abbreviation', $customer['kodbank'] ?? null, 'name'); + $pekerjaanNama = $this->lookupListValue($pekerjaanList, 'kodkerja', $customer['kodkerja'] ?? null, 'keterangan'); + if (($customer['kodkerja'] ?? '') == '6' && !empty($customer['nota_pekerjaan'])) { + $pekerjaanNama = $customer['nota_pekerjaan']; + } + + $photo_svg = '' + . '' + . '' + . '' + . ''; + $photo_src = 'data:image/svg+xml;base64,' . base64_encode($photo_svg); + if (!empty($customer['photo_base64'])) { + $mime = $customer['photo_mime'] ?? 'image/jpeg'; + $photo_src = 'data:' . $mime . ';base64,' . $customer['photo_base64']; + } + + return view('print.maklumat_pelanggan', [ + 'cawangan' => $cawangan_detail['namacaw'] ?? ($cawangan_detail['details_caw'] ?? $kodcaw), + 'kodcaw' => $kodcaw, + 'alamat1' => $cawangan_detail['alamat1'] ?? '', + 'alamat2' => $cawangan_detail['alamat2'] ?? '', + 'notel' => $cawangan_detail['notel'] ?? '', + 'tarikh' => Carbon::now()->format('d/m/Y'), + 'nama' => $this->printBlank($customer['nama'] ?? null), + 'nokp' => $this->printBlank(!empty($customer['kpbaru']) ? $customer['kpbaru'] : ($customer['kplama'] ?? $noic)), + 'nopelanggan' => $this->printBlank($customer['nopelanggan'] ?? null), + 'status' => (!empty($customer['tag_anggota']) && $customer['tag_anggota'] == 1) ? 'Anggota Koperasi' : 'Pelanggan', + 'telefon' => $this->printBlank($customer['telefon'] ?? null), + 'telefon2' => $this->printBlank($customer['telefon2'] ?? null), + 'alamat' => $this->printBlank($customer['alamat1'] ?? null), + 'poskod' => $this->printBlank($customer['poskod'] ?? null), + 'daerah' => $daerahNama, + 'negeri' => $negeriNama, + 'bank' => $this->printBlank($bankNama), + 'noakaun' => $this->printBlank($customer['noakaun'] ?? null), + 'pekerjaan' => $this->printBlank($pekerjaanNama), + 'nota' => $this->printBlank($customer['nota'] ?? null), + 'photo_src' => $photo_src, + ]); + } + + private function printBlank($value) + { + if ($value === null) { + return 'NIL'; + } + $value = is_string($value) ? trim($value) : $value; + return $value === '' ? 'NIL' : $value; + } + + private function lookupListValue($list, $key, $match, $nameKey) + { + if (!is_array($list) || $match === null || $match === '' || $match === '-') { + return ''; + } + foreach ($list as $item) { + if (is_array($item) && ($item[$key] ?? null) == $match) { + return $item[$nameKey] ?? ''; + } + } + return ''; + } } diff --git a/app/Http/Controllers/GadaianController.php b/app/Http/Controllers/GadaianController.php index a6cbbbea..2ac08566 100644 --- a/app/Http/Controllers/GadaianController.php +++ b/app/Http/Controllers/GadaianController.php @@ -211,17 +211,169 @@ class GadaianController extends Controller 'teller' => $auth_user['name'] ]); - $this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']); + $caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI')); + $pinjamanAmt = round((float) $request->pinjaman, 2); + + // Online only for RM5k–50k; otherwise force cash path. + if ($caraTerima === 'ONLINE' && ($pinjamanAmt < 5000 || $pinjamanAmt > 50000)) { + $caraTerima = 'TUNAI'; + } + + if ($caraTerima === 'ONLINE') { + $online = Http::withOptions(['verify' => config('app.api_verify')]) + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/' . $auth_user['cawangan'], [ + 'norujukan' => $request->norujukan_gadai, + 'nokp' => $request->nokp, + 'pinjaman' => $pinjamanAmt, + 'kodbank' => $request->kodbank, + 'noakaun' => $request->noakaun, + 'nama_bank' => $request->nama_bank, + 'nama_akaun' => $request->nama_akaun ?: $request->nama_pelanggan, + 'nama_pelanggan' => $request->nama_pelanggan, + 'nopelanggan' => $request->nopelanggan, + 'created_by' => $auth_user['name'], + 'update_customer_bank' => true, + ]); + + if (!$online->successful()) { + $body = $online->json(); + $msg = is_array($body) && isset($body['message']) + ? $body['message'] + : 'Gagal daftar pembiayaan online. Sila cuba lagi.'; + return redirect()->back()->with('error', $msg); + } + } else { + $this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']); + } event(new GadaiNotify('lulus', $auth_user['cawangan'])); $request->session()->forget('page_reload'); $request->session()->forget('norujukan'); $request->session()->flash('activate_tab', 'SAG_baru'); + if ($caraTerima === 'ONLINE') { + $request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'); + } return redirect()->route('customer_info'); } + public function printBorangPengesahanOnline($norujukan) + { + $auth_user = Auth::user(); + $kodcaw = $auth_user['cawangan']; + + $onlineResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/by-norujukan/' . $norujukan); + + if (!$onlineResp->successful()) { + abort(404, 'Pembiayaan online tidak dijumpai.'); + } + + $line = $onlineResp->json(); + if (!is_array($line) || empty($line['norujukan'])) { + abort(404, 'Pembiayaan online tidak dijumpai.'); + } + + return $this->renderBorangPengesahanOnline($line, $kodcaw); + } + + public function previewBorangPengesahanOnline(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $auth_user['cawangan']; + + $line = [ + 'norujukan' => $request->input('norujukan'), + 'nokp' => $request->input('nokp'), + 'nopelanggan' => $request->input('nopelanggan'), + 'nama_pelanggan' => $request->input('nama_pelanggan'), + 'pinjaman' => $request->input('pinjaman'), + 'kodbank' => $request->input('kodbank'), + 'nama_bank' => $request->input('nama_bank'), + 'noakaun' => $request->input('noakaun'), + 'nama_akaun' => $request->input('nama_akaun'), + 'created_by' => $auth_user['name'] ?? '', + 'created_at' => Carbon::now()->toDateTimeString(), + ]; + + if (empty($line['norujukan']) || empty($line['noakaun'])) { + abort(422, 'Sila lengkapkan maklumat bank sebelum cetak borang.'); + } + + return $this->renderBorangPengesahanOnline($line, $kodcaw); + } + + private function renderBorangPengesahanOnline(array $line, $kodcaw) + { + $auth_user = Auth::user(); + + $cawanganResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/cawangan/detail/' . $kodcaw) + ->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $nokpRaw = $line['nokp'] ?? ''; + $customer = []; + if ($nokpRaw !== '') { + $customerResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/customers/' . $nokpRaw) + ->json(); + if (is_array($customerResp)) { + $customer = $customerResp; + } + } + + $cawangan = $cawangan_detail['namacaw'] + ?? $cawangan_detail['details_caw'] + ?? $kodcaw; + $alamat1 = $cawangan_detail['alamat1'] ?? ''; + $alamat2 = $cawangan_detail['alamat2'] ?? ''; + $notel = $cawangan_detail['notel'] ?? ''; + + $nama = $line['nama_pelanggan'] ?? ($customer['nama'] ?? ''); + $nokp = !empty($customer['kpbaru']) ? $customer['kpbaru'] : ($customer['kplama'] ?? $nokpRaw); + $nopelanggan = $line['nopelanggan'] ?? ($customer['nopelanggan'] ?? ''); + $telefon = $customer['telefon'] ?? ''; + $teller = $line['created_by'] ?? ($auth_user['name'] ?? ''); + $bank = $line['nama_bank'] ?? ($line['kodbank'] ?? ''); + $noakaun = $line['noakaun'] ?? ''; + $nama_akaun = $line['nama_akaun'] ?? $nama; + $pinjaman = number_format((float) ($line['pinjaman'] ?? 0), 2); + $norujukan = $line['norujukan'] ?? ''; + + $tarikhSumber = $line['created_at'] ?? null; + try { + $tarikh = $tarikhSumber + ? Carbon::parse($tarikhSumber)->format('d/m/Y') + : Carbon::now()->format('d/m/Y'); + } catch (\Exception $e) { + $tarikh = Carbon::now()->format('d/m/Y'); + } + + return view('print.borang_pengesahan_online', compact( + 'cawangan', + 'kodcaw', + 'alamat1', + 'alamat2', + 'notel', + 'norujukan', + 'tarikh', + 'teller', + 'nama', + 'nokp', + 'nopelanggan', + 'telefon', + 'pinjaman', + 'bank', + 'noakaun', + 'nama_akaun' + )); + } + public function editstore(Request $request) { $auth_user = Auth::user(); diff --git a/app/Http/Controllers/KakitanganController.php b/app/Http/Controllers/KakitanganController.php index f8346092..3a519525 100644 --- a/app/Http/Controllers/KakitanganController.php +++ b/app/Http/Controllers/KakitanganController.php @@ -20,10 +20,43 @@ class KakitanganController extends Controller public function daftarKakitangan(){ $api_url = config('api.url'); $auth_user = Auth::user(); - $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + $http = Http::withOptions(['verify' => config('app.api_verify')]); + + $cawangan_all = $http->get($api_url . '/api/cawangan')->json(); + $cawangan_all = is_array($cawangan_all) ? $cawangan_all : []; + $cawangan_by_kod = collect($cawangan_all)->keyBy('kodcaw'); + $cawangan_info = $cawangan_by_kod->get($auth_user['cawangan'], []); + $role = Role::get(); - $kakitangan = User::all(); - return view('operasi.daftar_kakitangan',compact('cawangan_info','api_url','role','kakitangan','auth_user')); + $kakitangan_rows = User::where('roles', '!=', 'admin') + ->get() + ->values() + ->map(function ($staff) use ($cawangan_by_kod) { + $kod = (string) $staff->cawangan; + if (in_array($kod, ['operasi', 'callcenter', 'admin'], true)) { + $cawangan_nama = ucwords($kod); + } else { + $cawangan_nama = ucwords((string) data_get($cawangan_by_kod, $kod . '.details_caw', $kod)); + } + + return [ + 'id' => $staff->id, + 'name' => $staff->name, + 'roles' => strtoupper((string) $staff->roles), + 'role_harian' => $staff->role_harian ? strtoupper($staff->role_harian) : '-', + 'role_asal' => $staff->role_asal ? strtoupper($staff->role_asal) : '-', + 'cawangan' => $cawangan_nama, + ]; + }); + + return view('operasi.daftar_kakitangan', compact( + 'cawangan_info', + 'api_url', + 'role', + 'kakitangan_rows', + 'auth_user', + 'cawangan_all' + )); } public function registerStaff(Request $request){ diff --git a/app/Http/Controllers/KhazanahController.php b/app/Http/Controllers/KhazanahController.php index 03e72524..a4b074bb 100644 --- a/app/Http/Controllers/KhazanahController.php +++ b/app/Http/Controllers/KhazanahController.php @@ -505,6 +505,9 @@ class KhazanahController extends Controller public function mintakatalaluansemula(Request $request) { + if (app()->environment('local')) { + return "True"; + } if(Hash::check($request->passsemula, Auth::user()->password)) { diff --git a/app/Http/Controllers/OperasiController.php b/app/Http/Controllers/OperasiController.php index 74189123..7b5f6491 100644 --- a/app/Http/Controllers/OperasiController.php +++ b/app/Http/Controllers/OperasiController.php @@ -87,13 +87,19 @@ class OperasiController extends Controller 'siri' => $request->siri ])->json(); - $updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateKelulusan/' . $request->kodcaw . '/' . $norujukan, [ + $updateData = [ 'sahkan' => 1, 'namaoperasi' => $request->namaoperasi, 'siri' => $request->siri - ])->json(); - } else { - $updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateHapusAnsuran', [ + ]; + + if ($request->has('actions')) { + $updateData['actions'] = $request->actions; + } + + $updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateKelulusan/' . $request->kodcaw . '/' . $norujukan, $updateData)->json(); + }else{ + $updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateHapusAnsuran',[ 'siri' => $request->siri, "kodcaw" => $request->kodcaw, "norujukan" => $request->norujukan, @@ -139,6 +145,49 @@ class OperasiController extends Controller return response()->json($updatelulus, 200); } + public function createKelulusanByKhazanah(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + + $current = Carbon::now(); + $current = new Carbon(); + + $norujukan = $request->norujukan; + + $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + + $getsiri = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/ListLatestLulus/'. $auth_user['cawangan'] . '/' . $norujukan)->json(); + + if(isset($getsiri['siri'])) + { + $siri = $getsiri['siri'] + 1; + }else{ + $siri = 1; + } + + $createlulus = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/CreateKelulusan/' . $auth_user['cawangan'] . '/' . $norujukan . '/roles/khazanah', [ + 'komen' => $request->komen, + 'dategadai' => $current->toDateString(), + 'bilgadai' => 1, + 'siri' => $siri, + 'margin' => $request->margin, + 'berat' => $request->berat, + 'nilaimarhun' => $request->nilaimarhun, + 'status' => $request->status, + 'jumpinjam' => $request->jumpinjam, + 'jenistebus' => "G", + 'teller' => $request->teller, + 'peloperasi' => "operasi", + 'pelulus' => "operasi", + 'actions' => "Mohon Pengesahan Pembayaran Pampasan" + ])->json(); + + event(new OperasiNotice($auth_user['cawangan'],$auth_user['name'])); + + return response()->json($siri,201); + } + public function updatekelulusananggota(Request $request) { $auth_user = Auth::user(); @@ -149,7 +198,7 @@ class OperasiController extends Controller $kodcaw = $request->kodcaw; $komen = $request->komenkelulusan; $namaoperasi = $request->namaoperasi; - $taglulus = $request->taglulus; // contoh: 'Y' = lulus, 'N' = batal + $taglulus = $request->taglulus; // contoh: 'Y' = lulus, 'N' = batalmarginmutu() // 1️⃣ Tentukan status kelulusan $status = ($taglulus == 'Y') ? 1 : 2; // 1 = Lulus, 2 = Batal/Tolak @@ -321,6 +370,9 @@ class OperasiController extends Controller public function mintakatalaluansemula(Request $request) { + if (app()->environment('local')) { + return "True"; + } if (Hash::check($request->passsemula, Auth::user()->password)) { return "True"; @@ -401,4 +453,103 @@ class OperasiController extends Controller return view('operasi.kelulusan_anggota', compact('cawangan_info', 'api_url', 'arraykelulusan')); } + + public function marginmutu() + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + + // Get MaxMutuEmas data from API + $maxmutuemas = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/maxmutuemas')->json(); + + return view('operasi.marginmutu', compact('cawangan_info', 'api_url', 'maxmutuemas')); + } + + public function editMarginMutu() + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + + return view('operasi.edit_marginmutu', compact('cawangan_info', 'api_url')); + } + + public function laporanAudit() + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + + return view('homepage.operasi.laporan-audit', compact('cawangan_info', 'api_url')); + } + + public function cetakLaporanAuditTrail(Request $request) + { + $api_url = config('api.url'); + $auth_user = Auth::user(); + $current = Carbon::now(); + $current = new Carbon(); + $tarikh_cetak = $current->toDateString(); + + if($request->dateaudit == null){ + $date = $current->toDateString(); + }else{ + $split_date = explode('-', $request->dateaudit); + $date = $split_date[0] . '-' . $split_date[1] . '-' . $split_date[2]; + } + + if($request->dateaudit_till == null){ + $date_till = $current->toDateString(); + }else{ + $split_date_till = explode('-', $request->dateaudit_till); + $date_till = $split_date_till[0] . '-' . $split_date_till[1] . '-' . $split_date_till[2]; + } + + $audittrail_raw = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url'). '/api/reports/audittrail/operasi',['date' => $date,'date_till' => $date_till])->json(); + if(!$audittrail_raw){ + return redirect()->back()->with('errormessage', 'Tiada data pada tarikh tersebut.'); + } + + // Transform the data to match view format + $audittrail = []; + foreach($audittrail_raw as $item) { + $created_at = \Carbon\Carbon::parse($item['created_at']); + + // Format differences for display + $differences_text = '-'; + if (isset($item['differences']) && !empty($item['differences'])) { + $diff_parts = []; + foreach ($item['differences'] as $field => $changes) { + $old = $changes['old'] ?? 'null'; + $new = $changes['new'] ?? 'null'; + $diff_parts[] = "{$field}: {$old} → {$new}"; + } + $differences_text = implode('; ', $diff_parts); + } + + $audittrail[] = [ + 'tarikh' => $created_at->format('d/m/Y'), + 'masa' => $created_at->format('H:i:s'), + 'users' => $item['users'], + 'actions' => $item['actions'], + 'differ' => $differences_text + ]; + } + + $cawangan_detail = [ + 'kodcaw' => 'operasi', + 'namacaw' => 'Operasi Ar-Rahn', + 'alamat1' => 'Lot 107, Jalan Parit Dalam,', + 'alamat2' => 'Seksyen 8', + 'poskod' => '15000', + 'bandar' => 'Kota Bahru', + 'negeri' => 'Kelantan', + 'notel' => '03-12345678', + 'nofaks' => '03-12345678', + ]; + + return view('print.laporan_audittrail_operasi', compact('audittrail','cawangan_detail','tarikh_cetak','date','date_till','api_url')); + } + } diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php new file mode 100644 index 00000000..93b24e24 --- /dev/null +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -0,0 +1,842 @@ +middleware('auth'); + } + + protected function api() + { + return Http::withOptions(['verify' => config('app.api_verify')]); + } + + protected function kodcaw() + { + return Auth::user()->cawangan; + } + + /** + * PC senarai queue + batch history. + */ + public function index(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $queue = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/queue') + ->json(); + + $batches = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches') + ->json(); + + $banks = $this->api()->get($api_url . '/api/bank')->json(); + if (!is_array($banks)) { + $banks = []; + } + + $modal = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw) + ->json(); + + $lines = $queue['data'] ?? []; + $slot = $queue['slot_cadangan'] ?? null; + if (!is_array($batches)) { + $batches = []; + } + + return view('pembiayaan_online.pc.senarai', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'lines', + 'batches', + 'banks', + 'modal', + 'slot', + 'queue' + )); + } + + /** + * Update bank/docs for a queue line. + */ + public function updateLine(Request $request, $id) + { + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $payload = [ + 'kodbank' => $request->input('kodbank'), + 'noakaun' => $request->input('noakaun'), + 'nama_bank' => $request->input('nama_bank'), + 'nama_akaun' => $request->input('nama_akaun'), + 'nota' => $request->input('nota'), + 'update_customer_bank' => true, + ]; + + $res = $this->api() + ->asForm() + ->put($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id, $payload); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal kemaskini rekod.'; + return redirect()->back()->with('error', $msg); + } + + return redirect()->route('pembiayaan.online.pc')->with('message', 'Rekod dikemaskini.'); + } + + /** + * Submit selected lines as a batch to Operasi. + */ + public function submitBatch(Request $request) + { + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $auth_user = Auth::user(); + + $ids = $request->input('ids', []); + if (!is_array($ids)) { + $ids = array_filter(explode(',', (string) $ids)); + } + + $res = $this->api() + ->asForm() + ->post($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches/submit', [ + 'ids' => $ids, + 'pc_submitted_by' => $auth_user['name'], + 'slot' => $request->input('slot'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal hantar batch.'; + return redirect()->back()->with('error', $msg); + } + + $payload = $res->json(); + $batchNo = is_array($payload) ? ($payload['batch']['batch_no'] ?? '') : ''; + $appended = is_array($payload) && !empty($payload['appended']); + $message = $appended + ? ('Rekod ditambah ke batch' . ($batchNo ? ' ' . $batchNo : '') . '.') + : ('Batch dihantar ke Operasi' . ($batchNo ? ': ' . $batchNo : '.')); + + return redirect()->route('pembiayaan.online.pc')->with('message', $message); + } + + /** + * Batch detail. + */ + public function showBatch($batchId) + { + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect()->route('pembiayaan.online.pc')->with('error', 'Batch tidak dijumpai.'); + } + + return view('pembiayaan_online.pc.batch', [ + 'batch' => $detail['batch'], + 'lines' => $detail['lines'] ?? [], + 'kodcaw' => $kodcaw, + 'api_url' => $api_url, + 'cawangan_info' => $cawangan_info, + ]); + } + + /** + * PC: senarai PAID untuk tally + blast. + */ + public function paidIndex(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $blastFilter = $request->input('blast_sent', '0'); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $query = []; + if ($blastFilter !== 'all' && $blastFilter !== '') { + $query['blast_sent'] = $blastFilter; + } + + $paid = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/paid', $query) + ->json(); + + $lines = $paid['data'] ?? []; + if (!is_array($lines)) { + $lines = []; + } + + $bilangan = $paid['bilangan'] ?? count($lines); + $jumlah = $paid['jumlah'] ?? 0; + + return view('pembiayaan_online.pc.paid', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'lines', + 'bilangan', + 'jumlah', + 'blastFilter' + )); + } + + /** + * PC: view/download bukti bayaran inline. + */ + public function viewBukti($id) + { + $kodcaw = $this->kodcaw(); + $res = $this->api() + ->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/bukti'); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Bukti tidak dijumpai.'; + return redirect()->back()->with('error', $msg); + } + + $payload = $res->json(); + $bukti = $payload['bukti'] ?? null; + if (!$bukti || empty($bukti['data'])) { + return redirect()->back()->with('error', 'Fail bukti kosong.'); + } + + $raw = $bukti['data']; + if (strpos($raw, 'base64,') !== false) { + $raw = substr($raw, strpos($raw, 'base64,') + 7); + } + $binary = base64_decode($raw, true); + if ($binary === false) { + $binary = base64_decode($bukti['data']); + } + + $type = $bukti['type'] ?? 'application/octet-stream'; + $filename = $bukti['filename'] ?? ('bukti-' . $id); + + return response($binary, 200, [ + 'Content-Type' => $type, + 'Content-Disposition' => 'inline; filename="' . $filename . '"', + ]); + } + + /** + * PC: tanda rekod PAID sebagai selesai semak / blast. + */ + public function markBlast(Request $request) + { + $kodcaw = $this->kodcaw(); + $ids = $request->input('ids', []); + if (!is_array($ids)) { + $ids = array_filter(explode(',', (string) $ids)); + } + + if (count($ids) === 0) { + return redirect()->back()->with('error', 'Sila pilih sekurang-kurangnya satu rekod.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/paid/blast', [ + 'ids' => $ids, + 'blast_by' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tanda selesai.'; + return redirect()->back()->with('error', $msg); + } + + $n = $res->json()['dikemaskini'] ?? count($ids); + return redirect() + ->route('pembiayaan.online.pc.paid') + ->with('message', $n . ' rekod ditanda selesai semak/blast.'); + } + + /** + * Operasi: senarai batch semua cawangan. + */ + public function operasiIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $status = $request->input('status', 'SUBMITTED'); + $tarikh = $request->input('tarikh'); + + $query = ['status' => $status]; + if ($tarikh) { + $query['tarikh'] = $tarikh; + } + + $res = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/batches', $query) + ->json(); + + $batches = $res['data'] ?? []; + if (!is_array($batches)) { + $batches = []; + } + + return view('pembiayaan_online.operasi.senarai', compact( + 'auth_user', + 'api_url', + 'batches', + 'status', + 'tarikh' + )); + } + + /** + * Operasi: detail batch + lines. + */ + public function operasiShowBatch($kodcaw, $batchId) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect() + ->route('pembiayaan.online.operasi') + ->with('error', 'Batch tidak dijumpai.'); + } + + return view('pembiayaan_online.operasi.batch', [ + 'auth_user' => $auth_user, + 'batch' => $detail['batch'], + 'lines' => $detail['lines'] ?? [], + 'kodcaw' => $kodcaw, + 'api_url' => $api_url, + 'cawangan_info' => $cawangan_info, + ]); + } + + /** + * Operasi: printable batch kelulusan form. + */ + public function operasiPrintBatch($kodcaw, $batchId) + { + $api_url = config('api.url'); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect() + ->route('pembiayaan.online.operasi') + ->with('error', 'Batch tidak dijumpai.'); + } + + $batch = $detail['batch']; + $lines = $detail['lines'] ?? []; + if (!is_array($lines)) { + $lines = []; + } + + $cawanganResp = $this->api() + ->get($api_url . '/api/cawangan/detail/' . $kodcaw) + ->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $cawangan = $cawangan_detail['namacaw'] + ?? $cawangan_detail['details_caw'] + ?? $cawangan_detail['nama'] + ?? $kodcaw; + $alamat1 = $cawangan_detail['alamat1'] ?? ''; + $alamat2 = $cawangan_detail['alamat2'] ?? ''; + $notel = $cawangan_detail['notel'] ?? ''; + + $batchNo = $batch['batch_no'] ?? '-'; + $slot = $batch['slot'] ?? '-'; + $status = $batch['status'] ?? '-'; + $dihantarOleh = $batch['pc_submitted_by'] ?? '-'; + + $jumlah = (float) ($batch['jumlah_keseluruhan'] ?? 0); + if ($jumlah <= 0 && count($lines) > 0) { + $jumlah = (float) collect($lines)->sum(function ($line) { + return (float) ($line['pinjaman'] ?? 0); + }); + } + + $opsBy = Auth::user()['name'] ?? ''; + foreach ($lines as $line) { + if (!empty($line['ops_by'])) { + $opsBy = $line['ops_by']; + break; + } + } + + $tarikh = $this->formatPrintDate($batch['tarikh'] ?? null); + $dihantarPada = $this->formatPrintDate($batch['submitted_at'] ?? null, true); + + return view('print.borang_batch_pembiayaan_online', compact( + 'cawangan', + 'kodcaw', + 'alamat1', + 'alamat2', + 'notel', + 'tarikh', + 'batchNo', + 'slot', + 'status', + 'dihantarOleh', + 'dihantarPada', + 'opsBy', + 'lines', + 'jumlah' + )); + } + + protected function formatPrintDate($value, $withTime = false) + { + if ($value === null || $value === '' || $value === '-') { + return '-'; + } + + try { + return Carbon::parse($value)->format($withTime ? 'd/m/Y H:i' : 'd/m/Y'); + } catch (\Exception $e) { + return (string) $value; + } + } + + public function operasiApproveLine(Request $request, $kodcaw, $id) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/approve', [ + 'ops_by' => Auth::user()['name'], + ]); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod diluluskan.'); + } + + public function operasiRejectLine(Request $request, $kodcaw, $id) + { + $reason = trim((string) $request->input('reject_reason', '')); + if ($reason === '') { + return redirect()->back()->with('error', 'Sebab tolak diperlukan.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/reject', [ + 'ops_by' => Auth::user()['name'], + 'reject_reason' => $reason, + ]); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod ditolak. Dikembalikan ke PC.'); + } + + public function operasiApproveBatch(Request $request, $kodcaw, $batchId) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId . '/approve', [ + 'ops_by' => Auth::user()['name'], + ]); + + return $this->opsRedirect($res, $kodcaw, $batchId, 'Semua rekod menunggu dalam batch diluluskan.'); + } + + public function operasiMarkPaid(Request $request, $kodcaw, $id) + { + $payload = $this->buildPaidPayload($request); + if (isset($payload['error'])) { + return redirect()->back()->with('error', $payload['error']); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/paid', $payload); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod ditanda PAID.'); + } + + public function operasiMarkPaidBatch(Request $request, $kodcaw, $batchId) + { + $payload = $this->buildPaidPayload($request); + if (isset($payload['error'])) { + return redirect()->back()->with('error', $payload['error']); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId . '/paid', $payload); + + return $this->opsRedirect($res, $kodcaw, $batchId, 'Rekod APPROVED dalam batch ditanda PAID.'); + } + + /** + * @return array + */ + protected function buildPaidPayload(Request $request) + { + $noRujukan = trim((string) $request->input('no_rujukan_bayaran', '')); + if ($noRujukan === '') { + return ['error' => 'No. rujukan bayaran diperlukan.']; + } + + if (!$request->hasFile('bukti')) { + return ['error' => 'Fail bukti bayaran diperlukan.']; + } + + $file = $request->file('bukti'); + if (!$file->isValid()) { + return ['error' => 'Fail bukti tidak sah.']; + } + + $raw = base64_encode(file_get_contents($file->getRealPath())); + $mime = $file->getMimeType() ?: 'application/octet-stream'; + + return [ + 'ops_by' => Auth::user()['name'], + 'no_rujukan_bayaran' => $noRujukan, + 'tarikh_bayaran' => $request->input('tarikh_bayaran') ?: date('Y-m-d'), + 'bukti_data' => $raw, + 'bukti_filename' => $file->getClientOriginalName(), + 'bukti_type' => $mime, + ]; + } + + protected function opsRedirect($res, $kodcaw, $batchId, $successMsg) + { + $batchId = $batchId ?: null; + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Tindakan gagal.'; + return redirect()->back()->with('error', $msg); + } + + if ($batchId) { + return redirect() + ->route('pembiayaan.online.operasi.batch', ['kodcaw' => $kodcaw, 'batchId' => $batchId]) + ->with('message', $successMsg); + } + + return redirect()->route('pembiayaan.online.operasi')->with('message', $successMsg); + } + + /** + * PC: modal online float + top-up request form/history. + */ + public function topupIndex(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $status = $request->input('status', ''); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $modal = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw) + ->json(); + + $query = []; + if ($status !== '' && $status !== 'all') { + $query['status'] = $status; + } + + $reqRes = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw . '/topup/requests', $query) + ->json(); + + $requests = $reqRes['data'] ?? []; + if (!is_array($requests)) { + $requests = []; + } + + $pendingRes = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw . '/topup/requests', ['status' => 'PENDING']) + ->json(); + $pendingList = $pendingRes['data'] ?? []; + $hasPending = is_array($pendingList) && count($pendingList) > 0; + $isMissing = !empty($modal['missing']); + $pendingRequest = $modal['pending_request'] ?? null; + if (!$pendingRequest && $hasPending) { + $pendingRequest = $pendingList[0]; + } + + return view('pembiayaan_online.pc.topup', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'modal', + 'requests', + 'status', + 'hasPending', + 'isMissing', + 'pendingRequest' + )); + } + + /** + * PC: submit top-up request. + */ + public function topupRequest(Request $request) + { + $kodcaw = $this->kodcaw(); + $amaun = (float) $request->input('amaun', 0); + + if ($amaun <= 0) { + return redirect()->back()->with('error', 'Amaun mesti lebih daripada 0.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/topup/request', [ + 'amaun' => $amaun, + 'dimohon_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal hantar permohonan.'; + return redirect()->back()->with('error', $msg)->withInput(); + } + + return redirect() + ->route('pembiayaan.online.pc.topup') + ->with('message', 'Permohonan modal online dihantar. Menunggu kelulusan Operasi.'); + } + + /** + * Operasi: senarai permohonan tambah modal online. + */ + public function operasiTopupIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $status = $request->input('status', 'PENDING'); + $tarikh = $request->input('tarikh'); + + $query = []; + if ($status === 'all') { + $query['status'] = 'all'; + } elseif ($status !== '' && $status !== null) { + $query['status'] = $status; + } else { + $query['status'] = 'PENDING'; + } + if ($tarikh) { + $query['tarikh'] = $tarikh; + } + + $res = $this->api() + ->get($api_url . '/api/modal-online/operasi/topup/requests', $query) + ->json(); + + $requests = $res['data'] ?? []; + if (!is_array($requests)) { + $requests = []; + } + + return view('pembiayaan_online.operasi.topup', compact( + 'auth_user', + 'api_url', + 'requests', + 'status', + 'tarikh' + )); + } + + public function operasiTopupApprove(Request $request, $id) + { + $amaun = $request->input('amaun'); + if ($amaun !== null && $amaun !== '' && (float) $amaun <= 0) { + return redirect()->back()->with('error', 'Amaun diluluskan mesti lebih daripada 0.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/topup/' . $id . '/approve', [ + 'diluluskan_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + 'amaun' => $amaun, + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal lulus permohonan.'; + return redirect()->back()->with('error', $msg); + } + + $amaunLulus = $res->json()['request']['amaun'] ?? $amaun; + return redirect() + ->route('pembiayaan.online.operasi.topup') + ->with('message', 'Permohonan diluluskan RM ' . number_format((float) $amaunLulus, 2) . '. Modal online cawangan dikemas kini.'); + } + + public function operasiTopupReject(Request $request, $id) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/topup/' . $id . '/reject', [ + 'diluluskan_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tolak permohonan.'; + return redirect()->back()->with('error', $msg); + } + + return redirect() + ->route('pembiayaan.online.operasi.topup') + ->with('message', 'Permohonan ditolak.'); + } + + /** + * PC: EOD close branch modal online. + */ + public function closeModal(Request $request) + { + $kodcaw = $this->kodcaw(); + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/close', [ + 'ditutup_oleh' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup modal online.'; + return redirect()->back()->with('error', $msg); + } + + $dikembalikan = $res->json()['dikembalikan'] ?? 0; + return redirect() + ->route('pembiayaan.online.pc.topup') + ->with('message', 'Modal online ditutup. Baki dikembalikan RM ' . number_format((float) $dikembalikan, 2) . '.'); + } + + /** + * Operasi: EOD dashboard semua cawangan. + */ + public function operasiEodIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $tarikh = $request->input('tarikh') ?: date('Y-m-d'); + + $res = $this->api() + ->get($api_url . '/api/modal-online/operasi/eod', [ + 'zone' => 'all', + 'tarikh' => $tarikh, + ]) + ->json(); + + $rows = $res['data'] ?? []; + if (!is_array($rows)) { + $rows = []; + } + + $summary = [ + 'open' => $res['open'] ?? 0, + 'closed' => $res['closed'] ?? 0, + 'missing' => $res['missing'] ?? 0, + 'bilangan' => $res['bilangan'] ?? count($rows), + ]; + + return view('pembiayaan_online.operasi.eod', compact( + 'auth_user', + 'api_url', + 'rows', + 'tarikh', + 'summary' + )); + } + + public function operasiCloseModal(Request $request, $kodcaw) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/close', [ + 'ditutup_oleh' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup modal online.'; + return redirect()->back()->with('error', $msg); + } + + return redirect() + ->route('pembiayaan.online.operasi.eod') + ->with('message', 'Modal online ' . $kodcaw . ' ditutup.'); + } + + public function operasiCloseAllModal(Request $request) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/eod/close-all', [ + 'ditutup_oleh' => Auth::user()['name'], + 'zone' => 'all', + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup semua modal online.'; + return redirect()->back()->with('error', $msg); + } + + $closed = $res->json()['closed'] ?? 0; + $skipped = $res->json()['skipped'] ?? 0; + return redirect() + ->route('pembiayaan.online.operasi.eod') + ->with('message', 'EOD selesai: ' . $closed . ' ditutup, ' . $skipped . ' sudah ditutup.'); + } +} diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index 14befceb..39816113 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -12,7 +12,7 @@ class TrustProxies extends Middleware * * @var array|string|null */ - protected $proxies; + protected $proxies = '*'; /** * The headers that should be used to detect proxies. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ee8ca5bc..539a3f28 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -23,6 +23,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { - // + if (strpos((string) config('app.url'), 'https://') === 0) { + \Illuminate\Support\Facades\URL::forceScheme('https'); + } } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 30490683..08a3ae1b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,6 +25,8 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); - // + Gate::define('impersonate', function ($user) { + return $user->canImpersonate(); + }); } } diff --git a/app/User.php b/app/User.php index 33e49532..70034b80 100644 --- a/app/User.php +++ b/app/User.php @@ -6,9 +6,11 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Lab404\Impersonate\Models\Impersonate; class User extends Authenticatable { + use Impersonate; use Notifiable; protected $guard = 'web'; @@ -43,4 +45,14 @@ class User extends Authenticatable { return $this->hasOne(Cawangan::class, 'kodcaw', 'cawangan'); } + + public function canImpersonate(): bool + { + return in_array($this->roles ?? '', ['admin'], true); + } + + public function canBeImpersonated(): bool + { + return ($this->roles ?? '') !== 'admin'; + } } diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 00000000..974dacb6 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,15 @@ + env('API_URL'), - 'url_koop' => env('API_URL_KOOP','http://localhost:8000'), + 'url_koop' => env('API_URL_KOOP','https://apiujrah.erahn.com.my'), 'url_pkb' => env('API_URL_PKB','http://localhost:8000'), ]; \ No newline at end of file diff --git a/config/app.php b/config/app.php index 0025b5bc..345406b5 100644 --- a/config/app.php +++ b/config/app.php @@ -15,6 +15,8 @@ return [ 'name' => env('APP_NAME', 'Laravel'), + 'version' => env('APP_VERSION', ''), + /* |-------------------------------------------------------------------------- | Application Environment diff --git a/config/mykad-reader.php b/config/mykad-reader.php index e1dc6da9..21576946 100644 --- a/config/mykad-reader.php +++ b/config/mykad-reader.php @@ -1,7 +1,7 @@ 'http://localhost:' . $x . '/?MYKADJSON'; +$default_port = '17321'; +$getReaderEndpoint = fn($x) => 'http://localhost:' . $x . '/read'; return[ 'endpoint' => env('MYKAD_READER_ENDPOINT', $getReaderEndpoint(env('MYKAD_READER_PORT', $default_port))), diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 073e756d..92ae34c7 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,23 +1,8 @@ -# Staging stack — isolated from local docker-compose.yml (8081 / 3307) -# and from api_arrahn staging (8090 / 3308). -# -# Local: -# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --build -# -# CI (image already in registry): -# APP_IMAGE=git.koppkb.com/kopkb/kaunter IMAGE_TAG= \ -# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --no-build -# -# Host nginx reverse-proxies the public domain to 127.0.0.1:${HTTP_PORT}. -# See docker/nginx/host-proxy.example.conf -# -# If 8091 or 3309 is already taken, set HTTP_PORT / MYSQL_PUBLISH_PORT in .env.staging. - name: kaunter_staging services: app: - image: ${APP_IMAGE:-kaunter}:${IMAGE_TAG:-staging} + image: ${APP_IMAGE:-git.koppkb.com/kopkb/kaunter}:${IMAGE_TAG:-staging} build: context: . dockerfile: docker/php/Dockerfile diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 223a96c0..182f3243 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -9,7 +9,10 @@ server { client_max_body_size 50M; location / { - try_files $uri $uri/ @php; + # Production Apache public/.htaccess 301-strips trailing slashes. + # Nginx ignores .htaccess, so without this /path/ 404s while /path works. + rewrite ^/(.*)/$ /$1 last; + try_files $uri @php; } location @php { diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index b14f579b..e2a2c4f8 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -4,22 +4,22 @@ WORKDIR /var/www/html RUN apt-get update \ && apt-get install -y --no-install-recommends \ - git \ - pkg-config \ - unzip \ - libonig-dev \ - libzip-dev \ - libpng-dev \ - libjpeg62-turbo-dev \ - libfreetype6-dev \ + git \ + pkg-config \ + unzip \ + libonig-dev \ + libzip-dev \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" \ - pdo_mysql \ - mbstring \ - bcmath \ - zip \ - exif \ - gd \ + pdo_mysql \ + mbstring \ + bcmath \ + zip \ + exif \ + gd \ && rm -rf /var/lib/apt/lists/* COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer @@ -40,12 +40,12 @@ COPY --chown=www-data:www-data . /var/www/html RUN composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts \ && mkdir -p \ - storage/logs \ - storage/framework/cache/data \ - storage/framework/views \ - storage/framework/sessions \ - storage/app/public \ - bootstrap/cache \ + storage/logs \ + storage/framework/cache/data \ + storage/framework/views \ + storage/framework/sessions \ + storage/app/public \ + bootstrap/cache \ && chown -R www-data:www-data storage bootstrap/cache vendor FROM base AS development diff --git a/public/js/mykad-reader-customer.js b/public/js/mykad-reader-customer.js index 3c53b650..f07a8560 100644 --- a/public/js/mykad-reader-customer.js +++ b/public/js/mykad-reader-customer.js @@ -1,293 +1,403 @@ let button_read_mykad = document.getElementById("button_read_mykad"); let button_close_modal = document.querySelector("#daftar_pelanggan_mykad_reader button.btn.btn-danger"); -let original_photo_loader = document.querySelector("#photo-mykad").src; +let photo_mykad = document.querySelector("#photo-mykad"); +let original_photo_loader = photo_mykad ? photo_mykad.src : ""; - let showLoader = () => { - let pageLoader = document.getElementById('page_loader'); - pageLoader.removeAttribute('class'); - pageLoader.classList.add('loading'); - }; +const MYKAD_READER_TIMEOUT_MS = 45000; +const MYKAD_READER_DEFAULT_ENDPOINT = "http://localhost:17321/read"; - let hideLoader = () => { - let pageLoader = document.getElementById('page_loader'); - pageLoader.removeAttribute('class'); - }; - - function handleMyKadReader() - { - showLoader(); - WebSocketManager.init("wss://localhost:5963/mykadwebsocket"); - WebSocketManager.listReader(callbackDoneGetListCardReader); - } - - function callbackDoneGetListCardReader(data, respcode) - { - console.log('done get list card reader'); - - if(respcode === -1){ - console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} ); - } - - let isDevicesConnected = Object.keys(data).length > 0; - - if(! isDevicesConnected){ - hideLoader(); - - Swal.fire({ icon: 'error', title: 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', }); - - return; - } - - let slots = data.toString().split(','); - WebSocketManager.open(doneOpenReaderConnection, slots[0]); - } - - function doneOpenReaderConnection() - { - WebSocketManager.readAllText(callBackAfterReadMykad); - } - - function handleMyKadReaderError(sw12AndApiName) - { - hideLoader(); - - let error_handler_dictionary = { - '0x80100069' : 'Tiada MyKad dikesan, sila masukkan Mykad untuk dibaca', - '0x80100066' : 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul', - 'default' : sw12AndApiName, - }; - - let error_data = sw12AndApiName.split(" "); - - let error_code = error_data.filter(x => x.startsWith('0x')).splice(-1) - - let error_message = error_handler_dictionary['default']; - - let hasDefinedError = error_handler_dictionary.hasOwnProperty(error_code); - - if(hasDefinedError){ - error_message = error_handler_dictionary[error_code]; - } - - Swal.fire({ icon: 'error', title: error_message, }); - } - - function callBackAfterReadMykad(data, respcode, sw12AndApiName) - { - if(respcode !== 0){ - handleMyKadReaderError(sw12AndApiName); - return - } - - checkIfMyKadExists(data.IDNum) - .then(response => { - let isResponseArray = Array.isArray(response); - resolved_response = isResponseArray ? response.pop() : response; - - if (resolved_response === 'no-data') { - populateDaftarPelangganModal(data, handleReadPhoto); - } else { - let input_search_noic = document.getElementById("noic"); - let form_search_noic = document.querySelector("form[name='frmsearch']"); - - let getMyKadNumberWithoutHypens = myKadNumber => myKadNumber.replace(/-/g, ''); - input_search_noic.value = getMyKadNumberWithoutHypens(data.IDNum); - form_search_noic.submit(); - } - }) - .catch(error => { - Swal.fire({ - icon: 'error', - title: error, - }); - }); - - hideLoader(); - } - - function checkIfMyKadExists(myKadNumber) { - let myKadNumberWithoutHyphens = myKadNumber.replace(/-/g, ''); - - const url = wakil_caricustomer_endpoint + '/?icno=' + myKadNumberWithoutHyphens; - - const connectionConfig = { - headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "X-Requested-With": "XMLHttpRequest", - "X-CSRF-Token": document.querySelector('input[name="_token"]').value - }, - credentials: "same-origin", - }; - - return fetch(url, connectionConfig) - .then(response => response.json()) - } - - let populateDaftarPelangganModal = (data, callbackAfterPopulateDaftarPelanggan) => { - $('#daftar_pelanggan_mykad_reader').modal('show'); - let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group"); - - const religions = [ - ['01', 'Islam'], - ['02', 'Hindu'], - ['03', 'Buddha'], - ['04', 'Kristian'], - ['05', 'Lain-lain'], - ]; - - const races = [ - ['M', 'Melayu'], - ['C', 'Cina'], - ['I', 'India'], - ['L', 'Lain-lain'], - ]; - - let getCodeFromValue = (needle, haystack) => { - let title_cased_text = needle.split(' ') - .map(w => w[0].toUpperCase() + w.substring(1).toLowerCase()) - .join(' '); - - return haystack.filter(x => x[1] == title_cased_text)[0][0]; - }; - - let getMyKadNumberWithoutHypens = (IDNum) => { - return IDNum.replace(/-/g, ''); - } - - let getFormattedDateOfBirth = (date_of_birth) => { - - let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"); - - const [year, month, day] = date_of_birth_hyphenated.split('-'); - return day + '/' + month + '/' + year; - }; - - let getAgeFromDateOfBirth = (date_of_birth) => { - - let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"); - - const [birth_year, birth_month, birth_day] = date_of_birth_hyphenated.split('-'); - - let formatBirthMonth = (birth_month) => { - let newbirthMonth = birth_month < 9 ? birth_month[1] : birth_month; - return newbirthMonth; - }; - - const count_leap_year = countLeapYears(birth_year, formatBirthMonth(birth_month), birth_day) - 1; - - const start = new Date(date_of_birth_hyphenated), - end = new Date(), - diff = new Date(end - start), - days = ((diff / 1000 / 60 / 60 / 24) - count_leap_year) / 365; - - return parseInt(days); - }; - - let getBumiputeraStatus = (race) => { - - const non_bumiputera_ethnicity = ['INDIA', 'CINA']; - - let isBumiputera = !non_bumiputera_ethnicity.includes(race); - - return isBumiputera; - }; - - let disablePointerEventsOnClickableInputs = () => { - modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none'; - modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none'; - - modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none'; - modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none'; - } - - let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); - let photo_container = mykad_modal.querySelector("#photo-mykad-container"); - - modal_form.querySelector("textarea[name='name']").value = data.OrgName; - modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHypens(data.IDNum); - modal_form.querySelector("input[name='tarikh_lahir']").value = getFormattedDateOfBirth(data.BirthDate); - modal_form.querySelector("input[name='negeri_lahir']").value = data.BirthPlace; - modal_form.querySelector("input[name='jantina']").value = data.Gender == 'LELAKI' ? 'Lelaki' : 'Perempuan'; - modal_form.querySelector("input[name='warganegara']").checked = data.Citizenship === 'WARGANEGARA' ? true : false; - modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.Race); - modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.Religion, religions); - modal_form.querySelector("textarea[name='alamat']").value = [data.Address1, data.Address2, data.Address3].join(", "); - - let getPostCode = (postCodeCity) => postCodeCity.replace(/\D/g, ''); - modal_form.querySelector("input[name='poskod']").value = getPostCode(data.PostcodeCity); - - let getCityName = (postCodeCity) => postCodeCity.replace(/\d/g, ''); - modal_form.querySelector("input[name='daerah']").value = getCityName(data.PostcodeCity).trim(); - - modal_form.querySelector("input[name='negeri']").value = data.State; - modal_form.querySelector("input[name='age']").value = getAgeFromDateOfBirth(data.BirthDate); - modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.Race, races); - - calculatePassportAge(); - - photo_container.classList.remove('d-none'); - photo_container.classList.add('d-flex'); - - $('#daftar_pelanggan_mykad_reader').modal('show'); - - - disablePointerEventsOnClickableInputs(); - callbackAfterPopulateDaftarPelanggan(); -} - - -function handleReadPhoto() -{ - WebSocketManager.init("wss://localhost:5963/mykadwebsocket"); - WebSocketManager.listReader(callbackDoneGetListCardReader2); -} - -function callbackDoneGetListCardReader2(data, respcode) -{ - if(respcode === -1){ - console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} ); - } - - let slots = data.toString().split(','); - WebSocketManager.open(doneOpenReaderConnection2, slots[0]); -} - -function doneOpenReaderConnection2() -{ - WebSocketManager.readPhoto(callbackAfterReadPhoto2); -} - -function callbackAfterReadPhoto2(data, RespCode, RespSW12AndApiName) -{ - let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); - let photo = mykad_modal.querySelector("#photo-mykad"); - - let isDataEmpty = data - && Object.keys(data).length === 0 - && Object.getPrototypeOf(data) === Object.prototype; - - if(isDataEmpty){ - photo.src = original_photo_loader; +let showLoader = () => { + let pageLoader = document.getElementById('page_loader'); + if (!pageLoader) { return; } + pageLoader.removeAttribute('class'); + pageLoader.classList.add('loading'); +}; - photo.src = 'data:image/jpeg;base64, ' + data.Base64Photo; +let hideLoader = () => { + let pageLoader = document.getElementById('page_loader'); + if (!pageLoader) { + return; + } + pageLoader.removeAttribute('class'); +}; + +function getMyKadReaderEndpoint() { + let endpoint = (typeof mykad_reader_endpoint !== 'undefined' && mykad_reader_endpoint) + ? String(mykad_reader_endpoint).trim() + : ''; + + if (!endpoint || endpoint.includes('MYKADJSON') || endpoint.includes(':8085')) { + return MYKAD_READER_DEFAULT_ENDPOINT; + } + + return endpoint; } +async function parseJsonResponse(response) { + let text = await response.text(); + let trimmed = (text || '').trim(); + + if (!trimmed) { + throw new Error('Pembaca MyKad tidak memulangkan data. Sila cuba lagi.'); + } + + if (trimmed.charAt(0) === '<') { + throw new Error('Pembaca MyKad memulangkan halaman web, bukan data kad. Sila pastikan perkhidmatan mykad-helper sedang berjalan di localhost:17321.'); + } + + try { + return JSON.parse(trimmed); + } catch (error) { + throw new Error('Pembaca MyKad memulangkan data yang tidak sah. Sila cuba lagi.'); + } +} + +function getMyKadNumberWithoutHyphens(nric) { + return String(nric || '').replace(/-/g, ''); +} + +function titleCase(value) { + return String(value || '') + .toLowerCase() + .split(' ') + .filter(Boolean) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +function getCodeFromValue(needle, haystack, fallback) { + let normalised = titleCase(needle); + let match = haystack.find(item => item[1].toLowerCase() === normalised.toLowerCase()); + + return match ? match[0] : fallback; +} + +function mapGender(gender) { + let value = String(gender || '').toLowerCase(); + + if (['male', 'lelaki', 'l', 'm'].includes(value)) { + return 'Lelaki'; + } + + if (['female', 'perempuan', 'p', 'f'].includes(value)) { + return 'Perempuan'; + } + + return gender || ''; +} + +function formatDateOfBirth(dob) { + if (!dob) { + return ''; + } + + let hyphenated = String(dob).includes('-') + ? String(dob) + : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + + let [year, month, day] = hyphenated.split('-'); + + if (!year || !month || !day) { + return dob; + } + + return day + '/' + month + '/' + year; +} + +function getAgeFromDateOfBirth(dob) { + if (!dob) { + return ''; + } + + let hyphenated = String(dob).includes('-') + ? String(dob) + : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + + let birthDate = new Date(hyphenated); + if (Number.isNaN(birthDate.getTime())) { + return ''; + } + + let today = new Date(); + let age = today.getFullYear() - birthDate.getFullYear(); + let monthDiff = today.getMonth() - birthDate.getMonth(); + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age -= 1; + } + + return age; +} + +function getBumiputeraStatus(race) { + const non_bumiputera_ethnicity = ['INDIA', 'CINA', 'INDIAN', 'CHINESE']; + return !non_bumiputera_ethnicity.includes(String(race || '').toUpperCase()); +} + +function joinAddress(data) { + return [data.address1, data.address2, data.address3] + .map(line => String(line || '').trim()) + .filter(Boolean) + .join(', '); +} + +function mapReaderErrorCode(rawMessage) { + const error_handler_dictionary = { + '0x80100069': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', + '0x8010000C': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', + '0x80100066': 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul', + '0x80100067': 'MyKad tidak dapat dikesan. Sila cabut dan masukkan semula kad', + '0x80100016': 'Gagal membaca MyKad. Sila pastikan kad dimasukkan dengan betul dan cuba lagi', + '0x8010002E': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', + '0x80100017': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', + }; + + let message = String(rawMessage || ''); + let codes = message.match(/0x[0-9a-fA-F]+/g) || []; + let matched = codes.reverse().find(code => error_handler_dictionary[code]); + + return matched ? error_handler_dictionary[matched] : (message || null); +} + +function getReaderErrorMessage(payload) { + if (!payload) { + return 'Pembaca MyKad tidak dapat membaca kad. Sila cuba lagi.'; + } + + let raw = payload.message || payload.error || payload.detail || ''; + return mapReaderErrorCode(raw) + || 'Pembaca MyKad tidak dapat membaca kad. Sila pastikan MyKad dimasukkan dengan betul.'; +} + +function getFetchErrorTitle(error) { + if (error && error.name === 'AbortError') { + return 'Masa menunggu pembaca MyKad telah tamat. Sila cuba lagi.'; + } + + return 'Pembaca MyKad tidak dapat dikesan. Sila pastikan perkhidmatan pembaca MyKad sedang berjalan.'; +} + +function getDisplayError(error) { + if (!error) { + return getFetchErrorTitle(error); + } + + if (error.name === 'AbortError') { + return getFetchErrorTitle(error); + } + + let message = String(error.message || ''); + if (!message || message.startsWith('Failed to fetch') || message.includes('NetworkError')) { + return getFetchErrorTitle(error); + } + + return message; +} + +async function handleMyKadReader() { + showLoader(); + + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), MYKAD_READER_TIMEOUT_MS); + + try { + const response = await fetch(getMyKadReaderEndpoint(), { + method: 'POST', + headers: { + 'Accept': 'application/json', + }, + signal: controller.signal, + }); + + const payload = await parseJsonResponse(response); + + if (!response.ok) { + throw new Error(getReaderErrorMessage(payload)); + } + + if (!payload || payload.ok !== true || !payload.data) { + throw new Error(getReaderErrorMessage(payload)); + } + + await handleMyKadData(payload.data); + } catch (error) { + Swal.fire({ + icon: 'error', + title: getDisplayError(error), + }); + } finally { + clearTimeout(timeoutId); + hideLoader(); + } +} + +function handleMyKadData(data) { + return checkIfMyKadExists(data.nric) + .then(response => { + let isResponseArray = Array.isArray(response); + let resolved_response = isResponseArray ? response.pop() : response; + + if (!resolved_response || resolved_response === 'no-data') { + populateDaftarPelangganModal(data); + } else { + let input_search_noic = document.querySelector("form[name='frmsearch'] input[name='noic']"); + let form_search_noic = document.querySelector("form[name='frmsearch']"); + + input_search_noic.value = getMyKadNumberWithoutHyphens(data.nric); + form_search_noic.submit(); + } + }) + .catch(() => { + populateDaftarPelangganModal(data); + }); +} + +function checkIfMyKadExists(myKadNumber) { + let myKadNumberWithoutHyphens = getMyKadNumberWithoutHyphens(myKadNumber); + let lookupUrl = new URL(wakil_caricustomer_endpoint, window.location.origin); + lookupUrl.searchParams.set('icno', myKadNumberWithoutHyphens); + + const csrfToken = document.querySelector('input[name="_token"]'); + + return fetch(lookupUrl.toString(), { + method: 'GET', + headers: { + "Accept": "application/json", + "X-Requested-With": "XMLHttpRequest", + "X-CSRF-Token": csrfToken ? csrfToken.value : "" + }, + credentials: "same-origin", + }).then(async response => { + let text = await response.text(); + let trimmed = (text || '').trim(); + + if (!trimmed || trimmed.charAt(0) === '<') { + return 'no-data'; + } + + try { + return JSON.parse(trimmed); + } catch (error) { + return 'no-data'; + } + }); +} + +let populateDaftarPelangganModal = (data) => { + $('#daftar_pelanggan_mykad_reader').modal('show'); + let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group"); + + const religions = [ + ['01', 'Islam'], + ['02', 'Hindu'], + ['03', 'Buddha'], + ['04', 'Kristian'], + ['05', 'Lain-lain'], + ]; + + const races = [ + ['M', 'Melayu'], + ['C', 'Cina'], + ['I', 'India'], + ['L', 'Lain-lain'], + ]; + + let disablePointerEventsOnClickableInputs = () => { + modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none'; + modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none'; + modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none'; + modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none'; + }; + + let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); + let photo_container = mykad_modal.querySelector("#photo-mykad-container"); + let photo = mykad_modal.querySelector("#photo-mykad"); + + modal_form.querySelector("textarea[name='name']").value = data.name || data.gmpc_name || ''; + modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHyphens(data.nric); + modal_form.querySelector("input[name='tarikh_lahir']").value = formatDateOfBirth(data.dob); + modal_form.querySelector("input[name='negeri_lahir']").value = data.birth_place || ''; + modal_form.querySelector("input[name='jantina']").value = mapGender(data.gender); + modal_form.querySelector("input[name='warganegara']").checked = String(data.citizenship || '').toUpperCase() === 'WARGANEGARA'; + modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.race); + modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.religion, religions, '05'); + modal_form.querySelector("textarea[name='alamat']").value = joinAddress(data); + modal_form.querySelector("input[name='poskod']").value = data.postcode || ''; + modal_form.querySelector("input[name='daerah']").value = data.city || ''; + modal_form.querySelector("input[name='negeri']").value = data.state || ''; + modal_form.querySelector("input[name='age']").value = data.age != null && data.age !== '' + ? data.age + : getAgeFromDateOfBirth(data.dob); + modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.race, races, 'L'); + + if (data.has_photo && data.photo_base64) { + let mime = data.photo_mime || 'image/jpeg'; + photo.src = 'data:' + mime + ';base64,' + data.photo_base64; + photo_container.classList.remove('d-none'); + photo_container.classList.add('d-flex'); + setMykadPhotoFields(data.photo_base64, mime); + } else { + photo.src = original_photo_loader; + photo_container.classList.add('d-none'); + photo_container.classList.remove('d-flex'); + setMykadPhotoFields('', ''); + } + + disablePointerEventsOnClickableInputs(); + $(modal_form.querySelector("input[name='poskod']")).trigger('change'); +}; + let hideMykadPhoto = () => { - - WebSocketManager.close(() => console.log('Websocket closed')); - let photo = document.getElementById("photo-mykad"); let photo_container = document.getElementById("photo-mykad-container"); - photo.src = original_photo_loader; + if (photo) { + photo.src = original_photo_loader; + } - photo_container.classList.add('d-none'); - photo_container.classList.remove('d-flex'); + if (photo_container) { + photo_container.classList.add('d-none'); + photo_container.classList.remove('d-flex'); + } + + setMykadPhotoFields('', ''); }; -button_read_mykad.addEventListener("click", handleMyKadReader); -button_close_modal.addEventListener("click", hideMykadPhoto); +function setMykadPhotoFields(base64, mime) { + let form = document.querySelector("#daftar_pelanggan_mykad_reader form") + || document.getElementById("form-mykad-reader"); + let photoInput = (form && form.querySelector("input[name='photo_base64']")) + || document.getElementById("photo_base64"); + let mimeInput = (form && form.querySelector("input[name='photo_mime']")) + || document.getElementById("photo_mime"); + + if (photoInput) { + photoInput.value = base64 || ''; + } + + if (mimeInput) { + mimeInput.value = mime || ''; + } +} + +function copyMykadPhotoFromImage() { + let photo = document.querySelector("#daftar_pelanggan_mykad_reader #photo-mykad"); + if (!photo || !photo.src || photo.src.indexOf("base64,") === -1) { + return; + } + + let parts = photo.src.split("base64,"); + let mimeMatch = photo.src.match(/^data:([^;]+);/); + setMykadPhotoFields(parts[1] || "", mimeMatch ? mimeMatch[1] : "image/jpeg"); +} + +if (button_read_mykad) { + button_read_mykad.addEventListener("click", handleMyKadReader); +} + +if (button_close_modal) { + button_close_modal.addEventListener("click", hideMykadPhoto); +} diff --git a/resources/views/admin/customer_index.blade.php b/resources/views/admin/customer_index.blade.php index aba0ebb5..e6a9dcdf 100644 --- a/resources/views/admin/customer_index.blade.php +++ b/resources/views/admin/customer_index.blade.php @@ -306,7 +306,7 @@ @endsection \ No newline at end of file diff --git a/resources/views/aliran_tunai/index.blade.php b/resources/views/aliran_tunai/index.blade.php index 9459d54a..384fad46 100644 --- a/resources/views/aliran_tunai/index.blade.php +++ b/resources/views/aliran_tunai/index.blade.php @@ -26,7 +26,7 @@
@php $auth_user = Auth::user();@endphp - +
@@ -348,7 +348,7 @@
+ +
+ merge(['class' => 'table table-striped table-hover nowrap']) }} + style="width:100%" + data-exportable="{{ $exportable ? 1 : 0 }}" + data-export-filename="{{ $filename !== '' ? $filename : $id }}" + > + + + @foreach ($columns as $column) + + @endforeach + + + + {{ $slot }} +
{{ $column }}
+
diff --git a/resources/views/components/print/footer.blade.php b/resources/views/components/print/footer.blade.php new file mode 100644 index 00000000..3c21f8bd --- /dev/null +++ b/resources/views/components/print/footer.blade.php @@ -0,0 +1,44 @@ +@props([ + 'showCopies' => true, +]) + + + +
merge(['class' => 'print-footer']) }}> + @if($showCopies) +
+ Salinan: Kad Pengenalan    Akaun Bank/ Slip/ Penyata Bank +
+ @endif + + @if(trim($slot) !== '') +
+ {{ $slot }} +
+ @endif +
diff --git a/resources/views/components/print/header.blade.php b/resources/views/components/print/header.blade.php new file mode 100644 index 00000000..cd65e30a --- /dev/null +++ b/resources/views/components/print/header.blade.php @@ -0,0 +1,101 @@ +@props([ + 'cawangan' => '', + 'kodcaw' => '', + 'alamat1' => '', + 'alamat2' => '', + 'notel' => '', + 'tarikh' => '', + 'norujukan' => '', + 'rujukanLabel' => 'No. Rujukan SAG', + 'showDummyBadge' => false, +]) + + + +
merge(['class' => 'print-header']) }}> +
+

KOPERASI PERMODALAN KELANTAN BERHAD (D-5-0228)

+

Ar-Rahn {{ $cawangan }}

+

{{ $alamat1 }}

+

{{ $alamat2 }}

+

Tel: {{ $notel }}

+
+
+ KPKB + Ar-Rahn +
+
+ @if($kodcaw || $cawangan) +
+ Cawangan
+ {{ trim(implode(' — ', array_filter([$kodcaw, $cawangan]))) }} +
+ @endif + @if($tarikh) +
Tarikh
{{ $tarikh }}
+ @endif + @if($norujukan) +
{{ $rujukanLabel }}
{{ $norujukan }}
+ @endif + {{ $slot }} + @if($showDummyBadge) +
CONTOH SAHAJA
+ @endif +
+
diff --git a/resources/views/components/print/watermark.blade.php b/resources/views/components/print/watermark.blade.php new file mode 100644 index 00000000..38e1b73c --- /dev/null +++ b/resources/views/components/print/watermark.blade.php @@ -0,0 +1,25 @@ + + + diff --git a/resources/views/customers/v1/index.blade.php b/resources/views/customers/v1/index.blade.php index ca296dda..a2fc5dae 100644 --- a/resources/views/customers/v1/index.blade.php +++ b/resources/views/customers/v1/index.blade.php @@ -210,15 +210,17 @@