feat(shoraka): defer gadai persistence until commodity confirmation and add simulation support

This commit is contained in:
2026-10-19 13:07:10 +08:00
parent 63d8c23785
commit 011fbfe8b2
18 changed files with 437 additions and 59 deletions
+6 -5
View File
@@ -5,6 +5,9 @@
@section('content')
@include('modal.editmarhun')
<div class="container">
@if(session('gadai_edit_error'))
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
@endif
<div class="row justify-content-center">
<div class="col-md-12 col-lg-12">
<h3 class="text-center text-uppercase">Pembiayaan</h3>
@@ -946,6 +949,7 @@
text: `Komen : ${data.komen}`
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
$(func).submit();
return true;
});
@@ -1058,6 +1062,7 @@
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
$(func).submit();
return true;
@@ -1092,11 +1097,7 @@
return false;
}
else if(pinjaman < 10000 && pinjaman <= Number(pembiayaan_max_teller)){
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
});
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
return true;
}
}
+3 -3
View File
@@ -1162,9 +1162,9 @@
return false;
} else {
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
icon: 'info',
title: 'Sedang diproses',
text: 'Sedang menghantar pembiayaan untuk pengesahan Shoraka.'
});
$(func).attr("onSubmit","return true;");
+6 -5
View File
@@ -5,6 +5,9 @@
@section('content')
@include('modal.editmarhun')
<div class="container">
@if(session('gadai_edit_error'))
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
@endif
<div class="row justify-content-center">
<div class="col-md-12 col-lg-12">
<h3 class="text-center text-uppercase">Pembiayaan</h3>
@@ -1612,6 +1615,7 @@
text: `Komen : ${data.komen}`
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
$(func).submit();
return true;
});
@@ -1724,6 +1728,7 @@
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
$(func).submit();
return true;
@@ -1758,11 +1763,7 @@
return false;
}
else if(pinjaman < 10000 && pinjaman <= Number(pembiayaan_max_teller)){
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
});
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
return true;
}
}
+3 -3
View File
@@ -2000,9 +2000,9 @@
} else {
//insert code asal
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
icon: 'info',
title: 'Sedang diproses',
text: 'Sedang menghantar pembiayaan untuk pengesahan Shoraka.'
});
$(func).attr("onSubmit","return true;");
@@ -268,6 +268,11 @@
<a href="{{ route('khazanah.harga_emas') }}"
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
</div>
@if(in_array(Auth::user()->roles, ['pc', 'ppc'], true))
<div class="col-4 px-2">
<a href="{{ route('pc.shoraka.index') }}" class="btn btn-info btn-lg btn-block mb-3">Komoditi Shoraka</a>
</div>
@endif
@if(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']))
<div class="col-4 px-2">
<a href="{{ route('pembiayaan.online.pc') }}"
@@ -16,6 +16,7 @@
<div class="card-body text-center">
<a href="{{ route('operasi.kelulusananggota') }}" class="btn btn-info btn-lg d-inline-block m-1">Kelulusan Anggota</a>
<a href="{{ route('operasi.kelulusan') }}" class="btn btn-info btn-lg d-inline-block m-1">Kelulusan</a>
<a href="{{ route('operasi.shoraka.index') }}" class="btn btn-info btn-lg d-inline-block m-1">Komoditi Shoraka</a>
<a href="{{ route('pembiayaan.online.operasi') }}" class="btn btn-info btn-lg d-inline-block m-1">Pembiayaan Online</a>
<a href="{{ route('pembiayaan.online.operasi.topup') }}" class="btn btn-info btn-lg d-inline-block m-1">Modal Online</a>
<a href="{{ route('pembiayaan.online.operasi.eod') }}" class="btn btn-info btn-lg d-inline-block m-1">EOD Modal Online</a>
@@ -60,7 +60,7 @@
<a class="btn btn-info btn-lg" href="{{ url('/harga_emas') }}">HARGA EMAS</a>
<a class="btn btn-info btn-lg" href="{{ url('/aliran_tunai') }}">ALIRAN TUNAI</a>
@if(in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller')
<a class="btn btn-info btn-lg" href="{{ route('shoraka.index') }}">PEMBIAYAAN SHORAKA</a>
<a class="btn btn-info btn-lg" href="{{ route('shoraka.index') }}">KOMODITI SHORAKA</a>
@endif
{{-- <a class="btn btn-info btn-lg" href="#">TETAPAN</a> --}}
</div>
+2 -2
View File
@@ -615,7 +615,7 @@
<td>{{ $komoditi['no_sijil'] }}</td>
<td class="text-center">4</td>
<td>Kuantiti (Unit)</td>
<td>{{ number_format($kuantiti, 2) }} TAN</td>
<td>{{ $kuantiti }} TONNES</td>
</tr>
<tr>
<td class="text-center">2</td>
@@ -1849,7 +1849,7 @@
<td>{{ $komoditi['no_sijil'] }}</td>
<td class="text-center">4</td>
<td>Kuantiti</td>
<td>{{ number_format($kuantiti, 2) }}</td>
<td>{{ $kuantiti }}</td>
</tr>
<tr>
<td class="text-center">2</td>
+2 -2
View File
@@ -20,13 +20,13 @@
<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">Pembiayaan Shoraka</li>
<li class="breadcrumb-item active" aria-current="page">Komoditi Shoraka</li>
</ol>
</nav>
<div class="card shoraka-list-card">
<div class="card-header bg-secondary d-flex flex-wrap align-items-center justify-content-between">
<h5 class="mb-0 py-2">Pembiayaan Shoraka</h5>
<h5 class="mb-0 py-2">Komoditi Shoraka</h5>
<a class="btn btn-info btn-sm my-1" href="{{ route('home') }}">
<i class="fas fa-arrow-left mr-1" aria-hidden="true"></i> Kembali ke Paparan Utama
</a>
@@ -1,3 +1,50 @@
<div id="shoraka-modal" hidden style="position:fixed;inset:0;z-index:20000;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;padding:16px">
<div role="dialog" aria-modal="true" aria-labelledby="shoraka-modal-title" tabindex="-1" style="background:#fff;border-radius:5px;padding:24px;max-width:460px;width:100%;text-align:center;box-shadow:0 8px 30px #0003">
<h4 id="shoraka-modal-title">Pembelian Komoditi Shoraka</h4>
<p id="shoraka-modal-message" role="status" aria-live="polite">Sedang memproses pembelian komoditi...</p>
<ol style="text-align:left;padding-left:28px">
<li data-shoraka-step="1">Menghantar order ke Shoraka</li>
<li data-shoraka-step="2">Menunggu pengesahan Shoraka</li>
<li data-shoraka-step="3">Menyelesaikan pembiayaan dan menyimpan gadaian</li>
</ol>
<p class="text-muted small">Sila tunggu sehingga proses selesai. Jangan hantar transaksi semula.</p>
<div id="shoraka-modal-spinner" class="spinner-border text-primary" role="status"><span class="sr-only">Sedang memproses</span></div>
<button id="shoraka-modal-close" type="button" class="btn btn-secondary" hidden>Tutup</button>
</div>
</div>
<script>
window.shorakaProgress = (function () {
var modal = document.getElementById('shoraka-modal');
var close = document.getElementById('shoraka-modal-close');
var previousFocus;
close.addEventListener('click', function () {
modal.hidden = true;
modal.style.display = 'none';
if (previousFocus) { previousFocus.focus(); }
});
modal.addEventListener('keydown', function (event) {
if (event.key === 'Tab') {
event.preventDefault();
(close.hidden ? modal.querySelector('[role="dialog"]') : close).focus();
}
});
return function (step, message, stopped) {
if (modal.hidden) { previousFocus = document.activeElement; }
modal.hidden = false;
modal.style.display = 'flex';
document.getElementById('shoraka-modal-message').textContent = message || 'Sedang memproses pembelian komoditi...';
modal.querySelectorAll('[data-shoraka-step]').forEach(function (item) {
var number = Number(item.getAttribute('data-shoraka-step'));
item.className = number < step ? 'text-success' : (number === step ? 'text-primary font-weight-bold' : 'text-muted');
if (number === step) { item.setAttribute('aria-current', 'step'); }
else { item.removeAttribute('aria-current'); }
});
document.getElementById('shoraka-modal-spinner').style.display = stopped ? 'none' : 'inline-block';
close.hidden = !stopped;
(stopped ? close : modal.querySelector('[role="dialog"]')).focus();
};
}());
</script>
<script>
// Existing refinancing AJAX handlers must not print receipts or announce payment
// success when the server has deferred the financial completion.
+97
View File
@@ -0,0 +1,97 @@
@extends($isOperasi ? 'layouts.app_operasi' : 'layouts.app')
@section('content')
@php
$listRoute = $isOperasi ? 'operasi.shoraka.index' : 'pc.shoraka.index';
$statusLabels = ['WAITING' => 'Menunggu Shoraka / penyelesaian', 'PROCESSING' => 'Sedang diproses', 'REVIEW' => 'Perlu semakan operasi', 'DONE' => 'Selesai'];
$statusColors = ['WAITING' => 'warning', 'PROCESSING' => 'info', 'REVIEW' => 'danger', 'DONE' => 'success'];
@endphp
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Komoditi Shoraka — {{ $isOperasi ? 'Operasi' : strtoupper(Auth::user()->roles) }}</li>
</ol>
</nav>
<div class="card">
<div class="card-header d-flex flex-wrap justify-content-between align-items-center">
<h5 class="mb-0 py-2">Komoditi Shoraka — {{ $isOperasi ? 'Semua Cawangan' : Auth::user()->cawangan }}</h5>
<a href="{{ route('home') }}" class="btn btn-info btn-sm">Kembali ke Paparan Utama</a>
</div>
<div class="card-body">
<p class="text-muted">Paparan pemantauan rekod pembiayaan {{ $isOperasi ? 'semua cawangan' : 'cawangan sendiri' }}. Status di bawah ialah status penyelesaian dalam sistem kaunter, bukan status langsung daripada Shoraka.</p>
@if($errors->any())
<div class="alert alert-danger" role="alert">{{ $errors->first() }}</div>
@endif
<form method="get" action="{{ route($listRoute) }}" class="mb-4">
<div class="form-row align-items-end">
@if($isOperasi)
<div class="form-group col-md-3">
<label for="shoraka-branch">Cawangan</label>
<select id="shoraka-branch" name="kodcaw" class="form-control">
<option value="">{{ $isOperasi ? 'Semua cawangan' : Auth::user()->cawangan }}</option>
@foreach($branches as $branch)
<option value="{{ $branch }}" {{ ($filters['kodcaw'] ?? '') === $branch ? 'selected' : '' }}>{{ $branch }}</option>
@endforeach
</select>
</div>
@endif
<div class="form-group col-md-3">
<label for="shoraka-reference">No. rujukan penuh</label>
<input id="shoraka-reference" name="norujukan" class="form-control" maxlength="100" value="{{ $filters['norujukan'] ?? '' }}">
</div>
<div class="form-group col-md-3">
<label for="shoraka-state">Penyelesaian pembiayaan</label>
<select id="shoraka-state" name="state" class="form-control">
<option value="">Semua status</option>
@foreach($statusLabels as $state => $label)
<option value="{{ $state }}" {{ ($filters['state'] ?? '') === $state ? 'selected' : '' }}>{{ $label }}</option>
@endforeach
</select>
</div>
<div class="form-group col-md-3">
<label for="shoraka-date-from">Tarikh mula didaftarkan</label>
<input type="date" id="shoraka-date-from" name="date_from" class="form-control" value="{{ $filters['date_from'] ?? '' }}">
</div>
<div class="form-group col-md-3">
<label for="shoraka-date-to">Tarikh akhir didaftarkan</label>
<input type="date" id="shoraka-date-to" name="date_to" class="form-control" value="{{ $filters['date_to'] ?? '' }}">
</div>
<div class="form-group col-md-3">
<button type="submit" class="btn btn-primary">Tapis</button>
<a href="{{ route($listRoute) }}" class="btn btn-secondary">Reset</a>
</div>
</div>
</form>
<p>{{ $records->total() }} rekod</p>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover">
<thead class="thead-dark"><tr>
<th scope="col">Cawangan</th>
<th scope="col">No. rujukan</th>
<th scope="col">Teller</th>
<th scope="col">Penyelesaian pembiayaan</th>
<th scope="col">Didaftarkan</th>
<th scope="col">Dikemas kini</th>
</tr></thead>
<tbody>
@forelse($records as $record)
<tr>
<td>{{ $record->kodcaw }}</td>
<td class="text-nowrap">{{ $record->norujukan }}</td>
<td>{{ $record->teller }}</td>
<td><span class="badge badge-{{ $statusColors[$record->state] ?? 'secondary' }} p-2">{{ $statusLabels[$record->state] ?? $record->state }}</span></td>
<td class="text-nowrap">{{ $record->created_at ? $record->created_at->format('d/m/Y H:i') : '—' }}</td>
<td class="text-nowrap">{{ $record->updated_at ? $record->updated_at->format('d/m/Y H:i') : '—' }}</td>
</tr>
@empty
<tr><td colspan="6" class="text-center text-muted py-4">Tiada transaksi untuk tapisan ini.</td></tr>
@endforelse
</tbody>
</table>
</div>
{{ $records->links() }}
</div>
</div>
</div>
@endsection
+74 -1
View File
@@ -7,7 +7,7 @@
<nav aria-label="breadcrumb">
<ol class="breadcrumb pb-3">
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
<li class="breadcrumb-item"><a href="{{ route('shoraka.index') }}">Pembiayaan Shoraka</a></li>
<li class="breadcrumb-item"><a href="{{ route('shoraka.index') }}">Komoditi Shoraka</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ $norujukan }}</li>
</ol>
</nav>
@@ -20,6 +20,78 @@
</a>
</div>
<div class="card-body">
@if(!empty($data['simulation']))
<div class="alert alert-warning">SIMULASI LOCAL — tiada transaksi sebenar dihantar ke Shoraka.</div>
@endif
@if($record && !empty($record->context['temporary_gadai']) && $record->teller === Auth::user()->name && $record->state === 'WAITING')
<div id="shoraka-progress" class="alert alert-info" role="status">Sedang memproses pembiayaan dan menyemak komoditi Shoraka secara automatik. Sila tunggu.</div>
<script>
(function () {
var started = Date.now();
var failures = 0;
var completing = false;
window.shorakaProgress(2, 'Order telah dihantar. Sedang menyemak pengesahan Shoraka...');
var panel = document.getElementById('shoraka-progress');
function stop(message) {
panel.className = 'alert alert-warning';
panel.textContent = message;
window.shorakaProgress(completing ? 3 : 2, message, true);
}
function poll() {
if (Date.now() - started > 300000) {
stop('Masih menunggu pengesahan Shoraka. Jangan hantar transaksi semula. Buka semula halaman ini untuk menyambung semakan.');
return;
}
fetch(@json(route('shoraka.poll', $norujukan)), {
method: 'POST', credentials: 'same-origin',
headers: {'X-CSRF-TOKEN': @json(csrf_token()), 'Accept': 'application/json', 'Content-Type': 'application/json'},
body: JSON.stringify({progress: completing ? 'complete' : 'check'})
}).then(function (response) {
if (!response.ok) { throw new Error('Status unavailable'); }
return response.json();
}).then(function (data) {
failures = 0;
if (data.state === 'ready') {
completing = true;
window.shorakaProgress(3, 'Shoraka telah disahkan. Sedang menyelesaikan pembiayaan dan menyimpan gadaian...');
window.setTimeout(poll, 0);
return;
}
if (data.state === 'done') {
window.shorakaProgress(4, 'Pembiayaan berjaya diselesaikan.', true);
panel.className = 'alert alert-success';
panel.textContent = 'Pembiayaan berjaya diselesaikan.';
if (window.Swal && typeof window.Swal.fire === 'function') {
document.getElementById('shoraka-modal-close').click();
window.Swal.fire({
icon: 'success',
title: 'Pembiayaan berjaya',
text: 'Transaksi komoditi Shoraka dan proses pembiayaan telah selesai.',
confirmButtonText: 'OK',
allowOutsideClick: false,
allowEscapeKey: false
}).then(function () { window.location.assign(data.redirect); });
} else {
window.alert('Pembiayaan berjaya diselesaikan.');
window.location.assign(data.redirect);
}
return;
}
if (data.state === 'failed') { stop(data.message); return; }
window.setTimeout(poll, 5000);
}).catch(function () {
if (++failures >= 6) {
stop('Sambungan semakan terganggu. Jangan hantar transaksi semula. Muat semula halaman untuk menyambung semakan.');
return;
}
window.setTimeout(poll, 5000);
});
}
poll();
}());
</script>
@else
@if($error)<div class="alert alert-danger">{{ $error }}</div>@endif
@if(session('shoraka_message'))<div class="alert alert-info">{{ session('shoraka_message') }}</div>@endif
@php
@@ -80,6 +152,7 @@
<a class="btn btn-secondary mb-2" href="{{ route('shoraka.certificate', $norujukan) }}">Muat Turun Sijil</a>
@endif
</div>
@endif
</div>
</div>
</div>