diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php index 16847bf..3bf2089 100644 --- a/app/Controllers/AgentIncentiveController.php +++ b/app/Controllers/AgentIncentiveController.php @@ -88,20 +88,20 @@ class AgentIncentiveController extends ResourceController { try { /* ==================================================================== - * STEP 1 — Validate POST input (Agent ID removed) + * STEP 1 — Validate POST input * ==================================================================== */ $data = $this->request->getPost(); $month = $data['incentive_month'] ?? null; $createdBy = $data['created_by'] ?? null; /* ==================================================================== - * STEP 2 — Validation now only checks for month + * STEP 2 — Validation checks for month * ==================================================================== */ if (empty($month)) { return $this->respond([ 'status' => 'failed', 'code' => 400, - 'data' => 'Incentive month is required.', + 'data' => 'Date is required.', ], 200); } @@ -127,39 +127,18 @@ class AgentIncentiveController extends ResourceController $gridFile->move($uploadPath, $gridFileName); /* ==================================================================== - * STEP 5 — Save File record & GET THE ID - * ==================================================================== */ - $fileData = [ - 'incentive_month' => $month, - 'incentive_file_name' => $gridFileName, - 'file_type' => 'grid', - 'is_active' => 1, - 'created_by' => $createdBy, - ]; - - // insert() with returnID enabled returns the inserted primary key in CI4. - $fileId = $this->AgentIncentiveFileModel->insert($fileData, true); - if (empty($fileId) || (int)$fileId <= 0) { - $modelErrors = $this->AgentIncentiveFileModel->errors(); - return $this->respond([ - 'status' => 'failed', - 'code' => 400, - 'data' => !empty($modelErrors) ? $modelErrors : 'Unable to create incentive file record.', - ], 200); - } - - /* ==================================================================== - * STEP 6 — Parse Excel + * STEP 5 — Parse Excel FIRST (before saving file record) * ==================================================================== */ $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($uploadPath . $gridFileName); $rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); if (empty($rows)) { - return $this->respond(['status' => 'success', 'code' => 200, 'data' => 'File empty.'], 200); + if (file_exists($uploadPath . $gridFileName)) unlink($uploadPath . $gridFileName); + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'File is empty.'], 200); } /* ==================================================================== - * STEP 7 — Clean Numeric Helper (Modified to store only numbers) + * STEP 6 — Clean Numeric Helper * ==================================================================== */ $extractNumeric = static function ($value): ?float { $str = strtolower(trim((string) $value)); @@ -173,98 +152,179 @@ class AgentIncentiveController extends ResourceController } return $hasValid ? $sum : null; } - // Strip everything except numbers and decimals $str = preg_replace('/[^0-9.]/', '', $str); return is_numeric($str) ? (float) $str : null; }; /* ==================================================================== - * STEP 8 — Process Rows + * STEP 7 — Pre-validate ALL rows BEFORE inserting file record * ==================================================================== */ $currentVehicleType = null; $layoutHasComp = true; - $insertedCount = 0; + $errors = []; + $validRows = []; - foreach ($rows as $row) { + foreach ($rows as $rowIndex => $row) { while (count($row) < 9) $row[] = null; $col0 = trim((string) $row[0]); $col1 = trim((string) $row[1]); - // Section Header Detection - if ($col0 !== '' && ($col1 === '' || strtolower($col1) === 'nan') && strtoupper($col0) !== 'INSURER') { + // ── Section Header Detection (vehicle type row) ────────────────── + if ( + $col0 !== '' + && ($col1 === '' || strtolower($col1) === 'nan') + && strtoupper($col0) !== 'INSURER' + ) { $currentVehicleType = strtoupper($col0); continue; } - // Column Header Detection (Layout A vs B) + // ── Column Header Detection (Layout A vs B) ────────────────────── if (strtoupper($col0) === 'INSURER') { - $col4Upper = strtoupper((string)$row[4]); - $layoutHasComp = !preg_match('/(FUEL|PETROL|DIESEL|TP)/', $col4Upper); + $col4Upper = strtoupper((string) $row[4]); + // Layout B = col E header is FUEL/PETROL/DIESEL (Private Car layout) + $layoutHasComp = !preg_match('/(FUEL|PETROL|DIESEL)/', $col4Upper); continue; } - if (empty($col0) || $currentVehicleType === null) continue; + // ── Skip completely empty rows ─────────────────────────────────── + if (empty($col0)) continue; - // Mapping with Clean Numbers + // ── VALIDATION 1 : Vehicle type missing ────────────────────────── + if ($currentVehicleType === null) { + $errors[] = [ + 'row' => $rowIndex + 1, + 'insurer' => $col0, + 'message' => "Row " . ($rowIndex + 1) . " (Insurer: {$col0}): Vehicle type header is missing. " + . "Please add a vehicle type (e.g. 'TWO WHEELER NEW') before data rows.", + ]; + continue; + } + + // ── VALIDATION 2 : Segment missing ─────────────────────────────── + $segment = strtoupper(trim((string) $row[2])); + if ($segment === '' || strtolower($segment) === 'nan') { + $errors[] = [ + 'row' => $rowIndex + 1, + 'insurer' => $col0, + 'vehicle_type' => $currentVehicleType, + 'message' => "Row " . ($rowIndex + 1) . " (Insurer: {$col0}, Vehicle Type: {$currentVehicleType}): " + . "Segment is empty. Please provide a valid segment value.", + ]; + continue; + } + + // ── Map columns based on layout ─────────────────────────────────── if ($layoutHasComp) { + // Layout A: INSURER | RTO | SEGMENT | COMP | TP | REMARKS | ... $insurer = strtoupper($col0); - $rto = strtoupper((string)$row[1]); - $segment = strtoupper((string)$row[2]); - // We extract numeric values only for the storage - $comp = $extractNumeric($row[3]); + $rto = strtoupper((string) $row[1]); + $comp = $extractNumeric($row[3]); $tp = $extractNumeric($row[4]); $od = null; $fuel = null; - $brokerName = trim((string)$row[6]); + $brokerName = trim((string) $row[6]); $brokerExcelComp = $extractNumeric($row[7]); $brokerExcelTp = $extractNumeric($row[8]); $brokerExcelOd = null; - } else { + // Layout B: INSURER | RTO | SEGMENT | COMP | FUEL | REMARKS | ... $insurer = strtoupper($col0); - $rto = strtoupper((string)$row[1]); - $segment = strtoupper((string)$row[2]); + $rto = strtoupper((string) $row[1]); $comp = $extractNumeric($row[3]); $tp = null; $od = null; - $fuel = strtoupper((string)$row[4]); - $brokerName = trim((string)$row[6]); + $fuel = strtoupper(trim((string) $row[4])); + $brokerName = trim((string) $row[6]); $brokerExcelComp = null; $brokerExcelTp = $extractNumeric($row[7]); $brokerExcelOd = null; } - // OD logic (if COMP contains an OD value) - if ($comp !== null && stripos((string)$row[3], 'OD') !== false && $tp === null) { - $od = $comp; $comp = null; - $brokerExcelOd = $brokerExcelComp; $brokerExcelComp = null; + // ── OD logic: col D has "OD x.xX" prefix → move to od field ────── + if ($comp !== null && stripos(trim((string) $row[3]), 'OD') !== false) { + $od = $comp; $comp = null; + $brokerExcelOd = $brokerExcelComp; $brokerExcelComp = null; } - $gridRow = [ - 'partner_agent_incentive_file_id' => $fileId, // STORE THE FILE ID HERE - 'vehicle_type' => $currentVehicleType, - 'fuel' => $fuel, - 'insurer' => $insurer, - 'rto' => $rto, - 'broker_name' => $brokerName, - 'segment' => $segment, - 'comp' => $comp, - 'tp' => $tp, - 'od' => $od, - 'remarks' => trim((string)($row[5] ?? '')), - 'broker_comp' => $brokerExcelComp, - 'broker_tp' => $brokerExcelTp, - 'broker_od' => $brokerExcelOd, - ]; + // ── OD logic: col E (TP) has "OD x.xX" prefix in Layout A ──────── + if ($layoutHasComp && $tp !== null && stripos(trim((string) $row[4]), 'OD') !== false) { + $od = $tp; $tp = null; + $brokerExcelOd = $brokerExcelTp; $brokerExcelTp = null; + } - - $gridRow['created_by'] = $createdBy; - $this->PayoutGridModel->insert($gridRow); - $insertedCount++; - + // ── Store valid parsed row ──────────────────────────────────────── + $validRows[] = [ + 'vehicle_type' => $currentVehicleType, + 'fuel' => $fuel, + 'insurer' => $insurer, + 'rto' => $rto, + 'broker_name' => $brokerName, + 'segment' => $segment, + 'comp' => $comp, + 'tp' => $tp, + 'od' => $od, + 'remarks' => trim((string) ($row[5] ?? '')), + 'broker_comp' => $brokerExcelComp, + 'broker_tp' => $brokerExcelTp, + 'broker_od' => $brokerExcelOd, + 'created_by' => $createdBy, + ]; } + /* ==================================================================== + * STEP 8 — If ANY validation errors, delete file & return errors + * Do NOT insert file record at all + * ==================================================================== */ + if (!empty($errors)) { + if (file_exists($uploadPath . $gridFileName)) unlink($uploadPath . $gridFileName); + + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => [ + 'inserted' => 0, + 'errors' => $errors, + 'message' => count($errors) . ' row(s) had validation issues. File was not saved.', + ], + ], 200); + } + + /* ==================================================================== + * STEP 9 — All rows valid: NOW insert the file record + * ==================================================================== */ + $fileData = [ + 'incentive_month' => $month, + 'incentive_file_name' => $gridFileName, + 'file_type' => 'grid', + 'is_active' => 1, + 'created_by' => $createdBy, + ]; + + $fileId = $this->AgentIncentiveFileModel->insert($fileData, true); + if (empty($fileId) || (int)$fileId <= 0) { + $modelErrors = $this->AgentIncentiveFileModel->errors(); + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => !empty($modelErrors) ? $modelErrors : 'Unable to create incentive file record.', + ], 200); + } + + /* ==================================================================== + * STEP 10 — Insert all valid rows with the file ID + * ==================================================================== */ + $insertedCount = 0; + foreach ($validRows as $gridRow) { + $gridRow['partner_agent_incentive_file_id'] = $fileId; + $this->PayoutGridModel->insert($gridRow); + $insertedCount++; + } + + /* ==================================================================== + * STEP 11 — Full success response + * ==================================================================== */ return $this->respond([ 'status' => 'success', 'code' => 200, @@ -279,6 +339,201 @@ class AgentIncentiveController extends ResourceController return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); } } + // public function uploadGridFile_old() + // { + // try { + // /* ==================================================================== + // * STEP 1 — Validate POST input (Agent ID removed) + // * ==================================================================== */ + // $data = $this->request->getPost(); + // $month = $data['incentive_month'] ?? null; + // $createdBy = $data['created_by'] ?? null; + + // /* ==================================================================== + // * STEP 2 — Validation now only checks for month + // * ==================================================================== */ + // if (empty($month)) { + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'data' => 'Incentive month is required.', + // ], 200); + // } + + // /* ==================================================================== + // * STEP 3 — Validate uploaded file + // * ==================================================================== */ + // $gridFile = $this->request->getFile('incentive_file_name'); + // if (!$gridFile || !$gridFile->isValid()) { + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'data' => 'No valid file uploaded.', + // ], 200); + // } + + // /* ==================================================================== + // * STEP 4 — Move file + // * ==================================================================== */ + // $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; + // if (!is_dir($uploadPath)) mkdir($uploadPath, 0777, true); + + // $gridFileName = time() . '_' . $gridFile->getRandomName(); + // $gridFile->move($uploadPath, $gridFileName); + + // /* ==================================================================== + // * STEP 5 — Save File record & GET THE ID + // * ==================================================================== */ + // $fileData = [ + // 'incentive_month' => $month, + // 'incentive_file_name' => $gridFileName, + // 'file_type' => 'grid', + // 'is_active' => 1, + // 'created_by' => $createdBy, + // ]; + + // // insert() with returnID enabled returns the inserted primary key in CI4. + // $fileId = $this->AgentIncentiveFileModel->insert($fileData, true); + // if (empty($fileId) || (int)$fileId <= 0) { + // $modelErrors = $this->AgentIncentiveFileModel->errors(); + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'data' => !empty($modelErrors) ? $modelErrors : 'Unable to create incentive file record.', + // ], 200); + // } + + // /* ==================================================================== + // * STEP 6 — Parse Excel + // * ==================================================================== */ + // $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($uploadPath . $gridFileName); + // $rows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + // if (empty($rows)) { + // return $this->respond(['status' => 'success', 'code' => 200, 'data' => 'File empty.'], 200); + // } + + // /* ==================================================================== + // * STEP 7 — Clean Numeric Helper (Modified to store only numbers) + // * ==================================================================== */ + // $extractNumeric = static function ($value): ?float { + // $str = strtolower(trim((string) $value)); + // if ($str === '' || $str === 'nan') return null; + + // if (str_contains($str, '+')) { + // $sum = 0.0; $hasValid = false; + // foreach (explode('+', $str) as $part) { + // $cleanPart = preg_replace('/[^0-9.]/', '', $part); + // if (is_numeric($cleanPart)) { $sum += (float) $cleanPart; $hasValid = true; } + // } + // return $hasValid ? $sum : null; + // } + // // Strip everything except numbers and decimals + // $str = preg_replace('/[^0-9.]/', '', $str); + // return is_numeric($str) ? (float) $str : null; + // }; + + // /* ==================================================================== + // * STEP 8 — Process Rows + // * ==================================================================== */ + // $currentVehicleType = null; + // $layoutHasComp = true; + // $insertedCount = 0; + + // foreach ($rows as $row) { + // while (count($row) < 9) $row[] = null; + + // $col0 = trim((string) $row[0]); + // $col1 = trim((string) $row[1]); + + // // Section Header Detection + // if ($col0 !== '' && ($col1 === '' || strtolower($col1) === 'nan') && strtoupper($col0) !== 'INSURER') { + // $currentVehicleType = strtoupper($col0); + // continue; + // } + + // // Column Header Detection (Layout A vs B) + // if (strtoupper($col0) === 'INSURER') { + // $col4Upper = strtoupper((string)$row[4]); + // $layoutHasComp = !preg_match('/(FUEL|PETROL|DIESEL|TP)/', $col4Upper); + // continue; + // } + + // if (empty($col0) || $currentVehicleType === null) continue; + + // // Mapping with Clean Numbers + // if ($layoutHasComp) { + // $insurer = strtoupper($col0); + // $rto = strtoupper((string)$row[1]); + // $segment = strtoupper((string)$row[2]); + // // We extract numeric values only for the storage + // $comp = $extractNumeric($row[3]); + // $tp = $extractNumeric($row[4]); + // $od = null; + // $fuel = null; + // $brokerName = trim((string)$row[6]); + // $brokerExcelComp = $extractNumeric($row[7]); + // $brokerExcelTp = $extractNumeric($row[8]); + // $brokerExcelOd = null; + + // } else { + // $insurer = strtoupper($col0); + // $rto = strtoupper((string)$row[1]); + // $segment = strtoupper((string)$row[2]); + // $comp = $extractNumeric($row[3]); + // $tp = null; + // $od = null; + // $fuel = strtoupper((string)$row[4]); + // $brokerName = trim((string)$row[6]); + // $brokerExcelComp = null; + // $brokerExcelTp = $extractNumeric($row[7]); + // $brokerExcelOd = null; + // } + + // // OD logic (if COMP contains an OD value) + // if ($comp !== null && stripos((string)$row[3], 'OD') !== false && $tp === null) { + // $od = $comp; $comp = null; + // $brokerExcelOd = $brokerExcelComp; $brokerExcelComp = null; + // } + + // $gridRow = [ + // 'partner_agent_incentive_file_id' => $fileId, // STORE THE FILE ID HERE + // 'vehicle_type' => $currentVehicleType, + // 'fuel' => $fuel, + // 'insurer' => $insurer, + // 'rto' => $rto, + // 'broker_name' => $brokerName, + // 'segment' => $segment, + // 'comp' => $comp, + // 'tp' => $tp, + // 'od' => $od, + // 'remarks' => trim((string)($row[5] ?? '')), + // 'broker_comp' => $brokerExcelComp, + // 'broker_tp' => $brokerExcelTp, + // 'broker_od' => $brokerExcelOd, + // ]; + + + // $gridRow['created_by'] = $createdBy; + // $this->PayoutGridModel->insert($gridRow); + // $insertedCount++; + + // } + + // return $this->respond([ + // 'status' => 'success', + // 'code' => 200, + // 'data' => [ + // 'file_id' => $fileId, + // 'inserted' => $insertedCount, + // 'message' => "Processed: {$insertedCount} new", + // ], + // ], 200); + + // } catch (\Exception $e) { + // return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + // } + // } // ------------------------------------------------------------------------- // List all grid records (optional utility endpoint) @@ -289,9 +544,9 @@ class AgentIncentiveController extends ResourceController try { $request = $this->request; $role = $request->getGet('role'); - $file_id = $request->getGet('file_id'); + $file_id = $request->getGet('file_id'); - // 1. Check if Role is provided + // 1. Role is required if (!$role) { return $this->respond([ 'status' => 'failed', @@ -306,98 +561,115 @@ class AgentIncentiveController extends ResourceController $segment = $request->getGet('segment'); $vehicle_type = $request->getGet('vehicle_type'); - // 3. Build the Grid Query + // ── 3. Resolve file_id based on role ───────────────────────────────── + $db = \Config\Database::connect(); + + if (in_array($role, ['Manager', 'Accounts'], true)) { + + // Manager / Accounts: file_id must be supplied in query param + if (empty($file_id)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'file_id is required for Manager / Accounts role.' + ], 400); + } + + $resolvedFileId = (int) $file_id; + + } else { + + // Agent: find the latest id from partner_agent_incentive_file (no agent filter needed) + $fileRow = $db->table('partner_agent_incentive_file') + ->select('id') + ->orderBy('id', 'DESC') + ->limit(1) + ->get() + ->getRowArray(); + + if (empty($fileRow)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No incentive file found.' + ], 404); + } + + $resolvedFileId = (int) $fileRow['id']; + } + + // ── 4. Build Grid Query ─────────────────────────────────────────────── $builder = $this->PayoutGridModel->builder(); + $builder->where('partner_agent_incentive_file_id', $resolvedFileId); $builder->orderBy('vehicle_type', 'ASC')->orderBy('insurer', 'ASC'); - // Apply filters only for authorized roles - if (in_array($role, ['Manager', 'Accounts'])) { + if (in_array($role, ['Manager', 'Accounts'], true)) { + + // Optional filters for Manager / Accounts if (!empty($insurer)) $builder->where('insurer', $insurer); if (!empty($rto)) $builder->where('rto', $rto); if (!empty($segment)) $builder->where('segment', $segment); if (!empty($vehicle_type)) $builder->where('vehicle_type', $vehicle_type); - $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel, broker_name, remarks, broker_comp, - broker_tp, broker_od, created_by, created_at, updated_by, updated_at'); + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel, + broker_name, remarks, broker_comp, broker_tp, broker_od, + created_by, created_at, updated_by, updated_at'); + } else { - // Agent (and any other role): always return vehicle_type + payout columns for retention / partner_VT matching - $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel, broker_name, remarks, broker_comp, broker_tp, broker_od, partner_agent_incentive_file_id, created_at, updated_at', false); - } - - if ($file_id) { - $builder->where('partner_agent_incentive_file_id', $file_id); - } else { - $maxRow = $this->PayoutGridModel - ->selectMax('partner_agent_incentive_file_id') - ->first(); - - // Model may return array or object based on global returnType. - $maxFileId = null; - if (is_array($maxRow)) { - $maxFileId = $maxRow['partner_agent_incentive_file_id'] ?? null; - } elseif (is_object($maxRow)) { - $maxFileId = $maxRow->partner_agent_incentive_file_id ?? null; - } - - if (!empty($maxFileId)) { - $builder->where('partner_agent_incentive_file_id', $maxFileId); - } else { - // No data case - return $this->respond([ - 'status' => 'failed', - 'code' => 404, - 'data' => 'No data found' - ], 404); - } + // Agent + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel, + broker_name, remarks, broker_comp, broker_tp, broker_od, + partner_agent_incentive_file_id, created_at, updated_at', false); } + // ── 5. Execute (once) ───────────────────────────────────────────────── $gridResults = $builder->get()->getResultArray(); - // Manager / Accounts: comp/tp/od — no calculation; invalid → '-' + if (empty($gridResults)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No data found for the given file_id.' + ], 404); + } + + // ── 6. Role-based post-processing ───────────────────────────────────── if (in_array($role, ['Manager', 'Accounts'], true)) { $gridResults = PartnerPayoutGridRetention::applyManagerAccountsDisplayToRows($gridResults); } - // Partner (Agent): partner_VT / partner_RR + adjusted comp/tp/od when logged_id is present if (strtolower(trim((string) $role)) === 'agent') { $agentId = (int) trim((string) ($request->getGet('logged_id') ?? $request->getGet('agent_id') ?? 0)); + if ($agentId > 0) { - $db = \Config\Database::connect(); $gridResults = PartnerPayoutGridRetention::applyToRows($gridResults, $agentId, $db); } else { foreach ($gridResults as &$r) { - $r['oa'] = $r['comp'] ?? null; - $r['ob'] = $r['tp'] ?? null; - $r['oc'] = $r['od'] ?? null; + $r['oa'] = $r['comp'] ?? null; + $r['ob'] = $r['tp'] ?? null; + $r['oc'] = $r['od'] ?? null; $r['partner_VT'] = '-'; $r['partner_RR'] = '-'; - if (array_key_exists('comp', $r)) { - $r['comp'] = '-'; - } - if (array_key_exists('tp', $r)) { - $r['tp'] = '-'; - } - if (array_key_exists('od', $r)) { - $r['od'] = '-'; - } + $r['comp'] = '-'; + $r['tp'] = '-'; + $r['od'] = '-'; } unset($r); } } - // 4. Combine Grid Results with Dropdown Meta-data - $responseData = [ - 'grid' => $gridResults, - 'rtos' => $this->getUniqueColumnValues('rto'), - 'segments' => $this->getUniqueColumnValues('segment'), - 'vehicle_types' => $this->getUniqueColumnValues('vehicle_type'), - 'insurers' => $this->getUniqueColumnValues('insurer'), - ]; - + // ── 7. Respond ──────────────────────────────────────────────────────── return $this->respond([ 'status' => 'success', 'code' => 200, - 'data' => $responseData + 'data' => [ + 'id' => $resolvedFileId, + 'grid' => $gridResults, + 'rtos' => $this->getUniqueColumnValues('rto'), + 'segments' => $this->getUniqueColumnValues('segment'), + 'vehicle_types' => $this->getUniqueColumnValues('vehicle_type'), + 'insurers' => $this->getUniqueColumnValues('insurer'), + ] ], 200); } catch (\Exception $e) {