Feature/whatsapp blast #24
@@ -52,3 +52,5 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
MYKAD_READER_PORT=8085
|
||||
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
|
||||
|
||||
INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret
|
||||
+4
-2
@@ -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
@@ -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"
|
||||
@@ -11,12 +11,6 @@ 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
|
||||
|
||||
## Proses Gadaian
|
||||
|
||||
@@ -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;
|
||||
@@ -50,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 === '') {
|
||||
@@ -61,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)
|
||||
{
|
||||
|
||||
@@ -88,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'],
|
||||
@@ -112,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,
|
||||
@@ -124,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)
|
||||
@@ -191,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
|
||||
|
||||
@@ -242,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 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') {
|
||||
$online = Http::withOptions(['verify' => config('app.api_verify')])
|
||||
->asForm()
|
||||
@@ -278,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']);
|
||||
}
|
||||
@@ -286,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.');
|
||||
@@ -337,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);
|
||||
}
|
||||
|
||||
@@ -467,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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
foreach ($kelulusan as $index => $k) {
|
||||
foreach ($k['kelulusanall'] as $index1 => $kel) {
|
||||
$komen = explode("|", $kel['komen']);
|
||||
|
||||
$results = array_search("", $komen, true);
|
||||
if ($results !== false) {
|
||||
unset($komen[$results]);
|
||||
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'dateFrom', 'dateTo'));
|
||||
}
|
||||
|
||||
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'],]);
|
||||
}
|
||||
public function kelulusanrekodlepasData(Request $request)
|
||||
{
|
||||
$params = [
|
||||
'date_from' => $request->query('date_from'),
|
||||
'date_to' => $request->query('date_to'),
|
||||
'search' => $request->query('search'),
|
||||
];
|
||||
|
||||
$response = Http::withOptions(['verify' => config('app.api_verify')])
|
||||
->get(config('api.url') . '/api/ListOperasiKelulusan/', array_filter($params, function ($value) {
|
||||
return $value !== null && $value !== '';
|
||||
}));
|
||||
|
||||
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).');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
|
||||
* @var array
|
||||
*/
|
||||
protected $except = [
|
||||
//
|
||||
'internal/notifications',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -414,6 +416,7 @@
|
||||
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();
|
||||
@@ -425,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',
|
||||
@@ -1302,6 +1312,51 @@
|
||||
.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';
|
||||
if (pembiayaanOnlineEnabled && pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN) {
|
||||
@@ -1312,6 +1367,7 @@
|
||||
$('#bank_online_section').hide();
|
||||
$('#cetak_borang_online').hide();
|
||||
$('#cara_terima_tunai').prop('checked', true);
|
||||
syncTerimaButton();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1326,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();
|
||||
@@ -1368,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() {
|
||||
@@ -1524,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!',
|
||||
@@ -1533,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,77 +219,69 @@
|
||||
<div class="card-body text-center">
|
||||
|
||||
<div class="row form-group justify-content-center">
|
||||
<div class="col-3 mx-1">
|
||||
<div class="row">
|
||||
<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="{{ 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>
|
||||
<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">
|
||||
<a href="{{ url('pendahuluanserahan') }}"
|
||||
class="btn btn-info btn-lg btn-block mb-3">Pendahuluan - Serahan</a>
|
||||
</div>
|
||||
@if(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']))
|
||||
<div class="row">
|
||||
<a href="{{ route('pembiayaan.online.pc') }}"
|
||||
class="btn btn-info btn-lg btn-block mb-3">Pembiayaan Online</a>
|
||||
</div>
|
||||
@endif
|
||||
<div class="row">
|
||||
<a href="{{ route('khazanah.indexwakil') }}"
|
||||
class="btn btn-info btn-lg btn-block mb-3">Surat Wakil</a>
|
||||
</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">
|
||||
<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="row">
|
||||
<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="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="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>
|
||||
|
||||
@@ -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">×</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">×</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>
|
||||
|
||||
@@ -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">×</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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
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>×</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>×</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>×</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>×</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 & 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>
|
||||
· 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>
|
||||
· 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;
|
||||
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;
|
||||
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>
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ use Illuminate\Support\Facades\Storage;
|
||||
|
|
||||
*/
|
||||
|
||||
Route::post('/internal/notifications', ['as' => 'internal.notifications.store', 'uses' => 'InternalNotificationController@store']);
|
||||
|
||||
Route::get('/health', function () {
|
||||
return response()->json(['status' => 'ok']);
|
||||
});
|
||||
@@ -174,6 +176,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 +193,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 +415,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');
|
||||
|
||||
Reference in New Issue
Block a user