DONE: add 30 minutes banner popup when pc not submitted the customer list, disable terima button when the confirmation form is not yet printed on teller; WIP: auto approve from operasi for gadai between 10k - 45k, wire up with whatsapp template message

This commit is contained in:
ISMAIL MASSERAN
2026-09-22 08:05:57 +08:00
committed by ISMAIL MASSERAN
parent 0e8f340b29
commit f8e097523a
9 changed files with 577 additions and 24 deletions
+89 -13
View File
@@ -93,6 +93,65 @@ class GadaianController extends Controller
return is_numeric($cleaned) ? (float) $cleaned : 0.0; return is_numeric($cleaned) ? (float) $cleaned : 0.0;
} }
protected function normalizeBorangOnlineFields($norujukan, $pinjaman, $kodbank, $noakaun, $namaAkaun)
{
return [
'norujukan' => trim((string) $norujukan),
'pinjaman' => number_format((float) $this->parseMoney($pinjaman), 2, '.', ''),
'kodbank' => strtoupper(trim((string) $kodbank)),
'noakaun' => preg_replace('/\s+/', '', (string) $noakaun),
'nama_akaun' => strtoupper(trim((string) $namaAkaun)),
];
}
protected function borangOnlineFingerprint(array $fields)
{
return sha1(implode('|', [
$fields['norujukan'],
$fields['pinjaman'],
$fields['kodbank'],
$fields['noakaun'],
$fields['nama_akaun'],
]));
}
protected function markBorangOnlinePrinted(array $fields)
{
Session::put('borang_online_printed', array_merge($fields, [
'fingerprint' => $this->borangOnlineFingerprint($fields),
]));
}
protected function hasMatchingBorangOnlinePrint(Request $request)
{
$printed = $request->session()->get('borang_online_printed');
if (!is_array($printed)) {
return false;
}
$norujukan = (string) $request->input('norujukan_gadai', $request->input('norujukan'));
$fields = $this->normalizeBorangOnlineFields(
$norujukan,
$request->input('pinjaman'),
$request->input('kodbank'),
$request->input('noakaun'),
$request->input('nama_akaun') ?: $request->input('nama_pelanggan')
);
return ($printed['norujukan'] ?? '') === $fields['norujukan']
&& ($printed['fingerprint'] ?? '') === $this->borangOnlineFingerprint($fields);
}
protected function borangOnlinePrintedFor($norujukan)
{
$printed = Session::get('borang_online_printed');
if (!is_array($printed) || ($printed['norujukan'] ?? '') !== (string) $norujukan) {
return null;
}
return $printed;
}
public function index(Request $request) public function index(Request $request)
{ {
@@ -120,6 +179,7 @@ class GadaianController extends Controller
Session::put('nokp', $noic); Session::put('nokp', $noic);
Session::put('nopelanggan', $customer_info['nopelanggan']); Session::put('nopelanggan', $customer_info['nopelanggan']);
Session::put('page_reload', true); Session::put('page_reload', true);
Session::forget('borang_online_printed');
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [ $gadai = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
'norujukan' => $norujukan, 'norujukan' => $norujukan,
'nopelanggan' => $customer_info['nopelanggan'], 'nopelanggan' => $customer_info['nopelanggan'],
@@ -144,6 +204,7 @@ class GadaianController extends Controller
Session::put('nokp', $nokp); Session::put('nokp', $nokp);
Session::put('nopelanggan', $nopelanggan); Session::put('nopelanggan', $nopelanggan);
Session::put('page_reload', true); Session::put('page_reload', true);
Session::forget('borang_online_printed');
$gadai_baru = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [ $gadai_baru = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
'norujukan' => $norujukan_baru, 'norujukan' => $norujukan_baru,
'nopelanggan' => $nopelanggan, 'nopelanggan' => $nopelanggan,
@@ -156,7 +217,8 @@ class GadaianController extends Controller
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json(); $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
return view('gadaian.' . $cawangan_info['version'] . '.index', compact('customer_info', 'limit_pinjaman_semasa', 'cawangan_info', 'norujukan', 'api_url', 'current')); $borang_online_printed = $this->borangOnlinePrintedFor($norujukan);
return view('gadaian.' . $cawangan_info['version'] . '.index', compact('customer_info', 'limit_pinjaman_semasa', 'cawangan_info', 'norujukan', 'api_url', 'current', 'borang_online_printed'));
} }
public function edit(Request $request, $norujukan) public function edit(Request $request, $norujukan)
@@ -223,6 +285,22 @@ class GadaianController extends Controller
public function store(Request $request) public function store(Request $request)
{ {
$auth_user = Auth::user(); $auth_user = Auth::user();
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
// Online only within min/max pinjaman; otherwise force cash path.
if ($caraTerima === 'ONLINE' && ($pinjamanAmt < self::MIN_PINJAMAN || $pinjamanAmt > self::MAX_PINJAMAN)) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->hasMatchingBorangOnlinePrint($request)) {
return redirect()->back()->with('error', 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.');
}
//$total_nilai_marhun = $request->total_nilai_marhun; //$total_nilai_marhun = $request->total_nilai_marhun;
//need to get total nilai marhun by api get from table marhun //need to get total nilai marhun by api get from table marhun
@@ -274,18 +352,6 @@ class GadaianController extends Controller
'teller' => $auth_user['name'] 'teller' => $auth_user['name']
]); ]);
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
// Online only within min/max pinjaman; otherwise force cash path.
if ($caraTerima === 'ONLINE' && ($pinjamanAmt < self::MIN_PINJAMAN || $pinjamanAmt > self::MAX_PINJAMAN)) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE') { if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')]) $online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm() ->asForm()
@@ -320,6 +386,7 @@ class GadaianController extends Controller
$request->session()->forget('page_reload'); $request->session()->forget('page_reload');
$request->session()->forget('norujukan'); $request->session()->forget('norujukan');
$request->session()->forget('borang_online_printed');
$request->session()->flash('activate_tab', 'SAG_baru'); $request->session()->flash('activate_tab', 'SAG_baru');
if ($caraTerima === 'ONLINE') { if ($caraTerima === 'ONLINE') {
$request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'); $request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.');
@@ -371,6 +438,14 @@ class GadaianController extends Controller
abort(422, 'Sila lengkapkan maklumat bank sebelum cetak borang.'); abort(422, 'Sila lengkapkan maklumat bank sebelum cetak borang.');
} }
$this->markBorangOnlinePrinted($this->normalizeBorangOnlineFields(
$line['norujukan'],
$line['pinjaman'],
$line['kodbank'],
$line['noakaun'],
$line['nama_akaun'] ?: $line['nama_pelanggan']
));
return $this->renderBorangPengesahanOnline($line, $kodcaw); return $this->renderBorangPengesahanOnline($line, $kodcaw);
} }
@@ -501,6 +576,7 @@ class GadaianController extends Controller
Http::withOptions(['verify' => config('app.api_verify')])->delete(config('api.url') . '/api/gadai/batal/' . $auth_user['cawangan'] . '/' . $norujukan); Http::withOptions(['verify' => config('app.api_verify')])->delete(config('api.url') . '/api/gadai/batal/' . $auth_user['cawangan'] . '/' . $norujukan);
$request->session()->forget('page_reload'); $request->session()->forget('page_reload');
$request->session()->forget('norujukan'); $request->session()->forget('norujukan');
$request->session()->forget('borang_online_printed');
return redirect()->route('customer_info'); return redirect()->route('customer_info');
} }
@@ -97,6 +97,52 @@ class PembiayaanOnlineController extends Controller
)); ));
} }
/**
* Lightweight payload for the PC layout cutoff banner / browser notification.
*/
public function reminderStatus()
{
$cawangan_info = $this->cawanganInfo();
if (!$this->isPembiayaanOnlineEnabled($cawangan_info)) {
return response()->json(['enabled' => false]);
}
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$queue = $this->api()
->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/queue')
->json();
$modal = $this->api()
->get($api_url . '/api/modal-online/' . $kodcaw)
->json();
if (!is_array($queue)) {
$queue = [];
}
if (!is_array($modal)) {
$modal = [];
}
$slot = (is_array($queue) && isset($queue['slot_cadangan']) && is_array($queue['slot_cadangan']))
? $queue['slot_cadangan']
: [];
return response()->json([
'enabled' => true,
'ditutup' => (int) ($modal['ditutup'] ?? 0) === 1,
'bilangan' => (int) ($queue['bilangan'] ?? 0),
'slot' => $slot['slot'] ?? 'BATCH1',
'cutoff' => $slot['cutoff'] ?? '11:00',
'cutoff_at' => $slot['cutoff_at'] ?? null,
'server_now' => $slot['server_now'] ?? null,
'lewat' => !empty($slot['lewat']),
'baki_minit' => (int) ($slot['baki_minit'] ?? 0),
'url' => route('pembiayaan.online.pc'),
'remind_minutes' => 30,
'last_minute' => 10,
]);
}
/** /**
* Update bank/docs for a queue line. * Update bank/docs for a queue line.
*/ */
+76 -2
View File
@@ -305,6 +305,9 @@
<a href="{{ route('gadai.batal',['norujukan'=>$norujukan]) }}" class="float-right btn btn-danger mr-2">Batal</a> <a href="{{ route('gadai.batal',['norujukan'=>$norujukan]) }}" class="float-right btn btn-danger mr-2">Batal</a>
<input type="submit" name="terima_gadai" id="terima_gadai" class="float-right btn btn-primary mr-2" value="Terima"> <input type="submit" name="terima_gadai" id="terima_gadai" class="float-right btn btn-primary mr-2" value="Terima">
<button type="button" id="cetak_borang_online" class="float-right btn btn-info mr-2" style="display:none;">Cetak Borang</button> <button type="button" id="cetak_borang_online" class="float-right btn btn-info mr-2" style="display:none;">Cetak Borang</button>
<div id="cetak_borang_hint" class="text-danger text-right mt-2" style="clear:both; display:none;">
Sila cetak borang pengesahan pelanggan dahulu
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -413,6 +416,7 @@
var kodcaw_ujrah = "<?php echo $cawangan_info['kodcaw'] ?>"; var kodcaw_ujrah = "<?php echo $cawangan_info['kodcaw'] ?>";
const MIN_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MIN_PINJAMAN }}; const MIN_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MIN_PINJAMAN }};
const MAX_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MAX_PINJAMAN }}; const MAX_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MAX_PINJAMAN }};
var printedBorang = @json($borang_online_printed ?? null);
$('form input').keydown(function (e) { $('form input').keydown(function (e) {
if (e.keyCode == 13) { if (e.keyCode == 13) {
e.preventDefault(); e.preventDefault();
@@ -424,6 +428,13 @@
$(document).on("keydown", disableF5); $(document).on("keydown", disableF5);
var norujukan = $('#norujukan').val(); var norujukan = $('#norujukan').val();
getMarhun(norujukan); getMarhun(norujukan);
@if(session('error'))
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: @json(session('error'))
});
@endif
$.ajax({ $.ajax({
method:'GET', method:'GET',
@@ -1301,6 +1312,51 @@
.then(response => response.json()); .then(response => response.json());
} }
function currentBorangFields() {
var pinjaman = Number(String($('#pinjaman').val() || '').replace(/,/g, ''));
if (!isFinite(pinjaman)) {
pinjaman = 0;
}
return {
norujukan: ($('#norujukan_gadai').val() || $('#norujukan').val() || '').trim(),
pinjaman: pinjaman.toFixed(2),
kodbank: ($('#gadai_kodbank').val() || '').trim().toUpperCase(),
noakaun: ($('#gadai_noakaun').val() || '').replace(/\s+/g, ''),
nama_akaun: ($('#gadai_nama_akaun').val() || $('input[name="nama_pelanggan"]').val() || '').trim().toUpperCase()
};
}
function isOnlineCaraTerima() {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
var pinjaman = Number(String($('#pinjaman').val() || '').replace(/,/g, ''));
return pembiayaanOnlineEnabled
&& pinjaman >= MIN_PINJAMAN
&& pinjaman <= MAX_PINJAMAN
&& $('input[name="cara_terima"]:checked').val() === 'ONLINE';
}
function hasMatchingPrint() {
if (!printedBorang) {
return false;
}
var cur = currentBorangFields();
return printedBorang.norujukan === cur.norujukan
&& printedBorang.pinjaman === cur.pinjaman
&& printedBorang.kodbank === cur.kodbank
&& printedBorang.noakaun === cur.noakaun
&& printedBorang.nama_akaun === cur.nama_akaun;
}
function syncTerimaButton() {
var lock = isOnlineCaraTerima() && !hasMatchingPrint();
$('#terima_gadai').prop('disabled', lock);
if (lock) {
$('#cetak_borang_hint').show();
} else {
$('#cetak_borang_hint').hide();
}
}
function toggleCaraTerima(pinjaman) { function toggleCaraTerima(pinjaman) {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1'; var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
if (pembiayaanOnlineEnabled && pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN) { if (pembiayaanOnlineEnabled && pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN) {
@@ -1311,6 +1367,7 @@
$('#bank_online_section').hide(); $('#bank_online_section').hide();
$('#cetak_borang_online').hide(); $('#cetak_borang_online').hide();
$('#cara_terima_tunai').prop('checked', true); $('#cara_terima_tunai').prop('checked', true);
syncTerimaButton();
} }
} }
@@ -1325,16 +1382,20 @@
$('#bank_online_section').hide(); $('#bank_online_section').hide();
$('#cetak_borang_online').hide(); $('#cetak_borang_online').hide();
} }
syncTerimaButton();
} }
$(document).on('change', 'input[name="cara_terima"]', function () { $(document).on('change', 'input[name="cara_terima"]', function () {
toggleBankOnlineSection(); toggleBankOnlineSection();
}); });
$(document).on('change', '#gadai_kodbank', function () { $(document).on('change input', '#gadai_kodbank, #gadai_noakaun, #gadai_nama_akaun', function () {
$('#gadai_nama_bank').val($('#gadai_kodbank option:selected').data('nama') || ''); $('#gadai_nama_bank').val($('#gadai_kodbank option:selected').data('nama') || '');
syncTerimaButton();
}); });
syncTerimaButton();
$(document).on('click', '#cetak_borang_online', function () { $(document).on('click', '#cetak_borang_online', function () {
var kodbank = $('#gadai_kodbank').val(); var kodbank = $('#gadai_kodbank').val();
var noakaun = ($('#gadai_noakaun').val() || '').trim(); var noakaun = ($('#gadai_noakaun').val() || '').trim();
@@ -1367,6 +1428,9 @@
form.append($('<input>', { type: 'hidden', name: 'noakaun', value: noakaun })); form.append($('<input>', { type: 'hidden', name: 'noakaun', value: noakaun }));
form.append($('<input>', { type: 'hidden', name: 'nama_akaun', value: namaAkaun })); form.append($('<input>', { type: 'hidden', name: 'nama_akaun', value: namaAkaun }));
form.appendTo('body').trigger('submit').remove(); form.appendTo('body').trigger('submit').remove();
printedBorang = currentBorangFields();
syncTerimaButton();
}); });
async function getModalOnline() { async function getModalOnline() {
@@ -1523,7 +1587,7 @@
let bakiakhir = 0; let bakiakhir = 0;
var kodcaw = $('#kodcaw').val(); var kodcaw = $('#kodcaw').val();
if(pinjaman == 0){ if(pinjaman == 0){
$("#terima_gadai").prop('disabled', false); syncTerimaButton();
Swal.fire({ Swal.fire({
icon: 'error', icon: 'error',
title: 'Amaran!', title: 'Amaran!',
@@ -1532,6 +1596,16 @@
return false; return false;
} }
if (isOnlineCaraTerima() && !hasMatchingPrint()) {
syncTerimaButton();
Swal.fire({
icon: 'warning',
title: 'Borang belum dicetak',
text: 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.'
});
return false;
}
let floatOk = await checkDisbursementFloat(pinjaman); let floatOk = await checkDisbursementFloat(pinjaman);
if (!floatOk) { if (!floatOk) {
$("#terima_gadai").prop('disabled', false); $("#terima_gadai").prop('disabled', false);
+1
View File
@@ -401,6 +401,7 @@
</div> </div>
@else @else
@endif @endif
@include('layouts.partials.batch_cutoff_banner')
@endif @endif
<main class="py-4"> <main class="py-4">
@@ -0,0 +1,355 @@
<style>
#batch-cutoff-banner {
position: fixed;
top: 112px;
right: 24px;
z-index: 1080;
width: min(380px, calc(100vw - 32px));
margin: 0;
padding: 0;
color: #fff;
background: #b91c1c;
border-radius: 16px;
box-shadow: 0 18px 40px rgba(127, 29, 29, 0.35);
}
#batch-cutoff-banner.batch-cutoff-warn {
background: #db2777;
box-shadow: 0 18px 40px rgba(190, 24, 93, 0.32);
}
#batch-cutoff-banner.batch-cutoff-warn .batch-cutoff-link {
color: #9d174d !important;
}
#batch-cutoff-banner.batch-cutoff-urgent {
background: #991b1b;
}
#batch-cutoff-banner .batch-cutoff-inner {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px 16px 16px;
}
#batch-cutoff-banner .batch-cutoff-copy {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
#batch-cutoff-banner .batch-cutoff-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.14);
font-size: 15px;
}
#batch-cutoff-banner .batch-cutoff-title {
display: block;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.3;
}
#batch-cutoff-banner .batch-cutoff-sub {
display: block;
margin-top: 3px;
font-size: 12px;
line-height: 1.4;
color: rgba(255, 255, 255, 0.82);
}
#batch-cutoff-banner .batch-cutoff-actions {
display: flex;
align-items: center;
gap: 8px;
padding-left: 48px;
}
#batch-cutoff-banner .batch-cutoff-chip {
display: inline-flex;
align-items: center;
min-width: 64px;
justify-content: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.22);
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
#batch-cutoff-banner .batch-cutoff-link,
#batch-cutoff-banner .batch-cutoff-notify {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 7px 14px;
border: 0;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
text-decoration: none !important;
white-space: nowrap;
cursor: pointer;
}
#batch-cutoff-banner .batch-cutoff-link {
flex: 1;
color: #991b1b !important;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
#batch-cutoff-banner .batch-cutoff-link:hover {
color: #7f1d1d !important;
background: #f8fafc;
}
#batch-cutoff-banner .batch-cutoff-notify {
color: #fff !important;
background: transparent;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
}
#batch-cutoff-banner .batch-cutoff-notify:hover {
background: rgba(255, 255, 255, 0.12);
}
@media (max-width: 767px) {
#batch-cutoff-banner {
top: auto;
bottom: 16px;
right: 16px;
left: 16px;
width: auto;
}
#batch-cutoff-banner .batch-cutoff-actions {
padding-left: 0;
}
}
</style>
<div id="batch-cutoff-banner" class="d-none" role="alert">
<div class="batch-cutoff-inner">
<div class="batch-cutoff-copy">
<span class="batch-cutoff-icon" aria-hidden="true"><i class="fas fa-clock"></i></span>
<div>
<span id="batch-cutoff-title" class="batch-cutoff-title">Hantar senarai pembiayaan online</span>
<span id="batch-cutoff-body" class="batch-cutoff-sub"></span>
</div>
</div>
<div class="batch-cutoff-actions">
<span id="batch-cutoff-countdown" class="batch-cutoff-chip">00:00</span>
<a id="batch-cutoff-link" href="{{ route('pembiayaan.online.pc') }}" class="batch-cutoff-link">Buka senarai</a>
<button type="button" id="batch-cutoff-enable-notify" class="batch-cutoff-notify d-none">Benarkan notifikasi</button>
</div>
</div>
</div>
<script>
(function () {
var STATUS_URL = @json(route('pembiayaan.online.pc.reminder'));
var POLL_MS = 60000;
var TICK_MS = 1000;
var state = null;
var fetchedAt = 0;
var serverNowAtFetch = 0;
var banner = document.getElementById('batch-cutoff-banner');
var titleEl = document.getElementById('batch-cutoff-title');
var bodyEl = document.getElementById('batch-cutoff-body');
var linkEl = document.getElementById('batch-cutoff-link');
var enableBtn = document.getElementById('batch-cutoff-enable-notify');
var countdownEl = document.getElementById('batch-cutoff-countdown');
if (!banner) {
return;
}
function todayKey() {
var d = new Date();
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
function pad(n) {
return n < 10 ? '0' + n : String(n);
}
function storageKey(kind, level) {
var slot = (state && state.slot) ? state.slot : 'BATCH';
return 'batchCutoff:' + kind + ':' + slot + ':' + todayKey() + ':' + level;
}
function was(kind, level) {
try {
return sessionStorage.getItem(storageKey(kind, level)) === '1';
} catch (e) {
return false;
}
}
function mark(kind, level) {
try {
sessionStorage.setItem(storageKey(kind, level), '1');
} catch (e) {}
}
function remainingMs() {
if (!state) {
return null;
}
var cutoffAt = state.cutoff_at ? Date.parse(state.cutoff_at) : NaN;
if (!isFinite(cutoffAt)) {
var parts = String(state.cutoff || '').split(':');
if (parts.length < 2) {
return null;
}
var now = new Date();
cutoffAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0).getTime();
}
var elapsed = Date.now() - fetchedAt;
var nowMs = (serverNowAtFetch || Date.now()) + elapsed;
return cutoffAt - nowMs;
}
function canNotify() {
return typeof Notification !== 'undefined' && Notification.permission === 'granted';
}
function notifyDefault() {
return typeof Notification !== 'undefined' && Notification.permission === 'default';
}
function sendBrowserNotification(level, title, body) {
if (!canNotify() || was('notify', level)) {
return;
}
mark('notify', level);
try {
var n = new Notification(title, {
body: body,
tag: 'batch-cutoff-' + ((state && state.slot) || '') + '-' + level,
requireInteraction: level === 1
});
n.onclick = function () {
window.focus();
if (state && state.url) {
window.location.href = state.url;
}
n.close();
};
} catch (e) {}
}
function hideBanner() {
banner.classList.add('d-none');
banner.classList.remove('batch-cutoff-urgent', 'batch-cutoff-warn');
}
function showBanner(level, remaining) {
var mins = Math.max(0, Math.floor(remaining / 60000));
var secs = Math.max(0, Math.floor((remaining % 60000) / 1000));
var slot = (state && state.slot) ? state.slot : 'Batch';
var cutoff = (state && state.cutoff) ? state.cutoff : '';
var count = (state && state.bilangan) ? state.bilangan : 0;
var countdown = pad(mins) + ':' + pad(secs);
if (countdownEl) {
countdownEl.textContent = countdown;
}
if (level === 1) {
titleEl.textContent = 'Sila hantar senarai pembiayaan online sekarang!! 😡 😡';
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod belum dihantar';
banner.classList.add('batch-cutoff-urgent');
banner.classList.remove('batch-cutoff-warn');
} else {
titleEl.textContent = 'Sila hantar senarai pembiayaan online 😇 😇';
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod menunggu';
banner.classList.add('batch-cutoff-warn');
banner.classList.remove('batch-cutoff-urgent');
}
if (state && state.url) {
linkEl.href = state.url;
}
if (notifyDefault()) {
enableBtn.classList.remove('d-none');
} else {
enableBtn.classList.add('d-none');
}
banner.classList.remove('d-none');
}
function tick() {
if (!state || !state.enabled || state.ditutup || state.lewat || !state.bilangan) {
hideBanner();
return;
}
var remaining = remainingMs();
if (remaining === null || remaining <= 0) {
hideBanner();
return;
}
var remindMs = (state.remind_minutes || 10) * 60000;
var lastMs = (state.last_minute || 1) * 60000;
if (remaining > remindMs) {
hideBanner();
return;
}
var level = remaining <= lastMs ? 1 : 10;
showBanner(level, remaining);
sendBrowserNotification(
level,
level === 1 ? '1 minit lagi: hantar senarai batch' : 'Hantar senarai pembiayaan online',
(state.slot || 'Batch') + ' tamat ' + (state.cutoff || '') + '. ' + state.bilangan + ' rekod belum dihantar.'
);
}
function applyStatus(data) {
if (!data || !data.enabled) {
state = null;
hideBanner();
return;
}
state = data;
fetchedAt = Date.now();
serverNowAtFetch = data.server_now ? Date.parse(data.server_now) : Date.now();
if (!isFinite(serverNowAtFetch)) {
serverNowAtFetch = Date.now();
}
tick();
}
function fetchStatus() {
fetch(STATUS_URL, {
credentials: 'same-origin',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (res) {
if (!res.ok) {
throw new Error('reminder status ' + res.status);
}
return res.json();
}).then(applyStatus).catch(function () {});
}
enableBtn.addEventListener('click', function () {
if (typeof Notification === 'undefined') {
return;
}
Notification.requestPermission().then(function () {
if (canNotify()) {
enableBtn.classList.add('d-none');
var remaining = remainingMs();
var lastMs = ((state && state.last_minute) || 1) * 60000;
var level = remaining !== null && remaining <= lastMs ? 1 : 10;
try { sessionStorage.removeItem(storageKey('notify', level)); } catch (e) {}
tick();
} else {
enableBtn.classList.add('d-none');
}
});
});
fetchStatus();
setInterval(fetchStatus, POLL_MS);
setInterval(tick, TICK_MS);
})();
</script>
@@ -165,11 +165,11 @@
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;"> <div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Pratinjau SAG — <span id="sagPreviewTitle"></span></h5> <h5 class="modal-title">Pratonton SAG — <span id="sagPreviewTitle"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button> <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div> </div>
<div class="modal-body p-2"> <div class="modal-body p-2">
<iframe id="sagPreviewFrame" title="Pratinjau SAG" src="about:blank" <iframe id="sagPreviewFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe> style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -85,11 +85,11 @@
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;"> <div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title">Pratinjau SAG — <span id="sagPreviewTitle"></span></h5> <h5 class="modal-title">Pratonton SAG — <span id="sagPreviewTitle"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button> <button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div> </div>
<div class="modal-body p-2"> <div class="modal-body p-2">
<iframe id="sagPreviewFrame" title="Pratinjau SAG" src="about:blank" <iframe id="sagPreviewFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe> style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -412,8 +412,8 @@
<p class="small text-muted mb-0">Jika rekod sudah SAH, simpan akan batal semakan. Sila semak semula sebelum hantar.</p> <p class="small text-muted mb-0">Jika rekod sudah SAH, simpan akan batal semakan. Sila semak semula sebelum hantar.</p>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="d-block">Pratinjau SAG</label> <label class="d-block">Pratonton SAG</label>
<iframe id="editSagFrame" title="Pratinjau SAG" src="about:blank" <iframe id="editSagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe> style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div> </div>
</div> </div>
@@ -486,12 +486,12 @@
</label> </label>
</div> </div>
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;"> <a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
Lihat Slip Bank Lihat Bukti Akaun Bank
</a> </a>
</div> </div>
<div class="col-md-8"> <div class="col-md-8">
<label class="d-block">Pratinjau SAG</label> <label class="d-block">Pratonton SAG</label>
<iframe id="verifySagFrame" title="Pratinjau SAG" src="about:blank" <iframe id="verifySagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe> style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div> </div>
</div> </div>
+1
View File
@@ -198,6 +198,7 @@ Route::post('/notifications/{id}/read',['as'=>'notifications.read','uses'=>'Noti
// Pembiayaan Online — PC // Pembiayaan Online — PC
Route::get('/pembiayaan-online/pc',['as'=>'pembiayaan.online.pc','uses'=>'PembiayaanOnlineController@index'])->middleware('auth','khazanah'); Route::get('/pembiayaan-online/pc',['as'=>'pembiayaan.online.pc','uses'=>'PembiayaanOnlineController@index'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/reminder',['as'=>'pembiayaan.online.pc.reminder','uses'=>'PembiayaanOnlineController@reminderStatus'])->middleware('auth','khazanah');
Route::put('/pembiayaan-online/pc/lines/{id}',['as'=>'pembiayaan.online.pc.update','uses'=>'PembiayaanOnlineController@updateLine'])->middleware('auth','khazanah'); Route::put('/pembiayaan-online/pc/lines/{id}',['as'=>'pembiayaan.online.pc.update','uses'=>'PembiayaanOnlineController@updateLine'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/lines/{id}/verify',['as'=>'pembiayaan.online.pc.verify','uses'=>'PembiayaanOnlineController@verifyLine'])->middleware('auth','khazanah'); Route::post('/pembiayaan-online/pc/lines/{id}/verify',['as'=>'pembiayaan.online.pc.verify','uses'=>'PembiayaanOnlineController@verifyLine'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/lines/{id}/unverify',['as'=>'pembiayaan.online.pc.unverify','uses'=>'PembiayaanOnlineController@unverifyLine'])->middleware('auth','khazanah'); Route::post('/pembiayaan-online/pc/lines/{id}/unverify',['as'=>'pembiayaan.online.pc.unverify','uses'=>'PembiayaanOnlineController@unverifyLine'])->middleware('auth','khazanah');