diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..79c1ea3 --- /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/.gitignore b/.gitignore index 1ed293c..80ba177 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 f821d23..ca86015 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 0000000..a80c8c5 --- /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 0000000..dab764a --- /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 f29fcc2..b9abb66 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 a6cbbbe..2dff367 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 03e7252..a4b074b 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 7418912..9c68534 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 0000000..f7414a2 --- /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/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 3049068..08a3ae1 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 33e4953..70034b8 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 0000000..974dacb --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,15 @@ + @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 9459d54..384fad4 100644 --- a/resources/views/aliran_tunai/index.blade.php +++ b/resources/views/aliran_tunai/index.blade.php @@ -26,7 +26,7 @@
| Cawangan | +Had | +Tambah | +Digunakan | +Kurang (EOD) | +Baki | +Status | +Ditutup Oleh | ++ |
|---|---|---|---|---|---|---|---|---|
|
+ {{ $row['cawangan_nama'] ?? '-' }}
+ {{ $row['kodcaw'] ?? '' }}
+ |
+ {{ number_format((float)($row['had'] ?? 0), 2) }} | +{{ number_format((float)($row['tambah'] ?? 0), 2) }} | +{{ number_format((float)($row['digunakan'] ?? 0), 2) }} | +{{ number_format((float)($row['kurang'] ?? 0), 2) }} | ++ @if($missing) + — + @else + {{ number_format((float)($row['baki'] ?? 0), 2) }} + @endif + | ++ @if($closed) + DITUTUP + @elseif($missing) + TIADA + @else + DIBUKA + @endif + | +
+ @if($closed)
+ {{ $row['ditutup_oleh'] ?? '-' }} + {{ $row['ditutup_at'] ?? '' }} + @else + — + @endif + |
+ + @if($closed) + OK + @elseif($missing) + Tiada had + @else + + @endif + | +
| Tiada data cawangan. | +||||||||
| Batch No | +Cawangan | +Tarikh | +Slot | +Bil | +Jumlah (RM) | +Status | +Dihantar Oleh | ++ |
|---|---|---|---|---|---|---|---|---|
| {{ $batch['batch_no'] ?? '-' }} | +
+ {{ $batch['cawangan_nama'] ?? '-' }}
+ {{ $batch['kodcaw'] ?? '' }}
+ |
+ {{ $batch['tarikh'] ?? '-' }} | +{{ $batch['slot'] ?? '-' }} | +{{ $batch['bilangan'] ?? 0 }} | +{{ number_format((float)($batch['jumlah_keseluruhan'] ?? 0), 2) }} | +{{ $st ?: '-' }} | +{{ $batch['pc_submitted_by'] ?? '-' }} | ++ + Semak + + | +
| Tiada batch untuk tapisan ini. | +||||||||
| ID | +Cawangan | +Tarikh | +Amaun (RM) | +Dimohon Oleh | +Status | +Nota | +Tindakan | +
|---|---|---|---|---|---|---|---|
| {{ $req['id'] ?? '-' }} | +
+ {{ $req['cawangan_nama'] ?? '-' }}
+ {{ $req['kodcaw'] ?? '' }}
+ |
+ {{ $req['tarikh'] ?? '-' }} | +{{ number_format((float)($req['amaun'] ?? 0), 2) }} | +{{ $req['dimohon_oleh'] ?? '-' }} | +{{ $st ?: '-' }} | +{{ $req['nota'] ?? '-' }} | ++ @if($st === 'PENDING') + + + @else + {{ $req['diluluskan_oleh'] ?? '-' }} + @endif + | +
| Tiada permohonan untuk tapisan ini. | +|||||||
| Status | +SAG | +Pelanggan | +No KP | +Bank | +No Akaun | +Pinjaman (RM) | +Rujukan Bayaran | +
|---|---|---|---|---|---|---|---|
| {{ $line['status'] ?? '-' }} | +{{ $line['norujukan'] ?? '-' }} | +{{ $line['nama_pelanggan'] ?? '-' }} | +{{ $line['nokp'] ?? '-' }} | +{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }} | +{{ $line['noakaun'] ?? '-' }} | +{{ number_format((float)($line['pinjaman'] ?? 0), 2) }} | +{{ $line['no_rujukan_bayaran'] ?? '-' }} | +
+ Semak jumlah & bukti bayaran sepadan dengan pelanggan. Setelah semak, tanda selesai. + (WhatsApp/SMS blast boleh disambung kemudian — butang ini menanda rekod sebagai selesai semak.) +
+ +| Batch No | +Tarikh | +Slot | +Bil | +Jumlah (RM) | +Status | +Dihantar Oleh | ++ |
|---|---|---|---|---|---|---|---|
| {{ $batch['batch_no'] ?? '-' }} | +{{ $batch['tarikh'] ?? '-' }} | +{{ $batch['slot'] ?? '-' }} | +{{ $batch['bilangan'] ?? 0 }} | +{{ number_format((float)($batch['jumlah_keseluruhan'] ?? 0), 2) }} | +{{ $batch['status'] ?? '-' }} | +{{ $batch['pc_submitted_by'] ?? '-' }} | ++ + Lihat + + | +
| Belum ada batch. | +|||||||
+ Tutup hari ini: baki yang tidak digunakan akan dikembalikan, + dan tiada lagi gadai online / permohonan untuk hari ini. + Esok mohon had baru daripada Operasi. +
+ @if($isClosed) + + @elseif($isMissing) + + @else + + @endif +| Tarikh | +Amaun (RM) | +Status | +Dimohon Oleh | +Diluluskan Oleh | +Nota | +
|---|---|---|---|---|---|
| {{ $req['tarikh'] ?? '-' }} | +{{ number_format((float)($req['amaun'] ?? 0), 2) }} | +{{ $st ?: '-' }} | +{{ $req['dimohon_oleh'] ?? '-' }} | +{{ $req['diluluskan_oleh'] ?? '-' }} | +{{ $req['nota'] ?? '-' }} | +
| Belum ada permohonan. | +|||||