10 Commits

Author SHA1 Message Date
afrina 510b118a23 feat: mandatory IC scan upload on customer register and customer_info 2026-10-27 13:31:33 +08:00
afrina 05f1700087 fix: buang klausa wakalah dan infaq baki lelongan RM10 pada cetakan SAG 2026-10-27 12:24:18 +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
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
30 changed files with 780 additions and 334 deletions
+2
View File
@@ -50,3 +50,5 @@ MYKAD_READER_PORT=17321
MYKAD_READER_ENABLED_CAWANGAN=''
INTERNAL_NOTIFICATION_SECRET=
LOGVIEWER_PASSWORD=
+1
View File
@@ -7,6 +7,7 @@ APP_URL=https://ujrah.erahn.com.my
API_URL=https://apiujrah.erahn.com.my
LOG_CHANNEL=stack
LOGVIEWER_PASSWORD="$2y$10$k7eiUyZdm3BruwAaT1gbmelTm.PPgZptOEQT1nlBjyiR84a4KBMlm"
DB_CONNECTION=mysql
DB_HOST=192.168.1.216
+2 -5
View File
@@ -4,17 +4,14 @@ APP_VERSION=v2.2-staging
APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8=
APP_DEBUG=true
APP_URL=https://staging.erahn.com.my
API_URL=https://api-staging.erahn.com.my
HTTP_PORT=8091
MYSQL_PUBLISH_PORT=3309
API_URL=https://api-staging.erahn.com.my
API_URL_KOOP=https://api-staging.erahn.com.my
API_URL_PKB=https://api-staging.erahn.com.my
API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
LOG_LEVEL=debug
LOGVIEWER_PASSWORD=test123
DB_CONNECTION=mysql
DB_HOST=mysql
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:WRTHcFTq5YARKVdA8YDUzsh9Ue8vD63ivgZgXNNBiGo=
APP_DEBUG=true
APP_URL=http://ujrahkopkb.erahn.com.my
API_URL=http://apiujrah-kopkb.erahn.com.my
#API_ARRAHNBID=http://lelong.test/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=kaunter_erahn
DB_USERNAME=spider
DB_PASSWORD=G0dz!ll4
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
+37 -18
View File
@@ -1,6 +1,9 @@
name: Deploy to Staging
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
image_tag:
@@ -18,10 +21,22 @@ jobs:
runs-on: host
steps:
- name: Resolve image tag
id: resolve
run: |
set -e
if [ "${{ gitea.event_name }}" = "workflow_dispatch" ]; then
IMAGE_TAG="${{ inputs.image_tag }}"
else
IMAGE_TAG="${{ gitea.ref_name }}"
fi
echo "IMAGE_TAG=${IMAGE_TAG}" >> "$GITHUB_OUTPUT"
echo "Deploying image tag: ${IMAGE_TAG}"
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.image_tag }}
ref: ${{ steps.resolve.outputs.IMAGE_TAG }}
- name: Login to Docker registry
run: |
@@ -33,28 +48,35 @@ jobs:
- name: Verify image exists in registry
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
IMAGE="${APP_IMAGE}:${IMAGE_TAG}"
echo "Checking if image exists: ${IMAGE}"
if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "ERROR: Image ${IMAGE} does not exist in registry!"
echo "Run the Build Docker Image workflow first."
exit 1
fi
echo "✓ ${IMAGE} found"
# Tag pushes build in parallel — wait for the image to appear
for i in $(seq 1 30); do
if docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then
echo "✓ ${IMAGE} found"
exit 0
fi
echo "Image not ready yet (attempt ${i}/30), waiting 20s..."
sleep 20
done
echo "ERROR: Image ${IMAGE} does not exist in registry after waiting!"
echo "Run the Build Docker Image workflow first."
exit 1
- name: Pull Docker image
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
docker pull "${APP_IMAGE}:${IMAGE_TAG}"
echo "✓ Image pulled successfully"
- name: Sync compose file and deploy
run: |
set -e
IMAGE_TAG="${{ inputs.image_tag }}"
IMAGE_TAG="${{ steps.resolve.outputs.IMAGE_TAG }}"
mkdir -p "${DEPLOY_DIR}/docker/nginx" "${DEPLOY_DIR}/public"
cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml"
@@ -62,15 +84,12 @@ jobs:
find "${DEPLOY_DIR}/public" -mindepth 1 -delete
cp -R public/. "${DEPLOY_DIR}/public/"
if [ ! -f "${DEPLOY_DIR}/.env.staging" ]; then
if [ -f .env.staging ]; then
cp .env.staging "${DEPLOY_DIR}/.env.staging"
echo "Copied .env.staging from repo (first deploy). Edit ${DEPLOY_DIR}/.env.staging on the server if needed."
else
echo "ERROR: ${DEPLOY_DIR}/.env.staging is missing on the server"
exit 1
fi
if [ ! -f .env.staging ]; then
echo "ERROR: .env.staging is missing in the repository"
exit 1
fi
cp .env.staging "${DEPLOY_DIR}/.env.staging"
echo "✓ Synced .env.staging from repo"
cd "${DEPLOY_DIR}"
+16 -18
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();
]);
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::to('/');
return redirect('/');
}
public function GetRolesHarian(Request $request, $kodcaw){
@@ -312,6 +312,61 @@ class CustomersController extends Controller
return response()->json($response->json() ?? ['stored' => false], $response->status());
}
/**
* Upload scanned IC / passport document for the current customer.
*/
public function storeIcScan(Request $request)
{
$icno = preg_replace('/[^0-9A-Za-z]/', '', (string) ($request->input('icno') ?: Session::get('noic')));
if ($icno === '') {
return response()->json(['error' => 'No. IC diperlukan.', 'stored' => false], 422);
}
list($scanPayload, $scanError) = $this->scanIcPayloadFromRequest($request);
if ($scanError) {
return response()->json(['error' => $scanError, 'stored' => false], 422);
}
$response = Http::withOptions(['verify' => config('app.api_verify')])
->post(config('api.url') . '/api/customers/' . $icno . '/ic-scan', $scanPayload);
return response()->json($response->json() ?? ['stored' => false], $response->status());
}
public function showIcScan(Request $request)
{
$icno = preg_replace('/[^0-9A-Za-z]/', '', (string) ($request->query('icno') ?: Session::get('noic')));
if ($icno === '') {
abort(404);
}
$response = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/customers/' . $icno . '/ic-scan');
$payload = $response->json();
$file = is_array($payload) ? ($payload['file'] ?? null) : null;
if (!$response->successful() || !$file || empty($file['data'])) {
abort(404);
}
$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($file['data']);
}
$type = $file['type'] ?? 'application/octet-stream';
$filename = $file['filename'] ?? 'scan-ic';
return response($binary, 200, [
'Content-Type' => $type,
'Content-Disposition' => 'inline; filename="' . $filename . '"',
]);
}
/**
* Show the form for creating a new resource.
*
@@ -330,6 +385,11 @@ class CustomersController extends Controller
*/
public function store(Request $request)
{
list($scanPayload, $scanError) = $this->scanIcPayloadFromRequest($request);
if ($scanError) {
return redirect()->back()->with('error', $scanError)->withInput();
}
if ($request->idtype == 'nokp') {
$nokpbaru = $request->noic;
$nokplama = '';
@@ -415,6 +475,10 @@ class CustomersController extends Controller
$customer_payload['photo_mime'] = $request->input('photo_mime') ?: 'image/jpeg';
}
$customer_payload['file_base64'] = $scanPayload['file_base64'];
$customer_payload['file_mime'] = $scanPayload['file_mime'];
$customer_payload['filename'] = $scanPayload['filename'];
$customer_info = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/customers/create/' . $auth_user['cawangan'], $customer_payload);
Session::put('customer_info', $customer_info);
@@ -692,6 +756,34 @@ class CustomersController extends Controller
]);
}
private function scanIcPayloadFromRequest(Request $request)
{
if (!$request->hasFile('scan_ic')) {
return [null, 'Sila muat naik scan dokumen IC / Passport.'];
}
$file = $request->file('scan_ic');
if (!$file->isValid()) {
return [null, 'Fail scan IC tidak sah.'];
}
if ($file->getSize() > 4 * 1024 * 1024) {
return [null, 'Saiz fail scan IC maksimum 4MB.'];
}
$mime = strtolower((string) ($file->getMimeType() ?: ''));
$allowed = ['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'];
if (!in_array($mime, $allowed, true)) {
return [null, 'Format fail scan IC mestilah JPG, PNG atau PDF.'];
}
return [[
'file_base64' => base64_encode(file_get_contents($file->getRealPath())),
'file_mime' => $mime,
'filename' => $file->getClientOriginalName(),
], null];
}
private function printBlank($value)
{
if ($value === null) {
+8 -1
View File
@@ -164,6 +164,9 @@ class GadaianController extends Controller
$noic = $request->noic;
$customers_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/customers/' . $noic)->json();
$customer_info = $customers_info;
if (!is_array($customer_info) || empty($customer_info['has_scan_ic'])) {
return redirect()->route('customer_info')->with('error', 'Sila muat naik scan dokumen IC sebelum daftar pembiayaan.');
}
$limit_pinjaman_semasa = 0;
$auth_user = Auth::user();
if ($request->session()->get('page_reload') == null) {
@@ -680,7 +683,11 @@ class GadaianController extends Controller
$kuantiti = $gadai_detail['pinjaman'] / $komoditi['nilai'];
if ($gadai_detail['tagbaru'] == 1) {
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan'));
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact('komoditi', 'product', 'kuantiti', 'gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kadar_ujrah', 'kakitangan'))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
} else {
return view('print.sag', compact('gadai_detail', 'customer_detail', 'marhun_detail', 'cawangan_detail', 'api_url', 'pembayaran_segera', 'auth_username', 'mutu', 'kakitangan'));
}
@@ -156,7 +156,6 @@ class PembiayaanOnlineController extends Controller
'kodbank' => $request->input('kodbank'),
'noakaun' => $request->input('noakaun'),
'nama_bank' => $request->input('nama_bank'),
'nama_akaun' => $request->input('nama_akaun'),
'nota' => $request->input('nota'),
'update_customer_bank' => true,
];
@@ -174,6 +173,19 @@ class PembiayaanOnlineController extends Controller
$payload['bukti_akaun_type'] = $file->getMimeType() ?: 'application/octet-stream';
}
if ($request->hasFile('borang')) {
$file = $request->file('borang');
if (!$file->isValid()) {
return redirect()->back()->with('error', 'Fail borang pengesahan tidak sah.');
}
if ($file->getSize() > 4 * 1024 * 1024) {
return redirect()->back()->with('error', 'Saiz fail maksimum 4MB.');
}
$payload['borang_data'] = base64_encode(file_get_contents($file->getRealPath()));
$payload['borang_filename'] = $file->getClientOriginalName();
$payload['borang_type'] = $file->getMimeType() ?: 'application/octet-stream';
}
$res = $this->api()
->asForm()
->put($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id, $payload);
@@ -394,6 +406,22 @@ class PembiayaanOnlineController extends Controller
return $this->streamBase64File($file, 'slip-bank-' . $id);
}
/**
* PC: view uploaded Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian.
*/
public function viewBorang($id)
{
$this->abortUnlessPembiayaanOnline();
return $this->streamLineFile(
$this->kodcaw(),
$id,
'/borang',
'Borang pengesahan tidak dijumpai.',
'Fail borang pengesahan kosong.',
'borang-pengesahan-' . $id
);
}
/**
* PC: view SAG for a queue line (does not mark SAG as printed).
*/
@@ -426,6 +454,21 @@ class PembiayaanOnlineController extends Controller
return $this->streamBase64File($file, 'slip-bank-' . $id);
}
/**
* Operasi: view uploaded Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian.
*/
public function operasiViewBorang($kodcaw, $id)
{
return $this->streamLineFile(
$kodcaw,
$id,
'/borang',
'Borang pengesahan tidak dijumpai.',
'Fail borang pengesahan kosong.',
'borang-pengesahan-' . $id
);
}
/**
* Operasi: view SAG for a batch line.
*/
@@ -434,6 +477,26 @@ class PembiayaanOnlineController extends Controller
return $this->renderLineSag($kodcaw, $id);
}
protected function streamLineFile($kodcaw, $id, $path, $missingMessage, $emptyMessage, $fallbackName)
{
$res = $this->api()
->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . $path);
if (!$res->successful()) {
$body = $res->json();
$msg = is_array($body) && isset($body['message']) ? $body['message'] : $missingMessage;
return redirect()->back()->with('error', $msg);
}
$payload = $res->json();
$file = $payload['file'] ?? null;
if (!$file || empty($file['data'])) {
return redirect()->back()->with('error', $emptyMessage);
}
return $this->streamBase64File($file, $fallbackName);
}
protected function streamBase64File(array $file, $fallbackName)
{
$raw = $file['data'];
@@ -533,6 +596,10 @@ class PembiayaanOnlineController extends Controller
$kuantiti = $nilai > 0 ? ((float) ($gadai_detail['pinjaman'] ?? 0) / $nilai) : 0;
if (($gadai_detail['tagbaru'] ?? 0) == 1) {
$cawangan_info = $this->api()
->get($api_url . '/api/cawangan/' . $kodcaw)->json();
$pembiayaanOnlineEnabled = !empty(($cawangan_info['features'] ?? [])['pembiayaan_online']);
return view('print.sag_ujrah_3', compact(
'komoditi',
'product',
@@ -547,7 +614,7 @@ class PembiayaanOnlineController extends Controller
'mutu',
'kadar_ujrah',
'kakitangan'
));
))->with('pembiayaanOnlineEnabled', $pembiayaanOnlineEnabled);
}
return view('print.sag', compact(
+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
@@ -45,6 +45,12 @@ class RoleAccess
}
break;
case 'masteradmin':
if ($userRole == 'masteradmin') {
return $next($request);
}
break;
case 'penilai':
if ($userRole == 'penilai') {
return $next($request);
+1
View File
@@ -1,6 +1,7 @@
<?php
return [
'password' => env('LOGVIEWER_PASSWORD'),
/*
|--------------------------------------------------------------------------
| Pattern and storage path settings
+3 -10
View File
@@ -5,6 +5,8 @@ services:
dockerfile: docker/php/Dockerfile
target: development
working_dir: /var/www/html
ports:
- "8081:8000"
volumes:
- ./:/var/www/html:cached
environment:
@@ -15,16 +17,7 @@ services:
- "host.docker.internal:host-gateway"
depends_on:
- mysql
web:
image: nginx:1.25-alpine
ports:
- "8081:80"
volumes:
- ./:/var/www/html:cached
- ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app
command: ["php", "artisan", "serve", "--host=0.0.0.0", "--port=8000"]
mysql:
image: mysql:8.0
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone2
DB_PORT=3306
DB_DATABASE=kaunterpkb
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.kgmk.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone_franchise
DB_PORT=3306
DB_DATABASE=kaunter_koopguru
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
-50
View File
@@ -1,50 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=base64:FFRogE6mJj8FaDSSHl/KpXpzqi2M+P4plxVSRgVM8JI=
APP_DEBUG=true
APP_URL=http://kaunterv2.localhost
API_URL=http://apiv2.krk.localhost
API_ARRAHNBID=http://lelong.localhost/api/
LOG_CHANNEL=stack
DB_CONNECTION=mysql
DB_HOST=database_zone_franchise
DB_PORT=3306
DB_DATABASE=kaunter_krk
DB_USERNAME=root
DB_PASSWORD=secret
BROADCAST_DRIVER=pusher
CACHE_DRIVER=file
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
PUSHER_APP_ID=1132718
PUSHER_APP_KEY=26b0ec2f297f94b7aa9b
PUSHER_APP_SECRET=421b61fb5a8ecce477a6
MAIL_MAILER=smtp
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS=null
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
MYKAD_READER_PORT=8085
MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED
@@ -0,0 +1,19 @@
<div class="ic-scan-register-field px-3 pb-2">
<div class="row form-group mb-0">
<div class="col-md-4">
Scan IC / Passport <span class="text-danger">*</span>
</div>
<div class="col-md-8">
<input class="form-control-file"
type="file"
name="scan_ic"
id="{{ $scanFieldId }}"
accept=".jpg,.jpeg,.png,.pdf,image/jpeg,image/png,application/pdf">
<small class="form-text text-muted">Wajib. JPG, PNG atau PDF (maks 4MB).</small>
<div class="ic-scan-register-preview mt-2 d-none">
<img src="" alt="Preview scan IC" style="max-width:180px;max-height:120px;object-fit:contain;border:1px solid #d0d5dd;border-radius:4px;background:#fff;">
<span class="ic-scan-register-filename small text-muted d-block"></span>
</div>
</div>
</div>
</div>
@@ -0,0 +1,80 @@
<script>
function requireScanIcFromForm(formEl, submitSelector) {
var input = formEl ? formEl.querySelector('input[name="scan_ic"]') : null;
var file = input && input.files && input.files[0] ? input.files[0] : null;
if (!file) {
$(submitSelector).prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: 'Sila muat naik scan dokumen IC / Passport'
});
if (typeof event !== 'undefined' && event) {
event.preventDefault();
}
return false;
}
if (file.size > 4 * 1024 * 1024) {
$(submitSelector).prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: 'Saiz fail scan IC maksimum 4MB'
});
if (typeof event !== 'undefined' && event) {
event.preventDefault();
}
return false;
}
var type = (file.type || '').toLowerCase();
if (['image/jpeg', 'image/jpg', 'image/png', 'application/pdf'].indexOf(type) === -1) {
$(submitSelector).prop('disabled', false);
Swal.fire({
icon: 'error',
title: 'Amaran!',
text: 'Format fail mestilah JPG, PNG atau PDF'
});
if (typeof event !== 'undefined' && event) {
event.preventDefault();
}
return false;
}
return true;
}
$(document).on('change', 'input[name="scan_ic"]', function () {
var file = this.files && this.files[0] ? this.files[0] : null;
var wrap = this.closest('.ic-scan-register-field');
if (!wrap) {
return;
}
var preview = wrap.querySelector('.ic-scan-register-preview');
var img = wrap.querySelector('.ic-scan-register-preview img');
var nameEl = wrap.querySelector('.ic-scan-register-filename');
if (!file) {
if (preview) {
preview.classList.add('d-none');
}
return;
}
if (preview) {
preview.classList.remove('d-none');
}
if (nameEl) {
nameEl.textContent = file.name;
}
if (!img) {
return;
}
if ((file.type || '') === 'application/pdf') {
img.classList.add('d-none');
return;
}
img.classList.remove('d-none');
var reader = new FileReader();
reader.onload = function (e) {
img.src = e.target.result;
};
reader.readAsDataURL(file);
});
</script>
@@ -0,0 +1,204 @@
@php
$hasScanIc = !empty($customer_info['has_scan_ic']);
$scanIcMime = strtolower((string) ($customer_info['scan_ic_mime'] ?? ''));
$scanIcIsPdf = strpos($scanIcMime, 'pdf') !== false;
$scanIcNoic = !empty($customer_info['kpbaru']) ? $customer_info['kpbaru'] : ($customer_info['kplama'] ?? '');
$scanIcPreviewUrl = $hasScanIc
? route('customer.showIcScan', ['icno' => $scanIcNoic, 't' => $customer_info['scan_ic'] ?? time()])
: '';
@endphp
<style>
.ic-scan-card {
border: 1px dashed #c5cdd6;
border-radius: 8px;
background: #f8f9fb;
padding: 16px;
margin: 0 15px 18px;
}
.ic-scan-card.is-missing {
border-color: #e0a800;
background: #fff8e8;
}
.ic-scan-preview {
max-width: 220px;
max-height: 140px;
object-fit: contain;
border: 1px solid #d0d5dd;
border-radius: 4px;
background: #fff;
}
</style>
<div id="ic-scan-card" class="ic-scan-card {{ $hasScanIc ? '' : 'is-missing' }}">
<div class="d-flex flex-wrap align-items-start justify-content-between">
<div>
<b>Scan Dokumen IC / Passport <span class="text-danger">*</span></b>
<div class="text-muted small mt-1">
Wajib dimuat naik sebelum daftar pembiayaan. Format JPG, PNG atau PDF (maks 4MB).
</div>
<div id="ic-scan-status" class="mt-2">
@if ($hasScanIc)
<span class="badge badge-success">Telah dimuat naik</span>
@else
<span class="badge badge-warning">Belum dimuat naik</span>
@endif
</div>
</div>
<div class="mt-2 mt-md-0">
<input type="file" id="ic_scan_file" accept=".jpg,.jpeg,.png,.pdf,image/jpeg,image/png,application/pdf" class="d-none">
<button type="button" class="btn btn-outline-primary btn-sm" id="ic_scan_pick">
<i class="fas fa-upload mr-1"></i>{{ $hasScanIc ? 'Muat naik semula' : 'Pilih fail' }}
</button>
<button type="button" class="btn btn-primary btn-sm d-none" id="ic_scan_upload">Muat Naik</button>
</div>
</div>
<div id="ic-scan-preview-wrap" class="mt-3 {{ $hasScanIc ? '' : 'd-none' }}">
@if ($hasScanIc && $scanIcIsPdf)
<a id="ic-scan-pdf-link" href="{{ $scanIcPreviewUrl }}" target="_blank" rel="noopener">Lihat fail PDF</a>
<img id="ic-scan-preview-img" src="" alt="Preview scan IC" class="ic-scan-preview d-none">
@else
<a id="ic-scan-pdf-link" href="{{ $scanIcPreviewUrl }}" target="_blank" rel="noopener" class="{{ $hasScanIc ? 'd-none' : 'd-none' }}">Lihat fail PDF</a>
<img id="ic-scan-preview-img"
src="{{ $hasScanIc ? $scanIcPreviewUrl : '' }}"
alt="Preview scan IC"
class="ic-scan-preview {{ $hasScanIc ? '' : 'd-none' }}">
@endif
<div id="ic-scan-filename" class="small text-muted mt-1">{{ $customer_info['scan_ic_filename'] ?? '' }}</div>
</div>
</div>
<script>
window.hasScanIc = @json($hasScanIc);
(function() {
var fileInput = document.getElementById('ic_scan_file');
var pickBtn = document.getElementById('ic_scan_pick');
var uploadBtn = document.getElementById('ic_scan_upload');
var previewWrap = document.getElementById('ic-scan-preview-wrap');
var previewImg = document.getElementById('ic-scan-preview-img');
var pdfLink = document.getElementById('ic-scan-pdf-link');
var filenameEl = document.getElementById('ic-scan-filename');
var statusEl = document.getElementById('ic-scan-status');
var card = document.getElementById('ic-scan-card');
var selectedFile = null;
var storeUrl = @json(route('customer.storeIcScan'));
var previewUrl = @json(route('customer.showIcScan', ['icno' => $scanIcNoic]));
var csrf = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
var icno = @json($scanIcNoic);
function showUploadedState(filename, mime) {
window.hasScanIc = true;
card.classList.remove('is-missing');
statusEl.innerHTML = '<span class="badge badge-success">Telah dimuat naik</span>';
pickBtn.innerHTML = '<i class="fas fa-upload mr-1"></i>Muat naik semula';
uploadBtn.classList.add('d-none');
previewWrap.classList.remove('d-none');
filenameEl.textContent = filename || '';
var url = previewUrl + (previewUrl.indexOf('?') === -1 ? '?' : '&') + 't=' + Date.now();
if ((mime || '').indexOf('pdf') !== -1) {
previewImg.classList.add('d-none');
pdfLink.classList.remove('d-none');
pdfLink.setAttribute('href', url);
} else {
pdfLink.classList.add('d-none');
previewImg.classList.remove('d-none');
previewImg.src = url;
}
}
pickBtn.addEventListener('click', function() {
fileInput.click();
});
fileInput.addEventListener('change', function() {
selectedFile = fileInput.files && fileInput.files[0] ? fileInput.files[0] : null;
if (!selectedFile) {
uploadBtn.classList.add('d-none');
return;
}
if (selectedFile.size > 4 * 1024 * 1024) {
selectedFile = null;
fileInput.value = '';
Swal.fire({
icon: 'error',
title: 'Fail terlalu besar',
text: 'Saiz fail maksimum 4MB.'
});
return;
}
uploadBtn.classList.remove('d-none');
previewWrap.classList.remove('d-none');
filenameEl.textContent = selectedFile.name;
if (selectedFile.type === 'application/pdf') {
previewImg.classList.add('d-none');
pdfLink.classList.remove('d-none');
pdfLink.setAttribute('href', URL.createObjectURL(selectedFile));
} else {
pdfLink.classList.add('d-none');
previewImg.classList.remove('d-none');
var reader = new FileReader();
reader.onload = function(e) {
previewImg.src = e.target.result;
};
reader.readAsDataURL(selectedFile);
}
});
uploadBtn.addEventListener('click', function() {
if (!selectedFile) {
Swal.fire({
icon: 'warning',
title: 'Tiada fail',
text: 'Sila pilih scan dokumen IC terlebih dahulu.'
});
return;
}
var formData = new FormData();
formData.append('icno', icno);
formData.append('scan_ic', selectedFile);
uploadBtn.disabled = true;
uploadBtn.textContent = 'Memuat naik...';
fetch(storeUrl, {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json'
},
body: formData
}).then(function(res) {
return res.json().then(function(body) {
return { ok: res.ok, body: body };
});
}).then(function(result) {
uploadBtn.disabled = false;
uploadBtn.textContent = 'Muat Naik';
if (!result.ok || !result.body || !result.body.stored) {
throw new Error((result.body && (result.body.error || result.body.message)) || 'Gagal memuat naik scan IC.');
}
showUploadedState(selectedFile.name, selectedFile.type);
selectedFile = null;
fileInput.value = '';
Swal.fire({
icon: 'success',
title: 'Berjaya',
text: 'Scan dokumen IC telah dimuat naik.'
});
}).catch(function(err) {
uploadBtn.disabled = false;
uploadBtn.textContent = 'Muat Naik';
Swal.fire({
icon: 'error',
title: 'Tidak berjaya',
text: err.message || 'Gagal memuat naik scan IC.'
});
});
});
})();
</script>
+27 -8
View File
@@ -21,6 +21,9 @@
<div class="card">
<div class="card-body">
@if(session()->has('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="text-center">
<h5 class="text-uppercase">masukkan nombor kad pengenalan</h5>
<form name="frmsearch" onsubmit="verifynokp()" action="{{ route('customer.search') }}" method="GET">
@@ -84,7 +87,7 @@
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
</div>
<div class="modal-body">
<form action="{{ route('customer.store') }}" onsubmit="verifycustomer()" method="POST">
<form action="{{ route('customer.store') }}" onsubmit="verifycustomer()" method="POST" enctype="multipart/form-data">
@csrf
<div class="row form-group">
<div class="col-md-6">
@@ -195,7 +198,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_manual'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="nokp">
@@ -210,7 +213,7 @@
<div class="modal fade" id="daftar_pelanggan_mykad_reader" tabindex="-1" data-backdrop="static" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="form-mykad-reader" action="{{ route('customer.store') }}" onsubmit="verifycustomerMyKadReader()" method="POST">
<form id="form-mykad-reader" action="{{ route('customer.store') }}" onsubmit="verifycustomerMyKadReader()" method="POST" enctype="multipart/form-data">
@csrf
<div class="modal-header">
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
@@ -336,7 +339,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_mykad'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="nokp">
@@ -355,7 +358,7 @@
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
</div>
<div class="modal-body">
<form action="{{ route('customer.store') }}" onsubmit="verifycustomerpassport()" method="POST">
<form action="{{ route('customer.store') }}" onsubmit="verifycustomerpassport()" method="POST" enctype="multipart/form-data">
@csrf
<div class="row form-group">
<div class="col-md-6">
@@ -461,6 +464,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_passport'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="passport">
@@ -474,6 +478,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_script')
<script>
var api_url = "{{ api_url_for_browser() }}";
var search = "<?php echo $search ?>";
@@ -550,15 +555,20 @@
file.trigger("click");
});
$('input[type="file"]').change(function(e) {
if (this.name === 'scan_ic' || !e.target.files || !e.target.files[0]) {
return;
}
var fileName = e.target.files[0].name;
$("#file").val(fileName);
var preview = document.getElementById("preview");
if (!preview) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
// get loaded data and render thumbnail.
document.getElementById("preview").src = e.target.result;
preview.src = e.target.result;
};
// read the image file as a data URL.
reader.readAsDataURL(this.files[0]);
});
@@ -808,6 +818,9 @@
}
}
}
if (!requireScanIcFromForm(document.querySelector('#daftar_pelanggan form'), '#daftar_pelanggan #terima_ic')) {
return false;
}
return true;
}
@@ -950,6 +963,9 @@
}
}
}
if (!requireScanIcFromForm(document.getElementById('form-mykad-reader'), '#daftar_pelanggan_mykad_reader #terima_ic')) {
return false;
}
return true;
}
@@ -1090,6 +1106,9 @@
}
}
}
if (!requireScanIcFromForm(document.querySelector('#passport_pelanggan form'), '#terima_passport')) {
return false;
}
return true;
}
@@ -24,6 +24,9 @@
@if(session()->has('message'))
<div class="alert alert-success">{{ session('message') }}</div>
@endif
@if(session()->has('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if(!empty($customer_info['photo_base64']))
<div class="form-group row col-md-12">
@@ -37,6 +40,8 @@
</div>
@endif
@include('customers.partials.ic_scan_upload')
<div class="form-group row col-md-12">
<div class="col-md-6">
<div class="form-group row">
@@ -443,6 +448,14 @@
var tagarrahnbid = "{{ $cawangan_info['tagarrahnbid'] }}";
var min_komoditi = "{{ $min_komoditi }}";
$('#daftar_gadai').on('click',function(){
if (!window.hasScanIc) {
Swal.fire({
icon: 'warning',
title: 'Scan IC diperlukan',
text: 'Sila muat naik scan dokumen IC sebelum daftar pembiayaan.'
});
return;
}
$("#daftar_gadai").prop('disabled', true);
var noic = $('#noic').val();
var iclama = "{{ $customer_info['kplama'] }}";
+27 -7
View File
@@ -23,6 +23,9 @@
<div class="card">
<div class="card-body">
@if(session()->has('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<div class="text-center">
<h5 class="text-uppercase">masukkan nombor kad pengenalan</h5>
<form name="frmsearch" onsubmit="verifynokp()" action="{{ route('customer.search') }}" method="GET">
@@ -102,7 +105,7 @@
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
</div>
<div class="modal-body">
<form name="form-store" action="{{ route('customer.store') }}" onsubmit="verifycustomer()" method="POST">
<form name="form-store" action="{{ route('customer.store') }}" onsubmit="verifycustomer()" method="POST" enctype="multipart/form-data">
@csrf
<div class="row form-group">
<div class="col-md-6">
@@ -215,6 +218,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_manual'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="nokp">
@@ -229,7 +233,7 @@
<div class="modal fade" id="daftar_pelanggan_mykad_reader" tabindex="-1" data-backdrop="static" aria-labelledby="ModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="form-mykad-reader" action="{{ route('customer.store') }}" onsubmit="verifycustomerMyKadReader()" method="POST">
<form id="form-mykad-reader" action="{{ route('customer.store') }}" onsubmit="verifycustomerMyKadReader()" method="POST" enctype="multipart/form-data">
@csrf
<div class="modal-header">
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
@@ -356,7 +360,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_mykad'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="nokp">
@@ -375,7 +379,7 @@
<h5 class="modal-title" id="ModalLabel">Daftar Pelanggan</h5>
</div>
<div class="modal-body">
<form name="form-store" action="{{ route('customer.store') }}" onsubmit="verifycustomerpassport()" method="POST">
<form name="form-store" action="{{ route('customer.store') }}" onsubmit="verifycustomerpassport()" method="POST" enctype="multipart/form-data">
@csrf
<div class="row form-group">
<div class="col-md-6">
@@ -482,6 +486,7 @@
</div>
</div>
</div>
@include('customers.partials.ic_scan_register_field', ['scanFieldId' => 'scan_ic_passport'])
</div>
<div class="modal-footer">
<input type="text" hidden name="idtype" value="passport">
@@ -496,6 +501,7 @@
</div>
</div>
@include('customers.partials.ic_scan_register_script')
<script>
var isAnggota = @json($isAnggota ?? false);
var isPelanggan = @json($isPelanggan ?? false);
@@ -629,15 +635,20 @@
file.trigger("click");
});
$('input[type="file"]').change(function(e) {
if (this.name === 'scan_ic' || !e.target.files || !e.target.files[0]) {
return;
}
var fileName = e.target.files[0].name;
$("#file").val(fileName);
var preview = document.getElementById("preview");
if (!preview) {
return;
}
var reader = new FileReader();
reader.onload = function(e) {
// get loaded data and render thumbnail.
document.getElementById("preview").src = e.target.result;
preview.src = e.target.result;
};
// read the image file as a data URL.
reader.readAsDataURL(this.files[0]);
});
@@ -887,6 +898,9 @@
}
}
}
if (!requireScanIcFromForm(document.querySelector('#daftar_pelanggan form'), '#daftar_pelanggan #terima_ic')) {
return false;
}
return true;
}
@@ -1029,6 +1043,9 @@
}
}
}
if (!requireScanIcFromForm(document.getElementById('form-mykad-reader'), '#daftar_pelanggan_mykad_reader #terima_ic')) {
return false;
}
return true;
}
@@ -1169,6 +1186,9 @@
}
}
}
if (!requireScanIcFromForm(document.querySelector('#passport_pelanggan form'), '#terima_passport')) {
return false;
}
return true;
}
+21 -2
View File
@@ -25,6 +25,9 @@
@if(session()->has('message'))
<div class="alert alert-success">{{ session('message') }}</div>
@endif
@if(session()->has('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
@if(!empty($customer_info['photo_base64']))
<div class="form-group row col-md-12">
@@ -38,6 +41,8 @@
</div>
@endif
@include('customers.partials.ic_scan_upload')
<div class="form-group row col-md-12">
<div class="col-md-6">
<div class="form-group row">
@@ -723,6 +728,14 @@
if ($(this).prop('disabled')) {
return;
}
if (!window.hasScanIc) {
Swal.fire({
icon: 'warning',
title: 'Scan IC diperlukan',
text: 'Sila muat naik scan dokumen IC sebelum daftar pembiayaan.'
});
return;
}
$("#daftar_gadai").prop('disabled', true);
var bakiLelong = Number(@json((float) $bakilelong));
@@ -801,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' }
]
@@ -811,6 +827,7 @@
var xtawarruq = initErahnDatatable('#xtawarruq', {
columnDefs: [
{ targets: 2, visible: pembiayaanOnlineEnabled, className: pembiayaanOnlineEnabled ? '' : 'no-export' },
{ targets: 0, orderable: false, width: '40px' }
]
});
@@ -2067,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(3)').text();
let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
if (+harini !== +tarikhgadai2) {
@@ -2484,7 +2502,8 @@
}
if ($("input[name='options']:checked").val() == 'belum_tebus') {
let test1 = $('input[name="belum"]:checked').closest('tr').children('td:eq(3)').text();
let selectedRow = $('input[name="belum"]:checked').closest('tr');
let test1 = selectedRow.length ? selectedRow.closest('table').DataTable().row(selectedRow).data()[3] : '';
let tarikhgadai2 = new Date(test1);
tarikhgadai2.setHours(0, 0, 0, 0);
@@ -74,7 +74,7 @@
id="batch_lines"
class="table-sm text-center"
:filename="'Batch_' . ($batch['batch_no'] ?? $batch['id'])"
:columns="['Bil', 'Status', 'SAG', 'Pelanggan', 'No KP', 'Bank', 'No Akaun', 'Nama Akaun', 'Pinjaman (RM)', 'Rujukan Bayaran', 'Dokumen', 'Tindakan']"
:columns="['Bil', 'Status', 'SAG', 'Pelanggan', 'No KP', 'Bank', 'No Akaun', 'Pinjaman (RM)', 'Rujukan Bayaran', 'Dokumen', 'Tindakan']"
:skipExport="['Dokumen', 'Tindakan']"
>
@foreach($lines as $line)
@@ -94,7 +94,6 @@
<td>{{ $line['nokp'] ?? '-' }}</td>
<td>{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}</td>
<td>{{ $line['noakaun'] ?? '-' }}</td>
<td class="text-left">{{ $line['nama_akaun'] ?? '-' }}</td>
<td class="text-right">{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}</td>
<td>{{ $line['no_rujukan_bayaran'] ?? '-' }}</td>
<td class="text-nowrap">
@@ -113,6 +112,13 @@
@else
<span class="text-muted small">Tiada slip</span>
@endif
@if(!empty($line['borang_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.operasi.borang', ['kodcaw' => $kodcaw, 'id' => $line['id']]) }}"
target="_blank">
Borang Pengesahan
</a>
@endif
</td>
<td class="text-nowrap">
@if(in_array($st, ['IN_BATCH', 'OPS_REVIEW'], true))
@@ -70,6 +70,13 @@
@else
<span class="text-muted small">Tiada slip</span>
@endif
@if(!empty($line['borang_file_id']))
<a class="btn btn-sm btn-outline-primary"
href="{{ route('pembiayaan.online.pc.borang', ['id' => $line['id']]) }}"
target="_blank">
Borang Pengesahan
</a>
@endif
</td>
</tr>
@endforeach
@@ -274,11 +274,11 @@
data-nokp="{{ $line['nokp'] ?? '' }}"
data-namabank="{{ $line['nama_bank'] ?? $line['kodbank'] ?? '' }}"
data-noakaun="{{ $line['noakaun'] ?? '' }}"
data-namaakaun="{{ $line['nama_akaun'] ?? '' }}"
data-pinjaman="{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}"
data-nota="{{ $line['nota'] ?? '' }}"
data-sebab="{{ $line['reject_reason'] ?? '' }}"
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}">
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}"
data-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Semak
</button>
@endif
@@ -286,11 +286,11 @@
data-id="{{ $line['id'] }}"
data-kodbank="{{ $line['kodbank'] ?? '' }}"
data-noakaun="{{ $line['noakaun'] ?? '' }}"
data-namaakaun="{{ $line['nama_akaun'] ?? '' }}"
data-namabank="{{ $line['nama_bank'] ?? '' }}"
data-nota="{{ $line['nota'] ?? '' }}"
data-norujukan="{{ $line['norujukan'] ?? '' }}"
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}">
data-bukti-akaun="{{ $line['bukti_akaun_file_id'] ?? '' }}"
data-borang-file="{{ $line['borang_file_id'] ?? '' }}">
Edit
</button>
</td>
@@ -390,10 +390,6 @@
<label>No. Akaun</label>
<input type="text" name="noakaun" id="editNoakaun" class="form-control" required>
</div>
<div class="form-group">
<label>Nama Akaun</label>
<input type="text" name="nama_akaun" id="editNamaakaun" class="form-control">
</div>
<div class="form-group">
<label>Slip Bank / Bukti Akaun</label>
<input type="file" name="bukti_akaun" id="editBuktiAkaun" class="form-control-file"
@@ -405,6 +401,17 @@
</div>
<small class="text-muted">PDF / imej slip akaun bank pelanggan (maks 4MB)</small>
</div>
<div class="form-group">
<label>Borang Pengesahan Penerimaan Pembiayaan Secara Atas Talian (Online)</label>
<input type="file" name="borang" id="editBorang" class="form-control-file"
accept=".pdf,.jpg,.jpeg,.png,.gif">
<div id="editBorangExisting" class="mt-2" style="display:none;">
<a href="#" id="editBorangLink" class="btn btn-sm btn-outline-primary" target="_blank">
Lihat borang sedia ada
</a>
</div>
<small class="text-muted">PDF / imej borang yang ditandatangani pelanggan (maks 4MB). Kosongkan jika tidak mahu ganti fail sedia ada.</small>
</div>
<div class="form-group">
<label>Nota</label>
<textarea name="nota" id="editNota" class="form-control" rows="2"></textarea>
@@ -462,10 +469,6 @@
<th>No Akaun</th>
<td id="verifyNoakaun"></td>
</tr>
<tr>
<th>Nama Akaun</th>
<td id="verifyNamaakaun"></td>
</tr>
<tr>
<th>Pinjaman</th>
<td>RM <span id="verifyPinjaman"></span></td>
@@ -485,9 +488,12 @@
Saya sahkan maklumat pelanggan, bank dan amaun adalah betul.
</label>
</div>
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
<a href="#" id="verifyBuktiAkaunLink" class="btn btn-sm btn-outline-primary mb-2" target="_blank" style="display:none;">
Lihat Bukti Akaun Bank
</a>
<a href="#" id="verifyBorangLink" class="btn btn-sm btn-outline-primary" target="_blank" style="display:none;">
Lihat Borang Pengesahan
</a>
</div>
<div class="col-md-8">
<label class="d-block">Pratonton SAG</label>
@@ -576,7 +582,6 @@
$('#verifyNokp').text($(this).data('nokp') || '-');
$('#verifyBank').text($(this).data('namabank') || '-');
$('#verifyNoakaun').text($(this).data('noakaun') || '-');
$('#verifyNamaakaun').text($(this).data('namaakaun') || '-');
$('#verifyPinjaman').text($(this).data('pinjaman') || '0.00');
$('#verifyNota').text($(this).data('nota') || '-');
if (sebab) {
@@ -592,6 +597,11 @@
} else {
$('#verifyBuktiAkaunLink').hide();
}
if ($(this).data('borangFile')) {
$('#verifyBorangLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/borang").show();
} else {
$('#verifyBorangLink').hide();
}
$('#verifyModal').modal('show');
});
@@ -605,10 +615,10 @@
$('#editNorujukan').text($(this).data('norujukan'));
$('#editKodbank').val($(this).data('kodbank'));
$('#editNoakaun').val($(this).data('noakaun'));
$('#editNamaakaun').val($(this).data('namaakaun'));
$('#editNamabank').val($(this).data('namabank'));
$('#editNota').val($(this).data('nota'));
$('#editBuktiAkaun').val('');
$('#editBorang').val('');
$('#editSagFrame').attr('src', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/sag");
if ($(this).data('buktiAkaun')) {
$('#editBuktiAkaunLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/bukti-akaun");
@@ -616,6 +626,12 @@
} else {
$('#editBuktiAkaunExisting').hide();
}
if ($(this).data('borangFile')) {
$('#editBorangLink').attr('href', "{{ url('/pembiayaan-online/pc/lines') }}/" + id + "/borang");
$('#editBorangExisting').show();
} else {
$('#editBorangExisting').hide();
}
$('#editModal').modal('show');
});
@@ -258,7 +258,7 @@
<th style="width:13%">No. KP</th>
<th style="width:11%">Bank</th>
<th style="width:13%">No. Akaun</th>
<th>Nama Akaun</th>
<th>No. Telefon</th>
<th style="width:12%">Pinjaman (RM)</th>
</tr>
</thead>
@@ -271,7 +271,7 @@
<td class="num">{{ $line['nokp'] ?? '-' }}</td>
<td>{{ $line['nama_bank'] ?? ($line['kodbank'] ?? '-') }}</td>
<td class="num">{{ $line['noakaun'] ?? '-' }}</td>
<td>{{ $line['nama_akaun'] ?? '-' }}</td>
<td>{{ !empty($line['telefon']) ? $line['telefon'] : '-' }}</td>
<td class="amt">{{ number_format((float) ($line['pinjaman'] ?? 0), 2) }}</td>
</tr>
@empty
@@ -306,7 +306,7 @@
<div class="signs">
<div class="sign-box">
<div class="role">Tandatangan Operasi</div>
<div class="role">Disemak oleh</div>
<div class="sign-line">
<p>Nama: {{ $opsBy }}</p>
<p>Jawatan: Operasi</p>
@@ -314,10 +314,10 @@
</div>
</div>
<div class="sign-box">
<div class="role">Pengurus Besar</div>
<div class="role">Disahkan oleh</div>
<div class="sign-line">
<p>Nama:</p>
<p>Cop:</p>
<p>Jawatan:</p>
<p>Tarikh:</p>
</div>
</div>
+2 -39
View File
@@ -1305,45 +1305,6 @@
<tr>
<td colspan="1"></td>
<td colspan="1"><b>6.</b></td>
<td colspan="2"><b>Pelanggan dengan ini bersetuju, memberi kuasa dan melantik
Ar-Rahn sebagai wakil (wakil di bawah prinsip Wakalah) untuk menguruskan baki
hasil lelongan yang menjadi hak Pelanggan,
sekiranya ada, yang bernilai RM10.00 (Ringgit Malaysia: Sepuluh) atau kurang,
dan tidak dituntut oleh Pelanggan dalam tempoh tuntutan yang ditetapkan oleh
Ar-Rahn setelah usaha munasabah telah diambil untuk memaklumkan kepada
Pelanggan.</b></td>
</tr>
<tr>
<td colspan="1"></td>
<td colspan="1"><b>7.</b></td>
<td colspan="2">
<b>
Pelanggan selanjutnya bersetuju bahawa baki hasil lelongan tersebut boleh
disalurkan oleh Ar-Rahn, bagi pihak Pelanggan, sebagai infaq kepada mana-mana
institusi agama Islam, badan kebajikan atau institusi lain yang diluluskan
oleh Jawatankuasa Syariah dan/atau pihak berkuasa yang berkaitan, tanpa
sebarang liabiliti ke atas Ar-Rahn selepas penyaluran tersebut dibuat dengan
jumlah tidak melebihi RM10.00.
</b>
<div style="margin-top: 8px;">
<span
style="
display: inline-block;
width: 13px;
height: 13px;
border: 1px solid #000;
margin-right: 6px;
vertical-align: middle;
"></span>
<b>Tandakan (✓) jika tidak bersetuju.</b>
</div>
</td>
</tr>
<tr>
<td colspan="1"></td>
<td colspan="1"><b>8.</b></td>
<td colspan="2"><b>Sekiranya Pelanggan mempunyai sebarang pertanyaan atau
aduan, Pelanggan boleh menghubungi KoPKB sebagaimana berikut:</b></td>
</tr>
@@ -2112,6 +2073,7 @@
</div>
<div class="main-page" style="font-family: Arial, Helvetica, sans-serif;">
<div class="sub-page" style="font-size: 11px;">
@if ($pembiayaanOnlineEnabled ?? false)
@php
$caraTerimaBayaran = strtoupper(trim((string) ($gadai_detail['cara_terima'] ?? '')));
@endphp
@@ -2128,6 +2090,7 @@
</span>
</div>
</div>
@endif
{{-- HEADER SECTION --}}
<table style="width: 100%; margin-top: 0;">
<tr style="vertical-align: top;">
+11 -1
View File
@@ -63,7 +63,13 @@ Route::get('test', function () {
return "event test je";
});
Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index');
Route::get('logs', '\Rap2hpoutre\LaravelLogViewer\LogViewerController@index')
->middleware([
'auth',
'role:admin,masteradmin,operasi',
'throttle:10,1',
\App\Http\Middleware\LogViewerPassword::class,
]);
Route::get('change-password', ['as'=>'user.password','uses'=>'ChangePasswordController@index']);
Route::post('change-password', ['as'=>'change.password','uses'=>'ChangePasswordController@store']);
@@ -138,6 +144,8 @@ Route::get('/customer/otp',['as'=>'customer.otp','uses'=>'CustomersController@ge
Route::get('/getwakil',['as'=>'wakil.search','uses'=>'PenebusanController@getWakil'])->middleware('auth','teller');
Route::get('/cari',['as'=>'wakil.caricustomer','uses'=>'CustomersController@cari'])->middleware('auth','teller');
Route::post('/customer/mykad-photo',['as'=>'customer.storeMykadPhoto','uses'=>'CustomersController@storeMykadPhotoIfMissing'])->middleware('auth','teller');
Route::post('/customer/ic-scan',['as'=>'customer.storeIcScan','uses'=>'CustomersController@storeIcScan'])->middleware('auth','teller');
Route::get('/customer/ic-scan',['as'=>'customer.showIcScan','uses'=>'CustomersController@showIcScan'])->middleware('auth','teller');
Route::post('/createwakil',['as'=>'wakil.create','uses'=>'PenebusanController@createWakil'])->middleware('auth','teller');
Route::get('/firstwakil',['as'=>'wakil.first','uses'=>'PenebusanController@getfirstWakil'])->middleware('auth','teller');
Route::get('/history',['as'=>'gadai.history','uses'=>'GadaianController@history'])->middleware('auth','teller');
@@ -206,6 +214,7 @@ Route::put('/pembiayaan-online/pc/lines/{id}',['as'=>'pembiayaan.online.pc.updat
Route::post('/pembiayaan-online/pc/lines/{id}/verify',['as'=>'pembiayaan.online.pc.verify','uses'=>'PembiayaanOnlineController@verifyLine'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/lines/{id}/unverify',['as'=>'pembiayaan.online.pc.unverify','uses'=>'PembiayaanOnlineController@unverifyLine'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/bukti-akaun',['as'=>'pembiayaan.online.pc.bukti.akaun','uses'=>'PembiayaanOnlineController@viewBuktiAkaun'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/borang',['as'=>'pembiayaan.online.pc.borang','uses'=>'PembiayaanOnlineController@viewBorang'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/lines/{id}/sag',['as'=>'pembiayaan.online.pc.sag','uses'=>'PembiayaanOnlineController@viewSag'])->middleware('auth','khazanah');
Route::post('/pembiayaan-online/pc/batches/submit',['as'=>'pembiayaan.online.pc.submit','uses'=>'PembiayaanOnlineController@submitBatch'])->middleware('auth','khazanah');
Route::get('/pembiayaan-online/pc/batches/{batchId}',['as'=>'pembiayaan.online.pc.batch','uses'=>'PembiayaanOnlineController@showBatch'])->middleware('auth','khazanah');
@@ -232,6 +241,7 @@ Route::post('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/approve',['as
Route::post('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/paid',['as'=>'pembiayaan.online.operasi.paid','uses'=>'PembiayaanOnlineController@operasiMarkPaid'])->middleware('auth','operasi');
Route::post('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/paid',['as'=>'pembiayaan.online.operasi.paid.batch','uses'=>'PembiayaanOnlineController@operasiMarkPaidBatch'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/bukti-akaun',['as'=>'pembiayaan.online.operasi.bukti.akaun','uses'=>'PembiayaanOnlineController@operasiViewBuktiAkaun'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/borang',['as'=>'pembiayaan.online.operasi.borang','uses'=>'PembiayaanOnlineController@operasiViewBorang'])->middleware('auth','operasi');
Route::get('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/sag',['as'=>'pembiayaan.online.operasi.sag','uses'=>'PembiayaanOnlineController@operasiViewSag'])->middleware('auth','operasi');
Route::get('/permohonan/rekod',['as'=>'permohonan.rekod','uses' => 'KhazanahController@RekodPermohonan'])->middleware('auth','khazanah');
Route::post('/permohonan/simpan',['as'=>'permohonan.simpan','uses' => 'KhazanahController@SimpanPermohonan'])->middleware('auth','khazanah');
+55
View File
@@ -0,0 +1,55 @@
<?php
namespace Tests\Feature;
use App\Http\Middleware\LogViewerPassword;
use Illuminate\Http\Request;
use Tests\TestCase;
class LogViewerPasswordTest extends TestCase
{
public function test_missing_configuration_denies_access()
{
config(['logviewer.password' => null]);
$response = $this->checkAccess();
$this->assertSame(503, $response->getStatusCode());
}
public function test_missing_or_incorrect_credentials_are_rejected()
{
config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]);
foreach ([[], ['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'wrong'],
['PHP_AUTH_USER' => 'other', 'PHP_AUTH_PW' => 'test-only-secret']] as $server) {
$response = $this->checkAccess($server);
$this->assertSame(401, $response->getStatusCode());
$this->assertStringContainsString('Basic realm=', $response->headers->get('WWW-Authenticate'));
}
}
public function test_correct_credentials_allow_access_without_caching()
{
config(['logviewer.password' => password_hash('test-only-secret', PASSWORD_DEFAULT)]);
$response = $this->checkAccess(['PHP_AUTH_USER' => 'logs', 'PHP_AUTH_PW' => 'test-only-secret']);
$this->assertSame(200, $response->getStatusCode());
$this->assertSame('allowed', $response->getContent());
$this->assertStringContainsString('no-store', $response->headers->get('Cache-Control'));
}
public function test_logs_route_has_auth_role_password_and_throttle_middleware()
{
$route = app('router')->getRoutes()->match(Request::create('/logs'));
$middleware = $route->gatherMiddleware();
$this->assertContains('auth', $middleware);
$this->assertContains('role:admin,masteradmin', $middleware);
$this->assertContains(LogViewerPassword::class, $middleware);
$this->assertContains('throttle:10,1', $middleware);
}
private function checkAccess(array $server = [])
{
$request = Request::create('/logs?dl=example', 'GET', [], [], [], $server);
return (new LogViewerPassword)->handle($request, function () {
return response('allowed');
});
}
}