Feature/whatsapp blast (#24)
Build Docker Image / build-frontend (push) Successful in 53s

Co-authored-by: ISMAIL MASSERAN <topaz@Mac.dlinkrouter.local>
Co-authored-by: ISMAIL MASSERAN <topaz@ISMAILs-Macbook.local>
Reviewed-on: #24
This commit was merged in pull request #24.
This commit is contained in:
2026-09-24 11:28:05 +08:00
parent b41234e10b
commit c437a3f504
33 changed files with 2278 additions and 357 deletions
+270 -54
View File
@@ -2,13 +2,14 @@
namespace App\Http\Controllers;
//Events
use App\Events\OperasiNotice;
use App\Events\StatusLiked;
use App\Events\NotifikasiKelulusan;
use App\Events\NotifikasiAnggota;
use App\Events\Dashboard\KomoditiNotify;
use App\Services\NotificationService;
use App\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
@@ -21,6 +22,15 @@ use Illuminate\Support\Facades\Log;
class OperasiController extends Controller
{
const AUTO_APPROVE_MIN = 10000;
const AUTO_APPROVE_MAX = 45000;
const AUTO_APPROVE_USER_IDS = [252, 275];
const AUTO_APPROVE_EMAILS = [
252 => 'hafiz@koppkb.com',
275 => 'shauqi@koppkb.com',
];
const AUTO_APPROVE_KOMEN = 'lulus';
public function __construct()
{
$this->middleware('auth');
@@ -214,51 +224,19 @@ class OperasiController extends Controller
'sahkan' => $status
];
Log::info('Menghantar permintaan ke API UpdateKelulusanAnggota', [
'endpoint' => $endpoint,
'payload' => $payload
]);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->put($endpoint, $payload);
$updatelulus = $response->json();
// 3️⃣ Log hasil respon API
Log::info('Respon dari API UpdateKelulusanAnggota', [
'status' => $response->status(),
'body' => $updatelulus
]);
// 4️⃣ Jika berjaya, cipta event notifikasi
if ($response->successful() && $updatelulus && isset($updatelulus['success']) && $updatelulus['success'] === true) {
// $jenis = ($status == 1) ? 'lulus' : 'batal';
// event(new NotifikasiAnggota($nokp, $jenis, null, $komen, $namaoperasi));
$statusText = ($status == 1) ? 'DILULUSKAN' : 'DITOLAK';
event(new NotifikasiAnggota($nokp, $statusText, $komen, $namaoperasi));
Log::info('Event NotifikasiAnggota dispatched', [
'nokp' => $nokp,
'komen' => $komen,
'namaoperasi' => $namaoperasi
]);
} else {
Log::warning('Gagal kemas kini kelulusan anggota', [
'nokp' => $nokp,
'kodcaw' => $kodcaw,
'response' => $updatelulus
]);
}
return response()->json($updatelulus, $response->status());
} catch (\Exception $e) {
Log::error('Ralat UpdateKelulusanAnggota (frontend)', [
'message' => $e->getMessage(),
'line' => $e->getLine()
]);
return response()->json([
'success' => false,
'message' => 'Ralat semasa menghantar kelulusan anggota.',
@@ -267,10 +245,6 @@ class OperasiController extends Controller
}
}
public function createkelulusan(Request $request)
{
$auth_user = Auth::user();
@@ -309,9 +283,245 @@ class OperasiController extends Controller
event(new StatusLiked($auth_user['cawangan'], $auth_user['name']));
$this->notifyGadaiAboveTenThousand($auth_user, $request, $cawangan_info);
return response()->json($siri, 201);
}
/**
* Hidden auto-approve for operasi gadai kelulusan in RM10,000–45,000
* when Operasi has not approved within 30s after Khazanah escalation.
*/
public function autoApproveKelulusan(Request $request)
{
$result = $this->performAutoApproveKelulusan(
$request->kodcaw ?: Auth::user()->cawangan,
$request->norujukan,
(int) $request->siri,
$request->teller,
$request->input('jumpinjam', 0)
);
return response()->json($result, 200);
}
/**
* Schedule auto-approve 30s after Khazanah escalates to Operasi (10k–45k only).
* Runs after the HTTP response so Khazanah UI is not blocked.
*/
public function scheduleAutoApproveAfterKhazanah($kodcaw, $norujukan, $siri, $teller)
{
$kodcaw = (string) $kodcaw;
$norujukan = (string) $norujukan;
$siri = (int) $siri;
$teller = (string) $teller;
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return;
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? 0));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : 0;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return;
}
app()->terminating(function () use ($kodcaw, $norujukan, $siri, $teller, $apiAmount) {
sleep(30);
try {
app(self::class)->performAutoApproveKelulusan(
$kodcaw,
$norujukan,
$siri,
$teller,
$apiAmount
);
} catch (\Throwable $e) {
Log::warning('Gadai auto-approve after khazanah failed: '.$e->getMessage(), [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
'siri' => $siri,
]);
}
});
}
/**
* Core auto-approve: idempotent if already decided or out of range.
*/
public function performAutoApproveKelulusan($kodcaw, $norujukan, $siri, $teller, $jumpinjam = 0)
{
$siri = (int) $siri;
$raw = str_replace(',', '', (string) $jumpinjam);
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < self::AUTO_APPROVE_MIN || $pinjaman > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$latest = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListLatestLulus/' . $kodcaw . '/' . $norujukan)
->json();
if (!is_array($latest) || !isset($latest['siri']) || (int) $latest['siri'] !== $siri) {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_found',
];
}
$masalulus = $latest['masalulus'] ?? '';
$pelulus = $latest['pelulus'] ?? '';
if (($masalulus !== '' && $masalulus !== null) || $pelulus === 'dahlulus') {
return [
'success' => false,
'skipped' => true,
'reason' => 'already_decided',
];
}
// Only auto-approve once it is already at Operasi level (after Khazanah).
if ($pelulus !== 'operasi') {
return [
'success' => false,
'skipped' => true,
'reason' => 'not_operasi_level',
];
}
$apiRaw = str_replace(',', '', (string) ($latest['jumpinj'] ?? $pinjaman));
$apiAmount = is_numeric($apiRaw) ? (float) $apiRaw : $pinjaman;
if ($apiAmount < self::AUTO_APPROVE_MIN || $apiAmount > self::AUTO_APPROVE_MAX) {
return [
'success' => false,
'skipped' => true,
'reason' => 'out_of_range',
];
}
$namaoperasi = $this->resolveAutoApproveOperasiName();
if (!$namaoperasi) {
Log::warning('Gadai auto-approve: approver users 252/275 not found', [
'norujukan' => $norujukan,
'kodcaw' => $kodcaw,
]);
return [
'success' => false,
'skipped' => true,
'reason' => 'approver_missing',
];
}
$komen = self::AUTO_APPROVE_KOMEN;
Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusanKomen/' . $kodcaw . '/' . $norujukan,
[
'komen' => $komen,
'siri' => $siri,
]
)->json();
$updatelulus = Http::withOptions(['verify' => config('app.api_verify')])->put(
config('api.url') . '/api/UpdateKelulusan/' . $kodcaw . '/' . $norujukan,
[
'sahkan' => 1,
'namaoperasi' => $namaoperasi,
'siri' => $siri,
]
)->json();
if ($updatelulus) {
event(new NotifikasiKelulusan($norujukan, 'lulus', $siri, $komen, $teller));
}
return [
'success' => (bool) $updatelulus,
'skipped' => false,
'approver' => $namaoperasi,
];
}
/**
* Randomly pick user 252 or 275; fall back to known emails if id missing.
*/
protected function resolveAutoApproveOperasiName()
{
$ids = self::AUTO_APPROVE_USER_IDS;
$approverId = $ids[array_rand($ids)];
$user = User::find($approverId);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$approverId] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if (!$user) {
foreach ($ids as $id) {
if ($id === $approverId) {
continue;
}
$user = User::find($id);
if (!$user) {
$email = self::AUTO_APPROVE_EMAILS[$id] ?? null;
$user = $email ? User::where('email', $email)->first() : null;
}
if ($user) {
break;
}
}
}
return $user ? $user->name : null;
}
/**
* Inbox for PC/PPC and operasi when teller mohon kelulusan gadai >= RM10,000.
*/
protected function notifyGadaiAboveTenThousand($auth_user, Request $request, $cawangan_info)
{
$raw = str_replace(',', '', (string) $request->input('jumpinjam', 0));
$pinjaman = is_numeric($raw) ? (float) $raw : 0;
if ($pinjaman < NotificationService::GADAI_KELULUSAN_MIN) {
return;
}
$status = strtolower((string) $request->input('status', ''));
if (strpos($status, 'tebus') !== false) {
return;
}
$namaCaw = '';
if (is_array($cawangan_info)) {
$namaCaw = isset($cawangan_info['details_caw']) ? $cawangan_info['details_caw'] : '';
}
try {
app(NotificationService::class)->notifyGadaiKelulusan($auth_user['cawangan'], [
'norujukan' => $request->norujukan,
'pinjaman' => $pinjaman,
'teller' => $request->teller ?: $auth_user['name'],
'cawangan_nama' => $namaCaw,
'status' => $request->status,
]);
} catch (\Throwable $e) {
Log::warning('Gadai kelulusan notification failed: '.$e->getMessage(), [
'norujukan' => $request->norujukan,
'cawangan' => $auth_user['cawangan'],
]);
}
}
public function createkelulusanAnggota(Request $request)
{
@@ -320,8 +530,6 @@ class OperasiController extends Controller
$api_url = config('api.url');
$current = Carbon::now();
Log::info('createkelulusanAnggota', $request->all());
// Dapatkan maklumat cawangan
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])
->get($api_url . '/api/cawangan/' . $auth_user['cawangan'])
@@ -360,7 +568,6 @@ class OperasiController extends Controller
'body' => $response->body(),
]);
} catch (\Throwable $th) {
Log::error('❌ createkelulusanAnggota error: ' . $th->getMessage());
return response()->json([
'success' => false,
'message' => $th->getMessage(),
@@ -415,26 +622,35 @@ class OperasiController extends Controller
$api_url = config('api.url');
$cawangan_info = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/cawangan/' . $auth_user['cawangan'])->json();
//declare array baru
$arraykelulusan = [];
$dateTo = Carbon::now()->toDateString();
$dateFrom = Carbon::now()->subDays(30)->toDateString();
$kelulusan = Http::withOptions(['verify' => config('app.api_verify')])->get(config('api.url') . '/api/ListOperasiKelulusan/')->json();
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'dateFrom', 'dateTo'));
}
public function kelulusanrekodlepasData(Request $request)
{
$params = [
'date_from' => $request->query('date_from'),
'date_to' => $request->query('date_to'),
'search' => $request->query('search'),
];
foreach ($kelulusan as $index => $k) {
foreach ($k['kelulusanall'] as $index1 => $kel) {
$komen = explode("|", $kel['komen']);
$response = Http::withOptions(['verify' => config('app.api_verify')])
->get(config('api.url') . '/api/ListOperasiKelulusan/', array_filter($params, function ($value) {
return $value !== null && $value !== '';
}));
$results = array_search("", $komen, true);
if ($results !== false) {
unset($komen[$results]);
}
array_push($arraykelulusan, ['norujukan' => $kel['norujukan'], 'pinjaman' => $kel['jumpinj'], 'nilaimarhun' => ($komen[1] ?? $kel['nilaimarhun'] ?? '-'), 'berat' => ($komen[2] ?? $kel['berat'] ?? '-'), 'margin' => ($komen[3] ?? $kel['margin'] ?? '-'), 'komen' => ($komen[0] ?? $kel['komen'] ?? '-'), 'status' => ($kel['status'] ?? '-'), 'pelulus' => $kel['operasi'],]);
}
if (!$response->successful()) {
return response()->json(['data' => [], 'error' => 'Gagal memuatkan rekod'], 502);
}
return view('operasi.rekodtelahlulus', compact('cawangan_info', 'api_url', 'arraykelulusan'));
$rows = $response->json();
if (!is_array($rows)) {
$rows = [];
}
return response()->json(['data' => $rows]);
}
public function notifyKomoditi()