16 Commits

Author SHA1 Message Date
ismailmasseran 347d99d8bd DONE: drop table shoraka financings, and improve UI checking on shoraka order status 2026-10-26 15:01:56 +08:00
ismailmasseran 9aee720bd0 DONE: remove cache, and remove dto financing 2026-10-26 12:50:02 +08:00
afrina cdc28fb010 Merge branch 'main' of https://git.koppkb.com/KoPKB/kaunter into feature/shoraka-integration 2026-10-22 13:03:49 +08:00
ismailmasseran 1138e193ae Fix/gadai online details (#30)
Build Docker Image / build-frontend (push) Successful in 46s
Deploy to Staging / deploy (push) Successful in 20s
Co-authored-by: afrina <afrina@koppkb.com>
Reviewed-on: #30
2026-10-22 11:51:55 +08:00
afrina a36cbcf930 refactor(shoraka): move financing records to master database 2026-10-19 13:35:42 +08:00
afrina 011fbfe8b2 feat(shoraka): defer gadai persistence until commodity confirmation and add simulation support 2026-10-19 13:07:10 +08:00
afrina 63d8c23785 improve shoraka UI 2026-10-13 14:20:52 +08:00
afrina 045adcaf1a fix: handle Shoraka certificate array when print SAG 2026-10-11 15:34:50 +08:00
nurafrinaalimi16 1d6baf3b1f feat(shoraka): integrate transactions with Shoraka API 2026-10-08 12:39:20 +08:00
ISMAIL MASSERAN 464d3e8dbb DONE: remove nginx for local docker compose 2026-10-01 13:45:40 +08:00
ismailmasseran abccad3219 Merge pull request 'DONE: update cicd to auto deploy on staging, clean up garbage files' (#29) from cicd/auto-deploy-staging into main
Build Docker Image / build-frontend (push) Successful in 39s
Deploy to Staging / deploy (push) Successful in 20s
Reviewed-on: #29
2026-10-01 12:01:23 +08:00
ISMAIL MASSERAN 7ad3fd4f86 DONE: update cicd to auto deploy on staging, clean up garbage files 2026-10-01 11:59:50 +08:00
ISMAIL MASSERAN 713d5a4923 DONE: update hashed password in .env.production 2026-09-28 15:58:30 +08:00
ismailmasseran b76566a2d6 Feat/logs password protection (#28)
Build Docker Image / build-frontend (push) Successful in 42s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Reviewed-on: #28
2026-09-28 15:05:56 +08:00
ismailmasseran 38f6db2e10 Merge pull request 'fix: papar cara terima pada senarai dan cetakan SAG hanya apabila pembiayaan online aktif' (#27) from fix/sag-cara-terima-feature-flag into main
Reviewed-on: #27
2026-09-28 09:44:25 +08:00
nurafrinaalimi16 f95bdf1214 fix: papar cara terima pada senarai dan cetakan SAG hanya apabila pembiayaan online aktif 2026-09-27 16:55:12 +08:00
50 changed files with 1211 additions and 502 deletions
+2
View File
@@ -50,3 +50,5 @@ MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=
LOGVIEWER_PASSWORD=
+1
View File
@@ -7,6 +7,7 @@ APP_URL=https://ujrah.erahn.com.my
API_URL=https://apiujrah.erahn.com.my
LOG_CHANNEL=stack
LOGVIEWER_PASSWORD="$2y$10$k7eiUyZdm3BruwAaT1gbmelTm.PPgZptOEQT1nlBjyiR84a4KBMlm"
DB_CONNECTION=mysql
DB_HOST=192.168.1.216
+2 -5
View File
@@ -4,17 +4,14 @@ APP_VERSION=v2.2-staging
APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8=
APP_DEBUG=true
APP_URL=https://staging.erahn.com.my
API_URL=https://api-staging.erahn.com.my
HTTP_PORT=8091
MYSQL_PUBLISH_PORT=3309
API_URL=https://api-staging.erahn.com.my
API_URL_KOOP=https://api-staging.erahn.com.my
API_URL_PKB=https://api-staging.erahn.com.my
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
LOG_LEVEL=debug
LOGVIEWER_PASSWORD=test123
DB_CONNECTION=mysql
DB_HOST=mysql
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:WRTHcFTq5YARKVdA8YDUzsh9Ue8vD63ivgZgXNNBiGo=
APP_DEBUG=true
APP_URL=http://ujrahkopkb.erahn.com.my
API_URL=http://apiujrah-kopkb.erahn.com.my
#API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kaunter_erahn
DB_USERNAME=spider
DB_PASSWORD=G0dz!ll4
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
+37 -18
View File
@@ -1,6 +1,9 @@
name: Deploy to Staging
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
image_tag:
@@ -18,10 +21,22 @@ jobs:
runs-on: host
steps:
- name: Resolve image tag
id: resolve
run: |
set -e
if [ "${{ gitea.event_name }}" = "workflow_dispatch" ]; then
IMAGE_TAG="${{ inputs.image_tag }}"
else
IMAGE_TAG="${{ gitea.ref_name }}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "Deploying image tag: ${IMAGE_TAG}"
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.image_tag }}
ref: ${{ steps.resolve.outputs.IMAGE_TAG }}
- name: Login to Docker registry
run: |
@@ -33,28 +48,35 @@ jobs:
- name: Verify image exists in registry
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
IMAGE="${APP_IMAGE}:${IMAGE_TAG}"
echo "Checking if image exists: ${IMAGE}"
if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "ERROR: Image ${IMAGE} does not exist in registry!"
echo "Run the Build Docker Image workflow first."
exit 1
fi
echo "✓ ${IMAGE} found"
# Tag pushes build in parallel — wait for the image to appear
for i in $(seq 1 30); do
if docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "✓ ${IMAGE} found"
exit 0
fi
echo "Image not ready yet (attempt ${i}/30), waiting 20s..."
sleep 20
done
echo "ERROR: Image ${IMAGE} does not exist in registry after waiting!"
echo "Run the Build Docker Image workflow first."
exit 1
- name: Pull Docker image
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
docker pull "${APP_IMAGE}:${IMAGE_TAG}"
echo "✓ Image pulled successfully"
- name: Sync compose file and deploy
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
mkdir -p "${DEPLOY_DIR}/docker/nginx" "${DEPLOY_DIR}/public"
cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml"
@@ -62,15 +84,12 @@ jobs:
find "${DEPLOY_DIR}/public" -mindepth 1 -delete
cp -R public/. "${DEPLOY_DIR}/public/"
if [ ! -f "${DEPLOY_DIR}/.env.staging" ]; then
if [ -f .env.staging ]; then
cp .env.staging "${DEPLOY_DIR}/.env.staging"
echo "Copied .env.staging from repo (first deploy). Edit ${DEPLOY_DIR}/.env.staging on the server if needed."
else
echo "ERROR: ${DEPLOY_DIR}/.env.staging is missing on the server"
exit 1
fi
if [ ! -f .env.staging ]; then
echo "ERROR: .env.staging is missing in the repository"
exit 1
fi
cp .env.staging "${DEPLOY_DIR}/.env.staging"
echo "✓ Synced .env.staging from repo"
cd "${DEPLOY_DIR}"
+12 -12
View File
@@ -125,10 +125,10 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan_gadai)->throw()->json();
$gadai_detail = $gadai[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $teller])->throw()->json();
$keluar = $getTunai2['keluar'];
$bakiakhir = $getTunai2['baki'];
@@ -143,7 +143,7 @@ class AliranTunaiController extends Controller
'keluar' => $keluar,
'masuk' => $masuk,
'bakiakhir' => $bakiakhir
]);
])->throw();
return response()->json($updateTunai,201);
@@ -153,13 +153,13 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json();
$gadai_detail = $gadai[0];
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json();
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json();
$tebus_detail = $tebus[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json();
$keluar=$getTunai2['keluar'];
$baki=$getTunai2['baki'];
@@ -181,7 +181,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}else{
@@ -199,7 +199,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}
}
@@ -208,13 +208,13 @@ class AliranTunaiController extends Controller
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->json();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/gadai/norujukan/'. $auth_user['cawangan'] .'/' .$request->norujukan)->throw()->json();
$gadai_detail = $gadai[0];
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->json();
$tebus = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/listpenebusanhistory/'. $auth_user['cawangan'],['norujukan' => $request->norujukan])->throw()->json();
$tebus_detail = $tebus[0];
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->json();
$getTunai2=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/GetCurrentTunai/'.$auth_user['cawangan'],['kodlaluan' => $tebus_detail['teller']])->throw()->json();
$keluar=$getTunai2['keluar'];
$baki=$getTunai2['baki'];
@@ -228,7 +228,7 @@ class AliranTunaiController extends Controller
'keluar'=>$keluar,
'masuk'=>$masuk,
'bakiakhir'=>$bakiakhir
]);
])->throw();
}
public function alirantunaiUpahBknTawarruq(Request $request){
+17 -19
View File
@@ -5,15 +5,12 @@ namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
use App\Dashboard;
use App\User;
use Carbon\Carbon;
class LoginController extends Controller
{
@@ -144,25 +141,26 @@ class LoginController extends Controller
}
}
public function logout(){
$current = Carbon::now();
$current = new Carbon();
$current = $current->toDateString();
public function logout(Request $request)
{
$auth_user = Auth::guard('web')->user();
Auth::logout();
Session::flush();
$session = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/logoutsession/' . $auth_user['cawangan'], [
'id' => $auth_user['id'],
'nama' => $auth_user['name'],
'nokp' => $auth_user['no_kp'],
'email' => $auth_user['email'],
'taraf' => $auth_user['role_harian'],
'tarikh' => $current,
]);
Auth::guard('web')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return Redirect::to('/');
if ($auth_user) {
Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/logoutsession/' . $auth_user['cawangan'], [
'id' => $auth_user['id'],
'nama' => $auth_user['name'],
'nokp' => $auth_user['no_kp'],
'email' => $auth_user['email'],
'taraf' => $auth_user['role_harian'],
'tarikh' => now()->toDateString(),
]);
}
return redirect('/');
}
public function GetRolesHarian(Request $request, $kodcaw){
+95 -44
View File
@@ -180,17 +180,17 @@ class GadaianController extends Controller
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'], [
Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
'norujukan' => $norujukan,
'nopelanggan' => $customer_info['nopelanggan'],
'nokp' => $noic,
'sirig' => $new_sirig
]);
'sirig' => $new_sirig,
])->throw();
} 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) {
@@ -205,12 +205,12 @@ class GadaianController extends Controller
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'], [
Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
'norujukan' => $norujukan_baru,
'nopelanggan' => $nopelanggan,
'nokp' => $nokp,
'sirig' => $new_sirig
]);
'sirig' => $new_sirig,
])->throw();
$norujukan = $norujukan_baru;
}
}
@@ -285,6 +285,15 @@ class GadaianController extends Controller
public function store(Request $request)
{
$auth_user = Auth::user();
$shoraka = app(\App\Services\ShorakaFinancingService::class);
if ($shoraka->hasOrder($request->norujukan_gadai)) {
$message = 'Order Shoraka telah dihantar. Jangan hantar semula.';
if ($request->ajax() || $request->expectsJson()) {
abort(409, $message);
}
return redirect()->back()->with('error', $message);
}
$shoraka->ensureReady();
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
@@ -342,16 +351,21 @@ class GadaianController extends Controller
'nokp' => $request->nokp,
'kadar_rebate' => $this->parseMoney($request->kadar_rebate),
'rebate' => $this->parseMoney($request->rebate),
])->json();
])->throw()->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/komuditi/transaksi', [
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $request->norujukan_gadai,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
'cara_terima' => $caraTerima,
'pinjaman' => $pinjamanAmt,
'temporary_gadai' => true,
]);
}
public function completeShorakaFinancing(Request $request)
{
$auth_user = Auth::user();
$caraTerima = $request->cara_terima;
$pinjamanAmt = round((float) $request->pinjaman, 2);
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -374,7 +388,7 @@ class GadaianController extends Controller
$msg = is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal daftar pembiayaan online. Sila cuba lagi.';
return redirect()->back()->with('error', $msg);
throw new \RuntimeException($msg);
}
$this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt);
@@ -388,9 +402,9 @@ class GadaianController extends Controller
$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.');
}
$request->session()->flash('message', $caraTerima === 'ONLINE'
? 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'
: 'Pembiayaan berjaya dihantar.');
return redirect()->route('customer_info');
}
@@ -535,31 +549,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']));
@@ -573,7 +611,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');
@@ -594,6 +632,7 @@ class GadaianController extends Controller
public function cetakSAG(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->printData($request->norujukan_modal);
$auth_user = Auth::user();
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/cetak/' . $auth_user['cawangan'] . '/' . $request->norujukan_modal);
@@ -614,6 +653,7 @@ class GadaianController extends Controller
}
foreach ($arraynorujukan as $index => $norujukan) {
app(\App\Services\ShorakaFinancingService::class)->printData($norujukan);
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/gadai/norujukan/serah/' . $auth_user['cawangan'] . '/' . $norujukan);
if ($gadai->failed()) {
@@ -671,6 +711,12 @@ class GadaianController extends Controller
// $pembayaran_segera = "https://spare.arrahn.com.my/pembayaransegera";
$pembayaran_segera = "https://pay.arrahn.com.my";
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komoditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komoditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
@@ -678,9 +724,14 @@ class GadaianController extends Controller
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komoditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
}
if ($gadai_detail['tagbaru'] == 1) {
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan'));
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan'))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
} else {
return view('print.sag', compact('gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kakitangan'));
}
@@ -156,7 +156,6 @@ class PembiayaanOnlineController extends Controller
'kodbank' => $request->input('kodbank'),
'noakaun' => $request->input('noakaun'),
'nama_bank' => $request->input('nama_bank'),
'nama_akaun' => $request->input('nama_akaun'),
'nota' => $request->input('nota'),
'update_customer_bank' => true,
];
@@ -174,6 +173,19 @@ class PembiayaanOnlineController extends Controller
$payload['bukti_akaun_type'] = $file->getMimeType() ?: 'application/octet-stream';
}
if ($request->hasFile('borang')) {
$file = $request->file('borang');
if (!$file->isValid()) {
return redirect()->back()->with('error', 'Fail borang pengesahan tidak sah.');
}
if ($file->getSize() > 4 * 1024 * 1024) {
return redirect()->back()->with('error', 'Saiz fail maksimum 4MB.');
}
$payload['borang_data'] = base64_encode(file_get_contents($file->getRealPath()));
$payload['borang_filename'] = $file->getClientOriginalName();
$payload['borang_type'] = $file->getMimeType() ?: 'application/octet-stream';
}
$res = $this->api()
->asForm()
->put($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id, $payload);
@@ -394,6 +406,22 @@ class PembiayaanOnlineController extends Controller
return $this->streamBase64File($file, 'slip-bank-' . $id);
}
/**
* PC: view uploaded Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian.
*/
public function viewBorang($id)
{
$this->abortUnlessPembiayaanOnline();
return $this->streamLineFile(
$this->kodcaw(),
$id,
'/borang',
'Borang pengesahan tidak dijumpai.',
'Fail borang pengesahan kosong.',
'borang-pengesahan-' . $id
);
}
/**
* PC: view SAG for a queue line (does not mark SAG as printed).
*/
@@ -426,6 +454,21 @@ class PembiayaanOnlineController extends Controller
return $this->streamBase64File($file, 'slip-bank-' . $id);
}
/**
* Operasi: view uploaded Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian.
*/
public function operasiViewBorang($kodcaw, $id)
{
return $this->streamLineFile(
$kodcaw,
$id,
'/borang',
'Borang pengesahan tidak dijumpai.',
'Fail borang pengesahan kosong.',
'borang-pengesahan-' . $id
);
}
/**
* Operasi: view SAG for a batch line.
*/
@@ -434,6 +477,26 @@ class PembiayaanOnlineController extends Controller
return $this->renderLineSag($kodcaw, $id);
}
protected function streamLineFile($kodcaw, $id, $path, $missingMessage, $emptyMessage, $fallbackName)
{
$res = $this->api()
->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . $path);
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : $missingMessage;
return redirect()->back()->with('error', $msg);
}
$payload = $res->json();
$file = $payload['file'] ?? null;
if (!$file || empty($file['data'])) {
return redirect()->back()->with('error', $emptyMessage);
}
return $this->streamBase64File($file, $fallbackName);
}
protected function streamBase64File(array $file, $fallbackName)
{
$raw = $file['data'];
@@ -522,6 +585,12 @@ class PembiayaanOnlineController extends Controller
$pembayaran_segera = 'https://pay.arrahn.com.my';
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komoditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komoditi = $this->api()->get($api_url . '/api/komuditi/check')->json();
$product = $this->api()
->get($api_url . '/api/komuditi/description', [
@@ -531,8 +600,13 @@ class PembiayaanOnlineController extends Controller
$nilai = is_array($komoditi) ? (float) ($komoditi['nilai'] ?? 0) : 0;
$kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0;
}
if (($gadai_detail['tagbaru'] ?? 0) == 1) {
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact(
'komoditi',
'product',
@@ -547,7 +621,7 @@ class PembiayaanOnlineController extends Controller
'mutu',
'kadar_ujrah',
'kakitangan'
));
))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
}
return view('print.sag', compact(
+24 -64
View File
@@ -271,6 +271,7 @@ class PenebusanController extends Controller
public function tambahpinjaman(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -405,33 +406,16 @@ class PenebusanController extends Controller
'upah12' => $keuntungan12bulan,
'hargajualan' => $hargajualan,
'kuantiti_marhun' => $request->kuantiti_marhun
])->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $norujukanbaru,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
]);
////////////////////////////
////Update History Gadaian///
////////////////////////////
$updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[
'historygadaian' => $request->norujukan,
'jenistebus' => $request->jenistebus,
])->json();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$this->AliranTunaiController->alirantunaiTebus($request);
])->throw()->json();
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $norujukanbaru, 'refinance', ['update_history' => true]
);
}
public function tambahpinjaman_latest(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -475,17 +459,18 @@ class PenebusanController extends Controller
])->json();
if($updatetebus['result'] == 'success'){
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$this->AliranTunaiController->alirantunaiTebus($request);
if (($updatetebus['result'] ?? null) === 'success') {
abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.');
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $updatetebus['norujukan_baru'], 'refinance'
);
}
return response()->json($updatetebus['result'],200);
return response()->json($updatetebus['result'] ?? 'error', 200);
}
public function tebusautotawarruq(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$auth_user = Auth::user();
////////////////////////
@@ -515,17 +500,18 @@ class PenebusanController extends Controller
"noic" => $request->noic,
"jenisAT" => $request->jenisAT,
])->json();
if($updatetebus['result'] == 'success'){
$alirantunai = new AliranTunaiController();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$alirantunai->alirantunaiTebusAutoTawarruq($request);
if (($updatetebus['result'] ?? null) === 'success') {
abort_unless(!empty($updatetebus['norujukan_baru']), 502, 'Rujukan gadaian baharu tidak diterima.');
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $updatetebus['norujukan_baru'], 'autotawarruq'
);
}
return response()->json($updatetebus['result'],200);
return response()->json($updatetebus['result'] ?? 'error', 200);
}
public function tebusseparuh(Request $request)
{
app(\App\Services\ShorakaFinancingService::class)->ensureReady();
$api_url = config('api.url');
$current = Carbon::now();
$current = new Carbon();
@@ -666,39 +652,13 @@ class PenebusanController extends Controller
'upah12' => $keuntungan12bulan,
'hargajualan' => $hargajualan,
'kuantiti_marhun' => $request->kuantiti_marhun
])->json();
])->throw()->json();
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/komuditi/transaksi',[
'unit_komuditi' => round($request->pinjaman, 2),
'norujukan' => $norujukanbaru,
'kodcaw' => $auth_user['cawangan'],
'teller' => $auth_user['name']
]);
////////////////////////////
////Update History Gadaian///
////////////////////////////
$updatehistorygadaian = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url').'/api/gadai/updatehistory/'. $auth_user['cawangan'] .'/' . $norujukanbaru,[
'historygadaian' => $request->norujukan,
'jenistebus' => $request->jenistebus,
])->json();
$alirantunai = new AliranTunaiController();
event(new TebusNotify('lulus',Auth::user()->cawangan));
event(new GadaiNotify('lulus',Auth::user()->cawangan));
$alirantunai->alirantunaiTebus($request);
return response()->json($updatemarhun,200);
return app(\App\Services\ShorakaFinancingService::class)->hold(
$request, $norujukanbaru, 'refinance', ['update_history' => true]
);
}
//Wakil Function
public function getfirstWakil()
{
$auth_user = Auth::user();
@@ -0,0 +1,67 @@
<?php
namespace App\Http\Controllers;
use App\Services\ShorakaFinancingService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Throwable;
class ShorakaController extends Controller
{
public function index()
{
$records = app(ShorakaFinancingService::class)->listForBranch(Auth::user()->cawangan);
$cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan];
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',
'status' => 'nullable|in:PENDING,Active,Completed,Cancelled,UNKNOWN,FAILED',
'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' : ''),
]);
[$records, $branches] = app(ShorakaFinancingService::class)->search(
$filters,
$isOperasi ? null : Auth::user()->cawangan
);
$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)
{
$data = null;
$error = null;
try {
$data = $service->snapshot($norujukan);
} catch (Throwable $exception) {
$error = 'Maklumat Shoraka tidak dapat diambil. Bayaran dan SAG masih ditahan.';
}
$cawangan_info = ['details_caw' => Auth::user()->cawangan, 'kodcaw' => Auth::user()->cawangan];
return view('shoraka.show', compact('norujukan', 'data', 'error', 'cawangan_info'));
}
public function certificate(string $norujukan, ShorakaFinancingService $service)
{
$response = $service->api()->get($service->url($norujukan, '/certificate'))->throw();
abort_unless(substr($response->body(), 0, 5) === '%PDF-', 502);
return response($response->body(), 200, [
'Content-Type' => 'application/pdf',
'Content-Disposition' => 'attachment; filename="sijil-shoraka.pdf"',
'Cache-Control' => 'private, no-store',
]);
}
}
+32
View File
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
class LogViewerPassword
{
public function handle($request, Closure $next)
{
$passwordHash = config('logviewer.password');
if (!is_string($passwordHash) || $passwordHash === '') {
return response('Akses log belum dikonfigurasi.', 503)
->header('Cache-Control', 'no-store');
}
$provided = $request->getPassword();
if ($request->getUser() !== 'logviewer' || !is_string($provided) ||
!password_verify($provided, $passwordHash)) {
return response('Kata laluan diperlukan untuk mengakses log.', 401, [
'WWW-Authenticate' => 'Basic realm="Log Viewer", charset="UTF-8"',
'Cache-Control' => 'no-store',
]);
}
$response = $next($request);
$response->headers->set('Cache-Control', 'no-store');
return $response;
}
}
+6
View File
@@ -44,6 +44,12 @@ class RoleAccess
return $next($request);
}
break;
case 'masteradmin':
if ($userRole == 'masteradmin') {
return $next($request);
}
break;
case 'penilai':
if ($userRole == 'penilai') {
+215
View File
@@ -0,0 +1,215 @@
<?php
namespace App\Services;
use App\Http\Controllers\GadaianController;
use App\Http\Controllers\AliranTunaiController;
use App\Events\Dashboard\GadaiNotify;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Http;
use Throwable;
class ShorakaFinancingService
{
public function ensureReady(): void
{
$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.');
}
public function api()
{
return Http::withOptions(['verify' => config('app.api_verify')])
->acceptJson()->timeout(75);
}
public function url(string $reference, string $suffix = ''): string
{
return rtrim(config('api.url'), '/') . '/api/shoraka/gadai/'
. rawurlencode(Auth::user()->cawangan) . '/' . rawurlencode($reference) . $suffix;
}
public function hasOrder(string $reference): bool
{
$snapshot = $this->snapshot($reference);
return !empty($snapshot['order']);
}
public function snapshot(string $reference): array
{
$response = $this->api()->get($this->url($reference));
if ($response->status() === 404) {
return [];
}
if ($response->serverError()) {
abort(503, 'Shoraka belum dikonfigurasi. Pembiayaan baharu ditahan.');
}
$response->throw();
return $response->json()['data'] ?? [];
}
public function listForBranch(string $branch)
{
$response = $this->api()->get(rtrim(config('api.url'), '/') . '/api/shoraka/transactions', [
'kodcaw' => $branch,
'per_page' => 100,
])->throw()->json();
return collect($response['data'] ?? []);
}
public function search(array $filters, ?string $onlyBranch): array
{
$query = [
'per_page' => 25,
'page' => max(1, (int) request()->input('page', 1)),
'with_branches' => 1,
];
if ($onlyBranch) {
$query['kodcaw'] = $onlyBranch;
$query['branch_scope'] = $onlyBranch;
} elseif (!empty($filters['kodcaw'])) {
$query['kodcaw'] = $filters['kodcaw'];
}
foreach (['norujukan', 'status', 'date_from', 'date_to'] as $field) {
if (!empty($filters[$field])) {
$query[$field] = $filters[$field];
}
}
$response = $this->api()->get(rtrim(config('api.url'), '/') . '/api/shoraka/transactions', $query)->throw()->json();
$items = collect($response['data'] ?? []);
$records = new LengthAwarePaginator(
$items,
(int) ($response['total'] ?? 0),
(int) ($response['per_page'] ?? 25),
(int) ($response['current_page'] ?? 1),
['path' => request()->url(), 'query' => request()->query()]
);
return [$records, collect($response['branches'] ?? [])];
}
public function hold(Request $request, string $reference, string $action, array $extra = [])
{
if ($this->hasOrder($reference)) {
return $this->failed($request, 'Order Shoraka telah dihantar. Jangan hantar semula.');
}
$context = array_merge($request->except(['_token', 'password']), $extra, ['action' => $action]);
try {
$response = $this->api()->post($this->url($reference, '/submit'), [
'teller' => Auth::user()->name,
]);
} catch (Throwable $exception) {
if ($this->hasOrder($reference)) {
return $this->failed($request, 'Order Shoraka telah dihantar. Jangan hantar semula.');
}
return $this->failed($request, 'Order Shoraka tidak berjaya dihantar. Gadaian tidak dihantar.');
}
if (!$response->successful()) {
$message = $response->json()['message'] ?? ($this->hasOrder($reference)
? 'Order Shoraka telah dihantar. Jangan hantar semula.'
: 'Order Shoraka tidak berjaya dihantar. Gadaian tidak dihantar.');
return $this->failed($request, $message);
}
$snapshot = $this->waitUntilCompleted($reference);
$status = $snapshot['order']['status'] ?? null;
if ($status !== 'Completed') {
return $this->failed($request, 'Pengesahan Shoraka belum Completed. Jangan hantar semula.');
}
if (empty($snapshot['can_complete'])
|| (array_key_exists('pinjaman', $context)
&& number_format((float) $context['pinjaman'], 2, '.', '')
!== number_format((float) ($snapshot['order_amount'] ?? 0), 2, '.', ''))) {
return $this->failed($request, 'Amaun pembiayaan berubah. Bayaran tidak diproses.');
}
return $this->complete($request, $reference, $context);
}
private function waitUntilCompleted(string $reference): array
{
set_time_limit(150);
$deadline = microtime(true) + 120;
$snapshot = [];
do {
try {
$this->api()->post($this->url($reference, '/refresh'))->throw();
$snapshot = $this->snapshot($reference);
} catch (Throwable $exception) {
$snapshot = [];
}
$status = $snapshot['order']['status'] ?? null;
if ($status === 'Completed' || in_array($status, ['Cancelled', 'FAILED', 'UNKNOWN'], true)) {
return $snapshot;
}
if (microtime(true) >= $deadline) {
return $snapshot;
}
sleep(2);
} while (true);
}
private function complete(Request $request, string $reference, array $context)
{
$branch = Auth::user()->cawangan;
$saved = new Request($context);
$saved->setLaravelSession($request->session());
if (($context['action'] ?? null) === 'gadai') {
return app(GadaianController::class)->completeShorakaFinancing($saved);
}
if (!empty($context['update_history'])) {
$this->api()->put(rtrim(config('api.url'), '/') . '/api/gadai/updatehistory/'
. rawurlencode($branch) . '/' . rawurlencode($reference), [
'historygadaian' => $saved->norujukan,
'jenistebus' => $saved->jenistebus,
])->throw();
}
$cash = app(AliranTunaiController::class);
if (($context['action'] ?? null) === 'autotawarruq') {
$cash->alirantunaiTebusAutoTawarruq($saved);
} else {
$cash->alirantunaiTebus($saved);
}
event(new \App\Events\Dashboard\TebusNotify('lulus', $branch));
event(new GadaiNotify('lulus', $branch));
return redirect()->route('customer_info');
}
public function printData(string $reference): ?array
{
$response = $this->api()->get($this->url($reference))->throw()->json()['data'];
$order = $response['order'] ?? null;
if (!$order) {
return null;
}
abort_unless(!empty($response['can_complete']), 409, 'SAG ditahan: Shoraka belum Completed.');
$details = $response['details'] ?? [];
$certificateDetails = $details['certificateDetails1'] ?? '';
if (is_array($certificateDetails)) {
$certificateDetails = $certificateDetails[0] ?? '';
}
$certificate = is_string($certificateDetails) ? trim(explode('|', $certificateDetails)[0]) : '';
abort_unless($certificate !== '' && isset($details['unit'], $details['volume'], $details['commodityType']),
409, 'Maklumat sijil belum lengkap. Semak status Shoraka dahulu.');
return [
'komoditi' => ['no_sijil' => $certificate],
'product' => [
'kodproduk' => $details['commodityType'],
'description' => $details['commodityType'] === '000-COPPER' ? 'Copper' : 'Plastic resin',
'unit' => $details['unit'],
],
'kuantiti' => $details['volume'],
];
}
private function failed(Request $request, string $message)
{
if ($request->ajax() || $request->expectsJson()) {
abort(502, $message);
}
return redirect()->back()->with('error', $message);
}
}
+2 -2
View File
@@ -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
View File
@@ -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'),
];
+1
View File
@@ -1,6 +1,7 @@
<?php
return [
'password' => env('LOGVIEWER_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Pattern and storage path settings
+3 -10
View File
@@ -5,6 +5,8 @@ services:
dockerfile: docker/php/Dockerfile
target: development
working_dir: /var/www/html
ports:
- "8081:8000"
volumes:
- ./:/var/www/html:cached
environment:
@@ -15,16 +17,7 @@ services:
- "host.docker.internal:host-gateway"
depends_on:
- mysql
web:
image: nginx:1.25-alpine
ports:
- "8081:80"
volumes:
- ./:/var/www/html:cached
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
command: ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
mysql:
image: mysql:8.0
+11 -2
View File
@@ -2,7 +2,14 @@ FROM php:7.4-fpm AS base
WORKDIR /var/www/html
RUN apt-get update \
# PHP 7.4 uses Debian Bullseye; use its archived package repositories.
# Archived security metadata expires, but package signatures remain verified.
RUN printf '%s\n' \
'deb https://archive.debian.org/debian bullseye main' \
'deb [check-valid-until=no] https://archive.debian.org/debian-security bullseye-security main' \
> /etc/apt/sources.list \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get -o Acquire::Retries=3 update \
&& apt-get install -y --no-install-recommends \
git \
pkg-config \
@@ -28,7 +35,9 @@ COPY docker/php/php.ini /usr/local/etc/php/conf.d/app.ini
COPY docker/php/zz-clear-env.conf /usr/local/etc/php-fpm.d/zz-clear-env.conf
COPY docker/php/entrypoint.sh /usr/local/bin/app-entrypoint.sh
RUN chmod +x /usr/local/bin/app-entrypoint.sh \
# Normalize Windows checkout line endings before executing the script.
RUN sed -i 's/\r$//' /usr/local/bin/app-entrypoint.sh \
&& chmod +x /usr/local/bin/app-entrypoint.sh \
&& usermod -u 1000 www-data && groupmod -g 1000 www-data
ENTRYPOINT ["app-entrypoint.sh"]
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone2
DB_PORT=3306
DB_DATABASE=kaunterpkb
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.kgmk.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone_franchise
DB_PORT=3306
DB_DATABASE=kaunter_koopguru
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.krk.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone_franchise
DB_PORT=3306
DB_DATABASE=kaunter_krk
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
@@ -2814,25 +2814,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+7 -7
View File
@@ -461,25 +461,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) <= min_komoditi){
if (!data.ready){
$("#daftar_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+15 -9
View File
@@ -758,25 +758,25 @@
} else {
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success: function(data) {
var check_komuditi = Object.keys(data).length;
if (check_komuditi == 0) {
if (!data.ready) {
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
if (Number(data['unit_komuditi']) <= min_komoditi) {
if (!data.ready) {
$("#daftar_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -801,9 +801,12 @@
}
});
var pembiayaanOnlineEnabled = @json(!empty(($cawangan_info['features'] ?? [])['pembiayaan_online']));
var tablegadai = initErahnDatatable('#myTable', {
order: [[1, 'desc']],
columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' },
{ targets: -1, orderable: false, searchable: false, width: '70px' }
]
@@ -811,6 +814,7 @@
var xtawarruq = initErahnDatatable('#xtawarruq', {
columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' }
]
});
@@ -2067,7 +2071,8 @@
harini.setHours(0, 0, 0, 0);
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text();
let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
if (+harini !== +tarikhgadai2) {
@@ -2484,7 +2489,8 @@
}
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text();
let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
+6 -5
View File
@@ -5,6 +5,9 @@
@section('content')
@include('modal.editmarhun')
<div class="container">
@if(session('gadai_edit_error'))
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
@endif
<div class="row justify-content-center">
<div class="col-md-12 col-lg-12">
<h3 class="text-center text-uppercase">Pembiayaan</h3>
@@ -946,6 +949,7 @@
text: `Komen : ${data.komen}`
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
$(func).submit();
return true;
});
@@ -1058,6 +1062,7 @@
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
$(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.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
return true;
}
}
+10 -10
View File
@@ -819,25 +819,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) < pinjaman){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -1162,9 +1162,9 @@
return false;
} else {
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
icon: 'info',
title: 'Sedang diproses',
text: 'Sedang menghantar pembiayaan untuk pengesahan Shoraka.'
});
$(func).attr("onSubmit","return true;");
+6 -5
View File
@@ -5,6 +5,9 @@
@section('content')
@include('modal.editmarhun')
<div class="container">
@if(session('gadai_edit_error'))
<div class="alert alert-danger" role="alert">{{ session('gadai_edit_error') }}</div>
@endif
<div class="row justify-content-center">
<div class="col-md-12 col-lg-12">
<h3 class="text-center text-uppercase">Pembiayaan</h3>
@@ -1612,6 +1615,7 @@
text: `Komen : ${data.komen}`
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
$(func).submit();
return true;
});
@@ -1724,6 +1728,7 @@
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
$(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.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan').val()));
return true;
}
}
+10 -14
View File
@@ -435,7 +435,6 @@
text: @json(session('error'))
});
@endif
$.ajax({
method:'GET',
url: api_url + '/api/jemas',
@@ -1655,25 +1654,25 @@
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
url: api_url + '/api/shoraka/readiness',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) < pinjaman){
if (!data.ready){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -1787,6 +1786,7 @@
}).then(function(){
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val()));
$(func).submit();
return true;
});
@@ -1936,6 +1936,7 @@
$(func).attr("onSubmit","return true;");
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val()));
$(func).submit();
return true;
@@ -1999,12 +2000,7 @@
return false;
} else {
//insert code asal
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
});
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val()));
$(func).attr("onSubmit","return true;");
$(func).submit();
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>
+54 -3
View File
@@ -1,16 +1,67 @@
@extends('layouts.app')
@section('content')
<style>
.teller-home-card {
border: 0;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(31, 45, 61, .12);
}
.teller-home-card .card-header {
padding: 20px;
border: 0;
font-size: 20px;
font-weight: 600;
}
.teller-home-menu {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 16px;
padding: 28px;
}
.teller-home-menu .btn {
display: flex;
align-items: center;
justify-content: center;
min-height: 88px;
padding: 18px 16px;
border-radius: 8px;
white-space: normal;
font-size: 18px;
font-weight: 600;
letter-spacing: .3px;
transition: background-color .15s ease, box-shadow .15s ease;
}
.teller-home-menu .btn:hover,
.teller-home-menu .btn:focus-visible {
box-shadow: 0 4px 12px rgba(23, 162, 184, .25);
}
@media (max-width: 575.98px) {
.teller-home-menu {
grid-template-columns: minmax(0, 1fr);
gap: 12px;
padding: 20px;
}
.teller-home-menu .btn {
min-height: 68px;
font-size: 16px;
}
}
</style>
<div class="container">
<div class="row justify-content-center" style="margin-top:60px;">
<div class="col-md-8">
<div class="card">
<div class="card-header bg-secondary text-center"><span style="font-weight:500;">{{ __('Paparan Utama') }}</span></div>
<div class="card teller-home-card">
<div class="card-header bg-secondary text-center">{{ __('Paparan Utama') }}</div>
<div class="card-body text-center">
<div class="card-body teller-home-menu text-center">
<a class="btn btn-info btn-lg" href="#" onclick="pelangganclick()">PELANGGAN</a>
<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') }}">KOMODITI SHORAKA</a>
@endif
{{-- <a class="btn btn-info btn-lg" href="#">TETAPAN</a> --}}
</div>
</div>
+1
View File
@@ -405,6 +405,7 @@
@endif
<main class="py-4">
@include('shoraka.navigation')
@yield('content')
</main>
</div>
@@ -74,7 +74,7 @@
id="batch_lines"
class="table-sm text-center"
:filename="'Batch_' . ($batch['batch_no'] ?? $batch['id'])"
:columns="['Bil', 'Status', 'SAG', 'Pelanggan', 'No KP', 'Bank', 'No Akaun', 'Nama Akaun', 'Pinjaman (RM)', 'Rujukan Bayaran', 'Dokumen', 'Tindakan']"
:columns="['Bil', 'Status', 'SAG', 'Pelanggan', 'No KP', 'Bank', 'No Akaun', 'Pinjaman (RM)', 'Rujukan Bayaran', 'Dokumen', 'Tindakan']"
:skipExport="['Dokumen', 'Tindakan']"
>
@foreach($lines as $line)
@@ -94,7 +94,6 @@
<td>{{ $line['nokp'] ?? '-' }}</td>
<td>{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}</td>
<td>{{ $line['noakaun'] ?? '-' }}</td>
<td class="text-left">{{ $line['nama_akaun'] ?? '-' }}</td>
<td class="text-right">{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}</td>
<td>{{ $line['no_rujukan_bayaran'] ?? '-' }}</td>
<td class="text-nowrap">
@@ -113,6 +112,13 @@
@else
<span class="text-muted small">Tiada slip</span>
@endif
@if(!empty($line['borang_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.operasi.borang', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
target="_blank">
Borang Pengesahan
</a>
@endif
</td>
<td class="text-nowrap">
@if(in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true))
@@ -70,6 +70,13 @@
@else
<span class="text-muted small">Tiada slip</span>
@endif
@if(!empty($line['borang_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.pc.borang', ['id' => $line['id']]) }}"
target="_blank">
Borang Pengesahan
</a>
@endif
</td>
</tr>
@endforeach
@@ -274,11 +274,11 @@
data-nokp="{{ $line['nokp'] ?? '' }}"
data-namabank="{{ $line['nama_bank'] ?? $line['kodbank'] ?? '' }}"
data-noakaun="{{ $line['noakaun'] ?? '' }}"
data-namaakaun="{{ $line['nama_akaun'] ?? '' }}"
data-pinjaman="{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}"
data-nota="{{ $line['nota'] ?? '' }}"
data-sebab="{{ $line['reject_reason'] ?? '' }}"
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}">
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}"
data-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Semak
</button>
@endif
@@ -286,11 +286,11 @@
data-id="{{ $line['id'] }}"
data-kodbank="{{ $line['kodbank'] ?? '' }}"
data-noakaun="{{ $line['noakaun'] ?? '' }}"
data-namaakaun="{{ $line['nama_akaun'] ?? '' }}"
data-namabank="{{ $line['nama_bank'] ?? '' }}"
data-nota="{{ $line['nota'] ?? '' }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}"
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}">
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}"
data-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Edit
</button>
</td>
@@ -390,10 +390,6 @@
<label>No. Akaun</label>
<input type="text" name="noakaun" id="editNoakaun" class="form-control" required>
</div>
<div class="form-group">
<label>Nama Akaun</label>
<input type="text" name="nama_akaun" id="editNamaakaun" class="form-control">
</div>
<div class="form-group">
<label>Slip Bank / Bukti Akaun</label>
<input type="file" name="bukti_akaun" id="editBuktiAkaun" class="form-control-file"
@@ -405,6 +401,17 @@
</div>
<small class="text-muted">PDF / imej slip akaun bank pelanggan (maks 4MB)</small>
</div>
<div class="form-group">
<label>Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian (Online)</label>
<input type="file" name="borang" id="editBorang" class="form-control-file"
accept=".pdf,.jpg,.jpeg,.png,.gif">
<div id="editBorangExisting" class="mt-2" style="display:none;">
<a href="#" id="editBorangLink" class="btn btn-sm btn-outline-primary" target="_blank">
Lihat borang sedia ada
</a>
</div>
<small class="text-muted">PDF / imej borang yang ditandatangani pelanggan (maks 4MB). Kosongkan jika tidak mahu ganti fail sedia ada.</small>
</div>
<div class="form-group">
<label>Nota</label>
<textarea name="nota" id="editNota" class="form-control" rows="2"></textarea>
@@ -462,10 +469,6 @@
<th>No Akaun</th>
<td id="verifyNoakaun"></td>
</tr>
<tr>
<th>Nama Akaun</th>
<td id="verifyNamaakaun"></td>
</tr>
<tr>
<th>Pinjaman</th>
<td>RM <span id="verifyPinjaman"></span></td>
@@ -485,9 +488,12 @@
Saya sahkan maklumat pelanggan, bank dan amaun adalah betul.
</label>
</div>
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary mb-2" target="_blank" style="display:none;">
Lihat Bukti Akaun Bank
</a>
<a href="#" id="verifyBorangLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
Lihat Borang Pengesahan
</a>
</div>
<div class="col-md-8">
<label class="d-block">Pratonton SAG</label>
@@ -576,7 +582,6 @@
$('#verifyNokp').text($(this).data('nokp') || '-');
$('#verifyBank').text($(this).data('namabank') || '-');
$('#verifyNoakaun').text($(this).data('noakaun') || '-');
$('#verifyNamaakaun').text($(this).data('namaakaun') || '-');
$('#verifyPinjaman').text($(this).data('pinjaman') || '0.00');
$('#verifyNota').text($(this).data('nota') || '-');
if (sebab) {
@@ -592,6 +597,11 @@
} else {
$('#verifyBuktiAkaunLink').hide();
}
if ($(this).data('borangFile')) {
$('#verifyBorangLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/borang").show();
} else {
$('#verifyBorangLink').hide();
}
$('#verifyModal').modal('show');
});
@@ -605,10 +615,10 @@
$('#editNorujukan').text($(this).data('norujukan'));
$('#editKodbank').val($(this).data('kodbank'));
$('#editNoakaun').val($(this).data('noakaun'));
$('#editNamaakaun').val($(this).data('namaakaun'));
$('#editNamabank').val($(this).data('namabank'));
$('#editNota').val($(this).data('nota'));
$('#editBuktiAkaun').val('');
$('#editBorang').val('');
$('#editSagFrame').attr('src', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/sag");
if ($(this).data('buktiAkaun')) {
$('#editBuktiAkaunLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/bukti-akaun");
@@ -616,6 +626,12 @@
} else {
$('#editBuktiAkaunExisting').hide();
}
if ($(this).data('borangFile')) {
$('#editBorangLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/borang");
$('#editBorangExisting').show();
} else {
$('#editBorangExisting').hide();
}
$('#editModal').modal('show');
});
+7 -7
View File
@@ -2022,25 +2022,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -2258,25 +2258,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
+21 -21
View File
@@ -2264,27 +2264,27 @@
let komuditiblock = false;
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
async:false,
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
@@ -2646,29 +2646,29 @@
let komuditiblock = false;
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
async:false,
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.close();
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.close();
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
komuditiblock = true;
event.preventDefault();
@@ -3106,25 +3106,25 @@
$.ajax({
method:'get',
url: api + '/api/komuditi/check',
url: api + '/api/shoraka/readiness',
success:function(komoditi){
var check_komuditi = Object.keys(komoditi).length;
if(check_komuditi == 0){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
}else{
if(Number(komoditi['unit_komuditi']) < pinjaman_baru){
if (!komoditi.ready){
$("#terima_transaksi").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
event.preventDefault();
return false;
@@ -258,7 +258,7 @@
<th style="width:13%">No. KP</th>
<th style="width:11%">Bank</th>
<th style="width:13%">No. Akaun</th>
<th>Nama Akaun</th>
<th>No. Telefon</th>
<th style="width:12%">Pinjaman (RM)</th>
</tr>
</thead>
@@ -271,7 +271,7 @@
<td class="num">{{ $line['nokp'] ?? '-' }}</td>
<td>{{ $line['nama_bank'] ?? ($line['kodbank'] ?? '-') }}</td>
<td class="num">{{ $line['noakaun'] ?? '-' }}</td>
<td>{{ $line['nama_akaun'] ?? '-' }}</td>
<td>{{ !empty($line['telefon']) ? $line['telefon'] : '-' }}</td>
<td class="amt">{{ number_format((float) ($line['pinjaman'] ?? 0), 2) }}</td>
</tr>
@empty
@@ -306,7 +306,7 @@
<div class="signs">
<div class="sign-box">
<div class="role">Tandatangan Operasi</div>
<div class="role">Disemak oleh</div>
<div class="sign-line">
<p>Nama: {{ $opsBy }}</p>
<p>Jawatan: Operasi</p>
@@ -314,10 +314,10 @@
</div>
</div>
<div class="sign-box">
<div class="role">Pengurus Besar</div>
<div class="role">Disahkan oleh</div>
<div class="sign-line">
<p>Nama:</p>
<p>Cop:</p>
<p>Jawatan:</p>
<p>Tarikh:</p>
</div>
</div>
+8 -1
View File
@@ -556,9 +556,16 @@
</table>
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json();
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/check')->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])->get($api_url . '/api/komuditi/description',['kodproduk'=>$komuditi['kodproduct']])->json();
$kuantiti = $gadai_detail['pinjaman']/$komuditi['nilai'];
}
@endphp
<table style="margin-bottom:10px;" id="keuntungan">
+8 -1
View File
@@ -654,13 +654,20 @@
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai'];
}
@endphp
<table style="margin-bottom:10px;" id="keuntungan">
+8 -1
View File
@@ -713,13 +713,20 @@
</tr>
</table>
@php
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
$shorakaPrint = app(\App\Services\ShorakaFinancingService::class)->printData($gadai_detail['norujukan']);
if ($shorakaPrint) {
$komuditi = $shorakaPrint['komoditi'];
$product = $shorakaPrint['product'];
$kuantiti = $shorakaPrint['kuantiti'];
} else {
$komuditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/check')
->json();
$product = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/komuditi/description', ['kodproduk' => $komuditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komuditi['nilai'];
}
@endphp
<table id="keuntungan" style="margin-bottom:10px;">
+4 -2
View File
@@ -615,7 +615,7 @@
<td>{{ $komoditi['no_sijil'] }}</td>
<td class="text-center">4</td>
<td>Kuantiti (Unit)</td>
<td>{{ number_format($kuantiti, 2) }} TAN</td>
<td>{{ $kuantiti }} TONNES</td>
</tr>
<tr>
<td class="text-center">2</td>
@@ -1849,7 +1849,7 @@
<td>{{ $komoditi['no_sijil'] }}</td>
<td class="text-center">4</td>
<td>Kuantiti</td>
<td>{{ number_format($kuantiti, 2) }}</td>
<td>{{ $kuantiti }}</td>
</tr>
<tr>
<td class="text-center">2</td>
@@ -2112,6 +2112,7 @@
</div>
<div class="main-page" style="font-family: Arial, Helvetica, sans-serif;">
<div class="sub-page" style="font-size: 11px;">
@if ($pembiayaanOnlineEnabled ?? false)
@php
$caraTerimaBayaran = strtoupper(trim((string) ($gadai_detail['cara_terima'] ?? '')));
@endphp
@@ -2128,6 +2129,7 @@
</span>
</div>
</div>
@endif
{{-- HEADER SECTION --}}
<table style="width: 100%; margin-top: 0;">
<tr style="vertical-align: top;">
+69
View File
@@ -0,0 +1,69 @@
@extends('layouts.app')
@section('content')
<style>
.shoraka-list-card .table th,
.shoraka-list-card .table td {
vertical-align: middle;
}
.shoraka-list-card .status-badge {
padding: 8px 12px;
font-size: 13px;
font-weight: 500;
white-space: normal;
line-height: 1.5;
}
</style>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<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">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">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>
</div>
<div class="card-body">
<p class="text-muted mb-4">Semak transaksi komoditi Shoraka cawangan ini.</p>
<div class="table-responsive">
<table class="table table-striped table-bordered table-hover mb-0">
<thead class="thead-dark">
<tr>
<th scope="col">No. rujukan</th>
<th scope="col">Teller</th>
<th scope="col">Status komoditi</th>
<th scope="col" class="text-center">Tindakan</th>
</tr>
</thead>
<tbody>
@forelse($records as $record)
<tr>
<td class="text-nowrap">{{ $record['norujukan'] }}</td>
<td>{{ $record['teller'] ?: '—' }}</td>
<td>
<span class="badge badge-info status-badge">{{ $record['status'] ?: '—' }}</span>
</td>
<td class="text-center">
<a class="btn btn-info btn-sm" href="{{ route('shoraka.show', $record['norujukan']) }}" aria-label="Buka pembiayaan {{ $record['norujukan'] }}">Buka</a>
</td>
</tr>
@empty
<tr><td colspan="4" class="text-center text-muted py-5">Tiada transaksi.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
@endsection
@@ -0,0 +1,63 @@
<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">Menyimpan gadaian dan menghantar order</li>
<li data-shoraka-step="2">Menunggu pengesahan Shoraka</li>
<li data-shoraka-step="3">Menyelesaikan pembiayaan</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>
</div>
</div>
<script>
window.shorakaProgress = (function () {
var modal = document.getElementById('shoraka-modal');
var previousFocus;
modal.addEventListener('keydown', function (event) {
if (event.key === 'Tab') {
event.preventDefault();
modal.querySelector('[role="dialog"]').focus();
}
});
return function (step, message, stopped) {
if (stopped) {
modal.hidden = true;
modal.style.display = 'none';
if (previousFocus) { previousFocus.focus(); }
return;
}
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'); }
});
modal.querySelector('[role="dialog"]').focus();
};
}());
window.shorakaTrack = function (statusUrl) {
if (window.shorakaTrackTimer) { clearInterval(window.shorakaTrackTimer); }
window.shorakaProgress(1, 'Menyimpan gadaian dan menghantar order...');
window.shorakaTrackTimer = setInterval(function () {
$.get(statusUrl).done(function (body) {
var order = body && body.data && body.data.order;
var status = order && order.status;
if (status === 'Completed') {
clearInterval(window.shorakaTrackTimer);
window.shorakaProgress(3, 'Menyelesaikan pembiayaan...');
} else if (status === 'Cancelled' || status === 'FAILED' || status === 'UNKNOWN') {
clearInterval(window.shorakaTrackTimer);
window.shorakaProgress(2, 'Transaksi Shoraka gagal. Sila tunggu...');
} else if (status) {
window.shorakaProgress(2, 'Menunggu pengesahan Shoraka...');
}
});
}, 1500);
};
</script>
+96
View File
@@ -0,0 +1,96 @@
@extends($isOperasi ? 'layouts.app_operasi' : 'layouts.app')
@section('content')
@php
$listRoute = $isOperasi ? 'operasi.shoraka.index' : 'pc.shoraka.index';
$statusLabels = ['PENDING' => 'PENDING', 'Active' => 'Active', 'Completed' => 'Completed', 'Cancelled' => 'Cancelled', 'UNKNOWN' => 'UNKNOWN', 'FAILED' => 'FAILED'];
@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 transaksi komoditi Shoraka {{ $isOperasi ? 'semua cawangan' : 'cawangan sendiri' }}.</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-status">Status komoditi</label>
<select id="shoraka-status" name="status" class="form-control">
<option value="">Semua status</option>
@foreach($statusLabels as $status => $label)
<option value="{{ $status }}" {{ ($filters['status'] ?? '') === $status ? '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">Status komoditi</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-info p-2">{{ $record['status'] ?: '—' }}</span></td>
<td class="text-nowrap">{{ !empty($record['created_at']) ? \Carbon\Carbon::parse($record['created_at'])->format('d/m/Y H:i') : '—' }}</td>
<td class="text-nowrap">{{ !empty($record['updated_at']) ? \Carbon\Carbon::parse($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
+71
View File
@@ -0,0 +1,71 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<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') }}">Komoditi Shoraka</a></li>
<li class="breadcrumb-item active" aria-current="page">{{ $norujukan }}</li>
</ol>
</nav>
<div class="card">
<div class="card-header bg-secondary d-flex flex-wrap align-items-center justify-content-between">
<h5 class="mb-0 py-2">Komoditi Shoraka</h5>
<a class="btn btn-info btn-sm my-1" href="{{ route('shoraka.index') }}">
<i class="fas fa-arrow-left mr-1" aria-hidden="true"></i> Kembali ke senarai
</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
@php
$order = $data['order'] ?? null;
$status = $order['status'] ?? 'Belum dihantar';
$ready = !empty($data['can_complete']);
@endphp
@if($error)<div class="alert alert-danger">{{ $error }}</div>@endif
<p class="text-muted mb-3">No. rujukan: <strong class="text-dark">{{ $norujukan }}</strong></p>
<div class="table-responsive mb-4">
<table class="table table-striped table-bordered mb-0">
<tbody>
<tr>
<th scope="row" class="align-middle">Status komoditi</th>
<td><span class="badge badge-info px-3 py-2">{{ $status }}</span></td>
</tr>
<tr>
<th scope="row">Order ID</th>
<td>{{ $order['order_id'] ?? '—' }}</td>
</tr>
<tr>
<th scope="row">Pembiayaan</th>
<td class="text-nowrap font-weight-bold">RM {{ number_format($data['order_amount'] ?? 0, 2) }}</td>
</tr>
<tr>
<th scope="row">Harga jualan</th>
<td class="text-nowrap font-weight-bold">RM {{ number_format($data['murabaha_amount'] ?? 0, 2) }}</td>
</tr>
</tbody>
</table>
</div>
@if(!$ready)
<div class="alert alert-warning">Bayaran pembiayaan dan SAG ditahan sehingga transaksi komoditi Completed.</div>
@endif
@if(in_array($status, ['UNKNOWN', 'FAILED', 'PENDING']))
<p>Jangan hantar order sekali lagi. Hubungi operasi untuk semakan dengan Shoraka.</p>
@endif
@if($ready)
<div class="d-flex flex-wrap pt-3 mt-4 border-top">
<a class="btn btn-secondary mb-2" href="{{ route('shoraka.certificate', $norujukan) }}">Muat Turun Sijil</a>
</div>
@endif
</div>
</div>
</div>
</div>
</div>
@endsection
+17 -2
View File
@@ -63,7 +63,13 @@ Route::get('test', function () {
return "event test je";
});
Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index');
Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index')
->middleware([
'auth',
'role:admin,masteradmin,operasi',
'throttle:10,1',
\App\Http\Middleware\LogViewerPassword::class,
]);
Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']);
Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']);
@@ -206,6 +212,7 @@ Route::put('/pembiayaan-online/pc/lines/{id}',['as'=>'pembiayaan.online.pc.updat
Route::post('/pembiayaan-online/pc/lines/{id}/verify',['as'=>'pembiayaan.online.pc.verify','uses'=>'PembiayaanOnlineController@verifyLine'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/lines/{id}/unverify',['as'=>'pembiayaan.online.pc.unverify','uses'=>'PembiayaanOnlineController@unverifyLine'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/bukti-akaun',['as'=>'pembiayaan.online.pc.bukti.akaun','uses'=>'PembiayaanOnlineController@viewBuktiAkaun'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/borang',['as'=>'pembiayaan.online.pc.borang','uses'=>'PembiayaanOnlineController@viewBorang'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/sag',['as'=>'pembiayaan.online.pc.sag','uses'=>'PembiayaanOnlineController@viewSag'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/batches/submit',['as'=>'pembiayaan.online.pc.submit','uses'=>'PembiayaanOnlineController@submitBatch'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/batches/{batchId}',['as'=>'pembiayaan.online.pc.batch','uses'=>'PembiayaanOnlineController@showBatch'])->middleware('auth','khazanah');
@@ -232,6 +239,7 @@ Route::post('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/approve',['as
Route::post('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/paid',['as'=>'pembiayaan.online.operasi.paid','uses'=>'PembiayaanOnlineController@operasiMarkPaid'])->middleware('auth','operasi');
Route::post('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/paid',['as'=>'pembiayaan.online.operasi.paid.batch','uses'=>'PembiayaanOnlineController@operasiMarkPaidBatch'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/bukti-akaun',['as'=>'pembiayaan.online.operasi.bukti.akaun','uses'=>'PembiayaanOnlineController@operasiViewBuktiAkaun'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/borang',['as'=>'pembiayaan.online.operasi.borang','uses'=>'PembiayaanOnlineController@operasiViewBorang'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/sag',['as'=>'pembiayaan.online.operasi.sag','uses'=>'PembiayaanOnlineController@operasiViewSag'])->middleware('auth','operasi');
Route::get('/permohonan/rekod',['as'=>'permohonan.rekod','uses' => 'KhazanahController@RekodPermohonan'])->middleware('auth','khazanah');
Route::post('/permohonan/simpan',['as'=>'permohonan.simpan','uses' => 'KhazanahController@SimpanPermohonan'])->middleware('auth','khazanah');
@@ -414,6 +422,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');
@@ -599,4 +609,9 @@ Route::post('/pampasan/terima',['as' => 'pampasan.terima', 'uses' => 'KhazanahCo
Route::get(('/pampasan/resit'), ['as' => 'pampasan.resit', 'uses' => 'KhazanahController@resitPampasan'])->middleware('auth', 'khazanah');
//Surat Pampasan
Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah');
Route::get('/pampasan/surat/{norujukan}', ['as' => 'pampasan.surat', 'uses' => 'KhazanahController@suratPampasan'])->middleware('auth', 'role:teller,khazanah');
Route::group(['prefix' => 'shoraka', 'middleware' => ['auth', 'role:teller,penilai,khazanah,admin,masteradmin,operasi']], function () {
Route::get('/', 'ShorakaController@index')->name('shoraka.index');
Route::get('/{norujukan}', 'ShorakaController@show')->name('shoraka.show');
Route::get('/{norujukan}/certificate', 'ShorakaController@certificate')->name('shoraka.certificate');
});
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\LogViewerPassword;
use Illuminate\Http\Request;
use Tests\TestCase;
class LogViewerPasswordTest extends TestCase
{
public function test_missing_configuration_denies_access()
{
config(['logviewer.password' => null]);
$response = $this->checkAccess();
$this->assertSame(503, $response->getStatusCode());
}
public function test_missing_or_incorrect_credentials_are_rejected()
{
config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]);
foreach ([[], ['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'wrong'],
['PHP_AUTH_USER' => 'other', 'PHP_AUTH_PW' => 'test-only-secret']] as $server) {
$response = $this->checkAccess($server);
$this->assertSame(401, $response->getStatusCode());
$this->assertStringContainsString('Basic realm=', $response->headers->get('WWW-Authenticate'));
}
}
public function test_correct_credentials_allow_access_without_caching()
{
config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]);
$response = $this->checkAccess(['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'test-only-secret']);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('allowed', $response->getContent());
$this->assertStringContainsString('no-store', $response->headers->get('Cache-Control'));
}
public function test_logs_route_has_auth_role_password_and_throttle_middleware()
{
$route = app('router')->getRoutes()->match(Request::create('/logs'));
$middleware = $route->gatherMiddleware();
$this->assertContains('auth', $middleware);
$this->assertContains('role:admin,masteradmin', $middleware);
$this->assertContains(LogViewerPassword::class, $middleware);
$this->assertContains('throttle:10,1', $middleware);
}
private function checkAccess(array $server = [])
{
$request = Request::create('/logs?dl=example', 'GET', [], [], [], $server);
return (new LogViewerPassword)->handle($request, function () {
return response('allowed');
});
}
}