diff --git a/.env.annur b/.env.annur index 057f883f..90993f00 100644 --- a/.env.annur +++ b/.env.annur @@ -47,4 +47,7 @@ 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 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED + +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true diff --git a/.env.docker b/.env.docker index b7f9519a..db6c6098 100644 --- a/.env.docker +++ b/.env.docker @@ -53,4 +53,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MYKAD_READER_PORT=8085 MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED -INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret \ No newline at end of file +INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret + +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true \ No newline at end of file diff --git a/.env.domain b/.env.domain index 1cfb20ce..3e238b7e 100644 --- a/.env.domain +++ b/.env.domain @@ -52,4 +52,7 @@ 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 +MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED + +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true diff --git a/.env.example b/.env.example index 25665a2d..6bdf372b 100644 --- a/.env.example +++ b/.env.example @@ -52,3 +52,8 @@ MYKAD_READER_ENABLED_CAWANGAN='' INTERNAL_NOTIFICATION_SECRET= LOGVIEWER_PASSWORD= + +# Commodity providers available when a teller submits gadai. +# Turn either or both on. When both are true, the teller chooses on the form. +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true diff --git a/.env.production b/.env.production index 020d9fd0..b556af48 100644 --- a/.env.production +++ b/.env.production @@ -50,4 +50,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' -INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd" \ No newline at end of file +INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd" + +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true \ No newline at end of file diff --git a/.env.staging b/.env.staging index bdf6ddc6..4d85c65e 100644 --- a/.env.staging +++ b/.env.staging @@ -54,4 +54,7 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' -INTERNAL_NOTIFICATION_SECRET=staging_noti_secret \ No newline at end of file +INTERNAL_NOTIFICATION_SECRET=staging_noti_secret + +COMMODITY_SHORAKA_ENABLED=true +COMMODITY_BSAS_ENABLED=true \ No newline at end of file diff --git a/app/Http/Controllers/GadaianController.php b/app/Http/Controllers/GadaianController.php index 041d0a9b..9a2db761 100644 --- a/app/Http/Controllers/GadaianController.php +++ b/app/Http/Controllers/GadaianController.php @@ -285,15 +285,24 @@ class GadaianController extends Controller public function store(Request $request) { $auth_user = Auth::user(); - $shoraka = app(\App\Services\ShorakaFinancingService::class); - if ($shoraka->hasOrder($request->norujukan_gadai)) { - $message = 'Order Shoraka telah dihantar. Jangan hantar semula.'; - if ($request->ajax() || $request->expectsJson()) { - abort(409, $message); - } - return redirect()->back()->with('error', $message); + if (!config('commodity.shoraka') && !config('commodity.bsas')) { + return $this->gadaiStoreResult($request, false, 'Tiada pembekal komoditi diaktifkan.'); } - $shoraka->ensureReady(); + + $provider = $this->selectedCommodityProvider($request); + if ($provider === null) { + return $this->gadaiStoreResult($request, false, 'Sila pilih pembekal komoditi yang diaktifkan.'); + } + + $shoraka = null; + if ($provider === 'shoraka') { + $shoraka = app(\App\Services\ShorakaFinancingService::class); + if ($shoraka->hasOrder($request->norujukan_gadai)) { + return $this->gadaiStoreResult($request, false, 'Order Shoraka telah dihantar. Jangan hantar semula.'); + } + $shoraka->ensureReady(); + } + $caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI')); $pinjamanAmt = round((float) $request->pinjaman, 2); @@ -307,7 +316,7 @@ class GadaianController extends Controller } if ($caraTerima === 'ONLINE' && !$this->hasMatchingBorangOnlinePrint($request)) { - return redirect()->back()->with('error', 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.'); + return $this->gadaiStoreResult($request, false, 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.'); } //$total_nilai_marhun = $request->total_nilai_marhun; @@ -329,6 +338,28 @@ class GadaianController extends Controller $keuntungan12bulan = $keuntungan_sebln * 12; $hargajualan = $request->pinjaman + $keuntungan12bulan; + + if ($provider === 'bsas') { + $bsas = Http::withOptions(['verify' => config('app.api_verify')]) + ->timeout(60) + ->post(config('api.url') . '/api/komuditi/bsas/order', [ + 'norujukan' => $request->norujukan_gadai, + 'kodcaw' => $auth_user['cawangan'], + 'pinjaman' => round((float) $request->pinjaman, 2), + 'hargajualan' => round((float) $hargajualan, 2), + 'nama_pelanggan' => $request->nama_pelanggan ?: $request->nama_akaun, + 'teller' => $auth_user['name'], + ]); + + if (!$bsas->successful()) { + $body = $bsas->json(); + $msg = is_array($body) && isset($body['message']) + ? $body['message'] + : 'Gagal pembelian komoditi BSAS. Sila cuba lagi.'; + return $this->gadaiStoreResult($request, false, $msg); + } + } + //Log::info('DEBUG NOKP:' . $request->nokp); $gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/' . $auth_user['cawangan'] . '/' . $request->norujukan_gadai, [ 'kodcaw' => $auth_user['cawangan'], @@ -353,12 +384,20 @@ class GadaianController extends Controller 'rebate' => $this->parseMoney($request->rebate), ])->throw()->json(); + if ($provider === 'shoraka') { + return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [ + 'cara_terima' => $caraTerima, + 'pinjaman' => $pinjamanAmt, + 'temporary_gadai' => true, + ]); + } - return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [ - 'cara_terima' => $caraTerima, - 'pinjaman' => $pinjamanAmt, - 'temporary_gadai' => true, - ]); + $error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt); + if ($error !== null) { + return $this->gadaiStoreResult($request, false, $error); + } + + return $this->gadaiStoreResult($request, true); } public function completeShorakaFinancing(Request $request) @@ -366,6 +405,20 @@ class GadaianController extends Controller $auth_user = Auth::user(); $caraTerima = $request->cara_terima; $pinjamanAmt = round((float) $request->pinjaman, 2); + $error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt); + if ($error !== null) { + throw new \RuntimeException($error); + } + + return redirect()->route('customer_info'); + } + + /** + * Cash or online disbursement after the commodity order and gadai save. + * Returns an error message, or null when disbursement succeeded. + */ + protected function disburseGadai(Request $request, $auth_user, string $caraTerima, float $pinjamanAmt): ?string + { if ($caraTerima === 'ONLINE') { $online = Http::withOptions(['verify' => config('app.api_verify')]) ->asForm() @@ -385,10 +438,9 @@ class GadaianController extends Controller if (!$online->successful()) { $body = $online->json(); - $msg = is_array($body) && isset($body['message']) + return is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal daftar pembiayaan online. Sila cuba lagi.'; - throw new \RuntimeException($msg); } $this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt); @@ -406,6 +458,55 @@ class GadaianController extends Controller ? 'Gadai berjaya. Pembiayaan online menunggu semakan PC.' : 'Pembiayaan berjaya dihantar.'); + return null; + } + + /** + * Provider chosen on the gadai form, limited to those enabled in config. + */ + protected function selectedCommodityProvider(Request $request): ?string + { + $enabled = array_values(array_filter([ + config('commodity.shoraka') ? 'shoraka' : null, + config('commodity.bsas') ? 'bsas' : null, + ])); + + if ($enabled === []) { + return null; + } + + $selected = strtolower(trim((string) $request->input('commodity_provider', ''))); + if ($selected === '' && count($enabled) === 1) { + return $enabled[0]; + } + + return in_array($selected, $enabled, true) ? $selected : null; + } + + /** + * JSON for AJAX Terima (BSAS progress UI); normal redirect otherwise. + */ + protected function gadaiStoreResult(Request $request, bool $ok, ?string $message = null) + { + if ($request->ajax() || $request->wantsJson()) { + if (!$ok) { + return response()->json([ + 'ok' => false, + 'message' => $message ?: 'Gadai tidak berjaya.', + ], 422); + } + + return response()->json([ + 'ok' => true, + 'message' => $message ?: 'Gadai berjaya.', + 'redirect' => route('customer_info'), + ]); + } + + if (!$ok) { + return redirect()->back()->with('error', $message ?: 'Gadai tidak berjaya.'); + } + return redirect()->route('customer_info'); } @@ -711,20 +812,11 @@ class GadaianController extends Controller // $pembayaran_segera = "https://spare.arrahn.com.my/pembayaransegera"; $pembayaran_segera = "https://pay.arrahn.com.my"; - $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); - if ($shorakaPrint) { - $komoditi = $shorakaPrint['komoditi']; - $product = $shorakaPrint['product']; - $kuantiti = $shorakaPrint['kuantiti']; - } else { - $komoditi = Http::withOptions(['verify' => config('app.api_verify')]) - ->get($api_url . '/api/komuditi/check') - ->json(); - $product = Http::withOptions(['verify' => config('app.api_verify')]) - ->get($api_url . '/api/komuditi/description', ['kodproduk' => $komoditi['kodproduct']]) - ->json(); - $kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai']; - } + $commodityPrint = app(\App\Services\CommodityCertificate::class) + ->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0); + $komoditi = $commodityPrint['komoditi']; + $product = $commodityPrint['product']; + $kuantiti = $commodityPrint['kuantiti']; if ($gadai_detail['tagbaru'] == 1) { $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')]) diff --git a/app/Http/Controllers/KomuditiController.php b/app/Http/Controllers/KomuditiController.php index 72474dbf..7fa44aa0 100644 --- a/app/Http/Controllers/KomuditiController.php +++ b/app/Http/Controllers/KomuditiController.php @@ -55,4 +55,274 @@ class KomuditiController extends Controller return redirect()->route('komuditi'); } + + /** + * Khazanah branch list of BSAS commodity orders. + */ + public function khazanahBsas() + { + $auth_user = Auth::user(); + $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan']) + ->json(); + + return view('homepage.khazanah.bsas_komoditi', compact('cawangan_info')); + } + + /** + * Dummy BSAS eCert layout preview (bid, OTC, STB, then one commodity schedule). + */ + public function previewBsasEcert() + { + $line = [ + ['SUPPLIER' => 'FGV TRADING SDN BHD', 'VOLUME' => '0.75276406'], + ]; + + return view('print.bsas_ecert', $this->presentBsasCertificates([ + 'e_cert_no' => 'CPO21OCT26-0000031-000', + 'serial_number' => '1', + 'otc_counter_party' => 'ALI BIN ABU', + 'otc_murabaha_value' => '3,814.40', + 'product_code' => 'CPO-MSIA-09', + 'bid_ecert' => [ + 'ECERTNO' => 'CPO21OCT26-0000031-000', + 'BUYER' => 'KOPERASI PERMODALAN KELANTAN BERHAD', + 'OWNER' => 'KOPERASI PERMODALAN KELANTAN BERHAD', + 'BIDNO' => '8', + 'TOTALVALUE' => '3,200.00', + 'CURRENCY' => 'MYR', + 'PRICE' => '4,251.00', + 'PRICE_MYR_EQUIVALENT' => '4,251.00', + 'PURCHASETIMEDATE' => '15:11:53.316 21 Oct 2026', + 'VALUEDATE' => '21 Oct 2026', + 'PNAME' => 'CPO-MSIA-09', + 'PVOLUME' => '0.75276406', + 'LINE' => $line, + ], + 'otc_ecert' => [ + 'ECERTNO' => 'CPO21OCT26-0000031-000', + 'SELLER' => 'KOPERASI PERMODALAN KELANTAN BERHAD', + 'BUYER' => 'ALI BIN ABU', + 'REPORTINGTIMEDATE' => '15:11:53.320 21 Oct 2026', + 'VALUEDATE' => '21 Oct 2026', + 'PNAME' => 'CPO-MSIA-09', + 'PVOLUME' => '0.75276406', + 'CURRENCY' => 'MYR', + 'PRICE' => '4,251.00', + 'PRICE_MYR_EQUIVALENT' => '4,251.00', + 'TOTALVALUE' => '3,200.00', + 'MURABAHAVALUE' => '3,814.40', + 'LINE' => $line, + ], + 'stb_ecert' => [ + 'ECERTNO' => 'CPO21OCT26-0000031-000', + 'SELLER' => 'ALI BIN ABU', + 'BUYER' => 'BURSA MALAYSIA ISLAMIC SERVICES', + 'TOTALVALUE' => '3,200.00', + 'CURRENCY' => 'MYR', + 'PRICE' => '4,251.00', + 'PRICE_MYR_EQUIVALENT' => '4,251.00', + 'SELLINGTIMEDATE' => '15:11:53.325 21 Oct 2026', + 'VALUEDATE' => '21 Oct 2026', + 'PNAME' => 'CPO-MSIA-09', + 'PVOLUME' => '0.75276406', + 'LINE' => $line, + ], + ], true)); + } + + /** + * Live BSAS eCerts from stored bid, OTC and STB payloads. + */ + public function showBsasEcert($uuid) + { + $response = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/komuditi/bsas/order/' . urlencode($uuid) . '/ecert'); + + if ($response->status() === 404) { + abort(404, $response->json('message') ?: 'eCert tidak dijumpai.'); + } + + if (!$response->successful()) { + abort(502, 'Gagal memuatkan eCert BSAS.'); + } + + $payload = $response->json(); + if (!is_array($payload)) { + abort(502, 'Gagal memuatkan eCert BSAS.'); + } + + $presented = $this->presentBsasCertificates($payload, false); + if ($presented['certificates'] === []) { + abort(404, 'eCert belum tersedia untuk order ini.'); + } + + return view('print.bsas_ecert', $presented); + } + + /** + * Bid, OTC and STB pages, plus one shared commodity schedule. + */ + protected function presentBsasCertificates(array $payload, bool $isDummy): array + { + $certificates = []; + + $bid = $payload['bid_ecert'] ?? null; + if (is_array($bid) && $bid !== []) { + $certificates[] = $this->mapBidCertificate($bid, $payload); + } + + $otc = $payload['otc_ecert'] ?? null; + if (is_array($otc) && $otc !== []) { + $certificates[] = $this->mapOtcCertificate($otc, $payload); + } + + $stb = $payload['stb_ecert'] ?? null; + if (is_array($stb) && $stb !== []) { + $certificates[] = $this->mapStbCertificate($stb, $payload); + } + + $scheduleSource = []; + if (is_array($otc) && $otc !== []) { + $scheduleSource = $otc; + } elseif (is_array($bid) && $bid !== []) { + $scheduleSource = $bid; + } elseif (is_array($stb) && $stb !== []) { + $scheduleSource = $stb; + } + + $ecertNo = $scheduleSource['ECERTNO'] ?? ($payload['e_cert_no'] ?? '-'); + + return [ + 'certificates' => $certificates, + 'ecert_no' => $ecertNo, + 'lines' => $this->mapBsasLines($scheduleSource), + 'isDummy' => $isDummy, + ]; + } + + protected function mapBidCertificate(array $ecert, array $orderMeta): array + { + [$time, $date] = $this->splitBsasDateTime((string) ($ecert['PURCHASETIMEDATE'] ?? '')); + + return $this->certificatePage( + 'Certificate of Purchase', + 'This is to certify that the following bid transaction has been executed
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.', + $ecert, + $orderMeta, + array_merge([ + ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'], + ['label' => 'Owner', 'value' => $ecert['OWNER'] ?? '-'], + ['label' => 'Bid No', 'value' => $ecert['BIDNO'] ?? ($orderMeta['serial_number'] ?? '-')], + ['label' => 'Purchase Time / Date', 'value' => $time . ' / ' . $date], + ], $this->commodityDetailRows($ecert, $orderMeta)) + ); + } + + protected function mapOtcCertificate(array $ecert, array $orderMeta): array + { + [$time, $date] = $this->splitBsasDateTime((string) ($ecert['REPORTINGTIMEDATE'] ?? '')); + + $page = $this->certificatePage( + 'Certificate of Murabahah', + 'This is to certify that the following transaction has been executed
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.', + $ecert, + $orderMeta, + array_merge([ + ['label' => 'Seller', 'value' => $ecert['SELLER'] ?? 'KOPERASI PERMODALAN KELANTAN BERHAD'], + ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? ($orderMeta['otc_counter_party'] ?? '-')], + ['label' => 'Bid No', 'value' => $orderMeta['serial_number'] ?? '1'], + ['label' => 'Reporting Time / Date', 'value' => $time . ' / ' . $date], + ], $this->commodityDetailRows($ecert, $orderMeta), [ + ['label' => 'Transaction Type', 'value' => '-'], + ]) + ); + + $page['show_murabaha'] = true; + $page['murabaha'] = $ecert['MURABAHAVALUE'] ?? ($orderMeta['otc_murabaha_value'] ?? '-'); + + return $page; + } + + protected function mapStbCertificate(array $ecert, array $orderMeta): array + { + [$time, $date] = $this->splitBsasDateTime((string) ($ecert['SELLINGTIMEDATE'] ?? '')); + + return $this->certificatePage( + 'Certificate of Sale', + 'This is to certify that the following sale transaction has been executed
through the Bursa Suq Al-Sila\' in accordance with the
Rules of Bursa Suq Al-Sila\'.', + $ecert, + $orderMeta, + array_merge([ + ['label' => 'Seller', 'value' => $ecert['SELLER'] ?? ($orderMeta['otc_counter_party'] ?? '-')], + ['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'], + ['label' => 'Selling Time / Date', 'value' => $time . ' / ' . $date], + ], $this->commodityDetailRows($ecert, $orderMeta)) + ); + } + + protected function certificatePage(string $title, string $statement, array $ecert, array $orderMeta, array $rows): array + { + return [ + 'title' => $title, + 'statement' => $statement, + 'ecert_no' => $ecert['ECERTNO'] ?? ($orderMeta['e_cert_no'] ?? '-'), + 'currency' => $ecert['CURRENCY'] ?? 'MYR', + 'rows' => $rows, + 'show_murabaha' => false, + 'murabaha' => null, + ]; + } + + protected function commodityDetailRows(array $ecert, array $orderMeta): array + { + return [ + ['label' => 'Value Date', 'value' => isset($ecert['VALUEDATE']) ? strtoupper((string) $ecert['VALUEDATE']) : '-'], + ['label' => 'Product Code / Description', 'value' => $ecert['PNAME'] ?? ($orderMeta['product_code'] ?? '-')], + ['label' => 'Unit', 'value' => 'Tonnage'], + ['label' => 'Volume', 'value' => $ecert['PVOLUME'] ?? '-'], + ['label' => 'Currency', 'value' => $ecert['CURRENCY'] ?? 'MYR'], + ['label' => 'Price (MYR)', 'value' => $ecert['PRICE'] ?? '-'], + ['label' => 'Price (MYR Equivalent)', 'value' => $ecert['PRICE_MYR_EQUIVALENT'] ?? ($ecert['PRICE'] ?? '-')], + ['label' => 'Value', 'value' => $ecert['TOTALVALUE'] ?? '-'], + ]; + } + + protected function mapBsasLines(array $ecert): array + { + $lines = []; + foreach (($ecert['LINE'] ?? []) as $line) { + if (!is_array($line)) { + continue; + } + $lines[] = [ + 'supplier' => $line['SUPPLIER'] ?? '-', + 'volume' => $line['VOLUME'] ?? ($ecert['PVOLUME'] ?? '-'), + ]; + } + + if ($lines === []) { + $lines[] = [ + 'supplier' => '-', + 'volume' => $ecert['PVOLUME'] ?? '-', + ]; + } + + return $lines; + } + + protected function splitBsasDateTime(string $value): array + { + $value = trim($value); + if ($value === '') { + return ['-', '-']; + } + + $parts = explode(' ', $value, 2); + + return [ + $parts[0] ?? '-', + isset($parts[1]) ? strtoupper($parts[1]) : '-', + ]; + } } diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php index 2644dae9..e3dbee6c 100644 --- a/app/Http/Controllers/PembiayaanOnlineController.php +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -585,22 +585,11 @@ class PembiayaanOnlineController extends Controller $pembayaran_segera = 'https://pay.arrahn.com.my'; - $shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']); - if ($shorakaPrint) { - $komoditi = $shorakaPrint['komoditi']; - $product = $shorakaPrint['product']; - $kuantiti = $shorakaPrint['kuantiti']; - } else { - $komoditi = $this->api()->get($api_url . '/api/komuditi/check')->json(); - $product = $this->api() - ->get($api_url . '/api/komuditi/description', [ - 'kodproduk' => is_array($komoditi) ? ($komoditi['kodproduct'] ?? '') : '', - ]) - ->json(); - - $nilai = is_array($komoditi) ? (float) ($komoditi['nilai'] ?? 0) : 0; - $kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0; - } + $commodityPrint = app(\App\Services\CommodityCertificate::class) + ->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0); + $komoditi = $commodityPrint['komoditi']; + $product = $commodityPrint['product']; + $kuantiti = $commodityPrint['kuantiti']; if (($gadai_detail['tagbaru'] ?? 0) == 1) { $cawangan_info = $this->api() diff --git a/app/Services/CommodityCertificate.php b/app/Services/CommodityCertificate.php new file mode 100644 index 00000000..2753185c --- /dev/null +++ b/app/Services/CommodityCertificate.php @@ -0,0 +1,94 @@ +shorakaPrint($reference); + if ($shoraka) { + return $this->normalize($shoraka['komoditi'], $shoraka['product'], $shoraka['kuantiti']); + } + + $bsas = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(rtrim((string) config('api.url'), '/') . '/api/komuditi/bsas/order/norujukan/' . urlencode($reference)); + + if ($bsas->successful() && is_array($bsas->json())) { + $payload = $bsas->json(); + return $this->normalize( + $payload['komoditi'] ?? [], + $payload['product'] ?? [], + (float) ($payload['kuantiti'] ?? 0) + ); + } + + return $this->legacy($pinjaman); + } + + private function shorakaPrint(string $reference): ?array + { + try { + return app(ShorakaFinancingService::class)->printData($reference); + } catch (HttpException $exception) { + if ($exception->getStatusCode() === 409) { + throw $exception; + } + return null; + } catch (Throwable $exception) { + return null; + } + } + + private function legacy($pinjaman): array + { + $api = rtrim((string) config('api.url'), '/'); + $komoditi = Http::withOptions(['verify' => config('app.api_verify')]) + ->get($api . '/api/komuditi/check') + ->json(); + if (!is_array($komoditi)) { + $komoditi = []; + } + + $product = Http::withOptions(['verify' => config('app.api_verify')]) + ->get($api . '/api/komuditi/description', [ + 'kodproduk' => $komoditi['kodproduct'] ?? '', + ]) + ->json(); + + $nilai = (float) ($komoditi['nilai'] ?? 0); + $kuantiti = $nilai > 0 ? ((float) $pinjaman / $nilai) : 0; + + return $this->normalize($komoditi, is_array($product) ? $product : [], $kuantiti); + } + + private function normalize($komoditi, $product, $kuantiti): array + { + if (!is_array($komoditi)) { + $komoditi = []; + } + if (!is_array($product)) { + $product = []; + } + + $code = $product['product_code'] ?? $product['kodproduk'] ?? ($komoditi['kodproduct'] ?? ''); + $product['product_code'] = $product['product_code'] ?? $code; + $product['kodproduk'] = $product['kodproduk'] ?? $code; + $product['description'] = $product['description'] ?? ''; + $product['unit'] = $product['unit'] ?? 'TAN'; + + return [ + 'komoditi' => $komoditi, + 'product' => $product, + 'kuantiti' => $kuantiti, + ]; + } +} diff --git a/config/commodity.php b/config/commodity.php new file mode 100644 index 00000000..c9db472a --- /dev/null +++ b/config/commodity.php @@ -0,0 +1,10 @@ + filter_var(env('COMMODITY_SHORAKA_ENABLED', false), FILTER_VALIDATE_BOOLEAN), + 'bsas' => filter_var(env('COMMODITY_BSAS_ENABLED', false), FILTER_VALIDATE_BOOLEAN), +]; diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index e5d5b986..107bfcad 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -2,14 +2,14 @@ FROM php:7.4-fpm AS base WORKDIR /var/www/html -# PHP 7.4 uses Debian Bullseye; use its archived package repositories. -# Archived security metadata expires, but package signatures remain verified. -RUN printf '%s\n' \ - 'deb https://archive.debian.org/debian bullseye main' \ - 'deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main' \ - > /etc/apt/sources.list \ - && rm -rf /var/lib/apt/lists/* \ - && apt-get -o Acquire::Retries=3 update \ +# php:7.4-fpm is Debian 11 (bullseye); after LTS end, packages live on archive.debian.org +RUN sed -i \ + -e 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' \ + -e 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' \ + -e 's|http://deb.debian.org/debian-security|http://archive.debian.org/debian-security|g' \ + /etc/apt/sources.list \ + && printf 'Acquire::Check-Valid-Until "false";\n' > /etc/apt/apt.conf.d/99no-check-valid-until \ + && apt-get update \ && apt-get install -y --no-install-recommends \ git \ pkg-config \ diff --git a/public/img/svg/logo-arrahn-new.svg b/public/img/svg/logo-arrahn-new.svg new file mode 100644 index 00000000..149be9fa --- /dev/null +++ b/public/img/svg/logo-arrahn-new.svg @@ -0,0 +1,5 @@ + +Vectorized Arabic calligraphy logo + + + \ No newline at end of file diff --git a/public/img/logo-arrahn.svg b/public/img/svg/logo-arrahn-old.svg similarity index 100% rename from public/img/logo-arrahn.svg rename to public/img/svg/logo-arrahn-old.svg diff --git a/public/img/svg/logo-kopkb.svg b/public/img/svg/logo-kopkb.svg new file mode 100644 index 00000000..5c3ea85b --- /dev/null +++ b/public/img/svg/logo-kopkb.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/print/bsas/footer.blade.php b/resources/views/components/print/bsas/footer.blade.php new file mode 100644 index 00000000..a914fef2 --- /dev/null +++ b/resources/views/components/print/bsas/footer.blade.php @@ -0,0 +1,50 @@ +@props([ + 'note' => '', + 'page' => null, + 'total' => null, +]) + + + + diff --git a/resources/views/components/print/bsas/header.blade.php b/resources/views/components/print/bsas/header.blade.php new file mode 100644 index 00000000..724754a1 --- /dev/null +++ b/resources/views/components/print/bsas/header.blade.php @@ -0,0 +1,63 @@ +@props([ + 'company' => 'Koperasi Permodalan Kelantan Berhad', + 'leftLogo' => null, + 'rightLogo' => null, +]) + +@php + $leftLogo = $leftLogo ?? asset('img/svg/logo-kopkb.svg'); + $rightLogo = $rightLogo ?? asset('img/svg/logo-arrahn-new.svg'); +@endphp + + + +
merge(['class' => 'bsas-ecert-header']) }}> +
+
+ KPKB +
+
+ Ar-Rahn +
+
+
+

{{ $company }}

+
+
diff --git a/resources/views/components/print/bsas/watermark.blade.php b/resources/views/components/print/bsas/watermark.blade.php new file mode 100644 index 00000000..a991f359 --- /dev/null +++ b/resources/views/components/print/bsas/watermark.blade.php @@ -0,0 +1,35 @@ +@props([ + 'leftLogo' => null, + 'rightLogo' => null, +]) + +@php + $leftLogo = $leftLogo ?? asset('img/svg/logo-kopkb.svg'); + $rightLogo = $rightLogo ?? asset('img/logo-arrahn.jpeg'); +@endphp + + + +
merge(['class' => 'bsas-ecert-watermark']) }} aria-hidden="true"> + + +
diff --git a/resources/views/customers/v2/info.blade.php b/resources/views/customers/v2/info.blade.php index 4d1cb40d..f9ce43c8 100644 --- a/resources/views/customers/v2/info.blade.php +++ b/resources/views/customers/v2/info.blade.php @@ -716,8 +716,6 @@ var api_url = "{{ api_url_for_browser() }}"; var kodcaw = "{{ $cawangan_info['kodcaw'] }}"; var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}"; - var min_komoditi = "{{ $min_komoditi }}"; - console.log('test api_arrahnbid: ', api_arrahnbid); $('#daftar_gadai').on('click', async function(event) { event.preventDefault(); if ($(this).prop('disabled')) { @@ -756,42 +754,57 @@ event.preventDefault(); return false; } else { - $.ajax({ - method: 'get', - url: api_url + '/api/shoraka/readiness', - success: function(data) { - var check_komuditi = Object.keys(data).length; - if (!data.ready) { + var commodityShoraka = @json((bool) config('commodity.shoraka')); + var commodityBsas = @json((bool) config('commodity.bsas')); + var openGadaian = function () { + if (icbaru.length > 0) { + window.location.href = + "{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}"; + } else { + window.location.href = + "{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}"; + } + }; + + if (!commodityShoraka && !commodityBsas) { + Swal.fire({ + icon: 'error', + title: 'Komoditi tidak diaktifkan', + text: 'Tiada pembekal komoditi diaktifkan. Hubungi operasi.' + }); + $("#daftar_gadai").prop('disabled', false); + return false; + } + + if (commodityShoraka && !commodityBsas) { + $.ajax({ + method: 'get', + url: api_url + '/api/shoraka/readiness', + success: function (data) { + if (!data || !data.ready) { + Swal.fire({ + icon: 'error', + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' + }); + $("#daftar_gadai").prop('disabled', false); + return false; + } + openGadaian(); + }, + error: function () { Swal.fire({ icon: 'error', title: 'Shoraka Belum Tersedia', text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' }); $("#daftar_gadai").prop('disabled', false); - event.preventDefault(); - return false; - } else { - if (!data.ready) { - $("#daftar_gadai").prop('disabled', false); - Swal.fire({ - icon: 'error', - title: 'Shoraka Belum Tersedia', - text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' - }); - event.preventDefault(); - return false; - } else { - if (icbaru.length > 0) { - window.location.href = - "{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}"; - } else { - window.location.href = - "{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}"; - } - } } - } - }); + }); + return false; + } + + openGadaian(); } }); diff --git a/resources/views/gadaian/v2/index.blade.php b/resources/views/gadaian/v2/index.blade.php index a78188f7..5638610c 100644 --- a/resources/views/gadaian/v2/index.blade.php +++ b/resources/views/gadaian/v2/index.blade.php @@ -294,6 +294,35 @@ +
+ @php + $commodityProviders = array_values(array_filter([ + config('commodity.shoraka') ? ['id' => 'shoraka', 'label' => 'Shoraka'] : null, + config('commodity.bsas') ? ['id' => 'bsas', 'label' => 'BSAS'] : null, + ])); + @endphp +
+
Pembekal Komoditi
+
+ @if (count($commodityProviders) === 0) + Tiada pembekal komoditi diaktifkan. + @elseif (count($commodityProviders) === 1) + + {{ $commodityProviders[0]['label'] }} + @else + @foreach ($commodityProviders as $provider) +
+ + +
+ @endforeach +
Pilih pembekal sebelum tekan Terima.
+ @endif +
+
+
@@ -1561,6 +1590,183 @@ return true; } + function bsasProgressHtml() { + return '' + + '
' + + '

Sedang memproses pembelian komoditi...

' + + '
    ' + + '
  1. ' + + '○' + + '1. Menghantar order ke BSAS' + + '
    ' + + '
  2. ' + + '
  3. ' + + '○' + + '2. Menunggu keputusan (long poll)' + + '
    ' + + '
  4. ' + + '
  5. ' + + '○' + + '3. Mengambil e-Cert bid, OTC dan STB & simpan gadai' + + '
    ' + + '
  6. ' + + '
' + + '

Sila tunggu, proses ini boleh mengambil masa 10–30 saat.

' + + '
'; + } + + function setBsasStep(step, state, remark) { + var $el = $('#bsas-step-' + step); + if (!$el.length) { + return; + } + var icon = '○'; + var color = 'text-muted'; + if (state === 'active') { + icon = '●'; + color = 'text-primary font-weight-bold'; + } else if (state === 'done') { + icon = '✓'; + color = 'text-success'; + } else if (state === 'error') { + icon = '✕'; + color = 'text-danger'; + } + $el.attr('data-state', state) + .removeClass('text-muted text-primary text-success text-danger font-weight-bold') + .addClass(color); + $el.find('.bsas-step-icon').text(icon); + $el.find('.bsas-step-remark').text(remark || ''); + } + + function delayMs(ms) { + return new Promise(function (resolve) { + setTimeout(resolve, ms); + }); + } + + async function submitGadaiWithBsasProgress(func) { + $("#terima_gadai").prop('disabled', true); + + Swal.fire({ + title: 'Pembelian Komoditi BSAS', + html: bsasProgressHtml(), + allowOutsideClick: false, + allowEscapeKey: false, + showConfirmButton: false, + didOpen: function () { + Swal.showLoading(); + } + }); + + try { + setBsasStep(1, 'active', 'Menyediakan & menghantar order...'); + await delayMs(1200); + setBsasStep(1, 'done', 'Order dihantar.'); + + setBsasStep(2, 'active', 'Menunggu padanan BSAS...'); + await delayMs(800); + + var response = await $.ajax({ + url: $(func).attr('action'), + method: 'POST', + data: $(func).serialize(), + dataType: 'json', + headers: { + 'Accept': 'application/json', + 'X-Requested-With': 'XMLHttpRequest' + }, + timeout: 90000 + }); + + setBsasStep(2, 'done', 'Keputusan diterima.'); + setBsasStep(3, 'active', 'Mengambil e-Cert bid, OTC dan STB & menyimpan gadai...'); + await delayMs(1000); + setBsasStep(3, 'done', 'Selesai.'); + await delayMs(600); + + window.location.href = (response && response.redirect) + ? response.redirect + : "{{ route('customer_info') }}"; + } catch (err) { + var msg = 'Gagal pembelian komoditi BSAS. Sila cuba lagi.'; + if (err && err.responseJSON && err.responseJSON.message) { + msg = err.responseJSON.message; + } else if (err && err.statusText === 'timeout') { + msg = 'Proses BSAS tamat masa. Sila semak rekod BSAS / cuba semula.'; + } + + var activeStep = $('#bsas-progress-box li[data-state="active"]').attr('id'); + var stepNum = activeStep ? parseInt(activeStep.replace('bsas-step-', ''), 10) : 2; + setBsasStep(stepNum, 'error', msg); + + Swal.fire({ + icon: 'error', + title: 'Tidak Berjaya', + text: msg + }).then(function () { + $("#terima_gadai").prop('disabled', false); + }); + } + } + + function selectedCommodityProvider() { + var checked = $('input[name="commodity_provider"]:checked').val(); + if (checked) { + return checked; + } + var hidden = $('input[name="commodity_provider"][type="hidden"]').val(); + return hidden || ''; + } + + function submitGadaiByProvider(func) { + var provider = selectedCommodityProvider(); + if (!provider) { + $("#terima_gadai").prop('disabled', false); + Swal.fire({ + icon: 'error', + title: 'Pembekal Komoditi', + text: 'Sila pilih pembekal komoditi.' + }); + return false; + } + + if (provider === 'shoraka') { + $.ajax({ + method: 'get', + url: api_url + '/api/shoraka/readiness', + success: function (data) { + if (!data || !data.ready) { + $("#terima_gadai").prop('disabled', false); + Swal.fire({ + icon: 'error', + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' + }); + return false; + } + $(func).attr('onSubmit', 'return true;'); + if (typeof window.shorakaTrack === 'function') { + window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val())); + } + $(func).submit(); + }, + error: function () { + $("#terima_gadai").prop('disabled', false); + Swal.fire({ + icon: 'error', + title: 'Shoraka Belum Tersedia', + text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' + }); + } + }); + return true; + } + + submitGadaiWithBsasProgress(func); + return true; + } + async function verifyGadai(func){ $("#terima_gadai").prop('disabled', true); if (typeof event !== 'undefined' && event && event.preventDefault) { @@ -1652,32 +1858,6 @@ event.preventDefault(); return false; }else{ - $.ajax({ - method:'get', - url: api_url + '/api/shoraka/readiness', - success:function(data){ - var check_komuditi = Object.keys(data).length; - if (!data.ready){ - $("#terima_gadai").prop('disabled', false); - Swal.fire({ - icon: 'error', - title: 'Shoraka Belum Tersedia', - text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' - }); - event.preventDefault(); - return false; - }else{ - if (!data.ready){ - $("#terima_gadai").prop('disabled', false); - Swal.fire({ - icon: 'error', - title: 'Shoraka Belum Tersedia', - text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.' - }); - event.preventDefault(); - return false; - } - else{ if(pinjaman >= 10000 && pinjaman <= Number(pembiyaan_max_pelulus)){ //new added $.ajax({ @@ -1784,10 +1964,7 @@ title: 'Telah Disahkan', text: `Komen : ${data.komen}` }).then(function(){ - - $(func).attr("onSubmit","return true;"); - window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val())); - $(func).submit(); + submitGadaiByProvider(func); return true; }); @@ -1933,13 +2110,8 @@ icon: 'success', title: 'Telah Disahkan' }).then(function(){ - - - $(func).attr("onSubmit","return true;"); - window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val())); - $(func).submit(); + submitGadaiByProvider(func); return true; - }); } else if(data.kelulusan == 'batal') @@ -2000,9 +2172,7 @@ return false; } else { //insert code asal - window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val())); - $(func).attr("onSubmit","return true;"); - $(func).submit(); + submitGadaiByProvider(func); return true; //end insert code asal } @@ -2014,10 +2184,6 @@ } - } - } - } - }); } } diff --git a/resources/views/homepage/khazanah/bsas_komoditi.blade.php b/resources/views/homepage/khazanah/bsas_komoditi.blade.php new file mode 100644 index 00000000..86da062c --- /dev/null +++ b/resources/views/homepage/khazanah/bsas_komoditi.blade.php @@ -0,0 +1,168 @@ +@extends('layouts.app') + +@section('content') +
+
+
+
+ +
{{ __('Rekod Komoditi BSAS') }}
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+
+
+ +@endsection diff --git a/resources/views/homepage/khazanah/home.blade.php b/resources/views/homepage/khazanah/home.blade.php index 56b281e6..c2d90ebe 100644 --- a/resources/views/homepage/khazanah/home.blade.php +++ b/resources/views/homepage/khazanah/home.blade.php @@ -268,7 +268,7 @@ Harga Emas
- @if(in_array(Auth::user()->roles, ['pc', 'ppc'], true)) + @if(config('commodity.shoraka') && in_array(Auth::user()->roles, ['pc', 'ppc'], true))
Komoditi Shoraka
@@ -287,6 +287,12 @@ Surat Wakil + @if(config('commodity.bsas')) +
+ Komoditi BSAS +
+ @endif diff --git a/resources/views/homepage/operasi/home.blade.php b/resources/views/homepage/operasi/home.blade.php index 1ba11cff..ee02e50e 100644 --- a/resources/views/homepage/operasi/home.blade.php +++ b/resources/views/homepage/operasi/home.blade.php @@ -16,7 +16,9 @@
Kelulusan Anggota Kelulusan + @if(config('commodity.shoraka')) Komoditi Shoraka + @endif Pembiayaan Online Modal Online EOD Modal Online diff --git a/resources/views/homepage/teller/home.blade.php b/resources/views/homepage/teller/home.blade.php index ea240d1e..8b5dedef 100644 --- a/resources/views/homepage/teller/home.blade.php +++ b/resources/views/homepage/teller/home.blade.php @@ -59,7 +59,7 @@ PELANGGAN HARGA EMAS ALIRAN TUNAI - @if(in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller') + @if(config('commodity.shoraka') && (in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller')) KOMODITI SHORAKA @endif {{-- TETAPAN --}} diff --git a/resources/views/khazanah/v2/info.blade.php b/resources/views/khazanah/v2/info.blade.php index ffab81ba..4bc5fe21 100644 --- a/resources/views/khazanah/v2/info.blade.php +++ b/resources/views/khazanah/v2/info.blade.php @@ -25,6 +25,18 @@ + @if(!empty($customer_info['photo_base64'])) +
+
+ Gambar MyKad +
+
+ @endif +
@@ -154,7 +166,7 @@ {{-- Sudah Tebus --}} - - - - - - - - - - - -
BilNo RujukanTarikh Penyelesaian PembiayaanBaki PembiayaanMarginTempoh
+ {{-- Lelong --}} - - - - - - - - - - - - - - -
BilNo RujukanTarikh LelongTarikh TuntutPembiayaanHarga JualJum Tuntut/BayarKeuntunganCaj Lelong
+
@@ -504,21 +473,15 @@ - var tablegadai = $('#myTable').DataTable({ - "scrollX": true, - order: [[1,'desc']] + var tablegadai = initErahnDatatable('#myTable', { + order: [[1, 'desc']] }); - var xtawarruq = $('#xtawarruq').DataTable({ - "scrollX": true, - }); + var xtawarruq = initErahnDatatable('#xtawarruq'); - var tabledahtebus = $('#dahtebusTable').DataTable({ - }); + var tabledahtebus = initErahnDatatable('#dahtebusTable'); - var tablelelong = $('#lelongTable').DataTable({ - "scrollX": true - }); + var tablelelong = initErahnDatatable('#lelongTable'); var historybayarandatatable = $('#tablehistorybayaran').DataTable({ "ordering": false @@ -960,6 +923,9 @@ success: function(data){ $('#myTable').parents('div.dataTables_wrapper').first().show(); gadaiTable(data,"belum_tebus"); + if (typeof adjustErahnDatatables === 'function') { + adjustErahnDatatables(); + } } }); @@ -1493,6 +1459,9 @@ url: api_url + "/api/customers/"+noic+"/gadai/belum_tebus/"+kodcaw, success: function(data){ gadaiTable(data,"belum_tebus"); + if (typeof adjustErahnDatatables === 'function') { + adjustErahnDatatables(); + } } }); } @@ -1741,9 +1710,10 @@ }); document.addEventListener('DOMContentLoaded', function(){ - $('a[data-toggle="tab"]').on('shown.bs.tab', function(e){ - $($.fn.dataTable.tables(true)).DataTable() - .columns.adjust(); + $('a[data-toggle="tab"]').on('shown.bs.tab', function(){ + if (typeof adjustErahnDatatables === 'function') { + adjustErahnDatatables(); + } }); }); diff --git a/resources/views/operasi/komuditi/index.blade.php b/resources/views/operasi/komuditi/index.blade.php index a755c4d6..4e16334c 100644 --- a/resources/views/operasi/komuditi/index.blade.php +++ b/resources/views/operasi/komuditi/index.blade.php @@ -73,43 +73,81 @@
-
{{ __('Rekod Pembelian/Penjualan Komoditi') }}
+
{{ __('Rekod Komoditi') }}
-
-
-
- - -
+ +
+
+
+
+
+ + +
+
+
+ +
+
+
+
+
+ Loading... +
+ +
+
-
- -
-
-
-
-
- Loading... -
- - - - - - - - - - - - -
BilNo SijilTarikhMasaNilai KomoditiTransaksi OlehTransaksi
+
+
+
+
+ + +
+
+
+ +
+
+
+
+ + +
+
@@ -239,15 +277,44 @@