diff --git a/.env.docker b/.env.docker new file mode 100644 index 00000000..79c1ea3a --- /dev/null +++ b/.env.docker @@ -0,0 +1,54 @@ +APP_NAME=Laravel +APP_ENV=local +APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= +APP_DEBUG=true +APP_URL=http://localhost +# API_URL=http://api-annurv2.test +API_URL=http://host.docker.internal:8080 +API_URL_KOOP=http://host.docker.internal:8080 +API_URL_PKB=http://host.docker.internal:8080 +API_ARRAHNBID=http://lelong.test/api/ + +LOG_CHANNEL=stack +LOG_LEVEL=debug + +DB_CONNECTION=mysql +DB_HOST=mysql +DB_PORT=3306 +DB_DATABASE=kaunter_erahn +DB_USERNAME=kaunteruser +DB_PASSWORD=password + +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 \ No newline at end of file diff --git a/.env.example b/.env.example index 1e819c27..e7821ee4 100644 --- a/.env.example +++ b/.env.example @@ -46,5 +46,5 @@ AWS_BUCKET= MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -MYKAD_READER_PORT=8085 +MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED diff --git a/.env.staging b/.env.staging index 27304237..91b447ec 100644 --- a/.env.staging +++ b/.env.staging @@ -1,15 +1,16 @@ APP_NAME=Laravel APP_ENV=staging +APP_VERSION=v2.2-staging APP_KEY=base64:O1XZDqK4H6+3GdrODgCaLr2l+OoWvgVhYr0GJ1KizW8= APP_DEBUG=true -APP_URL=http://172.16.6.200:8091 +APP_URL=https://staging.erahn.com.my HTTP_PORT=8091 MYSQL_PUBLISH_PORT=3309 -API_URL=http://172.16.6.200:8090 -API_URL_KOOP=http://172.16.6.200:8090 -API_URL_PKB=http://172.16.6.200:8090 +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 @@ -53,5 +54,5 @@ AWS_BUCKET= MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -MYKAD_READER_PORT=8085 +MYKAD_READER_PORT=17321 MYKAD_READER_ENABLED_CAWANGAN='' # IF NO CAWANGAN STATED, ALL CAWANGAN IS ENABLED \ No newline at end of file diff --git a/.gitea/workflows/deploy-staging.yml b/.gitea/workflows/deploy-staging.yml index 30d130cd..ab79a87f 100644 --- a/.gitea/workflows/deploy-staging.yml +++ b/.gitea/workflows/deploy-staging.yml @@ -56,11 +56,11 @@ jobs: set -e IMAGE_TAG="${{ inputs.image_tag }}" - mkdir -p "${DEPLOY_DIR}/docker/nginx" + mkdir -p "${DEPLOY_DIR}/docker/nginx" "${DEPLOY_DIR}/public" cp docker-compose.staging.yml "${DEPLOY_DIR}/docker-compose.yml" cp docker/nginx/default.conf "${DEPLOY_DIR}/docker/nginx/default.conf" - rm -rf "${DEPLOY_DIR}/public" - cp -R public "${DEPLOY_DIR}/public" + 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 @@ -78,7 +78,7 @@ jobs: export IMAGE_TAG docker compose --env-file .env.staging -f docker-compose.yml pull app - docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --remove-orphans + docker compose --env-file .env.staging -f docker-compose.yml up -d --no-build --force-recreate --remove-orphans app web echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}" diff --git a/.gitignore b/.gitignore index 1ed293c1..80ba177a 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,5 @@ Homestead.yaml npm-debug.log yarn-error.log /.vscode -docker-compose.yml docker_src/ +**/.DS_Store \ No newline at end of file diff --git a/README.md b/README.md index f821d23f..ca86015c 100644 --- a/README.md +++ b/README.md @@ -1 +1,51 @@ If you does't know what is this you probably in wrong project + +```sql +--- for all ip +CREATE USER 'kaunteruser'@'%' IDENTIFIED BY 'your_password'; + +--- for specific ip +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 + +## Proses Gadaian +1) Pelanggan datang bawa marhun +2) Teller daftar maklumat pelanggan jika pelanggan baharu +3) Serah marhun ke penilai untuk timbang +4) Teller daftar marhun di sistem berdasarkan maklumat dari penilai +5) Penilai print SAG dan serah marhun ke Khazanah (PC/PPC) - __khazanah can still scan still scan though__ +6) Khazanah scan barcode dekat SAG untuk masuk ke dalam peti besi +* table involved - gadai.semakbarcode (default is blank, after is Lulus) +7) DONE + +## Setup new branch command: +1. Duplicate previous branch table - use erahn_{{name}} +```sql +SET FOREIGN_KEY_CHECKS = 0; + +SET SESSION group_concat_max_len = 1000000; + +SELECT GROUP_CONCAT( + CONCAT('TRUNCATE TABLE `', table_name, '`') + SEPARATOR '; ' +) INTO @sql +FROM information_schema.tables +WHERE table_schema = DATABASE(); + +SELECT @sql; --- copy the output and paste it + +SET FOREIGN_KEY_CHECKS = 1; +``` +2. Add new cawangan kod on master_erahn.arrahn_master_db and and kaunter_erahn.cawangan +3. Register new cawangan detail in erahn_{{name}}.cawangan \ No newline at end of file diff --git a/app/Http/Controllers/AdminFeatureController.php b/app/Http/Controllers/AdminFeatureController.php new file mode 100644 index 00000000..aa72a39d --- /dev/null +++ b/app/Http/Controllers/AdminFeatureController.php @@ -0,0 +1,69 @@ +middleware(['auth', 'admin']); + } + + protected function api() + { + return Http::withOptions(['verify' => config('app.api_verify')]); + } + + public function index() + { + $api_url = config('api.url'); + + $metaRes = $this->api()->get($api_url . '/api/features'); + $meta = $metaRes->successful() ? $metaRes->json() : []; + $keys = is_array($meta) && !empty($meta['keys']) ? $meta['keys'] : ['pembiayaan_online']; + $labels = is_array($meta) && !empty($meta['labels']) ? $meta['labels'] : [ + 'pembiayaan_online' => 'Pembiayaan Online', + ]; + + $listRes = $this->api()->get($api_url . '/api/features/cawangan'); + $cawangan = ($listRes->successful() && is_array($listRes->json())) ? $listRes->json() : []; + + if (!$listRes->successful()) { + session()->now('error', 'Gagal muat senarai ciri cawangan.'); + } + + return view('admin.features', compact('api_url', 'keys', 'labels', 'cawangan')); + } + + public function update(Request $request) + { + $kodcaw = trim((string) $request->input('kodcaw', '')); + $feature = trim((string) $request->input('feature', '')); + $enabled = (int) $request->input('enabled') ? 1 : 0; + + if ($kodcaw === '' || $feature === '') { + return redirect()->route('admin.features')->with('error', 'Kod cawangan dan ciri diperlukan.'); + } + + $res = $this->api() + ->put(config('api.url') . '/api/features/' . $kodcaw . '/' . $feature, [ + 'enabled' => $enabled, + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) + ? $body['message'] + : 'Gagal kemaskini ciri cawangan.'; + return redirect()->route('admin.features')->with('error', $msg); + } + + $label = $enabled ? 'diaktifkan' : 'dinyahaktifkan'; + return redirect() + ->route('admin.features') + ->with('message', $feature . ' ' . $label . ' untuk ' . $kodcaw . '.'); + } +} diff --git a/app/Http/Controllers/Auth/ImpersonateController.php b/app/Http/Controllers/Auth/ImpersonateController.php new file mode 100644 index 00000000..dab764ab --- /dev/null +++ b/app/Http/Controllers/Auth/ImpersonateController.php @@ -0,0 +1,52 @@ +middleware('auth'); + } + + public function impersonate(Request $request, $id): RedirectResponse + { + Gate::authorize('impersonate'); + + $impersonator = $request->user(); + $id = (int) $id; + $target = User::findOrFail($id); + + if ($target->id === $request->user()->id) { + return back()->with('error', 'Tidak boleh impersonate diri sendiri.'); + } + + if (method_exists($target, 'canBeImpersonated') && !$target->canBeImpersonated()) { + return back()->with('error', 'Akaun ini tidak boleh di-impersonate.'); + } + + $request->user()->impersonate($target); + + return redirect()->route('home'); + } + + public function leave(Request $request): RedirectResponse + { + if ($request->user() && method_exists($request->user(), 'isImpersonated') && $request->user()->isImpersonated()) { + $impersonated = $request->user(); + $impersonator = method_exists($impersonated, 'impersonator') ? $impersonated->impersonator() : null; + + $request->user()->leaveImpersonation(); + } + + return redirect()->route('home'); + } +} + diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index f29fcc28..b9abb660 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -60,9 +60,7 @@ class LoginController extends Controller 'password' => 'required|string', ]); - $credentials = $request->only('email', 'password'); - - if (Auth::guard('web')->attempt($credentials)) { + if ($this->attemptWebLogin($request)) { $auth_user = Auth::guard('web')->user(); $api_url = config('api.url'); @@ -103,9 +101,7 @@ class LoginController extends Controller 'password' => 'required|string', ]); - $credentials = $request->only('email', 'password'); - - if (Auth::guard('dashboard')->attempt($credentials,true)) { + if ($this->attemptDashboardLogin($request)) { $auth_user = Auth::guard('dashboard')->user(); $user_login['nama'] = $auth_user['name']; @@ -187,4 +183,42 @@ class LoginController extends Controller return response()->json($kakitangan,200); } + + /** + * In local environment, log in by email only (password ignored). + */ + private function attemptWebLogin(Request $request): bool + { + if (app()->environment('local')) { + $user = User::where('email', $request->email)->first(); + if ($user) { + Auth::guard('web')->login($user); + + return true; + } + + return false; + } + + return Auth::guard('web')->attempt($request->only('email', 'password')); + } + + /** + * In local environment, log in by email only (password ignored). + */ + private function attemptDashboardLogin(Request $request): bool + { + if (app()->environment('local')) { + $user = Dashboard::where('email', $request->email)->first(); + if ($user) { + Auth::guard('dashboard')->login($user, true); + + return true; + } + + return false; + } + + return Auth::guard('dashboard')->attempt($request->only('email', 'password'), true); + } } diff --git a/app/Http/Controllers/CustomersController.php b/app/Http/Controllers/CustomersController.php index 40fee0c9..2913fdbc 100644 --- a/app/Http/Controllers/CustomersController.php +++ b/app/Http/Controllers/CustomersController.php @@ -289,6 +289,29 @@ class CustomersController extends Controller return response(["no-data"], 200); } + /** + * Store MyKad photo for an existing customer when gambar_mykad is still empty. + */ + public function storeMykadPhotoIfMissing(Request $request) + { + $icno = preg_replace('/[^0-9A-Za-z]/', '', (string) $request->input('icno')); + if ($icno === '') { + return response()->json(['error' => 'IC number is required.', 'stored' => false], 422); + } + + if (!$request->filled('photo_base64')) { + return response()->json(['error' => 'Photo is required.', 'stored' => false], 422); + } + + $response = Http::withOptions(['verify' => config('app.api_verify')]) + ->post(config('api.url') . '/api/customers/' . $icno . '/mykad-photo', [ + 'photo_base64' => $request->input('photo_base64'), + 'photo_mime' => $request->input('photo_mime') ?: 'image/jpeg', + ]); + + return response()->json($response->json() ?? ['stored' => false], $response->status()); + } + /** * Show the form for creating a new resource. * @@ -358,7 +381,7 @@ class CustomersController extends Controller ])->json(); } - $customer_info = Http::withOptions(['verify' => config('app.api_verify')])->post(config('api.url') . '/api/customers/create/' . $auth_user['cawangan'], [ + $customer_payload = [ 'kodcaw' => $auth_user['cawangan'], 'nopelanggan' => $no_pelanggan, 'nokpbaru' => $nokpbaru, @@ -385,7 +408,14 @@ class CustomersController extends Controller 'nota_pekerjaan' => $request->nota_pekerjaan, 'notaarcc' => $request->notaarcc, 'tujuangadai' => $request->tujuan_gadai, - ]); + ]; + + if ($request->filled('photo_base64')) { + $customer_payload['photo_base64'] = $request->input('photo_base64'); + $customer_payload['photo_mime'] = $request->input('photo_mime') ?: 'image/jpeg'; + } + + $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); Session::put('noic', $request->noic); @@ -574,4 +604,113 @@ class CustomersController extends Controller return redirect()->back()->with('error', 'Gagal mengemaskini status anggota. Sila cuba semula.'); } } + + public function printMaklumat(Request $request) + { + $noic = $request->query('noic') ?: Session::get('noic'); + if (empty($noic)) { + return redirect('/pelanggan'); + } + + $auth_user = Auth::user(); + $api_url = config('api.url'); + $verify = ['verify' => config('app.api_verify')]; + $kodcaw = $auth_user['cawangan']; + + $customer = Http::withOptions($verify)->get($api_url . '/api/customers/' . $noic)->json(); + if (!is_array($customer) || (empty($customer['nama']) && empty($customer['nopelanggan']) && empty($customer['kpbaru']) && empty($customer['kplama']))) { + abort(404, 'Pelanggan tidak dijumpai.'); + } + + $cawanganResp = Http::withOptions($verify)->get($api_url . '/api/cawangan/detail/' . $kodcaw)->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $banks = Http::withOptions($verify)->get($api_url . '/api/bank')->json(); + $pekerjaanList = Http::withOptions($verify)->get($api_url . '/api/pekerjaan')->json(); + + $poskodKod = $customer['poskod'] ?? ''; + $poskod = []; + if ($poskodKod !== '' && $poskodKod !== null) { + $poskodResp = Http::withOptions($verify)->get($api_url . '/api/poskod/' . $poskodKod)->json(); + if (is_array($poskodResp)) { + $poskod = $poskodResp; + } + } + + $daerahNama = 'NIL'; + $negeriNama = 'NIL'; + if (!empty($poskod['koddaerah'])) { + $daerahResp = Http::withOptions($verify)->get($api_url . '/api/daerah/' . $poskod['koddaerah'])->json(); + $daerahNama = is_array($daerahResp) ? ($daerahResp['namadaerah'] ?? 'NIL') : 'NIL'; + } + if (!empty($poskod['kodnegeri'])) { + $negeriResp = Http::withOptions($verify)->get($api_url . '/api/negeri/' . $poskod['kodnegeri'])->json(); + $negeriNama = is_array($negeriResp) ? ($negeriResp['negeri'] ?? 'NIL') : 'NIL'; + } + + $bankNama = $this->lookupListValue($banks, 'abbreviation', $customer['kodbank'] ?? null, 'name'); + $pekerjaanNama = $this->lookupListValue($pekerjaanList, 'kodkerja', $customer['kodkerja'] ?? null, 'keterangan'); + if (($customer['kodkerja'] ?? '') == '6' && !empty($customer['nota_pekerjaan'])) { + $pekerjaanNama = $customer['nota_pekerjaan']; + } + + $photo_svg = '' + . '' + . '' + . '' + . ''; + $photo_src = 'data:image/svg+xml;base64,' . base64_encode($photo_svg); + if (!empty($customer['photo_base64'])) { + $mime = $customer['photo_mime'] ?? 'image/jpeg'; + $photo_src = 'data:' . $mime . ';base64,' . $customer['photo_base64']; + } + + return view('print.maklumat_pelanggan', [ + 'cawangan' => $cawangan_detail['namacaw'] ?? ($cawangan_detail['details_caw'] ?? $kodcaw), + 'kodcaw' => $kodcaw, + 'alamat1' => $cawangan_detail['alamat1'] ?? '', + 'alamat2' => $cawangan_detail['alamat2'] ?? '', + 'notel' => $cawangan_detail['notel'] ?? '', + 'tarikh' => Carbon::now()->format('d/m/Y'), + 'nama' => $this->printBlank($customer['nama'] ?? null), + 'nokp' => $this->printBlank(!empty($customer['kpbaru']) ? $customer['kpbaru'] : ($customer['kplama'] ?? $noic)), + 'nopelanggan' => $this->printBlank($customer['nopelanggan'] ?? null), + 'status' => (!empty($customer['tag_anggota']) && $customer['tag_anggota'] == 1) ? 'Anggota Koperasi' : 'Pelanggan', + 'telefon' => $this->printBlank($customer['telefon'] ?? null), + 'telefon2' => $this->printBlank($customer['telefon2'] ?? null), + 'alamat' => $this->printBlank($customer['alamat1'] ?? null), + 'poskod' => $this->printBlank($customer['poskod'] ?? null), + 'daerah' => $daerahNama, + 'negeri' => $negeriNama, + 'bank' => $this->printBlank($bankNama), + 'noakaun' => $this->printBlank($customer['noakaun'] ?? null), + 'pekerjaan' => $this->printBlank($pekerjaanNama), + 'nota' => $this->printBlank($customer['nota'] ?? null), + 'photo_src' => $photo_src, + ]); + } + + private function printBlank($value) + { + if ($value === null) { + return 'NIL'; + } + $value = is_string($value) ? trim($value) : $value; + return $value === '' ? 'NIL' : $value; + } + + private function lookupListValue($list, $key, $match, $nameKey) + { + if (!is_array($list) || $match === null || $match === '' || $match === '-') { + return ''; + } + foreach ($list as $item) { + if (is_array($item) && ($item[$key] ?? null) == $match) { + return $item[$nameKey] ?? ''; + } + } + return ''; + } } diff --git a/app/Http/Controllers/GadaianController.php b/app/Http/Controllers/GadaianController.php index a6cbbbea..a656d422 100644 --- a/app/Http/Controllers/GadaianController.php +++ b/app/Http/Controllers/GadaianController.php @@ -32,6 +32,16 @@ class GadaianController extends Controller $this->AliranTunaiController = new AliranTunaiController(); } + protected function isPembiayaanOnlineEnabled($kodcaw) + { + $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']); + } + public function index(Request $request) { @@ -211,17 +221,173 @@ class GadaianController extends Controller 'teller' => $auth_user['name'] ]); - $this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']); + $caraTerima = strtoupper((string) $request->input('cara_terima', 'TUNAI')); + $pinjamanAmt = round((float) $request->pinjaman, 2); + + // Online only for RM5k–50k; otherwise force cash path. + if ($caraTerima === 'ONLINE' && ($pinjamanAmt < 5000 || $pinjamanAmt > 50000)) { + $caraTerima = 'TUNAI'; + } + + if ($caraTerima === 'ONLINE' && !$this->isPembiayaanOnlineEnabled($auth_user['cawangan'])) { + $caraTerima = 'TUNAI'; + } + + if ($caraTerima === 'ONLINE') { + $online = Http::withOptions(['verify' => config('app.api_verify')]) + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/' . $auth_user['cawangan'], [ + 'norujukan' => $request->norujukan_gadai, + 'nokp' => $request->nokp, + 'pinjaman' => $pinjamanAmt, + 'kodbank' => $request->kodbank, + 'noakaun' => $request->noakaun, + 'nama_bank' => $request->nama_bank, + 'nama_akaun' => $request->nama_akaun ?: $request->nama_pelanggan, + 'nama_pelanggan' => $request->nama_pelanggan, + 'nopelanggan' => $request->nopelanggan, + 'created_by' => $auth_user['name'], + 'update_customer_bank' => true, + ]); + + if (!$online->successful()) { + $body = $online->json(); + $msg = is_array($body) && isset($body['message']) + ? $body['message'] + : 'Gagal daftar pembiayaan online. Sila cuba lagi.'; + return redirect()->back()->with('error', $msg); + } + } else { + $this->AliranTunaiController->alirantunaiGadai($request, $auth_user['name']); + } event(new GadaiNotify('lulus', $auth_user['cawangan'])); $request->session()->forget('page_reload'); $request->session()->forget('norujukan'); $request->session()->flash('activate_tab', 'SAG_baru'); + if ($caraTerima === 'ONLINE') { + $request->session()->flash('message', 'Gadai berjaya. Pembiayaan online menunggu semakan PC.'); + } return redirect()->route('customer_info'); } + public function printBorangPengesahanOnline($norujukan) + { + $auth_user = Auth::user(); + $kodcaw = $auth_user['cawangan']; + + $onlineResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/by-norujukan/' . $norujukan); + + if (!$onlineResp->successful()) { + abort(404, 'Pembiayaan online tidak dijumpai.'); + } + + $line = $onlineResp->json(); + if (!is_array($line) || empty($line['norujukan'])) { + abort(404, 'Pembiayaan online tidak dijumpai.'); + } + + return $this->renderBorangPengesahanOnline($line, $kodcaw); + } + + public function previewBorangPengesahanOnline(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $auth_user['cawangan']; + + $line = [ + 'norujukan' => $request->input('norujukan'), + 'nokp' => $request->input('nokp'), + 'nopelanggan' => $request->input('nopelanggan'), + 'nama_pelanggan' => $request->input('nama_pelanggan'), + 'pinjaman' => $request->input('pinjaman'), + 'kodbank' => $request->input('kodbank'), + 'nama_bank' => $request->input('nama_bank'), + 'noakaun' => $request->input('noakaun'), + 'nama_akaun' => $request->input('nama_akaun'), + 'created_by' => $auth_user['name'] ?? '', + 'created_at' => Carbon::now()->toDateTimeString(), + ]; + + if (empty($line['norujukan']) || empty($line['noakaun'])) { + abort(422, 'Sila lengkapkan maklumat bank sebelum cetak borang.'); + } + + return $this->renderBorangPengesahanOnline($line, $kodcaw); + } + + private function renderBorangPengesahanOnline(array $line, $kodcaw) + { + $auth_user = Auth::user(); + + $cawanganResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/cawangan/detail/' . $kodcaw) + ->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $nokpRaw = $line['nokp'] ?? ''; + $customer = []; + if ($nokpRaw !== '') { + $customerResp = Http::withOptions(['verify' => config('app.api_verify')]) + ->get(config('api.url') . '/api/customers/' . $nokpRaw) + ->json(); + if (is_array($customerResp)) { + $customer = $customerResp; + } + } + + $cawangan = $cawangan_detail['namacaw'] + ?? $cawangan_detail['details_caw'] + ?? $kodcaw; + $alamat1 = $cawangan_detail['alamat1'] ?? ''; + $alamat2 = $cawangan_detail['alamat2'] ?? ''; + $notel = $cawangan_detail['notel'] ?? ''; + + $nama = $line['nama_pelanggan'] ?? ($customer['nama'] ?? ''); + $nokp = !empty($customer['kpbaru']) ? $customer['kpbaru'] : ($customer['kplama'] ?? $nokpRaw); + $nopelanggan = $line['nopelanggan'] ?? ($customer['nopelanggan'] ?? ''); + $telefon = $customer['telefon'] ?? ''; + $teller = $line['created_by'] ?? ($auth_user['name'] ?? ''); + $bank = $line['nama_bank'] ?? ($line['kodbank'] ?? ''); + $noakaun = $line['noakaun'] ?? ''; + $nama_akaun = $line['nama_akaun'] ?? $nama; + $pinjaman = number_format((float) ($line['pinjaman'] ?? 0), 2); + $norujukan = $line['norujukan'] ?? ''; + + $tarikhSumber = $line['created_at'] ?? null; + try { + $tarikh = $tarikhSumber + ? Carbon::parse($tarikhSumber)->format('d/m/Y') + : Carbon::now()->format('d/m/Y'); + } catch (\Exception $e) { + $tarikh = Carbon::now()->format('d/m/Y'); + } + + return view('print.borang_pengesahan_online', compact( + 'cawangan', + 'kodcaw', + 'alamat1', + 'alamat2', + 'notel', + 'norujukan', + 'tarikh', + 'teller', + 'nama', + 'nokp', + 'nopelanggan', + 'telefon', + 'pinjaman', + 'bank', + 'noakaun', + 'nama_akaun' + )); + } + public function editstore(Request $request) { $auth_user = Auth::user(); diff --git a/app/Http/Controllers/KakitanganController.php b/app/Http/Controllers/KakitanganController.php index f8346092..3a519525 100644 --- a/app/Http/Controllers/KakitanganController.php +++ b/app/Http/Controllers/KakitanganController.php @@ -20,10 +20,43 @@ class KakitanganController extends Controller public function daftarKakitangan(){ $api_url = config('api.url'); $auth_user = Auth::user(); - $cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/'. $auth_user['cawangan'])->json(); + $http = Http::withOptions(['verify' => config('app.api_verify')]); + + $cawangan_all = $http->get($api_url . '/api/cawangan')->json(); + $cawangan_all = is_array($cawangan_all) ? $cawangan_all : []; + $cawangan_by_kod = collect($cawangan_all)->keyBy('kodcaw'); + $cawangan_info = $cawangan_by_kod->get($auth_user['cawangan'], []); + $role = Role::get(); - $kakitangan = User::all(); - return view('operasi.daftar_kakitangan',compact('cawangan_info','api_url','role','kakitangan','auth_user')); + $kakitangan_rows = User::where('roles', '!=', 'admin') + ->get() + ->values() + ->map(function ($staff) use ($cawangan_by_kod) { + $kod = (string) $staff->cawangan; + if (in_array($kod, ['operasi', 'callcenter', 'admin'], true)) { + $cawangan_nama = ucwords($kod); + } else { + $cawangan_nama = ucwords((string) data_get($cawangan_by_kod, $kod . '.details_caw', $kod)); + } + + return [ + 'id' => $staff->id, + 'name' => $staff->name, + 'roles' => strtoupper((string) $staff->roles), + 'role_harian' => $staff->role_harian ? strtoupper($staff->role_harian) : '-', + 'role_asal' => $staff->role_asal ? strtoupper($staff->role_asal) : '-', + 'cawangan' => $cawangan_nama, + ]; + }); + + return view('operasi.daftar_kakitangan', compact( + 'cawangan_info', + 'api_url', + 'role', + 'kakitangan_rows', + 'auth_user', + 'cawangan_all' + )); } public function registerStaff(Request $request){ diff --git a/app/Http/Controllers/KhazanahController.php b/app/Http/Controllers/KhazanahController.php index 03e72524..d40aad0e 100644 --- a/app/Http/Controllers/KhazanahController.php +++ b/app/Http/Controllers/KhazanahController.php @@ -505,6 +505,9 @@ class KhazanahController extends Controller public function mintakatalaluansemula(Request $request) { + if (app()->environment('local')) { + return "True"; + } if(Hash::check($request->passsemula, Auth::user()->password)) { @@ -3987,7 +3990,7 @@ class KhazanahController extends Controller } - public function cetakLaporanGadaiUjrah(Request $request){ + public function cetakLaporanGadaiUjrah(Request $request){ $api_url = config('api.url'); $auth_user = Auth::user(); @@ -4009,12 +4012,21 @@ class KhazanahController extends Controller $hingga_date = $split_date[2] . '-' . $split_date[1] . '-' . $split_date[0]; } - - $gadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url'). '/api/gadailaporanRangeDate/'. $auth_user['cawangan'] ,['mula' => $mula_date,'akhir' => $hingga_date])->json(); + $reportGadai = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url'). '/api/gadailaporanRangeDate/'. $auth_user['cawangan'] ,['mula' => $mula_date,'akhir' => $hingga_date])->json(); + $gadai = $reportGadai['gadai']; + $statistik =$reportGadai['statistik']; + $reportPembiayaan= $statistik['reportPembiayaan']; + $reportLoanAmount=$statistik['reportLoanAmount']; + $reportMarhunValue=$statistik['reportMarhunValue']; + $reportMarhunMargin=$statistik['reportMarhunMargin']; + $reportCustomerJob=$statistik['reportCustomerJob']; + $reportCustomerRace=$statistik['reportCustomerRace']; + // dd($reportCustomerRace); + $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_gadaian_ujrah', compact('gadai','cawangan_detail','tarikh_cetak','mula_date','api_url','hingga_date')); + 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')); } diff --git a/app/Http/Controllers/OperasiController.php b/app/Http/Controllers/OperasiController.php index 93a12505..7b5f6491 100644 --- a/app/Http/Controllers/OperasiController.php +++ b/app/Http/Controllers/OperasiController.php @@ -370,6 +370,9 @@ class OperasiController extends Controller public function mintakatalaluansemula(Request $request) { + if (app()->environment('local')) { + return "True"; + } if (Hash::check($request->passsemula, Auth::user()->password)) { return "True"; diff --git a/app/Http/Controllers/PembiayaanOnlineController.php b/app/Http/Controllers/PembiayaanOnlineController.php new file mode 100644 index 00000000..9dfee280 --- /dev/null +++ b/app/Http/Controllers/PembiayaanOnlineController.php @@ -0,0 +1,868 @@ +middleware('auth'); + } + + protected function api() + { + return Http::withOptions(['verify' => config('app.api_verify')]); + } + + protected function kodcaw() + { + return Auth::user()->cawangan; + } + + protected function cawanganInfo() + { + return $this->api() + ->get(config('api.url') . '/api/cawangan/' . $this->kodcaw()) + ->json(); + } + + protected function isPembiayaanOnlineEnabled($cawangan_info = null) + { + if (!is_array($cawangan_info)) { + $cawangan_info = $this->cawanganInfo(); + } + $features = $cawangan_info['features'] ?? []; + + return !empty($features['pembiayaan_online']); + } + + protected function abortUnlessPembiayaanOnline($cawangan_info = null) + { + if (!$this->isPembiayaanOnlineEnabled($cawangan_info)) { + abort(403, 'Pembiayaan online tidak diaktifkan untuk cawangan ini.'); + } + } + + /** + * PC senarai queue + batch history. + */ + public function index(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $cawangan_info = $this->cawanganInfo(); + $this->abortUnlessPembiayaanOnline($cawangan_info); + + $queue = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/queue') + ->json(); + + $batches = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches') + ->json(); + + $banks = $this->api()->get($api_url . '/api/bank')->json(); + if (!is_array($banks)) { + $banks = []; + } + + $modal = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw) + ->json(); + + $lines = $queue['data'] ?? []; + $slot = $queue['slot_cadangan'] ?? null; + if (!is_array($batches)) { + $batches = []; + } + + return view('pembiayaan_online.pc.senarai', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'lines', + 'batches', + 'banks', + 'modal', + 'slot', + 'queue' + )); + } + + /** + * Update bank/docs for a queue line. + */ + public function updateLine(Request $request, $id) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $payload = [ + '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, + ]; + + $res = $this->api() + ->asForm() + ->put($api_url . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id, $payload); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal kemaskini rekod.'; + return redirect()->back()->with('error', $msg); + } + + return redirect()->route('pembiayaan.online.pc')->with('message', 'Rekod dikemaskini.'); + } + + /** + * Submit selected lines as a batch to Operasi. + */ + public function submitBatch(Request $request) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $auth_user = Auth::user(); + + $ids = $request->input('ids', []); + if (!is_array($ids)) { + $ids = array_filter(explode(',', (string) $ids)); + } + + $res = $this->api() + ->asForm() + ->post($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches/submit', [ + 'ids' => $ids, + 'pc_submitted_by' => $auth_user['name'], + 'slot' => $request->input('slot'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal hantar batch.'; + return redirect()->back()->with('error', $msg); + } + + $payload = $res->json(); + $batchNo = is_array($payload) ? ($payload['batch']['batch_no'] ?? '') : ''; + $appended = is_array($payload) && !empty($payload['appended']); + $message = $appended + ? ('Rekod ditambah ke batch' . ($batchNo ? ' ' . $batchNo : '') . '.') + : ('Batch dihantar ke Operasi' . ($batchNo ? ': ' . $batchNo : '.')); + + return redirect()->route('pembiayaan.online.pc')->with('message', $message); + } + + /** + * Batch detail. + */ + public function showBatch($batchId) + { + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + + $cawangan_info = $this->cawanganInfo(); + $this->abortUnlessPembiayaanOnline($cawangan_info); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect()->route('pembiayaan.online.pc')->with('error', 'Batch tidak dijumpai.'); + } + + return view('pembiayaan_online.pc.batch', [ + 'batch' => $detail['batch'], + 'lines' => $detail['lines'] ?? [], + 'kodcaw' => $kodcaw, + 'api_url' => $api_url, + 'cawangan_info' => $cawangan_info, + ]); + } + + /** + * PC: senarai PAID untuk tally + blast. + */ + public function paidIndex(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $blastFilter = $request->input('blast_sent', '0'); + + $cawangan_info = $this->cawanganInfo(); + $this->abortUnlessPembiayaanOnline($cawangan_info); + + $query = []; + if ($blastFilter !== 'all' && $blastFilter !== '') { + $query['blast_sent'] = $blastFilter; + } + + $paid = $this->api() + ->get($api_url . '/api/pembiayaan-online/' . $kodcaw . '/paid', $query) + ->json(); + + $lines = $paid['data'] ?? []; + if (!is_array($lines)) { + $lines = []; + } + + $bilangan = $paid['bilangan'] ?? count($lines); + $jumlah = $paid['jumlah'] ?? 0; + + return view('pembiayaan_online.pc.paid', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'lines', + 'bilangan', + 'jumlah', + 'blastFilter' + )); + } + + /** + * PC: view/download bukti bayaran inline. + */ + public function viewBukti($id) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + $res = $this->api() + ->get(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/lines/' . $id . '/bukti'); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Bukti tidak dijumpai.'; + return redirect()->back()->with('error', $msg); + } + + $payload = $res->json(); + $bukti = $payload['bukti'] ?? null; + if (!$bukti || empty($bukti['data'])) { + return redirect()->back()->with('error', 'Fail bukti kosong.'); + } + + $raw = $bukti['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']); + } + + $type = $bukti['type'] ?? 'application/octet-stream'; + $filename = $bukti['filename'] ?? ('bukti-' . $id); + + return response($binary, 200, [ + 'Content-Type' => $type, + 'Content-Disposition' => 'inline; filename="' . $filename . '"', + ]); + } + + /** + * PC: tanda rekod PAID sebagai selesai semak / blast. + */ + public function markBlast(Request $request) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + $ids = $request->input('ids', []); + if (!is_array($ids)) { + $ids = array_filter(explode(',', (string) $ids)); + } + + if (count($ids) === 0) { + return redirect()->back()->with('error', 'Sila pilih sekurang-kurangnya satu rekod.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/' . $kodcaw . '/paid/blast', [ + 'ids' => $ids, + 'blast_by' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tanda selesai.'; + return redirect()->back()->with('error', $msg); + } + + $n = $res->json()['dikemaskini'] ?? count($ids); + return redirect() + ->route('pembiayaan.online.pc.paid') + ->with('message', $n . ' rekod ditanda selesai semak/blast.'); + } + + /** + * Operasi: senarai batch semua cawangan. + */ + public function operasiIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $status = $request->input('status', 'SUBMITTED'); + $tarikh = $request->input('tarikh'); + + $query = ['status' => $status]; + if ($tarikh) { + $query['tarikh'] = $tarikh; + } + + $res = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/batches', $query) + ->json(); + + $batches = $res['data'] ?? []; + if (!is_array($batches)) { + $batches = []; + } + + return view('pembiayaan_online.operasi.senarai', compact( + 'auth_user', + 'api_url', + 'batches', + 'status', + 'tarikh' + )); + } + + /** + * Operasi: detail batch + lines. + */ + public function operasiShowBatch($kodcaw, $batchId) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + + $cawangan_info = $this->api() + ->get($api_url . '/api/cawangan/' . $kodcaw) + ->json(); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect() + ->route('pembiayaan.online.operasi') + ->with('error', 'Batch tidak dijumpai.'); + } + + return view('pembiayaan_online.operasi.batch', [ + 'auth_user' => $auth_user, + 'batch' => $detail['batch'], + 'lines' => $detail['lines'] ?? [], + 'kodcaw' => $kodcaw, + 'api_url' => $api_url, + 'cawangan_info' => $cawangan_info, + ]); + } + + /** + * Operasi: printable batch kelulusan form. + */ + public function operasiPrintBatch($kodcaw, $batchId) + { + $api_url = config('api.url'); + + $detail = $this->api() + ->get($api_url . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId) + ->json(); + + if (!is_array($detail) || empty($detail['batch'])) { + return redirect() + ->route('pembiayaan.online.operasi') + ->with('error', 'Batch tidak dijumpai.'); + } + + $batch = $detail['batch']; + $lines = $detail['lines'] ?? []; + if (!is_array($lines)) { + $lines = []; + } + + $cawanganResp = $this->api() + ->get($api_url . '/api/cawangan/detail/' . $kodcaw) + ->json(); + $cawangan_detail = (is_array($cawanganResp) && isset($cawanganResp[0])) + ? $cawanganResp[0] + : (is_array($cawanganResp) ? $cawanganResp : []); + + $cawangan = $cawangan_detail['namacaw'] + ?? $cawangan_detail['details_caw'] + ?? $cawangan_detail['nama'] + ?? $kodcaw; + $alamat1 = $cawangan_detail['alamat1'] ?? ''; + $alamat2 = $cawangan_detail['alamat2'] ?? ''; + $notel = $cawangan_detail['notel'] ?? ''; + + $batchNo = $batch['batch_no'] ?? '-'; + $slot = $batch['slot'] ?? '-'; + $status = $batch['status'] ?? '-'; + $dihantarOleh = $batch['pc_submitted_by'] ?? '-'; + + $jumlah = (float) ($batch['jumlah_keseluruhan'] ?? 0); + if ($jumlah <= 0 && count($lines) > 0) { + $jumlah = (float) collect($lines)->sum(function ($line) { + return (float) ($line['pinjaman'] ?? 0); + }); + } + + $opsBy = Auth::user()['name'] ?? ''; + foreach ($lines as $line) { + if (!empty($line['ops_by'])) { + $opsBy = $line['ops_by']; + break; + } + } + + $tarikh = $this->formatPrintDate($batch['tarikh'] ?? null); + $dihantarPada = $this->formatPrintDate($batch['submitted_at'] ?? null, true); + + return view('print.borang_batch_pembiayaan_online', compact( + 'cawangan', + 'kodcaw', + 'alamat1', + 'alamat2', + 'notel', + 'tarikh', + 'batchNo', + 'slot', + 'status', + 'dihantarOleh', + 'dihantarPada', + 'opsBy', + 'lines', + 'jumlah' + )); + } + + protected function formatPrintDate($value, $withTime = false) + { + if ($value === null || $value === '' || $value === '-') { + return '-'; + } + + try { + return Carbon::parse($value)->format($withTime ? 'd/m/Y H:i' : 'd/m/Y'); + } catch (\Exception $e) { + return (string) $value; + } + } + + public function operasiApproveLine(Request $request, $kodcaw, $id) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/approve', [ + 'ops_by' => Auth::user()['name'], + ]); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod diluluskan.'); + } + + public function operasiRejectLine(Request $request, $kodcaw, $id) + { + $reason = trim((string) $request->input('reject_reason', '')); + if ($reason === '') { + return redirect()->back()->with('error', 'Sebab tolak diperlukan.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/reject', [ + 'ops_by' => Auth::user()['name'], + 'reject_reason' => $reason, + ]); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod ditolak. Dikembalikan ke PC.'); + } + + public function operasiApproveBatch(Request $request, $kodcaw, $batchId) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId . '/approve', [ + 'ops_by' => Auth::user()['name'], + ]); + + return $this->opsRedirect($res, $kodcaw, $batchId, 'Semua rekod menunggu dalam batch diluluskan.'); + } + + public function operasiMarkPaid(Request $request, $kodcaw, $id) + { + $payload = $this->buildPaidPayload($request); + if (isset($payload['error'])) { + return redirect()->back()->with('error', $payload['error']); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/lines/' . $id . '/paid', $payload); + + return $this->opsRedirect($res, $kodcaw, $request->input('batch_id'), 'Rekod ditanda PAID.'); + } + + public function operasiMarkPaidBatch(Request $request, $kodcaw, $batchId) + { + $payload = $this->buildPaidPayload($request); + if (isset($payload['error'])) { + return redirect()->back()->with('error', $payload['error']); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/pembiayaan-online/operasi/' . $kodcaw . '/batches/' . $batchId . '/paid', $payload); + + return $this->opsRedirect($res, $kodcaw, $batchId, 'Rekod APPROVED dalam batch ditanda PAID.'); + } + + /** + * @return array + */ + protected function buildPaidPayload(Request $request) + { + $noRujukan = trim((string) $request->input('no_rujukan_bayaran', '')); + if ($noRujukan === '') { + return ['error' => 'No. rujukan bayaran diperlukan.']; + } + + if (!$request->hasFile('bukti')) { + return ['error' => 'Fail bukti bayaran diperlukan.']; + } + + $file = $request->file('bukti'); + if (!$file->isValid()) { + return ['error' => 'Fail bukti tidak sah.']; + } + + $raw = base64_encode(file_get_contents($file->getRealPath())); + $mime = $file->getMimeType() ?: 'application/octet-stream'; + + return [ + 'ops_by' => Auth::user()['name'], + 'no_rujukan_bayaran' => $noRujukan, + 'tarikh_bayaran' => $request->input('tarikh_bayaran') ?: date('Y-m-d'), + 'bukti_data' => $raw, + 'bukti_filename' => $file->getClientOriginalName(), + 'bukti_type' => $mime, + ]; + } + + protected function opsRedirect($res, $kodcaw, $batchId, $successMsg) + { + $batchId = $batchId ?: null; + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Tindakan gagal.'; + return redirect()->back()->with('error', $msg); + } + + if ($batchId) { + return redirect() + ->route('pembiayaan.online.operasi.batch', ['kodcaw' => $kodcaw, 'batchId' => $batchId]) + ->with('message', $successMsg); + } + + return redirect()->route('pembiayaan.online.operasi')->with('message', $successMsg); + } + + /** + * PC: modal online float + top-up request form/history. + */ + public function topupIndex(Request $request) + { + $auth_user = Auth::user(); + $kodcaw = $this->kodcaw(); + $api_url = config('api.url'); + $status = $request->input('status', ''); + + $cawangan_info = $this->cawanganInfo(); + $this->abortUnlessPembiayaanOnline($cawangan_info); + + $modal = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw) + ->json(); + + $query = []; + if ($status !== '' && $status !== 'all') { + $query['status'] = $status; + } + + $reqRes = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw . '/topup/requests', $query) + ->json(); + + $requests = $reqRes['data'] ?? []; + if (!is_array($requests)) { + $requests = []; + } + + $pendingRes = $this->api() + ->get($api_url . '/api/modal-online/' . $kodcaw . '/topup/requests', ['status' => 'PENDING']) + ->json(); + $pendingList = $pendingRes['data'] ?? []; + $hasPending = is_array($pendingList) && count($pendingList) > 0; + $isMissing = !empty($modal['missing']); + $pendingRequest = $modal['pending_request'] ?? null; + if (!$pendingRequest && $hasPending) { + $pendingRequest = $pendingList[0]; + } + + return view('pembiayaan_online.pc.topup', compact( + 'auth_user', + 'kodcaw', + 'api_url', + 'cawangan_info', + 'modal', + 'requests', + 'status', + 'hasPending', + 'isMissing', + 'pendingRequest' + )); + } + + /** + * PC: submit top-up request. + */ + public function topupRequest(Request $request) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + $amaun = (float) $request->input('amaun', 0); + + if ($amaun <= 0) { + return redirect()->back()->with('error', 'Amaun mesti lebih daripada 0.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/topup/request', [ + 'amaun' => $amaun, + 'dimohon_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal hantar permohonan.'; + return redirect()->back()->with('error', $msg)->withInput(); + } + + return redirect() + ->route('pembiayaan.online.pc.topup') + ->with('message', 'Permohonan modal online dihantar. Menunggu kelulusan Operasi.'); + } + + /** + * Operasi: senarai permohonan tambah modal online. + */ + public function operasiTopupIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $status = $request->input('status', 'PENDING'); + $tarikh = $request->input('tarikh'); + + $query = []; + if ($status === 'all') { + $query['status'] = 'all'; + } elseif ($status !== '' && $status !== null) { + $query['status'] = $status; + } else { + $query['status'] = 'PENDING'; + } + if ($tarikh) { + $query['tarikh'] = $tarikh; + } + + $res = $this->api() + ->get($api_url . '/api/modal-online/operasi/topup/requests', $query) + ->json(); + + $requests = $res['data'] ?? []; + if (!is_array($requests)) { + $requests = []; + } + + return view('pembiayaan_online.operasi.topup', compact( + 'auth_user', + 'api_url', + 'requests', + 'status', + 'tarikh' + )); + } + + public function operasiTopupApprove(Request $request, $id) + { + $amaun = $request->input('amaun'); + if ($amaun !== null && $amaun !== '' && (float) $amaun <= 0) { + return redirect()->back()->with('error', 'Amaun diluluskan mesti lebih daripada 0.'); + } + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/topup/' . $id . '/approve', [ + 'diluluskan_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + 'amaun' => $amaun, + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal lulus permohonan.'; + return redirect()->back()->with('error', $msg); + } + + $amaunLulus = $res->json()['request']['amaun'] ?? $amaun; + return redirect() + ->route('pembiayaan.online.operasi.topup') + ->with('message', 'Permohonan diluluskan RM ' . number_format((float) $amaunLulus, 2) . '. Modal online cawangan dikemas kini.'); + } + + public function operasiTopupReject(Request $request, $id) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/topup/' . $id . '/reject', [ + 'diluluskan_oleh' => Auth::user()['name'], + 'nota' => $request->input('nota'), + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tolak permohonan.'; + return redirect()->back()->with('error', $msg); + } + + return redirect() + ->route('pembiayaan.online.operasi.topup') + ->with('message', 'Permohonan ditolak.'); + } + + /** + * PC: EOD close branch modal online. + */ + public function closeModal(Request $request) + { + $this->abortUnlessPembiayaanOnline(); + $kodcaw = $this->kodcaw(); + + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/close', [ + 'ditutup_oleh' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup modal online.'; + return redirect()->back()->with('error', $msg); + } + + $dikembalikan = $res->json()['dikembalikan'] ?? 0; + return redirect() + ->route('pembiayaan.online.pc.topup') + ->with('message', 'Modal online ditutup. Baki dikembalikan RM ' . number_format((float) $dikembalikan, 2) . '.'); + } + + /** + * Operasi: EOD dashboard semua cawangan. + */ + public function operasiEodIndex(Request $request) + { + $auth_user = Auth::user(); + $api_url = config('api.url'); + $tarikh = $request->input('tarikh') ?: date('Y-m-d'); + + $res = $this->api() + ->get($api_url . '/api/modal-online/operasi/eod', [ + 'zone' => 'all', + 'tarikh' => $tarikh, + ]) + ->json(); + + $rows = $res['data'] ?? []; + if (!is_array($rows)) { + $rows = []; + } + + $summary = [ + 'open' => $res['open'] ?? 0, + 'closed' => $res['closed'] ?? 0, + 'missing' => $res['missing'] ?? 0, + 'bilangan' => $res['bilangan'] ?? count($rows), + ]; + + return view('pembiayaan_online.operasi.eod', compact( + 'auth_user', + 'api_url', + 'rows', + 'tarikh', + 'summary' + )); + } + + public function operasiCloseModal(Request $request, $kodcaw) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/' . $kodcaw . '/close', [ + 'ditutup_oleh' => Auth::user()['name'], + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup modal online.'; + return redirect()->back()->with('error', $msg); + } + + return redirect() + ->route('pembiayaan.online.operasi.eod') + ->with('message', 'Modal online ' . $kodcaw . ' ditutup.'); + } + + public function operasiCloseAllModal(Request $request) + { + $res = $this->api() + ->asForm() + ->post(config('api.url') . '/api/modal-online/operasi/eod/close-all', [ + 'ditutup_oleh' => Auth::user()['name'], + 'zone' => 'all', + ]); + + if (!$res->successful()) { + $body = $res->json(); + $msg = is_array($body) && isset($body['message']) ? $body['message'] : 'Gagal tutup semua modal online.'; + return redirect()->back()->with('error', $msg); + } + + $closed = $res->json()['closed'] ?? 0; + $skipped = $res->json()['skipped'] ?? 0; + return redirect() + ->route('pembiayaan.online.operasi.eod') + ->with('message', 'EOD selesai: ' . $closed . ' ditutup, ' . $skipped . ' sudah ditutup.'); + } +} diff --git a/app/Http/Middleware/TrustProxies.php b/app/Http/Middleware/TrustProxies.php index 14befceb..39816113 100644 --- a/app/Http/Middleware/TrustProxies.php +++ b/app/Http/Middleware/TrustProxies.php @@ -12,7 +12,7 @@ class TrustProxies extends Middleware * * @var array|string|null */ - protected $proxies; + protected $proxies = '*'; /** * The headers that should be used to detect proxies. diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index ee8ca5bc..539a3f28 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -23,6 +23,8 @@ class AppServiceProvider extends ServiceProvider */ public function boot() { - // + if (strpos((string) config('app.url'), 'https://') === 0) { + \Illuminate\Support\Facades\URL::forceScheme('https'); + } } } diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 30490683..08a3ae1b 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -25,6 +25,8 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); - // + Gate::define('impersonate', function ($user) { + return $user->canImpersonate(); + }); } } diff --git a/app/User.php b/app/User.php index 33e49532..70034b80 100644 --- a/app/User.php +++ b/app/User.php @@ -6,9 +6,11 @@ use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Relations\HasOne; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; +use Lab404\Impersonate\Models\Impersonate; class User extends Authenticatable { + use Impersonate; use Notifiable; protected $guard = 'web'; @@ -43,4 +45,14 @@ class User extends Authenticatable { return $this->hasOne(Cawangan::class, 'kodcaw', 'cawangan'); } + + public function canImpersonate(): bool + { + return in_array($this->roles ?? '', ['admin'], true); + } + + public function canBeImpersonated(): bool + { + return ($this->roles ?? '') !== 'admin'; + } } diff --git a/app/helpers.php b/app/helpers.php new file mode 100644 index 00000000..974dacb6 --- /dev/null +++ b/app/helpers.php @@ -0,0 +1,15 @@ + env('APP_NAME', 'Laravel'), + 'version' => env('APP_VERSION', ''), + /* |-------------------------------------------------------------------------- | Application Environment diff --git a/config/mykad-reader.php b/config/mykad-reader.php index e1dc6da9..21576946 100644 --- a/config/mykad-reader.php +++ b/config/mykad-reader.php @@ -1,7 +1,7 @@ 'http://localhost:' . $x . '/?MYKADJSON'; +$default_port = '17321'; +$getReaderEndpoint = fn($x) => 'http://localhost:' . $x . '/read'; return[ 'endpoint' => env('MYKAD_READER_ENDPOINT', $getReaderEndpoint(env('MYKAD_READER_PORT', $default_port))), diff --git a/docker-compose.staging.yml b/docker-compose.staging.yml index 073e756d..92ae34c7 100644 --- a/docker-compose.staging.yml +++ b/docker-compose.staging.yml @@ -1,23 +1,8 @@ -# Staging stack — isolated from local docker-compose.yml (8081 / 3307) -# and from api_arrahn staging (8090 / 3308). -# -# Local: -# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --build -# -# CI (image already in registry): -# APP_IMAGE=git.koppkb.com/kopkb/kaunter IMAGE_TAG= \ -# docker compose --env-file .env.staging -f docker-compose.staging.yml up -d --no-build -# -# Host nginx reverse-proxies the public domain to 127.0.0.1:${HTTP_PORT}. -# See docker/nginx/host-proxy.example.conf -# -# If 8091 or 3309 is already taken, set HTTP_PORT / MYSQL_PUBLISH_PORT in .env.staging. - name: kaunter_staging services: app: - image: ${APP_IMAGE:-kaunter}:${IMAGE_TAG:-staging} + image: ${APP_IMAGE:-git.koppkb.com/kopkb/kaunter}:${IMAGE_TAG:-staging} build: context: . dockerfile: docker/php/Dockerfile diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf index 223a96c0..182f3243 100644 --- a/docker/nginx/default.conf +++ b/docker/nginx/default.conf @@ -9,7 +9,10 @@ server { client_max_body_size 50M; location / { - try_files $uri $uri/ @php; + # Production Apache public/.htaccess 301-strips trailing slashes. + # Nginx ignores .htaccess, so without this /path/ 404s while /path works. + rewrite ^/(.*)/$ /$1 last; + try_files $uri @php; } location @php { diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index b14f579b..e2a2c4f8 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -4,22 +4,22 @@ WORKDIR /var/www/html RUN apt-get update \ && apt-get install -y --no-install-recommends \ - git \ - pkg-config \ - unzip \ - libonig-dev \ - libzip-dev \ - libpng-dev \ - libjpeg62-turbo-dev \ - libfreetype6-dev \ + git \ + pkg-config \ + unzip \ + libonig-dev \ + libzip-dev \ + libpng-dev \ + libjpeg62-turbo-dev \ + libfreetype6-dev \ && docker-php-ext-configure gd --with-freetype --with-jpeg \ && docker-php-ext-install -j"$(nproc)" \ - pdo_mysql \ - mbstring \ - bcmath \ - zip \ - exif \ - gd \ + pdo_mysql \ + mbstring \ + bcmath \ + zip \ + exif \ + gd \ && rm -rf /var/lib/apt/lists/* COPY --from=composer:2 /usr/bin/composer /usr/local/bin/composer @@ -40,12 +40,12 @@ COPY --chown=www-data:www-data . /var/www/html RUN composer install --no-interaction --prefer-dist --no-dev --optimize-autoloader --no-scripts \ && mkdir -p \ - storage/logs \ - storage/framework/cache/data \ - storage/framework/views \ - storage/framework/sessions \ - storage/app/public \ - bootstrap/cache \ + storage/logs \ + storage/framework/cache/data \ + storage/framework/views \ + storage/framework/sessions \ + storage/app/public \ + bootstrap/cache \ && chown -R www-data:www-data storage bootstrap/cache vendor FROM base AS development diff --git a/public/js/mykad-reader-customer.js b/public/js/mykad-reader-customer.js index 3c53b650..dce93c83 100644 --- a/public/js/mykad-reader-customer.js +++ b/public/js/mykad-reader-customer.js @@ -1,293 +1,457 @@ let button_read_mykad = document.getElementById("button_read_mykad"); let button_close_modal = document.querySelector("#daftar_pelanggan_mykad_reader button.btn.btn-danger"); -let original_photo_loader = document.querySelector("#photo-mykad").src; +let photo_mykad = document.querySelector("#photo-mykad"); +let original_photo_loader = photo_mykad ? photo_mykad.src : ""; - let showLoader = () => { - let pageLoader = document.getElementById('page_loader'); - pageLoader.removeAttribute('class'); - pageLoader.classList.add('loading'); +const MYKAD_READER_TIMEOUT_MS = 45000; +const MYKAD_READER_DEFAULT_ENDPOINT = "http://localhost:17321/read"; + +let showLoader = () => { + let pageLoader = document.getElementById('page_loader'); + if (!pageLoader) { + return; + } + pageLoader.removeAttribute('class'); + pageLoader.classList.add('loading'); +}; + +let hideLoader = () => { + let pageLoader = document.getElementById('page_loader'); + if (!pageLoader) { + return; + } + pageLoader.removeAttribute('class'); +}; + +function getMyKadReaderEndpoint() { + let endpoint = (typeof mykad_reader_endpoint !== 'undefined' && mykad_reader_endpoint) + ? String(mykad_reader_endpoint).trim() + : ''; + + if (!endpoint || endpoint.includes('MYKADJSON') || endpoint.includes(':8085')) { + return MYKAD_READER_DEFAULT_ENDPOINT; + } + + return endpoint; +} + +async function parseJsonResponse(response) { + let text = await response.text(); + let trimmed = (text || '').trim(); + + if (!trimmed) { + throw new Error('Pembaca MyKad tidak memulangkan data. Sila cuba lagi.'); + } + + if (trimmed.charAt(0) === '<') { + throw new Error('Pembaca MyKad memulangkan halaman web, bukan data kad. Sila pastikan perkhidmatan mykad-helper sedang berjalan di localhost:17321.'); + } + + try { + return JSON.parse(trimmed); + } catch (error) { + throw new Error('Pembaca MyKad memulangkan data yang tidak sah. Sila cuba lagi.'); + } +} + +function getMyKadNumberWithoutHyphens(nric) { + return String(nric || '').replace(/-/g, ''); +} + +function titleCase(value) { + return String(value || '') + .toLowerCase() + .split(' ') + .filter(Boolean) + .map(word => word.charAt(0).toUpperCase() + word.slice(1)) + .join(' '); +} + +function getCodeFromValue(needle, haystack, fallback) { + let normalised = titleCase(needle); + let match = haystack.find(item => item[1].toLowerCase() === normalised.toLowerCase()); + + return match ? match[0] : fallback; +} + +function mapGender(gender) { + let value = String(gender || '').toLowerCase(); + + if (['male', 'lelaki', 'l', 'm'].includes(value)) { + return 'Lelaki'; + } + + if (['female', 'perempuan', 'p', 'f'].includes(value)) { + return 'Perempuan'; + } + + return gender || ''; +} + +function formatDateOfBirth(dob) { + if (!dob) { + return ''; + } + + let hyphenated = String(dob).includes('-') + ? String(dob) + : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + + let [year, month, day] = hyphenated.split('-'); + + if (!year || !month || !day) { + return dob; + } + + return day + '/' + month + '/' + year; +} + +function getAgeFromDateOfBirth(dob) { + if (!dob) { + return ''; + } + + let hyphenated = String(dob).includes('-') + ? String(dob) + : String(dob).replace(/(\d{4})(\d{2})(\d{2})/, '$1-$2-$3'); + + let birthDate = new Date(hyphenated); + if (Number.isNaN(birthDate.getTime())) { + return ''; + } + + let today = new Date(); + let age = today.getFullYear() - birthDate.getFullYear(); + let monthDiff = today.getMonth() - birthDate.getMonth(); + + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) { + age -= 1; + } + + return age; +} + +function getBumiputeraStatus(race) { + const non_bumiputera_ethnicity = ['INDIA', 'CINA', 'INDIAN', 'CHINESE']; + return !non_bumiputera_ethnicity.includes(String(race || '').toUpperCase()); +} + +function joinAddress(data) { + return [data.address1, data.address2, data.address3] + .map(line => String(line || '').trim()) + .filter(Boolean) + .join(', '); +} + +function mapReaderErrorCode(rawMessage) { + const error_handler_dictionary = { + '0x80100069': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', + '0x8010000C': 'Tiada MyKad dikesan, sila masukkan MyKad untuk dibaca', + '0x80100066': 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul', + '0x80100067': 'MyKad tidak dapat dikesan. Sila cabut dan masukkan semula kad', + '0x80100016': 'Gagal membaca MyKad. Sila pastikan kad dimasukkan dengan betul dan cuba lagi', + '0x8010002E': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', + '0x80100017': 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', }; - let hideLoader = () => { - let pageLoader = document.getElementById('page_loader'); - pageLoader.removeAttribute('class'); - }; + let message = String(rawMessage || ''); + let codes = message.match(/0x[0-9a-fA-F]+/g) || []; + let matched = codes.reverse().find(code => error_handler_dictionary[code]); - function handleMyKadReader() - { - showLoader(); - WebSocketManager.init("wss://localhost:5963/mykadwebsocket"); - WebSocketManager.listReader(callbackDoneGetListCardReader); + return matched ? error_handler_dictionary[matched] : (message || null); +} + +function getReaderErrorMessage(payload) { + if (!payload) { + return 'Pembaca MyKad tidak dapat membaca kad. Sila cuba lagi.'; } - function callbackDoneGetListCardReader(data, respcode) - { - console.log('done get list card reader'); + let raw = payload.message || payload.error || payload.detail || ''; + return mapReaderErrorCode(raw) + || 'Pembaca MyKad tidak dapat membaca kad. Sila pastikan MyKad dimasukkan dengan betul.'; +} - if(respcode === -1){ - console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} ); - } - - let isDevicesConnected = Object.keys(data).length > 0; - - if(! isDevicesConnected){ - hideLoader(); - - Swal.fire({ icon: 'error', title: 'Pembaca MyKad tidak dapat dikesan, sila periksa sambungan', }); - - return; - } - - let slots = data.toString().split(','); - WebSocketManager.open(doneOpenReaderConnection, slots[0]); - } - - function doneOpenReaderConnection() - { - WebSocketManager.readAllText(callBackAfterReadMykad); +function getFetchErrorTitle(error) { + if (error && error.name === 'AbortError') { + return 'Masa menunggu pembaca MyKad telah tamat. Sila cuba lagi.'; } - function handleMyKadReaderError(sw12AndApiName) - { - hideLoader(); + return 'Pembaca MyKad tidak dapat dikesan. Sila pastikan perkhidmatan pembaca MyKad sedang berjalan.'; +} - let error_handler_dictionary = { - '0x80100069' : 'Tiada MyKad dikesan, sila masukkan Mykad untuk dibaca', - '0x80100066' : 'MyKad mungkin dimasukkan terbalik, sila masukkan dengan betul', - 'default' : sw12AndApiName, - }; - - let error_data = sw12AndApiName.split(" "); - - let error_code = error_data.filter(x => x.startsWith('0x')).splice(-1) - - let error_message = error_handler_dictionary['default']; - - let hasDefinedError = error_handler_dictionary.hasOwnProperty(error_code); - - if(hasDefinedError){ - error_message = error_handler_dictionary[error_code]; - } - - Swal.fire({ icon: 'error', title: error_message, }); +function getDisplayError(error) { + if (!error) { + return getFetchErrorTitle(error); } - function callBackAfterReadMykad(data, respcode, sw12AndApiName) - { - if(respcode !== 0){ - handleMyKadReaderError(sw12AndApiName); - return - } + if (error.name === 'AbortError') { + return getFetchErrorTitle(error); + } - checkIfMyKadExists(data.IDNum) - .then(response => { - let isResponseArray = Array.isArray(response); - resolved_response = isResponseArray ? response.pop() : response; + let message = String(error.message || ''); + if (!message || message.startsWith('Failed to fetch') || message.includes('NetworkError')) { + return getFetchErrorTitle(error); + } - if (resolved_response === 'no-data') { - populateDaftarPelangganModal(data, handleReadPhoto); - } else { - let input_search_noic = document.getElementById("noic"); - let form_search_noic = document.querySelector("form[name='frmsearch']"); + return message; +} - let getMyKadNumberWithoutHypens = myKadNumber => myKadNumber.replace(/-/g, ''); - input_search_noic.value = getMyKadNumberWithoutHypens(data.IDNum); - form_search_noic.submit(); - } - }) - .catch(error => { - Swal.fire({ - icon: 'error', - title: error, - }); - }); +async function handleMyKadReader() { + showLoader(); - hideLoader(); - } + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), MYKAD_READER_TIMEOUT_MS); - function checkIfMyKadExists(myKadNumber) { - let myKadNumberWithoutHyphens = myKadNumber.replace(/-/g, ''); - - const url = wakil_caricustomer_endpoint + '/?icno=' + myKadNumberWithoutHyphens; - - const connectionConfig = { + try { + const response = await fetch(getMyKadReaderEndpoint(), { + method: 'POST', headers: { - "Content-Type": "application/json", - "Accept": "application/json", - "X-Requested-With": "XMLHttpRequest", - "X-CSRF-Token": document.querySelector('input[name="_token"]').value + 'Accept': 'application/json', }, - credentials: "same-origin", - }; + signal: controller.signal, + }); - return fetch(url, connectionConfig) - .then(response => response.json()) - } + const payload = await parseJsonResponse(response); - let populateDaftarPelangganModal = (data, callbackAfterPopulateDaftarPelanggan) => { - $('#daftar_pelanggan_mykad_reader').modal('show'); - let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group"); - - const religions = [ - ['01', 'Islam'], - ['02', 'Hindu'], - ['03', 'Buddha'], - ['04', 'Kristian'], - ['05', 'Lain-lain'], - ]; - - const races = [ - ['M', 'Melayu'], - ['C', 'Cina'], - ['I', 'India'], - ['L', 'Lain-lain'], - ]; - - let getCodeFromValue = (needle, haystack) => { - let title_cased_text = needle.split(' ') - .map(w => w[0].toUpperCase() + w.substring(1).toLowerCase()) - .join(' '); - - return haystack.filter(x => x[1] == title_cased_text)[0][0]; - }; - - let getMyKadNumberWithoutHypens = (IDNum) => { - return IDNum.replace(/-/g, ''); + if (!response.ok) { + throw new Error(getReaderErrorMessage(payload)); } - let getFormattedDateOfBirth = (date_of_birth) => { - - let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"); - - const [year, month, day] = date_of_birth_hyphenated.split('-'); - return day + '/' + month + '/' + year; - }; - - let getAgeFromDateOfBirth = (date_of_birth) => { - - let date_of_birth_hyphenated = date_of_birth.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"); - - const [birth_year, birth_month, birth_day] = date_of_birth_hyphenated.split('-'); - - let formatBirthMonth = (birth_month) => { - let newbirthMonth = birth_month < 9 ? birth_month[1] : birth_month; - return newbirthMonth; - }; - - const count_leap_year = countLeapYears(birth_year, formatBirthMonth(birth_month), birth_day) - 1; - - const start = new Date(date_of_birth_hyphenated), - end = new Date(), - diff = new Date(end - start), - days = ((diff / 1000 / 60 / 60 / 24) - count_leap_year) / 365; - - return parseInt(days); - }; - - let getBumiputeraStatus = (race) => { - - const non_bumiputera_ethnicity = ['INDIA', 'CINA']; - - let isBumiputera = !non_bumiputera_ethnicity.includes(race); - - return isBumiputera; - }; - - let disablePointerEventsOnClickableInputs = () => { - modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none'; - modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none'; - - modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none'; - modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none'; + if (!payload || payload.ok !== true || !payload.data) { + throw new Error(getReaderErrorMessage(payload)); } - let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); - let photo_container = mykad_modal.querySelector("#photo-mykad-container"); - - modal_form.querySelector("textarea[name='name']").value = data.OrgName; - modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHypens(data.IDNum); - modal_form.querySelector("input[name='tarikh_lahir']").value = getFormattedDateOfBirth(data.BirthDate); - modal_form.querySelector("input[name='negeri_lahir']").value = data.BirthPlace; - modal_form.querySelector("input[name='jantina']").value = data.Gender == 'LELAKI' ? 'Lelaki' : 'Perempuan'; - modal_form.querySelector("input[name='warganegara']").checked = data.Citizenship === 'WARGANEGARA' ? true : false; - modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.Race); - modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.Religion, religions); - modal_form.querySelector("textarea[name='alamat']").value = [data.Address1, data.Address2, data.Address3].join(", "); - - let getPostCode = (postCodeCity) => postCodeCity.replace(/\D/g, ''); - modal_form.querySelector("input[name='poskod']").value = getPostCode(data.PostcodeCity); - - let getCityName = (postCodeCity) => postCodeCity.replace(/\d/g, ''); - modal_form.querySelector("input[name='daerah']").value = getCityName(data.PostcodeCity).trim(); - - modal_form.querySelector("input[name='negeri']").value = data.State; - modal_form.querySelector("input[name='age']").value = getAgeFromDateOfBirth(data.BirthDate); - modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.Race, races); - - calculatePassportAge(); - - photo_container.classList.remove('d-none'); - photo_container.classList.add('d-flex'); - - $('#daftar_pelanggan_mykad_reader').modal('show'); - - - disablePointerEventsOnClickableInputs(); - callbackAfterPopulateDaftarPelanggan(); + await handleMyKadData(payload.data); + } catch (error) { + Swal.fire({ + icon: 'error', + title: getDisplayError(error), + }); + } finally { + clearTimeout(timeoutId); + hideLoader(); + } } - -function handleReadPhoto() -{ - WebSocketManager.init("wss://localhost:5963/mykadwebsocket"); - WebSocketManager.listReader(callbackDoneGetListCardReader2); -} - -function callbackDoneGetListCardReader2(data, respcode) -{ - if(respcode === -1){ - console.log({'location': 'callbackDoneGetListCardReader', 'data' : data} ); +function customerNeedsMykadPhoto(customer) { + if (!customer || typeof customer !== 'object') { + return false; } - let slots = data.toString().split(','); - WebSocketManager.open(doneOpenReaderConnection2, slots[0]); + return !customer.gambar_mykad && !customer.photo_base64; } -function doneOpenReaderConnection2() -{ - WebSocketManager.readPhoto(callbackAfterReadPhoto2); -} - -function callbackAfterReadPhoto2(data, RespCode, RespSW12AndApiName) -{ - let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); - let photo = mykad_modal.querySelector("#photo-mykad"); - - let isDataEmpty = data - && Object.keys(data).length === 0 - && Object.getPrototypeOf(data) === Object.prototype; - - if(isDataEmpty){ - photo.src = original_photo_loader; +async function storeMykadPhotoForExistingCustomerIfMissing(customer, readerData) { + if (!customerNeedsMykadPhoto(customer)) { return; } - photo.src = 'data:image/jpeg;base64, ' + data.Base64Photo; + if (!readerData || !readerData.has_photo || !readerData.photo_base64) { + return; + } + + if (typeof store_mykad_photo_endpoint === 'undefined' || !store_mykad_photo_endpoint) { + return; + } + + const csrfToken = document.querySelector('input[name="_token"]'); + const nric = getMyKadNumberWithoutHyphens( + customer.kpbaru || customer.kplama || readerData.nric || '' + ); + + if (!nric) { + return; + } + + try { + await fetch(store_mykad_photo_endpoint, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-TOKEN': csrfToken ? csrfToken.value : '', + }, + credentials: 'same-origin', + body: JSON.stringify({ + icno: nric, + photo_base64: readerData.photo_base64, + photo_mime: readerData.photo_mime || 'image/jpeg', + }), + }); + } catch (error) { + // Do not block customer search if photo backfill fails. + console.warn('Failed to store MyKad photo for existing customer', error); + } } +function handleMyKadData(data) { + return checkIfMyKadExists(data.nric) + .then(async response => { + let isResponseArray = Array.isArray(response); + let resolved_response = isResponseArray ? response.pop() : response; + + if (!resolved_response || resolved_response === 'no-data') { + populateDaftarPelangganModal(data); + } else { + await storeMykadPhotoForExistingCustomerIfMissing(resolved_response, data); + + let input_search_noic = document.querySelector("form[name='frmsearch'] input[name='noic']"); + let form_search_noic = document.querySelector("form[name='frmsearch']"); + + input_search_noic.value = getMyKadNumberWithoutHyphens(data.nric); + form_search_noic.submit(); + } + }) + .catch(() => { + populateDaftarPelangganModal(data); + }); +} + +function checkIfMyKadExists(myKadNumber) { + let myKadNumberWithoutHyphens = getMyKadNumberWithoutHyphens(myKadNumber); + let lookupUrl = new URL(wakil_caricustomer_endpoint, window.location.origin); + lookupUrl.searchParams.set('icno', myKadNumberWithoutHyphens); + + const csrfToken = document.querySelector('input[name="_token"]'); + + return fetch(lookupUrl.toString(), { + method: 'GET', + headers: { + "Accept": "application/json", + "X-Requested-With": "XMLHttpRequest", + "X-CSRF-Token": csrfToken ? csrfToken.value : "" + }, + credentials: "same-origin", + }).then(async response => { + let text = await response.text(); + let trimmed = (text || '').trim(); + + if (!trimmed || trimmed.charAt(0) === '<') { + return 'no-data'; + } + + try { + return JSON.parse(trimmed); + } catch (error) { + return 'no-data'; + } + }); +} + +let populateDaftarPelangganModal = (data) => { + $('#daftar_pelanggan_mykad_reader').modal('show'); + let modal_form = document.querySelector("#daftar_pelanggan_mykad_reader .modal-body .row.form-group"); + + const religions = [ + ['01', 'Islam'], + ['02', 'Hindu'], + ['03', 'Buddha'], + ['04', 'Kristian'], + ['05', 'Lain-lain'], + ]; + + const races = [ + ['M', 'Melayu'], + ['C', 'Cina'], + ['I', 'India'], + ['L', 'Lain-lain'], + ]; + + let disablePointerEventsOnClickableInputs = () => { + modal_form.querySelector("input[name='bumiputera']").style.pointerEvents = 'none'; + modal_form.querySelector("input[name='warganegara']").style.pointerEvents = 'none'; + modal_form.querySelector("select[name='bangsa']").style.pointerEvents = 'none'; + modal_form.querySelector("select[name='agama']").style.pointerEvents = 'none'; + }; + + let mykad_modal = document.querySelector("#daftar_pelanggan_mykad_reader"); + let photo_container = mykad_modal.querySelector("#photo-mykad-container"); + let photo = mykad_modal.querySelector("#photo-mykad"); + + modal_form.querySelector("textarea[name='name']").value = data.name || data.gmpc_name || ''; + modal_form.querySelector("input[name='noic']").value = getMyKadNumberWithoutHyphens(data.nric); + modal_form.querySelector("input[name='tarikh_lahir']").value = formatDateOfBirth(data.dob); + modal_form.querySelector("input[name='negeri_lahir']").value = data.birth_place || ''; + modal_form.querySelector("input[name='jantina']").value = mapGender(data.gender); + modal_form.querySelector("input[name='warganegara']").checked = String(data.citizenship || '').toUpperCase() === 'WARGANEGARA'; + modal_form.querySelector("input[name='bumiputera']").checked = getBumiputeraStatus(data.race); + modal_form.querySelector("select[name='agama']").value = getCodeFromValue(data.religion, religions, '05'); + modal_form.querySelector("textarea[name='alamat']").value = joinAddress(data); + modal_form.querySelector("input[name='poskod']").value = data.postcode || ''; + modal_form.querySelector("input[name='daerah']").value = data.city || ''; + modal_form.querySelector("input[name='negeri']").value = data.state || ''; + modal_form.querySelector("input[name='age']").value = data.age != null && data.age !== '' + ? data.age + : getAgeFromDateOfBirth(data.dob); + modal_form.querySelector("select[name='bangsa']").value = getCodeFromValue(data.race, races, 'L'); + + if (data.has_photo && data.photo_base64) { + let mime = data.photo_mime || 'image/jpeg'; + photo.src = 'data:' + mime + ';base64,' + data.photo_base64; + photo_container.classList.remove('d-none'); + photo_container.classList.add('d-flex'); + setMykadPhotoFields(data.photo_base64, mime); + } else { + photo.src = original_photo_loader; + photo_container.classList.add('d-none'); + photo_container.classList.remove('d-flex'); + setMykadPhotoFields('', ''); + } + + disablePointerEventsOnClickableInputs(); + $(modal_form.querySelector("input[name='poskod']")).trigger('change'); +}; + let hideMykadPhoto = () => { - - WebSocketManager.close(() => console.log('Websocket closed')); - let photo = document.getElementById("photo-mykad"); let photo_container = document.getElementById("photo-mykad-container"); - photo.src = original_photo_loader; + if (photo) { + photo.src = original_photo_loader; + } - photo_container.classList.add('d-none'); - photo_container.classList.remove('d-flex'); + if (photo_container) { + photo_container.classList.add('d-none'); + photo_container.classList.remove('d-flex'); + } + + setMykadPhotoFields('', ''); }; -button_read_mykad.addEventListener("click", handleMyKadReader); -button_close_modal.addEventListener("click", hideMykadPhoto); +function setMykadPhotoFields(base64, mime) { + let form = document.querySelector("#daftar_pelanggan_mykad_reader form") + || document.getElementById("form-mykad-reader"); + let photoInput = (form && form.querySelector("input[name='photo_base64']")) + || document.getElementById("photo_base64"); + let mimeInput = (form && form.querySelector("input[name='photo_mime']")) + || document.getElementById("photo_mime"); + + if (photoInput) { + photoInput.value = base64 || ''; + } + + if (mimeInput) { + mimeInput.value = mime || ''; + } +} + +function copyMykadPhotoFromImage() { + let photo = document.querySelector("#daftar_pelanggan_mykad_reader #photo-mykad"); + if (!photo || !photo.src || photo.src.indexOf("base64,") === -1) { + return; + } + + let parts = photo.src.split("base64,"); + let mimeMatch = photo.src.match(/^data:([^;]+);/); + setMykadPhotoFields(parts[1] || "", mimeMatch ? mimeMatch[1] : "image/jpeg"); +} + +if (button_read_mykad) { + button_read_mykad.addEventListener("click", handleMyKadReader); +} + +if (button_close_modal) { + button_close_modal.addEventListener("click", hideMykadPhoto); +} diff --git a/resources/views/admin/customer_index.blade.php b/resources/views/admin/customer_index.blade.php index aba0ebb5..e6a9dcdf 100644 --- a/resources/views/admin/customer_index.blade.php +++ b/resources/views/admin/customer_index.blade.php @@ -306,7 +306,7 @@ +@endsection diff --git a/resources/views/admin/poskod.blade.php b/resources/views/admin/poskod.blade.php index 7b30aa22..19254337 100644 --- a/resources/views/admin/poskod.blade.php +++ b/resources/views/admin/poskod.blade.php @@ -48,6 +48,6 @@ @endsection \ No newline at end of file diff --git a/resources/views/aliran_tunai/index.blade.php b/resources/views/aliran_tunai/index.blade.php index 9459d54a..384fad46 100644 --- a/resources/views/aliran_tunai/index.blade.php +++ b/resources/views/aliran_tunai/index.blade.php @@ -26,7 +26,7 @@
@php $auth_user = Auth::user();@endphp - +
@@ -348,7 +348,7 @@
+ +
+ merge(['class' => 'table table-striped table-hover nowrap']) }} + style="width:100%" + data-exportable="{{ $exportable ? 1 : 0 }}" + data-export-filename="{{ $filename !== '' ? $filename : $id }}" + > + + + @foreach ($columns as $column) + + @endforeach + + + + {{ $slot }} +
{{ $column }}
+
diff --git a/resources/views/components/print/footer.blade.php b/resources/views/components/print/footer.blade.php new file mode 100644 index 00000000..3c21f8bd --- /dev/null +++ b/resources/views/components/print/footer.blade.php @@ -0,0 +1,44 @@ +@props([ + 'showCopies' => true, +]) + + + +
merge(['class' => 'print-footer']) }}> + @if($showCopies) +
+ Salinan: Kad Pengenalan    Akaun Bank/ Slip/ Penyata Bank +
+ @endif + + @if(trim($slot) !== '') +
+ {{ $slot }} +
+ @endif +
diff --git a/resources/views/components/print/header.blade.php b/resources/views/components/print/header.blade.php new file mode 100644 index 00000000..52507e38 --- /dev/null +++ b/resources/views/components/print/header.blade.php @@ -0,0 +1,104 @@ +@props([ + 'cawangan' => '', + 'kodcaw' => '', + 'alamat1' => '', + 'alamat2' => '', + 'notel' => '', + 'tarikh' => '', + 'norujukan' => '', + 'rujukanLabel' => 'No. Rujukan SAG', + 'showDummyBadge' => false, +]) + + + +
merge(['class' => 'print-header']) }}> +
+

KOPERASI PERMODALAN KELANTAN BERHAD (D-5-0228)

+

{{ $cawangan }}

+

{{ $alamat1 }}

+

{{ $alamat2 }}

+

Tel: {{ $notel }}

+
+
+ KPKB + Ar-Rahn +
+
+ @if($kodcaw || $cawangan) +
+ Cawangan
+ {{ trim(implode(' — ', array_filter([$kodcaw, $cawangan]))) }} +
+ @endif + @if($tarikh) +
Tarikh
{{ $tarikh }}
+ @endif + @if($norujukan) +
{{ $rujukanLabel }}
{{ $norujukan }}
+ @endif + {{ $slot }} + @if($showDummyBadge) +
CONTOH SAHAJA
+ @endif +
+
diff --git a/resources/views/components/print/watermark.blade.php b/resources/views/components/print/watermark.blade.php new file mode 100644 index 00000000..38e1b73c --- /dev/null +++ b/resources/views/components/print/watermark.blade.php @@ -0,0 +1,25 @@ + + + diff --git a/resources/views/customers/v1/index.blade.php b/resources/views/customers/v1/index.blade.php index ca296dda..54eca7af 100644 --- a/resources/views/customers/v1/index.blade.php +++ b/resources/views/customers/v1/index.blade.php @@ -210,15 +210,17 @@ +@endsection diff --git a/resources/views/pembiayaan_online/operasi/eod.blade.php b/resources/views/pembiayaan_online/operasi/eod.blade.php new file mode 100644 index 00000000..588e0a69 --- /dev/null +++ b/resources/views/pembiayaan_online/operasi/eod.blade.php @@ -0,0 +1,152 @@ +@extends('layouts.app_operasi') +@section('content') +
+
+
+ + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + +
+
+
+
Tarikh
+

{{ $tarikh }}

+
+
+
+
+
Masih Dibuka
+

{{ $summary['open'] ?? 0 }}

+
+
+
+
+
Sudah Ditutup
+

{{ $summary['closed'] ?? 0 }}

+
+
+
+
+
Belum Ada Rekod
+

{{ $summary['missing'] ?? 0 }}

+
+
+
+ +
+
+
+ + + + Top-up +
+ @if(($summary['open'] ?? 0) > 0) +
+ @csrf + +
+ @endif +
+
+ +
+
+
Status Modal Online Mengikut Cawangan
+
+
+ + + + + + + + + + + + + + + + @forelse($rows as $row) + @php + $closed = (int)($row['ditutup'] ?? 0) === 1; + $missing = !empty($row['missing']); + @endphp + + + + + + + + + + + + @empty + + + + @endforelse + +
CawanganHadTambahDigunakanKurang (EOD)BakiStatusDitutup Oleh
+
{{ $row['cawangan_nama'] ?? '-' }}
+ {{ $row['kodcaw'] ?? '' }} +
{{ number_format((float)($row['had'] ?? 0), 2) }}{{ number_format((float)($row['tambah'] ?? 0), 2) }}{{ number_format((float)($row['digunakan'] ?? 0), 2) }}{{ number_format((float)($row['kurang'] ?? 0), 2) }} + @if($missing) + — + @else + {{ number_format((float)($row['baki'] ?? 0), 2) }} + @endif + + @if($closed) + DITUTUP + @elseif($missing) + TIADA + @else + DIBUKA + @endif + + @if($closed) + {{ $row['ditutup_oleh'] ?? '-' }}
+ {{ $row['ditutup_at'] ?? '' }} + @else + — + @endif +
+ @if($closed) + OK + @elseif($missing) + Tiada had + @else +
+ @csrf + +
+ @endif +
Tiada data cawangan.
+
+
+
+
+
+@endsection diff --git a/resources/views/pembiayaan_online/operasi/senarai.blade.php b/resources/views/pembiayaan_online/operasi/senarai.blade.php new file mode 100644 index 00000000..2f2e6505 --- /dev/null +++ b/resources/views/pembiayaan_online/operasi/senarai.blade.php @@ -0,0 +1,102 @@ +@extends('layouts.app_operasi') +@section('content') +
+
+
+ + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + +
+
+
+ + + + + + Reset +
+
+
+ +
+
+
Senarai Batch Pembiayaan Online
+
+ Modal Online + EOD + {{ count($batches) }} batch +
+
+
+ + + + + + + + + + + + + + + + @forelse($batches as $batch) + @php + $st = $batch['status'] ?? ''; + $badge = 'badge-secondary'; + if ($st === 'SUBMITTED') $badge = 'badge-primary'; + elseif ($st === 'PARTIAL') $badge = 'badge-warning'; + elseif ($st === 'OPS_DONE') $badge = 'badge-success'; + elseif ($st === 'PAID') $badge = 'badge-dark'; + @endphp + + + + + + + + + + + + @empty + + + + @endforelse + +
Batch NoCawanganTarikhSlotBilJumlah (RM)StatusDihantar Oleh
{{ $batch['batch_no'] ?? '-' }} +
{{ $batch['cawangan_nama'] ?? '-' }}
+ {{ $batch['kodcaw'] ?? '' }} +
{{ $batch['tarikh'] ?? '-' }}{{ $batch['slot'] ?? '-' }}{{ $batch['bilangan'] ?? 0 }}{{ number_format((float)($batch['jumlah_keseluruhan'] ?? 0), 2) }}{{ $st ?: '-' }}{{ $batch['pc_submitted_by'] ?? '-' }} + + Semak + +
Tiada batch untuk tapisan ini.
+
+
+
+
+
+@endsection diff --git a/resources/views/pembiayaan_online/operasi/topup.blade.php b/resources/views/pembiayaan_online/operasi/topup.blade.php new file mode 100644 index 00000000..139a0ae4 --- /dev/null +++ b/resources/views/pembiayaan_online/operasi/topup.blade.php @@ -0,0 +1,198 @@ +@extends('layouts.app_operasi') +@section('content') +
+
+
+ + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + +
+
+
+ + + + + + Reset + Batch Online +
+
+
+ +
+
+
Permohonan Had / Tambah Modal Online
+ {{ count($requests) }} rekod +
+
+ + + + + + + + + + + + + + + @forelse($requests as $req) + @php + $st = $req['status'] ?? ''; + $badge = 'badge-secondary'; + if ($st === 'PENDING') $badge = 'badge-warning'; + elseif ($st === 'APPROVED') $badge = 'badge-success'; + elseif ($st === 'REJECTED') $badge = 'badge-danger'; + @endphp + + + + + + + + + + + @empty + + + + @endforelse + +
IDCawanganTarikhAmaun (RM)Dimohon OlehStatusNotaTindakan
{{ $req['id'] ?? '-' }} +
{{ $req['cawangan_nama'] ?? '-' }}
+ {{ $req['kodcaw'] ?? '' }} +
{{ $req['tarikh'] ?? '-' }}{{ number_format((float)($req['amaun'] ?? 0), 2) }}{{ $req['dimohon_oleh'] ?? '-' }}{{ $st ?: '-' }}{{ $req['nota'] ?? '-' }} + @if($st === 'PENDING') + + + @else + {{ $req['diluluskan_oleh'] ?? '-' }} + @endif +
Tiada permohonan untuk tapisan ini.
+
+
+
+
+
+ + + + + + +@endsection diff --git a/resources/views/pembiayaan_online/pc/batch.blade.php b/resources/views/pembiayaan_online/pc/batch.blade.php new file mode 100644 index 00000000..ac2db5af --- /dev/null +++ b/resources/views/pembiayaan_online/pc/batch.blade.php @@ -0,0 +1,65 @@ +@extends('layouts.app') +@section('content') +
+
+
+ + +
+
+
+
Batch No
{{ $batch['batch_no'] ?? '-' }}
+
Tarikh
{{ $batch['tarikh'] ?? '-' }}
+
Slot
{{ $batch['slot'] ?? '-' }}
+
Status
{{ $batch['status'] ?? '-' }}
+
Jumlah
RM {{ number_format((float)($batch['jumlah_keseluruhan'] ?? 0), 2) }}
+
+
+
+ +
+
+
Senarai Dalam Batch
+ Kembali +
+
+ + + + + + + + + + + + + + + @foreach($lines as $line) + + + + + + + + + + + @endforeach + +
StatusSAGPelangganNo KPBankNo AkaunPinjaman (RM)Rujukan Bayaran
{{ $line['status'] ?? '-' }}{{ $line['norujukan'] ?? '-' }}{{ $line['nama_pelanggan'] ?? '-' }}{{ $line['nokp'] ?? '-' }}{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}{{ $line['noakaun'] ?? '-' }}{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}{{ $line['no_rujukan_bayaran'] ?? '-' }}
+
+
+
+
+
+@endsection diff --git a/resources/views/pembiayaan_online/pc/paid.blade.php b/resources/views/pembiayaan_online/pc/paid.blade.php new file mode 100644 index 00000000..138ba469 --- /dev/null +++ b/resources/views/pembiayaan_online/pc/paid.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.app') +@section('content') +
+
+
+ + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + +
+
+
+
+
Bilangan PAID
+

{{ $bilangan }} rekod

+
+
+
+
+
+
+
Jumlah Pinjaman
+

RM {{ number_format((float) $jumlah, 2) }}

+
+
+
+
+
+
+
Tapisan
+

+ @if(($blastFilter ?? '0') === '0') Belum Blast + @elseif(($blastFilter ?? '') === '1') Sudah Blast + @else Semua + @endif +

+
+
+
+
+ +
+
+
+ + + + Kembali Queue +
+
+
+ +
+
+
Senarai Pembiayaan Online — PAID
+ +
+
+

+ Semak jumlah & bukti bayaran sepadan dengan pelanggan. Setelah semak, tanda selesai. + (WhatsApp/SMS blast boleh disambung kemudian — butang ini menanda rekod sebagai selesai semak.) +

+
+ @csrf +
+ + + + + + + + + + + + + + + + + + + @forelse($lines as $line) + @php $blasted = (int)($line['blast_sent'] ?? 0) === 1; @endphp + + + + + + + + + + + + + + + @empty + + + + @endforelse + + @if(count($lines) > 0) + + + + + + + + @endif +
+ @if(($blastFilter ?? '0') !== '1') + + @endif + BlastSAGPelangganNo KPBankNo AkaunPinjaman (RM)Rujukan BayaranTarikh BayaranPaid AtBukti
+ @if(!$blasted) + + @else + — + @endif + + @if($blasted) + SELESAI + @else + BELUM + @endif + {{ $line['norujukan'] ?? '-' }}{{ $line['nama_pelanggan'] ?? '-' }}{{ $line['nokp'] ?? '-' }}{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}{{ $line['noakaun'] ?? '-' }}{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}{{ $line['no_rujukan_bayaran'] ?? '-' }}{{ $line['tarikh_bayaran'] ?? '-' }}{{ $line['paid_at'] ?? '-' }} + @if(!empty($line['bukti_file_id'])) + + Lihat + + @else + Tiada + @endif +
Tiada rekod PAID untuk tapisan ini.
Jumlah dipaparkan{{ number_format((float) $jumlah, 2) }}
+
+
+
+
+
+
+
+ + +@endsection diff --git a/resources/views/pembiayaan_online/pc/senarai.blade.php b/resources/views/pembiayaan_online/pc/senarai.blade.php new file mode 100644 index 00000000..a26b4819 --- /dev/null +++ b/resources/views/pembiayaan_online/pc/senarai.blade.php @@ -0,0 +1,304 @@ +@extends('layouts.app') +@section('content') +
+
+
+ + + + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + + @php + $isMissing = !empty($modal['missing']); + $isClosed = (int)($modal['ditutup'] ?? 0) === 1; + $pendingReq = $modal['pending_request'] ?? null; + if (is_object($pendingReq)) { + $pendingReq = json_decode(json_encode($pendingReq), true); + } + @endphp + + @if($isClosed) +
Modal online hari ini sudah ditutup (EOD).
+ @elseif($isMissing) +
+ Belum ada had modal online hari ini. + @if($pendingReq) + Permohonan RM {{ number_format((float)($pendingReq['amaun'] ?? 0), 2) }} + daripada {{ $pendingReq['dimohon_oleh'] ?? '-' }} masih PENDING. + @else + Mohon had di + Had Modal Online. + @endif +
+ @elseif($pendingReq) +
+ Permohonan tambah RM {{ number_format((float)($pendingReq['amaun'] ?? 0), 2) }} + daripada {{ $pendingReq['dimohon_oleh'] ?? '-' }} menunggu kelulusan Operasi. + Semak +
+ @endif + +
+
+
+
+
Modal Online Hari Ini
+

+ @if(!empty($modal['missing'])) + Belum ada had + @else + Baki RM {{ number_format((float)($modal['baki'] ?? 0), 2) }} + @endif +

+ + @if(!empty($modal['missing'])) + Tunggu kelulusan Operasi + @else + Digunakan RM {{ number_format((float)($modal['digunakan'] ?? 0), 2) }} + / Had RM {{ number_format((float)($modal['had'] ?? 0) + (float)($modal['tambah'] ?? 0), 2) }} + @endif + +
+
+
+
+
+
+
Queue PC
+

{{ count($lines) }} rekod

+ Jumlah RM {{ number_format((float)($queue['jumlah'] ?? 0), 2) }} +
+
+
+
+
+
+
Slot Cadangan
+

{{ $slot['slot'] ?? '-' }}

+ {{ $slot['nota'] ?? '' }} +
+
+
+
+ +
+
+
Senarai Menunggu (PENDING / REJECTED)
+ +
+
+
+ @csrf + +
+ + + + + + + + + + + + + + + + + @forelse($lines as $line) + + + + + + + + + + + + + @empty + + + + @endforelse + +
StatusSAGPelangganNo KPBankNo AkaunPinjaman (RM)Sebab TolakTindakan
+ + + @if(($line['status'] ?? '') === 'REJECTED_PC') + REJECTED + @else + PENDING + @endif + {{ $line['norujukan'] ?? '-' }}{{ $line['nama_pelanggan'] ?? '-' }}{{ $line['nokp'] ?? '-' }}{{ $line['nama_bank'] ?? $line['kodbank'] ?? '-' }}{{ $line['noakaun'] ?? '-' }}{{ number_format((float)($line['pinjaman'] ?? 0), 2) }}{{ $line['reject_reason'] ?? '-' }} + +
Tiada rekod dalam queue.
+
+
+
+
+ +
+
+
Sejarah Batch
+
+
+
+ + + + + + + + + + + + + + + @forelse($batches as $batch) + + + + + + + + + + + @empty + + + + @endforelse + +
Batch NoTarikhSlotBilJumlah (RM)StatusDihantar Oleh
{{ $batch['batch_no'] ?? '-' }}{{ $batch['tarikh'] ?? '-' }}{{ $batch['slot'] ?? '-' }}{{ $batch['bilangan'] ?? 0 }}{{ number_format((float)($batch['jumlah_keseluruhan'] ?? 0), 2) }}{{ $batch['status'] ?? '-' }}{{ $batch['pc_submitted_by'] ?? '-' }} + + Lihat + +
Belum ada batch.
+
+
+
+
+
+
+ +{{-- Edit modal --}} + + + +@endsection diff --git a/resources/views/pembiayaan_online/pc/topup.blade.php b/resources/views/pembiayaan_online/pc/topup.blade.php new file mode 100644 index 00000000..18a5fec9 --- /dev/null +++ b/resources/views/pembiayaan_online/pc/topup.blade.php @@ -0,0 +1,215 @@ +@extends('layouts.app') +@section('content') +@php + $isClosed = (int)($modal['ditutup'] ?? 0) === 1; + $isMissing = !empty($isMissing) || !empty($modal['missing']); + $pendingRequest = $pendingRequest ?? ($modal['pending_request'] ?? null); +@endphp +
+
+
+ + + @if(session()->has('message')) +
{{ session('message') }}
+ @endif + @if(session()->has('error')) +
{{ session('error') }}
+ @endif + + @if($isClosed) +
+ Modal online hari ini sudah ditutup (EOD) + oleh {{ $modal['ditutup_oleh'] ?? '-' }} + pada {{ $modal['ditutup_at'] ?? '-' }}. + Esok PC perlu mohon had baru daripada Operasi. +
+ @elseif($isMissing) +
+ Belum ada had modal online hari ini. + Mohon amaun di bawah. Operasi akan luluskan permohonan. +
+ @endif + + @if($hasPending && $pendingRequest) +
+ Permohonan PENDING RM {{ number_format((float)($pendingRequest['amaun'] ?? 0), 2) }} + daripada {{ $pendingRequest['dimohon_oleh'] ?? '-' }}. + Menunggu kelulusan Operasi. +
+ @endif + +
+
+
+
+
Baki Hari Ini
+

+ @if($isMissing) + — + @else + RM {{ number_format((float)($modal['baki'] ?? 0), 2) }} + @endif +

+
+
+
+
+
+
+
Had Asas
+

+ @if($isMissing) + — + @else + RM {{ number_format((float)($modal['had'] ?? 0), 2) }} + @endif +

+
+
+
+
+
+
+
Tambah Diluluskan
+

RM {{ number_format((float)($modal['tambah'] ?? 0), 2) }}

+
+
+
+
+
+
+
Digunakan
+

RM {{ number_format((float)($modal['digunakan'] ?? 0), 2) }}

+
+
+
+
+ +
+
+
+
+
{{ $isMissing ? 'Mohon Had Modal Online' : 'Mohon Tambah Modal Online' }}
+
+
+ @if($isClosed) +
+ Tidak boleh mohon selepas EOD. +
+ @else + @if($hasPending) +
+ Masih ada permohonan PENDING. Tunggu kelulusan Operasi sebelum mohon lagi. +
+ @endif +
+ @csrf +
+ + +
+
+ + +
+ + Kembali +
+ @endif +
+
+ +
+
+
Tutup Modal Online (EOD)
+
+
+

+ Tutup hari ini: baki yang tidak digunakan akan dikembalikan, + dan tiada lagi gadai online / permohonan untuk hari ini. + Esok mohon had baru daripada Operasi. +

+ @if($isClosed) + + @elseif($isMissing) + + @else +
+ @csrf + +
+ @endif +
+
+
+ +
+
+
+
Permohonan PC
+
+ +
+
+
+ + + + + + + + + + + + + @forelse($requests as $req) + @php + $st = $req['status'] ?? ''; + $badge = 'badge-secondary'; + if ($st === 'PENDING') $badge = 'badge-warning'; + elseif ($st === 'APPROVED') $badge = 'badge-success'; + elseif ($st === 'REJECTED') $badge = 'badge-danger'; + @endphp + + + + + + + + + @empty + + + + @endforelse + +
TarikhAmaun (RM)StatusDimohon OlehDiluluskan OlehNota
{{ $req['tarikh'] ?? '-' }}{{ number_format((float)($req['amaun'] ?? 0), 2) }}{{ $st ?: '-' }}{{ $req['dimohon_oleh'] ?? '-' }}{{ $req['diluluskan_oleh'] ?? '-' }}{{ $req['nota'] ?? '-' }}
Belum ada permohonan.
+
+
+
+
+
+
+
+@endsection diff --git a/resources/views/penebusan/v1/index-khazanah.blade.php b/resources/views/penebusan/v1/index-khazanah.blade.php index 6d441b7f..a15206bf 100644 --- a/resources/views/penebusan/v1/index-khazanah.blade.php +++ b/resources/views/penebusan/v1/index-khazanah.blade.php @@ -340,7 +340,7 @@ "ordering": false }); - var api = ''; + var api = "{{ api_url_for_browser() }}"; let nw = 0; $('input[type="checkbox"]').hide(); diff --git a/resources/views/penebusan/v1/index.blade.php b/resources/views/penebusan/v1/index.blade.php index 12e7ad3c..a43f13de 100644 --- a/resources/views/penebusan/v1/index.blade.php +++ b/resources/views/penebusan/v1/index.blade.php @@ -371,7 +371,7 @@ "ordering": false }); - var api = ''; + var api = "{{ api_url_for_browser() }}"; let nw = 0; $('input[type="checkbox"]').hide(); diff --git a/resources/views/penebusan/v2/index-khazanah.blade.php b/resources/views/penebusan/v2/index-khazanah.blade.php index a99406b4..54a367fa 100644 --- a/resources/views/penebusan/v2/index-khazanah.blade.php +++ b/resources/views/penebusan/v2/index-khazanah.blade.php @@ -470,7 +470,7 @@ "ordering": false }); - var api = ''; + var api = "{{ api_url_for_browser() }}"; let nw = 0; let jenisAT = 'none'; diff --git a/resources/views/penebusan/v2/index.blade-backup.php b/resources/views/penebusan/v2/index.blade-backup.php index 3522f503..e4fb41c2 100644 --- a/resources/views/penebusan/v2/index.blade-backup.php +++ b/resources/views/penebusan/v2/index.blade-backup.php @@ -652,7 +652,7 @@ "ordering": false }); - var api = ''; + var api = "{{ api_url_for_browser() }}"; let nw = 0; let jenisAT = 'none'; diff --git a/resources/views/penebusan/v2/index.blade.php b/resources/views/penebusan/v2/index.blade.php index c3d88f26..77daaa21 100644 --- a/resources/views/penebusan/v2/index.blade.php +++ b/resources/views/penebusan/v2/index.blade.php @@ -1130,7 +1130,7 @@ let pertambahpinjaman = "{{ $pertambahpinjaman }}"; - var api = ''; + var api = "{{ api_url_for_browser() }}"; let nw = 0; let jenisAT = 'none'; @@ -1813,7 +1813,7 @@ }); try { - const pampasanListResponse = await fetch(`{{ config('api.url') }}/api/palsu/laporan?${params}`, { + const pampasanListResponse = await fetch(`{{ api_url_for_browser() }}/api/palsu/laporan?${params}`, { method: 'GET', headers: { 'Content-Type': 'application/json', diff --git a/resources/views/print/borang_batch_pembiayaan_online.blade.php b/resources/views/print/borang_batch_pembiayaan_online.blade.php new file mode 100644 index 00000000..91d743a7 --- /dev/null +++ b/resources/views/print/borang_batch_pembiayaan_online.blade.php @@ -0,0 +1,336 @@ + + + + + + Borang Kelulusan Batch — {{ $batchNo }} + + + + + +
+
Borang Kelulusan Batch Pembiayaan Online
+ +
+ +
+
+ + +
+

Borang Kelulusan Pembiayaan Online
Senarai Batch Pembayaran

+

Untuk semakan Operasi dan kelulusan Pengurus Besar. Bukan untuk tandatangan pelanggan.

+
+ +
+
A. Maklumat Batch
+ + + + + + + + + + + + + + + + + + + + + + + + + +
No. Batch{{ $batchNo }}Cawangan{{ $kodcaw }} — {{ $cawangan }}
Tarikh / Slot{{ $tarikh }}  ·  {{ $slot }}Status Batch{{ $status }}
Dihantar Oleh (PC){{ $dihantarOleh }}Dihantar Pada{{ $dihantarPada }}
Bilangan Rekod{{ count($lines) }}Jumlah KeseluruhanRM {{ number_format((float) $jumlah, 2) }}
+
+ +
+
B. Senarai Penerima Dalam Batch
+ + + + + + + + + + + + + + + @forelse($lines as $i => $line) + + + + + + + + + + + @empty + + + + @endforelse + + + + + + + +
BilNo. SAGPelangganNo. KPBankNo. AkaunNama AkaunPinjaman (RM)
{{ $i + 1 }}{{ $line['norujukan'] ?? '-' }}{{ $line['nama_pelanggan'] ?? '-' }}{{ $line['nokp'] ?? '-' }}{{ $line['nama_bank'] ?? ($line['kodbank'] ?? '-') }}{{ $line['noakaun'] ?? '-' }}{{ $line['nama_akaun'] ?? '-' }}{{ number_format((float) ($line['pinjaman'] ?? 0), 2) }}
Tiada rekod dalam batch.
JUMLAH{{ number_format((float) $jumlah, 2) }}
+
+ +
+
C. Pengesahan Operasi
+
+ Dengan ini disahkan bahawa senarai di Bahagian B telah disemak dan amaun pembiayaan + sebanyak RM {{ number_format((float) $jumlah, 2) }} ({{ count($lines) }} rekod) + bagi batch {{ $batchNo }} cawangan {{ $cawangan }} adalah lengkap + untuk proses pembayaran ke akaun bank pelanggan. +
    +
  1. Maklumat SAG, nama, no. KP, bank dan no. akaun telah disemak terhadap rekod sistem.
  2. +
  3. Pembayaran akan dikreditkan ke akaun penerima seperti senarai, bukan tunai di kaunter.
  4. +
  5. Sebarang ralat akaun yang dikesan selepas kelulusan hendaklah dilaporkan serta-merta.
  6. +
+
+
+ +
+
+
Tandatangan Operasi
+
+

Nama: {{ $opsBy }}

+

Jawatan: Operasi

+

Tarikh: {{ $tarikh }}

+
+
+
+
Pengurus Besar
+
+

Nama:

+

Cop / Paraf:

+

Tarikh:

+
+
+
+ + + Nota: +

1. Borang ini dicetak dari skrin Operasi — Pembiayaan Online (Batch) untuk kelulusan pembayaran.

+

2. Hanya dua tandatangan diperlukan: Operasi dan Pengurus Besar.

+

3. Simpan salinan bersama bukti pindahan bank selepas status PAID.

+
+
+
+ + diff --git a/resources/views/print/borang_pengesahan_online.blade.php b/resources/views/print/borang_pengesahan_online.blade.php new file mode 100644 index 00000000..b6c98b97 --- /dev/null +++ b/resources/views/print/borang_pengesahan_online.blade.php @@ -0,0 +1,302 @@ + + + + + + Borang Pengesahan Online — {{ $norujukan }} + + + + + +
+
Borang Pengesahan Pembiayaan Online
+ +
+ +
+
+ + +
+

Borang Pengesahan Penerimaan Pembiayaan
Secara Online (Akaun Bank)

+

Untuk diisi dan ditandatangani oleh pelanggan semasa transaksi gadai.

+
+ +
+
A. Maklumat Pelanggan
+ + + + + + + + + + + + + + + + + +
Nama Pelanggan{{ $nama }}
No. MyKad / Passport{{ $nokp }}No. Pelanggan{{ $nopelanggan }}
No. Telefon{{ $telefon }}Cawangan{{ $cawangan }}
+
+ +
+
B. Maklumat Pembiayaan
+ + + + + + + + + + + + + + + + + +
No. Surat Akuan Gadai{{ $norujukan }}
Amaun PembiayaanRM {{ $pinjaman }}
Cara Terima PembiayaanOnline (kredit ke akaun bank) — bukan tunai
Teller{{ $teller }}
+
+ +
+
C. Maklumat Akaun Bank Penerima
+ + + + + + + + + + + + + +
Nama Bank{{ $bank }}
No. Akaun{{ $noakaun }}
Nama Pemegang Akaun{{ $nama_akaun }}
+
+ +
+
D. Akuan Pelanggan
+
+ Saya, {{ $nama }} (No. KP: {{ $nokp }}), dengan ini mengesahkan bahawa: +
    +
  1. Maklumat akaun bank di Bahagian C adalah benar, tepat, dan milik saya sendiri.
  2. +
  3. Saya memohon supaya amaun pembiayaan gadai sebanyak RM {{ $pinjaman }} dikreditkan ke akaun tersebut dan bukan diterima secara tunai.
  4. +
  5. Saya faham kredit ke akaun tertakluk kepada semakan Pengurus Cawangan dan proses pembayaran pihak Koperasi.
  6. +
  7. Saya bertanggungjawab sepenuhnya jika maklumat akaun tidak tepat, dan tidak akan menuntut ganti rugi atas kelewatan atau ralat yang berpunca daripadanya.
  8. +
+
+
+ +
+
+
Tandatangan Pelanggan
+
+

Nama: {{ $nama }}

+

No. KP: {{ $nokp }}

+

Tarikh: {{ $tarikh }}

+
+
+
+
Tandatangan Teller
+
+

Nama: {{ $teller }}

+

Cop / Paraf:

+

Tarikh: {{ $tarikh }}

+
+
+
+
Pengurus Cawangan
+
+

Nama:

+

Cop cawangan:

+

Tarikh:

+
+
+
+ + + Nota: +

1. Borang ini dicetak semasa pelanggan memilih cara terima pembiayaan secara Online.

+

2. Pembiayaan akan dikreditkan ke akaun selepas semakan PC. Pelanggan tidak menerima wang tunai di kaunter.

+

3. Sila pastikan nama pemegang akaun sepadan dengan nama pelanggan pada MyKad.

+
+
+
+ + diff --git a/resources/views/print/maklumat_pelanggan.blade.php b/resources/views/print/maklumat_pelanggan.blade.php new file mode 100644 index 00000000..aec8c4cd --- /dev/null +++ b/resources/views/print/maklumat_pelanggan.blade.php @@ -0,0 +1,260 @@ + + + + + + Maklumat Pelanggan — {{ $nopelanggan }} + + + +
+
+ Maklumat Pelanggan + — {{ $nama }} +
+ +
+ +
+
+ + +
+

Maklumat Pelanggan

+

Salinan maklumat pengenalan diri pelanggan untuk rujukan cawangan.

+
+ +
+
+
A. Pengenalan Diri
+ + + + + + + + + + + + + + + + + +
Nama{{ $nama }}
No. Kad Pengenalan{{ $nokp }}
No. Pelanggan{{ $nopelanggan }}
Status{{ $status }}
+
+
+ Gambar MyKad +
Gambar MyKad
+
+
+ +
+
B. Maklumat Hubungan & Alamat
+ + + + + + + + + + + + + + + + + + + + + +
No. Telefon 1{{ $telefon }}No. Telefon 2{{ $telefon2 }}
Alamat{{ $alamat }}
Poskod{{ $poskod }}Daerah{{ $daerah }}
Negeri{{ $negeri }}
+
+ +
+
C. Bank & Pekerjaan
+ + + + + + + + + + + + + + + + + +
Bank{{ $bank }}
No. Akaun{{ $noakaun }}
Pekerjaan{{ $pekerjaan }}
Nota{{ $nota }}
+
+ + + Nota: +

1. Borang ini dicetak dari skrin Maklumat Pelanggan untuk rujukan cawangan.

+

2. Gambar diambil daripada rekod MyKad pelanggan dalam sistem.

+
+
+
+ + diff --git a/routes/web.php b/routes/web.php index 020f7660..83bd92f5 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,19 @@ Route::get('/health', function () { return response()->json(['status' => 'ok']); }); +// Dummy print form — no auth, for layout design only +Route::get('/dummy/borang-pengesahan-online', function () { + return view('dummy.borang_pengesahan_online'); +}); + +Route::get('/dummy/borang-batch-pembiayaan-online', function () { + return view('dummy.borang_batch_pembiayaan_online'); +}); + +Route::get('/dummy/maklumat-pelanggan', function () { + return view('dummy.maklumat_pelanggan'); +}); + Route::get('/',['as'=>'login','uses'=>'HomeController@index']); Route::get('/testtry',function(ImbangdugaServices $imbangduga){ @@ -81,12 +94,19 @@ Route::get('login', 'Auth\LoginController@login')->name('login'); Route::post('login', 'Auth\LoginController@authenticate'); // Route::get('logout', 'Auth\LoginController@logout')->name('logout'); +// Impersonation (lab404/laravel-impersonate) +Route::post('/impersonate/leave', ['as' => 'impersonate.leave', 'uses' => 'Auth\ImpersonateController@leave'])->middleware('auth'); +Route::post('/impersonate/{id}', ['as' => 'impersonate.start', 'uses' => 'Auth\ImpersonateController@impersonate']) + ->where('id', '[0-9]+') + ->middleware('auth'); + Route::get('/', 'HomeController@index')->name('home'); Route::get('/home', 'HomeController@index')->name('home'); //Teller Route::get('/pelanggan','CustomersController@index')->middleware('auth','teller'); Route::get('/customer_info',['as'=>'customer_info','uses'=>'CustomersController@info'])->middleware('auth','teller'); +Route::get('/customer/maklumat/cetak',['as'=>'customer.print.maklumat','uses'=>'CustomersController@printMaklumat'])->middleware('auth','teller'); Route::get('/gadaian',['as'=>'gadaian','uses'=>'GadaianController@index'])->middleware('auth','teller'); Route::get('/daftar_marhun',['as'=>'daftar_marhun','uses'=>'MarhunController@index'])->middleware('auth','teller'); Route::get('/penebusan/{norujukan}',['as'=>'penebusan','uses' => 'PenebusanController@index'])->middleware('auth'); @@ -107,10 +127,13 @@ Route::post('/profile/edit/store',['as'=>'profile.editstore','uses'=>'UserContro Route::get('/customer/search',['as'=>'customer.search','uses'=>'CustomersController@search'])->middleware('auth','teller'); Route::post('/customer/store',['as'=>'customer.store','uses'=>'CustomersController@store'])->middleware('auth','teller'); Route::post('/gadai/store',['as'=>'gadai.store','uses'=>'GadaianController@store'])->middleware('auth','teller'); +Route::get('/gadai/borang-pengesahan-online/{norujukan}',['as'=>'gadai.print.borang.online','uses'=>'GadaianController@printBorangPengesahanOnline'])->middleware('auth','teller'); +Route::post('/gadai/borang-pengesahan-online/preview',['as'=>'gadai.print.borang.online.preview','uses'=>'GadaianController@previewBorangPengesahanOnline'])->middleware('auth','teller'); Route::get('/gadai/batal',['as'=>'gadai.batal','uses'=>'GadaianController@batal'])->middleware('auth','teller'); Route::get('/customer/otp',['as'=>'customer.otp','uses'=>'CustomersController@getOTP'])->middleware('auth','teller'); 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('/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'); @@ -166,6 +189,34 @@ Route::post('/khazanah/pendahuluanserahanHQ',['as'=>'khazanah.pendahuluanserahan Route::get('/penebusankhazanah/{norujukan}',['as'=>'penebusan.khazanah','uses' => 'PenebusanController@PenebusanKhazanah'])->middleware('auth','khazanah'); Route::get('/lelongankhazanah/{norujukan}',['as'=>'lelongan.khazanah','uses' => 'KhazanahController@lelongan'])->middleware('auth','khazanah'); Route::get('/permohonan',['as'=>'permohonan.index','uses' => 'KhazanahController@PaparanPermohonan'])->middleware('auth','khazanah'); + +// Pembiayaan Online — PC +Route::get('/pembiayaan-online/pc',['as'=>'pembiayaan.online.pc','uses'=>'PembiayaanOnlineController@index'])->middleware('auth','khazanah'); +Route::put('/pembiayaan-online/pc/lines/{id}',['as'=>'pembiayaan.online.pc.update','uses'=>'PembiayaanOnlineController@updateLine'])->middleware('auth','khazanah'); +Route::post('/pembiayaan-online/pc/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'); +Route::get('/pembiayaan-online/pc/paid',['as'=>'pembiayaan.online.pc.paid','uses'=>'PembiayaanOnlineController@paidIndex'])->middleware('auth','khazanah'); +Route::get('/pembiayaan-online/pc/lines/{id}/bukti',['as'=>'pembiayaan.online.pc.bukti','uses'=>'PembiayaanOnlineController@viewBukti'])->middleware('auth','khazanah'); +Route::post('/pembiayaan-online/pc/paid/blast',['as'=>'pembiayaan.online.pc.blast','uses'=>'PembiayaanOnlineController@markBlast'])->middleware('auth','khazanah'); +Route::get('/pembiayaan-online/pc/topup',['as'=>'pembiayaan.online.pc.topup','uses'=>'PembiayaanOnlineController@topupIndex'])->middleware('auth','khazanah'); +Route::post('/pembiayaan-online/pc/topup',['as'=>'pembiayaan.online.pc.topup.request','uses'=>'PembiayaanOnlineController@topupRequest'])->middleware('auth','khazanah'); +Route::post('/pembiayaan-online/pc/topup/close',['as'=>'pembiayaan.online.pc.topup.close','uses'=>'PembiayaanOnlineController@closeModal'])->middleware('auth','khazanah'); + +// Pembiayaan Online — Operasi +Route::get('/pembiayaan-online/operasi',['as'=>'pembiayaan.online.operasi','uses'=>'PembiayaanOnlineController@operasiIndex'])->middleware('auth','operasi'); +Route::get('/pembiayaan-online/operasi/topup',['as'=>'pembiayaan.online.operasi.topup','uses'=>'PembiayaanOnlineController@operasiTopupIndex'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/topup/{id}/approve',['as'=>'pembiayaan.online.operasi.topup.approve','uses'=>'PembiayaanOnlineController@operasiTopupApprove'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/topup/{id}/reject',['as'=>'pembiayaan.online.operasi.topup.reject','uses'=>'PembiayaanOnlineController@operasiTopupReject'])->middleware('auth','operasi'); +Route::get('/pembiayaan-online/operasi/eod',['as'=>'pembiayaan.online.operasi.eod','uses'=>'PembiayaanOnlineController@operasiEodIndex'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/eod/close-all',['as'=>'pembiayaan.online.operasi.eod.closeall','uses'=>'PembiayaanOnlineController@operasiCloseAllModal'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/eod/{kodcaw}/close',['as'=>'pembiayaan.online.operasi.eod.close','uses'=>'PembiayaanOnlineController@operasiCloseModal'])->middleware('auth','operasi'); +Route::get('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}',['as'=>'pembiayaan.online.operasi.batch','uses'=>'PembiayaanOnlineController@operasiShowBatch'])->middleware('auth','operasi'); +Route::get('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/print',['as'=>'pembiayaan.online.operasi.batch.print','uses'=>'PembiayaanOnlineController@operasiPrintBatch'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/approve',['as'=>'pembiayaan.online.operasi.approve','uses'=>'PembiayaanOnlineController@operasiApproveLine'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/{kodcaw}/lines/{id}/reject',['as'=>'pembiayaan.online.operasi.reject','uses'=>'PembiayaanOnlineController@operasiRejectLine'])->middleware('auth','operasi'); +Route::post('/pembiayaan-online/operasi/{kodcaw}/batches/{batchId}/approve',['as'=>'pembiayaan.online.operasi.approve.batch','uses'=>'PembiayaanOnlineController@operasiApproveBatch'])->middleware('auth','operasi'); +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('/permohonan/rekod',['as'=>'permohonan.rekod','uses' => 'KhazanahController@RekodPermohonan'])->middleware('auth','khazanah'); Route::post('/permohonan/simpan',['as'=>'permohonan.simpan','uses' => 'KhazanahController@SimpanPermohonan'])->middleware('auth','khazanah'); Route::get('/permohonan/status/{mohonid}',['as'=>'permohonan.status','uses' => 'KhazanahController@StatusPermohonan'])->middleware('auth','khazanah'); @@ -350,6 +401,8 @@ Route::post('/barcodescanned',['as'=>'barcode.scanned','uses'=>'KhazanahControll Route::get('/operasi/kelulusan','OperasiController@kelulusan')->name('operasi.kelulusan')->middleware('auth','operasi'); Route::get('/operasi/kelulusan/anggota','OperasiController@kelulusanAnggota')->name('operasi.kelulusananggota')->middleware('auth','operasi'); Route::post('/operasi/createkelulusan',['as'=>'operasi.createkelulusan','uses'=>'OperasiController@createkelulusan'])->middleware('auth','teller'); +Route::post('/operasi/createkelulusan-by-khazanah',['as'=>'operasi.createkelulusan-by-khazanah','uses'=>'OperasiController@createkelulusanByKhazanah'])->middleware('auth','khazanah'); +Route::post('/operasi/createkelulusan-anggota',['as'=>'operasi.createkelulusan-anggota','uses'=>'OperasiController@createkelulusanAnggota'])->middleware('auth','teller'); Route::get('/operasi/getkelulusan',['as'=>'operasi.getkelulusan','uses'=>'OperasiController@getkelulusan'])->middleware('auth','teller'); Route::put('/operasi/updatekelulusan',['as'=>'operasi.updatekelulusan','uses'=>'OperasiController@updatekelulusan'])->middleware('auth','operasi'); Route::put('/operasi/updatekelulusananggota',['as'=>'operasi.updatekelulusananggota','uses'=>'OperasiController@updatekelulusananggota'])->middleware('auth','operasi'); @@ -416,6 +469,8 @@ Route::get('admin/pelanggan',['as'=>'admin.pelanggan','uses'=>'KhazanahControlle Route::get('admin/customer_info',['as'=>'admin.customer_info','uses'=>'KhazanahController@infoAdmin'])->middleware('auth','admin'); Route::get('admin/searchpelanggan',['as'=>'admin.searchpelanggan','uses'=>'KhazanahController@searchAdmin'])->middleware('auth','admin'); Route::get('admin/poskod',['as'=>'admin.poskod','uses'=>'HomeController@poskod']); +Route::get('admin/features',['as'=>'admin.features','uses'=>'AdminFeatureController@index'])->middleware('auth','admin'); +Route::post('admin/features',['as'=>'admin.features.update','uses'=>'AdminFeatureController@update'])->middleware('auth','admin'); Route::get('pembeliannotifykomuditi',['as'=>'beli.notifyKomoditi','uses'=>'OperasiController@notifyKomoditi'])->middleware('auth','operasi');