2 Commits

5 changed files with 315 additions and 105 deletions
+2 -1
View File
@@ -63,7 +63,8 @@ class Customer extends Model
'nota_pekerjaan',
'tagpelanggan',
'tag_anggota',
'gambar_mykad'
'gambar_mykad',
'scan_ic'
];
/**
+199 -6
View File
@@ -370,6 +370,7 @@ class CustomerController extends Controller
]);
$this->attachMykadPhoto($customer, $request);
$this->attachIcScan($customer, $request);
$audit = new Audit();
$audit->users = $request->teller;
@@ -1160,6 +1161,81 @@ public function AdminsearchCustomerBySAG(Request $request)
], 200);
}
/**
* Store or replace the scanned IC / passport document for a customer.
*/
public function storeIcScan($ic, Request $request)
{
$customer = Customer::on('mysql7')
->where('kplama', '=', $ic)
->orWhere('kpbaru', '=', $ic)
->first();
if (!$customer) {
return response()->json(['error' => 'Customer not found.', 'stored' => false], 404);
}
$fileId = $this->storeCustomerDocument(
$this->fileBase64FromRequest($request),
$request->input('file_mime', $request->json('file_mime')),
$request->input('filename', $request->json('filename')),
$customer,
'scan-ic',
4 * 1024 * 1024
);
if (!$fileId) {
return response()->json([
'error' => 'Fail scan IC tidak sah. Sila muat naik JPG, PNG atau PDF (maks 4MB).',
'stored' => false,
], 422);
}
Customer::on('mysql7')
->where('kpbaru', $customer->kpbaru)
->where('kplama', $customer->kplama)
->update(['scan_ic' => $fileId]);
$customer->scan_ic = $fileId;
return response()->json([
'message' => 'Scan dokumen IC berjaya dimuat naik.',
'scan_ic' => $customer->scan_ic,
'has_scan_ic' => true,
'stored' => true,
], 200);
}
public function showIcScan($ic)
{
$customer = Customer::on('mysql7')
->where('kplama', '=', $ic)
->orWhere('kpbaru', '=', $ic)
->first();
if (!$customer) {
return response()->json(['error' => 'Customer not found.'], 404);
}
if (empty($customer->scan_ic)) {
return response()->json(['error' => 'Scan IC belum dimuat naik.'], 404);
}
$file = ARFiles::on('mysql7')->where('id', $customer->scan_ic)->first();
if (!$file || empty($file->data)) {
return response()->json(['error' => 'Fail scan IC tidak dijumpai.'], 404);
}
return response()->json([
'file' => [
'data' => $file->data,
'type' => $file->type ?: 'application/octet-stream',
'filename' => $file->filename ?: 'scan-ic',
],
'has_scan_ic' => true,
]);
}
/**
* Save MyKad photo into arrahn_files and link it on customer.gambar_mykad.
*/
@@ -1191,6 +1267,41 @@ public function AdminsearchCustomerBySAG(Request $request)
}
}
protected function attachIcScan($customer, Request $request)
{
if (!$customer) {
return;
}
$raw = $this->fileBase64FromRequest($request);
if ($raw === '') {
return;
}
try {
$fileId = $this->storeCustomerDocument(
$raw,
$request->input('file_mime', $request->json('file_mime')),
$request->input('filename', $request->json('filename')),
$customer,
'scan-ic',
4 * 1024 * 1024
);
if (!$fileId) {
return;
}
Customer::on('mysql7')
->where('kpbaru', $customer->kpbaru)
->where('kplama', $customer->kplama)
->update(['scan_ic' => $fileId]);
$customer->scan_ic = $fileId;
} catch (\Throwable $e) {
Log::error('Failed to attach IC scan: ' . $e->getMessage());
}
}
protected function photoBase64FromRequest(Request $request)
{
$raw = $request->input('photo_base64');
@@ -1254,20 +1365,102 @@ public function AdminsearchCustomerBySAG(Request $request)
$payload = $customer->toArray();
$payload['photo_base64'] = null;
$payload['photo_mime'] = null;
$payload['has_scan_ic'] = !empty($customer->scan_ic);
$payload['scan_ic'] = $customer->scan_ic;
$payload['scan_ic_mime'] = null;
$payload['scan_ic_filename'] = null;
if (empty($customer->gambar_mykad)) {
return $payload;
if (!empty($customer->scan_ic)) {
$scanFile = ARFiles::on('mysql7')->select('type', 'filename')->where('id', $customer->scan_ic)->first();
if ($scanFile) {
$payload['scan_ic_mime'] = $scanFile->type ?: null;
$payload['scan_ic_filename'] = $scanFile->filename ?: null;
}
}
$file = ARFiles::on('mysql7')->where('id', $customer->gambar_mykad)->first();
if ($file && $file->data) {
$payload['photo_base64'] = $file->data;
$payload['photo_mime'] = $file->type ?: 'image/jpeg';
if (!empty($customer->gambar_mykad)) {
$file = ARFiles::on('mysql7')->where('id', $customer->gambar_mykad)->first();
if ($file && $file->data) {
$payload['photo_base64'] = $file->data;
$payload['photo_mime'] = $file->type ?: 'image/jpeg';
}
}
return $payload;
}
protected function fileBase64FromRequest(Request $request)
{
$raw = $request->input('file_base64');
if ($raw === null || $raw === '') {
$raw = $request->json('file_base64');
}
if ($raw === null || $raw === '') {
$raw = $this->photoBase64FromRequest($request);
}
return is_string($raw) ? $raw : '';
}
protected function storeCustomerDocument($raw, $mime, $filename, $customer, $fallbackName, $maxBytes)
{
$commaPos = strpos($raw, ',');
if ($commaPos !== false) {
$raw = substr($raw, $commaPos + 1);
}
$raw = preg_replace('/\s+/', '', $raw);
if ($raw === '') {
return null;
}
$decoded = base64_decode($raw, true);
if ($decoded === false || $decoded === '') {
Log::warning('Customer document skipped: invalid base64');
return null;
}
if (strlen($decoded) > $maxBytes) {
Log::warning('Customer document skipped: payload too large');
return null;
}
$allowed = [
'image/jpeg' => 'jpg',
'image/jpg' => 'jpg',
'image/png' => 'png',
'application/pdf' => 'pdf',
];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$detected = $finfo ? finfo_buffer($finfo, $decoded) : false;
if ($finfo) {
finfo_close($finfo);
}
$detected = strtolower((string) ($detected ?: ''));
$claimed = strtolower((string) ($mime ?: ''));
if (isset($allowed[$detected])) {
$mime = $detected;
} elseif (isset($allowed[$claimed]) && ($claimed === 'image/jpg' || $claimed === $detected)) {
$mime = $claimed;
} else {
return null;
}
$nric = preg_replace('/[^0-9A-Za-z]/', '', (string) ($customer->kpbaru ?: $customer->kplama ?: ''));
$ext = $allowed[$mime];
$safeName = $filename ? pathinfo((string) $filename, PATHINFO_FILENAME) : '';
$safeName = preg_replace('/[^0-9A-Za-z_-]/', '', $safeName);
$file = new ARFiles();
$file->data = $raw;
$file->filename = ($nric !== '' ? $nric . '-' : '') . ($safeName !== '' ? $safeName : $fallbackName) . '.' . $ext;
$file->type = $mime;
$file->tarikhmasa = Carbon::now()->toDateTimeString();
$file->save();
return $file->id;
}
// public function createNotaArcc($kodcaw, Request $request)
// {
// $current = Carbon::now();
+66 -98
View File
@@ -53,7 +53,6 @@ class StokAuditController extends Controller
$hinggatempoh = $request->hinggatempoh;
$cawangan = Cawangan::on('mysql7')->where('kodcaw', '=', $request->kodcaw)->first();
// Query asal gadai
$audit = Gadai::on($cawangan['mysql'])
->where([
['tagpalsu', '=', 0],
@@ -66,47 +65,7 @@ class StokAuditController extends Controller
->orderBy('sirig', 'ASC')
->get();
// Dapatkan semua norujukan awal
$arraynorujukan = $audit->pluck('norujukan')->toArray();
// Dapatkan tarikh terkini untuk setiap norujukan ikut trans_type T
$tarikhTerkiniT = Transaction::on('mysql7')
->select('norujukan', DB::raw('MAX(created_at) as tarikh_max'))
->where('trans_type', 'T')
->whereIn('norujukan', $arraynorujukan)
->groupBy('norujukan')
->pluck('tarikh_max', 'norujukan')
->toArray();
// Dapatkan tarikh terkini untuk setiap norujukan ikut trans_type A
$tarikhTerkiniA = Transaction::on('mysql7')
->select('norujukan', DB::raw('MAX(created_at) as tarikh_max'))
->where('trans_type', 'A')
->whereIn('norujukan', $arraynorujukan)
->groupBy('norujukan')
->pluck('tarikh_max', 'norujukan')
->toArray();
// Filter audit ikut tarikh masing-masing
$rekodaudit = $audit->filter(function ($item) use ($tarikhTerkiniT, $tarikhTerkiniA) {
$norujukan = $item->norujukan;
$adaTebusTerkini = isset($tarikhTerkiniT[$norujukan]) &&
Transaction::on('mysql7')
->where('norujukan', $norujukan)
->where('trans_type', 'T')
->whereDate('created_at', '>=', date('Y-m-d', strtotime($tarikhTerkiniT[$norujukan])))
->exists();
$adaAnsuranTerkini = isset($tarikhTerkiniA[$norujukan]) &&
Transaction::on('mysql7')
->where('norujukan', $norujukan)
->where('trans_type', 'A')
->whereDate('created_at', '>=', date('Y-m-d', strtotime($tarikhTerkiniA[$norujukan])))
->exists();
return !$adaTebusTerkini && !$adaAnsuranTerkini;
})->values();
$rekodaudit = $this->filterRekodAuditKhas($audit);
// Gunakan hanya norujukan yang dah ditapis
$arraynorujukan = $rekodaudit->pluck('norujukan')->toArray();
@@ -179,13 +138,17 @@ class StokAuditController extends Controller
$audit = Gadai::on($cawangan['mysql'])
->where([['tagpalsu','=',0],['tempoh','>=',$daritempoh],['tempoh','<=',$hinggatempoh],['sttebus','=',''],['taglel','=',0]])
->orderBy('t_gadai', 'ASC')
->orderBy('sirig', 'ASC');
// ->get();
// dd($audit);
$rekodaudit = $audit->get();
$arraynorujukan = $audit->pluck('norujukan')->toArray();
->orderBy('sirig', 'ASC')
->get();
$rekodaudit = $this->filterRekodAuditKhas($audit);
$arraynorujukan = $rekodaudit->pluck('norujukan')->toArray();
$keybynorujukan = $rekodaudit->keyBy('norujukan');
if (empty($arraynorujukan)) {
return response()->json('success');
}
$gadainokp = Gadai::on($cawangan['mysql'])->whereIn('norujukan', $arraynorujukan)
->select('nokp')
->groupBy('nokp')
@@ -261,12 +224,17 @@ class StokAuditController extends Controller
$audit = Gadai::on($cawangan['mysql'])
->where([['tagpalsu','=',0],['tempoh','>=',$daritempoh],['tempoh','<=',$hinggatempoh],['sttebus','=',''],['taglel','=',0]])
->orderBy('t_gadai', 'ASC')
->orderBy('sirig', 'ASC');
->orderBy('sirig', 'ASC')
->get();
$rekodaudit = $audit->get();
$arraynorujukan = $audit->pluck('norujukan')->toArray();
$rekodaudit = $this->filterRekodAuditKhas($audit);
$arraynorujukan = $rekodaudit->pluck('norujukan')->toArray();
$keybynorujukan = $rekodaudit->keyBy('norujukan');
if (empty($arraynorujukan)) {
return response()->json('success');
}
$gadainokp = Gadai::on($cawangan['mysql'])->whereIn('norujukan', $arraynorujukan)
// ->select('nokp')
// ->groupBy('nokp')
@@ -441,7 +409,6 @@ class StokAuditController extends Controller
$cawangan = Cawangan::on('mysql7')->where('kodcaw', '=', $request->kodcaw)->first();
// Ambil semua rekod gadai asas
$audit = Gadai::on($cawangan['mysql'])
->where([
['tagpalsu', '=', 0],
@@ -450,52 +417,10 @@ class StokAuditController extends Controller
['sttebus', '=', ''],
['taglel', '=', 0],
])
->orderBy('norujukan', 'ASC') // susun ikut norujukan
->orderBy('norujukan', 'ASC')
->get();
$arraynorujukan = $audit->pluck('norujukan')->toArray();
// Tarikh terkini ikut trans_type T
$tarikhTerkiniT = Transaction::on('mysql7')
->select('norujukan', DB::raw('MAX(created_at) as tarikh_max'))
->where('trans_type', 'T')
->whereIn('norujukan', $arraynorujukan)
->groupBy('norujukan')
->pluck('tarikh_max', 'norujukan')
->toArray();
// Tarikh terkini ikut trans_type A
$tarikhTerkiniA = Transaction::on('mysql7')
->select('norujukan', DB::raw('MAX(created_at) as tarikh_max'))
->where('trans_type', 'A')
->whereIn('norujukan', $arraynorujukan)
->groupBy('norujukan')
->pluck('tarikh_max', 'norujukan')
->toArray();
// Filter audit
$rekodaudit = $audit->filter(function ($item) use ($tarikhTerkiniT, $tarikhTerkiniA) {
if ($item->lelong_tawarruq == 0) {
return true;
}
$norujukan = $item->norujukan;
$adaTebusTerkini = isset($tarikhTerkiniT[$norujukan]) &&
Transaction::on('mysql7')
->where('norujukan', $norujukan)
->where('trans_type', 'T')
->whereDate('created_at', '>=', date('Y-m-d', strtotime($tarikhTerkiniT[$norujukan])))
->exists();
$adaAnsuranTerkini = isset($tarikhTerkiniA[$norujukan]) &&
Transaction::on('mysql7')
->where('norujukan', $norujukan)
->where('trans_type', 'A')
->whereDate('created_at', '>=', date('Y-m-d', strtotime($tarikhTerkiniA[$norujukan])))
->exists();
return !$adaTebusTerkini && !$adaAnsuranTerkini;
})->values();
$rekodaudit = $this->filterRekodAuditKhas($audit);
// Attach customer info + blast untuk setiap norujukan
foreach ($rekodaudit as $index => $rekod) {
@@ -563,7 +488,50 @@ class StokAuditController extends Controller
return ['ujrah' => $ujrah_semasa, 'onepercent' => $onepercent_semasa];
}
// dalam Controller (private/protected)
/**
* SAG untuk blast Stok Audit Khas:
* - Fasa 1 (belum lanjut): buang jika dah ada tebus (T) atau bayaran ansuran (A).
* - Fasa 2 (dah lanjut, lelong_tawarruq=0): buang jika masih sekitar matang pertama (tempoh < 11).
* Kekalkan tempoh >= 11 untuk peringatan tamat kontrak.
*/
private function filterRekodAuditKhas($audit)
{
$arraynorujukan = $audit->pluck('norujukan')->filter()->unique()->values()->all();
$norujukanWithT = [];
$norujukanWithA = [];
if (!empty($arraynorujukan)) {
$norujukanWithT = array_flip(
Transaction::on('mysql7')
->where('trans_type', 'T')
->whereIn('norujukan', $arraynorujukan)
->distinct()
->pluck('norujukan')
->all()
);
$norujukanWithA = array_flip(
Transaction::on('mysql7')
->where('trans_type', 'A')
->whereIn('norujukan', $arraynorujukan)
->distinct()
->pluck('norujukan')
->all()
);
}
return $audit->filter(function ($item) use ($norujukanWithT, $norujukanWithA) {
if ((int) $item->lelong_tawarruq === 0) {
return (float) $item->tempoh >= 11;
}
$norujukan = $item->norujukan;
return !isset($norujukanWithT[$norujukan]) && !isset($norujukanWithA[$norujukan]);
})->values();
}
private function floorTo5SenFrom2dp($amount)
{
// pastikan numeric
@@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddScanIcToCustomerTable extends Migration
{
/**
* Master DB (mysql7) — stores arrahn_files.id for the scanned IC document.
*
* @return void
*/
public function up()
{
if (
Schema::connection('mysql7')->hasTable('customer')
&& !Schema::connection('mysql7')->hasColumn('customer', 'scan_ic')
) {
Schema::connection('mysql7')->table('customer', function (Blueprint $table) {
if (Schema::connection('mysql7')->hasColumn('customer', 'gambar_mykad')) {
$table->unsignedBigInteger('scan_ic')->nullable()->after('gambar_mykad');
} else {
$table->unsignedBigInteger('scan_ic')->nullable();
}
});
}
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if (
Schema::connection('mysql7')->hasTable('customer')
&& Schema::connection('mysql7')->hasColumn('customer', 'scan_ic')
) {
Schema::connection('mysql7')->table('customer', function (Blueprint $table) {
$table->dropColumn('scan_ic');
});
}
}
}
+2
View File
@@ -58,6 +58,8 @@ $router->group(['prefix'=>'api'], function () use ($router){
$router->get('customers/{ic}/gadai/{status}/{kodcaw}',['uses'=>'CustomerController@showOneCustomerGadaiByCawangan']);
$router->post('customers/create/{kodcaw}',['uses'=>'CustomerController@createCustomer']);
$router->post('customers/{ic}/mykad-photo', ['uses' => 'CustomerController@storeMykadPhotoIfMissing']);
$router->post('customers/{ic}/ic-scan', ['uses' => 'CustomerController@storeIcScan']);
$router->get('customers/{ic}/ic-scan', ['uses' => 'CustomerController@showIcScan']);
$router->delete('customers/{kodcaw}/{nokp}', ['uses' => 'CustomerController@delete']);
$router->put('customers/update/{nokp}', ['uses' => 'CustomerController@customerUpdate']);
$router->put('customers/update/kaunter/{nokp}/{kodcaw}', ['uses' => 'CustomerController@customerUpdateKaunter']);