From a2d85c3354e2f33de883c0a662d102b8110bb8fd Mon Sep 17 00:00:00 2001 From: ismailmasseran Date: Tue, 1 Sep 2026 14:13:09 +0800 Subject: [PATCH] Explore/ismail (#11) Co-authored-by: ISMAIL MASSERAN Co-authored-by: ISMAIL MASSERAN Reviewed-on: https://git.koppkb.com/KoPKB/kaunter/pulls/11 --- .env.docker | 54 ++ .env.example | 2 +- .env.staging | 11 +- .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/GadaianController.php | 39 +- app/Http/Controllers/KhazanahController.php | 3 + app/Http/Controllers/OperasiController.php | 3 + .../PembiayaanOnlineController.php | 744 ++++++++++++++++++ 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/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 | 630 ++++++++------- .../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 +- resources/views/customers/v1/index.blade.php | 5 +- resources/views/customers/v1/info.blade.php | 2 +- resources/views/customers/v2/index.blade.php | 12 +- resources/views/customers/v2/info.blade.php | 103 ++- 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 | 247 +++++- .../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 | 46 ++ .../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 | 271 +++++++ .../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 +- routes/web.php | 35 + 124 files changed, 3506 insertions(+), 521 deletions(-) create mode 100644 .env.docker 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/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 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/.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/GadaianController.php b/app/Http/Controllers/GadaianController.php index a6cbbbea..2dff3673 100644 --- a/app/Http/Controllers/GadaianController.php +++ b/app/Http/Controllers/GadaianController.php @@ -211,13 +211,50 @@ 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'); } 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..9c68534a 100644 --- a/app/Http/Controllers/OperasiController.php +++ b/app/Http/Controllers/OperasiController.php @@ -321,6 +321,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"; diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php new file mode 100644 index 00000000..f7414a21 --- /dev/null +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -0,0 +1,744 @@ +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); + } + + $batchNo = $res->json()['batch']['batch_no'] ?? ''; + return redirect()->route('pembiayaan.online.pc') + ->with('message', 'Batch dihantar ke Operasi' . ($batchNo ? ': ' . $batchNo : '.')); + } + + /** + * 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, + ]); + } + + 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('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..e15ccbcb 100644 --- a/public/js/mykad-reader-customer.js +++ b/public/js/mykad-reader-customer.js @@ -1,293 +1,371 @@ 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'); + } else { + photo.src = original_photo_loader; + photo_container.classList.add('d-none'); + photo_container.classList.remove('d-flex'); + } + + 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'); + } }; -button_read_mykad.addEventListener("click", handleMyKadReader); -button_close_modal.addEventListener("click", hideMykadPhoto); +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 @@
- - + @endpush diff --git a/resources/views/customers/v1/info.blade.php b/resources/views/customers/v1/info.blade.php index 0ec69725..9b306683 100644 --- a/resources/views/customers/v1/info.blade.php +++ b/resources/views/customers/v1/info.blade.php @@ -422,7 +422,7 @@ - - + @endpush diff --git a/resources/views/customers/v2/info.blade.php b/resources/views/customers/v2/info.blade.php index beded8f1..3b10e165 100644 --- a/resources/views/customers/v2/info.blade.php +++ b/resources/views/customers/v2/info.blade.php @@ -230,7 +230,6 @@ // 4️⃣ Hantar permohonan ke backend (Laravel) const response = await fetch("{{ route('operasi.createkelulusan-anggota') }}", { - method: 'POST', headers: { "Content-Type": "application/json", @@ -238,15 +237,14 @@ }, body: JSON.stringify({ mohon: "Assign Anggota", - noic: noic, + nokp: noic, norujukan: norujukan, komen: komen, teller: "{{ Auth::user()->name }}", tagoperasi: 1 // 1 = tunggu kelulusan operasi }) - - console.log(`Response: ${response}`); }); + console.log(`Response: ${response}`); const result = await response.json(); @@ -714,68 +712,67 @@
- @endsection diff --git a/resources/views/khazanah/saginfo.blade.php b/resources/views/khazanah/saginfo.blade.php index 402bfd01..40638d27 100644 --- a/resources/views/khazanah/saginfo.blade.php +++ b/resources/views/khazanah/saginfo.blade.php @@ -104,7 +104,7 @@ @@ -199,7 +199,7 @@