diff --git a/app/Http/Controllers/KhazanahController.php b/app/Http/Controllers/KhazanahController.php index 6332ed02..dd6c3a19 100644 --- a/app/Http/Controllers/KhazanahController.php +++ b/app/Http/Controllers/KhazanahController.php @@ -29,6 +29,7 @@ use Carbon\Carbon; use PDF; use DateTime; use App\Helper; +use App\Services\NotificationService; use Log; class KhazanahController extends Controller @@ -1084,6 +1085,7 @@ class KhazanahController extends Controller if($test == 'success'){ event(new PendahuluanSerahan("Lulus","pendahuluan",$auth_user['cawangan'],$request->teller)); + $this->notifyTellerAliranTunai('Lulus', 'pendahuluan', $auth_user['cawangan'], $request->teller, $amount); return redirect()->route('khazanah.pendahulu'); }else{ return redirect()->route('khazanah.pendahulu')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.'); @@ -1109,6 +1111,7 @@ class KhazanahController extends Controller if($test == 'success'){ event(new PendahuluanSerahan("Lulus","serahan",$auth_user['cawangan'],$request->teller)); + $this->notifyTellerAliranTunai('Lulus', 'serahan', $auth_user['cawangan'], $request->teller, $request->amt); return redirect()->route('khazanah.serahan'); }else{ return redirect()->route('khazanah.serahan')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.'); @@ -2848,10 +2851,43 @@ class KhazanahController extends Controller ]); event(new PendahuluanSerahan("Batal","all",$auth_user['cawangan'],$kodlaluan)); + $this->notifyTellerAliranTunai('Batal', 'all', $kodcaw, $kodlaluan); return response('Berjaya'); } + /** + * Inbox for the teller waiting on aliran tunai. Failures must not block approval. + * + * @param string $pengesahan + * @param string $jenis + * @param string $kodcaw + * @param string $teller + * @param mixed $amaun + * @return void + */ + protected function notifyTellerAliranTunai($pengesahan, $jenis, $kodcaw, $teller, $amaun = 0) + { + try { + $raw = str_replace(',', '', (string) $amaun); + $amount = is_numeric($raw) ? (float) $raw : 0; + + app(NotificationService::class)->notifyAliranTunaiDecided($kodcaw, $teller, [ + 'pengesahan' => $pengesahan, + 'jenis' => $jenis, + 'amaun' => $amount, + 'kodlaluan' => $teller, + 'khazanah' => Auth::user() ? Auth::user()->name : '', + ]); + } catch (\Throwable $e) { + Log::warning('Aliran tunai teller notification failed: '.$e->getMessage(), [ + 'kodcaw' => $kodcaw, + 'teller' => $teller, + 'jenis' => $jenis, + ]); + } + } + public function SuratAkuanGadaian(Request $request) { $auth_user = Auth::user(); diff --git a/app/Http/Controllers/OperasiController.php b/app/Http/Controllers/OperasiController.php index 7b5f6491..c394cf34 100644 --- a/app/Http/Controllers/OperasiController.php +++ b/app/Http/Controllers/OperasiController.php @@ -9,6 +9,7 @@ use App\Events\StatusLiked; use App\Events\NotifikasiKelulusan; use App\Events\NotifikasiAnggota; use App\Events\Dashboard\KomoditiNotify; +use App\Services\NotificationService; use Illuminate\Support\Facades\Auth; use Illuminate\Http\Request; @@ -309,9 +310,48 @@ class OperasiController extends Controller event(new StatusLiked($auth_user['cawangan'], $auth_user['name'])); + $this->notifyGadaiAboveTenThousand($auth_user, $request, $cawangan_info); + return response()->json($siri, 201); } + /** + * Inbox for PC/PPC and operasi when teller mohon kelulusan gadai >= RM10,000. + */ + protected function notifyGadaiAboveTenThousand($auth_user, Request $request, $cawangan_info) + { + $raw = str_replace(',', '', (string) $request->input('jumpinjam', 0)); + $pinjaman = is_numeric($raw) ? (float) $raw : 0; + if ($pinjaman < NotificationService::GADAI_KELULUSAN_MIN) { + return; + } + + $status = strtolower((string) $request->input('status', '')); + if (strpos($status, 'tebus') !== false) { + return; + } + + $namaCaw = ''; + if (is_array($cawangan_info)) { + $namaCaw = isset($cawangan_info['details_caw']) ? $cawangan_info['details_caw'] : ''; + } + + try { + app(NotificationService::class)->notifyGadaiKelulusan($auth_user['cawangan'], [ + 'norujukan' => $request->norujukan, + 'pinjaman' => $pinjaman, + 'teller' => $request->teller ?: $auth_user['name'], + 'cawangan_nama' => $namaCaw, + 'status' => $request->status, + ]); + } catch (\Throwable $e) { + Log::warning('Gadai kelulusan notification failed: '.$e->getMessage(), [ + 'norujukan' => $request->norujukan, + 'cawangan' => $auth_user['cawangan'], + ]); + } + } + public function createkelulusanAnggota(Request $request) { @@ -320,8 +360,6 @@ class OperasiController extends Controller $api_url = config('api.url'); $current = Carbon::now(); - Log::info('createkelulusanAnggota', $request->all()); - // Dapatkan maklumat cawangan $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')]) ->get($api_url . '/api/cawangan/' . $auth_user['cawangan']) @@ -360,7 +398,6 @@ class OperasiController extends Controller 'body' => $response->body(), ]); } catch (\Throwable $th) { - Log::error('❌ createkelulusanAnggota error: ' . $th->getMessage()); return response()->json([ 'success' => false, 'message' => $th->getMessage(), diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index 1ca32a52..bb2953f6 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -4,6 +4,7 @@ namespace App\Services; use App\StaffNotification; use App\User; +use Illuminate\Support\Facades\Log; class NotificationService { @@ -12,6 +13,10 @@ class NotificationService const TYPE_PEMBIAYAAN_BATCH_OPERASI = 'pembiayaan_online.batch_operasi'; const TYPE_PEMBIAYAAN_REJECTED_PC = 'pembiayaan_online.rejected_pc'; const TYPE_PEMBIAYAAN_PAID_PC = 'pembiayaan_online.paid_pc'; + const TYPE_GADAI_KELULUSAN = 'gadai.kelulusan'; + const TYPE_ALIRAN_TUNAI_PENDING = 'aliran_tunai.pending_khazanah'; + const TYPE_ALIRAN_TUNAI_DECIDED = 'aliran_tunai.decided_teller'; + const GADAI_KELULUSAN_MIN = 10000; /** * Create one inbox row per staff matching role + branch. @@ -70,6 +75,10 @@ class NotificationService $role = isset($audience['role']) ? $audience['role'] : null; $data = isset($input['data']) && is_array($input['data']) ? $input['data'] : []; + if ($type === self::TYPE_ALIRAN_TUNAI_PENDING && $kodcaw) { + return $this->notifyAliranTunaiPending($kodcaw, $data); + } + $payload = $this->payloadForType($type, $kodcaw, $data); if (!empty($input['title'])) { @@ -93,6 +102,90 @@ class NotificationService return collect(); } + /** + * Gadai >= RM10,000: inbox for PC/PPC at the branch and all operasi. + * + * @param string $kodcaw + * @param array $data + * @return \Illuminate\Support\Collection + */ + public function notifyGadaiKelulusan($kodcaw, array $data) + { + $pinjaman = isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0; + if ($pinjaman < self::GADAI_KELULUSAN_MIN) { + return collect(); + } + + $payload = $this->payloadForType(self::TYPE_GADAI_KELULUSAN, $kodcaw, $data); + + $created = collect(); + foreach (['pc', 'ppc'] as $role) { + $created = $created->merge( + $this->notifyRoleAtCawangan($role, $kodcaw, array_merge($payload, [ + 'url' => '/kelulusan', + ])) + ); + } + + return $created->merge( + $this->notifyRole('operasi', array_merge($payload, [ + 'url' => '/operasi/kelulusan', + ])) + ); + } + + /** + * Teller pendahuluan/serahan: inbox for PC/PPC at the branch. + * + * @param string $kodcaw + * @param array $data + * @return \Illuminate\Support\Collection + */ + public function notifyAliranTunaiPending($kodcaw, array $data) + { + $payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_PENDING, $kodcaw, $data); + + $created = collect(); + foreach (['pc', 'ppc'] as $role) { + $created = $created->merge($this->notifyRoleAtCawangan($role, $kodcaw, $payload)); + } + + return $created; + } + + /** + * Khazanah lulus/batal: inbox for the teller who requested cash. + * + * @param string $kodcaw + * @param string $tellerName + * @param array $data + * @return \Illuminate\Support\Collection + */ + public function notifyAliranTunaiDecided($kodcaw, $tellerName, array $data) + { + $tellerName = trim((string) $tellerName); + if ($tellerName === '') { + return collect(); + } + + $payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_DECIDED, $kodcaw, $data); + + $user = User::query() + ->where('name', $tellerName) + ->where('cawangan', $kodcaw) + ->first(); + + if (!$user) { + $user = User::query()->where('name', $tellerName)->first(); + } + + if (!$user) { + return collect(); + } + + return collect([$this->notifyUser($user, $kodcaw, $user->roles, $payload)]); + } + /** * @param string $type * @param string|null $kodcaw @@ -223,6 +316,78 @@ class NotificationService ]; } + if ($type === self::TYPE_ALIRAN_TUNAI_PENDING) { + $jenis = $this->aliranTunaiJenis($data); + $amaun = $this->aliranTunaiAmaun($data, $jenis); + $teller = isset($data['kodlaluan']) ? trim((string) $data['kodlaluan']) : ''; + $label = $jenis === 'serahan' ? 'serahan' : 'pendahuluan'; + + return [ + 'type' => $type, + 'cawangan' => $kodcaw, + 'title' => $jenis === 'serahan' ? 'Permohonan serahan modal' : 'Permohonan pendahuluan', + 'body' => sprintf( + '%s mohon %s RM %s', + $teller !== '' ? $teller : 'Teller', + $label, + number_format($amaun, 2) + ), + 'url' => $jenis === 'serahan' ? '/serahan' : '/pendahuluan', + 'data' => $data, + ]; + } + + if ($type === self::TYPE_ALIRAN_TUNAI_DECIDED) { + $jenis = $this->aliranTunaiJenis($data); + $amaun = $this->aliranTunaiAmaun($data, $jenis); + $lulus = isset($data['pengesahan']) && strcasecmp((string) $data['pengesahan'], 'Lulus') === 0; + $label = 'Pendahuluan'; + if ($jenis === 'serahan') { + $label = 'Serahan'; + } elseif ($jenis === 'all') { + $label = 'Aliran tunai'; + } + + if ($lulus) { + $title = $label.' diluluskan'; + $body = sprintf('%s RM %s telah diluluskan', $label, number_format($amaun, 2)); + } else { + $title = $label.' tidak diluluskan'; + $body = sprintf('Permohonan %s tidak diluluskan', strtolower($label)); + } + + return [ + 'type' => $type, + 'cawangan' => $kodcaw, + 'title' => $title, + 'body' => $body, + 'url' => '/aliran_tunai', + 'data' => $data, + ]; + } + + if ($type === self::TYPE_GADAI_KELULUSAN) { + $norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'gadai'; + $amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2); + $teller = isset($data['teller']) ? trim((string) $data['teller']) : ''; + $namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw; + + return [ + 'type' => $type, + 'cawangan' => $kodcaw, + 'title' => 'Kelulusan gadai melebihi RM 10,000', + 'body' => sprintf( + '%s — %s (RM %s)%s', + $namaCaw, + $norujukan, + $amaun, + $teller !== '' ? ' — '.$teller : '' + ), + 'url' => '/kelulusan', + 'data' => $data, + ]; + } + return [ 'type' => $type, 'cawangan' => $kodcaw, @@ -233,6 +398,46 @@ class NotificationService ]; } + /** + * @param array $data + * @return string pendahuluan|serahan|all + */ + protected function aliranTunaiJenis(array $data) + { + if (!empty($data['jenis'])) { + $jenis = strtolower(trim((string) $data['jenis'])); + if ($jenis === 'serahan' || $jenis === 'pendahuluan' || $jenis === 'all') { + return $jenis; + } + } + + $modal = isset($data['modal']) ? (float) $data['modal'] : 0; + $serahan = isset($data['serahan']) ? (float) $data['serahan'] : 0; + if ($serahan > 0 && $modal <= 0) { + return 'serahan'; + } + + return 'pendahuluan'; + } + + /** + * @param array $data + * @param string $jenis + * @return float + */ + protected function aliranTunaiAmaun(array $data, $jenis) + { + if (isset($data['amaun']) && is_numeric($data['amaun'])) { + return (float) $data['amaun']; + } + + if ($jenis === 'serahan') { + return isset($data['serahan']) ? (float) $data['serahan'] : 0; + } + + return isset($data['modal']) ? (float) $data['modal'] : 0; + } + /** * Create one inbox row for a specific user. * diff --git a/resources/views/dummy/borang_pengesahan_online.blade.php b/resources/views/dummy/borang_pengesahan_online.blade.php index 1f88054d..e8844cd4 100644 --- a/resources/views/dummy/borang_pengesahan_online.blade.php +++ b/resources/views/dummy/borang_pengesahan_online.blade.php @@ -204,7 +204,7 @@ />
-

Borang Pengesahan Penerimaan Pembiayaan
Secara Online (Akaun Bank)

+

Borang Pengesahan Penerimaan Pembiayaan
Secara Atas Talian (Online)

Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.

diff --git a/resources/views/gadaian/v2/index.blade.php b/resources/views/gadaian/v2/index.blade.php index 67df6cb9..3ba68a2c 100644 --- a/resources/views/gadaian/v2/index.blade.php +++ b/resources/views/gadaian/v2/index.blade.php @@ -246,9 +246,8 @@
- +
- Pilihan Online untuk pembiayaan RM5,000 – RM50,000 sahaja. @@ -267,7 +271,7 @@ @@ -316,5 +320,25 @@ $('#paidNorujukan').text($(this).data('norujukan')); $('#paidModal').modal('show'); }); + + $(document).on('submit', '.js-swal-confirm', function (e) { + e.preventDefault(); + var form = this; + Swal.fire({ + title: $(form).attr('data-title') || 'Pengesahan', + html: $(form).attr('data-html') || 'Teruskan tindakan ini?', + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: $(form).attr('data-confirm') || 'Ya', + cancelButtonText: 'Batal', + allowOutsideClick: false + }).then(function (result) { + if (result.isConfirmed) { + form.submit(); + } + }); + }); @endsection diff --git a/resources/views/pembiayaan_online/pc/senarai.blade.php b/resources/views/pembiayaan_online/pc/senarai.blade.php index 7c38edf7..a5b141bf 100644 --- a/resources/views/pembiayaan_online/pc/senarai.blade.php +++ b/resources/views/pembiayaan_online/pc/senarai.blade.php @@ -36,6 +36,17 @@ $lineRows = collect($lines); $verifiedCount = $lineRows->where('status', 'VERIFIED_PC')->count(); $unverifiedCount = $lineRows->count() - $verifiedCount; + $slotArr = is_array($slot) ? $slot : []; + $slotName = $slotArr['slot'] ?? 'BATCH1'; + $slotCutoff = $slotArr['cutoff'] ?? '11:00'; + $slotProses = $slotArr['proses'] ?? '12:30'; + $slotLewat = !empty($slotArr['lewat']); + $slotJadual = $slotArr['jadual'] ?? [ + ['slot' => 'BATCH1', 'label' => 'Batch 1', 'mula' => '00:00', 'cutoff' => '11:00', 'proses' => '12:30'], + ['slot' => 'BATCH2', 'label' => 'Batch 2', 'mula' => '11:00', 'cutoff' => '15:00', 'proses' => '16:30'], + ]; + $slotBakiMinit = (int) ($slotArr['baki_minit'] ?? 0); + $slotHampirTamat = !$slotLewat && $slotBakiMinit > 0 && $slotBakiMinit <= 30; @endphp @if($isClosed) @@ -59,6 +70,66 @@ @endif +
+
+
+ Jadual Slot Batch +
+ PC / PPC sila semak dan hantar senarai sebelum tamat slot. + Rekod yang dihantar selepas cutoff mungkin diproses pada slot atau hari berikutnya. +
+
+
+ @if($slotLewat) + Slot semasa: {{ $slotName }} (selepas cutoff {{ $slotCutoff }}) + @else + Slot semasa: {{ $slotName }} + · Hantar sebelum {{ $slotCutoff }} +
+ @if($slotBakiMinit > 0) + Baki masa: {{ intdiv($slotBakiMinit, 60) }}j {{ $slotBakiMinit % 60 }}m + @endif +
+ @endif +
+
+
+ + + + + + + + + + @foreach($slotJadual as $row) + @php + $rowSlot = $row['slot'] ?? ''; + $isCurrent = $rowSlot === $slotName && !$slotLewat; + $isDone = $slotLewat + || ($slotName === 'BATCH2' && $rowSlot === 'BATCH1'); + @endphp + + + + + + @endforeach + +
SlotHantar senarai sebelumProses Operasi
+ {{ $row['label'] ?? $rowSlot }} + @if($isCurrent) + Semasa + @elseif($isDone) + Tamat + @endif + {{ $row['cutoff'] ?? '-' }}~ {{ $row['proses'] ?? '-' }}
+
+
+
@@ -85,7 +156,7 @@
-
Queue PC
+
Senarai Pelanggan menunggu semakan

{{ count($lines) }} rekod

{{ $verifiedCount }} sudah sah @@ -96,11 +167,19 @@
-
+
-
Slot Cadangan
-

{{ $slot['slot'] ?? '-' }}

- {{ $slot['nota'] ?? '' }} +
Slot Semasa
+

{{ $slotName }}

+ + @if($slotLewat) + Cutoff {{ $slotCutoff }} sudah tamat
+ Masih boleh hantar; proses mungkin esok + @else + Hantar sebelum {{ $slotCutoff }} + · Proses ~{{ $slotProses }} + @endif +
@@ -115,10 +194,15 @@

Semak setiap rekod satu-satu (maklumat pelanggan, bank, amaun). Hanya rekod berstatus SAH boleh dipilih dan dihantar ke Operasi. + @if($slotLewat) + Cutoff {{ $slotName }} ({{ $slotCutoff }}) sudah tamat — hantar sekarang jika perlu; Operasi mungkin proses esok. + @else + Sila hantar senarai {{ $slotName }} sebelum {{ $slotCutoff }}. + @endif

@csrf - +
@@ -440,17 +524,46 @@ }); $('#btnSubmitBatch').on('click', function () { - if ($('.row-check:not(:disabled):checked').length === 0) return; - if (!confirm('Hantar rekod yang sudah disahkan ke Operasi?')) return; - $('#formSubmitBatch').submit(); + var n = $('.row-check:not(:disabled):checked').length; + if (n === 0) return; + Swal.fire({ + title: 'Hantar Batch ke Operasi?', + html: n + ' rekod yang sudah SAH akan dihantar sebagai {{ $slotName }}.' + + '
Sila hantar sebelum cutoff {{ $slotCutoff }}.', + icon: 'question', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Ya, Hantar', + cancelButtonText: 'Batal', + allowOutsideClick: false + }).then(function (result) { + if (result.isConfirmed) { + $('#formSubmitBatch').submit(); + } + }); }); $('.btn-unverify').on('click', function () { var id = $(this).data('id'); var sag = $(this).data('norujukan') || ''; - if (!confirm('Batal semakan ' + sag + '? Rekod ini tidak boleh dihantar sehingga disemak semula.')) return; - $('#formUnverify').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/unverify"); - $('#formUnverify').submit(); + Swal.fire({ + title: 'Batal Semakan?', + html: 'Batal semakan ' + (sag || 'rekod ini') + '?' + + '
Rekod ini tidak boleh dihantar sehingga disemak semula.', + icon: 'warning', + showCancelButton: true, + confirmButtonColor: '#3085d6', + cancelButtonColor: '#d33', + confirmButtonText: 'Ya, Batal', + cancelButtonText: 'Tutup', + allowOutsideClick: false + }).then(function (result) { + if (result.isConfirmed) { + $('#formUnverify').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/unverify"); + $('#formUnverify').submit(); + } + }); }); $('.btn-verify').on('click', function () { @@ -513,5 +626,32 @@ $('#editKodbank').on('change', function () { $('#editNamabank').val($('#editKodbank option:selected').data('nama') || ''); }); + + function pad2(n) { + return n < 10 ? '0' + n : String(n); + } + + function tickSlotCountdown() { + var el = document.getElementById('slotCountdown'); + if (!el) return; + if (el.getAttribute('data-lewat') === '1') return; + var cutoff = el.getAttribute('data-cutoff') || ''; + var parts = cutoff.split(':'); + if (parts.length < 2) return; + var now = new Date(); + var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0); + var diff = end - now; + if (diff <= 0) { + el.textContent = 'Cutoff telah tamat — sila muat semula halaman'; + return; + } + var h = Math.floor(diff / 3600000); + var m = Math.floor((diff % 3600000) / 60000); + var s = Math.floor((diff % 60000) / 1000); + el.textContent = 'Baki masa: ' + h + 'j ' + pad2(m) + 'm ' + pad2(s) + 's'; + } + + tickSlotCountdown(); + setInterval(tickSlotCountdown, 1000); @endsection diff --git a/resources/views/pembiayaan_online/pc/topup.blade.php b/resources/views/pembiayaan_online/pc/topup.blade.php index 18a5fec9..df12dda8 100644 --- a/resources/views/pembiayaan_online/pc/topup.blade.php +++ b/resources/views/pembiayaan_online/pc/topup.blade.php @@ -131,7 +131,7 @@ -
+ {{--
Tutup Modal Online (EOD)
@@ -153,7 +153,7 @@ @endif
-
+ --}}
diff --git a/resources/views/print/borang_pengesahan_online.blade.php b/resources/views/print/borang_pengesahan_online.blade.php index affd244f..d4d19d76 100644 --- a/resources/views/print/borang_pengesahan_online.blade.php +++ b/resources/views/print/borang_pengesahan_online.blade.php @@ -195,7 +195,7 @@ />
-

Borang Pengesahan Penerimaan Pembiayaan
Secara Online (Akaun Bank)

+

Borang Pengesahan Penerimaan Pembiayaan
Secara Atas Talian (Online)

Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.