16 Commits

Author SHA1 Message Date
ISMAIL MASSERAN d614677601 DONE: add proper vector logo arrahn
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 15:58:05 +08:00
ismailmasseran 7cd21ef610 Feat/dasar privasi (#26)
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Reviewed-on: #26
2026-09-27 15:56:09 +08:00
nurafrinaalimi16 ca2a02e354 hotfix: betulkan lajur tarikh untuk semakan ubah dan hapus tebus
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 15:10:29 +08:00
nurafrinaalimi16 76176ed453 hotfix:ubah hapus gadai
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 13:05:54 +08:00
ISMAIL MASSERAN 17a5bfafdd DONE: update env.staging
Build Docker Image / build-frontend (push) Successful in 41s
2026-09-26 18:51:36 +08:00
ismailmasseran 5f29f064a0 Merge pull request 'feat(khazanah): papar tarikh ansuran terkini dalam senarai gadaian' (#25) from feat/khazanah-tarikh-ansuran-terkini into main
Reviewed-on: #25
2026-09-24 16:16:16 +08:00
nurafrinaalimi16 9dae32a462 feat(khazanah): papar tarikh ansuran terkini dalam senarai gadaian 2026-09-24 16:15:20 +08:00
ismailmasseran c437a3f504 Feature/whatsapp blast (#24)
Build Docker Image / build-frontend (push) Successful in 53s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #24
2026-09-24 11:28:05 +08:00
ismailmasseran b41234e10b Merge pull request 'DONE: fix duplicate url when print aliran tunai report' (#23) from hotfix/aliran-tunai into main
Reviewed-on: #23
2026-09-22 10:30:49 +08:00
ismailmasseran b7d217948c Merge branch 'main' into hotfix/aliran-tunai 2026-09-22 10:24:11 +08:00
ISMAIL MASSERAN 2d282897fc DONE: fix duplicate url when print aliran tunai report
Build Docker Image / build-frontend (push) Successful in 41s
2026-09-22 10:14:52 +08:00
ismailmasseran 94f8f6e855 Merge pull request 'fix: selaraskan tarikh lelong khazanah dan tambah pengesahan baki lelong sebelum daftar pembiayaan' (#22) from fix/tarikh-lelong-khazanah into main
Reviewed-on: #22
2026-09-21 16:22:54 +08:00
nurafrinaalimi16 72108d3f87 fix: selaraskan tarikh lelong khazanah dan tambah pengesahan baki lelong sebelum daftar pembiayaan 2026-09-21 14:57:28 +08:00
ISMAIL MASSERAN 3a64cc2400 DONE: drop column cara terima on report 2026-09-20 16:37:30 +08:00
ismailmasseran 1d71a31078 Merge pull request 'DONE: use constant for min and max pinjaman' (#21) from bugfix/limit-error-cara-terima into main
Reviewed-on: #21
2026-09-20 16:28:38 +08:00
ISMAIL MASSERAN 8e19d0488a DONE: use constant for min and max pinjaman 2026-09-20 16:28:01 +08:00
42 changed files with 2618 additions and 379 deletions
+3 -1
View File
@@ -51,4 +51,6 @@ 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
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret
+4 -2
View File
@@ -1,10 +1,10 @@
APP_NAME=Laravel
APP_ENV=local
APP_VERSION=v3.3
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://localhost
API_URL=http://localhost:8000
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
@@ -47,4 +47,6 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=
+3 -1
View File
@@ -47,4 +47,6 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd"
+3 -1
View File
@@ -55,4 +55,6 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=staging_noti_secret
+1 -7
View File
@@ -9,13 +9,7 @@ CREATE USER 'kaunteruser'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON kaunter_erahn.* TO 'kaunteruser'@'%';
FLUSH PRIVILEGES;
```
# ADDITIONAL NOTE
## customers/v2/index.blade.php is modal to register customer
## Min Komoditi = RM5000
```
# System Flowchart
+126 -13
View File
@@ -11,6 +11,7 @@ use App\Events\SerahanPenilai;
use Illuminate\Http\Request;
use App\Services\CustomerService;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Auth;
@@ -22,6 +23,9 @@ use Illuminate\Support\Facades\Log;
class GadaianController extends Controller
{
const MIN_PINJAMAN = 5.00;
const MAX_PINJAMAN = 50000.00;
public $AliranTunaiController;
public $customerService;
@@ -47,6 +51,37 @@ class GadaianController extends Controller
return $this->isFeatureEnabled($kodcaw, 'pembiayaan_online');
}
/**
* Inbox row for PC at this branch. Failures must not block gadai.
*/
protected function notifyPcPembiayaanOnlinePending($auth_user, Request $request, $pinjamanAmt)
{
try {
app(NotificationService::class)->notifyRoleAtCawangan('pc', $auth_user['cawangan'], [
'type' => NotificationService::TYPE_PEMBIAYAAN_PENDING_PC,
'title' => 'Pembiayaan online menunggu semakan',
'body' => sprintf(
'%s — %s (RM %s)',
$request->nama_pelanggan,
$request->norujukan_gadai,
number_format((float) $pinjamanAmt, 2)
),
'url' => route('pembiayaan.online.pc'),
'data' => [
'norujukan' => $request->norujukan_gadai,
'nama_pelanggan' => $request->nama_pelanggan,
'pinjaman' => $pinjamanAmt,
'created_by' => $auth_user['name'],
],
]);
} catch (\Throwable $e) {
Log::warning('Pembiayaan online PC notification failed: '.$e->getMessage(), [
'norujukan' => $request->norujukan_gadai,
'cawangan' => $auth_user['cawangan'],
]);
}
}
protected function parseMoney($value)
{
if ($value === null || $value === '') {
@@ -58,6 +93,65 @@ class GadaianController extends Controller
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)
{
@@ -85,6 +179,7 @@ class GadaianController extends Controller
Session::put('nokp', $noic);
Session::put('nopelanggan', $customer_info['nopelanggan']);
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'], [
'norujukan' => $norujukan,
'nopelanggan' => $customer_info['nopelanggan'],
@@ -109,6 +204,7 @@ class GadaianController extends Controller
Session::put('nokp', $nokp);
Session::put('nopelanggan', $nopelanggan);
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'], [
'norujukan' => $norujukan_baru,
'nopelanggan' => $nopelanggan,
@@ -121,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();
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)
@@ -188,6 +285,22 @@ class GadaianController extends Controller
public function store(Request $request)
{
$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;
//need to get total nilai marhun by api get from table marhun
@@ -239,18 +352,6 @@ class GadaianController extends Controller
'teller' => $auth_user['name']
]);
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
// Online only for RM5k–50k; otherwise force cash path.
if ($caraTerima === 'ONLINE' && ($pinjamanAmt < 5000 || $pinjamanAmt > 50000)) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -275,6 +376,8 @@ class GadaianController extends Controller
: 'Gagal daftar pembiayaan online. Sila cuba lagi.';
return redirect()->back()->with('error', $msg);
}
$this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt);
} else {
$this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']);
}
@@ -283,6 +386,7 @@ class GadaianController extends Controller
$request->session()->forget('page_reload');
$request->session()->forget('norujukan');
$request->session()->forget('borang_online_printed');
$request->session()->flash('activate_tab', 'SAG_baru');
if ($caraTerima === 'ONLINE') {
$request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.');
@@ -334,6 +438,14 @@ class GadaianController extends Controller
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);
}
@@ -464,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);
$request->session()->forget('page_reload');
$request->session()->forget('norujukan');
$request->session()->forget('borang_online_printed');
return redirect()->route('customer_info');
}
-23
View File
@@ -5,9 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Services\CustomerService;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Hash;
use Carbon\Carbon;
class HomeController extends Controller
@@ -41,17 +39,6 @@ class HomeController extends Controller
if(session()->has('iklan') && session()->get('iklan') == 0)
{
session()->put('iklan', 1);
}else{
if (Hash::check(config('app.reset_password'), $auth_user['password']) || Hash::check($auth_user['no_kp'], $auth_user['password'])) {
session()->put('iklan', 0);
}
}
if($auth_user['roles'] == 'teller'){
if($auth_user['role_harian'] == 'teller'){
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
@@ -117,16 +104,6 @@ class HomeController extends Controller
]);
if(session()->has('iklan') && session()->get('iklan') == 0)
{
session()->put('iklan', 1);
}else{
if (Hash::check(config('app.reset_password'), $auth_user['password']) || Hash::check($auth_user['no_kp'], $auth_user['password'])) {
session()->put('iklan', 0);
}
}
if($auth_user['roles'] == 'teller'){
if($auth_user['role_harian'] == 'teller'){
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Services\NotificationService;
use Illuminate\Http\Request;
class InternalNotificationController extends Controller
{
public function store(Request $request, NotificationService $notifications)
{
$expected = (string) config('notifications.internal_secret');
$provided = (string) $request->header('X-Internal-Token', '');
if ($expected === '' || !hash_equals($expected, $provided)) {
return response()->json(['message' => 'Unauthorized'], 401);
}
$audience = $request->input('audience', []);
$role = is_array($audience) && !empty($audience['role']) ? $audience['role'] : null;
if (!$role) {
return response()->json(['message' => 'audience.role diperlukan'], 422);
}
$created = $notifications->dispatch($request->all());
return response()->json([
'created' => $created->count(),
], 201);
}
}
@@ -29,6 +29,7 @@ use Carbon\Carbon;
use PDF;
use DateTime;
use App\Helper;
use App\Services\NotificationService;
use Log;
class KhazanahController extends Controller
@@ -668,6 +669,23 @@ class KhazanahController extends Controller
"namakhazanah" => $request->namakhazanah
])->json();
// Start 30s Operasi auto-approve clock only after Khazanah escalates (10k–45k).
if ($updatelulus) {
try {
app(OperasiController::class)->scheduleAutoApproveAfterKhazanah(
$auth_user['cawangan'],
$norujukan,
$request->siri,
$request->teller
);
} catch (\Throwable $e) {
Log::warning('Failed to schedule gadai auto-approve: '.$e->getMessage(), [
'norujukan' => $norujukan,
'cawangan' => $auth_user['cawangan'],
]);
}
}
}else{
$updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateKelulusan/' . $auth_user['cawangan'] . '/' . $norujukan,[
@@ -1084,6 +1102,7 @@ class KhazanahController extends Controller
if($test == 'success'){
event(new PendahuluanSerahan("Lulus","pendahuluan",$auth_user['cawangan'],$request->teller));
$this->notifyTellerAliranTunai('Lulus', 'pendahuluan', $auth_user['cawangan'], $request->teller, $amount);
return redirect()->route('khazanah.pendahulu');
}else{
return redirect()->route('khazanah.pendahulu')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.');
@@ -1109,6 +1128,7 @@ class KhazanahController extends Controller
if($test == 'success'){
event(new PendahuluanSerahan("Lulus","serahan",$auth_user['cawangan'],$request->teller));
$this->notifyTellerAliranTunai('Lulus', 'serahan', $auth_user['cawangan'], $request->teller, $request->amt);
return redirect()->route('khazanah.serahan');
}else{
return redirect()->route('khazanah.serahan')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.');
@@ -2848,10 +2868,43 @@ class KhazanahController extends Controller
]);
event(new PendahuluanSerahan("Batal","all",$auth_user['cawangan'],$kodlaluan));
$this->notifyTellerAliranTunai('Batal', 'all', $kodcaw, $kodlaluan);
return response('Berjaya');
}
/**
* Inbox for the teller waiting on aliran tunai. Failures must not block approval.
*
* @param string $pengesahan
* @param string $jenis
* @param string $kodcaw
* @param string $teller
* @param mixed $amaun
* @return void
*/
protected function notifyTellerAliranTunai($pengesahan, $jenis, $kodcaw, $teller, $amaun = 0)
{
try {
$raw = str_replace(',', '', (string) $amaun);
$amount = is_numeric($raw) ? (float) $raw : 0;
app(NotificationService::class)->notifyAliranTunaiDecided($kodcaw, $teller, [
'pengesahan' => $pengesahan,
'jenis' => $jenis,
'amaun' => $amount,
'kodlaluan' => $teller,
'khazanah' => Auth::user() ? Auth::user()->name : '',
]);
} catch (\Throwable $e) {
Log::warning('Aliran tunai teller notification failed: '.$e->getMessage(), [
'kodcaw' => $kodcaw,
'teller' => $teller,
'jenis' => $jenis,
]);
}
}
public function SuratAkuanGadaian(Request $request)
{
$auth_user = Auth::user();
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(NotificationService $notifications)
{
return response()->json($notifications->inboxPayload(Auth::user()));
}
public function markAsRead(NotificationService $notifications, $id)
{
$notification = $notifications->markAsRead(Auth::user(), $id);
if (!$notification) {
return response()->json(['message' => 'Notifikasi tidak dijumpai.'], 404);
}
return response()->json([
'notification' => $notification->toInboxArray(),
'unread_count' => $notifications->unreadCount(Auth::user()),
]);
}
public function markAllAsRead(NotificationService $notifications)
{
$updated = $notifications->markAllAsRead(Auth::user());
return response()->json([
'updated' => $updated,
'unread_count' => 0,
]);
}
}
+270 -54
View File
@@ -2,13 +2,14 @@
namespace App\Http\Controllers;
//Events
use App\Events\OperasiNotice;
use App\Events\StatusLiked;
use App\Events\NotifikasiKelulusan;
use App\Events\NotifikasiAnggota;
use App\Events\Dashboard\KomoditiNotify;
use App\Services\NotificationService;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
@@ -21,6 +22,15 @@ use Illuminate\Support\Facades\Log;
class OperasiController extends Controller
{
const AUTO_APPROVE_MIN = 10000;
const AUTO_APPROVE_MAX = 45000;
const AUTO_APPROVE_USER_IDS = [252, 275];
const AUTO_APPROVE_EMAILS = [
252 => 'hafiz@koppkb.com',
275 => 'shauqi@koppkb.com',
];
const AUTO_APPROVE_KOMEN = 'lulus';
public function __construct()
{
$this->middleware('auth');
@@ -214,51 +224,19 @@ class OperasiController extends Controller
'sahkan' => $status
];
Log::info('Menghantar permintaan ke API UpdateKelulusanAnggota', [
'endpoint' => $endpoint,
'payload' => $payload
]);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->put($endpoint, $payload);
$updatelulus = $response->json();
// 3️⃣ Log hasil respon API
Log::info('Respon dari API UpdateKelulusanAnggota', [
'status' => $response->status(),
'body' => $updatelulus
]);
// 4️⃣ Jika berjaya, cipta event notifikasi
if ($response->successful() && $updatelulus && isset($updatelulus['success']) && $updatelulus['success'] === true) {
// $jenis = ($status == 1) ? 'lulus' : 'batal';
// event(new NotifikasiAnggota($nokp, $jenis, null, $komen, $namaoperasi));
$statusText = ($status == 1) ? 'DILULUSKAN' : 'DITOLAK';
event(new NotifikasiAnggota($nokp, $statusText, $komen, $namaoperasi));
Log::info('Event NotifikasiAnggota dispatched', [
'nokp' => $nokp,
'komen' => $komen,
'namaoperasi' => $namaoperasi
]);
} else {
Log::warning('Gagal kemas kini kelulusan anggota', [
'nokp' => $nokp,
'kodcaw' => $kodcaw,
'response' => $updatelulus
]);
}
return response()->json($updatelulus, $response->status());
} catch (\Exception $e) {
Log::error('Ralat UpdateKelulusanAnggota (frontend)', [
'message' => $e->getMessage(),
'line' => $e->getLine()
]);
return response()->json([
'success' => false,
'message' => 'Ralat semasa menghantar kelulusan anggota.',
@@ -267,10 +245,6 @@ class OperasiController extends Controller
}
}
public function createkelulusan(Request $request)
{
$auth_user = Auth::user();
@@ -309,9 +283,245 @@ class OperasiController extends Controller
event(new StatusLiked($auth_user['cawangan'], $auth_user['name']));
$this->notifyGadaiAboveTenThousand($auth_user, $request, $cawangan_info);
return response()->json($siri, 201);
}
/**
* Hidden auto-approve for operasi gadai kelulusan in RM10,000–45,000
* when Operasi has not approved within 30s after Khazanah escalation.
*/
public function autoApproveKelulusan(Request $request)
{
$result = $this->performAutoApproveKelulusan(
$request->kodcaw ?: Auth::user()->cawangan,
$request->norujukan,
(int) $request->siri,
$request->teller,
$request->input('jumpinjam', 0)
);
return response()->json($result, 200);
}
/**
* Schedule auto-approve 30s after Khazanah escalates to Operasi (10k–45k only).
* Runs after the HTTP response so Khazanah UI is not blocked.
*/
public function scheduleAutoApproveAfterKhazanah($kodcaw, $norujukan, $siri, $teller)
{
$kodcaw = (string) $kodcaw;
$norujukan = (string) $norujukan;
$siri = (int) $siri;
$teller = (string) $teller;
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return;
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? 0));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : 0;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return;
}
app()->terminating(function () use ($kodcaw, $norujukan, $siri, $teller, $apiAmount) {
sleep(30);
try {
app(self::class)->performAutoApproveKelulusan(
$kodcaw,
$norujukan,
$siri,
$teller,
$apiAmount
);
} catch (\Throwable $e) {
Log::warning('Gadai auto-approve after khazanah failed: '.$e->getMessage(), [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
'siri' => $siri,
]);
}
});
}
/**
* Core auto-approve: idempotent if already decided or out of range.
*/
public function performAutoApproveKelulusan($kodcaw, $norujukan, $siri, $teller, $jumpinjam = 0)
{
$siri = (int) $siri;
$raw = str_replace(',', '', (string) $jumpinjam);
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < self::AUTO_APPROVE_MIN || $pinjaman > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_found',
];
}
$masalulus = $latest['masalulus'] ?? '';
$pelulus = $latest['pelulus'] ?? '';
if (($masalulus !== '' && $masalulus !== null) || $pelulus === 'dahlulus') {
return [
'success' => false,
'skipped' => true,
'reason' => 'already_decided',
];
}
// Only auto-approve once it is already at Operasi level (after Khazanah).
if ($pelulus !== 'operasi') {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_operasi_level',
];
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? $pinjaman));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : $pinjaman;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$namaoperasi = $this->resolveAutoApproveOperasiName();
if (!$namaoperasi) {
Log::warning('Gadai auto-approve: approver users 252/275 not found', [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
]);
return [
'success' => false,
'skipped' => true,
'reason' => 'approver_missing',
];
}
$komen = self::AUTO_APPROVE_KOMEN;
Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusanKomen/' . $kodcaw . '/' . $norujukan,
[
'komen' => $komen,
'siri' => $siri,
]
)->json();
$updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusan/' . $kodcaw . '/' . $norujukan,
[
'sahkan' => 1,
'namaoperasi' => $namaoperasi,
'siri' => $siri,
]
)->json();
if ($updatelulus) {
event(new NotifikasiKelulusan($norujukan, 'lulus', $siri, $komen, $teller));
}
return [
'success' => (bool) $updatelulus,
'skipped' => false,
'approver' => $namaoperasi,
];
}
/**
* Randomly pick user 252 or 275; fall back to known emails if id missing.
*/
protected function resolveAutoApproveOperasiName()
{
$ids = self::AUTO_APPROVE_USER_IDS;
$approverId = $ids[array_rand($ids)];
$user = User::find($approverId);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$approverId] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if (!$user) {
foreach ($ids as $id) {
if ($id === $approverId) {
continue;
}
$user = User::find($id);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$id] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if ($user) {
break;
}
}
}
return $user ? $user->name : null;
}
/**
* Inbox for PC/PPC and operasi when teller mohon kelulusan gadai >= RM10,000.
*/
protected function notifyGadaiAboveTenThousand($auth_user, Request $request, $cawangan_info)
{
$raw = str_replace(',', '', (string) $request->input('jumpinjam', 0));
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < NotificationService::GADAI_KELULUSAN_MIN) {
return;
}
$status = strtolower((string) $request->input('status', ''));
if (strpos($status, 'tebus') !== false) {
return;
}
$namaCaw = '';
if (is_array($cawangan_info)) {
$namaCaw = isset($cawangan_info['details_caw']) ? $cawangan_info['details_caw'] : '';
}
try {
app(NotificationService::class)->notifyGadaiKelulusan($auth_user['cawangan'], [
'norujukan' => $request->norujukan,
'pinjaman' => $pinjaman,
'teller' => $request->teller ?: $auth_user['name'],
'cawangan_nama' => $namaCaw,
'status' => $request->status,
]);
} catch (\Throwable $e) {
Log::warning('Gadai kelulusan notification failed: '.$e->getMessage(), [
'norujukan' => $request->norujukan,
'cawangan' => $auth_user['cawangan'],
]);
}
}
public function createkelulusanAnggota(Request $request)
{
@@ -320,8 +530,6 @@ class OperasiController extends Controller
$api_url = config('api.url');
$current = Carbon::now();
Log::info('createkelulusanAnggota', $request->all());
// Dapatkan maklumat cawangan
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $auth_user['cawangan'])
@@ -360,7 +568,6 @@ class OperasiController extends Controller
'body' => $response->body(),
]);
} catch (\Throwable $th) {
Log::error('❌ createkelulusanAnggota error: ' . $th->getMessage());
return response()->json([
'success' => false,
'message' => $th->getMessage(),
@@ -415,26 +622,35 @@ class OperasiController extends Controller
$api_url = config('api.url');
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
//declare array baru
$arraykelulusan = [];
$dateTo = Carbon::now()->toDateString();
$dateFrom = Carbon::now()->subDays(30)->toDateString();
$kelulusan = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/ListOperasiKelulusan/')->json();
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'dateFrom', 'dateTo'));
}
public function kelulusanrekodlepasData(Request $request)
{
$params = [
'date_from' => $request->query('date_from'),
'date_to' => $request->query('date_to'),
'search' => $request->query('search'),
];
foreach ($kelulusan as $index => $k) {
foreach ($k['kelulusanall'] as $index1 => $kel) {
$komen = explode("|", $kel['komen']);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListOperasiKelulusan/', array_filter($params, function ($value) {
return $value !== null && $value !== '';
}));
$results = array_search("", $komen, true);
if ($results !== false) {
unset($komen[$results]);
}
array_push($arraykelulusan, ['norujukan' => $kel['norujukan'], 'pinjaman' => $kel['jumpinj'], 'nilaimarhun' => ($komen[1] ?? $kel['nilaimarhun'] ?? '-'), 'berat' => ($komen[2] ?? $kel['berat'] ?? '-'), 'margin' => ($komen[3] ?? $kel['margin'] ?? '-'), 'komen' => ($komen[0] ?? $kel['komen'] ?? '-'), 'status' => ($kel['status'] ?? '-'), 'pelulus' => $kel['operasi'],]);
}
if (!$response->successful()) {
return response()->json(['data' => [], 'error' => 'Gagal memuatkan rekod'], 502);
}
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'arraykelulusan'));
$rows = $response->json();
if (!is_array($rows)) {
$rows = [];
}
return response()->json(['data' => $rows]);
}
public function notifyKomoditi()
@@ -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.
*/
@@ -549,7 +595,7 @@ class PembiayaanOnlineController extends Controller
$n = $res->json()['dikemaskini'] ?? count($ids);
return redirect()
->route('pembiayaan.online.pc.paid')
->with('message', $n . ' rekod ditanda selesai semak/blast.');
->with('message', $n . ' rekod ditanda selesai. WhatsApp notis dihantar (nombor ujian jika bukan production).');
}
/**
+1 -1
View File
@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
//
'internal/notifications',
];
}
+568
View File
@@ -0,0 +1,568 @@
<?php
namespace App\Services;
use App\StaffNotification;
use App\User;
use Illuminate\Support\Facades\Log;
class NotificationService
{
const TYPE_PEMBIAYAAN_PENDING_PC = 'pembiayaan_online.pending_pc';
const TYPE_MODAL_PENDING_OPERASI = 'modal_online.pending_operasi';
const TYPE_PEMBIAYAAN_BATCH_OPERASI = 'pembiayaan_online.batch_operasi';
const TYPE_PEMBIAYAAN_REJECTED_PC = 'pembiayaan_online.rejected_pc';
const TYPE_PEMBIAYAAN_PAID_PC = 'pembiayaan_online.paid_pc';
const TYPE_GADAI_KELULUSAN = 'gadai.kelulusan';
const TYPE_ANGGOTA_PENDING_OPERASI = 'anggota.pending_operasi';
const TYPE_ALIRAN_TUNAI_PENDING = 'aliran_tunai.pending_khazanah';
const TYPE_ALIRAN_TUNAI_DECIDED = 'aliran_tunai.decided_teller';
const GADAI_KELULUSAN_MIN = 10000;
/**
* Create one inbox row per staff matching role + branch.
*
* @param string $role
* @param string $kodcaw
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRoleAtCawangan($role, $kodcaw, array $payload)
{
$users = User::query()
->where('roles', $role)
->where('cawangan', $kodcaw)
->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Create one inbox row per staff matching role (any branch). For HQ like operasi.
*
* @param string $role
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRole($role, array $payload)
{
$kodcaw = isset($payload['cawangan']) ? $payload['cawangan'] : null;
$users = User::query()->where('roles', $role)->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Entry point for api_arrahn (and local callers). Builds title/url from type when omitted.
*
* @param array $input
* @return \Illuminate\Support\Collection
*/
public function dispatch(array $input)
{
$type = isset($input['type']) ? $input['type'] : '';
$kodcaw = isset($input['cawangan']) ? $input['cawangan'] : null;
$audience = isset($input['audience']) && is_array($input['audience']) ? $input['audience'] : [];
$role = isset($audience['role']) ? $audience['role'] : null;
$data = isset($input['data']) && is_array($input['data']) ? $input['data'] : [];
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING && $kodcaw) {
return $this->notifyAliranTunaiPending($kodcaw, $data);
}
$payload = $this->payloadForType($type, $kodcaw, $data);
if (!empty($input['title'])) {
$payload['title'] = $input['title'];
}
if (array_key_exists('body', $input) && $input['body'] !== null && $input['body'] !== '') {
$payload['body'] = $input['body'];
}
if (!empty($input['url'])) {
$payload['url'] = $input['url'];
}
if ($role === 'operasi') {
return $this->notifyRole('operasi', $payload);
}
if ($role && $kodcaw) {
return $this->notifyRoleAtCawangan($role, $kodcaw, $payload);
}
return collect();
}
/**
* Gadai >= RM10,000: inbox for PC/PPC at the branch and all operasi.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyGadaiKelulusan($kodcaw, array $data)
{
$pinjaman = isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0;
if ($pinjaman < self::GADAI_KELULUSAN_MIN) {
return collect();
}
$payload = $this->payloadForType(self::TYPE_GADAI_KELULUSAN, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge(
$this->notifyRoleAtCawangan($role, $kodcaw, array_merge($payload, [
'url' => '/kelulusan',
]))
);
}
return $created->merge(
$this->notifyRole('operasi', array_merge($payload, [
'url' => '/operasi/kelulusan',
]))
);
}
/**
* Teller pendahuluan/serahan: inbox for PC/PPC at the branch.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiPending($kodcaw, array $data)
{
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_PENDING, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge($this->notifyRoleAtCawangan($role, $kodcaw, $payload));
}
return $created;
}
/**
* Khazanah lulus/batal: inbox for the teller who requested cash.
*
* @param string $kodcaw
* @param string $tellerName
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiDecided($kodcaw, $tellerName, array $data)
{
$tellerName = trim((string) $tellerName);
if ($tellerName === '') {
return collect();
}
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_DECIDED, $kodcaw, $data);
$user = User::query()
->where('name', $tellerName)
->where('cawangan', $kodcaw)
->first();
if (!$user) {
$user = User::query()->where('name', $tellerName)->first();
}
if (!$user) {
return collect();
}
return collect([$this->notifyUser($user, $kodcaw, $user->roles, $payload)]);
}
/**
* @param string $type
* @param string|null $kodcaw
* @param array $data
* @return array
*/
protected function payloadForType($type, $kodcaw, array $data)
{
if ($type === self::TYPE_MODAL_PENDING_OPERASI) {
$jenis = isset($data['jenis']) ? strtoupper((string) $data['jenis']) : 'HAD';
$label = $jenis === 'TOPUP' ? 'tambah modal' : 'had modal';
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$amaun = number_format(isset($data['amaun']) ? (float) $data['amaun'] : 0, 2);
$oleh = isset($data['dimohon_oleh']) ? trim((string) $data['dimohon_oleh']) : '';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan modal online',
'body' => sprintf(
'%s mohon %s RM %s%s',
$nama,
$label,
$amaun,
$oleh !== '' ? ' — '.$oleh : ''
),
'url' => '/pembiayaan-online/operasi/topup',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_BATCH_OPERASI) {
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : 'batch';
$batchId = isset($data['batch_id']) ? $data['batch_id'] : null;
$submitted = isset($data['submitted_count']) ? (int) $data['submitted_count'] : 0;
$jumlah = number_format(isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0, 2);
$oleh = isset($data['pc_submitted_by']) ? trim((string) $data['pc_submitted_by']) : '';
$appended = !empty($data['appended']);
if ($appended) {
$body = sprintf(
'%s tambah %d rekod ke %s (RM %s)%s',
$nama,
$submitted,
$batchNo,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
} else {
$body = sprintf(
'%s hantar %s — %d rekod (RM %s)%s',
$nama,
$batchNo,
$submitted,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Batch pembiayaan online',
'body' => $body,
'url' => ($kodcaw && $batchId)
? '/pembiayaan-online/operasi/'.$kodcaw.'/batches/'.$batchId
: '/pembiayaan-online/operasi',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_REJECTED_PC) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'rekod';
$reason = isset($data['reject_reason']) ? trim((string) $data['reject_reason']) : '';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online ditolak',
'body' => sprintf(
'%s (RM %s) ditolak%s',
$norujukan,
$amaun,
$reason !== '' ? ' — '.$reason : ''
),
'url' => '/pembiayaan-online/pc',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_PAID_PC) {
$paidCount = isset($data['paid_count']) ? (int) $data['paid_count'] : 1;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : '';
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : '';
$nama = isset($data['nama_pelanggan']) ? trim((string) $data['nama_pelanggan']) : '';
$amaun = number_format(
isset($data['pinjaman'])
? (float) $data['pinjaman']
: (isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0),
2
);
if ($paidCount > 1 || ($batchNo !== '' && $norujukan === '')) {
$body = sprintf(
'%d rekod telah dibayar%s (RM %s)',
$paidCount > 0 ? $paidCount : 0,
$batchNo !== '' ? ' dalam '.$batchNo : '',
$amaun
);
} else {
$body = sprintf(
'%s telah dibayar (RM %s)%s',
$norujukan !== '' ? $norujukan : 'Rekod',
$amaun,
$nama !== '' ? ' — '.$nama : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online telah dibayar',
'body' => $body,
'url' => '/pembiayaan-online/pc/paid',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$teller = isset($data['kodlaluan']) ? trim((string) $data['kodlaluan']) : '';
$label = $jenis === 'serahan' ? 'serahan' : 'pendahuluan';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $jenis === 'serahan' ? 'Permohonan serahan modal' : 'Permohonan pendahuluan',
'body' => sprintf(
'%s mohon %s RM %s',
$teller !== '' ? $teller : 'Teller',
$label,
number_format($amaun, 2)
),
'url' => $jenis === 'serahan' ? '/serahan' : '/pendahuluan',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_DECIDED) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$lulus = isset($data['pengesahan']) && strcasecmp((string) $data['pengesahan'], 'Lulus') === 0;
$label = 'Pendahuluan';
if ($jenis === 'serahan') {
$label = 'Serahan';
} elseif ($jenis === 'all') {
$label = 'Aliran tunai';
}
if ($lulus) {
$title = $label.' diluluskan';
$body = sprintf('%s RM %s telah diluluskan', $label, number_format($amaun, 2));
} else {
$title = $label.' tidak diluluskan';
$body = sprintf('Permohonan %s tidak diluluskan', strtolower($label));
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $title,
'body' => $body,
'url' => '/aliran_tunai',
'data' => $data,
];
}
if ($type === self::TYPE_ANGGOTA_PENDING_OPERASI) {
$nokp = !empty($data['nokp']) ? $data['nokp'] : '';
$nama = isset($data['nama']) ? trim((string) $data['nama']) : '';
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$komen = isset($data['komen']) ? trim((string) $data['komen']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$who = $nama !== '' ? $nama : 'pelanggan';
if ($nokp !== '') {
$who .= ' ('.$nokp.')';
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan assign anggota',
'body' => sprintf(
'%s — %s%s%s',
$namaCaw,
$who,
$teller !== '' ? ' — '.$teller : '',
$komen !== '' ? ' — '.$komen : ''
),
'url' => '/operasi/kelulusan/anggota',
'data' => $data,
];
}
if ($type === self::TYPE_GADAI_KELULUSAN) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'gadai';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Kelulusan gadai melebihi RM 10,000',
'body' => sprintf(
'%s — %s (RM %s)%s',
$namaCaw,
$norujukan,
$amaun,
$teller !== '' ? ' — '.$teller : ''
),
'url' => '/kelulusan',
'data' => $data,
];
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => isset($data['title']) ? $data['title'] : $type,
'body' => isset($data['body']) ? $data['body'] : null,
'url' => isset($data['url']) ? $data['url'] : null,
'data' => $data,
];
}
/**
* @param array $data
* @return string pendahuluan|serahan|all
*/
protected function aliranTunaiJenis(array $data)
{
if (!empty($data['jenis'])) {
$jenis = strtolower(trim((string) $data['jenis']));
if ($jenis === 'serahan' || $jenis === 'pendahuluan' || $jenis === 'all') {
return $jenis;
}
}
$modal = isset($data['modal']) ? (float) $data['modal'] : 0;
$serahan = isset($data['serahan']) ? (float) $data['serahan'] : 0;
if ($serahan > 0 && $modal <= 0) {
return 'serahan';
}
return 'pendahuluan';
}
/**
* @param array $data
* @param string $jenis
* @return float
*/
protected function aliranTunaiAmaun(array $data, $jenis)
{
if (isset($data['amaun']) && is_numeric($data['amaun'])) {
return (float) $data['amaun'];
}
if ($jenis === 'serahan') {
return isset($data['serahan']) ? (float) $data['serahan'] : 0;
}
return isset($data['modal']) ? (float) $data['modal'] : 0;
}
/**
* Create one inbox row for a specific user.
*
* @param \App\User $user
* @param string|null $kodcaw
* @param string|null $role
* @param array $payload
* @return \App\StaffNotification
*/
public function notifyUser(User $user, $kodcaw, $role, array $payload)
{
return StaffNotification::create([
'user_id' => $user->id,
'cawangan' => $kodcaw,
'role' => $role ?: $user->roles,
'type' => $payload['type'],
'title' => $payload['title'],
'body' => isset($payload['body']) ? $payload['body'] : null,
'url' => isset($payload['url']) ? $payload['url'] : null,
'data' => isset($payload['data']) ? $payload['data'] : null,
]);
}
/**
* @param \App\User $user
* @param int $limit
* @return \Illuminate\Support\Collection
*/
public function inboxFor(User $user, $limit = 20)
{
return StaffNotification::query()
->where('user_id', $user->id)
->orderByDesc('id')
->limit($limit)
->get();
}
/**
* @param \App\User $user
* @return int
*/
public function unreadCount(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->count();
}
/**
* @param \App\User $user
* @param int $id
* @return \App\StaffNotification|null
*/
public function markAsRead(User $user, $id)
{
$notification = StaffNotification::query()
->where('user_id', $user->id)
->where('id', $id)
->first();
if (!$notification) {
return null;
}
if ($notification->read_at === null) {
$notification->read_at = now();
$notification->save();
}
return $notification;
}
/**
* @param \App\User $user
* @return int
*/
public function markAllAsRead(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->update(['read_at' => now()]);
}
/**
* @param \App\User $user
* @param int $limit
* @return array
*/
public function inboxPayload(User $user, $limit = 20)
{
return [
'unread_count' => $this->unreadCount($user),
'notifications' => $this->inboxFor($user, $limit)->map(function (StaffNotification $notification) {
return $notification->toInboxArray();
})->values(),
];
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class StaffNotification extends Model
{
protected $table = 'staff_notifications';
protected $fillable = [
'user_id',
'cawangan',
'role',
'type',
'title',
'body',
'url',
'data',
'read_at',
];
protected $casts = [
'data' => 'array',
'read_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function isRead()
{
return $this->read_at !== null;
}
public function scopeUnread($query)
{
return $query->whereNull('read_at');
}
public function toInboxArray()
{
return [
'id' => $this->id,
'type' => $this->type,
'title' => $this->title,
'body' => $this->body,
'url' => $this->url,
'read_at' => $this->read_at ? $this->read_at->toIso8601String() : null,
'created_at' => $this->created_at ? $this->created_at->toIso8601String() : null,
'created_at_human' => $this->created_at ? $this->created_at->diffForHumans() : null,
];
}
}
+5
View File
@@ -46,6 +46,11 @@ class User extends Authenticatable
return $this->hasOne(Cawangan::class, 'kodcaw', 'cawangan');
}
public function staffNotifications()
{
return $this->hasMany(StaffNotification::class);
}
public function canImpersonate(): bool
{
return in_array($this->roles ?? '', ['admin'], true);
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'internal_secret' => env('INTERNAL_NOTIFICATION_SECRET', ''),
];
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateStaffNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('staff_notifications', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('cawangan', 20)->nullable();
$table->string('role', 30)->nullable();
$table->string('type', 80);
$table->string('title');
$table->text('body')->nullable();
$table->string('url', 500)->nullable();
$table->text('data')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'read_at']);
$table->index(['cawangan', 'type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('staff_notifications');
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 166 KiB

+1 -1
View File
@@ -559,7 +559,7 @@
function cetaklaporanalirantunai()
{
let urlroute = '{{ route("laporan.alirantunai") }}';
window.location.href = api_url + urlroute;
window.location.href = urlroute;
}
+3
View File
@@ -115,6 +115,9 @@
</form>
</div>
<div class="card-footer text-center pt-0 px-lg-2 px-1">
<p class="mb-3 text-sm">
<a href="{{ route('dasar.privasi') }}" class="text-info font-weight-bold">Dasar Privasi</a>
</p>
<p class="mb-4 text-sm mx-auto">
Masalah untuk log masuk?
<a class="text-info text-gradient font-weight-bold">Hubungi
+24 -3
View File
@@ -718,8 +718,29 @@
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var min_komoditi = "{{ $min_komoditi }}";
console.log('test api_arrahnbid: ', api_arrahnbid);
$('#daftar_gadai').on('click', function() {
$('#daftar_gadai').on('click', async function(event) {
event.preventDefault();
if ($(this).prop('disabled')) {
return;
}
$("#daftar_gadai").prop('disabled', true);
var bakiLelong = Number(@json((float) $bakilelong));
if (bakiLelong > 0) {
var confirmation = await Swal.fire({
icon: 'warning',
title: 'Baki Lelong',
text: 'Pelanggan mempunyai baki lelong, teruskan gadai?',
showCancelButton: true,
confirmButtonText: 'Teruskan',
cancelButtonText: 'Batal',
focusCancel: true
});
if (!(confirmation.isConfirmed || confirmation.value === true)) {
$("#daftar_gadai").prop('disabled', false);
return;
}
}
var noic = $('#noic').val();
var iclama = "{{ $customer_info['kplama'] }}";
var icbaru = "{{ $customer_info['kpbaru'] }}";
@@ -2046,7 +2067,7 @@
harini.setHours(0, 0, 0, 0);
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(2)').text();
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text();
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
if (+harini !== +tarikhgadai2) {
@@ -2463,7 +2484,7 @@
}
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(2)').text();
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text();
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dasar Privasi | Kaunter arOS</title>
<link rel="icon" href="{{ asset('img/icon_pkb.png') }}">
<link href="{{ asset('css/login-v2.css') }}" rel="stylesheet">
<style>
body { background: #f4f7fb; color: #344767; }
.privacy-container { max-width: 900px; margin: 0 auto; padding: 32px 20px; }
.privacy-brand { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
.privacy-brand img { width: 60px; height: auto; }
.privacy-card { background: #fff; border-radius: 16px; padding: 40px; box-shadow: 0 8px 32px rgba(24, 49, 83, .08); }
.privacy-card h1 { font-size: 2rem; }
.privacy-card h2 { font-size: 1.2rem; margin-top: 28px; }
.privacy-card p, .privacy-card li { line-height: 1.8; }
.privacy-card ul { padding-left: 24px; }
.privacy-back { display: inline-block; margin-top: 24px; color: #075985; text-decoration: underline; }
a:focus-visible { outline: 3px solid #075985; outline-offset: 4px; }
@media (max-width: 576px) {
.privacy-container { padding: 20px 16px; }
.privacy-card { padding: 24px 20px; }
.privacy-card h1 { font-size: 1.7rem; }
}
</style>
</head>
<body>
<main class="privacy-container">
<div class="privacy-brand">
<img src="{{ asset('img/icon_pkb.png') }}" alt="Logo ar-Rahnu">
<span class="font-weight-bold">Kaunter arOS &middot; ar-rahn Online System</span>
</div>
<article class="privacy-card" aria-labelledby="privacy-title">
<h1 id="privacy-title">Dasar Privasi</h1>
<p class="text-sm text-muted">Versi dasar: 1.0 &middot; Tarikh kemas kini: <time datetime="2026-09-27">27 September 2026</time></p>
<h2>1. Pengenalan</h2>
<p>Selamat datang ke sistem Kaunter arOS Koperasi Permodalan Kelantan Berhad (KoPKB). Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan dan melindungi data peribadi anda bagi urusan Ar-Rahn, dengan mengambil kira keperluan Akta Perlindungan Data Peribadi 2010 (PDPA) dan amalan keselamatan maklumat yang berkaitan.</p>
<p>Dasar ini terpakai kepada kakitangan, pentadbir dan mana-mana pihak yang diberi kuasa untuk mengakses atau menggunakan sistem Kaunter arOS KoPKB, serta pengendalian data pelanggan dalam perkhidmatan yang diuruskan melalui sistem ini.</p>
<h2>2. Data yang Dikumpul</h2>
<p>Kami mengumpul dan memproses maklumat yang berkaitan dengan urusan sistem, termasuk:</p>
<ul>
<li><strong>Maklumat Pengenalan Peribadi:</strong> Nama penuh, nombor pengenalan, alamat, alamat e-mel dan nombor telefon pelanggan atau pengguna.</li>
<li><strong>Maklumat Akaun:</strong> Maklumat akaun pengguna, cawangan dan peranan akses dalam sistem.</li>
<li><strong>Data Teknikal:</strong> Maklumat seperti alamat IP, jenis pelayar dan log aktiviti sistem yang direkodkan bagi tujuan operasi dan keselamatan.</li>
<li><strong>Data Transaksi:</strong> Rekod pembiayaan, gadaian, maklumat marhun, pembayaran, penebusan, lelongan dan dokumen sokongan berkaitan.</li>
</ul>
<h2>3. Tujuan Penggunaan Data</h2>
<p>Data anda digunakan untuk tujuan berikut:</p>
<ul>
<li><strong>Operasi Sistem:</strong> Menguruskan akaun pengguna dan memproses urusan pelanggan serta transaksi Ar-Rahn.</li>
<li><strong>Pengesahan:</strong> Mengesahkan identiti dan mengawal akses pengguna mengikut peranan serta cawangan yang dibenarkan.</li>
<li><strong>Komunikasi:</strong> Menyampaikan notis berkaitan urusan, makluman pentadbiran dan sokongan pelanggan.</li>
<li><strong>Pelaporan dan Penambahbaikan:</strong> Menyediakan laporan operasi, menyokong audit serta menambah baik prestasi dan kualiti sistem.</li>
</ul>
<h2>4. Perkongsian &amp; Pendedahan Data</h2>
<p>Kami tidak menjual data peribadi anda. Akses, perkongsian atau pendedahan data dihadkan kepada keperluan urusan yang berkaitan, termasuk:</p>
<ul>
<li><strong>Kakitangan yang Diberi Kuasa:</strong> Kakitangan dan pentadbir KoPKB yang memerlukan maklumat untuk menjalankan tugas.</li>
<li><strong>Pembekal Perkhidmatan:</strong> Penyedia teknologi atau perkhidmatan yang menyokong pengehosan, penyelenggaraan sistem dan pemprosesan pembayaran, jika berkaitan.</li>
<li><strong>Pematuhan Undang-undang:</strong> Pihak berkuasa atau pihak lain apabila pendedahan diperlukan oleh undang-undang atau perintah mahkamah.</li>
</ul>
<h2>5. Keselamatan Data</h2>
<p>Langkah keselamatan teknikal dan pentadbiran digunakan untuk membantu melindungi data yang dikendalikan melalui sistem, termasuk kawalan akses mengikut peranan pengguna dan pengesahan akaun.</p>
<p>Pengguna hendaklah mengakses sistem melalui sambungan HTTPS yang selamat, menjaga kerahsiaan kata laluan, tidak berkongsi akaun dan log keluar selepas selesai menggunakan sistem. Sebarang aktiviti yang mencurigakan hendaklah dilaporkan kepada pentadbir sistem.</p>
<h2>6. Hak Pengguna</h2>
<p>Anda boleh menghubungi kami untuk mengemukakan permohonan berkaitan data peribadi anda, termasuk:</p>
<ul>
<li><strong>Akses &amp; Pembetulan:</strong> Memohon semakan dan pembetulan maklumat peribadi yang tidak tepat atau tidak terkini.</li>
<li><strong>Pemadaman:</strong> Memohon pemadaman akaun atau data dalam keadaan yang sesuai, tertakluk kepada keperluan penyimpanan rekod transaksi, audit dan undang-undang.</li>
<li><strong>Sekatan:</strong> Memohon supaya pemprosesan data tertentu dihadkan, tertakluk kepada keperluan perkhidmatan dan kewajipan yang terpakai.</li>
</ul>
<p>Permohonan akan dinilai mengikut keperluan yang terpakai. Pengesahan identiti mungkin diperlukan sebelum permohonan diproses.</p>
<h2>7. Tempoh Penyimpanan</h2>
<p>Data disimpan selama diperlukan untuk pengendalian urusan Ar-Rahn, penyelenggaraan rekod pembiayaan dan transaksi, audit serta pematuhan undang-undang. Selepas keperluan penyimpanan berakhir, data akan diuruskan untuk pemadaman atau penyahkenalpastian mengikut prosedur organisasi yang terpakai.</p>
<h2>8. Kuki &amp; Teknologi Penjejakan</h2>
<p>Sistem Kaunter arOS menggunakan kuki (cookies) dan sesi untuk menyokong log masuk, mengekalkan sesi pengguna dan melindungi permintaan dalam sistem. Anda boleh menguruskan pilihan kuki melalui tetapan pelayar. Menyekat kuki boleh menyebabkan fungsi tertentu, termasuk log masuk, tidak berfungsi dengan baik.</p>
<h2>9. Perubahan Dasar</h2>
<p>Dasar ini boleh dikemas kini dari semasa ke semasa apabila terdapat perubahan pada perkhidmatan, pengendalian sistem atau keperluan organisasi. Versi terkini akan dipaparkan pada halaman ini. Sebarang perubahan besar akan dimaklumkan melalui e-mel atau notis dalam sistem sebelum tarikh kuat kuasa baharu.</p>
<h2>10. Hubungi Kami</h2>
<p>Jika anda mempunyai pertanyaan, komen atau permohonan berkaitan dasar ini dan pengendalian data peribadi dalam sistem Kaunter arOS, sila hubungi:</p>
<p><strong>AFRINA ALIMI</strong><br>
Koperasi Permodalan Kelantan Berhad (KoPKB)<br>
E-mel: <a href="mailto:afrina@koppkb.com">afrina@koppkb.com</a>
</p>
<a class="privacy-back" href="{{ route('login') }}">Kembali ke Log Masuk</a>
</article>
</main>
</body>
</html>
@@ -204,7 +204,7 @@
/>
<div class="title">
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Online (Akaun Bank)</h1>
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Atas Talian (Online)</h1>
<p>Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.</p>
</div>
+81 -7
View File
@@ -246,9 +246,8 @@
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="cara_terima" id="cara_terima_online" value="ONLINE">
<label class="form-check-label" for="cara_terima_online">Online (Akaun Bank)</label>
<label class="form-check-label" for="cara_terima_online">Atas Talian (Online)</label>
</div>
<small class="form-text text-muted">Pilihan Online untuk pembiayaan RM5,000 – RM50,000 sahaja.</small>
</div>
</div>
<div id="bank_online_section" style="display:none;">
@@ -306,6 +305,9 @@
<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">
<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>
</form>
@@ -412,6 +414,9 @@
<script>
var api_url = "{{ api_url_for_browser() }}";
var kodcaw_ujrah = "<?php echo $cawangan_info['kodcaw'] ?>";
const MIN_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MIN_PINJAMAN }};
const MAX_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MAX_PINJAMAN }};
var printedBorang = @json($borang_online_printed ?? null);
$('form input').keydown(function (e) {
if (e.keyCode == 13) {
e.preventDefault();
@@ -423,6 +428,13 @@
$(document).on("keydown", disableF5);
var norujukan = $('#norujukan').val();
getMarhun(norujukan);
@if(session('error'))
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: @json(session('error'))
});
@endif
$.ajax({
method:'GET',
@@ -1300,10 +1312,54 @@
.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) {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
// MIN PINJAMAN = 1.00
if (pembiayaanOnlineEnabled && pinjaman >= 1 && pinjaman <= 50000) {
if (pembiayaanOnlineEnabled && pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN) {
$('#cara_terima_section').show();
toggleBankOnlineSection();
} else {
@@ -1311,6 +1367,7 @@
$('#bank_online_section').hide();
$('#cetak_borang_online').hide();
$('#cara_terima_tunai').prop('checked', true);
syncTerimaButton();
}
}
@@ -1325,16 +1382,20 @@
$('#bank_online_section').hide();
$('#cetak_borang_online').hide();
}
syncTerimaButton();
}
$(document).on('change', 'input[name="cara_terima"]', function () {
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') || '');
syncTerimaButton();
});
syncTerimaButton();
$(document).on('click', '#cetak_borang_online', function () {
var kodbank = $('#gadai_kodbank').val();
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: 'nama_akaun', value: namaAkaun }));
form.appendTo('body').trigger('submit').remove();
printedBorang = currentBorangFields();
syncTerimaButton();
});
async function getModalOnline() {
@@ -1416,7 +1480,7 @@
async function checkDisbursementFloat(pinjaman) {
var cara = $('input[name="cara_terima"]:checked').val() || 'TUNAI';
if (!(pinjaman >= 5000 && pinjaman <= 50000)) {
if (!(pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN)) {
cara = 'TUNAI';
}
@@ -1523,7 +1587,7 @@
let bakiakhir = 0;
var kodcaw = $('#kodcaw').val();
if(pinjaman == 0){
$("#terima_gadai").prop('disabled', false);
syncTerimaButton();
Swal.fire({
icon: 'error',
title: 'Amaran!',
@@ -1532,6 +1596,16 @@
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);
if (!floatOk) {
$("#terima_gadai").prop('disabled', false);
@@ -219,78 +219,70 @@
<div class="card-body text-center">
<div class="row form-group justify-content-center">
<div class="col-3 mx-1">
<div class="row">
<a href="{{ url('dashboard') }}" disabled
class="btn btn-info btn-lg btn-block mb-3">Dashboard</a>
</div>
<div class="row">
<a href="{{ url('kakitangan') }}"
class="btn btn-info btn-lg btn-block mb-3">Kakitangan</a>
</div>
<div class="row">
<a href="{{ url('kelulusan') }}" id="btnkelulusan"
class="btn btn-info btn-lg btn-block mb-3">Kelulusan</a>
</div>
{{-- <div class="row">
<a href="{{ url('permohonan') }}" class="btn btn-danger btn-lg btn-block mb-3">Permohonan</a>
</div> --}}
<div class="row">
<a href="#" data-target="#denominasiShowModal" data-backdrop="static" data-keyboard="false"
data-toggle="modal" class="btn btn-info btn-lg btn-block mb-3">Denominasi</a>
<div class="col-4 px-2">
<a href="{{ url('dashboard') }}" disabled
class="btn btn-info btn-lg btn-block mb-3">Dashboard</a>
</div>
<div class="row">
<a href="{{ route('khazanah.harga_emas') }}"
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
</div>
</div>
<div class="col-3 mx-1">
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('khazanah.pelanggan') }}"
class="btn btn-info btn-lg btn-block mb-3">Pelanggan</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('lelong') }}" class="btn btn-info btn-lg btn-block mb-3">Lelong</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('kakitangan') }}"
class="btn btn-info btn-lg btn-block mb-3">Kakitangan</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('laporan') }}" class="btn btn-info btn-lg btn-block mb-3">Laporan</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ url('panjar') }}" class="btn btn-info btn-lg btn-block mb-3">Perbelanjaan</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('kelulusan') }}" id="btnkelulusan"
class="btn btn-info btn-lg btn-block mb-3">Kelulusan</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('menu') }}"
id="btn-khazanah"
class="btn {{ $countkhazanah > 0 ? 'btn-danger' : 'btn-info' }} btn-lg btn-block mb-3">Khazanah</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ url('alirantunai') }}" class="btn btn-info btn-lg btn-block mb-3">Aliran
Tunai</a>
</div>
<div class="col-4 px-2">
<a href="#" data-target="#denominasiShowModal" data-backdrop="static" data-keyboard="false"
data-toggle="modal" class="btn btn-info btn-lg btn-block mb-3">Denominasi</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('pendahuluanserahan') }}"
class="btn btn-info btn-lg btn-block mb-3">Pendahuluan - Serahan</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('khazanah.bajet') }}" class="btn btn-info btn-lg btn-block mb-3">Bajet</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('khazanah.harga_emas') }}"
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']))
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('pembiayaan.online.pc') }}"
class="btn btn-info btn-lg btn-block mb-3">Pembiayaan Online</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('pembiayaan.online.pc.topup') }}"
class="btn btn-info btn-lg btn-block mb-3">Had / Modal Online</a>
</div>
@endif
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('khazanah.indexwakil') }}"
class="btn btn-info btn-lg btn-block mb-3">Surat Wakil</a>
</div>
</div>
<div class="col-3 mx-1">
{{-- <div class="row">
<a href="#" class="btn btn-danger btn-lg btn-block mb-3">Capaian</a>
</div> --}}
<div class="row">
<a href="{{ route('lelong') }}" class="btn btn-info btn-lg btn-block mb-3">Lelong</a>
</div>
<div class="row">
<a href="{{ url('panjar') }}" class="btn btn-info btn-lg btn-block mb-3">Perbelanjaan</a>
</div>
<div class="row">
<a href="{{ url('alirantunai') }}" class="btn btn-info btn-lg btn-block mb-3">Aliran
Tunai</a>
</div>
<div class="row">
<a href="{{ route('khazanah.bajet') }}" class="btn btn-info btn-lg btn-block mb-3">Bajet</a>
</div>
</div>
</div>
</div>
+69 -9
View File
@@ -185,6 +185,7 @@
<th>Keuntungan</th>
<th>Lelong</th>
<th>Pembayaran Terakhir</th>
<th>Tarikh Ansuran Terkini</th>
</tr>
</thead>
</table>
@@ -204,6 +205,7 @@
<th>Keuntungan</th>
<th>Lelong</th>
<th>Pembayaran Terakhir</th>
<th>Tarikh Ansuran Terkini</th>
</tr>
</thead>
</table>
@@ -634,6 +636,45 @@
};
}
function tarikhAnsuranTerkini(transactions)
{
if (!Array.isArray(transactions)) {
throw new Error('Invalid transaction response');
}
return transactions.reduce(function(latest, transaction) {
if (transaction.trans_type !== 'A') return latest;
var date = String(transaction.created_at || '').slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return latest;
return date > latest ? date : latest;
}, '') || 'Tiada bayaran';
}
function muatTarikhAnsuranTerkini(row, norujukan)
{
var originalData = row.data();
function update(value) {
// Ignore responses for rows removed when the customer/table changes.
if (row.data() !== originalData) return;
var updatedData = originalData.slice();
updatedData[11] = value;
row.data(updatedData).draw(false);
}
$.ajax({
method: 'get',
url: api_url + '/api/transaction/' + encodeURIComponent(norujukan),
success: function(transactions) {
try {
update(tarikhAnsuranTerkini(transactions));
} catch (error) {
update('Gagal dimuatkan');
}
},
error: function() {
update('Gagal dimuatkan');
}
});
}
function gadaiTable(data,jenistebus){
var counter = 1;
@@ -665,7 +706,15 @@
var taglel = '';
if(gadai_detail['taglel'] == 1){
if(tagarrahnbid == 1){
taglel = gadai_detail['t_lelong'];
$.ajax({
method: "GET",
async: false,
url: api_arrahnbid + 'portalApi',
data: { norujukan: gadai_detail['norujukan'] },
success: function(data) {
taglel = (data && data.lelong && data.lelong.t_lelong) || '';
}
});
} else {
$.ajax({
method:"GET",
@@ -685,8 +734,8 @@
let table_norujukan = gadai_detail["norujukan"];
// let radio_action = "onclick=checkCanChangeGadaianDetail(" + "'" + table_norujukan + "'" + ")";
if(gadai_detail["percentpinj"] != 0)
xtawarruq.row.add([
if(gadai_detail["percentpinj"] != 0) {
var ansuranRow = xtawarruq.row.add([
'<input type="radio" value='+ gadai_detail["norujukan"] + ' name="belum"/>',
// '<a href="#" onclick="checkserah(this)" data-pinjaman='+ gadai_detail['pinjaman'] +' data-hargajualan='+ gadai_detail['hargajualan'] +' data-norujukan='+ gadai_detail['norujukan'] +' data-url=' + urlroute +'>' + gadai_detail['norujukan'] + '</a>',
(function() {
@@ -716,15 +765,24 @@
let color = bayaranInfo.blocked ? 'red' : 'green';
return '<div style="white-space: nowrap; color: ' + color + '; font-weight: bold;">' +
bayaranInfo.tarikh + '</div>';
})()
})(),
'Memuatkan...'
]).draw();
muatTarikhAnsuranTerkini(ansuranRow, gadai_detail['norujukan']);
}
}else{
var taglel = '';
if(gadai_detail['taglel'] == 1){
if(tagarrahnbid == 1){
if(gadai_detail['t_lelong'] != null) {
taglel = gadai_detail['t_lelong'];
}
$.ajax({
method: "GET",
async: false,
url: api_arrahnbid + 'portalApi',
data: { norujukan: gadai_detail['norujukan'] },
success: function(data) {
taglel = (data && data.lelong && data.lelong.t_lelong) || '';
}
});
} else {
$.ajax({
method:"GET",
@@ -743,7 +801,7 @@
let table_norujukan = gadai_detail["norujukan"];
// let radio_action = "onclick=checkCanChangeGadaianDetail(" + "'" + table_norujukan + "'" + ")";
tablegadai.row.add([
var ansuranRow = tablegadai.row.add([
'<input type="radio" value='+ gadai_detail["norujukan"] + ' name="belum"/>',
// '<a href="#" onclick="checkserah(this)" data-pinjaman='+ gadai_detail['pinjaman'] +' data-hargajualan='+ gadai_detail['hargajualan'] +' data-norujukan='+ gadai_detail['norujukan'] +' data-url=' + urlroute +'>' + gadai_detail['norujukan'] + '</a>',
(function() {
@@ -773,8 +831,10 @@
let color = bayaranInfo.blocked ? 'red' : 'green';
return '<div style="white-space: nowrap; color: ' + color + '; font-weight: bold;">' +
bayaranInfo.tarikh + '</div>';
})()
})(),
'Memuatkan...'
]).draw();
muatTarikhAnsuranTerkini(ansuranRow, gadai_detail['norujukan']);
}
}else if(jenistebus == "sudah_tebus"){
+9 -75
View File
@@ -214,14 +214,16 @@
v-pre
style="color: {{ isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan 2' ? 'black' : '#1abc9c' }};">
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
<div id="divbiasa" class="d-none">
<a class="dropdown-item" id="linkz" href="#">
Tiada Data
</a>
</div>
@include('layouts.partials.notification_inbox')
</div>
</li>
@@ -281,7 +283,7 @@
});
</script>
@elseif($auth_user['role_harian'] == 'teller')
@elseif($auth_user['role_harian'] == 'teller' || $auth_user['roles'] == 'teller')
<li class="nav-item dropdown" style="margin-right:10px;">
<a id="navbarDropdown"
class="nav-link"
@@ -293,60 +295,13 @@
v-pre
style="color: {{ isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan' ? 'white' : (isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan 2' ? 'black' : '#1abc9c') }};">
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<a class="dropdown-item" id="gadailinkz" href="#">
Tiada Data
</a>
</div>
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
@include('layouts.partials.notification_inbox')
</div>
</li>
{{-- <script>
// Enable pusher logging - don't include this in production
// Pusher.logToConsole = true;
var pusher = new Pusher("{{ env('PUSHER_APP_KEY') }}", {
cluster: 'ap1',
encrypted: true
});
var channel = pusher.subscribe('notifikasi-pembayaran');
channel.bind('trigger-pembayaran', function(data) {
let message = JSON.stringify(data);
$('#bell').css("color", "red");
$('#gadailinkz').text("Khazanah Serah Marhun Teller");
// $("#gadailinkz").attr("href", "{{ url('kelulusan') }}");
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000,
timerProgressBar: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
}
});
Toast.fire({
icon: 'error',
title: 'Notifikasi Pembayaran!'
});
});
$("#navbarDropdown").click(function(){
$('#bell').css("color", "grey");
});
</script> --}}
@endif
@@ -446,30 +401,9 @@
</div>
@else
@endif
@include('layouts.partials.batch_cutoff_banner')
@endif
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@yield('content')
</main>
@@ -164,29 +164,6 @@
</div>
</nav>
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@yield('content')
</main>
+5 -25
View File
@@ -111,14 +111,17 @@
<li class="nav-item dropdown" style="margin-right:10px;">
<a id="navbarDropdown" class="nav-link " href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
<div id="divbiasa" class="d-none">
<a class="dropdown-item" id="gadailinkz" href="#">
Tiada Notifikasi
</a>
<a class="dropdown-item" id="linkz" href="#"></a>
</div>
@include('layouts.partials.notification_inbox')
</div>
</li>
@@ -254,29 +257,6 @@
</div>
</nav>
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@yield('content')
</main>
@@ -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>
@@ -0,0 +1,244 @@
<style>
.staff-notification-menu {
min-width: 340px;
max-width: 380px;
padding: 0;
}
.staff-notification-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid #eee;
font-weight: 600;
font-size: 13px;
}
.staff-notification-header button {
border: 0;
background: none;
color: #1abc9c;
font-size: 12px;
padding: 0;
cursor: pointer;
}
.staff-notification-header button:disabled {
color: #bbb;
cursor: default;
}
.staff-notification-list {
max-height: 360px;
overflow-y: auto;
}
.staff-notification-item {
display: block;
padding: 10px 14px;
border-bottom: 1px solid #f3f3f3;
white-space: normal;
color: #333;
text-decoration: none;
}
.staff-notification-item:hover {
background: #f8f9fa;
text-decoration: none;
color: #333;
}
.staff-notification-item.is-unread {
background: #eafaf6;
}
.staff-notification-item .sn-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
}
.staff-notification-item.is-unread .sn-title {
font-weight: 700;
}
.staff-notification-item .sn-body,
.staff-notification-item .sn-time {
font-size: 12px;
color: #666;
margin: 0;
}
.staff-notification-empty {
padding: 18px 14px;
text-align: center;
color: #888;
font-size: 13px;
}
.staff-notification-badge {
position: absolute;
top: 2px;
right: 0;
font-size: 10px;
min-width: 16px;
padding: 2px 5px;
border-radius: 10px;
background: #e74c3c;
color: #fff;
line-height: 1.2;
display: none;
}
.staff-notification-toggle {
position: relative;
display: inline-block;
}
a.nav-link #staff-notification-badge {
position: absolute;
top: 2px;
right: -4px;
}
li.nav-item.dropdown > a.nav-link {
position: relative;
}
</style>
<div id="staff-notification-inbox">
<div class="staff-notification-header">
<span>Notifikasi</span>
<button type="button" id="staff-notification-mark-all" disabled>Tanda semua dibaca</button>
</div>
<div id="staff-notification-list" class="staff-notification-list">
<div class="staff-notification-empty">Memuatkan...</div>
</div>
</div>
<script>
(function () {
var listUrl = @json(route('notifications.index'));
var markAllUrl = @json(route('notifications.read.all'));
var markOneUrlTemplate = @json(url('/notifications/__ID__/read'));
var csrf = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrf ? csrf.getAttribute('content') : '';
function markOneUrl(id) {
return markOneUrlTemplate.replace('__ID__', id);
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function setBadge(count) {
var badge = document.getElementById('staff-notification-badge');
var bell = document.getElementById('bell');
if (!badge) {
return;
}
if (count > 0) {
badge.style.display = 'inline-block';
badge.textContent = count > 99 ? '99+' : String(count);
if (bell) {
bell.style.color = '#e74c3c';
}
} else {
badge.style.display = 'none';
badge.textContent = '';
if (bell) {
bell.style.color = '';
}
}
var markAll = document.getElementById('staff-notification-mark-all');
if (markAll) {
markAll.disabled = count < 1;
}
}
function render(payload) {
var wrap = document.getElementById('staff-notification-list');
if (!wrap) {
return;
}
var items = payload.notifications || [];
setBadge(payload.unread_count || 0);
if (!items.length) {
wrap.innerHTML = '<div class="staff-notification-empty">Tiada notifikasi</div>';
return;
}
wrap.innerHTML = items.map(function (item) {
var unread = !item.read_at;
return (
'<a class="dropdown-item staff-notification-item' + (unread ? ' is-unread' : '') + '" href="' + escapeHtml(item.url || '#') + '" data-id="' + item.id + '">' +
'<div class="sn-title">' + escapeHtml(item.title) + '</div>' +
(item.body ? '<p class="sn-body">' + escapeHtml(item.body) + '</p>' : '') +
'<p class="sn-time">' + escapeHtml(item.created_at_human || '') + '</p>' +
'</a>'
);
}).join('');
}
function loadInbox() {
return fetch(listUrl, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin'
}).then(function (res) {
if (!res.ok) {
throw new Error('Gagal muat notifikasi');
}
return res.json();
}).then(render).catch(function () {
var wrap = document.getElementById('staff-notification-list');
if (wrap) {
wrap.innerHTML = '<div class="staff-notification-empty">Tidak dapat memuatkan notifikasi</div>';
}
});
}
function postJson(url) {
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken
},
credentials: 'same-origin',
body: '{}'
}).then(function (res) {
if (!res.ok) {
throw new Error('Gagal kemaskini notifikasi');
}
return res.json();
});
}
document.addEventListener('click', function (event) {
var target = event.target;
if (target && target.nodeType !== 1) {
target = target.parentElement;
}
if (!target || !target.closest) {
return;
}
var item = target.closest('.staff-notification-item');
if (!item) {
return;
}
var id = item.getAttribute('data-id');
if (!id) {
return;
}
postJson(markOneUrl(id)).catch(function () {});
});
var markAll = document.getElementById('staff-notification-mark-all');
if (markAll) {
markAll.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
postJson(markAllUrl).then(function () {
loadInbox();
}).catch(function () {});
});
}
loadInbox();
setInterval(loadInbox, 30000);
})();
</script>
@@ -13,56 +13,95 @@
</ol>
</nav>
<div class="card-body text-center">
<div class="row mb-4">
<div class="col-2 justify-content-start">
<div class="card-body">
<div class="row mb-3 align-items-end">
<div class="col-md-3">
<label for="date_from">Tarikh Dari</label>
<input type="date" id="date_from" class="form-control" value="{{ $dateFrom }}">
</div>
<div class="col-8 mt-3 justify-content-center">
<div class="col-md-3">
<label for="date_to">Tarikh Hingga</label>
<input type="date" id="date_to" class="form-control" value="{{ $dateTo }}">
</div>
<div class="col-2">
<div class="col-md-3">
<button type="button" id="btn-filter-rekod" class="btn btn-primary mt-3 mt-md-0">Cari</button>
</div>
</div>
<table class='table table-striped table-hover' id='table-kelulusan' style="width:100%;">
<thead class="thead-dark">
<tr>
<th>No Rujukan</th>
<th>Status</th>
<th>Komen</th>
<th>Nilai Pembiayaan</th>
<th>Nilai Marhun</th>
<th>Berat</th>
<th>Margin</th>
<th>Pelulus</th>
</tr>
</thead>
<tbody>
@foreach($arraykelulusan as $index=>$k)
<tr>
<td>{{ $k['norujukan'] }}</td>
<td>{{ $k['status'] }}</td>
<td>{{ $k['komen'] }}</td>
<td>{{ $k['pinjaman'] }}</td>
<td>{{ $k['nilaimarhun'] }}</td>
<td>{{ $k['berat'] }}</td>
<td>{{ $k['margin'] }}</td>
<td>{{ $k['pelulus'] }}</td>
</tr>
@endforeach
</tbody>
</table>
<x-datatable
id="table-kelulusan"
filename="Rekod_Lulus_Operasi"
class="text-center"
:columns="['Tarikh', 'No Rujukan', 'Status', 'Komen', 'Nilai Pembiayaan', 'Nilai Marhun', 'Berat', 'Margin', 'Pelulus']"
/>
</div>
</div>
</div>
</div>
</div>
<script>
function escapeHtml(value) {
return $('<div>').text(value == null ? '' : String(value)).html();
}
var tablekelulusan = $('#table-kelulusan').DataTable({
"scrollX": true
var tablekelulusan = initErahnDatatable('#table-kelulusan', {
deferRender: true,
order: [[0, 'desc']],
language: {
emptyTable: 'Tiada rekod',
zeroRecords: 'Tiada rekod dijumpai'
}
});
function setLoading(isLoading) {
$('#btn-filter-rekod').prop('disabled', isLoading).text(isLoading ? 'Memuatkan...' : 'Cari');
}
function loadRekodLulus() {
var dateFrom = $('#date_from').val();
var dateTo = $('#date_to').val();
setLoading(true);
tablekelulusan.clear().draw();
$.ajax({
url: "{{ route('operasi.rekodlepas.data') }}",
method: 'GET',
data: {
date_from: dateFrom,
date_to: dateTo
},
success: function (response) {
var rows = response.data || [];
var data = rows.map(function (row) {
return [
escapeHtml(row.tarikh || '-'),
escapeHtml(row.norujukan || '-'),
escapeHtml(row.status || '-'),
escapeHtml(row.komen || '-'),
escapeHtml(row.pinjaman || '-'),
escapeHtml(row.nilaimarhun || '-'),
escapeHtml(row.berat || '-'),
escapeHtml(row.margin || '-'),
escapeHtml(row.pelulus || '-')
];
});
tablekelulusan.rows.add(data).draw();
},
error: function () {
Swal.fire({
icon: 'error',
title: 'Ralat',
text: 'Gagal memuatkan rekod lulus.'
});
},
complete: function () {
setLoading(false);
}
});
}
$('#btn-filter-rekod').on('click', loadRekodLulus);
loadRekodLulus();
</script>
@endsection
@@ -54,15 +54,17 @@
@if($pendingCount > 0)
<form method="post"
action="{{ route('pembiayaan.online.operasi.approve.batch', ['kodcaw' => $kodcaw, 'batchId' => $batch['id']]) }}"
class="d-inline"
onsubmit="return confirm('Luluskan semua {{ $pendingCount }} rekod menunggu dalam batch ini?');">
class="d-inline js-swal-confirm"
data-title="Lulus Semua Menunggu?"
data-html="Luluskan semua <b>{{ $pendingCount }}</b> rekod menunggu dalam batch <b>{{ $batch['batch_no'] ?? '-' }}</b>?"
data-confirm="Ya, Lulus Semua">
@csrf
<button type="submit" class="btn btn-success btn-sm">Lulus Semua Menunggu ({{ $pendingCount }})</button>
</form>
@endif
@if($approvedCount > 0)
<button type="button" class="btn btn-dark btn-sm" data-toggle="modal" data-target="#paidBatchModal">
Tanda PAID Batch ({{ $approvedCount }})
Tanda semua sebagai dibayar ({{ $approvedCount }})
</button>
@endif
</div>
@@ -116,8 +118,10 @@
@if(in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true))
<form method="post"
action="{{ route('pembiayaan.online.operasi.approve', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
class="d-inline"
onsubmit="return confirm('Luluskan rekod {{ $line['norujukan'] ?? '' }}?');">
class="d-inline js-swal-confirm"
data-title="Luluskan Rekod?"
data-html="Luluskan rekod <b>{{ $line['norujukan'] ?? '-' }}</b>?"
data-confirm="Ya, Lulus">
@csrf
<input type="hidden" name="batch_id" value="{{ $batch['id'] }}">
<button type="submit" class="btn btn-success btn-sm">Lulus</button>
@@ -133,7 +137,7 @@
class="btn btn-dark btn-sm btn-paid"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
PAID + Bukti
Tanda sebagai dibayar
</button>
<button type="button"
class="btn btn-outline-danger btn-sm btn-reject"
@@ -161,11 +165,11 @@
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content">
<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>
</div>
<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>
</div>
<div class="modal-footer">
@@ -211,7 +215,7 @@
<input type="hidden" name="batch_id" value="{{ $batch['id'] }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tanda PAID — <span id="paidNorujukan"></span></h5>
<h5 class="modal-title">Tanda sebagai dibayar — <span id="paidNorujukan"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
@@ -226,12 +230,12 @@
<div class="form-group">
<label>Bukti Bayaran <span class="text-danger">*</span></label>
<input type="file" name="bukti" class="form-control-file" accept=".pdf,.jpg,.jpeg,.png,.gif" required>
<small class="text-muted">PDF / imej slip pindahan bank</small>
<small class="text-muted">PDF / imej slip bank</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-dark">Simpan PAID</button>
<button type="submit" class="btn btn-dark">Simpan</button>
</div>
</div>
</form>
@@ -247,7 +251,7 @@
@csrf
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tanda PAID Semua APPROVED ({{ $approvedCount }})</h5>
<h5 class="modal-title">Tanda semua sebagai dibayar ({{ $approvedCount }})</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
@@ -267,7 +271,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-dark">Simpan PAID Batch</button>
<button type="submit" class="btn btn-dark">Simpan</button>
</div>
</div>
</form>
@@ -316,5 +320,25 @@
$('#paidNorujukan').text($(this).data('norujukan'));
$('#paidModal').modal('show');
});
$(document).on('submit', '.js-swal-confirm', function (e) {
e.preventDefault();
var form = this;
Swal.fire({
title: $(form).attr('data-title') || 'Pengesahan',
html: $(form).attr('data-html') || 'Teruskan tindakan ini?',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: $(form).attr('data-confirm') || 'Ya',
cancelButtonText: 'Batal',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
form.submit();
}
});
});
</script>
@endsection
@@ -85,11 +85,11 @@
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content">
<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>
</div>
<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>
</div>
<div class="modal-footer">
@@ -69,13 +69,13 @@
<div class="card-header d-flex justify-content-between align-items-center flex-wrap">
<h5 class="mb-0">Senarai Pembiayaan Online — PAID</h5>
<button type="button" class="btn btn-success btn-sm mt-2 mt-md-0" id="btnMarkBlast" disabled>
Sahkan Tally / Selesai
Sahkan semua sebagai dibayar
</button>
</div>
<div class="card-body">
<p class="small text-muted">
Semak jumlah &amp; bukti bayaran sepadan dengan pelanggan. Setelah semak, tanda selesai.
(WhatsApp/SMS blast boleh disambung kemudian — butang ini menanda rekod sebagai selesai semak.)
WhatsApp notis (template Cloud API) akan dihantar selepas tally. Di persekitaran ujian ia pergi ke nombor admin, bukan pelanggan.
</p>
<form id="formMarkBlast" method="post" action="{{ route('pembiayaan.online.pc.blast') }}">
@csrf
@@ -168,7 +168,7 @@
function syncBlastButton() {
var n = $('.row-check:checked').length;
$('#btnMarkBlast').prop('disabled', n === 0);
$('#btnMarkBlast').text(n ? ('Sahkan Tally / Selesai (' + n + ')') : 'Sahkan Tally / Selesai');
$('#btnMarkBlast').text(n ? ('Sahkan semua sebagai dibayar (' + n + ')') : 'Sahkan semua sebagai dibayar');
}
$('#checkAll').on('change', function () {
@@ -180,7 +180,7 @@
$('#btnMarkBlast').on('click', function () {
if ($('.row-check:checked').length === 0) return;
if (!confirm('Sahkan tally untuk rekod dipilih? Rekod akan ditanda selesai semak/blast.')) return;
if (!confirm('Sahkan semua sebagai dibayar untuk rekod dipilih? Rekod akan ditanda sebagai dibayar dan WhatsApp notis akan dihantar.')) return;
$('#formMarkBlast').submit();
});
</script>
@@ -36,6 +36,17 @@
$lineRows = collect($lines);
$verifiedCount = $lineRows->where('status', 'VERIFIED_PC')->count();
$unverifiedCount = $lineRows->count() - $verifiedCount;
$slotArr = is_array($slot) ? $slot : [];
$slotName = $slotArr['slot'] ?? 'BATCH1';
$slotCutoff = $slotArr['cutoff'] ?? '11:00';
$slotProses = $slotArr['proses'] ?? '12:30';
$slotLewat = !empty($slotArr['lewat']);
$slotJadual = $slotArr['jadual'] ?? [
['slot' => 'BATCH1', 'label' => 'Batch 1', 'mula' => '00:00', 'cutoff' => '11:00', 'proses' => '12:30'],
['slot' => 'BATCH2', 'label' => 'Batch 2', 'mula' => '11:00', 'cutoff' => '15:00', 'proses' => '16:30'],
];
$slotBakiMinit = (int) ($slotArr['baki_minit'] ?? 0);
$slotHampirTamat = !$slotLewat && $slotBakiMinit > 0 && $slotBakiMinit <= 30;
@endphp
@if($isClosed)
@@ -59,6 +70,66 @@
</div>
@endif
<div class="alert {{ $slotLewat ? 'alert-warning' : ($slotHampirTamat ? 'alert-warning' : 'alert-info') }} mb-3">
<div class="d-flex justify-content-between align-items-start flex-wrap">
<div class="mb-2 mr-3">
<strong>Jadual Slot Batch</strong>
<div class="mt-1">
PC / PPC sila semak dan <b>hantar senarai sebelum tamat slot</b>.
Rekod yang dihantar selepas cutoff mungkin diproses pada slot atau hari berikutnya.
</div>
</div>
<div class="mb-2 text-md-right">
@if($slotLewat)
Slot semasa: <b>{{ $slotName }}</b> (selepas cutoff {{ $slotCutoff }})
@else
Slot semasa: <b>{{ $slotName }}</b>
&middot; Hantar sebelum <b>{{ $slotCutoff }}</b>
<div class="small" id="slotCountdown"
data-cutoff="{{ $slotCutoff }}"
data-lewat="{{ $slotLewat ? '1' : '0' }}">
@if($slotBakiMinit > 0)
Baki masa: {{ intdiv($slotBakiMinit, 60) }}j {{ $slotBakiMinit % 60 }}m
@endif
</div>
@endif
</div>
</div>
<div class="table-responsive mb-0">
<table class="table table-sm table-bordered mb-0 bg-white">
<thead class="thead-light">
<tr>
<th>Slot</th>
<th>Hantar senarai sebelum</th>
<th>Proses Operasi</th>
</tr>
</thead>
<tbody>
@foreach($slotJadual as $row)
@php
$rowSlot = $row['slot'] ?? '';
$isCurrent = $rowSlot === $slotName && !$slotLewat;
$isDone = $slotLewat
|| ($slotName === 'BATCH2' && $rowSlot === 'BATCH1');
@endphp
<tr class="{{ $isCurrent ? 'table-primary' : '' }}">
<td>
{{ $row['label'] ?? $rowSlot }}
@if($isCurrent)
<span class="badge badge-primary">Semasa</span>
@elseif($isDone)
<span class="badge badge-secondary">Tamat</span>
@endif
</td>
<td>{{ $row['cutoff'] ?? '-' }}</td>
<td>~ {{ $row['proses'] ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="card">
@@ -85,7 +156,7 @@
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<div class="text-muted">Queue PC</div>
<div class="text-muted">Senarai Pelanggan menunggu semakan</div>
<h4 class="mb-0">{{ count($lines) }} rekod</h4>
<small>
{{ $verifiedCount }} sudah sah
@@ -96,11 +167,19 @@
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card {{ $slotLewat || $slotHampirTamat ? 'border-warning' : '' }}">
<div class="card-body text-center">
<div class="text-muted">Slot Cadangan</div>
<h4 class="mb-0">{{ $slot['slot'] ?? '-' }}</h4>
<small>{{ $slot['nota'] ?? '' }}</small>
<div class="text-muted">Slot Semasa</div>
<h4 class="mb-0">{{ $slotName }}</h4>
<small>
@if($slotLewat)
Cutoff {{ $slotCutoff }} sudah tamat<br>
Masih boleh hantar; proses mungkin esok
@else
Hantar sebelum <b>{{ $slotCutoff }}</b>
&middot; Proses ~{{ $slotProses }}
@endif
</small>
</div>
</div>
</div>
@@ -115,10 +194,15 @@
<p class="small text-muted">
Semak setiap rekod <b>satu-satu</b> (maklumat pelanggan, bank, amaun).
Hanya rekod berstatus <span class="badge badge-success">SAH</span> boleh dipilih dan dihantar ke Operasi.
@if($slotLewat)
Cutoff {{ $slotName }} ({{ $slotCutoff }}) sudah tamat — hantar sekarang jika perlu; Operasi mungkin proses esok.
@else
Sila hantar senarai <b>{{ $slotName }}</b> sebelum <b>{{ $slotCutoff }}</b>.
@endif
</p>
<form id="formSubmitBatch" method="post" action="{{ route('pembiayaan.online.pc.submit') }}">
@csrf
<input type="hidden" name="slot" value="{{ $slot['slot'] ?? 'BATCH1' }}">
<input type="hidden" name="slot" value="{{ $slotName }}">
<div class="table-responsive">
<table class="table table-striped table-hover table-sm text-center" id="queueTable">
<thead class="thead-dark">
@@ -328,8 +412,8 @@
<p class="small text-muted mb-0">Jika rekod sudah SAH, simpan akan batal semakan. Sila semak semula sebelum hantar.</p>
</div>
<div class="col-md-8">
<label class="d-block">Pratinjau SAG</label>
<iframe id="editSagFrame" title="Pratinjau SAG" src="about:blank"
<label class="d-block">Pratonton SAG</label>
<iframe id="editSagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
</div>
@@ -402,12 +486,12 @@
</label>
</div>
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
Lihat Slip Bank
Lihat Bukti Akaun Bank
</a>
</div>
<div class="col-md-8">
<label class="d-block">Pratinjau SAG</label>
<iframe id="verifySagFrame" title="Pratinjau SAG" src="about:blank"
<label class="d-block">Pratonton SAG</label>
<iframe id="verifySagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
</div>
@@ -440,17 +524,46 @@
});
$('#btnSubmitBatch').on('click', function () {
if ($('.row-check:not(:disabled):checked').length === 0) return;
if (!confirm('Hantar rekod yang sudah disahkan ke Operasi?')) return;
$('#formSubmitBatch').submit();
var n = $('.row-check:not(:disabled):checked').length;
if (n === 0) return;
Swal.fire({
title: 'Hantar Batch ke Operasi?',
html: n + ' rekod yang sudah SAH akan dihantar sebagai <b>{{ $slotName }}</b>.'
+ '<br>Sila hantar sebelum cutoff <b>{{ $slotCutoff }}</b>.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Hantar',
cancelButtonText: 'Batal',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
$('#formSubmitBatch').submit();
}
});
});
$('.btn-unverify').on('click', function () {
var id = $(this).data('id');
var sag = $(this).data('norujukan') || '';
if (!confirm('Batal semakan ' + sag + '? Rekod ini tidak boleh dihantar sehingga disemak semula.')) return;
$('#formUnverify').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/unverify");
$('#formUnverify').submit();
Swal.fire({
title: 'Batal Semakan?',
html: 'Batal semakan <b>' + (sag || 'rekod ini') + '</b>?'
+ '<br>Rekod ini tidak boleh dihantar sehingga disemak semula.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Batal',
cancelButtonText: 'Tutup',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
$('#formUnverify').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/unverify");
$('#formUnverify').submit();
}
});
});
$('.btn-verify').on('click', function () {
@@ -513,5 +626,32 @@
$('#editKodbank').on('change', function () {
$('#editNamabank').val($('#editKodbank option:selected').data('nama') || '');
});
function pad2(n) {
return n < 10 ? '0' + n : String(n);
}
function tickSlotCountdown() {
var el = document.getElementById('slotCountdown');
if (!el) return;
if (el.getAttribute('data-lewat') === '1') return;
var cutoff = el.getAttribute('data-cutoff') || '';
var parts = cutoff.split(':');
if (parts.length < 2) return;
var now = new Date();
var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0);
var diff = end - now;
if (diff <= 0) {
el.textContent = 'Cutoff telah tamat — sila muat semula halaman';
return;
}
var h = Math.floor(diff / 3600000);
var m = Math.floor((diff % 3600000) / 60000);
var s = Math.floor((diff % 60000) / 1000);
el.textContent = 'Baki masa: ' + h + 'j ' + pad2(m) + 'm ' + pad2(s) + 's';
}
tickSlotCountdown();
setInterval(tickSlotCountdown, 1000);
</script>
@endsection
@@ -131,7 +131,7 @@
</div>
</div>
<div class="card">
{{-- <div class="card">
<div class="card-header">
<h5 class="mb-0">Tutup Modal Online (EOD)</h5>
</div>
@@ -153,7 +153,7 @@
</form>
@endif
</div>
</div>
</div> --}}
</div>
<div class="col-md-7 mb-3">
@@ -195,7 +195,7 @@
/>
<div class="title">
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Online (Akaun Bank)</h1>
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Atas Talian (Online)</h1>
<p>Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.</p>
</div>
@@ -220,7 +220,6 @@
<tr>
<th>Bil</th>
<th>Norujukan</th>
<th>Cara Terima</th>
<th>Nama</th>
<th>Alamat</th>
<th>No K/P</th>
@@ -263,7 +262,7 @@
@if($index > 0)
@if($item['teller'] != $gadai[$index - 1]['teller'])
<tr>
<td colspan=11>Teller : <b>{{ $item['teller'] }}</b></td>
<td colspan=10>Teller : <b>{{ $item['teller'] }}</b></td>
</tr>
@php
$totalpinjamanteller = 0;
@@ -277,7 +276,7 @@
@endif
@else
<tr>
<td colspan=11>Teller : <b>{{ $item['teller'] }}</b></td>
<td colspan=10>Teller : <b>{{ $item['teller'] }}</b></td>
</tr>
@php
$totalpinjamanteller = 0;
@@ -333,7 +332,7 @@
@if($item['teller'] != $gadai[$index + 1]['teller'])
@php if($item['teller'] != '')$bil += ($counter-1); @endphp
<tr>
<td colspan="8"><b>{{ $counter-1 }}</b> Jumlah surat bagi teller : <b>{{ $item['teller'] }}</b></td>
<td colspan="7"><b>{{ $counter-1 }}</b> Jumlah surat bagi teller : <b>{{ $item['teller'] }}</b></td>
<td><b>{{ number_format($totalberatteller,2) }} </b></td>
<td><b>{{ number_format($totalnmteller,2) }} </b></td>
<td><b>{{ number_format($totalpinjamanteller,2) }} </b></td>
@@ -360,7 +359,7 @@
@else
@php $bil += ($counter-1); @endphp
<tr>
<td colspan="8"><b>{{ $counter-1 }}</b> Jumlah surat bagi teller : <b>{{ $item['teller'] }}</b></td>
<td colspan="7"><b>{{ $counter-1 }}</b> Jumlah surat bagi teller : <b>{{ $item['teller'] }}</b></td>
<td><b>{{ number_format($totalberatteller,2) }} </b></td>
<td><b>{{ number_format($totalnmteller,2) }} </b></td>
<td><b>{{ number_format($totalpinjamanteller,2) }} </b></td>
+11
View File
@@ -15,6 +15,10 @@ use Illuminate\Support\Facades\Storage;
|
*/
Route::post('/internal/notifications', ['as' => 'internal.notifications.store', 'uses' => 'InternalNotificationController@store']);
Route::view('/dasar-privasi', 'dasar-privasi')->name('dasar.privasi');
Route::get('/health', function () {
return response()->json(['status' => 'ok']);
});
@@ -174,6 +178,7 @@ Route::get('/khazanahtebus',['as'=>'khazanah.tebus','uses'=>'KhazanahController@
Route::get('/kelulusan','KhazanahController@kelulusan')->middleware('auth','khazanah');
Route::get('/kelulusanrekodlepas',['as'=>'khazanah.rekodlepas','uses'=>'KhazanahController@kelulusanrekodlepas'])->middleware('auth','khazanah');
Route::get('/kelulusanrekodlepasoperasi',['as'=>'operasi.rekodlepas','uses'=>'OperasiController@kelulusanrekodlepas'])->middleware('auth','operasi');
Route::get('/kelulusanrekodlepasoperasi/data',['as'=>'operasi.rekodlepas.data','uses'=>'OperasiController@kelulusanrekodlepasData'])->middleware('auth','operasi');
Route::get('/pendahuluan',['as'=>'khazanah.pendahulu','uses'=>'KhazanahController@pendahuluan'])->middleware('auth','khazanah');
Route::get('/serahan',['as'=>'khazanah.serahan','uses'=>'KhazanahController@serahan'])->middleware('auth','khazanah');
Route::get('/laporanscanbarcode',['as'=>'laporanscanbarcode','uses'=>'KhazanahController@LaporanSemakBarcode'])->middleware('auth','khazanah');
@@ -190,8 +195,13 @@ Route::get('/penebusankhazanah/{norujukan}',['as'=>'penebusan.khazanah','uses' =
Route::get('/lelongankhazanah/{norujukan}',['as'=>'lelongan.khazanah','uses' => 'KhazanahController@lelongan'])->middleware('auth','khazanah');
Route::get('/permohonan',['as'=>'permohonan.index','uses' => 'KhazanahController@PaparanPermohonan'])->middleware('auth','khazanah');
Route::get('/notifications',['as'=>'notifications.index','uses'=>'NotificationController@index'])->middleware('auth');
Route::post('/notifications/read-all',['as'=>'notifications.read.all','uses'=>'NotificationController@markAllAsRead'])->middleware('auth');
Route::post('/notifications/{id}/read',['as'=>'notifications.read','uses'=>'NotificationController@markAsRead'])->middleware('auth');
// Pembiayaan Online — PC
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::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');
@@ -407,6 +417,7 @@ Route::post('/barcodescanned',['as'=>'barcode.scanned','uses'=>'KhazanahControll
Route::get('/operasi/kelulusan','OperasiController@kelulusan')->name('operasi.kelulusan')->middleware('auth','operasi');
Route::get('/operasi/kelulusan/anggota','OperasiController@kelulusanAnggota')->name('operasi.kelulusananggota')->middleware('auth','operasi');
Route::post('/operasi/createkelulusan',['as'=>'operasi.createkelulusan','uses'=>'OperasiController@createkelulusan'])->middleware('auth','teller');
Route::post('/operasi/auto-approve-kelulusan',['as'=>'operasi.autoApproveKelulusan','uses'=>'OperasiController@autoApproveKelulusan'])->middleware('auth','teller');
Route::post('/operasi/createkelulusan-by-khazanah',['as'=>'operasi.createkelulusan-by-khazanah','uses'=>'OperasiController@createkelulusanByKhazanah'])->middleware('auth','khazanah');
Route::post('/operasi/createkelulusan-anggota',['as'=>'operasi.createkelulusan-anggota','uses'=>'OperasiController@createkelulusanAnggota'])->middleware('auth','teller');
Route::get('/operasi/getkelulusan',['as'=>'operasi.getkelulusan','uses'=>'OperasiController@getkelulusan'])->middleware('auth','teller');