feat(shoraka): defer gadai persistence until commodity confirmation and add simulation support
This commit is contained in:
@@ -184,13 +184,16 @@ class GadaianController extends Controller
|
||||
'norujukan' => $norujukan,
|
||||
'nopelanggan' => $customer_info['nopelanggan'],
|
||||
'nokp' => $noic,
|
||||
'sirig' => $new_sirig
|
||||
]);
|
||||
'sirig' => $new_sirig,
|
||||
'shoraka_new' => true,
|
||||
])->throw()->json();
|
||||
$norujukan = $gadai['norujukan'];
|
||||
Session::put('norujukan', $norujukan);
|
||||
} else {
|
||||
$norujukan = Session::get('norujukan');
|
||||
$nokp = Session::get('nokp');
|
||||
$nopelanggan = Session::get('nopelanggan');
|
||||
$gadai_session = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/check/sag', ['kodcaw' => $auth_user['cawangan'], 'norujukan' => $norujukan, 'nokp' => $nokp])->json();
|
||||
$gadai_session = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/check/sag', ['kodcaw' => $auth_user['cawangan'], 'norujukan' => $norujukan, 'nokp' => $nokp])->throw()->json();
|
||||
if ($gadai_session == null) {
|
||||
$gadai_transaction = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/' . $auth_user['cawangan'] . '/' . $current->toDateString())->json();
|
||||
if ($gadai_transaction) {
|
||||
@@ -209,9 +212,11 @@ class GadaianController extends Controller
|
||||
'norujukan' => $norujukan_baru,
|
||||
'nopelanggan' => $nopelanggan,
|
||||
'nokp' => $nokp,
|
||||
'sirig' => $new_sirig
|
||||
]);
|
||||
$norujukan = $norujukan_baru;
|
||||
'sirig' => $new_sirig,
|
||||
'shoraka_new' => true,
|
||||
])->throw()->json();
|
||||
$norujukan = $gadai_baru['norujukan'];
|
||||
Session::put('norujukan', $norujukan);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +358,7 @@ class GadaianController extends Controller
|
||||
return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
|
||||
'cara_terima' => $caraTerima,
|
||||
'pinjaman' => $pinjamanAmt,
|
||||
'temporary_gadai' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -544,31 +550,55 @@ class GadaianController extends Controller
|
||||
$keuntungan6bulan = $keuntungan_sebln * 6;
|
||||
$keuntungan12bulan = $keuntungan_sebln * 12;
|
||||
$hargajualan = $request->pinjaman + $keuntungan12bulan;
|
||||
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/' . $auth_user['cawangan'] . '/' . $request->norujukan_gadai, [
|
||||
'kodcaw' => $auth_user['cawangan'],
|
||||
'nilaimarhun' => round($total_nilai_marhun, 2),
|
||||
'berat' => round($request->total_berat, 2),
|
||||
'pinjaman' => round($request->pinjaman, 2),
|
||||
'kadarupah' => round($kadarupah, 2),
|
||||
'marhun' => $request->marhun_detail,
|
||||
'percentpinj' => $margin_pinjaman,
|
||||
'teller' => $auth_user['name'],
|
||||
'upah6' => $keuntungan6bulan,
|
||||
'upah12' => $keuntungan12bulan,
|
||||
'hargajualan' => $hargajualan,
|
||||
'kuantiti_marhun' => $request->kuantiti_marhun,
|
||||
'transaction' => 'Ubah Gadai',
|
||||
'kadarujrah' => round($kadarupah, 2),
|
||||
'ujrah' => $ujrah,
|
||||
'onepercent' => $onepercent,
|
||||
'margin_semasa' => $margin_pinjaman,
|
||||
'kadar_rebate' => $this->parseMoney($request->kadar_rebate),
|
||||
'rebate' => $this->parseMoney($request->rebate),
|
||||
])->json();
|
||||
$update_komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/komuditi/update', [
|
||||
'unit_komuditi' => round($request->pinjaman, 2),
|
||||
'norujukan' => $request->norujukan_gadai
|
||||
])->json();
|
||||
try {
|
||||
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/' . $auth_user['cawangan'] . '/' . $request->norujukan_gadai, [
|
||||
'kodcaw' => $auth_user['cawangan'],
|
||||
'nilaimarhun' => round($total_nilai_marhun, 2),
|
||||
'berat' => round($request->total_berat, 2),
|
||||
'pinjaman' => round($request->pinjaman, 2),
|
||||
'kadarupah' => round($kadarupah, 2),
|
||||
'marhun' => $request->marhun_detail,
|
||||
'percentpinj' => $margin_pinjaman,
|
||||
'teller' => $auth_user['name'],
|
||||
'upah6' => $keuntungan6bulan,
|
||||
'upah12' => $keuntungan12bulan,
|
||||
'hargajualan' => $hargajualan,
|
||||
'kuantiti_marhun' => $request->kuantiti_marhun,
|
||||
'transaction' => 'Ubah Gadai',
|
||||
'kadarujrah' => round($kadarupah, 2),
|
||||
'ujrah' => $ujrah,
|
||||
'onepercent' => $onepercent,
|
||||
'margin_semasa' => $margin_pinjaman,
|
||||
'kadar_rebate' => $this->parseMoney($request->kadar_rebate),
|
||||
'rebate' => $this->parseMoney($request->rebate),
|
||||
]);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $exception) {
|
||||
return back()->withInput()->with('gadai_edit_error',
|
||||
'Sambungan terganggu. Semak rekod gadai sebelum mencuba semula.');
|
||||
}
|
||||
if (!$gadai->successful()) {
|
||||
$message = 'Ubah Gadai tidak berjaya. Sila semak dengan operasi.';
|
||||
if ($gadai->status() === 409 || $gadai->status() === 422) {
|
||||
$apiMessage = $gadai->json()['message'] ?? null;
|
||||
if (is_string($apiMessage) && $apiMessage !== '') {
|
||||
$message = $apiMessage;
|
||||
}
|
||||
}
|
||||
return back()->withInput()->with('gadai_edit_error', $message);
|
||||
}
|
||||
try {
|
||||
$update_komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/komuditi/update', [
|
||||
'unit_komuditi' => round($request->pinjaman, 2),
|
||||
'norujukan' => $request->norujukan_gadai
|
||||
]);
|
||||
} catch (\Illuminate\Http\Client\ConnectionException $exception) {
|
||||
return back()->with('gadai_edit_error',
|
||||
'Rekod gadai telah dikemas kini tetapi kemas kini komoditi tidak dapat disahkan. Sila semak dengan operasi.');
|
||||
}
|
||||
if (!$update_komuditi->successful()) {
|
||||
return back()->with('gadai_edit_error',
|
||||
'Rekod gadai telah dikemas kini tetapi kemas kini komoditi gagal. Sila semak dengan operasi.');
|
||||
}
|
||||
|
||||
event(new GadaiNotify('lulus', $auth_user['cawangan']));
|
||||
|
||||
@@ -582,7 +612,7 @@ class GadaianController extends Controller
|
||||
$norujukan = $request->norujukan;
|
||||
$auth_user = Auth::user();
|
||||
|
||||
Http::withOptions(['verify' => config('app.api_verify')])->delete(config('api.url') . '/api/gadai/batal/' . $auth_user['cawangan'] . '/' . $norujukan);
|
||||
Http::withOptions(['verify' => config('app.api_verify')])->delete(config('api.url') . '/api/gadai/batal/' . $auth_user['cawangan'] . '/' . $norujukan)->throw();
|
||||
$request->session()->forget('page_reload');
|
||||
$request->session()->forget('norujukan');
|
||||
$request->session()->forget('borang_online_printed');
|
||||
|
||||
@@ -18,6 +18,49 @@ class ShorakaController extends Controller
|
||||
return view('shoraka.index', compact('records', 'cawangan_info'));
|
||||
}
|
||||
|
||||
public function operasiIndex(Request $request)
|
||||
{
|
||||
abort_unless(in_array(Auth::user()->roles, ['operasi', 'pc', 'ppc'], true), 403);
|
||||
$isOperasi = Auth::user()->roles === 'operasi';
|
||||
$filters = $request->validate([
|
||||
'kodcaw' => 'nullable|string|max:30',
|
||||
'norujukan' => 'nullable|string|max:100',
|
||||
'state' => 'nullable|in:WAITING,PROCESSING,REVIEW,DONE',
|
||||
'date_from' => 'nullable|date_format:Y-m-d',
|
||||
'date_to' => 'nullable|date_format:Y-m-d' . ($request->filled('date_from') ? '|after_or_equal:date_from' : ''),
|
||||
]);
|
||||
$branchQuery = ShorakaFinancing::select('kodcaw');
|
||||
if (!$isOperasi) {
|
||||
$branchQuery->where('kodcaw', Auth::user()->cawangan);
|
||||
}
|
||||
$branches = $branchQuery->distinct()->orderBy('kodcaw')->pluck('kodcaw');
|
||||
$query = ShorakaFinancing::select('id', 'kodcaw', 'norujukan', 'teller', 'action', 'state', 'created_at', 'updated_at');
|
||||
if (!$isOperasi) {
|
||||
$query->where('kodcaw', Auth::user()->cawangan);
|
||||
}
|
||||
foreach (['kodcaw', 'norujukan', 'state'] as $field) {
|
||||
if (!empty($filters[$field])) {
|
||||
$query->where($field, $filters[$field]);
|
||||
}
|
||||
}
|
||||
if (!empty($filters['date_from'])) {
|
||||
$query->where('created_at', '>=', $filters['date_from'] . ' 00:00:00');
|
||||
}
|
||||
if (!empty($filters['date_to'])) {
|
||||
$query->where('created_at', '<', \Carbon\Carbon::parse($filters['date_to'])->addDay()->format('Y-m-d') . ' 00:00:00');
|
||||
}
|
||||
$records = $query->orderByDesc('created_at')->orderByDesc('id')
|
||||
->paginate(25)->appends($filters);
|
||||
$cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan];
|
||||
return view('shoraka.operasi', compact('records', 'branches', 'filters', 'isOperasi', 'cawangan_info'));
|
||||
}
|
||||
|
||||
public function pcIndex(Request $request)
|
||||
{
|
||||
abort_unless(in_array(Auth::user()->roles, ['pc', 'ppc'], true), 403);
|
||||
return $this->operasiIndex($request);
|
||||
}
|
||||
|
||||
public function show(string $norujukan, ShorakaFinancingService $service)
|
||||
{
|
||||
$record = $service->local($norujukan);
|
||||
@@ -32,6 +75,62 @@ class ShorakaController extends Controller
|
||||
return view('shoraka.show', compact('norujukan', 'record', 'data', 'error', 'cawangan_info'));
|
||||
}
|
||||
|
||||
public function poll(Request $request, string $norujukan, ShorakaFinancingService $service)
|
||||
{
|
||||
$record = $service->local($norujukan);
|
||||
abort_unless($record && $record->teller === Auth::user()->name && $record->action === 'gadai', 403);
|
||||
if ($record->state === 'DONE') {
|
||||
return response()->json(['state' => 'done', 'redirect' => route('customer_info')]);
|
||||
}
|
||||
abort_unless(!empty($record->context['temporary_gadai']), 409);
|
||||
if ($record->state === 'REVIEW') {
|
||||
return response()->json(['state' => 'failed', 'message' => 'Penyelesaian memerlukan semakan operasi. Jangan ulang bayaran.']);
|
||||
}
|
||||
if ($record->state === 'PROCESSING') {
|
||||
return response()->json(['state' => 'pending']);
|
||||
}
|
||||
try {
|
||||
$data = $service->api()->get($service->url($norujukan))->throw()->json()['data'];
|
||||
$order = $data['order'] ?? null;
|
||||
if ($order && !empty($order['order_id']) && ($order['status'] ?? '') !== 'Cancelled') {
|
||||
$service->api()->post($service->url($norujukan, '/refresh'))->throw();
|
||||
$data = $service->api()->get($service->url($norujukan))->throw()->json()['data'];
|
||||
$order = $data['order'] ?? null;
|
||||
}
|
||||
$status = $order['status'] ?? null;
|
||||
if (in_array($status, ['Cancelled', 'FAILED', 'UNKNOWN'], true) || !$order) {
|
||||
return response()->json(['state' => 'failed', 'message' => !empty($data['simulation'])
|
||||
? 'Simulasi gagal berjaya diuji. Tiada gadaian atau marhun disimpan dan tiada bayaran diproses.'
|
||||
: 'Transaksi gagal atau hasil penghantaran belum dapat dipastikan. Hubungi operasi; jangan hantar semula.']);
|
||||
}
|
||||
if (!empty($data['can_complete'])) {
|
||||
if ($request->input('progress') === 'check') {
|
||||
return response()->json(['state' => 'ready']);
|
||||
}
|
||||
$result = $service->finish($request, $norujukan);
|
||||
return response()->json(['state' => 'done', 'redirect' => $result->getTargetUrl()]);
|
||||
}
|
||||
if ($status === 'Completed') {
|
||||
return response()->json(['state' => 'failed', 'message' => 'Komoditi selesai tetapi maklumat gadaian belum dapat disahkan. Hubungi operasi.']);
|
||||
}
|
||||
return response()->json(['state' => 'pending']);
|
||||
} catch (Throwable $exception) {
|
||||
if ($record->fresh()->state === 'REVIEW') {
|
||||
return response()->json(['state' => 'failed', 'message' => 'Penyelesaian memerlukan semakan operasi. Jangan ulang bayaran.']);
|
||||
}
|
||||
if ($exception instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface
|
||||
&& $exception->getStatusCode() >= 400 && $exception->getStatusCode() < 500) {
|
||||
return response()->json(['state' => 'failed', 'message' => $exception->getMessage()]);
|
||||
}
|
||||
if ($exception instanceof \Illuminate\Http\Client\RequestException
|
||||
&& in_array($exception->response->status(), [404, 409], true)) {
|
||||
return response()->json(['state' => 'failed', 'message' => 'Maklumat transaksi tidak dapat disahkan. Hubungi operasi; jangan hantar semula.']);
|
||||
}
|
||||
// Connection loss is not proof of a failed commodity trade.
|
||||
return response()->json(['state' => 'pending']);
|
||||
}
|
||||
}
|
||||
|
||||
public function submit(string $norujukan, ShorakaFinancingService $service)
|
||||
{
|
||||
$record = $service->local($norujukan);
|
||||
|
||||
@@ -10,12 +10,13 @@ use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Throwable;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class ShorakaFinancingService
|
||||
{
|
||||
public function ensureReady(): void
|
||||
{
|
||||
// Check local migration before any registration side effect.
|
||||
// Check the configured financing database before any registration side effect.
|
||||
ShorakaFinancing::where('kodcaw', Auth::user()->cawangan)->exists();
|
||||
$response = $this->api()->get(rtrim(config('api.url'), '/') . '/api/shoraka/readiness')->throw()->json();
|
||||
abort_unless(!empty($response['ready']), 503, 'Shoraka belum dikonfigurasi. Pembiayaan baharu ditahan.');
|
||||
@@ -42,15 +43,25 @@ class ShorakaFinancingService
|
||||
public function hold(Request $request, string $reference, string $action, array $extra = [])
|
||||
{
|
||||
$user = Auth::user();
|
||||
$context = array_merge($request->except(['_token', 'password']), $extra);
|
||||
if (!empty($extra['temporary_gadai'])) {
|
||||
$key = 'shoraka:payout:' . hash('sha256', $user->cawangan . ':' . $reference);
|
||||
$temporaryContext = $context;
|
||||
$context = ['temporary_gadai' => true, 'pinjaman' => $extra['pinjaman']];
|
||||
}
|
||||
$record = ShorakaFinancing::firstOrCreate(
|
||||
['kodcaw' => $user->cawangan, 'norujukan' => $reference],
|
||||
[
|
||||
'teller' => $user->name, 'action' => $action, 'state' => 'WAITING',
|
||||
'context' => array_merge($request->except(['_token', 'password']), $extra),
|
||||
'context' => $context,
|
||||
]
|
||||
);
|
||||
// A duplicate request only opens the saved workflow; it never replays registration.
|
||||
if ($record->wasRecentlyCreated) {
|
||||
if (isset($temporaryContext) && !Cache::store('file')->put($key, $temporaryContext, 604800)) {
|
||||
$record->update(['state' => 'REVIEW']);
|
||||
abort(503, 'Maklumat pembiayaan sementara tidak dapat disimpan.');
|
||||
}
|
||||
try {
|
||||
$this->api()->post($this->url($reference, '/submit'));
|
||||
} catch (Throwable $exception) {
|
||||
@@ -91,11 +102,17 @@ class ShorakaFinancingService
|
||||
409, 'Amaun pembiayaan berubah. Semakan operasi diperlukan sebelum bayaran.'
|
||||
);
|
||||
|
||||
$context = $record->context;
|
||||
if (!empty($context['temporary_gadai'])) {
|
||||
$context = Cache::store('file')->get('shoraka:payout:' . hash('sha256', $record->kodcaw . ':' . $reference));
|
||||
abort_unless(is_array($context), 409, 'Maklumat sementara tamat. Semakan operasi diperlukan sebelum bayaran.');
|
||||
}
|
||||
|
||||
$claimed = ShorakaFinancing::where('id', $record->id)->where('state', 'WAITING')
|
||||
->update(['state' => 'PROCESSING', 'updated_at' => now()]);
|
||||
abort_unless($claimed === 1, 409, 'Penyelesaian sedang diproses.');
|
||||
|
||||
$saved = new Request($record->context);
|
||||
$saved = new Request($context);
|
||||
$saved->setLaravelSession($request->session());
|
||||
try {
|
||||
if ($record->action === 'gadai') {
|
||||
@@ -119,6 +136,9 @@ class ShorakaFinancingService
|
||||
$result = redirect()->route('shoraka.show', ['norujukan' => $reference]);
|
||||
}
|
||||
$record->update(['state' => 'DONE', 'context' => []]);
|
||||
if (!empty($context['temporary_gadai'])) {
|
||||
Cache::store('file')->forget('shoraka:payout:' . hash('sha256', $record->kodcaw . ':' . $reference));
|
||||
}
|
||||
return $result;
|
||||
} catch (Throwable $exception) {
|
||||
// A failed remote payment response can be ambiguous. Never retry automatically.
|
||||
|
||||
+2
-2
@@ -2,13 +2,13 @@
|
||||
|
||||
if (!function_exists('api_url_for_browser')) {
|
||||
/**
|
||||
* Base URL for JavaScript in the browser, derived from API_URL (config api.url).
|
||||
* Base URL for JavaScript, using API_BROWSER_URL when configured.
|
||||
* Docker dev often uses host.docker.internal for PHP→host; browsers may not resolve it.
|
||||
* Same host-mapped port is reachable at 127.0.0.1. Production URLs are unchanged.
|
||||
*/
|
||||
function api_url_for_browser(): string
|
||||
{
|
||||
$url = rtrim((string) config('api.url'), '/');
|
||||
$url = rtrim((string) (config('api.browser_url') ?: config('api.url')), '/');
|
||||
|
||||
return str_ireplace('host.docker.internal', '127.0.0.1', $url);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
return [
|
||||
'url' => env('API_URL'),
|
||||
'browser_url' => env('API_BROWSER_URL'),
|
||||
'url_koop' => env('API_URL_KOOP','https://apiujrah.erahn.com.my'),
|
||||
'url_pkb' => env('API_URL_PKB','http://localhost:8000'),
|
||||
];
|
||||
@@ -5,6 +5,9 @@
|
||||
@section('content')
|
||||
@include('modal.editmarhun')
|
||||
<div class="container">
|
||||
@if(session('gadai_edit_error'))
|
||||
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
|
||||
@endif
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
<h3 class="text-center text-uppercase">Pembiayaan</h3>
|
||||
@@ -946,6 +949,7 @@
|
||||
text: `Komen : ${data.komen}`
|
||||
}).then(function(){
|
||||
$(func).attr("onSubmit","return true;");
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
$(func).submit();
|
||||
return true;
|
||||
});
|
||||
@@ -1058,6 +1062,7 @@
|
||||
}).then(function(){
|
||||
|
||||
$(func).attr("onSubmit","return true;");
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
$(func).submit();
|
||||
return true;
|
||||
|
||||
@@ -1092,11 +1097,7 @@
|
||||
return false;
|
||||
}
|
||||
else if(pinjaman < 10000 && pinjaman <= Number(pembiayaan_max_teller)){
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berjaya!',
|
||||
text: 'Pembiayaan Berjaya Disimpan'
|
||||
});
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1162,9 +1162,9 @@
|
||||
return false;
|
||||
} else {
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berjaya!',
|
||||
text: 'Pembiayaan Berjaya Disimpan'
|
||||
icon: 'info',
|
||||
title: 'Sedang diproses',
|
||||
text: 'Sedang menghantar pembiayaan untuk pengesahan Shoraka.'
|
||||
});
|
||||
|
||||
$(func).attr("onSubmit","return true;");
|
||||
|
||||
@@ -5,6 +5,9 @@
|
||||
@section('content')
|
||||
@include('modal.editmarhun')
|
||||
<div class="container">
|
||||
@if(session('gadai_edit_error'))
|
||||
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
|
||||
@endif
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-12 col-lg-12">
|
||||
<h3 class="text-center text-uppercase">Pembiayaan</h3>
|
||||
@@ -1612,6 +1615,7 @@
|
||||
text: `Komen : ${data.komen}`
|
||||
}).then(function(){
|
||||
$(func).attr("onSubmit","return true;");
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
$(func).submit();
|
||||
return true;
|
||||
});
|
||||
@@ -1724,6 +1728,7 @@
|
||||
}).then(function(){
|
||||
|
||||
$(func).attr("onSubmit","return true;");
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
$(func).submit();
|
||||
return true;
|
||||
|
||||
@@ -1758,11 +1763,7 @@
|
||||
return false;
|
||||
}
|
||||
else if(pinjaman < 10000 && pinjaman <= Number(pembiayaan_max_teller)){
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berjaya!',
|
||||
text: 'Pembiayaan Berjaya Disimpan'
|
||||
});
|
||||
window.shorakaProgress(1, 'Sedang menghantar pembiayaan kepada Shoraka...');
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2000,9 +2000,9 @@
|
||||
} else {
|
||||
//insert code asal
|
||||
Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Berjaya!',
|
||||
text: 'Pembiayaan Berjaya Disimpan'
|
||||
icon: 'info',
|
||||
title: 'Sedang diproses',
|
||||
text: 'Sedang menghantar pembiayaan untuk pengesahan Shoraka.'
|
||||
});
|
||||
|
||||
$(func).attr("onSubmit","return true;");
|
||||
|
||||
@@ -268,6 +268,11 @@
|
||||
<a href="{{ route('khazanah.harga_emas') }}"
|
||||
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
|
||||
</div>
|
||||
@if(in_array(Auth::user()->roles, ['pc', 'ppc'], true))
|
||||
<div class="col-4 px-2">
|
||||
<a href="{{ route('pc.shoraka.index') }}" class="btn btn-info btn-lg btn-block mb-3">Komoditi Shoraka</a>
|
||||
</div>
|
||||
@endif
|
||||
@if(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']))
|
||||
<div class="col-4 px-2">
|
||||
<a href="{{ route('pembiayaan.online.pc') }}"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
<div class="card-body text-center">
|
||||
<a href="{{ route('operasi.kelulusananggota') }}" class="btn btn-info btn-lg d-inline-block m-1">Kelulusan Anggota</a>
|
||||
<a href="{{ route('operasi.kelulusan') }}" class="btn btn-info btn-lg d-inline-block m-1">Kelulusan</a>
|
||||
<a href="{{ route('operasi.shoraka.index') }}" class="btn btn-info btn-lg d-inline-block m-1">Komoditi Shoraka</a>
|
||||
<a href="{{ route('pembiayaan.online.operasi') }}" class="btn btn-info btn-lg d-inline-block m-1">Pembiayaan Online</a>
|
||||
<a href="{{ route('pembiayaan.online.operasi.topup') }}" class="btn btn-info btn-lg d-inline-block m-1">Modal Online</a>
|
||||
<a href="{{ route('pembiayaan.online.operasi.eod') }}" class="btn btn-info btn-lg d-inline-block m-1">EOD Modal Online</a>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<a class="btn btn-info btn-lg" href="{{ url('/harga_emas') }}">HARGA EMAS</a>
|
||||
<a class="btn btn-info btn-lg" href="{{ url('/aliran_tunai') }}">ALIRAN TUNAI</a>
|
||||
@if(in_array(Auth::user()->roles, ['teller', 'penilai', 'pc', 'ppc', 'admin', 'masteradmin', 'operasi']) || Auth::user()->role_harian === 'teller')
|
||||
<a class="btn btn-info btn-lg" href="{{ route('shoraka.index') }}">PEMBIAYAAN SHORAKA</a>
|
||||
<a class="btn btn-info btn-lg" href="{{ route('shoraka.index') }}">KOMODITI SHORAKA</a>
|
||||
@endif
|
||||
{{-- <a class="btn btn-info btn-lg" href="#">TETAPAN</a> --}}
|
||||
</div>
|
||||
|
||||
@@ -615,7 +615,7 @@
|
||||
<td>{{ $komoditi['no_sijil'] }}</td>
|
||||
<td class="text-center">4</td>
|
||||
<td>Kuantiti (Unit)</td>
|
||||
<td>{{ number_format($kuantiti, 2) }} TAN</td>
|
||||
<td>{{ $kuantiti }} TONNES</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">2</td>
|
||||
@@ -1849,7 +1849,7 @@
|
||||
<td>{{ $komoditi['no_sijil'] }}</td>
|
||||
<td class="text-center">4</td>
|
||||
<td>Kuantiti</td>
|
||||
<td>{{ number_format($kuantiti, 2) }}</td>
|
||||
<td>{{ $kuantiti }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="text-center">2</td>
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb pb-3">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Pembiayaan Shoraka</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Komoditi Shoraka</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="card shoraka-list-card">
|
||||
<div class="card-header bg-secondary d-flex flex-wrap align-items-center justify-content-between">
|
||||
<h5 class="mb-0 py-2">Pembiayaan Shoraka</h5>
|
||||
<h5 class="mb-0 py-2">Komoditi Shoraka</h5>
|
||||
<a class="btn btn-info btn-sm my-1" href="{{ route('home') }}">
|
||||
<i class="fas fa-arrow-left mr-1" aria-hidden="true"></i> Kembali ke Paparan Utama
|
||||
</a>
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
<div id="shoraka-modal" hidden style="position:fixed;inset:0;z-index:20000;background:rgba(0,0,0,.5);display:none;align-items:center;justify-content:center;padding:16px">
|
||||
<div role="dialog" aria-modal="true" aria-labelledby="shoraka-modal-title" tabindex="-1" style="background:#fff;border-radius:5px;padding:24px;max-width:460px;width:100%;text-align:center;box-shadow:0 8px 30px #0003">
|
||||
<h4 id="shoraka-modal-title">Pembelian Komoditi Shoraka</h4>
|
||||
<p id="shoraka-modal-message" role="status" aria-live="polite">Sedang memproses pembelian komoditi...</p>
|
||||
<ol style="text-align:left;padding-left:28px">
|
||||
<li data-shoraka-step="1">Menghantar order ke Shoraka</li>
|
||||
<li data-shoraka-step="2">Menunggu pengesahan Shoraka</li>
|
||||
<li data-shoraka-step="3">Menyelesaikan pembiayaan dan menyimpan gadaian</li>
|
||||
</ol>
|
||||
<p class="text-muted small">Sila tunggu sehingga proses selesai. Jangan hantar transaksi semula.</p>
|
||||
<div id="shoraka-modal-spinner" class="spinner-border text-primary" role="status"><span class="sr-only">Sedang memproses</span></div>
|
||||
<button id="shoraka-modal-close" type="button" class="btn btn-secondary" hidden>Tutup</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
window.shorakaProgress = (function () {
|
||||
var modal = document.getElementById('shoraka-modal');
|
||||
var close = document.getElementById('shoraka-modal-close');
|
||||
var previousFocus;
|
||||
close.addEventListener('click', function () {
|
||||
modal.hidden = true;
|
||||
modal.style.display = 'none';
|
||||
if (previousFocus) { previousFocus.focus(); }
|
||||
});
|
||||
modal.addEventListener('keydown', function (event) {
|
||||
if (event.key === 'Tab') {
|
||||
event.preventDefault();
|
||||
(close.hidden ? modal.querySelector('[role="dialog"]') : close).focus();
|
||||
}
|
||||
});
|
||||
return function (step, message, stopped) {
|
||||
if (modal.hidden) { previousFocus = document.activeElement; }
|
||||
modal.hidden = false;
|
||||
modal.style.display = 'flex';
|
||||
document.getElementById('shoraka-modal-message').textContent = message || 'Sedang memproses pembelian komoditi...';
|
||||
modal.querySelectorAll('[data-shoraka-step]').forEach(function (item) {
|
||||
var number = Number(item.getAttribute('data-shoraka-step'));
|
||||
item.className = number < step ? 'text-success' : (number === step ? 'text-primary font-weight-bold' : 'text-muted');
|
||||
if (number === step) { item.setAttribute('aria-current', 'step'); }
|
||||
else { item.removeAttribute('aria-current'); }
|
||||
});
|
||||
document.getElementById('shoraka-modal-spinner').style.display = stopped ? 'none' : 'inline-block';
|
||||
close.hidden = !stopped;
|
||||
(stopped ? close : modal.querySelector('[role="dialog"]')).focus();
|
||||
};
|
||||
}());
|
||||
</script>
|
||||
<script>
|
||||
// Existing refinancing AJAX handlers must not print receipts or announce payment
|
||||
// success when the server has deferred the financial completion.
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
@extends($isOperasi ? 'layouts.app_operasi' : 'layouts.app')
|
||||
|
||||
@section('content')
|
||||
@php
|
||||
$listRoute = $isOperasi ? 'operasi.shoraka.index' : 'pc.shoraka.index';
|
||||
$statusLabels = ['WAITING' => 'Menunggu Shoraka / penyelesaian', 'PROCESSING' => 'Sedang diproses', 'REVIEW' => 'Perlu semakan operasi', 'DONE' => 'Selesai'];
|
||||
$statusColors = ['WAITING' => 'warning', 'PROCESSING' => 'info', 'REVIEW' => 'danger', 'DONE' => 'success'];
|
||||
@endphp
|
||||
<div class="container-fluid">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Komoditi Shoraka — {{ $isOperasi ? 'Operasi' : strtoupper(Auth::user()->roles) }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="card">
|
||||
<div class="card-header d-flex flex-wrap justify-content-between align-items-center">
|
||||
<h5 class="mb-0 py-2">Komoditi Shoraka — {{ $isOperasi ? 'Semua Cawangan' : Auth::user()->cawangan }}</h5>
|
||||
<a href="{{ route('home') }}" class="btn btn-info btn-sm">Kembali ke Paparan Utama</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted">Paparan pemantauan rekod pembiayaan {{ $isOperasi ? 'semua cawangan' : 'cawangan sendiri' }}. Status di bawah ialah status penyelesaian dalam sistem kaunter, bukan status langsung daripada Shoraka.</p>
|
||||
@if($errors->any())
|
||||
<div class="alert alert-danger" role="alert">{{ $errors->first() }}</div>
|
||||
@endif
|
||||
<form method="get" action="{{ route($listRoute) }}" class="mb-4">
|
||||
<div class="form-row align-items-end">
|
||||
@if($isOperasi)
|
||||
<div class="form-group col-md-3">
|
||||
<label for="shoraka-branch">Cawangan</label>
|
||||
<select id="shoraka-branch" name="kodcaw" class="form-control">
|
||||
<option value="">{{ $isOperasi ? 'Semua cawangan' : Auth::user()->cawangan }}</option>
|
||||
@foreach($branches as $branch)
|
||||
<option value="{{ $branch }}" {{ ($filters['kodcaw'] ?? '') === $branch ? 'selected' : '' }}>{{ $branch }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
<div class="form-group col-md-3">
|
||||
<label for="shoraka-reference">No. rujukan penuh</label>
|
||||
<input id="shoraka-reference" name="norujukan" class="form-control" maxlength="100" value="{{ $filters['norujukan'] ?? '' }}">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label for="shoraka-state">Penyelesaian pembiayaan</label>
|
||||
<select id="shoraka-state" name="state" class="form-control">
|
||||
<option value="">Semua status</option>
|
||||
@foreach($statusLabels as $state => $label)
|
||||
<option value="{{ $state }}" {{ ($filters['state'] ?? '') === $state ? 'selected' : '' }}>{{ $label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label for="shoraka-date-from">Tarikh mula didaftarkan</label>
|
||||
<input type="date" id="shoraka-date-from" name="date_from" class="form-control" value="{{ $filters['date_from'] ?? '' }}">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<label for="shoraka-date-to">Tarikh akhir didaftarkan</label>
|
||||
<input type="date" id="shoraka-date-to" name="date_to" class="form-control" value="{{ $filters['date_to'] ?? '' }}">
|
||||
</div>
|
||||
<div class="form-group col-md-3">
|
||||
<button type="submit" class="btn btn-primary">Tapis</button>
|
||||
<a href="{{ route($listRoute) }}" class="btn btn-secondary">Reset</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<p>{{ $records->total() }} rekod</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-bordered table-hover">
|
||||
<thead class="thead-dark"><tr>
|
||||
<th scope="col">Cawangan</th>
|
||||
<th scope="col">No. rujukan</th>
|
||||
<th scope="col">Teller</th>
|
||||
<th scope="col">Penyelesaian pembiayaan</th>
|
||||
<th scope="col">Didaftarkan</th>
|
||||
<th scope="col">Dikemas kini</th>
|
||||
</tr></thead>
|
||||
<tbody>
|
||||
@forelse($records as $record)
|
||||
<tr>
|
||||
<td>{{ $record->kodcaw }}</td>
|
||||
<td class="text-nowrap">{{ $record->norujukan }}</td>
|
||||
<td>{{ $record->teller }}</td>
|
||||
<td><span class="badge badge-{{ $statusColors[$record->state] ?? 'secondary' }} p-2">{{ $statusLabels[$record->state] ?? $record->state }}</span></td>
|
||||
<td class="text-nowrap">{{ $record->created_at ? $record->created_at->format('d/m/Y H:i') : '—' }}</td>
|
||||
<td class="text-nowrap">{{ $record->updated_at ? $record->updated_at->format('d/m/Y H:i') : '—' }}</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="text-center text-muted py-4">Tiada transaksi untuk tapisan ini.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{ $records->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -7,7 +7,7 @@
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb pb-3">
|
||||
<li class="breadcrumb-item"><a href="{{ route('home') }}">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('shoraka.index') }}">Pembiayaan Shoraka</a></li>
|
||||
<li class="breadcrumb-item"><a href="{{ route('shoraka.index') }}">Komoditi Shoraka</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ $norujukan }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
@@ -20,6 +20,78 @@
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if(!empty($data['simulation']))
|
||||
<div class="alert alert-warning">SIMULASI LOCAL — tiada transaksi sebenar dihantar ke Shoraka.</div>
|
||||
@endif
|
||||
@if($record && !empty($record->context['temporary_gadai']) && $record->teller === Auth::user()->name && $record->state === 'WAITING')
|
||||
<div id="shoraka-progress" class="alert alert-info" role="status">Sedang memproses pembiayaan dan menyemak komoditi Shoraka secara automatik. Sila tunggu.</div>
|
||||
<script>
|
||||
(function () {
|
||||
var started = Date.now();
|
||||
var failures = 0;
|
||||
var completing = false;
|
||||
window.shorakaProgress(2, 'Order telah dihantar. Sedang menyemak pengesahan Shoraka...');
|
||||
var panel = document.getElementById('shoraka-progress');
|
||||
function stop(message) {
|
||||
panel.className = 'alert alert-warning';
|
||||
panel.textContent = message;
|
||||
window.shorakaProgress(completing ? 3 : 2, message, true);
|
||||
}
|
||||
function poll() {
|
||||
if (Date.now() - started > 300000) {
|
||||
stop('Masih menunggu pengesahan Shoraka. Jangan hantar transaksi semula. Buka semula halaman ini untuk menyambung semakan.');
|
||||
return;
|
||||
}
|
||||
fetch(@json(route('shoraka.poll', $norujukan)), {
|
||||
method: 'POST', credentials: 'same-origin',
|
||||
headers: {'X-CSRF-TOKEN': @json(csrf_token()), 'Accept': 'application/json', 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({progress: completing ? 'complete' : 'check'})
|
||||
}).then(function (response) {
|
||||
if (!response.ok) { throw new Error('Status unavailable'); }
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
failures = 0;
|
||||
if (data.state === 'ready') {
|
||||
completing = true;
|
||||
window.shorakaProgress(3, 'Shoraka telah disahkan. Sedang menyelesaikan pembiayaan dan menyimpan gadaian...');
|
||||
window.setTimeout(poll, 0);
|
||||
return;
|
||||
}
|
||||
if (data.state === 'done') {
|
||||
window.shorakaProgress(4, 'Pembiayaan berjaya diselesaikan.', true);
|
||||
panel.className = 'alert alert-success';
|
||||
panel.textContent = 'Pembiayaan berjaya diselesaikan.';
|
||||
if (window.Swal && typeof window.Swal.fire === 'function') {
|
||||
document.getElementById('shoraka-modal-close').click();
|
||||
window.Swal.fire({
|
||||
icon: 'success',
|
||||
title: 'Pembiayaan berjaya',
|
||||
text: 'Transaksi komoditi Shoraka dan proses pembiayaan telah selesai.',
|
||||
confirmButtonText: 'OK',
|
||||
allowOutsideClick: false,
|
||||
allowEscapeKey: false
|
||||
}).then(function () { window.location.assign(data.redirect); });
|
||||
} else {
|
||||
window.alert('Pembiayaan berjaya diselesaikan.');
|
||||
window.location.assign(data.redirect);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.state === 'failed') { stop(data.message); return; }
|
||||
window.setTimeout(poll, 5000);
|
||||
}).catch(function () {
|
||||
if (++failures >= 6) {
|
||||
stop('Sambungan semakan terganggu. Jangan hantar transaksi semula. Muat semula halaman untuk menyambung semakan.');
|
||||
return;
|
||||
}
|
||||
window.setTimeout(poll, 5000);
|
||||
});
|
||||
}
|
||||
poll();
|
||||
}());
|
||||
</script>
|
||||
@else
|
||||
|
||||
@if($error)<div class="alert alert-danger">{{ $error }}</div>@endif
|
||||
@if(session('shoraka_message'))<div class="alert alert-info">{{ session('shoraka_message') }}</div>@endif
|
||||
@php
|
||||
@@ -80,6 +152,7 @@
|
||||
<a class="btn btn-secondary mb-2" href="{{ route('shoraka.certificate', $norujukan) }}">Muat Turun Sijil</a>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -420,6 +420,8 @@ Route::group(['middleware' => ['khazanah']], function() {
|
||||
Route::post('/barcodescanned',['as'=>'barcode.scanned','uses'=>'KhazanahController@barcodescanned'])->middleware('auth','khazanah');
|
||||
|
||||
//Operasi
|
||||
Route::get('/pc/shoraka', 'ShorakaController@pcIndex')->name('pc.shoraka.index')->middleware('auth', 'khazanah');
|
||||
Route::get('/operasi/shoraka', 'ShorakaController@operasiIndex')->name('operasi.shoraka.index')->middleware('auth', 'operasi');
|
||||
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');
|
||||
@@ -610,6 +612,7 @@ Route::group(['prefix' => 'shoraka', 'middleware' => ['auth', 'role:teller,penil
|
||||
Route::get('/', 'ShorakaController@index')->name('shoraka.index');
|
||||
Route::get('/{norujukan}', 'ShorakaController@show')->name('shoraka.show');
|
||||
Route::post('/{norujukan}/submit', 'ShorakaController@submit')->name('shoraka.submit');
|
||||
Route::post('/{norujukan}/poll', 'ShorakaController@poll')->name('shoraka.poll');
|
||||
Route::post('/{norujukan}/refresh', 'ShorakaController@refresh')->name('shoraka.refresh');
|
||||
Route::post('/{norujukan}/finish', 'ShorakaController@finish')->name('shoraka.finish');
|
||||
Route::get('/{norujukan}/certificate', 'ShorakaController@certificate')->name('shoraka.certificate');
|
||||
|
||||
Reference in New Issue
Block a user