38 Commits

Author SHA1 Message Date
ismailmasseran d25364586a Merge branch 'main' of https://git.koppkb.com/KoPKB/kaunter into feature/shoraka-integration 2026-10-26 16:33:29 +08:00
ismailmasseran cde136516d Feature/bsas integration (#31)
Reviewed-on: #31
Co-authored-by: ISMAIL MASSERAN <ismail@koppkb.com>
Co-committed-by: ISMAIL MASSERAN <ismail@koppkb.com>
2026-10-26 15:34:27 +08:00
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
ISMAIL MASSERAN d614677601 DONE: add proper vector logo arrahn
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 15:58:05 +08:00
ismailmasseran 7cd21ef610 Feat/dasar privasi (#26)
Co-authored-by: nurafrinaalimi16 <nurafrinaalimi16@gmail.com>
Reviewed-on: #26
2026-09-27 15:56:09 +08:00
nurafrinaalimi16 ca2a02e354 hotfix: betulkan lajur tarikh untuk semakan ubah dan hapus tebus
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 15:10:29 +08:00
nurafrinaalimi16 76176ed453 hotfix:ubah hapus gadai
Build Docker Image / build-frontend (push) Successful in 40s
2026-09-27 13:05:54 +08:00
ISMAIL MASSERAN 17a5bfafdd DONE: update env.staging
Build Docker Image / build-frontend (push) Successful in 41s
2026-09-26 18:51:36 +08:00
ismailmasseran 5f29f064a0 Merge pull request 'feat(khazanah): papar tarikh ansuran terkini dalam senarai gadaian' (#25) from feat/khazanah-tarikh-ansuran-terkini into main
Reviewed-on: #25
2026-09-24 16:16:16 +08:00
nurafrinaalimi16 9dae32a462 feat(khazanah): papar tarikh ansuran terkini dalam senarai gadaian 2026-09-24 16:15:20 +08:00
ismailmasseran c437a3f504 Feature/whatsapp blast (#24)
Build Docker Image / build-frontend (push) Successful in 53s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #24
2026-09-24 11:28:05 +08:00
ismailmasseran b41234e10b Merge pull request 'DONE: fix duplicate url when print aliran tunai report' (#23) from hotfix/aliran-tunai into main
Reviewed-on: #23
2026-09-22 10:30:49 +08:00
ismailmasseran b7d217948c Merge branch 'main' into hotfix/aliran-tunai 2026-09-22 10:24:11 +08:00
ismailmasseran 94f8f6e855 Merge pull request 'fix: selaraskan tarikh lelong khazanah dan tambah pengesahan baki lelong sebelum daftar pembiayaan' (#22) from fix/tarikh-lelong-khazanah into main
Reviewed-on: #22
2026-09-21 16:22:54 +08:00
nurafrinaalimi16 72108d3f87 fix: selaraskan tarikh lelong khazanah dan tambah pengesahan baki lelong sebelum daftar pembiayaan 2026-09-21 14:57:28 +08:00
ISMAIL MASSERAN 3a64cc2400 DONE: drop column cara terima on report 2026-09-20 16:37:30 +08:00
ismailmasseran 1d71a31078 Merge pull request 'DONE: use constant for min and max pinjaman' (#21) from bugfix/limit-error-cara-terima into main
Reviewed-on: #21
2026-09-20 16:28:38 +08:00
ISMAIL MASSERAN 8e19d0488a DONE: use constant for min and max pinjaman 2026-09-20 16:28:01 +08:00
ismailmasseran 918502b504 Experiment/merge annur (#20)
Build Docker Image / build-frontend (push) Successful in 55s
Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Reviewed-on: #20
2026-09-20 12:49:39 +08:00
ismailmasseran 8a2bae7a88 Merge pull request 'tagging cara terima on sag and reports' (#19) from feature/tagging-cara-terima into main
Reviewed-on: #19
2026-09-20 09:38:02 +08:00
nurafrinaalimi16 67af37a1cd tagging cara terima on sag and reports 2026-09-17 16:26:00 +08:00
ismailmasseran 3f6fb8d985 Merge pull request 'feature/modal-online-pendahuluan' (#18) from feature/modal-online-pendahuluan into main
Reviewed-on: #18
2026-09-17 07:59:00 +08:00
nurafrinaalimi16 371e4d8168 feature/modal-online-pendahuluan 2026-09-14 15:22:28 +08:00
105 changed files with 6651 additions and 1220 deletions
+12 -9
View File
@@ -1,19 +1,19 @@
APP_NAME=Laravel
APP_ENV=local
APP_ENV=production
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/
APP_DEBUG=false
APP_URL=https://annurv2.erahn.com.my
API_URL=https://apiannurv2.erahn.com.my
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone2
DB_HOST=192.168.1.216
DB_PORT=3306
DB_DATABASE=kaunterpkb
DB_USERNAME=root
DB_PASSWORD=secret
DB_DATABASE=kaunter_annur
DB_USERNAME=spider
DB_PASSWORD=arrahn@123
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
@@ -48,3 +48,6 @@ MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+6 -1
View File
@@ -51,4 +51,9 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
INTERNAL_NOTIFICATION_SECRET=erahn-local-notification-secret
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+4 -1
View File
@@ -52,4 +52,7 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+11 -2
View File
@@ -1,10 +1,10 @@
APP_NAME=Laravel
APP_ENV=local
APP_VERSION=v3.3
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://localhost
API_URL=http://localhost:8000
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
@@ -47,4 +47,13 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=
LOGVIEWER_PASSWORD=
# Commodity providers available when a teller submits gadai.
# Turn either or both on. When both are true, the teller chooses on the form.
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+15 -9
View File
@@ -1,19 +1,20 @@
APP_NAME=Laravel
APP_ENV=local
APP_ENV=production
APP_VERSION=v3.3
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/
APP_DEBUG=false
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=127.0.0.1
DB_HOST=192.168.1.216
DB_PORT=3306
DB_DATABASE=kaunter_erahn
DB_USERNAME=spider
DB_PASSWORD=G0dz!ll4
DB_PASSWORD=arrahn@123
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
@@ -46,5 +47,10 @@ 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
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET="nIDTe(u@6[)VLJyd"
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+8 -6
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
@@ -55,4 +52,9 @@ MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=staging_noti_secret
COMMODITY_SHORAKA_ENABLED=true
COMMODITY_BSAS_ENABLED=true
+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}"
+1 -7
View File
@@ -9,13 +9,7 @@ CREATE USER 'kaunteruser'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON kaunter_erahn.* TO 'kaunteruser'@'%';
FLUSH PRIVILEGES;
```
# ADDITIONAL NOTE
## customers/v2/index.blade.php is modal to register customer
## Min Komoditi = RM5000
```
# System Flowchart
@@ -23,9 +23,10 @@ class AdminFeatureController extends Controller
$metaRes = $this->api()->get($api_url . '/api/features');
$meta = $metaRes->successful() ? $metaRes->json() : [];
$keys = is_array($meta) && !empty($meta['keys']) ? $meta['keys'] : ['pembiayaan_online'];
$keys = is_array($meta) && !empty($meta['keys']) ? $meta['keys'] : ['pembiayaan_online', 'rebet_annur'];
$labels = is_array($meta) && !empty($meta['labels']) ? $meta['labels'] : [
'pembiayaan_online' => 'Pembiayaan Online',
'rebet_annur' => 'Rebet / diskaun Annur',
];
$listRes = $this->api()->get($api_url . '/api/features/cawangan');
+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){
+337 -61
View File
@@ -11,6 +11,7 @@ use App\Events\SerahanPenilai;
use Illuminate\Http\Request;
use App\Services\CustomerService;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Auth;
@@ -22,6 +23,9 @@ use Illuminate\Support\Facades\Log;
class GadaianController extends Controller
{
const MIN_PINJAMAN = 5.00;
const MAX_PINJAMAN = 50000.00;
public $AliranTunaiController;
public $customerService;
@@ -32,14 +36,120 @@ class GadaianController extends Controller
$this->AliranTunaiController = new AliranTunaiController();
}
protected function isPembiayaanOnlineEnabled($kodcaw)
protected function isFeatureEnabled($kodcaw, $feature)
{
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/cawangan/' . $kodcaw)
->json();
$features = is_array($cawangan_info) ? ($cawangan_info['features'] ?? []) : [];
return !empty($features['pembiayaan_online']);
return !empty($features[$feature]);
}
protected function isPembiayaanOnlineEnabled($kodcaw)
{
return $this->isFeatureEnabled($kodcaw, 'pembiayaan_online');
}
/**
* Inbox row for PC at this branch. Failures must not block gadai.
*/
protected function notifyPcPembiayaanOnlinePending($auth_user, Request $request, $pinjamanAmt)
{
try {
app(NotificationService::class)->notifyRoleAtCawangan('pc', $auth_user['cawangan'], [
'type' => NotificationService::TYPE_PEMBIAYAAN_PENDING_PC,
'title' => 'Pembiayaan online menunggu semakan',
'body' => sprintf(
'%s — %s (RM %s)',
$request->nama_pelanggan,
$request->norujukan_gadai,
number_format((float) $pinjamanAmt, 2)
),
'url' => route('pembiayaan.online.pc'),
'data' => [
'norujukan' => $request->norujukan_gadai,
'nama_pelanggan' => $request->nama_pelanggan,
'pinjaman' => $pinjamanAmt,
'created_by' => $auth_user['name'],
],
]);
} catch (\Throwable $e) {
Log::warning('Pembiayaan online PC notification failed: '.$e->getMessage(), [
'norujukan' => $request->norujukan_gadai,
'cawangan' => $auth_user['cawangan'],
]);
}
}
protected function parseMoney($value)
{
if ($value === null || $value === '') {
return 0.0;
}
$cleaned = preg_replace('/[^0-9.\-]/', '', str_replace([',', '%', ' '], '', (string) $value));
return is_numeric($cleaned) ? (float) $cleaned : 0.0;
}
protected function normalizeBorangOnlineFields($norujukan, $pinjaman, $kodbank, $noakaun, $namaAkaun)
{
return [
'norujukan' => trim((string) $norujukan),
'pinjaman' => number_format((float) $this->parseMoney($pinjaman), 2, '.', ''),
'kodbank' => strtoupper(trim((string) $kodbank)),
'noakaun' => preg_replace('/\s+/', '', (string) $noakaun),
'nama_akaun' => strtoupper(trim((string) $namaAkaun)),
];
}
protected function borangOnlineFingerprint(array $fields)
{
return sha1(implode('|', [
$fields['norujukan'],
$fields['pinjaman'],
$fields['kodbank'],
$fields['noakaun'],
$fields['nama_akaun'],
]));
}
protected function markBorangOnlinePrinted(array $fields)
{
Session::put('borang_online_printed', array_merge($fields, [
'fingerprint' => $this->borangOnlineFingerprint($fields),
]));
}
protected function hasMatchingBorangOnlinePrint(Request $request)
{
$printed = $request->session()->get('borang_online_printed');
if (!is_array($printed)) {
return false;
}
$norujukan = (string) $request->input('norujukan_gadai', $request->input('norujukan'));
$fields = $this->normalizeBorangOnlineFields(
$norujukan,
$request->input('pinjaman'),
$request->input('kodbank'),
$request->input('noakaun'),
$request->input('nama_akaun') ?: $request->input('nama_pelanggan')
);
return ($printed['norujukan'] ?? '') === $fields['norujukan']
&& ($printed['fingerprint'] ?? '') === $this->borangOnlineFingerprint($fields);
}
protected function borangOnlinePrintedFor($norujukan)
{
$printed = Session::get('borang_online_printed');
if (!is_array($printed) || ($printed['norujukan'] ?? '') !== (string) $norujukan) {
return null;
}
return $printed;
}
public function index(Request $request)
@@ -69,17 +179,18 @@ class GadaianController extends Controller
Session::put('nokp', $noic);
Session::put('nopelanggan', $customer_info['nopelanggan']);
Session::put('page_reload', true);
$gadai = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
Session::forget('borang_online_printed');
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) {
@@ -93,19 +204,21 @@ class GadaianController extends Controller
Session::put('nokp', $nokp);
Session::put('nopelanggan', $nopelanggan);
Session::put('page_reload', true);
$gadai_baru = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/gadai/' . $auth_user['cawangan'], [
Session::forget('borang_online_printed');
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;
}
}
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
return view('gadaian.' . $cawangan_info['version'] . '.index', compact('customer_info', 'limit_pinjaman_semasa', 'cawangan_info', 'norujukan', 'api_url', 'current'));
$borang_online_printed = $this->borangOnlinePrintedFor($norujukan);
return view('gadaian.' . $cawangan_info['version'] . '.index', compact('customer_info', 'limit_pinjaman_semasa', 'cawangan_info', 'norujukan', 'api_url', 'current', 'borang_online_printed'));
}
public function edit(Request $request, $norujukan)
@@ -172,6 +285,40 @@ class GadaianController extends Controller
public function store(Request $request)
{
$auth_user = Auth::user();
if (!config('commodity.shoraka') && !config('commodity.bsas')) {
return $this->gadaiStoreResult($request, false, 'Tiada pembekal komoditi diaktifkan.');
}
$provider = $this->selectedCommodityProvider($request);
if ($provider === null) {
return $this->gadaiStoreResult($request, false, 'Sila pilih pembekal komoditi yang diaktifkan.');
}
$shoraka = null;
if ($provider === 'shoraka') {
$shoraka = app(\App\Services\ShorakaFinancingService::class);
if ($shoraka->hasOrder($request->norujukan_gadai)) {
return $this->gadaiStoreResult($request, false, 'Order Shoraka telah dihantar. Jangan hantar semula.');
}
$shoraka->ensureReady();
}
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
$pinjamanAmt = round((float) $request->pinjaman, 2);
// Online only within min/max pinjaman; otherwise force cash path.
if ($caraTerima === 'ONLINE' && ($pinjamanAmt < self::MIN_PINJAMAN || $pinjamanAmt > self::MAX_PINJAMAN)) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
if ($caraTerima === 'ONLINE' && !$this->hasMatchingBorangOnlinePrint($request)) {
return $this->gadaiStoreResult($request, false, 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.');
}
//$total_nilai_marhun = $request->total_nilai_marhun;
//need to get total nilai marhun by api get from table marhun
@@ -191,6 +338,28 @@ class GadaianController extends Controller
$keuntungan12bulan = $keuntungan_sebln * 12;
$hargajualan = $request->pinjaman + $keuntungan12bulan;
if ($provider === 'bsas') {
$bsas = Http::withOptions(['verify' => config('app.api_verify')])
->timeout(60)
->post(config('api.url') . '/api/komuditi/bsas/order', [
'norujukan' => $request->norujukan_gadai,
'kodcaw' => $auth_user['cawangan'],
'pinjaman' => round((float) $request->pinjaman, 2),
'hargajualan' => round((float) $hargajualan, 2),
'nama_pelanggan' => $request->nama_pelanggan ?: $request->nama_akaun,
'teller' => $auth_user['name'],
]);
if (!$bsas->successful()) {
$body = $bsas->json();
$msg = is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal pembelian komoditi BSAS. Sila cuba lagi.';
return $this->gadaiStoreResult($request, false, $msg);
}
}
//Log::info('DEBUG NOKP:' . $request->nokp);
$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'],
@@ -211,28 +380,45 @@ class GadaianController extends Controller
'onepercent' => $onepercent,
'margin_semasa' => $margin_pinjaman,
'nokp' => $request->nokp,
])->json();
'kadar_rebate' => $this->parseMoney($request->kadar_rebate),
'rebate' => $this->parseMoney($request->rebate),
])->throw()->json();
if ($provider === 'shoraka') {
return $shoraka->hold($request, $request->norujukan_gadai, 'gadai', [
'cara_terima' => $caraTerima,
'pinjaman' => $pinjamanAmt,
'temporary_gadai' => true,
]);
}
$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']
]);
$error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt);
if ($error !== null) {
return $this->gadaiStoreResult($request, false, $error);
}
$caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI'));
return $this->gadaiStoreResult($request, true);
}
public function completeShorakaFinancing(Request $request)
{
$auth_user = Auth::user();
$caraTerima = $request->cara_terima;
$pinjamanAmt = round((float) $request->pinjaman, 2);
// Online only for RM5k–50k; otherwise force cash path.
if ($caraTerima === 'ONLINE' && ($pinjamanAmt < 5000 || $pinjamanAmt > 50000)) {
$caraTerima = 'TUNAI';
$error = $this->disburseGadai($request, $auth_user, $caraTerima, $pinjamanAmt);
if ($error !== null) {
throw new \RuntimeException($error);
}
if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) {
$caraTerima = 'TUNAI';
}
return redirect()->route('customer_info');
}
/**
* Cash or online disbursement after the commodity order and gadai save.
* Returns an error message, or null when disbursement succeeded.
*/
protected function disburseGadai(Request $request, $auth_user, string $caraTerima, float $pinjamanAmt): ?string
{
if ($caraTerima === 'ONLINE') {
$online = Http::withOptions(['verify' => config('app.api_verify')])
->asForm()
@@ -252,11 +438,12 @@ class GadaianController extends Controller
if (!$online->successful()) {
$body = $online->json();
$msg = is_array($body) && isset($body['message'])
return is_array($body) && isset($body['message'])
? $body['message']
: 'Gagal daftar pembiayaan online. Sila cuba lagi.';
return redirect()->back()->with('error', $msg);
}
$this->notifyPcPembiayaanOnlinePending($auth_user, $request, $pinjamanAmt);
} else {
$this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']);
}
@@ -265,9 +452,59 @@ class GadaianController extends Controller
$request->session()->forget('page_reload');
$request->session()->forget('norujukan');
$request->session()->forget('borang_online_printed');
$request->session()->flash('activate_tab', 'SAG_baru');
if ($caraTerima === 'ONLINE') {
$request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.');
$request->session()->flash('message', $caraTerima === 'ONLINE'
? 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'
: 'Pembiayaan berjaya dihantar.');
return null;
}
/**
* Provider chosen on the gadai form, limited to those enabled in config.
*/
protected function selectedCommodityProvider(Request $request): ?string
{
$enabled = array_values(array_filter([
config('commodity.shoraka') ? 'shoraka' : null,
config('commodity.bsas') ? 'bsas' : null,
]));
if ($enabled === []) {
return null;
}
$selected = strtolower(trim((string) $request->input('commodity_provider', '')));
if ($selected === '' && count($enabled) === 1) {
return $enabled[0];
}
return in_array($selected, $enabled, true) ? $selected : null;
}
/**
* JSON for AJAX Terima (BSAS progress UI); normal redirect otherwise.
*/
protected function gadaiStoreResult(Request $request, bool $ok, ?string $message = null)
{
if ($request->ajax() || $request->wantsJson()) {
if (!$ok) {
return response()->json([
'ok' => false,
'message' => $message ?: 'Gadai tidak berjaya.',
], 422);
}
return response()->json([
'ok' => true,
'message' => $message ?: 'Gadai berjaya.',
'redirect' => route('customer_info'),
]);
}
if (!$ok) {
return redirect()->back()->with('error', $message ?: 'Gadai tidak berjaya.');
}
return redirect()->route('customer_info');
@@ -316,6 +553,14 @@ class GadaianController extends Controller
abort(422, 'Sila lengkapkan maklumat bank sebelum cetak borang.');
}
$this->markBorangOnlinePrinted($this->normalizeBorangOnlineFields(
$line['norujukan'],
$line['pinjaman'],
$line['kodbank'],
$line['noakaun'],
$line['nama_akaun'] ?: $line['nama_pelanggan']
));
return $this->renderBorangPengesahanOnline($line, $kodcaw);
}
@@ -405,29 +650,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,
])->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']));
@@ -441,9 +712,10 @@ 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');
return redirect()->route('customer_info');
}
@@ -461,6 +733,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);
@@ -481,6 +754,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()) {
@@ -538,16 +812,18 @@ class GadaianController extends Controller
// $pembayaran_segera = "https://spare.arrahn.com.my/pembayaransegera";
$pembayaran_segera = "https://pay.arrahn.com.my";
$komoditi = 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' => $komoditi['kodproduct']])
->json();
$kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
$commodityPrint = app(\App\Services\CommodityCertificate::class)
->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0);
$komoditi = $commodityPrint['komoditi'];
$product = $commodityPrint['product'];
$kuantiti = $commodityPrint['kuantiti'];
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'));
}
-23
View File
@@ -5,9 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Services\CustomerService;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Hash;
use Carbon\Carbon;
class HomeController extends Controller
@@ -41,17 +39,6 @@ class HomeController extends Controller
if(session()->has('iklan') && session()->get('iklan') == 0)
{
session()->put('iklan', 1);
}else{
if (Hash::check(config('app.reset_password'), $auth_user['password']) || Hash::check($auth_user['no_kp'], $auth_user['password'])) {
session()->put('iklan', 0);
}
}
if($auth_user['roles'] == 'teller'){
if($auth_user['role_harian'] == 'teller'){
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
@@ -117,16 +104,6 @@ class HomeController extends Controller
]);
if(session()->has('iklan') && session()->get('iklan') == 0)
{
session()->put('iklan', 1);
}else{
if (Hash::check(config('app.reset_password'), $auth_user['password']) || Hash::check($auth_user['no_kp'], $auth_user['password'])) {
session()->put('iklan', 0);
}
}
if($auth_user['roles'] == 'teller'){
if($auth_user['role_harian'] == 'teller'){
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
@@ -0,0 +1,32 @@
<?php
namespace App\Http\Controllers;
use App\Services\NotificationService;
use Illuminate\Http\Request;
class InternalNotificationController extends Controller
{
public function store(Request $request, NotificationService $notifications)
{
$expected = (string) config('notifications.internal_secret');
$provided = (string) $request->header('X-Internal-Token', '');
if ($expected === '' || !hash_equals($expected, $provided)) {
return response()->json(['message' => 'Unauthorized'], 401);
}
$audience = $request->input('audience', []);
$role = is_array($audience) && !empty($audience['role']) ? $audience['role'] : null;
if (!$role) {
return response()->json(['message' => 'audience.role diperlukan'], 422);
}
$created = $notifications->dispatch($request->all());
return response()->json([
'created' => $created->count(),
], 201);
}
}
+170 -98
View File
@@ -29,6 +29,7 @@ use Carbon\Carbon;
use PDF;
use DateTime;
use App\Helper;
use App\Services\NotificationService;
use Log;
class KhazanahController extends Controller
@@ -668,6 +669,23 @@ class KhazanahController extends Controller
"namakhazanah" => $request->namakhazanah
])->json();
// Start 30s Operasi auto-approve clock only after Khazanah escalates (10k–45k).
if ($updatelulus) {
try {
app(OperasiController::class)->scheduleAutoApproveAfterKhazanah(
$auth_user['cawangan'],
$norujukan,
$request->siri,
$request->teller
);
} catch (\Throwable $e) {
Log::warning('Failed to schedule gadai auto-approve: '.$e->getMessage(), [
'norujukan' => $norujukan,
'cawangan' => $auth_user['cawangan'],
]);
}
}
}else{
$updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(config('api.url') . '/api/UpdateKelulusan/' . $auth_user['cawangan'] . '/' . $norujukan,[
@@ -1084,6 +1102,7 @@ class KhazanahController extends Controller
if($test == 'success'){
event(new PendahuluanSerahan("Lulus","pendahuluan",$auth_user['cawangan'],$request->teller));
$this->notifyTellerAliranTunai('Lulus', 'pendahuluan', $auth_user['cawangan'], $request->teller, $amount);
return redirect()->route('khazanah.pendahulu');
}else{
return redirect()->route('khazanah.pendahulu')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.');
@@ -1109,6 +1128,7 @@ class KhazanahController extends Controller
if($test == 'success'){
event(new PendahuluanSerahan("Lulus","serahan",$auth_user['cawangan'],$request->teller));
$this->notifyTellerAliranTunai('Lulus', 'serahan', $auth_user['cawangan'], $request->teller, $request->amt);
return redirect()->route('khazanah.serahan');
}else{
return redirect()->route('khazanah.serahan')->with('error','Permohonan tidak berjaya! Permohonan Semula perlu melebihi 1 minit.');
@@ -2473,6 +2493,14 @@ class KhazanahController extends Controller
$hapuskira = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/CalculationHapusKira/' . $auth_user['cawangan'],['tarikh' => $tarikh_imbangan2])->json();
$xtra = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/Xtra/' . $auth_user['cawangan'],['tarikh' => $tarikh_imbangan2])->json();
$rebetAnnur = !empty(($cawangan_info['features'] ?? [])['rebet_annur']);
$rebetpromo = 0;
$pelarasanrebate = 0;
if ($rebetAnnur) {
$rebetpromo = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/RebetPromoCalculation/' . $auth_user['cawangan'],['tarikh' => $tarikh_imbangan2])->json();
$pelarasanrebate = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/PelarasanRebate/' . $auth_user['cawangan'],['tarikh' => $tarikh_imbangan2])->json();
}
//TunaiDitangan Sementara Upah
if($tarikh_imbangan2 >= '2021-04-22' && $tarikh_imbangan2 < '2021-05-05')
@@ -2523,6 +2551,7 @@ class KhazanahController extends Controller
if($imbangduga['pindahkekpkb'] > 0) array_push($hartasemasa,['glcode' => '1000/090','descpt' => 'Pindah Ke KPKB(IP)', 'amaun' => $imbangduga['pindahkekpkb']]);
if($peruntukanhapuskira > 0) array_push($hartasemasa,['glcode' => '1000/015','descpt' => 'Peruntukan Hapus Kira', 'amaun' => $peruntukanhapuskira]);
if($belian1stop > 0) array_push($hartasemasa,['glcode' => '8000/501','descpt' => 'Belian 1 Stop', 'amaun' => $belian1stop]);
if($rebetpromo > 0) array_push($hartasemasa,['glcode' => '5000/242','descpt' => 'Diskaun Promosi', 'amaun' => ($rebetpromo * -1)]);
$totalhartasemasa = 0;
foreach($hartasemasa as $index => $hs)
@@ -2554,6 +2583,8 @@ class KhazanahController extends Controller
if($utgterkumpulxtra > 0) array_push($tanggungansemasa,['glcode' => '7000/201','descpt' => 'Untung/Rugi Terkumpul Xtra','amaun' => $utgterkumpulxtra]);
if($jualan1stop > 0) array_push($tanggungansemasa,['glcode' => '9000/501','descpt' => 'Jualan 1 Stop','amaun' => $jualan1stop]);
if($hapuskira > 0) array_push($tanggungansemasa,['glcode' => '3000/011','descpt' => 'Hapus Kira','amaun' => -$hapuskira]);
if($rebetpromo > 0) array_push($tanggungansemasa,['glcode' => '5000/242','descpt' => 'Diskaun Promosi', 'amaun' => ($rebetpromo * -1)]);
if($pelarasanrebate > 0) array_push($tanggungansemasa,['glcode' => '5000/243','descpt' => 'Pelarasan Rebate', 'amaun' => ($pelarasanrebate * -1)]);
$totaltanggungansemasa = 0;
foreach($tanggungansemasa as $index => $ts)
@@ -2586,108 +2617,114 @@ class KhazanahController extends Controller
}
//print lejer_AM
public function cetak_lejer_am(Request $request)
{
public function cetak_lejer_am(Request $request)
{
$auth_user = Auth::user();
$api_url = config('api.url');
$auth_user = Auth::user();
$api_url = config('api.url');
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
$cawangan = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/cawangan/detail/'.$auth_user['cawangan'])->json();
$cawangan_detail = $cawangan[0];
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
$cawangan=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/cawangan/detail/'.$auth_user['cawangan'])->json();
$cawangan_detail=$cawangan[0];
$num = $request->num;
$current = new Carbon();
$tarikh_cetak = $current->toDateString();
$auth_user = Auth::user();
$api_url = config('api.url');
// fix: bagi default value supaya $tarikh_mula / $tarikh_akhir tak undefined
$tarikh_mula = $tarikh_cetak;
$tarikh_akhir = $tarikh_cetak;
$num=$request->num;
$current = Carbon::now();
$current = new Carbon();
$tarikh_cetak = $current->toDateString();
if($request->datetimepicker1 == null){
$tarikh_cetak = $current->toDateString();
}else {
$split_date = explode('-',$request->datetimepicker1);
$tarikh_mula = $split_date[2] . '-' . $split_date[1] . '-' . $split_date[0];
}
if($request->datetimepicker2 == null){
$tarikh_cetak = $current->toDateString();
}
else{
$split_date = explode('-',$request->datetimepicker2);
$tarikh_akhir = $split_date[2] . '-' . $split_date[1] . '-' . $split_date[0];
}
$lejeramdata = Lejeram::orderBy('glcode')->get();
$lejeram = array();
$len = count($lejeramdata);
$totalkredit = 0;
$totaldebit = 0;
foreach($lejeramdata as $index => $lejer)
{
if($lejer->api_redirect != null)
{
${$lejer['variable']} = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url'). $lejer['api_redirect'] . $auth_user['cawangan'],['mula' => $tarikh_mula, 'hingga' => $tarikh_akhir])->json();
if($lejer->namadebit == null)
{
$debit = null;
}else{
if($lejer->oneVar == 1)
{
$debit = ${$lejer['variable']};
}else{
$debit = ${$lejer['variable']}[$lejer->namadebit];
}
$totaldebit += floatval($debit);
}
if($lejer->namakredit == null)
{
$kredit = null;
}else{
if($lejer->oneVar == 1)
{
$kredit = ${$lejer['variable']};
}else{
$kredit = ${$lejer['variable']}[$lejer->namakredit];
}
$totalkredit += floatval($kredit);
}
if(!($kredit == null && $debit == null))
{
array_push($lejeram,['glcode' => $lejer->glcode, 'descpt' => $lejer->descpt,'debit' => ($debit) ? $debit : 0,'kredit' => ($kredit) ? $kredit : 0]);
}
}else{
array_push($lejeram,['glcode' => $lejer->glcode, 'descpt' => $lejer->descpt,'debit' => 0,'kredit' => 0]);
}
if($index == $len-1)
{
$lejeram[0]['debit'] = $totalkredit;
$lejeram[0]['kredit'] = $totaldebit;
}
}
$totalsamadebit = 0;
$totalsamakredit = 0;
foreach($lejeram as $index=>$lejer)
{
$totalsamadebit += floatval($lejer['debit']);
$totalsamakredit += floatval($lejer['kredit']);
}
return view('print.laporan_lejer_am',compact('cawangan','tarikh_cetak','cawangan_info', 'api_url','cawangan_detail','tarikh_akhir','tarikh_mula','lejeram','totalsamadebit','totalsamakredit'));
if($request->datetimepicker1 != null){
$split_date = explode('-', $request->datetimepicker1);
$tarikh_mula = $split_date[2] . '-' . $split_date[1] . '-' . $split_date[0];
}
if($request->datetimepicker2 != null){
$split_date = explode('-', $request->datetimepicker2);
$tarikh_akhir = $split_date[2] . '-' . $split_date[1] . '-' . $split_date[0];
}
// ambil 'had' daripada modal_online melalui API (VaultController@CalculationModalOnlineLejer)
$had_response = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/ModalOnlineLejer/' . $auth_user['cawangan'], [
'mula' => $tarikh_mula,
'hingga' => $tarikh_akhir,
])->json();
$had_modal_online = $had_response['had'] ?? 0;
$lejeramdata = Lejeram::orderBy('glcode')->get();
$lejeram = array();
$len = count($lejeramdata);
$totalkredit = 0;
$totaldebit = 0;
foreach($lejeramdata as $index => $lejer)
{
if($lejer->api_redirect != null)
{
${$lejer['variable']} = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url'). $lejer['api_redirect'] . $auth_user['cawangan'], ['mula' => $tarikh_mula, 'hingga' => $tarikh_akhir])->json();
if($lejer->namadebit == null)
{
$debit = null;
}else{
if($lejer->oneVar == 1)
{
$debit = ${$lejer['variable']};
}else{
$debit = ${$lejer['variable']}[$lejer->namadebit];
}
$totaldebit += floatval($debit);
}
if($lejer->namakredit == null)
{
$kredit = null;
}else{
if($lejer->oneVar == 1)
{
$kredit = ${$lejer['variable']};
}else{
$kredit = ${$lejer['variable']}[$lejer->namakredit];
}
// tambah 'had' modal_online khusus untuk Pendahuluan Daripada HQ
if($lejer->glcode == '7000/102')
{
$kredit = floatval($kredit) + $had_modal_online;
}
$totalkredit += floatval($kredit);
}
if(!($kredit == null && $debit == null))
{
array_push($lejeram, ['glcode' => $lejer->glcode, 'descpt' => $lejer->descpt, 'debit' => ($debit) ? $debit : 0, 'kredit' => ($kredit) ? $kredit : 0]);
}
}else{
array_push($lejeram, ['glcode' => $lejer->glcode, 'descpt' => $lejer->descpt, 'debit' => 0, 'kredit' => 0]);
}
if($index == $len-1)
{
$lejeram[0]['debit'] = $totalkredit;
$lejeram[0]['kredit'] = $totaldebit;
}
}
$totalsamadebit = 0;
$totalsamakredit = 0;
foreach($lejeram as $index => $lejer)
{
$totalsamadebit += floatval($lejer['debit']);
$totalsamakredit += floatval($lejer['kredit']);
}
return view('print.laporan_lejer_am', compact('cawangan','tarikh_cetak','cawangan_info', 'api_url','cawangan_detail','tarikh_akhir','tarikh_mula','lejeram','totalsamadebit','totalsamakredit'));
}
public function laporan_tuntutbaki(Request $request)
{
$auth_user = Auth::user();
@@ -2831,10 +2868,43 @@ class KhazanahController extends Controller
]);
event(new PendahuluanSerahan("Batal","all",$auth_user['cawangan'],$kodlaluan));
$this->notifyTellerAliranTunai('Batal', 'all', $kodcaw, $kodlaluan);
return response('Berjaya');
}
/**
* Inbox for the teller waiting on aliran tunai. Failures must not block approval.
*
* @param string $pengesahan
* @param string $jenis
* @param string $kodcaw
* @param string $teller
* @param mixed $amaun
* @return void
*/
protected function notifyTellerAliranTunai($pengesahan, $jenis, $kodcaw, $teller, $amaun = 0)
{
try {
$raw = str_replace(',', '', (string) $amaun);
$amount = is_numeric($raw) ? (float) $raw : 0;
app(NotificationService::class)->notifyAliranTunaiDecided($kodcaw, $teller, [
'pengesahan' => $pengesahan,
'jenis' => $jenis,
'amaun' => $amount,
'kodlaluan' => $teller,
'khazanah' => Auth::user() ? Auth::user()->name : '',
]);
} catch (\Throwable $e) {
Log::warning('Aliran tunai teller notification failed: '.$e->getMessage(), [
'kodcaw' => $kodcaw,
'teller' => $teller,
'jenis' => $jenis,
]);
}
}
public function SuratAkuanGadaian(Request $request)
{
$auth_user = Auth::user();
@@ -4025,8 +4095,9 @@ class KhazanahController extends Controller
$cawangan=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/cawangan/detail/'.$auth_user['cawangan'])->json();
$cawangan_detail=$cawangan[0];
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
return view('print.laporan_gadaian_ujrah', compact('gadai','cawangan_detail','tarikh_cetak','mula_date','api_url','hingga_date','statistik','reportPembiayaan','reportLoanAmount','reportMarhunValue','reportMarhunMargin','reportCustomerJob','reportCustomerRace','reportCustomerRace'));
return view('print.laporan_gadaian_ujrah', compact('gadai','cawangan_detail','cawangan_info','tarikh_cetak','mula_date','api_url','hingga_date','statistik','reportPembiayaan','reportLoanAmount','reportMarhunValue','reportMarhunMargin','reportCustomerJob','reportCustomerRace','reportCustomerRace'));
}
@@ -4069,7 +4140,8 @@ class KhazanahController extends Controller
$cawangan=Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url').'/api/cawangan/detail/'.$auth_user['cawangan'])->json();
$cawangan_detail=$cawangan[0];
return view('print.laporan_tebus_ujrah', compact('tebus','cawangan_detail','tarikh_cetak','mula_date','hingga_date','tebuspengasingan','api_url','upahlama','ansuran','totalkeuntunganrebet'));
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json();
return view('print.laporan_tebus_ujrah', compact('tebus','cawangan_detail','cawangan_info','tarikh_cetak','mula_date','hingga_date','tebuspengasingan','api_url','upahlama','ansuran','totalkeuntunganrebet'));
}
public function laporanAnsuranUjrah(){
+270
View File
@@ -55,4 +55,274 @@ class KomuditiController extends Controller
return redirect()->route('komuditi');
}
/**
* Khazanah branch list of BSAS commodity orders.
*/
public function khazanahBsas()
{
$auth_user = Auth::user();
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])
->json();
return view('homepage.khazanah.bsas_komoditi', compact('cawangan_info'));
}
/**
* Dummy BSAS eCert layout preview (bid, OTC, STB, then one commodity schedule).
*/
public function previewBsasEcert()
{
$line = [
['SUPPLIER' => 'FGV TRADING SDN BHD', 'VOLUME' => '0.75276406'],
];
return view('print.bsas_ecert', $this->presentBsasCertificates([
'e_cert_no' => 'CPO21OCT26-0000031-000',
'serial_number' => '1',
'otc_counter_party' => 'ALI BIN ABU',
'otc_murabaha_value' => '3,814.40',
'product_code' => 'CPO-MSIA-09',
'bid_ecert' => [
'ECERTNO' => 'CPO21OCT26-0000031-000',
'BUYER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
'OWNER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
'BIDNO' => '8',
'TOTALVALUE' => '3,200.00',
'CURRENCY' => 'MYR',
'PRICE' => '4,251.00',
'PRICE_MYR_EQUIVALENT' => '4,251.00',
'PURCHASETIMEDATE' => '15:11:53.316 21 Oct 2026',
'VALUEDATE' => '21 Oct 2026',
'PNAME' => 'CPO-MSIA-09',
'PVOLUME' => '0.75276406',
'LINE' => $line,
],
'otc_ecert' => [
'ECERTNO' => 'CPO21OCT26-0000031-000',
'SELLER' => 'KOPERASI PERMODALAN KELANTAN BERHAD',
'BUYER' => 'ALI BIN ABU',
'REPORTINGTIMEDATE' => '15:11:53.320 21 Oct 2026',
'VALUEDATE' => '21 Oct 2026',
'PNAME' => 'CPO-MSIA-09',
'PVOLUME' => '0.75276406',
'CURRENCY' => 'MYR',
'PRICE' => '4,251.00',
'PRICE_MYR_EQUIVALENT' => '4,251.00',
'TOTALVALUE' => '3,200.00',
'MURABAHAVALUE' => '3,814.40',
'LINE' => $line,
],
'stb_ecert' => [
'ECERTNO' => 'CPO21OCT26-0000031-000',
'SELLER' => 'ALI BIN ABU',
'BUYER' => 'BURSA MALAYSIA ISLAMIC SERVICES',
'TOTALVALUE' => '3,200.00',
'CURRENCY' => 'MYR',
'PRICE' => '4,251.00',
'PRICE_MYR_EQUIVALENT' => '4,251.00',
'SELLINGTIMEDATE' => '15:11:53.325 21 Oct 2026',
'VALUEDATE' => '21 Oct 2026',
'PNAME' => 'CPO-MSIA-09',
'PVOLUME' => '0.75276406',
'LINE' => $line,
],
], true));
}
/**
* Live BSAS eCerts from stored bid, OTC and STB payloads.
*/
public function showBsasEcert($uuid)
{
$response = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/komuditi/bsas/order/' . urlencode($uuid) . '/ecert');
if ($response->status() === 404) {
abort(404, $response->json('message') ?: 'eCert tidak dijumpai.');
}
if (!$response->successful()) {
abort(502, 'Gagal memuatkan eCert BSAS.');
}
$payload = $response->json();
if (!is_array($payload)) {
abort(502, 'Gagal memuatkan eCert BSAS.');
}
$presented = $this->presentBsasCertificates($payload, false);
if ($presented['certificates'] === []) {
abort(404, 'eCert belum tersedia untuk order ini.');
}
return view('print.bsas_ecert', $presented);
}
/**
* Bid, OTC and STB pages, plus one shared commodity schedule.
*/
protected function presentBsasCertificates(array $payload, bool $isDummy): array
{
$certificates = [];
$bid = $payload['bid_ecert'] ?? null;
if (is_array($bid) && $bid !== []) {
$certificates[] = $this->mapBidCertificate($bid, $payload);
}
$otc = $payload['otc_ecert'] ?? null;
if (is_array($otc) && $otc !== []) {
$certificates[] = $this->mapOtcCertificate($otc, $payload);
}
$stb = $payload['stb_ecert'] ?? null;
if (is_array($stb) && $stb !== []) {
$certificates[] = $this->mapStbCertificate($stb, $payload);
}
$scheduleSource = [];
if (is_array($otc) && $otc !== []) {
$scheduleSource = $otc;
} elseif (is_array($bid) && $bid !== []) {
$scheduleSource = $bid;
} elseif (is_array($stb) && $stb !== []) {
$scheduleSource = $stb;
}
$ecertNo = $scheduleSource['ECERTNO'] ?? ($payload['e_cert_no'] ?? '-');
return [
'certificates' => $certificates,
'ecert_no' => $ecertNo,
'lines' => $this->mapBsasLines($scheduleSource),
'isDummy' => $isDummy,
];
}
protected function mapBidCertificate(array $ecert, array $orderMeta): array
{
[$time, $date] = $this->splitBsasDateTime((string) ($ecert['PURCHASETIMEDATE'] ?? ''));
return $this->certificatePage(
'Certificate of Purchase',
'This is to certify that the following bid transaction has been executed<br>through the Bursa Suq Al-Sila\' in accordance with the<br>Rules of Bursa Suq Al-Sila\'.',
$ecert,
$orderMeta,
array_merge([
['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'],
['label' => 'Owner', 'value' => $ecert['OWNER'] ?? '-'],
['label' => 'Bid No', 'value' => $ecert['BIDNO'] ?? ($orderMeta['serial_number'] ?? '-')],
['label' => 'Purchase Time / Date', 'value' => $time . ' / ' . $date],
], $this->commodityDetailRows($ecert, $orderMeta))
);
}
protected function mapOtcCertificate(array $ecert, array $orderMeta): array
{
[$time, $date] = $this->splitBsasDateTime((string) ($ecert['REPORTINGTIMEDATE'] ?? ''));
$page = $this->certificatePage(
'Certificate of Murabahah',
'This is to certify that the following transaction has been executed<br>through the Bursa Suq Al-Sila\' in accordance with the<br>Rules of Bursa Suq Al-Sila\'.',
$ecert,
$orderMeta,
array_merge([
['label' => 'Seller', 'value' => $ecert['SELLER'] ?? 'KOPERASI PERMODALAN KELANTAN BERHAD'],
['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? ($orderMeta['otc_counter_party'] ?? '-')],
['label' => 'Bid No', 'value' => $orderMeta['serial_number'] ?? '1'],
['label' => 'Reporting Time / Date', 'value' => $time . ' / ' . $date],
], $this->commodityDetailRows($ecert, $orderMeta), [
['label' => 'Transaction Type', 'value' => '-'],
])
);
$page['show_murabaha'] = true;
$page['murabaha'] = $ecert['MURABAHAVALUE'] ?? ($orderMeta['otc_murabaha_value'] ?? '-');
return $page;
}
protected function mapStbCertificate(array $ecert, array $orderMeta): array
{
[$time, $date] = $this->splitBsasDateTime((string) ($ecert['SELLINGTIMEDATE'] ?? ''));
return $this->certificatePage(
'Certificate of Sale',
'This is to certify that the following sale transaction has been executed<br>through the Bursa Suq Al-Sila\' in accordance with the<br>Rules of Bursa Suq Al-Sila\'.',
$ecert,
$orderMeta,
array_merge([
['label' => 'Seller', 'value' => $ecert['SELLER'] ?? ($orderMeta['otc_counter_party'] ?? '-')],
['label' => 'Buyer', 'value' => $ecert['BUYER'] ?? '-'],
['label' => 'Selling Time / Date', 'value' => $time . ' / ' . $date],
], $this->commodityDetailRows($ecert, $orderMeta))
);
}
protected function certificatePage(string $title, string $statement, array $ecert, array $orderMeta, array $rows): array
{
return [
'title' => $title,
'statement' => $statement,
'ecert_no' => $ecert['ECERTNO'] ?? ($orderMeta['e_cert_no'] ?? '-'),
'currency' => $ecert['CURRENCY'] ?? 'MYR',
'rows' => $rows,
'show_murabaha' => false,
'murabaha' => null,
];
}
protected function commodityDetailRows(array $ecert, array $orderMeta): array
{
return [
['label' => 'Value Date', 'value' => isset($ecert['VALUEDATE']) ? strtoupper((string) $ecert['VALUEDATE']) : '-'],
['label' => 'Product Code / Description', 'value' => $ecert['PNAME'] ?? ($orderMeta['product_code'] ?? '-')],
['label' => 'Unit', 'value' => 'Tonnage'],
['label' => 'Volume', 'value' => $ecert['PVOLUME'] ?? '-'],
['label' => 'Currency', 'value' => $ecert['CURRENCY'] ?? 'MYR'],
['label' => 'Price (MYR)', 'value' => $ecert['PRICE'] ?? '-'],
['label' => 'Price (MYR Equivalent)', 'value' => $ecert['PRICE_MYR_EQUIVALENT'] ?? ($ecert['PRICE'] ?? '-')],
['label' => 'Value', 'value' => $ecert['TOTALVALUE'] ?? '-'],
];
}
protected function mapBsasLines(array $ecert): array
{
$lines = [];
foreach (($ecert['LINE'] ?? []) as $line) {
if (!is_array($line)) {
continue;
}
$lines[] = [
'supplier' => $line['SUPPLIER'] ?? '-',
'volume' => $line['VOLUME'] ?? ($ecert['PVOLUME'] ?? '-'),
];
}
if ($lines === []) {
$lines[] = [
'supplier' => '-',
'volume' => $ecert['PVOLUME'] ?? '-',
];
}
return $lines;
}
protected function splitBsasDateTime(string $value): array
{
$value = trim($value);
if ($value === '') {
return ['-', '-'];
}
$parts = explode(' ', $value, 2);
return [
$parts[0] ?? '-',
isset($parts[1]) ? strtoupper($parts[1]) : '-',
];
}
}
@@ -0,0 +1,43 @@
<?php
namespace App\Http\Controllers;
use App\Services\NotificationService;
use Illuminate\Support\Facades\Auth;
class NotificationController extends Controller
{
public function __construct()
{
$this->middleware('auth');
}
public function index(NotificationService $notifications)
{
return response()->json($notifications->inboxPayload(Auth::user()));
}
public function markAsRead(NotificationService $notifications, $id)
{
$notification = $notifications->markAsRead(Auth::user(), $id);
if (!$notification) {
return response()->json(['message' => 'Notifikasi tidak dijumpai.'], 404);
}
return response()->json([
'notification' => $notification->toInboxArray(),
'unread_count' => $notifications->unreadCount(Auth::user()),
]);
}
public function markAllAsRead(NotificationService $notifications)
{
$updated = $notifications->markAllAsRead(Auth::user());
return response()->json([
'updated' => $updated,
'unread_count' => 0,
]);
}
}
+270 -54
View File
@@ -2,13 +2,14 @@
namespace App\Http\Controllers;
//Events
use App\Events\OperasiNotice;
use App\Events\StatusLiked;
use App\Events\NotifikasiKelulusan;
use App\Events\NotifikasiAnggota;
use App\Events\Dashboard\KomoditiNotify;
use App\Services\NotificationService;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
@@ -21,6 +22,15 @@ use Illuminate\Support\Facades\Log;
class OperasiController extends Controller
{
const AUTO_APPROVE_MIN = 10000;
const AUTO_APPROVE_MAX = 45000;
const AUTO_APPROVE_USER_IDS = [252, 275];
const AUTO_APPROVE_EMAILS = [
252 => 'hafiz@koppkb.com',
275 => 'shauqi@koppkb.com',
];
const AUTO_APPROVE_KOMEN = 'lulus';
public function __construct()
{
$this->middleware('auth');
@@ -214,51 +224,19 @@ class OperasiController extends Controller
'sahkan' => $status
];
Log::info('Menghantar permintaan ke API UpdateKelulusanAnggota', [
'endpoint' => $endpoint,
'payload' => $payload
]);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->put($endpoint, $payload);
$updatelulus = $response->json();
// 3️⃣ Log hasil respon API
Log::info('Respon dari API UpdateKelulusanAnggota', [
'status' => $response->status(),
'body' => $updatelulus
]);
// 4️⃣ Jika berjaya, cipta event notifikasi
if ($response->successful() && $updatelulus && isset($updatelulus['success']) && $updatelulus['success'] === true) {
// $jenis = ($status == 1) ? 'lulus' : 'batal';
// event(new NotifikasiAnggota($nokp, $jenis, null, $komen, $namaoperasi));
$statusText = ($status == 1) ? 'DILULUSKAN' : 'DITOLAK';
event(new NotifikasiAnggota($nokp, $statusText, $komen, $namaoperasi));
Log::info('Event NotifikasiAnggota dispatched', [
'nokp' => $nokp,
'komen' => $komen,
'namaoperasi' => $namaoperasi
]);
} else {
Log::warning('Gagal kemas kini kelulusan anggota', [
'nokp' => $nokp,
'kodcaw' => $kodcaw,
'response' => $updatelulus
]);
}
return response()->json($updatelulus, $response->status());
} catch (\Exception $e) {
Log::error('Ralat UpdateKelulusanAnggota (frontend)', [
'message' => $e->getMessage(),
'line' => $e->getLine()
]);
return response()->json([
'success' => false,
'message' => 'Ralat semasa menghantar kelulusan anggota.',
@@ -267,10 +245,6 @@ class OperasiController extends Controller
}
}
public function createkelulusan(Request $request)
{
$auth_user = Auth::user();
@@ -309,9 +283,245 @@ class OperasiController extends Controller
event(new StatusLiked($auth_user['cawangan'], $auth_user['name']));
$this->notifyGadaiAboveTenThousand($auth_user, $request, $cawangan_info);
return response()->json($siri, 201);
}
/**
* Hidden auto-approve for operasi gadai kelulusan in RM10,000–45,000
* when Operasi has not approved within 30s after Khazanah escalation.
*/
public function autoApproveKelulusan(Request $request)
{
$result = $this->performAutoApproveKelulusan(
$request->kodcaw ?: Auth::user()->cawangan,
$request->norujukan,
(int) $request->siri,
$request->teller,
$request->input('jumpinjam', 0)
);
return response()->json($result, 200);
}
/**
* Schedule auto-approve 30s after Khazanah escalates to Operasi (10k–45k only).
* Runs after the HTTP response so Khazanah UI is not blocked.
*/
public function scheduleAutoApproveAfterKhazanah($kodcaw, $norujukan, $siri, $teller)
{
$kodcaw = (string) $kodcaw;
$norujukan = (string) $norujukan;
$siri = (int) $siri;
$teller = (string) $teller;
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return;
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? 0));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : 0;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return;
}
app()->terminating(function () use ($kodcaw, $norujukan, $siri, $teller, $apiAmount) {
sleep(30);
try {
app(self::class)->performAutoApproveKelulusan(
$kodcaw,
$norujukan,
$siri,
$teller,
$apiAmount
);
} catch (\Throwable $e) {
Log::warning('Gadai auto-approve after khazanah failed: '.$e->getMessage(), [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
'siri' => $siri,
]);
}
});
}
/**
* Core auto-approve: idempotent if already decided or out of range.
*/
public function performAutoApproveKelulusan($kodcaw, $norujukan, $siri, $teller, $jumpinjam = 0)
{
$siri = (int) $siri;
$raw = str_replace(',', '', (string) $jumpinjam);
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < self::AUTO_APPROVE_MIN || $pinjaman > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_found',
];
}
$masalulus = $latest['masalulus'] ?? '';
$pelulus = $latest['pelulus'] ?? '';
if (($masalulus !== '' && $masalulus !== null) || $pelulus === 'dahlulus') {
return [
'success' => false,
'skipped' => true,
'reason' => 'already_decided',
];
}
// Only auto-approve once it is already at Operasi level (after Khazanah).
if ($pelulus !== 'operasi') {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_operasi_level',
];
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? $pinjaman));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : $pinjaman;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$namaoperasi = $this->resolveAutoApproveOperasiName();
if (!$namaoperasi) {
Log::warning('Gadai auto-approve: approver users 252/275 not found', [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
]);
return [
'success' => false,
'skipped' => true,
'reason' => 'approver_missing',
];
}
$komen = self::AUTO_APPROVE_KOMEN;
Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusanKomen/' . $kodcaw . '/' . $norujukan,
[
'komen' => $komen,
'siri' => $siri,
]
)->json();
$updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusan/' . $kodcaw . '/' . $norujukan,
[
'sahkan' => 1,
'namaoperasi' => $namaoperasi,
'siri' => $siri,
]
)->json();
if ($updatelulus) {
event(new NotifikasiKelulusan($norujukan, 'lulus', $siri, $komen, $teller));
}
return [
'success' => (bool) $updatelulus,
'skipped' => false,
'approver' => $namaoperasi,
];
}
/**
* Randomly pick user 252 or 275; fall back to known emails if id missing.
*/
protected function resolveAutoApproveOperasiName()
{
$ids = self::AUTO_APPROVE_USER_IDS;
$approverId = $ids[array_rand($ids)];
$user = User::find($approverId);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$approverId] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if (!$user) {
foreach ($ids as $id) {
if ($id === $approverId) {
continue;
}
$user = User::find($id);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$id] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if ($user) {
break;
}
}
}
return $user ? $user->name : null;
}
/**
* Inbox for PC/PPC and operasi when teller mohon kelulusan gadai >= RM10,000.
*/
protected function notifyGadaiAboveTenThousand($auth_user, Request $request, $cawangan_info)
{
$raw = str_replace(',', '', (string) $request->input('jumpinjam', 0));
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < NotificationService::GADAI_KELULUSAN_MIN) {
return;
}
$status = strtolower((string) $request->input('status', ''));
if (strpos($status, 'tebus') !== false) {
return;
}
$namaCaw = '';
if (is_array($cawangan_info)) {
$namaCaw = isset($cawangan_info['details_caw']) ? $cawangan_info['details_caw'] : '';
}
try {
app(NotificationService::class)->notifyGadaiKelulusan($auth_user['cawangan'], [
'norujukan' => $request->norujukan,
'pinjaman' => $pinjaman,
'teller' => $request->teller ?: $auth_user['name'],
'cawangan_nama' => $namaCaw,
'status' => $request->status,
]);
} catch (\Throwable $e) {
Log::warning('Gadai kelulusan notification failed: '.$e->getMessage(), [
'norujukan' => $request->norujukan,
'cawangan' => $auth_user['cawangan'],
]);
}
}
public function createkelulusanAnggota(Request $request)
{
@@ -320,8 +530,6 @@ class OperasiController extends Controller
$api_url = config('api.url');
$current = Carbon::now();
Log::info('createkelulusanAnggota', $request->all());
// Dapatkan maklumat cawangan
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $auth_user['cawangan'])
@@ -360,7 +568,6 @@ class OperasiController extends Controller
'body' => $response->body(),
]);
} catch (\Throwable $th) {
Log::error('❌ createkelulusanAnggota error: ' . $th->getMessage());
return response()->json([
'success' => false,
'message' => $th->getMessage(),
@@ -415,26 +622,35 @@ class OperasiController extends Controller
$api_url = config('api.url');
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
//declare array baru
$arraykelulusan = [];
$dateTo = Carbon::now()->toDateString();
$dateFrom = Carbon::now()->subDays(30)->toDateString();
$kelulusan = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/ListOperasiKelulusan/')->json();
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'dateFrom', 'dateTo'));
}
public function kelulusanrekodlepasData(Request $request)
{
$params = [
'date_from' => $request->query('date_from'),
'date_to' => $request->query('date_to'),
'search' => $request->query('search'),
];
foreach ($kelulusan as $index => $k) {
foreach ($k['kelulusanall'] as $index1 => $kel) {
$komen = explode("|", $kel['komen']);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListOperasiKelulusan/', array_filter($params, function ($value) {
return $value !== null && $value !== '';
}));
$results = array_search("", $komen, true);
if ($results !== false) {
unset($komen[$results]);
}
array_push($arraykelulusan, ['norujukan' => $kel['norujukan'], 'pinjaman' => $kel['jumpinj'], 'nilaimarhun' => ($komen[1] ?? $kel['nilaimarhun'] ?? '-'), 'berat' => ($komen[2] ?? $kel['berat'] ?? '-'), 'margin' => ($komen[3] ?? $kel['margin'] ?? '-'), 'komen' => ($komen[0] ?? $kel['komen'] ?? '-'), 'status' => ($kel['status'] ?? '-'), 'pelulus' => $kel['operasi'],]);
}
if (!$response->successful()) {
return response()->json(['data' => [], 'error' => 'Gagal memuatkan rekod'], 502);
}
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'arraykelulusan'));
$rows = $response->json();
if (!is_array($rows)) {
$rows = [];
}
return response()->json(['data' => $rows]);
}
public function notifyKomoditi()
@@ -97,6 +97,52 @@ class PembiayaanOnlineController extends Controller
));
}
/**
* Lightweight payload for the PC layout cutoff banner / browser notification.
*/
public function reminderStatus()
{
$cawangan_info = $this->cawanganInfo();
if (!$this->isPembiayaanOnlineEnabled($cawangan_info)) {
return response()->json(['enabled' => false]);
}
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$queue = $this->api()
->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/queue')
->json();
$modal = $this->api()
->get($api_url . '/api/modal-online/' . $kodcaw)
->json();
if (!is_array($queue)) {
$queue = [];
}
if (!is_array($modal)) {
$modal = [];
}
$slot = (is_array($queue) && isset($queue['slot_cadangan']) && is_array($queue['slot_cadangan']))
? $queue['slot_cadangan']
: [];
return response()->json([
'enabled' => true,
'ditutup' => (int) ($modal['ditutup'] ?? 0) === 1,
'bilangan' => (int) ($queue['bilangan'] ?? 0),
'slot' => $slot['slot'] ?? 'BATCH1',
'cutoff' => $slot['cutoff'] ?? '11:00',
'cutoff_at' => $slot['cutoff_at'] ?? null,
'server_now' => $slot['server_now'] ?? null,
'lewat' => !empty($slot['lewat']),
'baki_minit' => (int) ($slot['baki_minit'] ?? 0),
'url' => route('pembiayaan.online.pc'),
'remind_minutes' => 30,
'last_minute' => 10,
]);
}
/**
* Update bank/docs for a queue line.
*/
@@ -110,11 +156,36 @@ 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,
];
if ($request->hasFile('bukti_akaun')) {
$file = $request->file('bukti_akaun');
if (!$file->isValid()) {
return redirect()->back()->with('error', 'Fail slip bank tidak sah.');
}
if ($file->getSize() > 4 * 1024 * 1024) {
return redirect()->back()->with('error', 'Saiz fail maksimum 4MB.');
}
$payload['bukti_akaun_data'] = base64_encode(file_get_contents($file->getRealPath()));
$payload['bukti_akaun_filename'] = $file->getClientOriginalName();
$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);
@@ -125,7 +196,55 @@ class PembiayaanOnlineController extends Controller
return redirect()->back()->with('error', $msg);
}
return redirect()->route('pembiayaan.online.pc')->with('message', 'Rekod dikemaskini.');
return redirect()->route('pembiayaan.online.pc')
->with('message', 'Rekod dikemaskini. Sila semak semula sebelum hantar ke Operasi.');
}
/**
* PC verifies one queue line before it can be submitted to Operasi.
*/
public function verifyLine(Request $request, $id)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$auth_user = Auth::user();
$res = $this->api()
->asForm()
->post($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/verify', [
'pc_verified_by' => $auth_user['name'],
]);
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal semak rekod.';
return redirect()->back()->with('error', $msg);
}
return redirect()->route('pembiayaan.online.pc')->with('message', 'Rekod disahkan. Boleh dipilih untuk hantar ke Operasi.');
}
/**
* Undo PC verification on a queue line.
*/
public function unverifyLine(Request $request, $id)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$api_url = config('api.url');
$res = $this->api()
->asForm()
->post($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/unverify');
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal batal semakan.';
return redirect()->back()->with('error', $msg);
}
return redirect()->route('pembiayaan.online.pc')->with('message', 'Semakan dibatalkan. Sila semak semula sebelum hantar.');
}
/**
@@ -259,17 +378,138 @@ class PembiayaanOnlineController extends Controller
return redirect()->back()->with('error', 'Fail bukti kosong.');
}
$raw = $bukti['data'];
return $this->streamBase64File($bukti, 'bukti-' . $id);
}
/**
* PC: view uploaded customer bank slip.
*/
public function viewBuktiAkaun($id)
{
$this->abortUnlessPembiayaanOnline();
$kodcaw = $this->kodcaw();
$res = $this->api()
->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/bukti-akaun');
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Slip bank tidak dijumpai.';
return redirect()->back()->with('error', $msg);
}
$payload = $res->json();
$file = $payload['file'] ?? null;
if (!$file || empty($file['data'])) {
return redirect()->back()->with('error', 'Fail slip bank kosong.');
}
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).
*/
public function viewSag($id)
{
$this->abortUnlessPembiayaanOnline();
return $this->renderLineSag($this->kodcaw(), $id);
}
/**
* Operasi: view uploaded customer bank slip.
*/
public function operasiViewBuktiAkaun($kodcaw, $id)
{
$res = $this->api()
->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/bukti-akaun');
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Slip bank tidak dijumpai.';
return redirect()->back()->with('error', $msg);
}
$payload = $res->json();
$file = $payload['file'] ?? null;
if (!$file || empty($file['data'])) {
return redirect()->back()->with('error', 'Fail slip bank kosong.');
}
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.
*/
public function operasiViewSag($kodcaw, $id)
{
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'];
if (strpos($raw, 'base64,') !== false) {
$raw = substr($raw, strpos($raw, 'base64,') + 7);
}
$binary = base64_decode($raw, true);
if ($binary === false) {
$binary = base64_decode($bukti['data']);
$binary = base64_decode($file['data']);
}
$type = $bukti['type'] ?? 'application/octet-stream';
$filename = $bukti['filename'] ?? ('bukti-' . $id);
$type = $file['type'] ?? 'application/octet-stream';
$filename = $file['filename'] ?? $fallbackName;
return response($binary, 200, [
'Content-Type' => $type,
@@ -277,6 +517,115 @@ class PembiayaanOnlineController extends Controller
]);
}
protected function renderLineSag($kodcaw, $id)
{
$api_url = config('api.url');
$res = $this->api()->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id);
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Rekod tidak dijumpai.';
return redirect()->back()->with('error', $msg);
}
$line = $res->json();
$norujukan = is_array($line) ? trim((string) ($line['norujukan'] ?? '')) : '';
if ($norujukan === '') {
return redirect()->back()->with('error', 'No. SAG tidak dijumpai.');
}
return $this->renderSagByNorujukan($kodcaw, $norujukan);
}
protected function renderSagByNorujukan($kodcaw, $norujukan)
{
$api_url = config('api.url');
$auth_user = Auth::user();
$auth_username = $auth_user['name'] ?? '';
$gadai = $this->api()
->get($api_url . '/api/gadai/norujukan/' . $kodcaw . '/' . $norujukan)
->json();
$gadai_detail = is_array($gadai) ? ($gadai[0] ?? null) : null;
if (!is_array($gadai_detail) || empty($gadai_detail['norujukan'])) {
return redirect()->back()->with('error', 'SAG tidak dijumpai.');
}
$kakitangan = \App\User::where([['role_harian', 'khazanah'], ['cawangan', $kodcaw]])->first();
$customer = $this->api()
->get($api_url . '/api/customers/' . ($gadai_detail['nokp'] ?? ''))
->json();
$customer_detail = is_array($customer) ? $customer : [];
$marhun = $this->api()
->get($api_url . '/api/marhun/' . $norujukan)
->json();
$marhun_detail = is_array($marhun) ? ($marhun[0] ?? []) : [];
if (!is_array($marhun_detail)) {
$marhun_detail = [];
}
$mutu = $this->api()
->get($api_url . '/api/marhun/mutu/sag', [
'kodcaw' => $kodcaw,
'norujukan' => $norujukan,
])
->json();
$cawangan = $this->api()
->get($api_url . '/api/cawangan/detail/' . $kodcaw)
->json();
$cawangan_detail = is_array($cawangan) ? ($cawangan[0] ?? []) : [];
$kadar_ujrah = $this->api()
->get($api_url . '/api/kadarujrah/by_group_id/', [
'kodcaw' => $cawangan_detail['kodcaw'] ?? $kodcaw,
])
->json();
$pembayaran_segera = 'https://pay.arrahn.com.my';
$commodityPrint = app(\App\Services\CommodityCertificate::class)
->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0);
$komoditi = $commodityPrint['komoditi'];
$product = $commodityPrint['product'];
$kuantiti = $commodityPrint['kuantiti'];
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',
'kuantiti',
'gadai_detail',
'customer_detail',
'marhun_detail',
'cawangan_detail',
'api_url',
'pembayaran_segera',
'auth_username',
'mutu',
'kadar_ujrah',
'kakitangan'
))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
}
return view('print.sag', compact(
'gadai_detail',
'customer_detail',
'marhun_detail',
'cawangan_detail',
'api_url',
'pembayaran_segera',
'auth_username',
'mutu',
'kakitangan'
));
}
/**
* PC: tanda rekod PAID sebagai selesai semak / blast.
*/
@@ -309,7 +658,7 @@ class PembiayaanOnlineController extends Controller
$n = $res->json()['dikemaskini'] ?? count($ids);
return redirect()
->route('pembiayaan.online.pc.paid')
->with('message', $n . ' rekod ditanda selesai semak/blast.');
->with('message', $n . ' rekod ditanda selesai. WhatsApp notis dihantar (nombor ujian jika bukan production).');
}
/**
+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') {
+1 -1
View File
@@ -12,6 +12,6 @@ class VerifyCsrfToken extends Middleware
* @var array
*/
protected $except = [
//
'internal/notifications',
];
}
+94
View File
@@ -0,0 +1,94 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Throwable;
class CommodityCertificate
{
/**
* Certificate fields for a SAG. Shoraka is used when that order exists,
* otherwise the BSAS order, otherwise the legacy daily commodity stock.
*/
public function forReference(string $reference, $pinjaman = null): array
{
$shoraka = $this->shorakaPrint($reference);
if ($shoraka) {
return $this->normalize($shoraka['komoditi'], $shoraka['product'], $shoraka['kuantiti']);
}
$bsas = Http::withOptions(['verify' => config('app.api_verify')])
->get(rtrim((string) config('api.url'), '/') . '/api/komuditi/bsas/order/norujukan/' . urlencode($reference));
if ($bsas->successful() && is_array($bsas->json())) {
$payload = $bsas->json();
return $this->normalize(
$payload['komoditi'] ?? [],
$payload['product'] ?? [],
(float) ($payload['kuantiti'] ?? 0)
);
}
return $this->legacy($pinjaman);
}
private function shorakaPrint(string $reference): ?array
{
try {
return app(ShorakaFinancingService::class)->printData($reference);
} catch (HttpException $exception) {
if ($exception->getStatusCode() === 409) {
throw $exception;
}
return null;
} catch (Throwable $exception) {
return null;
}
}
private function legacy($pinjaman): array
{
$api = rtrim((string) config('api.url'), '/');
$komoditi = Http::withOptions(['verify' => config('app.api_verify')])
->get($api . '/api/komuditi/check')
->json();
if (!is_array($komoditi)) {
$komoditi = [];
}
$product = Http::withOptions(['verify' => config('app.api_verify')])
->get($api . '/api/komuditi/description', [
'kodproduk' => $komoditi['kodproduct'] ?? '',
])
->json();
$nilai = (float) ($komoditi['nilai'] ?? 0);
$kuantiti = $nilai > 0 ? ((float) $pinjaman / $nilai) : 0;
return $this->normalize($komoditi, is_array($product) ? $product : [], $kuantiti);
}
private function normalize($komoditi, $product, $kuantiti): array
{
if (!is_array($komoditi)) {
$komoditi = [];
}
if (!is_array($product)) {
$product = [];
}
$code = $product['product_code'] ?? $product['kodproduk'] ?? ($komoditi['kodproduct'] ?? '');
$product['product_code'] = $product['product_code'] ?? $code;
$product['kodproduk'] = $product['kodproduk'] ?? $code;
$product['description'] = $product['description'] ?? '';
$product['unit'] = $product['unit'] ?? 'TAN';
return [
'komoditi' => $komoditi,
'product' => $product,
'kuantiti' => $kuantiti,
];
}
}
+568
View File
@@ -0,0 +1,568 @@
<?php
namespace App\Services;
use App\StaffNotification;
use App\User;
use Illuminate\Support\Facades\Log;
class NotificationService
{
const TYPE_PEMBIAYAAN_PENDING_PC = 'pembiayaan_online.pending_pc';
const TYPE_MODAL_PENDING_OPERASI = 'modal_online.pending_operasi';
const TYPE_PEMBIAYAAN_BATCH_OPERASI = 'pembiayaan_online.batch_operasi';
const TYPE_PEMBIAYAAN_REJECTED_PC = 'pembiayaan_online.rejected_pc';
const TYPE_PEMBIAYAAN_PAID_PC = 'pembiayaan_online.paid_pc';
const TYPE_GADAI_KELULUSAN = 'gadai.kelulusan';
const TYPE_ANGGOTA_PENDING_OPERASI = 'anggota.pending_operasi';
const TYPE_ALIRAN_TUNAI_PENDING = 'aliran_tunai.pending_khazanah';
const TYPE_ALIRAN_TUNAI_DECIDED = 'aliran_tunai.decided_teller';
const GADAI_KELULUSAN_MIN = 10000;
/**
* Create one inbox row per staff matching role + branch.
*
* @param string $role
* @param string $kodcaw
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRoleAtCawangan($role, $kodcaw, array $payload)
{
$users = User::query()
->where('roles', $role)
->where('cawangan', $kodcaw)
->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Create one inbox row per staff matching role (any branch). For HQ like operasi.
*
* @param string $role
* @param array $payload
* @return \Illuminate\Support\Collection
*/
public function notifyRole($role, array $payload)
{
$kodcaw = isset($payload['cawangan']) ? $payload['cawangan'] : null;
$users = User::query()->where('roles', $role)->get();
$created = collect();
foreach ($users as $user) {
$created->push($this->notifyUser($user, $kodcaw, $role, $payload));
}
return $created;
}
/**
* Entry point for api_arrahn (and local callers). Builds title/url from type when omitted.
*
* @param array $input
* @return \Illuminate\Support\Collection
*/
public function dispatch(array $input)
{
$type = isset($input['type']) ? $input['type'] : '';
$kodcaw = isset($input['cawangan']) ? $input['cawangan'] : null;
$audience = isset($input['audience']) && is_array($input['audience']) ? $input['audience'] : [];
$role = isset($audience['role']) ? $audience['role'] : null;
$data = isset($input['data']) && is_array($input['data']) ? $input['data'] : [];
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING && $kodcaw) {
return $this->notifyAliranTunaiPending($kodcaw, $data);
}
$payload = $this->payloadForType($type, $kodcaw, $data);
if (!empty($input['title'])) {
$payload['title'] = $input['title'];
}
if (array_key_exists('body', $input) && $input['body'] !== null && $input['body'] !== '') {
$payload['body'] = $input['body'];
}
if (!empty($input['url'])) {
$payload['url'] = $input['url'];
}
if ($role === 'operasi') {
return $this->notifyRole('operasi', $payload);
}
if ($role && $kodcaw) {
return $this->notifyRoleAtCawangan($role, $kodcaw, $payload);
}
return collect();
}
/**
* Gadai >= RM10,000: inbox for PC/PPC at the branch and all operasi.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyGadaiKelulusan($kodcaw, array $data)
{
$pinjaman = isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0;
if ($pinjaman < self::GADAI_KELULUSAN_MIN) {
return collect();
}
$payload = $this->payloadForType(self::TYPE_GADAI_KELULUSAN, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge(
$this->notifyRoleAtCawangan($role, $kodcaw, array_merge($payload, [
'url' => '/kelulusan',
]))
);
}
return $created->merge(
$this->notifyRole('operasi', array_merge($payload, [
'url' => '/operasi/kelulusan',
]))
);
}
/**
* Teller pendahuluan/serahan: inbox for PC/PPC at the branch.
*
* @param string $kodcaw
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiPending($kodcaw, array $data)
{
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_PENDING, $kodcaw, $data);
$created = collect();
foreach (['pc', 'ppc'] as $role) {
$created = $created->merge($this->notifyRoleAtCawangan($role, $kodcaw, $payload));
}
return $created;
}
/**
* Khazanah lulus/batal: inbox for the teller who requested cash.
*
* @param string $kodcaw
* @param string $tellerName
* @param array $data
* @return \Illuminate\Support\Collection
*/
public function notifyAliranTunaiDecided($kodcaw, $tellerName, array $data)
{
$tellerName = trim((string) $tellerName);
if ($tellerName === '') {
return collect();
}
$payload = $this->payloadForType(self::TYPE_ALIRAN_TUNAI_DECIDED, $kodcaw, $data);
$user = User::query()
->where('name', $tellerName)
->where('cawangan', $kodcaw)
->first();
if (!$user) {
$user = User::query()->where('name', $tellerName)->first();
}
if (!$user) {
return collect();
}
return collect([$this->notifyUser($user, $kodcaw, $user->roles, $payload)]);
}
/**
* @param string $type
* @param string|null $kodcaw
* @param array $data
* @return array
*/
protected function payloadForType($type, $kodcaw, array $data)
{
if ($type === self::TYPE_MODAL_PENDING_OPERASI) {
$jenis = isset($data['jenis']) ? strtoupper((string) $data['jenis']) : 'HAD';
$label = $jenis === 'TOPUP' ? 'tambah modal' : 'had modal';
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$amaun = number_format(isset($data['amaun']) ? (float) $data['amaun'] : 0, 2);
$oleh = isset($data['dimohon_oleh']) ? trim((string) $data['dimohon_oleh']) : '';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan modal online',
'body' => sprintf(
'%s mohon %s RM %s%s',
$nama,
$label,
$amaun,
$oleh !== '' ? ' — '.$oleh : ''
),
'url' => '/pembiayaan-online/operasi/topup',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_BATCH_OPERASI) {
$nama = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : 'batch';
$batchId = isset($data['batch_id']) ? $data['batch_id'] : null;
$submitted = isset($data['submitted_count']) ? (int) $data['submitted_count'] : 0;
$jumlah = number_format(isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0, 2);
$oleh = isset($data['pc_submitted_by']) ? trim((string) $data['pc_submitted_by']) : '';
$appended = !empty($data['appended']);
if ($appended) {
$body = sprintf(
'%s tambah %d rekod ke %s (RM %s)%s',
$nama,
$submitted,
$batchNo,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
} else {
$body = sprintf(
'%s hantar %s — %d rekod (RM %s)%s',
$nama,
$batchNo,
$submitted,
$jumlah,
$oleh !== '' ? ' — '.$oleh : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Batch pembiayaan online',
'body' => $body,
'url' => ($kodcaw && $batchId)
? '/pembiayaan-online/operasi/'.$kodcaw.'/batches/'.$batchId
: '/pembiayaan-online/operasi',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_REJECTED_PC) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'rekod';
$reason = isset($data['reject_reason']) ? trim((string) $data['reject_reason']) : '';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online ditolak',
'body' => sprintf(
'%s (RM %s) ditolak%s',
$norujukan,
$amaun,
$reason !== '' ? ' — '.$reason : ''
),
'url' => '/pembiayaan-online/pc',
'data' => $data,
];
}
if ($type === self::TYPE_PEMBIAYAAN_PAID_PC) {
$paidCount = isset($data['paid_count']) ? (int) $data['paid_count'] : 1;
$batchNo = !empty($data['batch_no']) ? $data['batch_no'] : '';
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : '';
$nama = isset($data['nama_pelanggan']) ? trim((string) $data['nama_pelanggan']) : '';
$amaun = number_format(
isset($data['pinjaman'])
? (float) $data['pinjaman']
: (isset($data['jumlah_keseluruhan']) ? (float) $data['jumlah_keseluruhan'] : 0),
2
);
if ($paidCount > 1 || ($batchNo !== '' && $norujukan === '')) {
$body = sprintf(
'%d rekod telah dibayar%s (RM %s)',
$paidCount > 0 ? $paidCount : 0,
$batchNo !== '' ? ' dalam '.$batchNo : '',
$amaun
);
} else {
$body = sprintf(
'%s telah dibayar (RM %s)%s',
$norujukan !== '' ? $norujukan : 'Rekod',
$amaun,
$nama !== '' ? ' — '.$nama : ''
);
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Pembiayaan online telah dibayar',
'body' => $body,
'url' => '/pembiayaan-online/pc/paid',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_PENDING) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$teller = isset($data['kodlaluan']) ? trim((string) $data['kodlaluan']) : '';
$label = $jenis === 'serahan' ? 'serahan' : 'pendahuluan';
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $jenis === 'serahan' ? 'Permohonan serahan modal' : 'Permohonan pendahuluan',
'body' => sprintf(
'%s mohon %s RM %s',
$teller !== '' ? $teller : 'Teller',
$label,
number_format($amaun, 2)
),
'url' => $jenis === 'serahan' ? '/serahan' : '/pendahuluan',
'data' => $data,
];
}
if ($type === self::TYPE_ALIRAN_TUNAI_DECIDED) {
$jenis = $this->aliranTunaiJenis($data);
$amaun = $this->aliranTunaiAmaun($data, $jenis);
$lulus = isset($data['pengesahan']) && strcasecmp((string) $data['pengesahan'], 'Lulus') === 0;
$label = 'Pendahuluan';
if ($jenis === 'serahan') {
$label = 'Serahan';
} elseif ($jenis === 'all') {
$label = 'Aliran tunai';
}
if ($lulus) {
$title = $label.' diluluskan';
$body = sprintf('%s RM %s telah diluluskan', $label, number_format($amaun, 2));
} else {
$title = $label.' tidak diluluskan';
$body = sprintf('Permohonan %s tidak diluluskan', strtolower($label));
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => $title,
'body' => $body,
'url' => '/aliran_tunai',
'data' => $data,
];
}
if ($type === self::TYPE_ANGGOTA_PENDING_OPERASI) {
$nokp = !empty($data['nokp']) ? $data['nokp'] : '';
$nama = isset($data['nama']) ? trim((string) $data['nama']) : '';
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$komen = isset($data['komen']) ? trim((string) $data['komen']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
$who = $nama !== '' ? $nama : 'pelanggan';
if ($nokp !== '') {
$who .= ' ('.$nokp.')';
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Permohonan assign anggota',
'body' => sprintf(
'%s — %s%s%s',
$namaCaw,
$who,
$teller !== '' ? ' — '.$teller : '',
$komen !== '' ? ' — '.$komen : ''
),
'url' => '/operasi/kelulusan/anggota',
'data' => $data,
];
}
if ($type === self::TYPE_GADAI_KELULUSAN) {
$norujukan = !empty($data['norujukan']) ? $data['norujukan'] : 'gadai';
$amaun = number_format(isset($data['pinjaman']) ? (float) $data['pinjaman'] : 0, 2);
$teller = isset($data['teller']) ? trim((string) $data['teller']) : '';
$namaCaw = !empty($data['cawangan_nama']) ? $data['cawangan_nama'] : $kodcaw;
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => 'Kelulusan gadai melebihi RM 10,000',
'body' => sprintf(
'%s — %s (RM %s)%s',
$namaCaw,
$norujukan,
$amaun,
$teller !== '' ? ' — '.$teller : ''
),
'url' => '/kelulusan',
'data' => $data,
];
}
return [
'type' => $type,
'cawangan' => $kodcaw,
'title' => isset($data['title']) ? $data['title'] : $type,
'body' => isset($data['body']) ? $data['body'] : null,
'url' => isset($data['url']) ? $data['url'] : null,
'data' => $data,
];
}
/**
* @param array $data
* @return string pendahuluan|serahan|all
*/
protected function aliranTunaiJenis(array $data)
{
if (!empty($data['jenis'])) {
$jenis = strtolower(trim((string) $data['jenis']));
if ($jenis === 'serahan' || $jenis === 'pendahuluan' || $jenis === 'all') {
return $jenis;
}
}
$modal = isset($data['modal']) ? (float) $data['modal'] : 0;
$serahan = isset($data['serahan']) ? (float) $data['serahan'] : 0;
if ($serahan > 0 && $modal <= 0) {
return 'serahan';
}
return 'pendahuluan';
}
/**
* @param array $data
* @param string $jenis
* @return float
*/
protected function aliranTunaiAmaun(array $data, $jenis)
{
if (isset($data['amaun']) && is_numeric($data['amaun'])) {
return (float) $data['amaun'];
}
if ($jenis === 'serahan') {
return isset($data['serahan']) ? (float) $data['serahan'] : 0;
}
return isset($data['modal']) ? (float) $data['modal'] : 0;
}
/**
* Create one inbox row for a specific user.
*
* @param \App\User $user
* @param string|null $kodcaw
* @param string|null $role
* @param array $payload
* @return \App\StaffNotification
*/
public function notifyUser(User $user, $kodcaw, $role, array $payload)
{
return StaffNotification::create([
'user_id' => $user->id,
'cawangan' => $kodcaw,
'role' => $role ?: $user->roles,
'type' => $payload['type'],
'title' => $payload['title'],
'body' => isset($payload['body']) ? $payload['body'] : null,
'url' => isset($payload['url']) ? $payload['url'] : null,
'data' => isset($payload['data']) ? $payload['data'] : null,
]);
}
/**
* @param \App\User $user
* @param int $limit
* @return \Illuminate\Support\Collection
*/
public function inboxFor(User $user, $limit = 20)
{
return StaffNotification::query()
->where('user_id', $user->id)
->orderByDesc('id')
->limit($limit)
->get();
}
/**
* @param \App\User $user
* @return int
*/
public function unreadCount(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->count();
}
/**
* @param \App\User $user
* @param int $id
* @return \App\StaffNotification|null
*/
public function markAsRead(User $user, $id)
{
$notification = StaffNotification::query()
->where('user_id', $user->id)
->where('id', $id)
->first();
if (!$notification) {
return null;
}
if ($notification->read_at === null) {
$notification->read_at = now();
$notification->save();
}
return $notification;
}
/**
* @param \App\User $user
* @return int
*/
public function markAllAsRead(User $user)
{
return StaffNotification::query()
->where('user_id', $user->id)
->unread()
->update(['read_at' => now()]);
}
/**
* @param \App\User $user
* @param int $limit
* @return array
*/
public function inboxPayload(User $user, $limit = 20)
{
return [
'unread_count' => $this->unreadCount($user),
'notifications' => $this->inboxFor($user, $limit)->map(function (StaffNotification $notification) {
return $notification->toInboxArray();
})->values(),
];
}
}
+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);
}
}
+56
View File
@@ -0,0 +1,56 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class StaffNotification extends Model
{
protected $table = 'staff_notifications';
protected $fillable = [
'user_id',
'cawangan',
'role',
'type',
'title',
'body',
'url',
'data',
'read_at',
];
protected $casts = [
'data' => 'array',
'read_at' => 'datetime',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function isRead()
{
return $this->read_at !== null;
}
public function scopeUnread($query)
{
return $query->whereNull('read_at');
}
public function toInboxArray()
{
return [
'id' => $this->id,
'type' => $this->type,
'title' => $this->title,
'body' => $this->body,
'url' => $this->url,
'read_at' => $this->read_at ? $this->read_at->toIso8601String() : null,
'created_at' => $this->created_at ? $this->created_at->toIso8601String() : null,
'created_at_human' => $this->created_at ? $this->created_at->diffForHumans() : null,
];
}
}
+5
View File
@@ -46,6 +46,11 @@ class User extends Authenticatable
return $this->hasOne(Cawangan::class, 'kodcaw', 'cawangan');
}
public function staffNotifications()
{
return $this->hasMany(StaffNotification::class);
}
public function canImpersonate(): bool
{
return in_array($this->roles ?? '', ['admin'], true);
+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'),
];
+10
View File
@@ -0,0 +1,10 @@
<?php
return [
/*
| Which commodity providers a teller may use when submitting gadai.
| Set either or both to true. When both are true, the teller chooses.
*/
'shoraka' => filter_var(env('COMMODITY_SHORAKA_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
'bsas' => filter_var(env('COMMODITY_BSAS_ENABLED', false), FILTER_VALIDATE_BOOLEAN),
];
+1
View File
@@ -1,6 +1,7 @@
<?php
return [
'password' => env('LOGVIEWER_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Pattern and storage path settings
+5
View File
@@ -0,0 +1,5 @@
<?php
return [
'internal_secret' => env('INTERNAL_NOTIFICATION_SECRET', ''),
];
@@ -0,0 +1,43 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateStaffNotificationsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('staff_notifications', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->string('cawangan', 20)->nullable();
$table->string('role', 30)->nullable();
$table->string('type', 80);
$table->string('title');
$table->text('body')->nullable();
$table->string('url', 500)->nullable();
$table->text('data')->nullable();
$table->timestamp('read_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'read_at']);
$table->index(['cawangan', 'type']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('staff_notifications');
}
}
+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-fpm is Debian 11 (bullseye); after LTS end, packages live on archive.debian.org
RUN sed -i \
-e 's|http://deb.debian.org/debian|http://archive.debian.org/debian|g' \
-e 's|http://security.debian.org/debian-security|http://archive.debian.org/debian-security|g' \
-e 's|http://deb.debian.org/debian-security|http://archive.debian.org/debian-security|g' \
/etc/apt/sources.list \
&& printf 'Acquire::Check-Valid-Until "false";\n' > /etc/apt/apt.conf.d/99no-check-valid-until \
&& apt-get 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.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
Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="1136" viewBox="0 0 1600 1136">
<title>Vectorized Arabic calligraphy logo</title>
<path d="M 379,572 L 385,578 L 403,585 L 418,593 L 423,594 L 429,598 L 415,588 Z M 949,551 L 945,547 L 939,546 L 933,550 L 928,557 L 851,719 L 814,784 L 784,827 L 754,862 L 720,894 L 690,917 L 647,944 L 595,969 L 543,987 L 504,996 L 453,1002 L 406,1002 L 374,999 L 344,994 L 312,986 L 274,973 L 249,962 L 227,950 L 255,967 L 267,972 L 268,974 L 322,995 L 376,1008 L 417,1013 L 438,1014 L 499,1011 L 543,1003 L 572,995 L 606,983 L 660,958 L 708,928 L 748,896 L 785,858 L 815,819 L 836,787 L 864,737 L 895,674 L 930,597 L 943,571 L 950,563 Z M 292,508 L 288,515 L 247,563 L 256,571 L 260,573 L 266,572 L 303,528 L 303,519 Z M 989,450 L 969,456 L 945,459 L 917,458 L 900,451 L 894,451 L 888,454 L 880,462 L 869,476 L 842,504 L 802,536 L 765,557 L 741,567 L 722,573 L 676,582 L 630,584 L 600,582 L 573,578 L 525,566 L 523,567 L 520,573 L 520,585 L 530,614 L 538,654 L 538,687 L 535,707 L 529,729 L 516,757 L 498,782 L 472,807 L 429,836 L 393,854 L 358,867 L 312,878 L 287,881 L 235,880 L 194,871 L 162,857 L 179,868 L 214,883 L 238,889 L 273,893 L 308,892 L 347,886 L 386,875 L 418,862 L 440,851 L 465,836 L 500,809 L 523,783 L 531,771 L 541,751 L 548,730 L 552,710 L 553,666 L 545,627 L 535,596 L 535,590 L 540,581 L 543,579 L 551,579 L 578,587 L 607,592 L 638,595 L 675,595 L 697,593 L 741,584 L 777,571 L 804,557 L 827,542 L 853,521 L 881,493 L 893,478 L 907,465 L 918,463 L 935,469 L 962,470 Z M 1071,446 L 1064,445 L 1063,450 L 1057,461 L 1047,471 L 1035,480 L 1011,505 L 1001,520 L 993,536 L 987,555 L 984,576 L 984,594 L 987,613 L 991,625 L 999,640 L 1007,650 L 1018,659 L 1010,648 L 1009,643 L 1007,641 L 1000,615 L 999,598 L 1003,566 L 1010,546 L 1023,523 L 1046,497 L 1069,478 L 1076,468 L 1078,462 L 1078,454 L 1076,450 Z M 1247,419 L 1238,416 L 1233,418 L 1228,423 L 1200,481 L 1191,494 L 1179,507 L 1170,531 L 1170,536 L 1177,534 L 1189,525 L 1203,511 L 1217,492 L 1241,441 L 1245,435 L 1252,430 L 1252,426 Z M 1403,405 L 1394,406 L 1388,412 L 1383,423 L 1378,473 L 1374,491 L 1369,505 L 1367,523 L 1363,536 L 1362,545 L 1365,545 L 1375,533 L 1387,511 L 1395,475 L 1396,451 L 1399,435 L 1405,424 L 1409,420 L 1415,418 L 1411,411 Z M 1474,363 L 1486,379 L 1493,393 L 1500,416 L 1506,464 L 1506,502 L 1502,540 L 1495,575 L 1482,617 L 1470,646 L 1443,696 L 1422,727 L 1390,766 L 1331,825 L 1223,928 L 1149,1004 L 1136,1027 L 1132,1037 L 1132,1047 L 1137,1046 L 1177,1003 L 1251,929 L 1329,855 L 1401,784 L 1426,755 L 1447,727 L 1471,688 L 1492,644 L 1507,601 L 1516,561 L 1521,519 L 1520,462 L 1513,422 L 1508,407 L 1502,395 L 1492,381 Z M 1289,362 L 1286,360 L 1293,369 L 1302,386 L 1309,413 L 1309,458 L 1305,496 L 1296,538 L 1286,569 L 1269,606 L 1247,641 L 1218,676 L 1195,698 L 1159,726 L 1131,742 L 1103,752 L 1063,759 L 1044,759 L 1029,757 L 1002,748 L 1007,750 L 1014,756 L 1029,763 L 1058,770 L 1092,769 L 1114,765 L 1135,759 L 1166,744 L 1207,714 L 1241,681 L 1265,651 L 1284,620 L 1303,578 L 1316,533 L 1324,476 L 1325,437 L 1320,408 L 1307,382 Z M 896,320 L 883,320 L 875,324 L 871,328 L 866,341 L 866,357 L 869,367 L 874,375 L 885,384 L 886,383 L 883,378 L 881,368 L 881,358 L 884,347 L 890,338 L 900,332 L 910,332 L 902,323 Z M 953,302 L 952,303 L 958,320 L 958,340 L 953,364 L 940,394 L 941,398 L 945,401 L 959,400 L 969,375 L 973,355 L 973,335 L 970,323 L 959,306 L 957,306 Z M 939,267 L 940,273 L 942,276 L 946,278 L 955,278 L 971,266 L 977,266 L 982,269 L 973,262 L 956,253 L 951,254 L 947,260 Z M 1053,228 L 1072,251 L 1086,277 L 1092,295 L 1095,310 L 1095,333 L 1089,357 L 1073,385 L 1051,410 L 1034,425 L 1020,434 L 1042,428 L 1050,428 L 1056,430 L 1061,428 L 1079,410 L 1096,387 L 1105,368 L 1109,354 L 1110,323 L 1102,299 L 1100,288 L 1080,256 L 1076,253 L 1076,251 Z M 1152,174 L 1142,186 L 1141,191 L 1153,227 L 1166,255 L 1176,271 L 1189,287 L 1205,301 L 1199,296 L 1199,294 L 1189,282 L 1189,280 L 1182,271 L 1171,248 L 1157,209 L 1155,196 L 1155,176 L 1154,174 Z M 363,117 L 359,124 L 354,145 L 353,176 L 347,215 L 335,255 L 325,279 L 311,307 L 285,349 L 263,379 L 216,436 L 185,483 L 164,525 L 155,548 L 145,581 L 138,615 L 135,648 L 136,672 L 140,695 L 146,715 L 159,743 L 171,760 L 186,773 L 179,766 L 165,739 L 157,716 L 152,693 L 151,647 L 157,608 L 162,588 L 177,544 L 193,510 L 209,482 L 237,442 L 274,398 L 300,363 L 327,319 L 345,282 L 361,234 L 368,191 L 368,140 L 365,120 Z M 930,105 L 929,108 L 940,125 L 951,138 L 968,154 L 960,146 L 958,141 L 949,131 L 934,104 Z M 1363,78 L 1363,86 L 1358,98 L 1343,115 L 1332,137 L 1329,151 L 1329,168 L 1332,184 L 1337,199 L 1348,222 L 1368,254 L 1391,283 L 1423,316 L 1401,292 L 1401,290 L 1391,279 L 1370,249 L 1353,215 L 1347,197 L 1344,176 L 1347,152 L 1355,134 L 1375,108 L 1378,98 L 1378,92 L 1375,85 L 1370,80 Z" fill="#111111" fill-rule="evenodd"/>
<path d="M 240,467 L 225,482 L 198,514 L 195,520 L 199,527 L 203,529 L 237,559 L 245,563 L 252,557 L 290,510 L 290,507 L 287,502 L 283,500 L 275,492 L 272,491 L 270,488 L 267,487 L 247,470 Z M 1300,384 L 1293,371 L 1279,352 L 1258,336 L 1228,319 L 1205,303 L 1186,285 L 1177,274 L 1157,239 L 1143,203 L 1139,183 L 1139,160 L 1141,151 L 1139,149 L 1114,185 L 1105,203 L 1098,231 L 1098,272 L 1103,298 L 1111,322 L 1127,351 L 1152,384 L 1160,402 L 1165,418 L 1168,449 L 1164,478 L 1162,481 L 1162,486 L 1158,500 L 1144,531 L 1151,528 L 1174,511 L 1189,495 L 1200,479 L 1227,423 L 1237,415 L 1247,418 L 1251,422 L 1254,430 L 1254,436 L 1256,441 L 1256,475 L 1250,499 L 1237,527 L 1225,546 L 1216,557 L 1216,559 L 1212,562 L 1172,610 L 1147,635 L 1121,656 L 1106,665 L 1090,672 L 1065,676 L 1052,675 L 1037,671 L 1013,657 L 1004,648 L 996,637 L 989,623 L 985,610 L 983,596 L 983,575 L 986,555 L 994,531 L 1009,506 L 1026,487 L 1055,462 L 1061,452 L 1063,440 L 1059,432 L 1044,429 L 1026,433 L 1025,430 L 1041,418 L 1063,396 L 1081,371 L 1090,351 L 1093,338 L 1093,304 L 1084,275 L 1074,256 L 1061,238 L 1032,208 L 976,163 L 948,136 L 929,111 L 927,105 L 920,95 L 915,81 L 913,81 L 896,126 L 892,141 L 890,161 L 894,183 L 900,193 L 910,202 L 915,209 L 919,219 L 920,260 L 922,264 L 926,266 L 937,267 L 942,264 L 952,252 L 957,252 L 986,270 L 1011,293 L 1022,308 L 1031,326 L 1036,343 L 1036,352 L 1034,358 L 1024,371 L 1005,386 L 985,396 L 971,400 L 944,402 L 940,399 L 939,394 L 952,364 L 957,339 L 956,316 L 949,298 L 939,288 L 920,280 L 897,280 L 871,288 L 852,300 L 838,316 L 832,328 L 826,356 L 826,382 L 831,399 L 840,414 L 855,429 L 855,433 L 814,465 L 794,478 L 768,492 L 745,502 L 715,512 L 688,518 L 659,522 L 604,522 L 595,520 L 585,520 L 561,516 L 528,508 L 496,498 L 494,496 L 485,494 L 478,490 L 460,484 L 417,464 L 373,440 L 368,440 L 366,445 L 364,464 L 364,479 L 357,550 L 358,558 L 362,563 L 401,580 L 422,591 L 431,597 L 454,619 L 469,641 L 480,666 L 480,670 L 482,673 L 482,692 L 480,698 L 473,709 L 464,719 L 422,759 L 395,778 L 373,788 L 341,796 L 306,799 L 265,798 L 232,794 L 219,791 L 201,784 L 179,769 L 165,754 L 152,732 L 141,703 L 135,673 L 134,643 L 137,614 L 143,584 L 156,542 L 165,520 L 181,488 L 198,460 L 217,433 L 268,371 L 295,332 L 312,303 L 333,257 L 344,223 L 349,199 L 352,174 L 353,138 L 347,91 L 345,88 L 343,91 L 336,129 L 317,191 L 292,253 L 278,279 L 255,311 L 186,385 L 145,436 L 113,484 L 86,536 L 76,561 L 68,587 L 60,632 L 60,694 L 64,719 L 69,738 L 76,758 L 88,783 L 110,814 L 124,829 L 144,846 L 176,863 L 213,875 L 247,880 L 296,879 L 327,874 L 354,867 L 383,857 L 410,845 L 450,822 L 473,805 L 496,783 L 516,755 L 526,734 L 531,719 L 537,685 L 537,656 L 535,640 L 528,611 L 519,585 L 519,572 L 523,566 L 529,565 L 545,571 L 586,579 L 632,583 L 675,581 L 710,575 L 758,559 L 791,542 L 827,516 L 847,498 L 878,463 L 891,451 L 901,450 L 918,457 L 955,457 L 983,451 L 984,454 L 960,472 L 940,493 L 902,538 L 858,600 L 816,665 L 764,741 L 735,781 L 704,819 L 675,848 L 649,869 L 634,879 L 601,896 L 547,916 L 525,922 L 520,922 L 496,928 L 489,928 L 484,930 L 432,935 L 374,935 L 336,932 L 286,924 L 240,912 L 220,905 L 180,886 L 161,873 L 151,864 L 149,864 L 133,849 L 128,847 L 130,856 L 146,883 L 173,911 L 175,911 L 192,927 L 194,927 L 202,934 L 225,948 L 258,965 L 309,984 L 350,994 L 409,1001 L 451,1001 L 476,999 L 518,992 L 549,984 L 602,965 L 626,954 L 664,933 L 687,918 L 716,896 L 743,872 L 761,853 L 782,828 L 801,802 L 828,759 L 863,693 L 923,564 L 931,551 L 939,545 L 947,547 L 951,553 L 950,574 L 938,608 L 936,618 L 936,645 L 941,670 L 947,687 L 960,710 L 967,719 L 988,739 L 1009,750 L 1025,755 L 1046,758 L 1071,757 L 1106,750 L 1126,743 L 1148,732 L 1174,714 L 1214,679 L 1243,645 L 1264,613 L 1284,571 L 1299,522 L 1307,470 L 1308,414 L 1305,398 Z M 888,318 L 899,320 L 908,327 L 914,337 L 918,349 L 918,364 L 914,376 L 906,384 L 896,388 L 881,383 L 872,374 L 867,365 L 864,351 L 866,336 L 870,328 L 879,320 Z M 1349,62 L 1336,62 L 1328,65 L 1321,70 L 1306,87 L 1296,108 L 1288,138 L 1282,176 L 1281,224 L 1284,254 L 1293,296 L 1302,324 L 1318,362 L 1340,403 L 1344,413 L 1350,437 L 1352,458 L 1350,493 L 1346,517 L 1336,543 L 1335,549 L 1362,515 L 1371,497 L 1377,472 L 1380,432 L 1384,417 L 1394,405 L 1403,404 L 1413,412 L 1424,431 L 1434,464 L 1434,469 L 1438,483 L 1441,509 L 1442,536 L 1440,563 L 1434,594 L 1422,629 L 1407,660 L 1376,708 L 1336,756 L 1309,784 L 1260,829 L 1232,857 L 1196,897 L 1176,922 L 1154,952 L 1121,1004 L 1112,1024 L 1110,1037 L 1111,1042 L 1143,1010 L 1162,988 L 1230,920 L 1337,818 L 1385,770 L 1410,741 L 1445,691 L 1467,650 L 1483,611 L 1495,570 L 1503,524 L 1505,499 L 1505,465 L 1500,422 L 1491,391 L 1483,376 L 1474,364 L 1453,343 L 1409,304 L 1375,265 L 1349,226 L 1333,191 L 1328,169 L 1328,150 L 1330,140 L 1337,123 L 1344,112 L 1357,98 L 1362,85 L 1362,75 L 1359,70 Z" fill="#F2CC00" fill-rule="evenodd"/>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 166 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 50 KiB

+3
View File
@@ -115,6 +115,9 @@
</form>
</div>
<div class="card-footer text-center pt-0 px-lg-2 px-1">
<p class="mb-3 text-sm">
<a href="{{ route('dasar.privasi') }}" class="text-info font-weight-bold">Dasar Privasi</a>
</p>
<p class="mb-4 text-sm mx-auto">
Masalah untuk log masuk?
<a class="text-info text-gradient font-weight-bold">Hubungi
+1 -1
View File
@@ -412,7 +412,7 @@
</div>
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
@@ -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;
+1 -1
View File
@@ -541,7 +541,7 @@
</div>
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
@@ -131,7 +131,8 @@
@endforeach
</tr>
</thead>
<tbody></tbody>
{{ $slot }}
<tbody>
{{ $slot }}
</tbody>
</table>
</div>
@@ -0,0 +1,50 @@
@props([
'note' => '',
'page' => null,
'total' => null,
])
<style>
.bsas-ecert-footer {
flex: 0 0 auto;
margin-top: auto;
padding-top: 12px;
border-top: 2px solid var(--accent, #0f4c5c);
font-family: Arial, Helvetica, sans-serif;
color: var(--muted, #5a6570);
}
.bsas-ecert-footer .footer-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.bsas-ecert-footer .footer-note {
margin: 0;
font-size: 10px;
text-align: left;
line-height: 1.4;
}
.bsas-ecert-footer .page-no {
flex: 0 0 auto;
font-size: 10px;
white-space: nowrap;
}
</style>
<footer {{ $attributes->merge(['class' => 'bsas-ecert-footer']) }}>
<div class="footer-row">
<p class="footer-note">
@if(trim($slot) !== '')
{{ $slot }}
@else
{{ $note }}
@endif
</p>
@if($page !== null && $total !== null)
<div class="page-no">Page {{ $page }} of {{ $total }}</div>
@elseif($page !== null)
<div class="page-no">{{ $page }}</div>
@endif
</div>
</footer>
@@ -0,0 +1,63 @@
@props([
'company' => 'Koperasi Permodalan Kelantan Berhad',
'leftLogo' => null,
'rightLogo' => null,
])
@php
$leftLogo = $leftLogo ?? asset('img/svg/logo-kopkb.svg');
$rightLogo = $rightLogo ?? asset('img/svg/logo-arrahn-new.svg');
@endphp
<style>
.bsas-ecert-header {
flex: 0 0 auto;
padding-bottom: 8px;
margin-bottom: 14px;
}
.bsas-ecert-header .brand-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.bsas-ecert-header .logos {
display: flex;
align-items: center;
gap: 12px;
}
.bsas-ecert-header .logos img {
height: 78px;
width: auto;
object-fit: contain;
}
.bsas-ecert-header .logos.is-end {
justify-content: flex-end;
}
.bsas-ecert-header .company-heading {
text-align: center;
margin: 14px 0 0;
}
.bsas-ecert-header .company-heading .name {
margin: 0;
font-size: 16px;
font-weight: 700;
letter-spacing: .06em;
color: var(--accent, #0f4c5c);
text-transform: uppercase;
}
</style>
<header {{ $attributes->merge(['class' => 'bsas-ecert-header']) }}>
<div class="brand-row">
<div class="logos">
<img src="{{ $leftLogo }}" alt="KPKB">
</div>
<div class="logos is-end">
<img src="{{ $rightLogo }}" alt="Ar-Rahn">
</div>
</div>
<div class="company-heading">
<p class="name">{{ $company }}</p>
</div>
</header>
@@ -0,0 +1,35 @@
@props([
'leftLogo' => null,
'rightLogo' => null,
])
@php
$leftLogo = $leftLogo ?? asset('img/svg/logo-kopkb.svg');
$rightLogo = $rightLogo ?? asset('img/logo-arrahn.jpeg');
@endphp
<style>
.bsas-ecert-watermark {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
gap: 28px;
pointer-events: none;
z-index: 0;
opacity: .09;
}
.bsas-ecert-watermark img {
height: 230px;
width: auto;
max-width: 300px;
object-fit: contain;
filter: grayscale(1);
}
</style>
<div {{ $attributes->merge(['class' => 'bsas-ecert-watermark']) }} aria-hidden="true">
<img src="{{ $leftLogo }}" alt="">
<img src="{{ $rightLogo }}" alt="">
</div>
+8 -8
View File
@@ -437,7 +437,7 @@
</div>
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
@@ -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;
+94 -39
View File
@@ -390,14 +390,14 @@
<x-datatable
id="myTable"
filename="SAG_Baru"
:columns="['Bil', 'No Rujukan', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Borang Online']"
:columns="['Bil', 'No Rujukan', 'Cara Terima', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Borang Online']"
/>
</div>
<div class="tab-pane fade {{ $activate_pane_SAG_lama }}" id="nav-xtawarruq" role="tabpanel" aria-labelledby="nav-xtawarruq-tab">
<x-datatable
id="xtawarruq"
filename="SAG_Lama"
:columns="['Bil', 'No Rujukan', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Borang Online']"
:columns="['Bil', 'No Rujukan', 'Cara Terima', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Borang Online']"
/>
</div>
</div>
@@ -712,13 +712,33 @@
</div>
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var min_komoditi = "{{ $min_komoditi }}";
$('#daftar_gadai').on('click', function() {
$('#daftar_gadai').on('click', async function(event) {
event.preventDefault();
if ($(this).prop('disabled')) {
return;
}
$("#daftar_gadai").prop('disabled', true);
var bakiLelong = Number(@json((float) $bakilelong));
if (bakiLelong > 0) {
var confirmation = await Swal.fire({
icon: 'warning',
title: 'Baki Lelong',
text: 'Pelanggan mempunyai baki lelong, teruskan gadai?',
showCancelButton: true,
confirmButtonText: 'Teruskan',
cancelButtonText: 'Batal',
focusCancel: true
});
if (!(confirmation.isConfirmed || confirmation.value === true)) {
$("#daftar_gadai").prop('disabled', false);
return;
}
}
var noic = $('#noic').val();
var iclama = "{{ $customer_info['kplama'] }}";
var icbaru = "{{ $customer_info['kpbaru'] }}";
@@ -734,42 +754,57 @@
event.preventDefault();
return false;
} else {
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/check',
success: function(data) {
var check_komuditi = Object.keys(data).length;
if (check_komuditi == 0) {
Swal.fire({
icon: 'error',
title: 'Unit Komoditi Berbaki Minima',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komoditi'
});
$("#daftar_gadai").prop('disabled', false);
event.preventDefault();
return false;
} else {
if (Number(data['unit_komuditi']) <= min_komoditi) {
$("#daftar_gadai").prop('disabled', false);
var commodityShoraka = @json((bool) config('commodity.shoraka'));
var commodityBsas = @json((bool) config('commodity.bsas'));
var openGadaian = function () {
if (icbaru.length > 0) {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}";
} else {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}";
}
};
if (!commodityShoraka && !commodityBsas) {
Swal.fire({
icon: 'error',
title: 'Komoditi tidak diaktifkan',
text: 'Tiada pembekal komoditi diaktifkan. Hubungi operasi.'
});
$("#daftar_gadai").prop('disabled', false);
return false;
}
if (commodityShoraka && !commodityBsas) {
$.ajax({
method: 'get',
url: api_url + '/api/shoraka/readiness',
success: function (data) {
if (!data || !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.'
});
event.preventDefault();
$("#daftar_gadai").prop('disabled', false);
return false;
} else {
if (icbaru.length > 0) {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kpbaru']]) }}";
} else {
window.location.href =
"{{ route('gadaian', ['noic' => $customer_info['kplama']]) }}";
}
}
openGadaian();
},
error: function () {
Swal.fire({
icon: 'error',
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
$("#daftar_gadai").prop('disabled', false);
}
}
});
});
return false;
}
openGadaian();
}
});
@@ -779,9 +814,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' }
]
@@ -789,6 +827,7 @@
var xtawarruq = initErahnDatatable('#xtawarruq', {
columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' }
]
});
@@ -817,6 +856,10 @@
return '{{ url("/gadai/borang-pengesahan-online") }}/' + encodeURIComponent(norujukan);
}
function formatCaraTerima(value) {
return String(value || 'TUNAI').toUpperCase() === 'ONLINE' ? 'Online' : 'Tunai';
}
function cetakBorangOnlineCell(gadai_detail) {
var cara = String(gadai_detail['cara_terima'] || '').toUpperCase();
if (cara !== 'ONLINE') {
@@ -1133,6 +1176,13 @@
text: 'Pembiayaan Sedang Dilelong..'
});
}
},
error: function() {
Swal.fire({
icon: 'error',
title: 'Amaran',
text: 'Tidak dapat semak status lelong. Sila cuba semula.'
});
}
});
@@ -1198,13 +1248,14 @@
let table_norujukan = gadai_detail["norujukan"];
if (gadai_detail["percentpinj"] != 0)
xtawarruq.row.add([
'<input type="radio" value=' + gadai_detail["norujukan"] +
' name="belum"/>',
'<input type="radio" value="' + gadai_detail["norujukan"] +
'" name="belum" data-cara-terima="' + (gadai_detail['cara_terima'] || '') + '"/>',
'<a href="#" onclick="checkserah(this)" data-pinjaman=' +
gadai_detail['pinjaman'] + ' data-hargajualan=' +
gadai_detail['hargajualan'] + ' data-norujukan=' +
gadai_detail['norujukan'] + ' data-url=' + urlroute + '>' +
gadai_detail['norujukan'] + '</a>',
formatCaraTerima(gadai_detail['cara_terima']),
gadai_detail['t_gadai'],
tarikh_matang,
'RM ' + formatter.format(gadai_detail['bakihargajualan']),
@@ -1216,6 +1267,7 @@
'RM ' + formatter.format(upahsemasa),
'<div style="white-space: nowrap; color:red; font-weight: bolder">' +
taglel + '</div>',
cetakBorangOnlineCell(gadai_detail),
]).draw();
} else {
var taglel = '';
@@ -1256,6 +1308,7 @@
gadai_detail['hargajualan'] + ' data-norujukan=' +
gadai_detail['norujukan'] + ' data-url=' + urlroute + '>' +
gadai_detail['norujukan'] + '</a>',
formatCaraTerima(gadai_detail['cara_terima']),
gadai_detail['t_gadai'],
tarikh_matang,
'RM ' + formatter.format(gadai_detail['bakihargajualan']),
@@ -2031,7 +2084,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(2)').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) {
@@ -2448,7 +2502,8 @@
}
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(2)').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);
+100
View File
@@ -0,0 +1,100 @@
<!DOCTYPE html>
<html lang="ms">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dasar Privasi | Kaunter arOS</title>
<link rel="icon" href="{{ asset('img/icon_pkb.png') }}">
<link href="{{ asset('css/login-v2.css') }}" rel="stylesheet">
<style>
body { background: #f4f7fb; color: #344767; }
.privacy-container { max-width: 900px; margin: 0 auto; padding: 32px 20px; }
.privacy-brand { display: flex; align-items: center; gap: 16px; margin-bottom: 24px; }
.privacy-brand img { width: 60px; height: auto; }
.privacy-card { background: #fff; border-radius: 16px; padding: 40px; box-shadow: 0 8px 32px rgba(24, 49, 83, .08); }
.privacy-card h1 { font-size: 2rem; }
.privacy-card h2 { font-size: 1.2rem; margin-top: 28px; }
.privacy-card p, .privacy-card li { line-height: 1.8; }
.privacy-card ul { padding-left: 24px; }
.privacy-back { display: inline-block; margin-top: 24px; color: #075985; text-decoration: underline; }
a:focus-visible { outline: 3px solid #075985; outline-offset: 4px; }
@media (max-width: 576px) {
.privacy-container { padding: 20px 16px; }
.privacy-card { padding: 24px 20px; }
.privacy-card h1 { font-size: 1.7rem; }
}
</style>
</head>
<body>
<main class="privacy-container">
<div class="privacy-brand">
<img src="{{ asset('img/icon_pkb.png') }}" alt="Logo ar-Rahnu">
<span class="font-weight-bold">Kaunter arOS &middot; ar-rahn Online System</span>
</div>
<article class="privacy-card" aria-labelledby="privacy-title">
<h1 id="privacy-title">Dasar Privasi</h1>
<p class="text-sm text-muted">Versi dasar: 1.0 &middot; Tarikh kemas kini: <time datetime="2026-09-27">27 September 2026</time></p>
<h2>1. Pengenalan</h2>
<p>Selamat datang ke sistem Kaunter arOS Koperasi Permodalan Kelantan Berhad (KoPKB). Kami amat menghargai kepercayaan yang anda berikan untuk mengendalikan maklumat peribadi anda. Dasar Privasi ini digubal untuk membantu anda memahami bagaimana kami mengumpul, menggunakan, mendedahkan dan melindungi data peribadi anda bagi urusan Ar-Rahn, dengan mengambil kira keperluan Akta Perlindungan Data Peribadi 2010 (PDPA) dan amalan keselamatan maklumat yang berkaitan.</p>
<p>Dasar ini terpakai kepada kakitangan, pentadbir dan mana-mana pihak yang diberi kuasa untuk mengakses atau menggunakan sistem Kaunter arOS KoPKB, serta pengendalian data pelanggan dalam perkhidmatan yang diuruskan melalui sistem ini.</p>
<h2>2. Data yang Dikumpul</h2>
<p>Kami mengumpul dan memproses maklumat yang berkaitan dengan urusan sistem, termasuk:</p>
<ul>
<li><strong>Maklumat Pengenalan Peribadi:</strong> Nama penuh, nombor pengenalan, alamat, alamat e-mel dan nombor telefon pelanggan atau pengguna.</li>
<li><strong>Maklumat Akaun:</strong> Maklumat akaun pengguna, cawangan dan peranan akses dalam sistem.</li>
<li><strong>Data Teknikal:</strong> Maklumat seperti alamat IP, jenis pelayar dan log aktiviti sistem yang direkodkan bagi tujuan operasi dan keselamatan.</li>
<li><strong>Data Transaksi:</strong> Rekod pembiayaan, gadaian, maklumat marhun, pembayaran, penebusan, lelongan dan dokumen sokongan berkaitan.</li>
</ul>
<h2>3. Tujuan Penggunaan Data</h2>
<p>Data anda digunakan untuk tujuan berikut:</p>
<ul>
<li><strong>Operasi Sistem:</strong> Menguruskan akaun pengguna dan memproses urusan pelanggan serta transaksi Ar-Rahn.</li>
<li><strong>Pengesahan:</strong> Mengesahkan identiti dan mengawal akses pengguna mengikut peranan serta cawangan yang dibenarkan.</li>
<li><strong>Komunikasi:</strong> Menyampaikan notis berkaitan urusan, makluman pentadbiran dan sokongan pelanggan.</li>
<li><strong>Pelaporan dan Penambahbaikan:</strong> Menyediakan laporan operasi, menyokong audit serta menambah baik prestasi dan kualiti sistem.</li>
</ul>
<h2>4. Perkongsian &amp; Pendedahan Data</h2>
<p>Kami tidak menjual data peribadi anda. Akses, perkongsian atau pendedahan data dihadkan kepada keperluan urusan yang berkaitan, termasuk:</p>
<ul>
<li><strong>Kakitangan yang Diberi Kuasa:</strong> Kakitangan dan pentadbir KoPKB yang memerlukan maklumat untuk menjalankan tugas.</li>
<li><strong>Pembekal Perkhidmatan:</strong> Penyedia teknologi atau perkhidmatan yang menyokong pengehosan, penyelenggaraan sistem dan pemprosesan pembayaran, jika berkaitan.</li>
<li><strong>Pematuhan Undang-undang:</strong> Pihak berkuasa atau pihak lain apabila pendedahan diperlukan oleh undang-undang atau perintah mahkamah.</li>
</ul>
<h2>5. Keselamatan Data</h2>
<p>Langkah keselamatan teknikal dan pentadbiran digunakan untuk membantu melindungi data yang dikendalikan melalui sistem, termasuk kawalan akses mengikut peranan pengguna dan pengesahan akaun.</p>
<p>Pengguna hendaklah mengakses sistem melalui sambungan HTTPS yang selamat, menjaga kerahsiaan kata laluan, tidak berkongsi akaun dan log keluar selepas selesai menggunakan sistem. Sebarang aktiviti yang mencurigakan hendaklah dilaporkan kepada pentadbir sistem.</p>
<h2>6. Hak Pengguna</h2>
<p>Anda boleh menghubungi kami untuk mengemukakan permohonan berkaitan data peribadi anda, termasuk:</p>
<ul>
<li><strong>Akses &amp; Pembetulan:</strong> Memohon semakan dan pembetulan maklumat peribadi yang tidak tepat atau tidak terkini.</li>
<li><strong>Pemadaman:</strong> Memohon pemadaman akaun atau data dalam keadaan yang sesuai, tertakluk kepada keperluan penyimpanan rekod transaksi, audit dan undang-undang.</li>
<li><strong>Sekatan:</strong> Memohon supaya pemprosesan data tertentu dihadkan, tertakluk kepada keperluan perkhidmatan dan kewajipan yang terpakai.</li>
</ul>
<p>Permohonan akan dinilai mengikut keperluan yang terpakai. Pengesahan identiti mungkin diperlukan sebelum permohonan diproses.</p>
<h2>7. Tempoh Penyimpanan</h2>
<p>Data disimpan selama diperlukan untuk pengendalian urusan Ar-Rahn, penyelenggaraan rekod pembiayaan dan transaksi, audit serta pematuhan undang-undang. Selepas keperluan penyimpanan berakhir, data akan diuruskan untuk pemadaman atau penyahkenalpastian mengikut prosedur organisasi yang terpakai.</p>
<h2>8. Kuki &amp; Teknologi Penjejakan</h2>
<p>Sistem Kaunter arOS menggunakan kuki (cookies) dan sesi untuk menyokong log masuk, mengekalkan sesi pengguna dan melindungi permintaan dalam sistem. Anda boleh menguruskan pilihan kuki melalui tetapan pelayar. Menyekat kuki boleh menyebabkan fungsi tertentu, termasuk log masuk, tidak berfungsi dengan baik.</p>
<h2>9. Perubahan Dasar</h2>
<p>Dasar ini boleh dikemas kini dari semasa ke semasa apabila terdapat perubahan pada perkhidmatan, pengendalian sistem atau keperluan organisasi. Versi terkini akan dipaparkan pada halaman ini. Sebarang perubahan besar akan dimaklumkan melalui e-mel atau notis dalam sistem sebelum tarikh kuat kuasa baharu.</p>
<h2>10. Hubungi Kami</h2>
<p>Jika anda mempunyai pertanyaan, komen atau permohonan berkaitan dasar ini dan pengendalian data peribadi dalam sistem Kaunter arOS, sila hubungi:</p>
<p><strong>AFRINA ALIMI</strong><br>
Koperasi Permodalan Kelantan Berhad (KoPKB)<br>
E-mel: <a href="mailto:afrina@koppkb.com">afrina@koppkb.com</a>
</p>
<a class="privacy-back" href="{{ route('login') }}">Kembali ke Log Masuk</a>
</article>
</main>
</body>
</html>
@@ -204,7 +204,7 @@
/>
<div class="title">
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Online (Akaun Bank)</h1>
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Atas Talian (Online)</h1>
<p>Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.</p>
</div>
@@ -243,7 +243,7 @@
</tr>
<tr>
<th>Cara Terima Pembiayaan</th>
<td>Online (kredit ke akaun bank) — bukan tunai</td>
<td>Atas Talian (Online)</td>
</tr>
<tr>
<th>Teller</th>
+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;
}
}
+324 -53
View File
@@ -29,6 +29,7 @@
<div class="form-group row col-12">
<input type="hidden" name="kodcaw" id="kodcaw" value="{{$cawangan_info['kodcaw']}}">
<input type="hidden" id="pembiayaan_online_enabled" value="{{ !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']) ? '1' : '0' }}">
<input type="hidden" id="rebet_annur_enabled" value="{{ !empty(($cawangan_info['features'] ?? [])['rebet_annur']) ? '1' : '0' }}">
<input type="hidden" name="nokp" id="nokp" value="{{ $customer_info['kpbaru'] }}">
<input type="hidden" name="tag_anggota" id="tag_anggota" value="{{ $customer_info['tag_anggota'] ?? 0 }}">
@@ -186,6 +187,19 @@
</div>
</diV>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<div class="col-4">
<div class="form-group row">
<div class="col-md-6">
Kadar Rebet
</div>
<div class="col-md-6">
<input type="text" class="form-control" readonly name="rebate" id="rebate">
<input type="text" class="form-control" readonly name="kadar_rebate" id="kadar_rebate">
</div>
</div>
</div>
@endif
<div class="col-4">
<div class="form-group row">
<div class="col-md-6">
@@ -232,9 +246,8 @@
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="cara_terima" id="cara_terima_online" value="ONLINE">
<label class="form-check-label" for="cara_terima_online">Online (Akaun Bank)</label>
<label class="form-check-label" for="cara_terima_online">Atas Talian (Online)</label>
</div>
<small class="form-text text-muted">Pilihan Online untuk pembiayaan RM5,000 – RM50,000 sahaja.</small>
</div>
</div>
<div id="bank_online_section" style="display:none;">
@@ -281,6 +294,35 @@
</div>
</div>
</div>
<div class="col-12" id="commodity_provider_section">
@php
$commodityProviders = array_values(array_filter([
config('commodity.shoraka') ? ['id' => 'shoraka', 'label' => 'Shoraka'] : null,
config('commodity.bsas') ? ['id' => 'bsas', 'label' => 'BSAS'] : null,
]));
@endphp
<div class="form-group row mb-2">
<div class="col-md-3">Pembekal Komoditi</div>
<div class="col-md-9">
@if (count($commodityProviders) === 0)
<span class="text-danger">Tiada pembekal komoditi diaktifkan.</span>
@elseif (count($commodityProviders) === 1)
<input type="hidden" name="commodity_provider" value="{{ $commodityProviders[0]['id'] }}">
<span>{{ $commodityProviders[0]['label'] }}</span>
@else
@foreach ($commodityProviders as $provider)
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="commodity_provider"
id="commodity_provider_{{ $provider['id'] }}"
value="{{ $provider['id'] }}">
<label class="form-check-label" for="commodity_provider_{{ $provider['id'] }}">{{ $provider['label'] }}</label>
</div>
@endforeach
<div class="text-muted small">Pilih pembekal sebelum tekan Terima.</div>
@endif
</div>
</div>
</div>
<div class="col-12">
<input type="text" name="norujukan_gadai" id="norujukan_gadai" value="{{ $norujukan }}" hidden>
<input type="text" name="marhun_detail" id="marhun_detail" value="" hidden>
@@ -292,6 +334,9 @@
<a href="{{ route('gadai.batal',['norujukan'=>$norujukan]) }}" class="float-right btn btn-danger mr-2">Batal</a>
<input type="submit" name="terima_gadai" id="terima_gadai" class="float-right btn btn-primary mr-2" value="Terima">
<button type="button" id="cetak_borang_online" class="float-right btn btn-info mr-2" style="display:none;">Cetak Borang</button>
<div id="cetak_borang_hint" class="text-danger text-right mt-2" style="clear:both; display:none;">
Sila cetak borang pengesahan pelanggan dahulu
</div>
</div>
</div>
</form>
@@ -398,6 +443,9 @@
<script>
var api_url = "{{ api_url_for_browser() }}";
var kodcaw_ujrah = "<?php echo $cawangan_info['kodcaw'] ?>";
const MIN_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MIN_PINJAMAN }};
const MAX_PINJAMAN = {{ \App\Http\Controllers\GadaianController::MAX_PINJAMAN }};
var printedBorang = @json($borang_online_printed ?? null);
$('form input').keydown(function (e) {
if (e.keyCode == 13) {
e.preventDefault();
@@ -409,7 +457,13 @@
$(document).on("keydown", disableF5);
var norujukan = $('#norujukan').val();
getMarhun(norujukan);
@if(session('error'))
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: @json(session('error'))
});
@endif
$.ajax({
method:'GET',
url: api_url + '/api/jemas',
@@ -789,6 +843,26 @@
$('#ujrah').val(formatter.format(getRoundedMethod(array_keuntungan['ujrah']).toFixed(2)));
$('#onepercent').val(formatter.format(getRoundedMethod(array_keuntungan['onepercent']).toFixed(2)));
if ($('#rebet_annur_enabled').val() === '1') {
var rebate, kadar_rebate;
if (pinjaman < 20000) {
rebate = keuntungan_sebulan * 0.05;
kadar_rebate = 5;
} else if (pinjaman >= 20000 && pinjaman <= 50000) {
rebate = keuntungan_sebulan * 0.10;
kadar_rebate = 10;
} else if (pinjaman > 50000 && pinjaman <= 100000) {
rebate = keuntungan_sebulan * 0.15;
kadar_rebate = 15;
} else {
rebate = keuntungan_sebulan * 0.20;
kadar_rebate = 20;
}
rebate = getRoundedMethod(rebate);
$('#rebate').val(formatter.format(rebate.toFixed(2)));
$('#kadar_rebate').val(formatter.format(kadar_rebate.toFixed(2)) + ' %');
}
});
const formatter = new Intl.NumberFormat('en-US', {
@@ -1266,9 +1340,54 @@
.then(response => response.json());
}
function currentBorangFields() {
var pinjaman = Number(String($('#pinjaman').val() || '').replace(/,/g, ''));
if (!isFinite(pinjaman)) {
pinjaman = 0;
}
return {
norujukan: ($('#norujukan_gadai').val() || $('#norujukan').val() || '').trim(),
pinjaman: pinjaman.toFixed(2),
kodbank: ($('#gadai_kodbank').val() || '').trim().toUpperCase(),
noakaun: ($('#gadai_noakaun').val() || '').replace(/\s+/g, ''),
nama_akaun: ($('#gadai_nama_akaun').val() || $('input[name="nama_pelanggan"]').val() || '').trim().toUpperCase()
};
}
function isOnlineCaraTerima() {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
var pinjaman = Number(String($('#pinjaman').val() || '').replace(/,/g, ''));
return pembiayaanOnlineEnabled
&& pinjaman >= MIN_PINJAMAN
&& pinjaman <= MAX_PINJAMAN
&& $('input[name="cara_terima"]:checked').val() === 'ONLINE';
}
function hasMatchingPrint() {
if (!printedBorang) {
return false;
}
var cur = currentBorangFields();
return printedBorang.norujukan === cur.norujukan
&& printedBorang.pinjaman === cur.pinjaman
&& printedBorang.kodbank === cur.kodbank
&& printedBorang.noakaun === cur.noakaun
&& printedBorang.nama_akaun === cur.nama_akaun;
}
function syncTerimaButton() {
var lock = isOnlineCaraTerima() && !hasMatchingPrint();
$('#terima_gadai').prop('disabled', lock);
if (lock) {
$('#cetak_borang_hint').show();
} else {
$('#cetak_borang_hint').hide();
}
}
function toggleCaraTerima(pinjaman) {
var pembiayaanOnlineEnabled = $('#pembiayaan_online_enabled').val() === '1';
if (pembiayaanOnlineEnabled && pinjaman >= 5000 && pinjaman <= 50000) {
if (pembiayaanOnlineEnabled && pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN) {
$('#cara_terima_section').show();
toggleBankOnlineSection();
} else {
@@ -1276,6 +1395,7 @@
$('#bank_online_section').hide();
$('#cetak_borang_online').hide();
$('#cara_terima_tunai').prop('checked', true);
syncTerimaButton();
}
}
@@ -1290,16 +1410,20 @@
$('#bank_online_section').hide();
$('#cetak_borang_online').hide();
}
syncTerimaButton();
}
$(document).on('change', 'input[name="cara_terima"]', function () {
toggleBankOnlineSection();
});
$(document).on('change', '#gadai_kodbank', function () {
$(document).on('change input', '#gadai_kodbank, #gadai_noakaun, #gadai_nama_akaun', function () {
$('#gadai_nama_bank').val($('#gadai_kodbank option:selected').data('nama') || '');
syncTerimaButton();
});
syncTerimaButton();
$(document).on('click', '#cetak_borang_online', function () {
var kodbank = $('#gadai_kodbank').val();
var noakaun = ($('#gadai_noakaun').val() || '').trim();
@@ -1332,6 +1456,9 @@
form.append($('<input>', { type: 'hidden', name: 'noakaun', value: noakaun }));
form.append($('<input>', { type: 'hidden', name: 'nama_akaun', value: namaAkaun }));
form.appendTo('body').trigger('submit').remove();
printedBorang = currentBorangFields();
syncTerimaButton();
});
async function getModalOnline() {
@@ -1381,7 +1508,7 @@
async function checkDisbursementFloat(pinjaman) {
var cara = $('input[name="cara_terima"]:checked').val() || 'TUNAI';
if (!(pinjaman >= 5000 && pinjaman <= 50000)) {
if (!(pinjaman >= MIN_PINJAMAN && pinjaman <= MAX_PINJAMAN)) {
cara = 'TUNAI';
}
@@ -1463,6 +1590,183 @@
return true;
}
function bsasProgressHtml() {
return '' +
'<div class="text-left" id="bsas-progress-box">' +
'<p class="mb-2">Sedang memproses pembelian komoditi...</p>' +
'<ol class="mb-2 pl-3" style="list-style:none;padding-left:0;">' +
'<li id="bsas-step-1" class="mb-2 small text-muted" data-state="pending">' +
'<span class="bsas-step-icon mr-1">○</span>' +
'<strong>1.</strong> Menghantar order ke BSAS' +
'<div class="bsas-step-remark text-muted" style="margin-left:1.25rem;"></div>' +
'</li>' +
'<li id="bsas-step-2" class="mb-2 small text-muted" data-state="pending">' +
'<span class="bsas-step-icon mr-1">○</span>' +
'<strong>2.</strong> Menunggu keputusan (long poll)' +
'<div class="bsas-step-remark text-muted" style="margin-left:1.25rem;"></div>' +
'</li>' +
'<li id="bsas-step-3" class="mb-2 small text-muted" data-state="pending">' +
'<span class="bsas-step-icon mr-1">○</span>' +
'<strong>3.</strong> Mengambil e-Cert bid, OTC dan STB &amp; simpan gadai' +
'<div class="bsas-step-remark text-muted" style="margin-left:1.25rem;"></div>' +
'</li>' +
'</ol>' +
'<p class="mb-0 small text-muted">Sila tunggu, proses ini boleh mengambil masa 10–30 saat.</p>' +
'</div>';
}
function setBsasStep(step, state, remark) {
var $el = $('#bsas-step-' + step);
if (!$el.length) {
return;
}
var icon = '○';
var color = 'text-muted';
if (state === 'active') {
icon = '●';
color = 'text-primary font-weight-bold';
} else if (state === 'done') {
icon = '✓';
color = 'text-success';
} else if (state === 'error') {
icon = '✕';
color = 'text-danger';
}
$el.attr('data-state', state)
.removeClass('text-muted text-primary text-success text-danger font-weight-bold')
.addClass(color);
$el.find('.bsas-step-icon').text(icon);
$el.find('.bsas-step-remark').text(remark || '');
}
function delayMs(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
async function submitGadaiWithBsasProgress(func) {
$("#terima_gadai").prop('disabled', true);
Swal.fire({
title: 'Pembelian Komoditi BSAS',
html: bsasProgressHtml(),
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
didOpen: function () {
Swal.showLoading();
}
});
try {
setBsasStep(1, 'active', 'Menyediakan & menghantar order...');
await delayMs(1200);
setBsasStep(1, 'done', 'Order dihantar.');
setBsasStep(2, 'active', 'Menunggu padanan BSAS...');
await delayMs(800);
var response = await $.ajax({
url: $(func).attr('action'),
method: 'POST',
data: $(func).serialize(),
dataType: 'json',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
timeout: 90000
});
setBsasStep(2, 'done', 'Keputusan diterima.');
setBsasStep(3, 'active', 'Mengambil e-Cert bid, OTC dan STB & menyimpan gadai...');
await delayMs(1000);
setBsasStep(3, 'done', 'Selesai.');
await delayMs(600);
window.location.href = (response && response.redirect)
? response.redirect
: "{{ route('customer_info') }}";
} catch (err) {
var msg = 'Gagal pembelian komoditi BSAS. Sila cuba lagi.';
if (err && err.responseJSON && err.responseJSON.message) {
msg = err.responseJSON.message;
} else if (err && err.statusText === 'timeout') {
msg = 'Proses BSAS tamat masa. Sila semak rekod BSAS / cuba semula.';
}
var activeStep = $('#bsas-progress-box li[data-state="active"]').attr('id');
var stepNum = activeStep ? parseInt(activeStep.replace('bsas-step-', ''), 10) : 2;
setBsasStep(stepNum, 'error', msg);
Swal.fire({
icon: 'error',
title: 'Tidak Berjaya',
text: msg
}).then(function () {
$("#terima_gadai").prop('disabled', false);
});
}
}
function selectedCommodityProvider() {
var checked = $('input[name="commodity_provider"]:checked').val();
if (checked) {
return checked;
}
var hidden = $('input[name="commodity_provider"][type="hidden"]').val();
return hidden || '';
}
function submitGadaiByProvider(func) {
var provider = selectedCommodityProvider();
if (!provider) {
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Pembekal Komoditi',
text: 'Sila pilih pembekal komoditi.'
});
return false;
}
if (provider === 'shoraka') {
$.ajax({
method: 'get',
url: api_url + '/api/shoraka/readiness',
success: function (data) {
if (!data || !data.ready) {
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
return false;
}
$(func).attr('onSubmit', 'return true;');
if (typeof window.shorakaTrack === 'function') {
window.shorakaTrack(api_url + '/api/shoraka/gadai/' + encodeURIComponent($('#kodcaw').val()) + '/' + encodeURIComponent($('#norujukan_gadai').val()));
}
$(func).submit();
},
error: function () {
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Shoraka Belum Tersedia',
text: 'Sila hubungi operasi untuk semakan sambungan Shoraka.'
});
}
});
return true;
}
submitGadaiWithBsasProgress(func);
return true;
}
async function verifyGadai(func){
$("#terima_gadai").prop('disabled', true);
if (typeof event !== 'undefined' && event && event.preventDefault) {
@@ -1488,7 +1792,7 @@
let bakiakhir = 0;
var kodcaw = $('#kodcaw').val();
if(pinjaman == 0){
$("#terima_gadai").prop('disabled', false);
syncTerimaButton();
Swal.fire({
icon: 'error',
title: 'Amaran!',
@@ -1497,6 +1801,16 @@
return false;
}
if (isOnlineCaraTerima() && !hasMatchingPrint()) {
syncTerimaButton();
Swal.fire({
icon: 'warning',
title: 'Borang belum dicetak',
text: 'Sila cetak borang pengesahan pelanggan sebelum tekan Terima.'
});
return false;
}
let floatOk = await checkDisbursementFloat(pinjaman);
if (!floatOk) {
$("#terima_gadai").prop('disabled', false);
@@ -1544,32 +1858,6 @@
event.preventDefault();
return false;
}else{
$.ajax({
method:'get',
url: api_url + '/api/komuditi/check',
success:function(data){
var check_komuditi = Object.keys(data).length;
if(check_komuditi == 0){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Unit Komuditi Berbaki Minumum',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
});
event.preventDefault();
return false;
}else{
if(Number(data['unit_komuditi']) < pinjaman){
$("#terima_gadai").prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Baki Unit Komuditi Tidak Mencukupi',
text: 'Sila Hubungi Operasi Untuk Membuat Pembelian Komuditi'
});
event.preventDefault();
return false;
}
else{
if(pinjaman >= 10000 && pinjaman <= Number(pembiyaan_max_pelulus)){
//new added
$.ajax({
@@ -1676,9 +1964,7 @@
title: 'Telah Disahkan',
text: `Komen : ${data.komen}`
}).then(function(){
$(func).attr("onSubmit","return true;");
$(func).submit();
submitGadaiByProvider(func);
return true;
});
@@ -1824,12 +2110,8 @@
icon: 'success',
title: 'Telah Disahkan'
}).then(function(){
$(func).attr("onSubmit","return true;");
$(func).submit();
submitGadaiByProvider(func);
return true;
});
}
else if(data.kelulusan == 'batal')
@@ -1890,14 +2172,7 @@
return false;
} else {
//insert code asal
Swal.fire({
icon: 'success',
title: 'Berjaya!',
text: 'Pembiayaan Berjaya Disimpan'
});
$(func).attr("onSubmit","return true;");
$(func).submit();
submitGadaiByProvider(func);
return true;
//end insert code asal
}
@@ -1909,10 +2184,6 @@
}
}
}
}
});
}
}
@@ -0,0 +1,168 @@
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<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 BSAS</li>
</ol>
</nav>
<div class="card-header text-center">{{ __('Rekod Komoditi BSAS') }}</div>
<div class="card-body text-center">
<div class="row justify-content-between mb-4">
<div class="col text-left">
<div class="form-group d-flex align-items-center">
<label for="limit_view_bsas" style="width: 600px" class="mb-0">Had Paparan Rekod BSAS</label>
<select onchange="loadBsasRecord()" name="limit_view_bsas" id="limit_view_bsas" class="form-control">
<option selected value="20">20</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
</div>
</div>
<div class="col text-right">
<button type="button" class="btn btn-primary" id="refresh_bsas" title="Muat Semula">
<i class="fas fa-sync-alt" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="spinner-border" role="status" id="loading-spinner-bsas" style="display:none;">
<span class="sr-only">Loading...</span>
</div>
<x-datatable
id="rekod_bsas"
filename="Rekod_Komoditi_BSAS"
class="text-center"
:columns="['Bil', 'Tarikh', 'UUID', 'No Rujukan', 'Cawangan', 'Pembeli', 'Bid Value', 'Murabaha', 'eCert No', 'Status', 'Oleh', 'Tindakan']"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
const formatter = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2
});
let api = "{{ api_url_for_browser() }}";
let kodcaw = "{{ Auth::user()->cawangan }}";
var tablebsas = initErahnDatatable('#rekod_bsas', {
pageLength: 1000,
bLengthChange: false,
deferRender: true,
order: [[1, 'desc']],
});
$('#refresh_bsas').on('click', function () {
loadBsasRecord();
});
$(function () {
loadBsasRecord();
});
function bsasStatusBadge(status) {
var label = status || '-';
var cls = 'secondary';
if (status === 'confirmed') cls = 'success';
else if (status === 'failed') cls = 'danger';
else if (status === 'submitted' || status === 'pending') cls = 'warning';
return '<span class="badge badge-' + cls + '">' + label + '</span>';
}
function formatBsasDateTime(value) {
if (!value) {
return '-';
}
var date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
var pad = function (n) { return String(n).padStart(2, '0'); };
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' +
pad(date.getMinutes()) + ':' +
pad(date.getSeconds());
}
function bsasEcertAction(row) {
var ecertNo = row['e_cert_no'] || '';
var hasEcert = !!(row['has_bid_ecert'] || row['has_otc_ecert'] || row['has_stb_ecert']);
var uuid = row['uuid'] || '';
if (!hasEcert || !uuid) {
return '<span class="text-muted">-</span>';
}
var url = "{{ url('/khazanah/komoditi/bsas/ecert') }}/" + encodeURIComponent(uuid);
return '<a class="btn btn-sm btn-outline-primary" href="' + url + '" target="_blank" title="Lihat eCert ' + ecertNo + '">' +
'<i class="fa fa-print" aria-hidden="true"></i> eCert' +
'</a>';
}
function loadBsasRecord() {
var selectedLimitView = $('#limit_view_bsas').find(':selected').val();
$('#loading-spinner-bsas').show();
tablebsas.clear().draw();
$.ajax({
method: 'get',
url: api + '/api/komuditi/bsas/orders',
data: {
'limit_view': selectedLimitView,
'kodcaw': kodcaw,
},
success: function (rekod) {
var bilcount = 0;
$.each(rekod, function (index, row) {
bilcount++;
var ecertNo = row['e_cert_no'] || '-';
if ((row['has_bid_ecert'] || row['has_otc_ecert'] || row['has_stb_ecert']) && row['uuid']) {
ecertNo = '<a href="{{ url('/khazanah/komoditi/bsas/ecert') }}/' + encodeURIComponent(row['uuid']) + '" target="_blank">' +
(row['e_cert_no'] || 'eCert') +
'</a>';
}
tablebsas.row.add([
bilcount,
formatBsasDateTime(row['created_at']),
row['uuid'] || '-',
row['norujukan'] || '-',
row['kodcaw'] || '-',
row['otc_counter_party'] || '-',
formatter.format(Number(row['bid_value'] || 0)),
formatter.format(Number(row['otc_murabaha_value'] || 0)),
ecertNo,
bsasStatusBadge(row['status']),
row['created_by'] || '-',
bsasEcertAction(row),
]).draw(false);
});
tablebsas.draw();
$('#loading-spinner-bsas').hide();
},
error: function () {
$('#loading-spinner-bsas').hide();
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: 'Gagal memuatkan rekod BSAS'
});
}
});
}
</script>
@endsection
@@ -219,77 +219,80 @@
<div class="card-body text-center">
<div class="row form-group justify-content-center">
<div class="col-3 mx-1">
<div class="row">
<a href="{{ url('dashboard') }}" disabled
class="btn btn-info btn-lg btn-block mb-3">Dashboard</a>
</div>
<div class="row">
<a href="{{ url('kakitangan') }}"
class="btn btn-info btn-lg btn-block mb-3">Kakitangan</a>
</div>
<div class="row">
<a href="{{ url('kelulusan') }}" id="btnkelulusan"
class="btn btn-info btn-lg btn-block mb-3">Kelulusan</a>
</div>
{{-- <div class="row">
<a href="{{ url('permohonan') }}" class="btn btn-danger btn-lg btn-block mb-3">Permohonan</a>
</div> --}}
<div class="row">
<a href="#" data-target="#denominasiShowModal" data-backdrop="static" data-keyboard="false"
data-toggle="modal" class="btn btn-info btn-lg btn-block mb-3">Denominasi</a>
<div class="col-4 px-2">
<a href="{{ url('dashboard') }}" disabled
class="btn btn-info btn-lg btn-block mb-3">Dashboard</a>
</div>
<div class="row">
<a href="{{ route('khazanah.harga_emas') }}"
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
</div>
</div>
<div class="col-3 mx-1">
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('khazanah.pelanggan') }}"
class="btn btn-info btn-lg btn-block mb-3">Pelanggan</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('lelong') }}" class="btn btn-info btn-lg btn-block mb-3">Lelong</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('kakitangan') }}"
class="btn btn-info btn-lg btn-block mb-3">Kakitangan</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('laporan') }}" class="btn btn-info btn-lg btn-block mb-3">Laporan</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ url('panjar') }}" class="btn btn-info btn-lg btn-block mb-3">Perbelanjaan</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('kelulusan') }}" id="btnkelulusan"
class="btn btn-info btn-lg btn-block mb-3">Kelulusan</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('menu') }}"
id="btn-khazanah"
class="btn {{ $countkhazanah > 0 ? 'btn-danger' : 'btn-info' }} btn-lg btn-block mb-3">Khazanah</a>
</div>
<div class="row">
<div class="col-4 px-2">
<a href="{{ url('alirantunai') }}" class="btn btn-info btn-lg btn-block mb-3">Aliran
Tunai</a>
</div>
<div class="col-4 px-2">
<a href="#" data-target="#denominasiShowModal" data-backdrop="static" data-keyboard="false"
data-toggle="modal" class="btn btn-info btn-lg btn-block mb-3">Denominasi</a>
</div>
<div class="col-4 px-2">
<a href="{{ url('pendahuluanserahan') }}"
class="btn btn-info btn-lg btn-block mb-3">Pendahuluan - Serahan</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('khazanah.bajet') }}" class="btn btn-info btn-lg btn-block mb-3">Bajet</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('khazanah.harga_emas') }}"
class="btn btn-info btn-lg btn-block mb-3">Harga Emas</a>
</div>
@if(config('commodity.shoraka') && 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="row">
<div class="col-4 px-2">
<a href="{{ route('pembiayaan.online.pc') }}"
class="btn btn-info btn-lg btn-block mb-3">Pembiayaan Online</a>
</div>
<div class="col-4 px-2">
<a href="{{ route('pembiayaan.online.pc.topup') }}"
class="btn btn-info btn-lg btn-block mb-3">Had / Modal Online</a>
</div>
@endif
<div class="row">
<div class="col-4 px-2">
<a href="{{ route('khazanah.indexwakil') }}"
class="btn btn-info btn-lg btn-block mb-3">Surat Wakil</a>
</div>
</div>
<div class="col-3 mx-1">
{{-- <div class="row">
<a href="#" class="btn btn-danger btn-lg btn-block mb-3">Capaian</a>
</div> --}}
<div class="row">
<a href="{{ route('lelong') }}" class="btn btn-info btn-lg btn-block mb-3">Lelong</a>
</div>
<div class="row">
<a href="{{ url('panjar') }}" class="btn btn-info btn-lg btn-block mb-3">Perbelanjaan</a>
</div>
<div class="row">
<a href="{{ url('alirantunai') }}" class="btn btn-info btn-lg btn-block mb-3">Aliran
Tunai</a>
</div>
<div class="row">
<a href="{{ route('khazanah.bajet') }}" class="btn btn-info btn-lg btn-block mb-3">Bajet</a>
@if(config('commodity.bsas'))
<div class="col-4 px-2">
<a href="{{ route('khazanah.bsas') }}"
class="btn btn-info btn-lg btn-block mb-3">Komoditi BSAS</a>
</div>
@endif
</div>
</div>
@@ -16,6 +16,9 @@
<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>
@if(config('commodity.shoraka'))
<a href="{{ route('operasi.shoraka.index') }}" class="btn btn-info btn-lg d-inline-block m-1">Komoditi Shoraka</a>
@endif
<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(config('commodity.shoraka') && (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 -1
View File
@@ -414,7 +414,7 @@
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var ppc = "{{ Auth::user()->name }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
@@ -52,6 +52,7 @@
<th>No K/P</th>
<th>Tarikh Pembiayaan</th>
<th>No Rujukan</th>
<th>Cara Terima</th>
<th>Pembiayaan (RM)</th>
<th>Keuntungan Lain-lain Caj (RM) </th>
<th>Keuntungan 1% (RM)</th>
@@ -61,7 +62,7 @@
</tr>
</thead>
<tfoot class="thead-dark">
<th colspan="5">TOTAL</th>
<th colspan="6">TOTAL</th>
<th id="pinjamantotal"></th>
<th id="ujrahtotal"></th>
<th id="onepercenttotal"></th>
@@ -300,6 +301,10 @@
total_upah12 += upah12;
var caraTerima = String(gadai['cara_terima'] || 'TUNAI').toUpperCase() === 'ONLINE'
? 'Online'
: 'Tunai';
// Pastikan semua nilai ada sebelum add row
tablegadai.row.add([
counter,
@@ -307,6 +312,7 @@
gadai['nokp'] || '-',
gadai['t_gadai'] || '-',
gadai['norujukan'] || '-',
caraTerima,
formatter.format(pinjaman),
formatter.format(ujrah * 12),
formatter.format(onepercent * 12),
+1 -1
View File
@@ -103,7 +103,7 @@
<script>
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
var api_url = "{{ api_url_for_browser() }}";
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
+1 -1
View File
@@ -414,7 +414,7 @@
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var ppc = "{{ Auth::user()->name }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
+116 -86
View File
@@ -25,6 +25,18 @@
</ol>
</nav>
@if(!empty($customer_info['photo_base64']))
<div class="form-group row col-md-12">
<div class="col-md-12 d-flex justify-content-center pb-3">
<img src="data:{{ $customer_info['photo_mime'] ?? 'image/jpeg' }};base64,{{ $customer_info['photo_base64'] }}"
alt="Gambar MyKad"
width="120"
height="160"
style="object-fit: cover;">
</div>
</div>
@endif
<div class="form-group row col-md-12">
<div class="col-md-6">
<div class="form-group row">
@@ -154,7 +166,7 @@
<nav class="mb-3">
<div class="nav nav-tabs" id="nav-tab" role="tablist">
@php
$activate_tab = request()->session()->pull('activate_tab', 'SAG_lama');
$activate_tab = request()->session()->pull('activate_tab', 'SAG_baru');
$activate_tab_SAG_baru = $activate_tab === 'SAG_baru' ? 'active' : '';
$aria_selected_SAG_baru = $activate_tab === 'SAG_baru' ? 'true' : 'false';
@@ -171,77 +183,36 @@
</nav>
<div class="tab-content" id="navtawarruqdannottawarruq">
<div class="tab-pane fade {{ $activate_pane_SAG_baru }}" id="nav-tawarruq" role="tabpanel" aria-labelledby="nav-tawarruq-tab">
<table class="table table-striped table-hover" id="myTable" style="width: 100%">
<thead class="thead-dark">
<tr>
<th>Bil</th>
<th>No Rujukan</th>
<th>Tarikh Pembiayaan</th>
<th>Tarikh Matang</th>
<th>Baki Harga Jualan</th>
<th>Baki Pembiayaan</th>
<th>Margin</th>
<th>Tempoh</th>
<th>Keuntungan</th>
<th>Lelong</th>
<th>Pembayaran Terakhir</th>
</tr>
</thead>
</table>
<x-datatable
id="myTable"
filename="SAG_Baru"
:columns="['Bil', 'No Rujukan', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Pembayaran Terakhir', 'Tarikh Ansuran Terkini']"
/>
</div>
<div class="tab-pane fade {{ $activate_pane_SAG_lama }}" id="nav-xtawarruq" role="tabpanel" aria-labelledby="nav-xtawarruq-tab">
<table id="xtawarruq" class="table table-striped table-hover" style="width:100%">
<thead class="thead-dark">
<tr>
<th>Bil</th>
<th>No Rujukan</th>
<th>Tarikh Pembiayaan</th>
<th>Tarikh Matang</th>
<th>Baki Harga Jualan</th>
<th>Baki Pembiayaan</th>
<th>Margin</th>
<th>Tempoh</th>
<th>Keuntungan</th>
<th>Lelong</th>
<th>Pembayaran Terakhir</th>
</tr>
</thead>
</table>
<x-datatable
id="xtawarruq"
filename="SAG_Lama"
:columns="['Bil', 'No Rujukan', 'Tarikh Pembiayaan', 'Tarikh Matang', 'Baki Harga Jualan', 'Baki Pembiayaan', 'Margin', 'Tempoh', 'Keuntungan', 'Lelong', 'Pembayaran Terakhir', 'Tarikh Ansuran Terkini']"
/>
</div>
</div>
{{-- Sudah Tebus --}}
<table id="dahtebusTable" class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Bil</th>
<th>No Rujukan</th>
<th>Tarikh Penyelesaian Pembiayaan</th>
<th>Baki Pembiayaan</th>
<th>Margin</th>
<th>Tempoh</th>
</tr>
</thead>
</table>
<x-datatable
id="dahtebusTable"
filename="SAG_Sudah_Tebus"
:columns="['Bil', 'No Rujukan', 'Tarikh Penyelesaian Pembiayaan', 'Baki Pembiayaan', 'Margin', 'Tempoh']"
/>
{{-- Lelong --}}
<table id="lelongTable" class="table table-striped table-hover" style="width:100%">
<thead class="thead-dark">
<tr>
<th>Bil</th>
<th>No Rujukan</th>
<th>Tarikh Lelong</th>
<th>Tarikh Tuntut</th>
<th>Pembiayaan</th>
<th>Harga Jual</th>
<th>Jum Tuntut/Bayar</th>
<th>Keuntungan</th>
<th>Caj Lelong</th>
</tr>
</thead>
</table>
<x-datatable
id="lelongTable"
filename="SAG_Lelong"
:columns="['Bil', 'No Rujukan', 'Tarikh Lelong', 'Tarikh Tuntut', 'Pembiayaan', 'Harga Jual', 'Jum Tuntut/Bayar', 'Keuntungan', 'Caj Lelong']"
/>
<div class="text-center">
<button class="btn btn-primary" onclick="histfunc()">Rekod Pembiayaan</button>
@@ -470,7 +441,7 @@
var kodcaw = "{{ $cawangan_info['kodcaw'] }}";
var ppc = "{{ Auth::user()->name }}";
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var api_arrahnbid = "<?php echo config('app.api_arrahnbid') ?>";
var api_arrahnbid = @json($cawangan_info['api_arrahnbid'] ?? config('app.api_arrahnbid'));
@@ -502,21 +473,15 @@
var tablegadai = $('#myTable').DataTable({
"scrollX": true,
order: [[1,'desc']]
var tablegadai = initErahnDatatable('#myTable', {
order: [[1, 'desc']]
});
var xtawarruq = $('#xtawarruq').DataTable({
"scrollX": true,
});
var xtawarruq = initErahnDatatable('#xtawarruq');
var tabledahtebus = $('#dahtebusTable').DataTable({
});
var tabledahtebus = initErahnDatatable('#dahtebusTable');
var tablelelong = $('#lelongTable').DataTable({
"scrollX": true
});
var tablelelong = initErahnDatatable('#lelongTable');
var historybayarandatatable = $('#tablehistorybayaran').DataTable({
"ordering": false
@@ -634,6 +599,45 @@
};
}
function tarikhAnsuranTerkini(transactions)
{
if (!Array.isArray(transactions)) {
throw new Error('Invalid transaction response');
}
return transactions.reduce(function(latest, transaction) {
if (transaction.trans_type !== 'A') return latest;
var date = String(transaction.created_at || '').slice(0, 10);
if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) return latest;
return date > latest ? date : latest;
}, '') || 'Tiada bayaran';
}
function muatTarikhAnsuranTerkini(row, norujukan)
{
var originalData = row.data();
function update(value) {
// Ignore responses for rows removed when the customer/table changes.
if (row.data() !== originalData) return;
var updatedData = originalData.slice();
updatedData[11] = value;
row.data(updatedData).draw(false);
}
$.ajax({
method: 'get',
url: api_url + '/api/transaction/' + encodeURIComponent(norujukan),
success: function(transactions) {
try {
update(tarikhAnsuranTerkini(transactions));
} catch (error) {
update('Gagal dimuatkan');
}
},
error: function() {
update('Gagal dimuatkan');
}
});
}
function gadaiTable(data,jenistebus){
var counter = 1;
@@ -665,7 +669,15 @@
var taglel = '';
if(gadai_detail['taglel'] == 1){
if(tagarrahnbid == 1){
taglel = gadai_detail['t_lelong'];
$.ajax({
method: "GET",
async: false,
url: api_arrahnbid + 'portalApi',
data: { norujukan: gadai_detail['norujukan'] },
success: function(data) {
taglel = (data && data.lelong && data.lelong.t_lelong) || '';
}
});
} else {
$.ajax({
method:"GET",
@@ -685,8 +697,8 @@
let table_norujukan = gadai_detail["norujukan"];
// let radio_action = "onclick=checkCanChangeGadaianDetail(" + "'" + table_norujukan + "'" + ")";
if(gadai_detail["percentpinj"] != 0)
xtawarruq.row.add([
if(gadai_detail["percentpinj"] != 0) {
var ansuranRow = xtawarruq.row.add([
'<input type="radio" value='+ gadai_detail["norujukan"] + ' name="belum"/>',
// '<a href="#" onclick="checkserah(this)" data-pinjaman='+ gadai_detail['pinjaman'] +' data-hargajualan='+ gadai_detail['hargajualan'] +' data-norujukan='+ gadai_detail['norujukan'] +' data-url=' + urlroute +'>' + gadai_detail['norujukan'] + '</a>',
(function() {
@@ -716,15 +728,24 @@
let color = bayaranInfo.blocked ? 'red' : 'green';
return '<div style="white-space: nowrap; color: ' + color + '; font-weight: bold;">' +
bayaranInfo.tarikh + '</div>';
})()
})(),
'Memuatkan...'
]).draw();
muatTarikhAnsuranTerkini(ansuranRow, gadai_detail['norujukan']);
}
}else{
var taglel = '';
if(gadai_detail['taglel'] == 1){
if(tagarrahnbid == 1){
if(gadai_detail['t_lelong'] != null) {
taglel = gadai_detail['t_lelong'];
}
$.ajax({
method: "GET",
async: false,
url: api_arrahnbid + 'portalApi',
data: { norujukan: gadai_detail['norujukan'] },
success: function(data) {
taglel = (data && data.lelong && data.lelong.t_lelong) || '';
}
});
} else {
$.ajax({
method:"GET",
@@ -743,7 +764,7 @@
let table_norujukan = gadai_detail["norujukan"];
// let radio_action = "onclick=checkCanChangeGadaianDetail(" + "'" + table_norujukan + "'" + ")";
tablegadai.row.add([
var ansuranRow = tablegadai.row.add([
'<input type="radio" value='+ gadai_detail["norujukan"] + ' name="belum"/>',
// '<a href="#" onclick="checkserah(this)" data-pinjaman='+ gadai_detail['pinjaman'] +' data-hargajualan='+ gadai_detail['hargajualan'] +' data-norujukan='+ gadai_detail['norujukan'] +' data-url=' + urlroute +'>' + gadai_detail['norujukan'] + '</a>',
(function() {
@@ -773,8 +794,10 @@
let color = bayaranInfo.blocked ? 'red' : 'green';
return '<div style="white-space: nowrap; color: ' + color + '; font-weight: bold;">' +
bayaranInfo.tarikh + '</div>';
})()
})(),
'Memuatkan...'
]).draw();
muatTarikhAnsuranTerkini(ansuranRow, gadai_detail['norujukan']);
}
}else if(jenistebus == "sudah_tebus"){
@@ -900,6 +923,9 @@
success: function(data){
$('#myTable').parents('div.dataTables_wrapper').first().show();
gadaiTable(data,"belum_tebus");
if (typeof adjustErahnDatatables === 'function') {
adjustErahnDatatables();
}
}
});
@@ -1433,6 +1459,9 @@
url: api_url + "/api/customers/"+noic+"/gadai/belum_tebus/"+kodcaw,
success: function(data){
gadaiTable(data,"belum_tebus");
if (typeof adjustErahnDatatables === 'function') {
adjustErahnDatatables();
}
}
});
}
@@ -1681,9 +1710,10 @@
});
document.addEventListener('DOMContentLoaded', function(){
$('a[data-toggle="tab"]').on('shown.bs.tab', function(e){
$($.fn.dataTable.tables(true)).DataTable()
.columns.adjust();
$('a[data-toggle="tab"]').on('shown.bs.tab', function(){
if (typeof adjustErahnDatatables === 'function') {
adjustErahnDatatables();
}
});
});
</script>
+10 -75
View File
@@ -214,14 +214,16 @@
v-pre
style="color: {{ isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan 2' ? 'black' : '#1abc9c' }};">
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
<div id="divbiasa" class="d-none">
<a class="dropdown-item" id="linkz" href="#">
Tiada Data
</a>
</div>
@include('layouts.partials.notification_inbox')
</div>
</li>
@@ -281,7 +283,7 @@
});
</script>
@elseif($auth_user['role_harian'] == 'teller')
@elseif($auth_user['role_harian'] == 'teller' || $auth_user['roles'] == 'teller')
<li class="nav-item dropdown" style="margin-right:10px;">
<a id="navbarDropdown"
class="nav-link"
@@ -293,60 +295,13 @@
v-pre
style="color: {{ isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan' ? 'white' : (isset($cawangan_info['details_caw']) && $cawangan_info['details_caw'] == 'Putatan 2' ? 'black' : '#1abc9c') }};">
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<a class="dropdown-item" id="gadailinkz" href="#">
Tiada Data
</a>
</div>
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
@include('layouts.partials.notification_inbox')
</div>
</li>
{{-- <script>
// Enable pusher logging - don't include this in production
// Pusher.logToConsole = true;
var pusher = new Pusher("{{ env('PUSHER_APP_KEY') }}", {
cluster: 'ap1',
encrypted: true
});
var channel = pusher.subscribe('notifikasi-pembayaran');
channel.bind('trigger-pembayaran', function(data) {
let message = JSON.stringify(data);
$('#bell').css("color", "red");
$('#gadailinkz').text("Khazanah Serah Marhun Teller");
// $("#gadailinkz").attr("href", "{{ url('kelulusan') }}");
const Toast = Swal.mixin({
toast: true,
position: 'top-end',
showConfirmButton: false,
timer: 5000,
timerProgressBar: true,
didOpen: (toast) => {
toast.addEventListener('mouseenter', Swal.stopTimer)
toast.addEventListener('mouseleave', Swal.resumeTimer)
}
});
Toast.fire({
icon: 'error',
title: 'Notifikasi Pembayaran!'
});
});
$("#navbarDropdown").click(function(){
$('#bell').css("color", "grey");
});
</script> --}}
@endif
@@ -446,31 +401,11 @@
</div>
@else
@endif
@include('layouts.partials.batch_cutoff_banner')
@endif
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@include('shoraka.navigation')
@yield('content')
</main>
</div>
@@ -164,29 +164,6 @@
</div>
</nav>
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@yield('content')
</main>
+5 -25
View File
@@ -111,14 +111,17 @@
<li class="nav-item dropdown" style="margin-right:10px;">
<a id="navbarDropdown" class="nav-link " href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
<i id="bell" class="fa fa-bell"></i>
<span id="staff-notification-badge" class="staff-notification-badge"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<div id="divbiasa">
<div class="dropdown-menu dropdown-menu-right staff-notification-menu" aria-labelledby="navbarDropdown">
<div id="divbiasa" class="d-none">
<a class="dropdown-item" id="gadailinkz" href="#">
Tiada Notifikasi
</a>
<a class="dropdown-item" id="linkz" href="#"></a>
</div>
@include('layouts.partials.notification_inbox')
</div>
</li>
@@ -254,29 +257,6 @@
</div>
</nav>
@if(session()->has('iklan') && session()->get('iklan') == 0)
<div class="toast" role="alert" aria-live="assertive" aria-atomic="true" style="position: absolute; top: 1rem; right: 1rem; width:100%;z-index:1;" data-autohide="false">
<div class="toast-header">
<i class="text-danger fa-2x fa fa-exclamation-triangle mr-2"></i>
<strong class="mr-auto text-danger fa-2x">Amaran</strong>
{{-- <small>11 mins ago</small> --}}
<button type="button" class="ml-2 mb-1 close" data-dismiss="toast" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="toast-body">
<b style="font-size:18px;">sila tukar kata laluan asal dengan segera!</b>
</div>
</div>
<script>
$('.toast').toast('show');
</script>
@endif
<main class="py-4">
@yield('content')
</main>
@@ -0,0 +1,355 @@
<style>
#batch-cutoff-banner {
position: fixed;
top: 112px;
right: 24px;
z-index: 1080;
width: min(380px, calc(100vw - 32px));
margin: 0;
padding: 0;
color: #fff;
background: #b91c1c;
border-radius: 16px;
box-shadow: 0 18px 40px rgba(127, 29, 29, 0.35);
}
#batch-cutoff-banner.batch-cutoff-warn {
background: #db2777;
box-shadow: 0 18px 40px rgba(190, 24, 93, 0.32);
}
#batch-cutoff-banner.batch-cutoff-warn .batch-cutoff-link {
color: #9d174d !important;
}
#batch-cutoff-banner.batch-cutoff-urgent {
background: #991b1b;
}
#batch-cutoff-banner .batch-cutoff-inner {
display: flex;
flex-direction: column;
gap: 12px;
padding: 14px 16px 16px;
}
#batch-cutoff-banner .batch-cutoff-copy {
display: flex;
align-items: flex-start;
gap: 12px;
min-width: 0;
}
#batch-cutoff-banner .batch-cutoff-icon {
flex-shrink: 0;
width: 36px;
height: 36px;
border-radius: 999px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255, 255, 255, 0.14);
font-size: 15px;
}
#batch-cutoff-banner .batch-cutoff-title {
display: block;
font-size: 14px;
font-weight: 600;
letter-spacing: -0.01em;
line-height: 1.3;
}
#batch-cutoff-banner .batch-cutoff-sub {
display: block;
margin-top: 3px;
font-size: 12px;
line-height: 1.4;
color: rgba(255, 255, 255, 0.82);
}
#batch-cutoff-banner .batch-cutoff-actions {
display: flex;
align-items: center;
gap: 8px;
padding-left: 48px;
}
#batch-cutoff-banner .batch-cutoff-chip {
display: inline-flex;
align-items: center;
min-width: 64px;
justify-content: center;
padding: 6px 10px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.22);
font-size: 13px;
font-weight: 700;
font-variant-numeric: tabular-nums;
letter-spacing: 0.04em;
}
#batch-cutoff-banner .batch-cutoff-link,
#batch-cutoff-banner .batch-cutoff-notify {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 7px 14px;
border: 0;
border-radius: 999px;
font-size: 13px;
font-weight: 600;
line-height: 1.2;
text-decoration: none !important;
white-space: nowrap;
cursor: pointer;
}
#batch-cutoff-banner .batch-cutoff-link {
flex: 1;
color: #991b1b !important;
background: #fff;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
#batch-cutoff-banner .batch-cutoff-link:hover {
color: #7f1d1d !important;
background: #f8fafc;
}
#batch-cutoff-banner .batch-cutoff-notify {
color: #fff !important;
background: transparent;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.55);
}
#batch-cutoff-banner .batch-cutoff-notify:hover {
background: rgba(255, 255, 255, 0.12);
}
@media (max-width: 767px) {
#batch-cutoff-banner {
top: auto;
bottom: 16px;
right: 16px;
left: 16px;
width: auto;
}
#batch-cutoff-banner .batch-cutoff-actions {
padding-left: 0;
}
}
</style>
<div id="batch-cutoff-banner" class="d-none" role="alert">
<div class="batch-cutoff-inner">
<div class="batch-cutoff-copy">
<span class="batch-cutoff-icon" aria-hidden="true"><i class="fas fa-clock"></i></span>
<div>
<span id="batch-cutoff-title" class="batch-cutoff-title">Hantar senarai pembiayaan online</span>
<span id="batch-cutoff-body" class="batch-cutoff-sub"></span>
</div>
</div>
<div class="batch-cutoff-actions">
<span id="batch-cutoff-countdown" class="batch-cutoff-chip">00:00</span>
<a id="batch-cutoff-link" href="{{ route('pembiayaan.online.pc') }}" class="batch-cutoff-link">Buka senarai</a>
<button type="button" id="batch-cutoff-enable-notify" class="batch-cutoff-notify d-none">Benarkan notifikasi</button>
</div>
</div>
</div>
<script>
(function () {
var STATUS_URL = @json(route('pembiayaan.online.pc.reminder'));
var POLL_MS = 60000;
var TICK_MS = 1000;
var state = null;
var fetchedAt = 0;
var serverNowAtFetch = 0;
var banner = document.getElementById('batch-cutoff-banner');
var titleEl = document.getElementById('batch-cutoff-title');
var bodyEl = document.getElementById('batch-cutoff-body');
var linkEl = document.getElementById('batch-cutoff-link');
var enableBtn = document.getElementById('batch-cutoff-enable-notify');
var countdownEl = document.getElementById('batch-cutoff-countdown');
if (!banner) {
return;
}
function todayKey() {
var d = new Date();
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate());
}
function pad(n) {
return n < 10 ? '0' + n : String(n);
}
function storageKey(kind, level) {
var slot = (state && state.slot) ? state.slot : 'BATCH';
return 'batchCutoff:' + kind + ':' + slot + ':' + todayKey() + ':' + level;
}
function was(kind, level) {
try {
return sessionStorage.getItem(storageKey(kind, level)) === '1';
} catch (e) {
return false;
}
}
function mark(kind, level) {
try {
sessionStorage.setItem(storageKey(kind, level), '1');
} catch (e) {}
}
function remainingMs() {
if (!state) {
return null;
}
var cutoffAt = state.cutoff_at ? Date.parse(state.cutoff_at) : NaN;
if (!isFinite(cutoffAt)) {
var parts = String(state.cutoff || '').split(':');
if (parts.length < 2) {
return null;
}
var now = new Date();
cutoffAt = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0).getTime();
}
var elapsed = Date.now() - fetchedAt;
var nowMs = (serverNowAtFetch || Date.now()) + elapsed;
return cutoffAt - nowMs;
}
function canNotify() {
return typeof Notification !== 'undefined' && Notification.permission === 'granted';
}
function notifyDefault() {
return typeof Notification !== 'undefined' && Notification.permission === 'default';
}
function sendBrowserNotification(level, title, body) {
if (!canNotify() || was('notify', level)) {
return;
}
mark('notify', level);
try {
var n = new Notification(title, {
body: body,
tag: 'batch-cutoff-' + ((state && state.slot) || '') + '-' + level,
requireInteraction: level === 1
});
n.onclick = function () {
window.focus();
if (state && state.url) {
window.location.href = state.url;
}
n.close();
};
} catch (e) {}
}
function hideBanner() {
banner.classList.add('d-none');
banner.classList.remove('batch-cutoff-urgent', 'batch-cutoff-warn');
}
function showBanner(level, remaining) {
var mins = Math.max(0, Math.floor(remaining / 60000));
var secs = Math.max(0, Math.floor((remaining % 60000) / 1000));
var slot = (state && state.slot) ? state.slot : 'Batch';
var cutoff = (state && state.cutoff) ? state.cutoff : '';
var count = (state && state.bilangan) ? state.bilangan : 0;
var countdown = pad(mins) + ':' + pad(secs);
if (countdownEl) {
countdownEl.textContent = countdown;
}
if (level === 1) {
titleEl.textContent = 'Sila hantar senarai pembiayaan online sekarang!! 😡 😡';
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod belum dihantar';
banner.classList.add('batch-cutoff-urgent');
banner.classList.remove('batch-cutoff-warn');
} else {
titleEl.textContent = 'Sila hantar senarai pembiayaan online 😇 😇';
bodyEl.textContent = slot + ' ditutup pada ' + cutoff + ' · ' + count + ' rekod menunggu';
banner.classList.add('batch-cutoff-warn');
banner.classList.remove('batch-cutoff-urgent');
}
if (state && state.url) {
linkEl.href = state.url;
}
if (notifyDefault()) {
enableBtn.classList.remove('d-none');
} else {
enableBtn.classList.add('d-none');
}
banner.classList.remove('d-none');
}
function tick() {
if (!state || !state.enabled || state.ditutup || state.lewat || !state.bilangan) {
hideBanner();
return;
}
var remaining = remainingMs();
if (remaining === null || remaining <= 0) {
hideBanner();
return;
}
var remindMs = (state.remind_minutes || 10) * 60000;
var lastMs = (state.last_minute || 1) * 60000;
if (remaining > remindMs) {
hideBanner();
return;
}
var level = remaining <= lastMs ? 1 : 10;
showBanner(level, remaining);
sendBrowserNotification(
level,
level === 1 ? '1 minit lagi: hantar senarai batch' : 'Hantar senarai pembiayaan online',
(state.slot || 'Batch') + ' tamat ' + (state.cutoff || '') + '. ' + state.bilangan + ' rekod belum dihantar.'
);
}
function applyStatus(data) {
if (!data || !data.enabled) {
state = null;
hideBanner();
return;
}
state = data;
fetchedAt = Date.now();
serverNowAtFetch = data.server_now ? Date.parse(data.server_now) : Date.now();
if (!isFinite(serverNowAtFetch)) {
serverNowAtFetch = Date.now();
}
tick();
}
function fetchStatus() {
fetch(STATUS_URL, {
credentials: 'same-origin',
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }
}).then(function (res) {
if (!res.ok) {
throw new Error('reminder status ' + res.status);
}
return res.json();
}).then(applyStatus).catch(function () {});
}
enableBtn.addEventListener('click', function () {
if (typeof Notification === 'undefined') {
return;
}
Notification.requestPermission().then(function () {
if (canNotify()) {
enableBtn.classList.add('d-none');
var remaining = remainingMs();
var lastMs = ((state && state.last_minute) || 1) * 60000;
var level = remaining !== null && remaining <= lastMs ? 1 : 10;
try { sessionStorage.removeItem(storageKey('notify', level)); } catch (e) {}
tick();
} else {
enableBtn.classList.add('d-none');
}
});
});
fetchStatus();
setInterval(fetchStatus, POLL_MS);
setInterval(tick, TICK_MS);
})();
</script>
@@ -0,0 +1,244 @@
<style>
.staff-notification-menu {
min-width: 340px;
max-width: 380px;
padding: 0;
}
.staff-notification-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid #eee;
font-weight: 600;
font-size: 13px;
}
.staff-notification-header button {
border: 0;
background: none;
color: #1abc9c;
font-size: 12px;
padding: 0;
cursor: pointer;
}
.staff-notification-header button:disabled {
color: #bbb;
cursor: default;
}
.staff-notification-list {
max-height: 360px;
overflow-y: auto;
}
.staff-notification-item {
display: block;
padding: 10px 14px;
border-bottom: 1px solid #f3f3f3;
white-space: normal;
color: #333;
text-decoration: none;
}
.staff-notification-item:hover {
background: #f8f9fa;
text-decoration: none;
color: #333;
}
.staff-notification-item.is-unread {
background: #eafaf6;
}
.staff-notification-item .sn-title {
font-size: 13px;
font-weight: 600;
margin-bottom: 2px;
}
.staff-notification-item.is-unread .sn-title {
font-weight: 700;
}
.staff-notification-item .sn-body,
.staff-notification-item .sn-time {
font-size: 12px;
color: #666;
margin: 0;
}
.staff-notification-empty {
padding: 18px 14px;
text-align: center;
color: #888;
font-size: 13px;
}
.staff-notification-badge {
position: absolute;
top: 2px;
right: 0;
font-size: 10px;
min-width: 16px;
padding: 2px 5px;
border-radius: 10px;
background: #e74c3c;
color: #fff;
line-height: 1.2;
display: none;
}
.staff-notification-toggle {
position: relative;
display: inline-block;
}
a.nav-link #staff-notification-badge {
position: absolute;
top: 2px;
right: -4px;
}
li.nav-item.dropdown > a.nav-link {
position: relative;
}
</style>
<div id="staff-notification-inbox">
<div class="staff-notification-header">
<span>Notifikasi</span>
<button type="button" id="staff-notification-mark-all" disabled>Tanda semua dibaca</button>
</div>
<div id="staff-notification-list" class="staff-notification-list">
<div class="staff-notification-empty">Memuatkan...</div>
</div>
</div>
<script>
(function () {
var listUrl = @json(route('notifications.index'));
var markAllUrl = @json(route('notifications.read.all'));
var markOneUrlTemplate = @json(url('/notifications/__ID__/read'));
var csrf = document.querySelector('meta[name="csrf-token"]');
var csrfToken = csrf ? csrf.getAttribute('content') : '';
function markOneUrl(id) {
return markOneUrlTemplate.replace('__ID__', id);
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function setBadge(count) {
var badge = document.getElementById('staff-notification-badge');
var bell = document.getElementById('bell');
if (!badge) {
return;
}
if (count > 0) {
badge.style.display = 'inline-block';
badge.textContent = count > 99 ? '99+' : String(count);
if (bell) {
bell.style.color = '#e74c3c';
}
} else {
badge.style.display = 'none';
badge.textContent = '';
if (bell) {
bell.style.color = '';
}
}
var markAll = document.getElementById('staff-notification-mark-all');
if (markAll) {
markAll.disabled = count < 1;
}
}
function render(payload) {
var wrap = document.getElementById('staff-notification-list');
if (!wrap) {
return;
}
var items = payload.notifications || [];
setBadge(payload.unread_count || 0);
if (!items.length) {
wrap.innerHTML = '<div class="staff-notification-empty">Tiada notifikasi</div>';
return;
}
wrap.innerHTML = items.map(function (item) {
var unread = !item.read_at;
return (
'<a class="dropdown-item staff-notification-item' + (unread ? ' is-unread' : '') + '" href="' + escapeHtml(item.url || '#') + '" data-id="' + item.id + '">' +
'<div class="sn-title">' + escapeHtml(item.title) + '</div>' +
(item.body ? '<p class="sn-body">' + escapeHtml(item.body) + '</p>' : '') +
'<p class="sn-time">' + escapeHtml(item.created_at_human || '') + '</p>' +
'</a>'
);
}).join('');
}
function loadInbox() {
return fetch(listUrl, {
headers: { 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' },
credentials: 'same-origin'
}).then(function (res) {
if (!res.ok) {
throw new Error('Gagal muat notifikasi');
}
return res.json();
}).then(render).catch(function () {
var wrap = document.getElementById('staff-notification-list');
if (wrap) {
wrap.innerHTML = '<div class="staff-notification-empty">Tidak dapat memuatkan notifikasi</div>';
}
});
}
function postJson(url) {
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': csrfToken
},
credentials: 'same-origin',
body: '{}'
}).then(function (res) {
if (!res.ok) {
throw new Error('Gagal kemaskini notifikasi');
}
return res.json();
});
}
document.addEventListener('click', function (event) {
var target = event.target;
if (target && target.nodeType !== 1) {
target = target.parentElement;
}
if (!target || !target.closest) {
return;
}
var item = target.closest('.staff-notification-item');
if (!item) {
return;
}
var id = item.getAttribute('data-id');
if (!id) {
return;
}
postJson(markOneUrl(id)).catch(function () {});
});
var markAll = document.getElementById('staff-notification-mark-all');
if (markAll) {
markAll.addEventListener('click', function (event) {
event.preventDefault();
event.stopPropagation();
postJson(markAllUrl).then(function () {
loadInbox();
}).catch(function () {});
});
}
loadInbox();
setInterval(loadInbox, 30000);
})();
</script>
+200 -38
View File
@@ -73,43 +73,81 @@
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="card-header">{{ __('Rekod Pembelian/Penjualan Komoditi') }}</div>
<div class="card-header">{{ __('Rekod Komoditi') }}</div>
<div class="card-body text-center">
<div class="row justify-content-between mb-4">
<div class="col text-left">
<div class="form-group d-flex align-items-center">
<label for="limit-komoditi" style="width: 600px" class="mb-0">Had Paparan Rekod Komoditi</label>
<select onchange="loadKomoditiRecord()" name="limit_view" id="limit_view" class="form-control">
<option selected value="20">20</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option disabled>Untuk rekod keseluruhan, sila print</option>
</select>
</div>
<ul class="nav nav-tabs mb-3" id="komoditi-rekod-tab" role="tablist">
<li class="nav-item">
<a class="nav-link active" id="tab-manual-link" data-toggle="tab" href="#tab-manual" role="tab" aria-controls="tab-manual" aria-selected="true">Beli / Jual Manual</a>
</li>
<li class="nav-item">
<a class="nav-link" id="tab-bsas-link" data-toggle="tab" href="#tab-bsas" role="tab" aria-controls="tab-bsas" aria-selected="false">Pembelian BSAS</a>
</li>
</ul>
<div class="tab-content" id="komoditi-rekod-tabContent">
<div class="tab-pane fade show active" id="tab-manual" role="tabpanel" aria-labelledby="tab-manual-link">
<div class="row justify-content-between mb-4">
<div class="col text-left">
<div class="form-group d-flex align-items-center">
<label for="limit_view" style="width: 600px" class="mb-0">Had Paparan Rekod Komoditi</label>
<select onchange="loadKomoditiRecord()" name="limit_view" id="limit_view" class="form-control">
<option selected value="20">20</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
<option disabled>Untuk rekod keseluruhan, sila print</option>
</select>
</div>
</div>
<div class="col text-right">
<a href="{{ route('laporan.komoditi') }}" target="_blank" class="btn btn-primary" title="Cetak Rekod Jual/Beli Komoditi"><i class="fa fa-print" aria-hidden="true"></i></a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="spinner-border" role="status" id="loading-spinner">
<span class="sr-only">Loading...</span>
</div>
<x-datatable
id="rekod_komuditi"
filename="Rekod_Komoditi_Manual"
class="text-center"
:columns="['Bil', 'No Sijil', 'Tarikh', 'Masa', 'Nilai Komoditi', 'Transaksi Oleh', 'Transaksi']"
/>
</div>
</div>
</div>
<div class="col text-right">
<a href="{{ route('laporan.komoditi') }}" target="_blank" class="btn btn-primary" title="Cetak Rekod Jual/Beli Komoditi"><i class="fa fa-print" aria-hidden="true"></i></a>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="spinner-border" role="status" id="loading-spinner">
<span class="sr-only">Loading...</span>
</div>
<table id="rekod_komuditi" class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Bil</th>
<th>No Sijil</th>
<th>Tarikh</th>
<th>Masa</th>
<th>Nilai Komoditi</th>
<th>Transaksi Oleh</th>
<th>Transaksi</th>
</tr>
</thead>
</table>
<div class="tab-pane fade" id="tab-bsas" role="tabpanel" aria-labelledby="tab-bsas-link">
<div class="row justify-content-between mb-4">
<div class="col text-left">
<div class="form-group d-flex align-items-center">
<label for="limit_view_bsas" style="width: 600px" class="mb-0">Had Paparan Rekod BSAS</label>
<select onchange="loadBsasRecord()" name="limit_view_bsas" id="limit_view_bsas" class="form-control">
<option selected value="20">20</option>
<option value="100">100</option>
<option value="500">500</option>
<option value="1000">1000</option>
</select>
</div>
</div>
<div class="col text-right">
<button type="button" class="btn btn-primary" id="refresh_bsas" title="Muat Semula">
<i class="fas fa-sync-alt" aria-hidden="true"></i>
</button>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="spinner-border" role="status" id="loading-spinner-bsas" style="display:none;">
<span class="sr-only">Loading...</span>
</div>
<x-datatable
id="rekod_bsas"
filename="Rekod_Komoditi_BSAS"
class="text-center"
:columns="['Bil', 'Tarikh', 'UUID', 'No Rujukan', 'Cawangan', 'Pembeli', 'Bid Value', 'Murabaha', 'eCert No', 'Status', 'Oleh', 'Tindakan']"
/>
</div>
</div>
</div>
</div>
</div>
@@ -239,15 +277,44 @@
<script>
let api_url = "{{ api_url_for_browser() }}";
var tablerekod = $('#rekod_komuditi').DataTable( {
"pageLength": 1000,
"bLengthChange": false,
var tablerekod = initErahnDatatable('#rekod_komuditi', {
pageLength: 1000,
bLengthChange: false,
deferRender: true,
order: [[2, 'desc']],
});
var tablebsas = initErahnDatatable('#rekod_bsas', {
pageLength: 1000,
bLengthChange: false,
deferRender: true,
order: [[1, 'desc']],
});
var bsasLoaded = false;
$(document).ready( function () {
checkKomuditi();
});
$('#komoditi-rekod-tab a[data-toggle="tab"]').on('shown.bs.tab', function () {
if (typeof adjustErahnDatatables === 'function') {
adjustErahnDatatables();
}
});
$('#tab-bsas-link').on('shown.bs.tab', function () {
if (!bsasLoaded) {
loadBsasRecord();
} else if (typeof adjustErahnDatatables === 'function') {
adjustErahnDatatables();
}
});
$('#refresh_bsas').on('click', function () {
loadBsasRecord();
});
const formatter = new Intl.NumberFormat('en-US', {
minimumFractionDigits: 2
});
@@ -395,6 +462,101 @@
})
}
function bsasStatusBadge(status) {
var label = status || '-';
var cls = 'secondary';
if (status === 'confirmed') cls = 'success';
else if (status === 'failed') cls = 'danger';
else if (status === 'submitted' || status === 'pending') cls = 'warning';
return '<span class="badge badge-' + cls + '">' + label + '</span>';
}
function formatBsasDateTime(value) {
if (!value) {
return '-';
}
var date = new Date(value);
if (isNaN(date.getTime())) {
return value;
}
var pad = function (n) { return String(n).padStart(2, '0'); };
return date.getFullYear() + '-' +
pad(date.getMonth() + 1) + '-' +
pad(date.getDate()) + ' ' +
pad(date.getHours()) + ':' +
pad(date.getMinutes()) + ':' +
pad(date.getSeconds());
}
function bsasEcertAction(row) {
var ecertNo = row['e_cert_no'] || '';
var hasEcert = !!(row['has_bid_ecert'] || row['has_otc_ecert'] || row['has_stb_ecert']);
var uuid = row['uuid'] || '';
if (!hasEcert || !uuid) {
return '<span class="text-muted">-</span>';
}
var url = "{{ url('/komoditi/bsas/ecert') }}/" + encodeURIComponent(uuid);
return '<a class="btn btn-sm btn-outline-primary" href="' + url + '" target="_blank" title="Lihat eCert ' + ecertNo + '">' +
'<i class="fa fa-print" aria-hidden="true"></i> eCert' +
'</a>';
}
function loadBsasRecord()
{
var selectedLimitView = $('#limit_view_bsas').find(':selected').val();
$('#loading-spinner-bsas').show();
tablebsas.clear().draw();
$.ajax({
method: 'get',
url: api_url + '/api/komuditi/bsas/orders',
data: {
'limit_view': selectedLimitView,
},
success: function (rekod) {
var bilcount = 0;
$.each(rekod, function (index, row) {
bilcount++;
var ecertNo = row['e_cert_no'] || '-';
if ((row['has_bid_ecert'] || row['has_otc_ecert'] || row['has_stb_ecert']) && row['uuid']) {
ecertNo = '<a href="{{ url('/komoditi/bsas/ecert') }}/' + encodeURIComponent(row['uuid']) + '" target="_blank">' +
(row['e_cert_no'] || 'eCert') +
'</a>';
}
tablebsas.row.add([
bilcount,
formatBsasDateTime(row['created_at']),
row['uuid'] || '-',
row['norujukan'] || '-',
row['kodcaw'] || '-',
row['otc_counter_party'] || '-',
formatter.format(Number(row['bid_value'] || 0)),
formatter.format(Number(row['otc_murabaha_value'] || 0)),
ecertNo,
bsasStatusBadge(row['status']),
row['created_by'] || '-',
bsasEcertAction(row),
]).draw(false);
});
tablebsas.draw();
bsasLoaded = true;
$('#loading-spinner-bsas').hide();
},
error: function () {
bsasLoaded = false;
$('#loading-spinner-bsas').hide();
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: 'Gagal memuatkan rekod BSAS'
});
}
});
}
$('#detail_komuditi').on('show.bs.modal', function(event) {
var button = $(event.relatedTarget);
var kodproduk = button.data('kodproduk');
@@ -47,6 +47,7 @@
</div>
</div>
<script>
var api_url = '{{ api_url_for_browser() }}';
var table_margin = $('#table-margin').DataTable();
$.ajax({
@@ -13,56 +13,95 @@
</ol>
</nav>
<div class="card-body text-center">
<div class="row mb-4">
<div class="col-2 justify-content-start">
<div class="card-body">
<div class="row mb-3 align-items-end">
<div class="col-md-3">
<label for="date_from">Tarikh Dari</label>
<input type="date" id="date_from" class="form-control" value="{{ $dateFrom }}">
</div>
<div class="col-8 mt-3 justify-content-center">
<div class="col-md-3">
<label for="date_to">Tarikh Hingga</label>
<input type="date" id="date_to" class="form-control" value="{{ $dateTo }}">
</div>
<div class="col-2">
<div class="col-md-3">
<button type="button" id="btn-filter-rekod" class="btn btn-primary mt-3 mt-md-0">Cari</button>
</div>
</div>
<table class='table table-striped table-hover' id='table-kelulusan' style="width:100%;">
<thead class="thead-dark">
<tr>
<th>No Rujukan</th>
<th>Status</th>
<th>Komen</th>
<th>Nilai Pembiayaan</th>
<th>Nilai Marhun</th>
<th>Berat</th>
<th>Margin</th>
<th>Pelulus</th>
</tr>
</thead>
<tbody>
@foreach($arraykelulusan as $index=>$k)
<tr>
<td>{{ $k['norujukan'] }}</td>
<td>{{ $k['status'] }}</td>
<td>{{ $k['komen'] }}</td>
<td>{{ $k['pinjaman'] }}</td>
<td>{{ $k['nilaimarhun'] }}</td>
<td>{{ $k['berat'] }}</td>
<td>{{ $k['margin'] }}</td>
<td>{{ $k['pelulus'] }}</td>
</tr>
@endforeach
</tbody>
</table>
<x-datatable
id="table-kelulusan"
filename="Rekod_Lulus_Operasi"
class="text-center"
:columns="['Tarikh', 'No Rujukan', 'Status', 'Komen', 'Nilai Pembiayaan', 'Nilai Marhun', 'Berat', 'Margin', 'Pelulus']"
/>
</div>
</div>
</div>
</div>
</div>
<script>
function escapeHtml(value) {
return $('<div>').text(value == null ? '' : String(value)).html();
}
var tablekelulusan = $('#table-kelulusan').DataTable({
"scrollX": true
var tablekelulusan = initErahnDatatable('#table-kelulusan', {
deferRender: true,
order: [[0, 'desc']],
language: {
emptyTable: 'Tiada rekod',
zeroRecords: 'Tiada rekod dijumpai'
}
});
function setLoading(isLoading) {
$('#btn-filter-rekod').prop('disabled', isLoading).text(isLoading ? 'Memuatkan...' : 'Cari');
}
function loadRekodLulus() {
var dateFrom = $('#date_from').val();
var dateTo = $('#date_to').val();
setLoading(true);
tablekelulusan.clear().draw();
$.ajax({
url: "{{ route('operasi.rekodlepas.data') }}",
method: 'GET',
data: {
date_from: dateFrom,
date_to: dateTo
},
success: function (response) {
var rows = response.data || [];
var data = rows.map(function (row) {
return [
escapeHtml(row.tarikh || '-'),
escapeHtml(row.norujukan || '-'),
escapeHtml(row.status || '-'),
escapeHtml(row.komen || '-'),
escapeHtml(row.pinjaman || '-'),
escapeHtml(row.nilaimarhun || '-'),
escapeHtml(row.berat || '-'),
escapeHtml(row.margin || '-'),
escapeHtml(row.pelulus || '-')
];
});
tablekelulusan.rows.add(data).draw();
},
error: function () {
Swal.fire({
icon: 'error',
title: 'Ralat',
text: 'Gagal memuatkan rekod lulus.'
});
},
complete: function () {
setLoading(false);
}
});
}
$('#btn-filter-rekod').on('click', loadRekodLulus);
loadRekodLulus();
</script>
@endsection
@@ -54,104 +54,137 @@
@if($pendingCount > 0)
<form method="post"
action="{{ route('pembiayaan.online.operasi.approve.batch', ['kodcaw' => $kodcaw, 'batchId' => $batch['id']]) }}"
class="d-inline"
onsubmit="return confirm('Luluskan semua {{ $pendingCount }} rekod menunggu dalam batch ini?');">
class="d-inline js-swal-confirm"
data-title="Lulus Semua Menunggu?"
data-html="Luluskan semua <b>{{ $pendingCount }}</b> rekod menunggu dalam batch <b>{{ $batch['batch_no'] ?? '-' }}</b>?"
data-confirm="Ya, Lulus Semua">
@csrf
<button type="submit" class="btn btn-success btn-sm">Lulus Semua Menunggu ({{ $pendingCount }})</button>
</form>
@endif
@if($approvedCount > 0)
<button type="button" class="btn btn-dark btn-sm" data-toggle="modal" data-target="#paidBatchModal">
Tanda PAID Batch ({{ $approvedCount }})
Tanda semua sebagai dibayar ({{ $approvedCount }})
</button>
@endif
</div>
</div>
<div class="card-body table-responsive">
<table class="table table-striped table-sm text-center">
<thead class="thead-dark">
<div class="card-body">
<x-datatable
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', 'Pinjaman (RM)', 'Rujukan Bayaran', 'Dokumen', 'Tindakan']"
:skipExport="['Dokumen', 'Tindakan']"
>
@foreach($lines as $line)
@php
$st = $line['status'] ?? '';
$badge = 'badge-secondary';
if ($st === 'APPROVED') $badge = 'badge-success';
elseif ($st === 'PAID') $badge = 'badge-dark';
elseif ($st === 'REJECTED_PC') $badge = 'badge-danger';
elseif (in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true)) $badge = 'badge-primary';
@endphp
<tr>
<th>Status</th>
<th>SAG</th>
<th>Pelanggan</th>
<th>No KP</th>
<th>Bank</th>
<th>No Akaun</th>
<th>Nama Akaun</th>
<th>Pinjaman (RM)</th>
<th>Rujukan Bayaran</th>
<th>Tindakan</th>
<td>{{ $loop->iteration }}</td>
<td><span class="badge {{ $badge }}">{{ $st ?: '-' }}</span></td>
<td>{{ $line['norujukan'] ?? '-' }}</td>
<td class="text-left">{{ $line['nama_pelanggan'] ?? '-' }}</td>
<td>{{ $line['nokp'] ?? '-' }}</td>
<td>{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}</td>
<td>{{ $line['noakaun'] ?? '-' }}</td>
<td class="text-right">{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}</td>
<td>{{ $line['no_rujukan_bayaran'] ?? '-' }}</td>
<td class="text-nowrap">
<button type="button"
class="btn btn-sm btn-outline-dark btn-preview-sag"
data-url="{{ route('pembiayaan.online.operasi.sag', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
SAG
</button>
@if(!empty($line['bukti_akaun_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.operasi.bukti.akaun', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
target="_blank">
Slip Bank
</a>
@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))
<form method="post"
action="{{ route('pembiayaan.online.operasi.approve', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
class="d-inline js-swal-confirm"
data-title="Luluskan Rekod?"
data-html="Luluskan rekod <b>{{ $line['norujukan'] ?? '-' }}</b>?"
data-confirm="Ya, Lulus">
@csrf
<input type="hidden" name="batch_id" value="{{ $batch['id'] }}">
<button type="submit" class="btn btn-success btn-sm">Lulus</button>
</form>
<button type="button"
class="btn btn-danger btn-sm btn-reject"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Tolak
</button>
@elseif($st === 'APPROVED')
<button type="button"
class="btn btn-dark btn-sm btn-paid"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Tanda sebagai dibayar
</button>
<button type="button"
class="btn btn-outline-danger btn-sm btn-reject"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Tolak
</button>
@elseif($st === 'PAID')
<span class="text-muted small">Selesai</span>
@else
<span class="text-muted small">-</span>
@endif
</td>
</tr>
</thead>
<tbody>
@forelse($lines as $line)
@php
$st = $line['status'] ?? '';
$badge = 'badge-secondary';
if ($st === 'APPROVED') $badge = 'badge-success';
elseif ($st === 'PAID') $badge = 'badge-dark';
elseif ($st === 'REJECTED_PC') $badge = 'badge-danger';
elseif (in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true)) $badge = 'badge-primary';
@endphp
<tr>
<td><span class="badge {{ $badge }}">{{ $st ?: '-' }}</span></td>
<td>{{ $line['norujukan'] ?? '-' }}</td>
<td class="text-left">{{ $line['nama_pelanggan'] ?? '-' }}</td>
<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">
@if(in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true))
<form method="post"
action="{{ route('pembiayaan.online.operasi.approve', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
class="d-inline"
onsubmit="return confirm('Luluskan rekod {{ $line['norujukan'] ?? '' }}?');">
@csrf
<input type="hidden" name="batch_id" value="{{ $batch['id'] }}">
<button type="submit" class="btn btn-success btn-sm">Lulus</button>
</form>
<button type="button"
class="btn btn-danger btn-sm btn-reject"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Tolak
</button>
@elseif($st === 'APPROVED')
<button type="button"
class="btn btn-dark btn-sm btn-paid"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
PAID + Bukti
</button>
<button type="button"
class="btn btn-outline-danger btn-sm btn-reject"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Tolak
</button>
@elseif($st === 'PAID')
<span class="text-muted small">Selesai</span>
@else
<span class="text-muted small">-</span>
@endif
</td>
</tr>
@empty
<tr>
<td colspan="10">Tiada rekod dalam batch.</td>
</tr>
@endforelse
</tbody>
</table>
@endforeach
</x-datatable>
</div>
</div>
</div>
</div>
</div>
{{-- SAG preview modal --}}
<div class="modal fade" id="sagPreviewModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Pratonton SAG — <span id="sagPreviewTitle"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body p-2">
<iframe id="sagPreviewFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
{{-- Reject modal --}}
<div class="modal fade" id="rejectModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
@@ -188,7 +221,7 @@
<input type="hidden" name="batch_id" value="{{ $batch['id'] }}">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tanda PAID — <span id="paidNorujukan"></span></h5>
<h5 class="modal-title">Tanda sebagai dibayar — <span id="paidNorujukan"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
@@ -203,12 +236,12 @@
<div class="form-group">
<label>Bukti Bayaran <span class="text-danger">*</span></label>
<input type="file" name="bukti" class="form-control-file" accept=".pdf,.jpg,.jpeg,.png,.gif" required>
<small class="text-muted">PDF / imej slip pindahan bank</small>
<small class="text-muted">PDF / imej slip bank</small>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-dark">Simpan PAID</button>
<button type="submit" class="btn btn-dark">Simpan</button>
</div>
</div>
</form>
@@ -224,7 +257,7 @@
@csrf
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Tanda PAID Semua APPROVED ({{ $approvedCount }})</h5>
<h5 class="modal-title">Tanda semua sebagai dibayar ({{ $approvedCount }})</h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
@@ -244,7 +277,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-dark">Simpan PAID Batch</button>
<button type="submit" class="btn btn-dark">Simpan</button>
</div>
</div>
</form>
@@ -252,7 +285,28 @@
</div>
<script>
$('.btn-reject').on('click', function () {
var table = initErahnDatatable('#batch_lines', {
columnDefs: [
{ targets: 0, orderable: false, searchable: false, width: '40px' },
{ targets: [10, 11], orderable: false, searchable: false }
]
});
table.on('draw.dt', function () {
table.column(0, { search: 'applied', order: 'applied' }).nodes().each(function (cell, i) {
cell.innerHTML = i + 1;
});
}).draw();
$('#batch_lines').on('click', '.btn-preview-sag', function () {
$('#sagPreviewTitle').text($(this).data('norujukan') || '');
$('#sagPreviewFrame').attr('src', $(this).data('url'));
$('#sagPreviewModal').modal('show');
});
$('#sagPreviewModal').on('hidden.bs.modal', function () {
$('#sagPreviewFrame').attr('src', 'about:blank');
});
$('#batch_lines').on('click', '.btn-reject', function () {
var id = $(this).data('id');
$('#rejectForm').attr(
'action',
@@ -263,7 +317,7 @@
$('#rejectModal').modal('show');
});
$('.btn-paid').on('click', function () {
$('#batch_lines').on('click', '.btn-paid', function () {
var id = $(this).data('id');
$('#paidForm').attr(
'action',
@@ -272,5 +326,25 @@
$('#paidNorujukan').text($(this).data('norujukan'));
$('#paidModal').modal('show');
});
$(document).on('submit', '.js-swal-confirm', function (e) {
e.preventDefault();
var form = this;
Swal.fire({
title: $(form).attr('data-title') || 'Pengesahan',
html: $(form).attr('data-html') || 'Teruskan tindakan ini?',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: $(form).attr('data-confirm') || 'Ya',
cancelButtonText: 'Batal',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
form.submit();
}
});
});
</script>
@endsection
@@ -40,6 +40,7 @@
<th>No Akaun</th>
<th>Pinjaman (RM)</th>
<th>Rujukan Bayaran</th>
<th>Dokumen</th>
</tr>
</thead>
<tbody>
@@ -53,6 +54,30 @@
<td>{{ $line['noakaun'] ?? '-' }}</td>
<td class="text-right">{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}</td>
<td>{{ $line['no_rujukan_bayaran'] ?? '-' }}</td>
<td class="text-nowrap">
<button type="button"
class="btn btn-sm btn-outline-dark btn-preview-sag"
data-url="{{ route('pembiayaan.online.pc.sag', ['id' => $line['id']]) }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
SAG
</button>
@if(!empty($line['bukti_akaun_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.pc.bukti.akaun', ['id' => $line['id']]) }}"
target="_blank">
Slip Bank
</a>
@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
</tbody>
@@ -62,4 +87,32 @@
</div>
</div>
</div>
<div class="modal fade" id="sagPreviewModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Pratonton SAG — <span id="sagPreviewTitle"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body p-2">
<iframe id="sagPreviewFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:75vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Tutup</button>
</div>
</div>
</div>
</div>
<script>
$('.btn-preview-sag').on('click', function () {
$('#sagPreviewTitle').text($(this).data('norujukan') || '');
$('#sagPreviewFrame').attr('src', $(this).data('url'));
$('#sagPreviewModal').modal('show');
});
$('#sagPreviewModal').on('hidden.bs.modal', function () {
$('#sagPreviewFrame').attr('src', 'about:blank');
});
</script>
@endsection
@@ -69,13 +69,13 @@
<div class="card-header d-flex justify-content-between align-items-center flex-wrap">
<h5 class="mb-0">Senarai Pembiayaan Online — PAID</h5>
<button type="button" class="btn btn-success btn-sm mt-2 mt-md-0" id="btnMarkBlast" disabled>
Sahkan Tally / Selesai
Sahkan semua sebagai dibayar
</button>
</div>
<div class="card-body">
<p class="small text-muted">
Semak jumlah &amp; bukti bayaran sepadan dengan pelanggan. Setelah semak, tanda selesai.
(WhatsApp/SMS blast boleh disambung kemudian — butang ini menanda rekod sebagai selesai semak.)
WhatsApp notis (template Cloud API) akan dihantar selepas tally. Di persekitaran ujian ia pergi ke nombor admin, bukan pelanggan.
</p>
<form id="formMarkBlast" method="post" action="{{ route('pembiayaan.online.pc.blast') }}">
@csrf
@@ -168,7 +168,7 @@
function syncBlastButton() {
var n = $('.row-check:checked').length;
$('#btnMarkBlast').prop('disabled', n === 0);
$('#btnMarkBlast').text(n ? ('Sahkan Tally / Selesai (' + n + ')') : 'Sahkan Tally / Selesai');
$('#btnMarkBlast').text(n ? ('Sahkan semua sebagai dibayar (' + n + ')') : 'Sahkan semua sebagai dibayar');
}
$('#checkAll').on('change', function () {
@@ -180,7 +180,7 @@
$('#btnMarkBlast').on('click', function () {
if ($('.row-check:checked').length === 0) return;
if (!confirm('Sahkan tally untuk rekod dipilih? Rekod akan ditanda selesai semak/blast.')) return;
if (!confirm('Sahkan semua sebagai dibayar untuk rekod dipilih? Rekod akan ditanda sebagai dibayar dan WhatsApp notis akan dihantar.')) return;
$('#formMarkBlast').submit();
});
</script>
@@ -33,6 +33,20 @@
if (is_object($pendingReq)) {
$pendingReq = json_decode(json_encode($pendingReq), true);
}
$lineRows = collect($lines);
$verifiedCount = $lineRows->where('status', 'VERIFIED_PC')->count();
$unverifiedCount = $lineRows->count() - $verifiedCount;
$slotArr = is_array($slot) ? $slot : [];
$slotName = $slotArr['slot'] ?? 'BATCH1';
$slotCutoff = $slotArr['cutoff'] ?? '11:00';
$slotProses = $slotArr['proses'] ?? '12:30';
$slotLewat = !empty($slotArr['lewat']);
$slotJadual = $slotArr['jadual'] ?? [
['slot' => 'BATCH1', 'label' => 'Batch 1', 'mula' => '00:00', 'cutoff' => '11:00', 'proses' => '12:30'],
['slot' => 'BATCH2', 'label' => 'Batch 2', 'mula' => '11:00', 'cutoff' => '15:00', 'proses' => '16:30'],
];
$slotBakiMinit = (int) ($slotArr['baki_minit'] ?? 0);
$slotHampirTamat = !$slotLewat && $slotBakiMinit > 0 && $slotBakiMinit <= 30;
@endphp
@if($isClosed)
@@ -56,6 +70,66 @@
</div>
@endif
<div class="alert {{ $slotLewat ? 'alert-warning' : ($slotHampirTamat ? 'alert-warning' : 'alert-info') }} mb-3">
<div class="d-flex justify-content-between align-items-start flex-wrap">
<div class="mb-2 mr-3">
<strong>Jadual Slot Batch</strong>
<div class="mt-1">
PC / PPC sila semak dan <b>hantar senarai sebelum tamat slot</b>.
Rekod yang dihantar selepas cutoff mungkin diproses pada slot atau hari berikutnya.
</div>
</div>
<div class="mb-2 text-md-right">
@if($slotLewat)
Slot semasa: <b>{{ $slotName }}</b> (selepas cutoff {{ $slotCutoff }})
@else
Slot semasa: <b>{{ $slotName }}</b>
&middot; Hantar sebelum <b>{{ $slotCutoff }}</b>
<div class="small" id="slotCountdown"
data-cutoff="{{ $slotCutoff }}"
data-lewat="{{ $slotLewat ? '1' : '0' }}">
@if($slotBakiMinit > 0)
Baki masa: {{ intdiv($slotBakiMinit, 60) }}j {{ $slotBakiMinit % 60 }}m
@endif
</div>
@endif
</div>
</div>
<div class="table-responsive mb-0">
<table class="table table-sm table-bordered mb-0 bg-white">
<thead class="thead-light">
<tr>
<th>Slot</th>
<th>Hantar senarai sebelum</th>
<th>Proses Operasi</th>
</tr>
</thead>
<tbody>
@foreach($slotJadual as $row)
@php
$rowSlot = $row['slot'] ?? '';
$isCurrent = $rowSlot === $slotName && !$slotLewat;
$isDone = $slotLewat
|| ($slotName === 'BATCH2' && $rowSlot === 'BATCH1');
@endphp
<tr class="{{ $isCurrent ? 'table-primary' : '' }}">
<td>
{{ $row['label'] ?? $rowSlot }}
@if($isCurrent)
<span class="badge badge-primary">Semasa</span>
@elseif($isDone)
<span class="badge badge-secondary">Tamat</span>
@endif
</td>
<td>{{ $row['cutoff'] ?? '-' }}</td>
<td>~ {{ $row['proses'] ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<div class="row mb-3">
<div class="col-md-4">
<div class="card">
@@ -82,18 +156,30 @@
<div class="col-md-4">
<div class="card">
<div class="card-body text-center">
<div class="text-muted">Queue PC</div>
<div class="text-muted">Senarai Pelanggan menunggu semakan</div>
<h4 class="mb-0">{{ count($lines) }} rekod</h4>
<small>Jumlah RM {{ number_format((float)($queue['jumlah'] ?? 0), 2) }}</small>
<small>
{{ $verifiedCount }} sudah sah
/ {{ $unverifiedCount }} belum semak
&middot; Jumlah RM {{ number_format((float)($queue['jumlah'] ?? 0), 2) }}
</small>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card {{ $slotLewat || $slotHampirTamat ? 'border-warning' : '' }}">
<div class="card-body text-center">
<div class="text-muted">Slot Cadangan</div>
<h4 class="mb-0">{{ $slot['slot'] ?? '-' }}</h4>
<small>{{ $slot['nota'] ?? '' }}</small>
<div class="text-muted">Slot Semasa</div>
<h4 class="mb-0">{{ $slotName }}</h4>
<small>
@if($slotLewat)
Cutoff {{ $slotCutoff }} sudah tamat<br>
Masih boleh hantar; proses mungkin esok
@else
Hantar sebelum <b>{{ $slotCutoff }}</b>
&middot; Proses ~{{ $slotProses }}
@endif
</small>
</div>
</div>
</div>
@@ -101,18 +187,27 @@
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">Senarai Menunggu (PENDING / REJECTED)</h5>
<h5 class="mb-0">Senarai Menunggu (PENDING / REJECTED / SAH)</h5>
<button type="button" class="btn btn-primary" id="btnSubmitBatch" disabled>Hantar Batch ke Operasi</button>
</div>
<div class="card-body">
<p class="small text-muted">
Semak setiap rekod <b>satu-satu</b> (maklumat pelanggan, bank, amaun).
Hanya rekod berstatus <span class="badge badge-success">SAH</span> boleh dipilih dan dihantar ke Operasi.
@if($slotLewat)
Cutoff {{ $slotName }} ({{ $slotCutoff }}) sudah tamat — hantar sekarang jika perlu; Operasi mungkin proses esok.
@else
Sila hantar senarai <b>{{ $slotName }}</b> sebelum <b>{{ $slotCutoff }}</b>.
@endif
</p>
<form id="formSubmitBatch" method="post" action="{{ route('pembiayaan.online.pc.submit') }}">
@csrf
<input type="hidden" name="slot" value="{{ $slot['slot'] ?? 'BATCH1' }}">
<input type="hidden" name="slot" value="{{ $slotName }}">
<div class="table-responsive">
<table class="table table-striped table-hover table-sm text-center" id="queueTable">
<thead class="thead-dark">
<tr>
<th><input type="checkbox" id="checkAll"></th>
<th><input type="checkbox" id="checkAll" title="Pilih semua yang sudah sah"></th>
<th>Status</th>
<th>SAG</th>
<th>Pelanggan</th>
@@ -126,15 +221,31 @@
</thead>
<tbody>
@forelse($lines as $line)
<tr class="{{ ($line['status'] ?? '') === 'REJECTED_PC' ? 'table-warning' : '' }}">
@php
$st = $line['status'] ?? '';
$isVerified = $st === 'VERIFIED_PC';
$isRejected = $st === 'REJECTED_PC';
$bankOk = !empty($line['kodbank']) && !empty($line['noakaun']);
@endphp
<tr class="{{ $isRejected ? 'table-warning' : ($isVerified ? 'table-success' : '') }}">
<td>
<input type="checkbox" class="row-check" name="ids[]" value="{{ $line['id'] }}">
<input type="checkbox"
class="row-check"
name="ids[]"
value="{{ $line['id'] }}"
{{ $isVerified ? '' : 'disabled' }}
title="{{ $isVerified ? '' : 'Sahkan rekod ini dahulu' }}">
</td>
<td>
@if(($line['status'] ?? '') === 'REJECTED_PC')
@if($isRejected)
<span class="badge badge-danger">REJECTED</span>
@elseif($isVerified)
<span class="badge badge-success">SAH</span>
@if(!empty($line['pc_verified_by']))
<div class="small text-muted">{{ $line['pc_verified_by'] }}</div>
@endif
@else
<span class="badge badge-secondary">PENDING</span>
<span class="badge badge-secondary">BELUM SEMAK</span>
@endif
</td>
<td>{{ $line['norujukan'] ?? '-' }}</td>
@@ -144,15 +255,42 @@
<td>{{ $line['noakaun'] ?? '-' }}</td>
<td class="text-right">{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}</td>
<td class="text-left small">{{ $line['reject_reason'] ?? '-' }}</td>
<td>
<td class="text-nowrap">
@if($isVerified)
<button type="button"
class="btn btn-sm btn-outline-secondary btn-unverify"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}">
Batal
</button>
@else
<button type="button"
class="btn btn-sm btn-success btn-verify"
{{ $bankOk ? '' : 'disabled' }}
title="{{ $bankOk ? 'Semak maklumat pelanggan' : 'Lengkapkan maklumat bank dahulu' }}"
data-id="{{ $line['id'] }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}"
data-nama="{{ $line['nama_pelanggan'] ?? '' }}"
data-nokp="{{ $line['nokp'] ?? '' }}"
data-namabank="{{ $line['nama_bank'] ?? $line['kodbank'] ?? '' }}"
data-noakaun="{{ $line['noakaun'] ?? '' }}"
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-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Semak
</button>
@endif
<button type="button" class="btn btn-sm btn-info btn-edit"
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-norujukan="{{ $line['norujukan'] ?? '' }}"
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}"
data-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Edit
</button>
</td>
@@ -166,6 +304,9 @@
</table>
</div>
</form>
<form id="formUnverify" method="post" action="" class="d-none">
@csrf
</form>
</div>
</div>
@@ -221,8 +362,8 @@
{{-- Edit modal --}}
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog" role="document">
<form method="post" id="editForm" action="">
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<form method="post" id="editForm" action="" enctype="multipart/form-data">
@csrf
@method('PUT')
<div class="modal-content">
@@ -231,29 +372,57 @@
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
<div class="form-group">
<label>Bank</label>
<select name="kodbank" id="editKodbank" class="form-control" required>
<option value="">-- Pilih Bank --</option>
@foreach($banks as $bank)
<option value="{{ $bank['abbreviation'] }}" data-nama="{{ $bank['name'] }}">
{{ $bank['name'] }}
</option>
@endforeach
</select>
<input type="hidden" name="nama_bank" id="editNamabank">
</div>
<div class="form-group">
<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>Nota</label>
<textarea name="nota" id="editNota" class="form-control" rows="2"></textarea>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<label>Bank</label>
<select name="kodbank" id="editKodbank" class="form-control" required>
<option value="">-- Pilih Bank --</option>
@foreach($banks as $bank)
<option value="{{ $bank['abbreviation'] }}" data-nama="{{ $bank['name'] }}">
{{ $bank['name'] }}
</option>
@endforeach
</select>
<input type="hidden" name="nama_bank" id="editNamabank">
</div>
<div class="form-group">
<label>No. Akaun</label>
<input type="text" name="noakaun" id="editNoakaun" class="form-control" required>
</div>
<div class="form-group">
<label>Slip Bank / Bukti Akaun</label>
<input type="file" name="bukti_akaun" id="editBuktiAkaun" class="form-control-file"
accept=".pdf,.jpg,.jpeg,.png,.gif">
<div id="editBuktiAkaunExisting" class="mt-2" style="display:none;">
<a href="#" id="editBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank">
Lihat slip sedia ada
</a>
</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>
</div>
<p class="small text-muted mb-0">Jika rekod sudah SAH, simpan akan batal semakan. Sila semak semula sebelum hantar.</p>
</div>
<div class="col-md-8">
<label class="d-block">Pratonton SAG</label>
<iframe id="editSagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
</div>
</div>
<div class="modal-footer">
@@ -265,24 +434,179 @@
</div>
</div>
{{-- Verify one-by-one modal --}}
<div class="modal fade" id="verifyModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-xl" role="document" style="max-width:1140px;">
<form method="post" id="verifyForm" action="">
@csrf
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Semak Rekod — <span id="verifyNorujukan"></span></h5>
<button type="button" class="close" data-dismiss="modal"><span>&times;</span></button>
</div>
<div class="modal-body">
<p class="small text-muted">Bandingkan maklumat pelanggan dengan SAG / slip bank sebelum sahkan.</p>
<div class="row">
<div class="col-md-4">
<table class="table table-sm table-bordered mb-3">
<tr>
<th style="width:40%">SAG</th>
<td id="verifySag"></td>
</tr>
<tr>
<th>Pelanggan</th>
<td id="verifyNama"></td>
</tr>
<tr>
<th>No KP</th>
<td id="verifyNokp"></td>
</tr>
<tr>
<th>Bank</th>
<td id="verifyBank"></td>
</tr>
<tr>
<th>No Akaun</th>
<td id="verifyNoakaun"></td>
</tr>
<tr>
<th>Pinjaman</th>
<td>RM <span id="verifyPinjaman"></span></td>
</tr>
<tr>
<th>Nota</th>
<td id="verifyNota"></td>
</tr>
</table>
<div class="alert alert-warning py-2" id="verifySebabBox" style="display:none;">
<b>Sebab tolak Operasi:</b>
<div id="verifySebab"></div>
</div>
<div class="form-check mb-3">
<input type="checkbox" class="form-check-input" id="verifyConfirm" required>
<label class="form-check-label" for="verifyConfirm">
Saya sahkan maklumat pelanggan, bank dan amaun adalah betul.
</label>
</div>
<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>
<iframe id="verifySagFrame" title="Pratonton SAG" src="about:blank"
style="width:100%;height:70vh;border:1px solid #dee2e6;background:#f8f9fa;"></iframe>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Batal</button>
<button type="submit" class="btn btn-success">Sahkan Rekod Ini</button>
</div>
</div>
</form>
</div>
</div>
<script>
function syncSubmitButton() {
var n = $('.row-check:checked').length;
var n = $('.row-check:not(:disabled):checked').length;
$('#btnSubmitBatch').prop('disabled', n === 0);
$('#btnSubmitBatch').text(n ? ('Hantar Batch ke Operasi (' + n + ')') : 'Hantar Batch ke Operasi');
}
$('#checkAll').on('change', function () {
$('.row-check').prop('checked', this.checked);
$('.row-check:not(:disabled)').prop('checked', this.checked);
syncSubmitButton();
});
$(document).on('change', '.row-check', syncSubmitButton);
$(document).on('change', '.row-check', function () {
var enabled = $('.row-check:not(:disabled)');
$('#checkAll').prop('checked', enabled.length > 0 && enabled.filter(':checked').length === enabled.length);
syncSubmitButton();
});
$('#btnSubmitBatch').on('click', function () {
if ($('.row-check:checked').length === 0) return;
if (!confirm('Hantar rekod dipilih ke Operasi?')) return;
$('#formSubmitBatch').submit();
var n = $('.row-check:not(:disabled):checked').length;
if (n === 0) return;
Swal.fire({
title: 'Hantar Batch ke Operasi?',
html: n + ' rekod yang sudah SAH akan dihantar sebagai <b>{{ $slotName }}</b>.'
+ '<br>Sila hantar sebelum cutoff <b>{{ $slotCutoff }}</b>.',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Hantar',
cancelButtonText: 'Batal',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
$('#formSubmitBatch').submit();
}
});
});
$('.btn-unverify').on('click', function () {
var id = $(this).data('id');
var sag = $(this).data('norujukan') || '';
Swal.fire({
title: 'Batal Semakan?',
html: 'Batal semakan <b>' + (sag || 'rekod ini') + '</b>?'
+ '<br>Rekod ini tidak boleh dihantar sehingga disemak semula.',
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Ya, Batal',
cancelButtonText: 'Tutup',
allowOutsideClick: false
}).then(function (result) {
if (result.isConfirmed) {
$('#formUnverify').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/unverify");
$('#formUnverify').submit();
}
});
});
$('.btn-verify').on('click', function () {
var id = $(this).data('id');
var sebab = $(this).data('sebab') || '';
$('#verifyForm').attr('action', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/verify");
$('#verifyNorujukan').text($(this).data('norujukan'));
$('#verifySag').text($(this).data('norujukan') || '-');
$('#verifyNama').text($(this).data('nama') || '-');
$('#verifyNokp').text($(this).data('nokp') || '-');
$('#verifyBank').text($(this).data('namabank') || '-');
$('#verifyNoakaun').text($(this).data('noakaun') || '-');
$('#verifyPinjaman').text($(this).data('pinjaman') || '0.00');
$('#verifyNota').text($(this).data('nota') || '-');
if (sebab) {
$('#verifySebab').text(sebab);
$('#verifySebabBox').show();
} else {
$('#verifySebabBox').hide();
}
$('#verifyConfirm').prop('checked', false);
$('#verifySagFrame').attr('src', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/sag");
if ($(this).data('buktiAkaun')) {
$('#verifyBuktiAkaunLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/bukti-akaun").show();
} else {
$('#verifyBuktiAkaunLink').hide();
}
if ($(this).data('borangFile')) {
$('#verifyBorangLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/borang").show();
} else {
$('#verifyBorangLink').hide();
}
$('#verifyModal').modal('show');
});
$('#verifyModal').on('hidden.bs.modal', function () {
$('#verifySagFrame').attr('src', 'about:blank');
});
$('.btn-edit').on('click', function () {
@@ -291,14 +615,59 @@
$('#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");
$('#editBuktiAkaunExisting').show();
} 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');
});
$('#editModal').on('hidden.bs.modal', function () {
$('#editSagFrame').attr('src', 'about:blank');
});
$('#editKodbank').on('change', function () {
$('#editNamabank').val($('#editKodbank option:selected').data('nama') || '');
});
function pad2(n) {
return n < 10 ? '0' + n : String(n);
}
function tickSlotCountdown() {
var el = document.getElementById('slotCountdown');
if (!el) return;
if (el.getAttribute('data-lewat') === '1') return;
var cutoff = el.getAttribute('data-cutoff') || '';
var parts = cutoff.split(':');
if (parts.length < 2) return;
var now = new Date();
var end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), parseInt(parts[0], 10), parseInt(parts[1], 10), 0);
var diff = end - now;
if (diff <= 0) {
el.textContent = 'Cutoff telah tamat — sila muat semula halaman';
return;
}
var h = Math.floor(diff / 3600000);
var m = Math.floor((diff % 3600000) / 60000);
var s = Math.floor((diff % 60000) / 1000);
el.textContent = 'Baki masa: ' + h + 'j ' + pad2(m) + 'm ' + pad2(s) + 's';
}
tickSlotCountdown();
setInterval(tickSlotCountdown, 1000);
</script>
@endsection
@@ -131,7 +131,7 @@
</div>
</div>
<div class="card">
{{-- <div class="card">
<div class="card-header">
<h5 class="mb-0">Tutup Modal Online (EOD)</h5>
</div>
@@ -153,7 +153,7 @@
</form>
@endif
</div>
</div>
</div> --}}
</div>
<div class="col-md-7 mb-3">
+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;
+52 -21
View File
@@ -326,6 +326,16 @@
<input type="text" id="jumlahtelahdibayar" class="form-control" readonly/>
</div>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<div class="form-group row">
<div class="col-4">
REBET DISKAUN (RM)
</div>
<div class="col-8">
<input type="text" id="jumlahDiskaun" value="{{ number_format($gadai['diskaun'] ?? 0, 2) }}" class="form-control" readonly/>
</div>
</div>
@endif
</div>
</div>
@@ -441,6 +451,16 @@
<input type="text" id="jumlahupah" class="form-control" readonly/>
</div>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<div class="form-group row">
<div class="col-4">
REBET DISKAUN (RM)
</div>
<div class="col-8">
<input type="text" id="jumlahDiskaun" value="{{ number_format($gadai['diskaun'] ?? 0, 2) }}" class="form-control" readonly/>
</div>
</div>
@endif
<div id="divbakipinjaman">
<div class="form-group row">
@@ -467,6 +487,17 @@
<input type="text" id="jumlahtelahdibayar" class="form-control" readonly/>
</div>
</div>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<div class="form-group row">
<div class="col-4">
REBET DISKAUN (RM)
</div>
<div class="col-8">
<input type="text" id="jumlahDiskaun" value="{{ number_format($gadai['diskaun'] ?? 0, 2) }}" class="form-control" readonly/>
</div>
</div>
@endif
<div class="form-group row" id="divbayaran">
@@ -2233,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();
@@ -2615,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();
@@ -3075,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;
@@ -214,8 +214,7 @@
/>
<div class="title">
<h1>Borang Kelulusan Pembiayaan Online<br>Senarai Batch Pembayaran</h1>
<p>Untuk semakan Operasi dan kelulusan Pengurus Besar. Bukan untuk tandatangan pelanggan.</p>
<h1>Borang Kelulusan Batch Pembiayaan Online</h1>
</div>
<div class="section">
@@ -259,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>
@@ -272,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
@@ -298,8 +297,8 @@
bagi batch <b>{{ $batchNo }}</b> cawangan <b>{{ $cawangan }}</b> adalah lengkap
untuk proses pembayaran ke akaun bank pelanggan.
<ol>
<li>Maklumat SAG, nama, no. KP, bank dan no. akaun telah disemak terhadap rekod sistem.</li>
<li>Pembayaran akan dikreditkan ke akaun penerima seperti senarai, bukan tunai di kaunter.</li>
<li>Maklumat SAG, nama, no. KP, bank dan no. akaun telah disemak didalam sistem.</li>
<li>Pembayaran akan dikreditkan ke akaun penerima seperti didalam senarai.</li>
<li>Sebarang ralat akaun yang dikesan selepas kelulusan hendaklah dilaporkan serta-merta.</li>
</ol>
</div>
@@ -307,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>
@@ -315,21 +314,14 @@
</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 / Paraf:</p>
<p>Jawatan:</p>
<p>Tarikh:</p>
</div>
</div>
</div>
<x-print.footer :show-copies="false">
<b>Nota:</b>
<p>1. Borang ini dicetak dari skrin Operasi — Pembiayaan Online (Batch) untuk kelulusan pembayaran.</p>
<p>2. Hanya dua tandatangan diperlukan: Operasi dan Pengurus Besar.</p>
<p>3. Simpan salinan bersama bukti pindahan bank selepas status PAID.</p>
</x-print.footer>
</div>
</div>
</body>
@@ -108,6 +108,19 @@
letter-spacing: 0.02em;
}
.copies {
margin-top: 12px;
font-size: 10px;
}
.copies span {
display: inline-block;
border: 1px solid #111;
width: 11px;
height: 11px;
margin-right: 4px;
vertical-align: -1px;
}
.akuan {
font-size: 11px;
line-height: 1.55;
@@ -121,10 +134,8 @@
.akuan li { margin-bottom: 4px; }
.signs {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 10px;
margin-top: 16px;
max-width: 34%;
}
.sign-box {
border: 1px solid #111;
@@ -184,7 +195,7 @@
/>
<div class="title">
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Online (Akaun Bank)</h1>
<h1>Borang Pengesahan Penerimaan Pembiayaan<br>Secara Atas Talian (Online)</h1>
<p>Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.</p>
</div>
@@ -214,7 +225,7 @@
<div class="section-title">B. Maklumat Pembiayaan</div>
<table class="fields">
<tr>
<th>No. Surat Akuan Gadai</th>
<th>No. Surat Akuan Gadaian (SAG)</th>
<td>{{ $norujukan }}</td>
</tr>
<tr>
@@ -222,8 +233,8 @@
<td class="amt">RM {{ $pinjaman }}</td>
</tr>
<tr>
<th>Cara Terima Pembiayaan</th>
<td>Online (kredit ke akaun bank) — bukan tunai</td>
<th>Cara Pembiayaan</th>
<td>Atas Talian (Online)</td>
</tr>
<tr>
<th>Teller</th>
@@ -250,15 +261,19 @@
</table>
</div>
<div class="copies">
Salinan: <span></span> Kad Pengenalan &nbsp;&nbsp; <span></span> Akaun Bank/ Slip/ Penyata Bank
</div>
<div class="section">
<div class="section-title">D. Akuan Pelanggan</div>
<div class="akuan">
Saya, <b>{{ $nama }}</b> (No. KP: <b>{{ $nokp }}</b>), dengan ini mengesahkan bahawa:
<ol>
<li>Maklumat akaun bank di Bahagian C adalah benar, tepat, dan milik saya sendiri.</li>
<li>Saya memohon supaya amaun pembiayaan gadai sebanyak <b>RM {{ $pinjaman }}</b> dikreditkan ke akaun tersebut dan <b>bukan</b> diterima secara tunai.</li>
<li>Saya faham kredit ke akaun tertakluk kepada semakan Pengurus Cawangan dan proses pembayaran pihak Koperasi.</li>
<li>Saya bertanggungjawab sepenuhnya jika maklumat akaun tidak tepat, dan tidak akan menuntut ganti rugi atas kelewatan atau ralat yang berpunca daripadanya.</li>
<li>Saya bersetuju menerima pembayaran pembiayaan Ar-Rahn KoPKB sebanyak <b>RM {{ $pinjaman }}</b> secara atas talian (Online) ke akaun tersebut.</li>
<li>Saya memahami bahawa pihak Ar-Rahn KoPKB tidak akan bertanggungjawab atas sebarang kesilapan akibat maklumat akaun yang diberikan oleh saya.</li>
<li>Saya mengesahkan bahawa segala maklumat yang diberikan adalah benar dan tepat.</li>
</ol>
</div>
</div>
@@ -272,29 +287,13 @@
<p>Tarikh: {{ $tarikh }}</p>
</div>
</div>
<div class="sign-box">
<div class="role">Tandatangan Teller</div>
<div class="sign-line">
<p>Nama: {{ $teller }}</p>
<p>Cop / Paraf:</p>
<p>Tarikh: {{ $tarikh }}</p>
</div>
</div>
<div class="sign-box">
<div class="role">Pengurus Cawangan</div>
<div class="sign-line">
<p>Nama:</p>
<p>Cop cawangan:</p>
<p>Tarikh:</p>
</div>
</div>
</div>
<x-print.footer>
<x-print.footer :show-copies="false">
<b>Nota:</b>
<p>1. Borang ini dicetak semasa pelanggan memilih cara terima pembiayaan secara Online.</p>
<p>2. Pembiayaan akan dikreditkan ke akaun selepas semakan PC. Pelanggan tidak menerima wang tunai di kaunter.</p>
<p>3. Sila pastikan nama pemegang akaun sepadan dengan nama pelanggan pada MyKad.</p>
<p>1. Pembayaran hanya dibuat ke akaun pelanggan sendiri.</p>
<p>2. Sebarang pembetulan maklumat hendaklah disahkan semula oleh pelanggan.</p>
<p>3. Borang ini hendaklah dilampirkan bersama dokumen transaksi gadaian untuk tujuan audit dan rekod.</p>
</x-print.footer>
</div>
</div>
+346
View File
@@ -0,0 +1,346 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BSAS eCert — {{ $ecert_no ?? 'Preview' }}</title>
<style>
:root {
--ink: #1a2332;
--muted: #5a6570;
--line: #c5a572;
--line-soft: #e2d4bc;
--accent: #0f4c5c;
--paper: #fbf8f2;
--panel: #ffffff;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
background: #d9dde3;
color: var(--ink);
font-family: "Palatino Linotype", Palatino, "Book Antiqua", Georgia, serif;
}
.preview-bar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
gap: 10px;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #0f4c5c;
color: #fff;
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
}
.preview-bar .badge {
display: inline-block;
padding: 2px 8px;
border: 1px solid rgba(255,255,255,.55);
border-radius: 3px;
letter-spacing: .08em;
font-size: 11px;
font-weight: 700;
}
.preview-bar button,
.preview-bar a {
border: 0;
background: #c5a572;
color: #1a2332;
font-weight: 700;
padding: 7px 14px;
border-radius: 4px;
cursor: pointer;
text-decoration: none;
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
}
.sheet-stack {
padding: 24px 12px 48px;
display: flex;
flex-direction: column;
align-items: center;
gap: 28px;
}
.page {
position: relative;
width: 210mm;
min-height: 297mm;
background: var(--paper);
box-shadow: 0 10px 30px rgba(0,0,0,.18);
overflow: hidden;
padding: 16mm 16mm 18mm;
}
.content {
position: relative;
z-index: 1;
min-height: calc(297mm - 34mm);
display: flex;
flex-direction: column;
}
.page-body {
flex: 1 1 auto;
}
.cert-title {
margin: 0 0 6px;
text-align: center;
font-size: 22px;
letter-spacing: .18em;
text-transform: uppercase;
color: var(--accent);
font-weight: 700;
}
.cert-no {
text-align: center;
font-family: Arial, Helvetica, sans-serif;
font-size: 13px;
margin-bottom: 18px;
}
.cert-no strong {
color: var(--accent);
letter-spacing: .04em;
}
.gold-rule {
height: 1px;
background: linear-gradient(90deg, transparent, var(--line), transparent);
margin: 0 8%;
}
.statement {
margin: 20px auto 24px;
max-width: 92%;
text-align: center;
font-size: 13.5px;
line-height: 1.65;
color: var(--ink);
}
.section-label {
margin: 0 0 10px;
font-family: Arial, Helvetica, sans-serif;
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--accent);
font-weight: 700;
border-left: 3px solid var(--line);
padding-left: 8px;
}
.detail-panel {
background: var(--panel);
border: 1px solid var(--line-soft);
border-radius: 2px;
padding: 14px 16px 8px;
}
.detail-grid {
width: 100%;
border-collapse: collapse;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
}
.detail-grid tr td {
padding: 7px 8px;
vertical-align: top;
border-bottom: 1px solid #eee6d8;
}
.detail-grid tr:last-child td {
border-bottom: 0;
}
.detail-grid .label {
width: 38%;
color: var(--muted);
font-weight: 600;
}
.detail-grid .value {
color: var(--ink);
font-weight: 700;
}
.commodity-table {
width: 100%;
border-collapse: collapse;
font-family: Arial, Helvetica, sans-serif;
font-size: 12px;
margin-top: 8px;
}
.commodity-table th,
.commodity-table td {
border: 1px solid var(--line-soft);
padding: 10px 12px;
text-align: left;
}
.commodity-table th {
background: #f3eee4;
color: var(--accent);
letter-spacing: .06em;
text-transform: uppercase;
font-size: 11px;
}
.commodity-table td.num {
text-align: right;
font-variant-numeric: tabular-nums;
}
.murabaha-box {
margin-top: 14px;
padding: 10px 12px;
border: 1px dashed var(--line);
background: rgba(197, 165, 114, .08);
font-family: Arial, Helvetica, sans-serif;
display: flex;
justify-content: space-between;
align-items: center;
}
.murabaha-box .k {
font-size: 11px;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
}
.murabaha-box .v {
font-size: 16px;
font-weight: 700;
color: var(--accent);
}
@media print {
body { background: #fff; }
.preview-bar { display: none !important; }
.sheet-stack { padding: 0; gap: 0; }
.page {
box-shadow: none;
margin: 0;
page-break-after: always;
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
.page:last-child { page-break-after: auto; }
}
@page {
size: A4 portrait;
margin: 0;
}
</style>
</head>
<body>
@php
$certificates = $certificates ?? [];
$lines = $lines ?? [];
$ecertNo = $ecert_no ?? 'CPO21OCT26-0000031-000';
$isDummy = $isDummy ?? false;
$pageTotal = count($certificates) + 1;
@endphp
<div class="preview-bar">
<div>
@if($isDummy)
<span class="badge">DUMMY PREVIEW</span>
&nbsp; BSAS e-Certificate layout — {{ $pageTotal }} pages A4
@else
BSAS e-Certificate — {{ $ecertNo }}
@endif
</div>
<div>
<button type="button" onclick="window.print()">Print / PDF</button>
</div>
</div>
<div class="sheet-stack">
@foreach($certificates as $index => $certificate)
<section class="page">
<x-print.bsas.watermark />
<div class="content">
<x-print.bsas.header />
<div class="page-body">
<h1 class="cert-title">{{ $certificate['title'] }}</h1>
<div class="cert-no">Certificate Number: <strong>{{ $certificate['ecert_no'] ?? $ecertNo }}</strong></div>
<div class="gold-rule"></div>
<p class="statement">{!! $certificate['statement'] !!}</p>
<h2 class="section-label">Detail</h2>
<div class="detail-panel">
<table class="detail-grid">
@foreach($certificate['rows'] as $row)
<tr>
<td class="label">{{ $row['label'] }}</td>
<td class="value">{{ $row['value'] }}</td>
</tr>
@endforeach
</table>
@if(!empty($certificate['show_murabaha']))
<div class="murabaha-box">
<span class="k">Murabaha Value</span>
<span class="v">{{ $certificate['currency'] ?? 'MYR' }} {{ $certificate['murabaha'] }}</span>
</div>
@endif
</div>
</div>
<x-print.bsas.footer
:page="$index + 1"
:total="$pageTotal"
/>
</div>
</section>
@endforeach
<section class="page">
<x-print.bsas.watermark />
<div class="content">
<x-print.bsas.header />
<div class="page-body">
<h1 class="cert-title" style="font-size:18px;">Commodity Schedule</h1>
<div class="cert-no">Certificate Number: <strong>{{ $ecertNo }}</strong></div>
<div class="gold-rule"></div>
<h2 class="section-label" style="margin-top:28px;">Commodity Information</h2>
<div class="detail-panel">
<table class="commodity-table">
<thead>
<tr>
<th>Commodity Supplying Participant</th>
<th style="width:28%;text-align:right;">Commodity Volume</th>
</tr>
</thead>
<tbody>
@foreach($lines as $line)
<tr>
<td>{{ $line['supplier'] ?? '-' }}</td>
<td class="num">{{ $line['volume'] ?? '-' }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
<x-print.bsas.footer :page="$pageTotal" :total="$pageTotal">
This schedule forms part of Certificate No. {{ $ecertNo }}
</x-print.bsas.footer>
</div>
</section>
</div>
</body>
</html>
@@ -233,7 +233,9 @@
@endif
<tr>
<td>{{ $counter }}</td>
<td style="width:100px">{{$item['norujukan']}}</td>
<td style="width:100px">{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>
{{ $item['nama'] }}
</td>
@@ -1,3 +1,4 @@
@php $rebetAnnur = !empty(($cawangan_info['features'] ?? [])['rebet_annur']); @endphp
<style>
#header {
font-family: Arial, Helvetica, sans-serif;
@@ -230,6 +231,9 @@
<th>Lain-Lain Caj (RM)</th>
<th>Keuntungan 1% (RM)</th>
<th>Keuntungan</th>
@if($rebetAnnur)
<th>Diskaun Rebet</th>
@endif
<th>Peratus (%)</th>
</tr>
@php
@@ -241,6 +245,7 @@
$bil = 0;
$full_total_ujrah = 0;
$full_total_onepercent = 0;
$totaldiskaun = 0;
@endphp
@foreach($gadai as $index=>$item)
@@ -251,6 +256,7 @@
$totalupah += $item['upah12'];
$full_total_ujrah += $item['ujrah']*12;
$full_total_onepercent += $item['onepercent']*12;
$totaldiskaun += $item['diskaun'] ?? 0;
@endphp
@if($index > 0)
@@ -264,6 +270,7 @@
$totalberatteller = 0;
$totalujrah = 0;
$totalonepercent = 0;
$totaldiskaunteller = 0;
@endphp
@endif
@@ -278,12 +285,15 @@
$totalupah12 = 0;
$totalujrah = 0;
$totalonepercent= 0;
$totaldiskaunteller = 0;
@endphp
@endif
<tr>
<td>{{ $counter }}</td>
<td style="width:100px">{{$item['norujukan']}}</td>
<td style="width:100px">{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>
{{ $item['nama'] }}
</td>
@@ -301,6 +311,9 @@
<td>{{ number_format($item['ujrah']*12,2) }}</td>
<td>{{ number_format($item['onepercent']*12,2) }}</td>
<td>{{ number_format($item['upah12'],2) }}</td>
@if($rebetAnnur)
<td>{{ number_format($item['diskaun'] ?? 0,2) }}</td>
@endif
<td>{{ $item['percentpinj'] }}</td>
</tr>
@php $counter++; @endphp
@@ -311,6 +324,7 @@
$totalupah12 += $item['upah12'];
$totalujrah += $item['ujrah']*12;
$totalonepercent += $item['onepercent']*12;
$totaldiskaunteller += $item['diskaun'] ?? 0;
@endphp
@@ -325,6 +339,9 @@
<td><b>{{ number_format($totalujrah,2) }}</b></td>
<td><b>{{ number_format($totalonepercent,2) }}</b></td>
<td><b>{{ number_format($totalupah12,2) }} </b></td>
@if($rebetAnnur)
<td><b>{{ number_format($totaldiskaunteller,2) }} </b></td>
@endif
</tr>
@@ -335,6 +352,7 @@
$totalnmteller = 0;
$totalujrah = 0;
$totalonepercent = 0;
$totaldiskaunteller = 0;
@endphp
@endif
@@ -348,6 +366,9 @@
<td><b>{{ number_format($totalujrah,2) }}</b></td>
<td><b>{{ number_format($totalonepercent,2) }}</b></td>
<td><b>{{ number_format($totalupah12,2) }} </b></td>
@if($rebetAnnur)
<td><b>{{ number_format($totaldiskaunteller,2) }} </b></td>
@endif
</tr>
@endif
@@ -219,7 +219,9 @@
@php if($item['hargajualan'] == null) $totalpinjaman += $item['pinjaman']; else $totalpinjaman += $item['bakipjm']; @endphp
<tr>
<td>{{ $counter }}</td>
<td>{{$item['norujukan']}}</td>
<td>{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>{{ $item['tempoh'] }}</td>
<td>{{$item['nokp']}}</td>
<td>{{ number_format($item['berat'],2) }}</td>
@@ -213,7 +213,9 @@
@php if($item['hargajualan'] == null) $totalpinjaman += $item['pinjaman']; else $totalpinjaman += $item['bakipjm']; @endphp
<tr>
<td>{{ $counter }}</td>
<td>{{$item['norujukan']}}</td>
<td>{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>{{ $item['jbg'] }}</td>
<td>{{$item['nokp']}}</td>
<td>{{ number_format($item['berat'],2) }}</td>
@@ -236,7 +236,9 @@
@endphp
<tr>
<td>{{ $counter }}</td>
<td>{{$item['norujukan']}}</td>
<td>{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>{{ $item['tempoh'] }}</td>
<td>{{$item['nokp']}}</td>
<td>{{ number_format($item['berat'],2) }}</td>
@@ -342,7 +344,9 @@
@endphp
<tr>
<td>{{ $counter }}</td>
<td>{{$item['norujukan']}}</td>
<td>{{$item['norujukan']}}<br>
@include('print.tag_cara_terima', ['caraTerima' => $item['cara_terima'] ?? null])
</td>
<td>{{ $item['jbg'] }}</td>
<td>{{$item['nokp']}}</td>
<td>{{ number_format($item['berat'],2) }}</td>
@@ -1,3 +1,4 @@
@php $rebetAnnur = !empty(($cawangan_info['features'] ?? [])['rebet_annur']); @endphp
<style>
#header {
font-family: Arial, Helvetica, sans-serif;
@@ -241,6 +242,9 @@
<th>B.Pembiayaan(RM)</th>
<th>Lain-Lain Caj (RM)</th>
<th>Untung 1% (RM)</th>
@if($rebetAnnur)
<th>Diskaun Rebet (RM)</th>
@endif
<th>Pendapatan(RM)</th>
<th>Bayaran(RM)</th>
<th>Jumlah(RM)</th>
@@ -254,6 +258,7 @@
$totalupah = 0;
$totalujrah = 0;
$totalonepercent = 0;
$totaldiskaunrebate = 0;
@endphp
@foreach ($tebus as $index => $item)
@@ -266,6 +271,7 @@
$totalnm += $item['nilaimarhun'];
$totalujrah += $item['ujrah'];
$totalonepercent += $item['onepercent'];
$totaldiskaunrebate += $item['diskaun_rebate'] ?? 0;
@endphp
@@ -282,6 +288,7 @@
$totalbayaranteller = 0;
$totalujrahteller = 0;
$totalonepercentteller = 0;
$totaldiskaunrebateteller = 0;
@endphp
@endif
@else
@@ -297,6 +304,7 @@
$totaljumlahteller = 0;
$totalujrahteller = 0;
$totalonepercentteller = 0;
$totaldiskaunrebateteller = 0;
@endphp
@endif
@@ -336,6 +344,9 @@
</td>
<td>{{ number_format($item['ujrah'], 2) }}</td>
<td>{{ number_format($item['onepercent'], 2) }}</td>
@if($rebetAnnur)
<td>{{ number_format($item['diskaun_rebate'] ?? 0, 2) }}</td>
@endif
{{-- <td>{{ number_format($item['bakiupah'], 2) }}</td> --}}
<td>
@php
@@ -371,6 +382,7 @@
$totalbayaranteller += $item['bakipjm']; //
$totalujrahteller += $item['ujrah'];
$totalonepercentteller += $item['onepercent'];
$totaldiskaunrebateteller += $item['diskaun_rebate'] ?? 0;
@endphp
@php
@@ -388,6 +400,9 @@
<td><b>{{ number_format($totalpinjamanteller, 2) }} </b></td>
<td><b>{{ number_format($totalujrahteller, 2) }} </b></td>
<td><b>{{ number_format($totalonepercentteller, 2) }} </b></td>
@if($rebetAnnur)
<td><b>{{ number_format($totaldiskaunrebateteller ?? 0, 2) }} </b></td>
@endif
<td><b>{{ number_format($totalupahteller, 2) }} </b></td>
<td><b>{{ number_format($totalbayaranteller, 2) }} </b></td>
<td><b>{{ number_format($totaljumlahteller, 2) }} </b></td>
@@ -402,6 +417,7 @@
$totaljumlahteller = 0;
$totalujrahteller = 0;
$totalonepercentteller = 0;
$totaldiskaunrebateteller = 0;
@endphp
@endif
@else
@@ -412,6 +428,9 @@
<td><b>{{ number_format($totalpinjamanteller, 2) }} </b></td>
<td><b>{{ number_format($totalujrahteller, 2) }} </b></td>
<td><b>{{ number_format($totalonepercentteller, 2) }} </b></td>
@if($rebetAnnur)
<td><b>{{ number_format($totaldiskaunrebateteller ?? 0, 2) }} </b></td>
@endif
<td><b>{{ number_format($totalupahteller, 2) }} </b></td>
<td><b>{{ number_format($totalbayaranteller, 2) }} </b></td>
<td><b>{{ number_format($totaljumlahteller, 2) }} </b></td>
@@ -652,6 +652,13 @@
{{ number_format($penebusan['onepercent'] + $penebusan['ujrah'], 2) }}</strong>
</td>
</tr>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<tr>
<td style="width:10px">(-) Diskaun Promosi</td>
<td style="width:10px">: RM
{{ number_format($gadai['diskaun'] ?? 0, 2) }} </td>
</tr>
@endif
<tr>
<td></td>
</tr>
@@ -885,6 +892,13 @@
{{ number_format($penebusan['onepercent'] + $penebusan['ujrah'], 2) }}</strong>
</td>
</tr>
@if(!empty(($cawangan_info['features'] ?? [])['rebet_annur']))
<tr>
<td style="width:10px">(-) Diskaun Promosi</td>
<td style="width:10px">: RM
{{ number_format($gadai['diskaun'] ?? 0, 2) }} </td>
</tr>
@endif
<tr>
<td></td>
</tr>
+9 -4
View File
@@ -6,6 +6,9 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function () {
if (window.self !== window.top) {
return;
}
window.print();
window.onfocus=function(){ window.close();}
});
@@ -553,9 +556,11 @@
</table>
@php
$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'];
$commodityPrint = app(\App\Services\CommodityCertificate::class)
->forReference($gadai_detail['norujukan'], $gadai_detail['pinjaman'] ?? 0);
$komuditi = $commodityPrint['komoditi'];
$product = $commodityPrint['product'];
$kuantiti = $commodityPrint['kuantiti'];
@endphp
<table style="margin-bottom:10px;" id="keuntungan">
@@ -573,7 +578,7 @@
<tr>
<td>2</td>
<td>Kod Produk / Diskripsi</td>
<td>{{ $product['kodproduk'] . ' / ' . $product['description'] }}</td>
<td>{{ $product['product_code'] ?? '' }}</td>
<td>5</td>
<td>Nilai</td>
<td>{{ 'RM ' . number_format($gadai_detail['pinjaman'],2) }}</td>
+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;">
+25 -4
View File
@@ -395,6 +395,9 @@
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script>
$(document).ready(function() {
if (window.self !== window.top) {
return;
}
window.print();
window.onfocus = function() {
window.close();
@@ -612,12 +615,12 @@
<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>
<td>Kod Produk / Deskripsi</td>
<td>{{ $product['kodproduk'] . ' / ' . $product['description'] }}</td>
<td>{{ $product['product_code'] ?? '' }}</td>
<td class="text-center">5</td>
<td>harga kos</td>
<td>{{ 'RM' . number_format($gadai_detail['pinjaman'], 2) }}</td>
@@ -1846,12 +1849,12 @@
<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>
<td>Kod Produk / Deskripsi</td>
<td>{{ $product['kodproduk'] . ' / ' . $product['description'] }}</td>
<td>{{ $product['product_code'] ?? '' }}</td>
<td class="text-center">5</td>
<td>Harga Kos</td>
<td>{{ 'RM' . number_format($gadai_detail['pinjaman'], 2) }}</td>
@@ -2109,6 +2112,24 @@
</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
<div style="border: 1px solid black; padding: 10px 12px; margin-bottom: 10px; break-inside: avoid;">
<div style="font-size: 13px; font-weight: bold; margin-bottom: 12px;">CARA TERIMA BAYARAN</div>
<div style="display: flex; align-items: center; gap: 40px; font-size: 12px; font-weight: bold;">
<span style="display: inline-flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 14px; height: 14px; border: 1px solid black; text-align: center; line-height: 12px;">@if ($caraTerimaBayaran === 'TUNAI')&#10003;@endif</span>
TUNAI
</span>
<span style="display: inline-flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 14px; height: 14px; border: 1px solid black; text-align: center; line-height: 12px;">@if ($caraTerimaBayaran === 'ONLINE')&#10003;@endif</span>
KREDIT KE AKAUN
</span>
</div>
</div>
@endif
{{-- HEADER SECTION --}}
<table style="width: 100%; margin-top: 0;">
<tr style="vertical-align: top;">
@@ -0,0 +1,6 @@
@php
$caraTerimaTag = strtoupper(trim((string) ($caraTerima ?? '')));
$caraTerimaLabel = ['TUNAI' => 'TUNAI', 'ONLINE' => 'ONLINE'][$caraTerimaTag] ?? 'TIDAK DINYATAKAN';
$caraTerimaWarna = ['TUNAI' => '#166534', 'ONLINE' => '#1e40af'][$caraTerimaTag] ?? '#555555';
@endphp
<span title="Cara Terima Bayaran" style="display: inline-block; margin-top: 3px; padding: 2px 5px; border: 1px solid {{ $caraTerimaWarna }}; border-radius: 3px; color: {{ $caraTerimaWarna }}; font-size: 9px; font-weight: bold; line-height: 1.2; white-space: nowrap;">{{ $caraTerimaLabel }}</span>
+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

Some files were not shown because too many files have changed in this diff Show More