4 Commits

Author SHA1 Message Date
ismailmasseran 44b1d847bc Explore/ismail (#17)
Build Docker Image / build-frontend (push) Successful in 14s
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #17
Co-authored-by: ismailmasseran <ismail@koppkb.com>
Co-committed-by: ismailmasseran <ismail@koppkb.com>
2026-09-09 14:46:15 +08:00
ismailmasseran 0cb85c2375 Merge pull request 'resolve bugs laporan audit operasi' (#13) from fix/resolve-bugs-laporan-audit-operasi into main
Reviewed-on: #13
2026-09-02 11:58:46 +08:00
nurafrinaalimi16 2a0ffde016 resolve bugs laporan audit operasi 2026-09-02 11:55:28 +08:00
ismailmasseran 92991f0d10 Prod kopkb (#12)
Co-authored-by: dev_kopkb <xtrabiz.sb@gmail.com>
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Co-authored-by: root <root@erahn.click>
Reviewed-on: #12
2026-09-01 15:10:30 +08:00
14 changed files with 308 additions and 48 deletions
-27
View File
@@ -1,27 +0,0 @@
## 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)
@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class AdminFeatureController extends Controller
{
public function __construct()
{
$this->middleware(['auth', 'admin']);
}
protected function api()
{
return Http::withOptions(['verify' => config('app.api_verify')]);
}
public function index()
{
$api_url = config('api.url');
$metaRes = $this->api()->get($api_url . '/api/features');
$meta = $metaRes->successful() ? $metaRes->json() : [];
$keys = is_array($meta) && !empty($meta['keys']) ? $meta['keys'] : ['pembiayaan_online'];
$labels = is_array($meta) && !empty($meta['labels']) ? $meta['labels'] : [
'pembiayaan_online' => 'Pembiayaan Online',
];
$listRes = $this->api()->get($api_url . '/api/features/cawangan');
$cawangan = ($listRes->successful() && is_array($listRes->json())) ? $listRes->json() : [];
if (!$listRes->successful()) {
session()->now('error', 'Gagal muat senarai ciri cawangan.');
}
return view('admin.features', compact('api_url', 'keys', 'labels', 'cawangan'));
}
public function update(Request $request)
{
$kodcaw = trim((string) $request->input('kodcaw', ''));
$feature = trim((string) $request->input('feature', ''));
$enabled = (int) $request->input('enabled') ? 1 : 0;
if ($kodcaw === '' || $feature === '') {
return redirect()->route('admin.features')->with('error', 'Kod cawangan dan ciri diperlukan.');
}
$res = $this->api()
->put(config('api.url') . '/api/features/' . $kodcaw . '/' . $feature, [
'enabled' => $enabled,
]);
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal kemaskini ciri cawangan.';
return redirect()->route('admin.features')->with('error', $msg);
}
$label = $enabled ? 'diaktifkan' : 'dinyahaktifkan';
return redirect()
->route('admin.features')
->with('message', $feature . ' ' . $label . ' untuk ' . $kodcaw . '.');
}
}
@@ -289,6 +289,29 @@ class CustomersController extends Controller
return response(["no-data"], 200);
}
/**
* Store MyKad photo for an existing customer when gambar_mykad is still empty.
*/
public function storeMykadPhotoIfMissing(Request $request)
{
$icno = preg_replace('/[^0-9A-Za-z]/', '', (string) $request->input('icno'));
if ($icno === '') {
return response()->json(['error' => 'IC number is required.', 'stored' => false], 422);
}
if (!$request->filled('photo_base64')) {
return response()->json(['error' => 'Photo is required.', 'stored' => false], 422);
}
$response = Http::withOptions(['verify' => config('app.api_verify')])
->post(config('api.url') . '/api/customers/' . $icno . '/mykad-photo', [
'photo_base64' => $request->input('photo_base64'),
'photo_mime' => $request->input('photo_mime') ?: 'image/jpeg',
]);
return response()->json($response->json() ?? ['stored' => false], $response->status());
}
/**
* Show the form for creating a new resource.
*
@@ -32,6 +32,16 @@ class GadaianController extends Controller
$this->AliranTunaiController = new AliranTunaiController();
}
protected function isPembiayaanOnlineEnabled($kodcaw)
{
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/cawangan/' . $kodcaw)
->json();
$features = is_array($cawangan_info) ? ($cawangan_info['features'] ?? []) : [];
return !empty($features['pembiayaan_online']);
}
public function index(Request $request)
{
@@ -219,6 +229,10 @@ class GadaianController extends Controller
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -24,6 +24,30 @@ class PembiayaanOnlineController extends Controller
return Auth::user()->cawangan;
}
protected function cawanganInfo()
{
return $this->api()
->get(config('api.url') . '/api/cawangan/' . $this->kodcaw())
->json();
}
protected function isPembiayaanOnlineEnabled($cawangan_info = null)
{
if (!is_array($cawangan_info)) {
$cawangan_info = $this->cawanganInfo();
}
$features = $cawangan_info['features'] ?? [];
return !empty($features['pembiayaan_online']);
}
protected function abortUnlessPembiayaanOnline($cawangan_info = null)
{
if (!$this->isPembiayaanOnlineEnabled($cawangan_info)) {
abort(403, 'Pembiayaan online tidak diaktifkan untuk cawangan ini.');
}
}
/**
* PC senarai queue + batch history.
*/
@@ -33,9 +57,8 @@ class PembiayaanOnlineController extends Controller
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)
->json();
$cawangan_info = $this->cawanganInfo();
$this->abortUnlessPembiayaanOnline($cawangan_info);
$queue = $this->api()
->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/queue')
@@ -79,6 +102,7 @@ class PembiayaanOnlineController extends Controller
*/
public function updateLine(Request $request, $id)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
@@ -109,6 +133,7 @@ class PembiayaanOnlineController extends Controller
*/
public function submitBatch(Request $request)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$auth_user = Auth::user();
@@ -150,9 +175,8 @@ class PembiayaanOnlineController extends Controller
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)
->json();
$cawangan_info = $this->cawanganInfo();
$this->abortUnlessPembiayaanOnline($cawangan_info);
$detail = $this->api()
->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches/' . $batchId)
@@ -181,9 +205,8 @@ class PembiayaanOnlineController extends Controller
$api_url = config('api.url');
$blastFilter = $request->input('blast_sent', '0');
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)
->json();
$cawangan_info = $this->cawanganInfo();
$this->abortUnlessPembiayaanOnline($cawangan_info);
$query = [];
if ($blastFilter !== 'all' && $blastFilter !== '') {
@@ -219,6 +242,7 @@ class PembiayaanOnlineController extends Controller
*/
public function viewBukti($id)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$res = $this->api()
->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/bukti');
@@ -258,6 +282,7 @@ class PembiayaanOnlineController extends Controller
*/
public function markBlast(Request $request)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$ids = $request->input('ids', []);
if (!is_array($ids)) {
@@ -572,9 +597,8 @@ class PembiayaanOnlineController extends Controller
$api_url = config('api.url');
$status = $request->input('status', '');
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)
->json();
$cawangan_info = $this->cawanganInfo();
$this->abortUnlessPembiayaanOnline($cawangan_info);
$modal = $this->api()
->get($api_url . '/api/modal-online/' . $kodcaw)
@@ -624,6 +648,7 @@ class PembiayaanOnlineController extends Controller
*/
public function topupRequest(Request $request)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$amaun = (float) $request->input('amaun', 0);
@@ -742,6 +767,7 @@ class PembiayaanOnlineController extends Controller
*/
public function closeModal(Request $request)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$res = $this->api()
+55 -1
View File
@@ -234,15 +234,69 @@ async function handleMyKadReader() {
}
}
function customerNeedsMykadPhoto(customer) {
if (!customer || typeof customer !== 'object') {
return false;
}
return !customer.gambar_mykad && !customer.photo_base64;
}
async function storeMykadPhotoForExistingCustomerIfMissing(customer, readerData) {
if (!customerNeedsMykadPhoto(customer)) {
return;
}
if (!readerData || !readerData.has_photo || !readerData.photo_base64) {
return;
}
if (typeof store_mykad_photo_endpoint === 'undefined' || !store_mykad_photo_endpoint) {
return;
}
const csrfToken = document.querySelector('input[name="_token"]');
const nric = getMyKadNumberWithoutHyphens(
customer.kpbaru || customer.kplama || readerData.nric || ''
);
if (!nric) {
return;
}
try {
await fetch(store_mykad_photo_endpoint, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken ? csrfToken.value : '',
},
credentials: 'same-origin',
body: JSON.stringify({
icno: nric,
photo_base64: readerData.photo_base64,
photo_mime: readerData.photo_mime || 'image/jpeg',
}),
});
} catch (error) {
// Do not block customer search if photo backfill fails.
console.warn('Failed to store MyKad photo for existing customer', error);
}
}
function handleMyKadData(data) {
return checkIfMyKadExists(data.nric)
.then(response => {
.then(async response => {
let isResponseArray = Array.isArray(response);
let resolved_response = isResponseArray ? response.pop() : response;
if (!resolved_response || resolved_response === 'no-data') {
populateDaftarPelangganModal(data);
} else {
await storeMykadPhotoForExistingCustomerIfMissing(resolved_response, data);
let input_search_noic = document.querySelector("form[name='frmsearch'] input[name='noic']");
let form_search_noic = document.querySelector("form[name='frmsearch']");
+88
View File
@@ -0,0 +1,88 @@
@extends('layouts.app_operasi')
@section('content')
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-md-10">
<nav aria-label="breadcrumb">
<ol class="breadcrumb pb-3">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Ciri Cawangan</li>
</ol>
</nav>
@if(session()->has('message'))
<div class="alert alert-success">{{ session('message') }}</div>
@endif
@if(session()->has('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Ciri Cawangan</h5>
<span class="text-muted">{{ count($cawangan) }} cawangan</span>
</div>
<div class="card-body table-responsive">
<table id="featureTable" class="table table-striped table-hover table-sm">
<thead class="thead-dark">
<tr>
<th>Kod</th>
<th>Cawangan</th>
@foreach($keys as $key)
<th class="text-center">{{ $labels[$key] ?? $key }}</th>
@endforeach
</tr>
</thead>
<tbody>
@forelse($cawangan as $row)
<tr>
<td>{{ $row['kodcaw'] ?? '-' }}</td>
<td>{{ $row['details_caw'] ?? '-' }}</td>
@foreach($keys as $key)
@php
$on = !empty(($row['features'] ?? [])[$key]);
@endphp
<td class="text-center">
<form method="post" action="{{ route('admin.features.update') }}" class="mb-0 d-inline"
@if($on) onsubmit="return confirm('Nyahaktifkan {{ $labels[$key] ?? $key }} untuk {{ $row['kodcaw'] }}?');" @endif>
@csrf
<input type="hidden" name="kodcaw" value="{{ $row['kodcaw'] }}">
<input type="hidden" name="feature" value="{{ $key }}">
<input type="hidden" name="enabled" value="{{ $on ? 0 : 1 }}">
<button type="submit" class="btn btn-sm {{ $on ? 'btn-success' : 'btn-secondary' }}">
{{ $on ? 'Aktif' : 'Tidak aktif' }}
</button>
</form>
</td>
@endforeach
</tr>
@empty
<tr>
<td colspan="{{ 2 + count($keys) }}" class="text-center text-muted">Tiada cawangan.</td>
</tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<script>
$(function () {
if ($.fn.DataTable && $('#featureTable tbody tr').length && !$('#featureTable tbody td[colspan]').length) {
$('#featureTable').DataTable({
pageLength: 25,
order: [[0, 'asc']],
language: {
search: 'Cari:',
lengthMenu: 'Papar _MENU_',
info: '_START__END_ daripada _TOTAL_',
paginate: { previous: 'Sebelum', next: 'Seterusnya' }
}
});
}
});
</script>
@endsection
@@ -13,9 +13,9 @@
<style>
.print-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
grid-template-columns: minmax(0, 1.75fr) auto minmax(0, 1fr);
align-items: start;
column-gap: 12px;
column-gap: 10px;
border-bottom: 2px solid #111;
padding-bottom: 8px;
font-family: Arial, Helvetica, sans-serif;
@@ -30,9 +30,12 @@
line-height: 1.35;
}
.print-header .org .name {
font-size: 11px;
font-size: 10px;
font-weight: bold;
}
.print-header .org .name .reg {
white-space: nowrap;
}
.print-header .logos {
display: flex;
align-items: center;
@@ -70,8 +73,8 @@
<div {{ $attributes->merge(['class' => 'print-header']) }}>
<div class="org">
<p class="name">KOPERASI PERMODALAN KELANTAN BERHAD (D-5-0228)</p>
<p>Ar-Rahn {{ $cawangan }}</p>
<p class="name">KOPERASI PERMODALAN KELANTAN BERHAD <span class="reg">(D-5-0228)</span></p>
<p>{{ $cawangan }}</p>
<p>{{ $alamat1 }}</p>
<p>{{ $alamat2 }}</p>
<p>Tel: {{ $notel }}</p>
+2 -1
View File
@@ -1279,9 +1279,10 @@
// mycard-read endpoint initialization
let mykad_reader_endpoint = "{{ config('mykad-reader.endpoint') }}";
let wakil_caricustomer_endpoint = "{{ route('wakil.caricustomer') }}";
let store_mykad_photo_endpoint = "{{ route('customer.storeMykadPhoto') }}";
let mykad_reader_driver_endpoint = "{{ route('mykadreader.driver') }}";
</script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=4"></script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=5"></script>
<script src="{{ asset('js/postcode-concerns.js') }}"></script>
@endpush
+2 -1
View File
@@ -1417,9 +1417,10 @@ document.getElementsByName('form-store')[0].addEventListener('keypress', functio
// mycard-read endpoint initialization
let mykad_reader_endpoint = "{{ config('mykad-reader.endpoint') }}";
let wakil_caricustomer_endpoint = "{{ route('wakil.caricustomer') }}";
let store_mykad_photo_endpoint = "{{ route('customer.storeMykadPhoto') }}";
let mykad_reader_driver_endpoint = "{{ route('mykadreader.driver') }}";
</script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=4"></script>
<script src="{{ asset('js/mykad-reader-customer.js') }}?v=5"></script>
<script src="{{ asset('js/postcode-concerns.js') }}"></script>
@endpush
+3 -1
View File
@@ -28,6 +28,7 @@
<div class="form-group row col-12">
<input type="hidden" name="kodcaw" id="kodcaw" value="{{$cawangan_info['kodcaw']}}">
<input type="hidden" id="pembiayaan_online_enabled" value="{{ !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']) ? '1' : '0' }}">
<input type="hidden" name="nokp" id="nokp" value="{{ $customer_info['kpbaru'] }}">
<input type="hidden" name="tag_anggota" id="tag_anggota" value="{{ $customer_info['tag_anggota'] ?? 0 }}">
@@ -1266,7 +1267,8 @@
}
function toggleCaraTerima(pinjaman) {
if (pinjaman >= 5000 && pinjaman <= 50000) {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
if (pembiayaanOnlineEnabled && pinjaman >= 5000 && pinjaman <= 50000) {
$('#cara_terima_section').show();
toggleBankOnlineSection();
} else {
@@ -16,6 +16,7 @@
<div class="card-body text-center">
<a href="{{ route('admin.daftar.kakitangan') }}" class="btn btn-info btn-lg">Kakitangan</a>
<a href="{{ route('admin.pelanggan') }}" class="btn btn-info btn-lg">Pelanggan</a>
<a href="{{ route('admin.features') }}" class="btn btn-info btn-lg">Ciri Cawangan</a>
<!-- <a href="{{ route('admin.poskod') }}" class="btn btn-info btn-lg">Poskod</a> -->
</div>
</div>
@@ -262,10 +262,12 @@
<a href="{{ url('pendahuluanserahan') }}"
class="btn btn-info btn-lg btn-block mb-3">Pendahuluan - Serahan</a>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']))
<div class="row">
<a href="{{ route('pembiayaan.online.pc') }}"
class="btn btn-info btn-lg btn-block mb-3">Pembiayaan Online</a>
</div>
@endif
<div class="row">
<a href="{{ route('khazanah.indexwakil') }}"
class="btn btn-info btn-lg btn-block mb-3">Surat Wakil</a>
+3
View File
@@ -133,6 +133,7 @@ Route::get('/gadai/batal',['as'=>'gadai.batal','uses'=>'GadaianController@batal'
Route::get('/customer/otp',['as'=>'customer.otp','uses'=>'CustomersController@getOTP'])->middleware('auth','teller');
Route::get('/getwakil',['as'=>'wakil.search','uses'=>'PenebusanController@getWakil'])->middleware('auth','teller');
Route::get('/cari',['as'=>'wakil.caricustomer','uses'=>'CustomersController@cari'])->middleware('auth','teller');
Route::post('/customer/mykad-photo',['as'=>'customer.storeMykadPhoto','uses'=>'CustomersController@storeMykadPhotoIfMissing'])->middleware('auth','teller');
Route::post('/createwakil',['as'=>'wakil.create','uses'=>'PenebusanController@createWakil'])->middleware('auth','teller');
Route::get('/firstwakil',['as'=>'wakil.first','uses'=>'PenebusanController@getfirstWakil'])->middleware('auth','teller');
Route::get('/history',['as'=>'gadai.history','uses'=>'GadaianController@history'])->middleware('auth','teller');
@@ -468,6 +469,8 @@ Route::get('admin/pelanggan',['as'=>'admin.pelanggan','uses'=>'KhazanahControlle
Route::get('admin/customer_info',['as'=>'admin.customer_info','uses'=>'KhazanahController@infoAdmin'])->middleware('auth','admin');
Route::get('admin/searchpelanggan',['as'=>'admin.searchpelanggan','uses'=>'KhazanahController@searchAdmin'])->middleware('auth','admin');
Route::get('admin/poskod',['as'=>'admin.poskod','uses'=>'HomeController@poskod']);
Route::get('admin/features',['as'=>'admin.features','uses'=>'AdminFeatureController@index'])->middleware('auth','admin');
Route::post('admin/features',['as'=>'admin.features.update','uses'=>'AdminFeatureController@update'])->middleware('auth','admin');
Route::get('pembeliannotifykomuditi',['as'=>'beli.notifyKomoditi','uses'=>'OperasiController@notifyKomoditi'])->middleware('auth','operasi');