diff --git a/app/Config/Routes.php b/app/Config/Routes.php index a0377d1..808e78f 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -28,7 +28,7 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { //api's with token - $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], function ($routes) { +$routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], function ($routes) { //logout $routes->get('auth/logout', 'StaffAuthController::logout'); @@ -74,6 +74,9 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->get('agent/deleteAgentIncentiveFile', 'AgentController::deleteAgentIncentiveFile'); $routes->get('agent/downloadAgentIncentiveFile', 'AgentController::downloadAgentIncentiveFile'); + $routes->get('grid', 'AgentIncentiveController::getGridData'); + $routes->post('grid/upload', 'AgentIncentiveController::uploadGridFile'); + $routes->get('grid/fileList', 'AgentIncentiveController::gridFileList'); //Staff $routes->get('staff/staffList', 'StaffController::staffList'); @@ -123,6 +126,7 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->get('policy/findPolicy', 'PolicyController::findPolicy'); $routes->post('policy/createPolicy', 'PolicyController::createPolicy'); $routes->post('policy/updatePolicy', 'PolicyController::updatePolicy'); + $routes->post('policy/updatePolicyCommission', 'PolicyController::updatePolicyCommission'); $routes->get('policy/downloadPolicyFile', 'PolicyController::downloadPolicyFile'); $routes->post('policy/uploadPolicyFile', 'PolicyController::uploadPolicyFile'); $routes->get("policy/searchThePolicies", "PolicyController::searchThePolicies"); @@ -143,7 +147,9 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->post('endorsement/createEndorsement', 'EndorsementController::createEndorsement'); $routes->post('endorsement/updateEndorsement', 'EndorsementController::updateEndorsement'); $routes->get('endorsement/downloadEndorsementCompletionFile', 'EndorsementController::downloadEndorsementCompletionFile'); - + $routes->post('endorsement/uploadEndorsementFile', 'EndorsementController::uploadEndorsementFile'); + $routes->get('endorsement/deleteEndorsement', 'EndorsementController::deleteEndorsement'); + //dashboard $routes->get('dashboard/managerDashboard', 'DashboardController::managerDashboard'); $routes->get('dashboard/handlerDashboard', 'DashboardController::handlerDashboard'); @@ -178,15 +184,43 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->get('reports/endorsement-excel', 'ExcelExportController::downloadExcelEndorsement'); + + // DASHBOARD Season 6 — Partner Portal + + // GET /partner/{id}/details + $routes->get('partner/(:num)/details', 'DashboardController::partnerDetails/$1'); + + // GET /partner/{id}/policies + $routes->get('partner/(:num)/policies', 'DashboardController::partnerPolicies/$1'); + + // GET /partner/{id}/renewals?days=N + $routes->get('partner/(:num)/renewals', 'DashboardController::partnerRenewals/$1'); + + // GET /partner/{id}/earnings + $routes->get('partner/(:num)/earnings', 'DashboardController::partnerEarnings/$1'); + + // DASHBOARD Season 7 — Partner Portal + $routes->get('grid/download', 'ExcelExportController::downloadExcelGrid'); //invoice $routes->get('invoice/list', 'InvoiceController::invoiceList'); $routes->get('invoice/details', 'InvoiceController::findInvoiceWithItems'); $routes->post('invoice/create-or-update', 'InvoiceController::createOrUpdateInvoice'); + $routes->get('invoice/utrDetails', 'InvoiceController::utrDetails'); + $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); // tempo + $routes->post('invoice/updateUtrDetails', 'InvoiceController::updateUtrDetails'); + $routes->get('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); // tempo + $routes->post('invoice/add-payment-history', 'InvoiceController::addPaymentHistory'); // tempo + $routes->get('invoice/listUtrDetails', 'InvoiceController::listUtrDetails'); + $routes->post('invoice/listUtrDetails', 'InvoiceController::listUtrDetails'); $routes->get('invoice/delete', 'InvoiceController::deleteInvoice'); $routes->post('invoice/commission-rate-list', 'InvoiceController::getCommissionRateList'); $routes->get('invoice/getAgentUnusedCommissionList', 'InvoiceController::getAgentUnusedCommissionList'); + $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); + $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); + $routes->post('invoice/bulk-upload-commission', 'InvoiceController::bulkUploadCommission'); + $routes->post('invoice/bulk-upload-commission/proceed', 'InvoiceController::bulkUploadCommissionProceed'); // SALES EXECUTIVE @@ -203,7 +237,7 @@ $routes->group('api', ['filter' => 'appSignature'], function ($routes) { $routes->get('audit/history', 'MasterController::getHistory'); - }); +}); // common @@ -688,15 +722,3 @@ $routes->get("processjob", "JobWorker::processJob"); - - - - - - - - - - - - diff --git a/app/Controllers/AgentController.php b/app/Controllers/AgentController.php index 47cd921..29f6658 100644 --- a/app/Controllers/AgentController.php +++ b/app/Controllers/AgentController.php @@ -269,8 +269,10 @@ class AgentController extends ResourceController try{ $agent_id = $this->request->getGet('agent_id'); + $type = $this->request->getGet('type'); - $data = $this->AgentIncentiveFileModel->where('agent_id',$agent_id)->where('is_active',1)->findAll(); + $fileType = (!empty($type) && $type === 'grid') ? 'grid' : 'incentive'; + $data = $this->AgentIncentiveFileModel->where('agent_id',$agent_id)->where('is_active',1)->where('file_type', $fileType)->findAll(); return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data], 200); @@ -287,7 +289,7 @@ class AgentController extends ResourceController $data = $this->request->getPost(); //duplicate check - $duplicateData = $this->AgentIncentiveFileModel->where('agent_id',$data['agent_id'])->where('incentive_month',$data['incentive_month'])->first(); + $duplicateData = $this->AgentIncentiveFileModel->where('agent_id',$data['agent_id'])->where('incentive_month',$data['incentive_month'])->where('file_type','incentive')->first(); if(!empty($duplicateData)){ return $this->respond(['status' => 'failed', 'code' => 200, 'data' => 'Duplicate Entry.'], 200); } @@ -311,6 +313,7 @@ class AgentController extends ResourceController 'agent_id' => $data['agent_id'], 'incentive_month' => $data['incentive_month'], 'incentive_file_name' => $incentiveFileName, + 'file_type' => 'incentive', 'created_by' => $data['created_by'] ?? null ]; @@ -332,7 +335,7 @@ class AgentController extends ResourceController // check if agent exists - $file = $this->AgentIncentiveFileModel->find((int)$id); + $file = $this->AgentIncentiveFileModel->where('file_type','incentive')->find((int)$id); if (!$file) { return $this->respond(['status' => 'failed','code' => 200, 'data' => 'File not found'], 200); } @@ -353,13 +356,15 @@ class AgentController extends ResourceController { try { $id = $this->request->getGet('id'); + $type = $this->request->getGet('type'); // 'incentive' or 'grid' if (!$id) { return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'ID is required'], 200); } + $fileType = (!empty($type) && $type === 'grid') ? 'grid' : 'incentive'; // Fetch record from DB - $fileRecord = $this->AgentIncentiveFileModel->where('is_active',1)->find((int)$id); + $fileRecord = $this->AgentIncentiveFileModel->where('is_active',1)->where('file_type',$fileType)->find((int)$id); if (!$fileRecord) { return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found'], 200); @@ -379,10 +384,6 @@ class AgentController extends ResourceController } } - - - - - + //Empty Commit } diff --git a/app/Controllers/AgentIncentiveController.php b/app/Controllers/AgentIncentiveController.php new file mode 100644 index 0000000..90697df --- /dev/null +++ b/app/Controllers/AgentIncentiveController.php @@ -0,0 +1,614 @@ +AgentIncentiveFileModel = new AgentIncentiveFileModel(); + $this->PayoutGridModel = new PartnerInsurancePayoutGridModel(); + $this->PartnerAgentModel = new AgentModel(); + } + + // GET /agent/gridFileList + public function gridFileList() + { + try { + $builder = $this->AgentIncentiveFileModel->builder(); + + // 1. Explicitly select and format dates in the SQL layer (Faster + Indian Format) + $builder->select(" + paif.id, + paif.agent_id, + paif.incentive_month, + DATE_FORMAT(paif.incentive_month, '%d-%m-%Y') as vaild_from, + paif.incentive_file_name, + ps.name as created_by_name, + paif.created_on, + DATE_FORMAT(paif.created_on, '%d-%m-%Y %h:%i %p') as created_date + "); + + $builder->from('partner_agent_incentive_file paif'); + + // 2. The Join + $builder->join('partner_staff ps', 'ps.id = paif.created_by', 'left'); + + // 3. Filters + $builder->where('paif.is_active', 1); + $builder->where('paif.file_type', 'grid'); + + // 4. THE FIX: Group by the primary ID to stop the "5 rows" duplication + $builder->groupBy('paif.id'); + + // 5. Order + $builder->orderBy('paif.id', 'DESC'); + + $query = $builder->get(); + $result = $query->getResult(); + + // Use $this->respond to maintain consistency with your other API methods + return $this->respond([ + 'status' => 'success', // Changed to 'success' to match your other methods + 'code' => 200, + 'data' => $result + ], 200); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage() + ], 500); + } + } + + // ------------------------------------------------------------------------- + // Upload Grid File (file_type = 'grid') + // Parses Excel and UPSERTs rows into partner_insurance_payout_grid + // ------------------------------------------------------------------------- + public function uploadGridFile() + { + 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) + // ------------------------------------------------------------------------- + // GET /agent/payoutGrid?role=Manager means all list + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&vehicle_type=TWO%20WHEELER&plan_type=comp + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&rto=TN&segment=BIKE&plan_type=comp + // GET /agent/payoutGrid?role=Manager&insurer=HDFC&segment=BIKE&vehicle_type=TWO%20WHEELER&plan_type=comp + public function getGridData() + { + try { + $request = $this->request; + $role = $request->getGet('role'); + $file_id = $request->getGet('file_id'); + + // 1. Check if Role is provided + if (!$role) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'Role is required to fetch payout data.' + ], 400); + } + + // 2. Extract Filters + $insurer = $request->getGet('insurer'); + $rto = $request->getGet('rto'); + $segment = $request->getGet('segment'); + $vehicle_type = $request->getGet('vehicle_type'); + $plan_type = $request->getGet('plan_type'); + + // 3. Build the Grid Query + $builder = $this->PayoutGridModel->builder(); + $builder->orderBy('vehicle_type', 'ASC')->orderBy('insurer', 'ASC'); + + // Apply filters only for authorized roles + if (in_array($role, ['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); + + // Handle dynamic column selection (comp/tp/od) + if (!empty($plan_type) && in_array($plan_type, ['comp', 'tp', 'od'])) { + $builder->select("id, insurer, vehicle_type, segment, rto, $plan_type as payout,fuel,broker_name,comp,tp,od,remarks,broker_comp, + broker_tp,broker_od,created_by,created_at,updated_by,updated_at"); + } else { + // Default selection if no plan_type or invalid plan_type + $builder->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, fuel,broker_name,comp,tp,od,remarks,broker_comp, + broker_tp,broker_od,created_by,created_at,updated_by,updated_at'); + } + } + + 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); + } + } + + $gridResults = $builder->get()->getResult(); + + // 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'), + 'plan_types' => [ + ['value' => 'comp', 'label' => 'Comprehensive'], + ['value' => 'tp', 'label' => 'Third Party'], + ['value' => 'od', 'label' => 'Own Damage'] + ] + ]; + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $responseData + ], 200); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage() + ], 500); + } + } + + /** + * Helper function to fetch unique non-empty values for a column + */ + private function getUniqueColumnValues($column) + { + $results = $this->PayoutGridModel->select($column) + ->distinct() + ->where("$column IS NOT NULL") + ->where("$column !=", '') + ->orderBy($column, 'ASC') + ->findAll(); + + return array_column($results, $column); + } + + // ------------------------------------------------------------------------- + // Update grid row by id + // Route: POST /agent/updateGrid + // ------------------------------------------------------------------------- + public function updateGrid() + { + try { + $jsonData = (array) ($this->request->getJSON(true) ?? []); + $postData = $this->request->getPost() ?? []; + $data = !empty($jsonData) ? $jsonData : $postData; + + $id = isset($data['id']) ? (int) $data['id'] : 0; + if ($id <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'id is required.', + ], 200); + } + + $existing = $this->PayoutGridModel->find($id); + if (empty($existing)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Grid record not found.', + ], 200); + } + + $updateData = [ + 'vehicle_type' => array_key_exists('vehicle_type', $data) ? $data['vehicle_type'] : null, + 'fuel' => array_key_exists('fuel', $data) ? $data['fuel'] : null, + 'rto' => array_key_exists('rto', $data) ? $data['rto'] : null, + 'segment' => array_key_exists('segment', $data) ? $data['segment'] : null, + 'broker_name' => array_key_exists('broker_name', $data) ? $data['broker_name'] : null, + 'comp' => array_key_exists('comp', $data) ? $data['comp'] : null, + 'tp' => array_key_exists('tp', $data) ? $data['tp'] : null, + 'od' => array_key_exists('od', $data) ? $data['od'] : null, + 'broker_comp' => array_key_exists('broker_comp', $data) ? $data['broker_comp'] : null, + 'broker_tp' => array_key_exists('broker_tp', $data) ? $data['broker_tp'] : null, + 'broker_od' => array_key_exists('broker_od', $data) ? $data['broker_od'] : null, + + 'remarks' => array_key_exists('remarks', $data) ? $data['remarks'] : null, + ]; + + $hasAnyField = false; + foreach (array_keys($updateData) as $field) { + if (array_key_exists($field, $data)) { + $hasAnyField = true; + break; + } + } + + if (!$hasAnyField) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'No update fields provided.', + ], 200); + } + + if (array_key_exists('updated_by', $data)) { + $updateData['updated_by'] = $data['updated_by']; + } + + $this->PayoutGridModel->updateGridById($id, $updateData); + $updated = $this->PayoutGridModel->find($id); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $updated, + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + + + // ------------------------------------------------------------------------- + // 2. Download Filtered Grid in Excel + // Route: $routes->get('agent/downloadGrid', 'AgentIncentiveController::downloadGridInExcel'); + // Purpose: Applies the exact same filters as getGridData/(:num)(), but instead + // of returning JSON, it generates and downloads an Excel file. + // ------------------------------------------------------------------------- + public function downloadGridInExcel() + { + try { + $request = $this->request; + + // Get GET parameters for filtering + $role = $request->getGet('role'); + $insurer = $request->getGet('insurer'); + $rto = $request->getGet('rto'); + $segment = $request->getGet('segment'); + $vehicle_type = $request->getGet('vehicle_type'); + $plan_type = $request->getGet('plan_type'); // comp, tp, od + + $builder = $this->PayoutGridModel->builder(); + $builder->orderBy('vehicle_type', 'ASC')->orderBy('insurer', 'ASC'); + + // Apply filters if the user is a Manager or Accounts role + if (in_array($role, ['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); + } + + // Fetch the filtered records + $records = $builder->get()->getResultArray(); + + if (empty($records)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'No records found to export based on your filters.' + ], 404); + } + + // --- Start Excel Generation --- + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + + // Setup Header Row + $sheet->setCellValue('A1', 'Vehicle Type'); + $sheet->setCellValue('B1', 'Insurer'); + $sheet->setCellValue('C1', 'RTO'); + $sheet->setCellValue('D1', 'Segment'); + $sheet->setCellValue('E1', 'Broker Name'); // Changed from 'ID' for clarity + + // Set the dynamic column header based on the selected plan type + if ($plan_type === 'comp') { + $sheet->setCellValue('F1', 'Comp'); + $sheet->setCellValue('G1', 'Comp'); + } elseif ($plan_type === 'tp') { + $sheet->setCellValue('F1', 'TP'); + $sheet->setCellValue('G1', 'TP'); + } elseif ($plan_type === 'od') { + $sheet->setCellValue('F1', 'OD'); + $sheet->setCellValue('G1', 'OD'); + } else { + // If no specific plan type is selected, show all + $sheet->setCellValue('F1', 'Comp'); + $sheet->setCellValue('G1', 'TP'); + $sheet->setCellValue('H1', 'OD'); + + $sheet->setCellValue('I1', 'Comp'); + $sheet->setCellValue('J1', 'TP'); + $sheet->setCellValue('K1', 'OD'); + } + + // Populate Excel Rows + $rowNumber = 2; // Start on row 2 (row 1 is headers) + foreach ($records as $row) { + $sheet->setCellValue('A' . $rowNumber, $row['vehicle_type']); + $sheet->setCellValue('B' . $rowNumber, $row['insurer']); + $sheet->setCellValue('C' . $rowNumber, $row['rto']); + $sheet->setCellValue('D' . $rowNumber, $row['segment']); + $sheet->setCellValue('E' . $rowNumber, $row['broker_name']); + + // Output dynamic columns based on selected plan type + if ($plan_type === 'comp') { + $sheet->setCellValue('F' . $rowNumber, $row['comp']); + + } elseif ($plan_type === 'tp') { + $sheet->setCellValue('F' . $rowNumber, $row['tp']); + + } elseif ($plan_type === 'od') { + $sheet->setCellValue('F' . $rowNumber, $row['od']); + + } else { + $sheet->setCellValue('F' . $rowNumber, $row['comp']); + $sheet->setCellValue('G' . $rowNumber, $row['tp']); + $sheet->setCellValue('H' . $rowNumber, $row['od']); + + } + $rowNumber++; + } + + // Auto-size columns for better readability + foreach (range('A', $sheet->getHighestColumn()) as $columnID) { + $sheet->getColumnDimension($columnID)->setAutoSize(true); + } + + // Write the file to a temporary location + $writer = new Xlsx($spreadsheet); + $fileName = 'Filtered_Payout_Grid_' . date('Y-m-d_H-i') . '.xlsx'; + $tempFile = tempnam(sys_get_temp_dir(), 'grid_export'); + $writer->save($tempFile); + + // Return the file as a direct download response + return $this->response->download($tempFile, null)->setFileName($fileName); + + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => 'Failed to generate Excel file: ' . $e->getMessage() + ], 500); + } + } + +} diff --git a/app/Controllers/DashboardController.php b/app/Controllers/DashboardController.php index 941b7ee..e4a42a9 100644 --- a/app/Controllers/DashboardController.php +++ b/app/Controllers/DashboardController.php @@ -1172,5 +1172,413 @@ class DashboardController extends ResourceController return array_values($final); } +// ───────────────────────────────────────────────────────────────────────────── +// DashboardController.php — Partner Portal API methods +// Routes: +// GET partner/(:num)/details → partnerDetails($id) +// GET partner/(:num)/policies → partnerPolicies($id) +// GET partner/(:num)/renewals → partnerRenewals($id) ?days=20 +// GET partner/(:num)/earnings → partnerEarnings($id) +// ───────────────────────────────────────────────────────────────────────────── + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/details +// agent_id = partner_agent.id +// Joins: partner_policy (agent_id), partner_enquiry (agent_id), +// partner_endorsement_request (agent_id) +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerDetails($id) +{ + $ref = []; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + // ── 1. Agent profile (partner_agent.id = $id) + $agent = $this->db->table('partner_agent pa') + ->select(' + pa.id, + pa.name AS agent_name, + pa.agent_code, + pa.mobile, + pa.email, + pa.is_active, + ps.name AS manager_name + ') + ->join('partner_staff ps', 'ps.id = pa.manager_id', 'left') + ->where('pa.id', $id) + ->get()->getRowArray(); + + if (empty($agent)) { + throw new \RuntimeException('Partner not found.', 404); + } + + // -- 2. Policy counts + premium + commission + // mapped_policies = ALL policies under this agent (226) + // issued_policies = policy_number IS NOT NULL AND is_active = 1 (217) + // pending_policies = policy_number IS NULL (9 — raised but not yet issued) + // total_premium / commission = from issued policies only + $policyStats = $this->db->table('partner_policy pp') + ->select(' + COUNT(pp.id) AS mapped_policies, + SUM(CASE WHEN pp.policy_number IS NOT NULL AND pp.is_active = 1 AND pp.premium_amount IS NOT NULL THEN 1 ELSE 0 END) AS issued_policies, + SUM(CASE WHEN pp.policy_number IS NULL THEN 1 ELSE 0 END) AS pending_policies, + COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount ELSE 0 END), 0) AS total_premium, + COALESCE(SUM(CASE WHEN pp.policy_number IS NOT NULL THEN pp.premium_amount * 0.15 ELSE 0 END), 0) AS commission_earned + ') + ->where('pp.agent_id', $id) + ->get()->getRowArray(); + + // ── 3. Enquiry counts + // enquiry_status enum: To be assigned | Assigned | In progress | Completed + $enquiryStats = $this->db->table('partner_enquiry pe') + ->select(' + COUNT(pe.id) AS enquiry_total, + SUM(CASE WHEN pe.enquiry_status = "Completed" THEN 1 ELSE 0 END) AS enquiry_completed, + SUM(CASE WHEN pe.enquiry_status != "Completed" THEN 1 ELSE 0 END) AS enquiry_pending + ') + ->where('pe.agent_id', $id) + ->where('pe.is_active', 1) + ->get()->getRowArray(); + + // ── 4. Endorsement counts + // status is varchar(20) — adjust "Completed" to match your actual values + $endorseStats = $this->db->table('partner_endorsement_request per') + ->select(' + COUNT(per.id) AS endorsement_total, + SUM(CASE WHEN per.status = "Completed" THEN 1 ELSE 0 END) AS endorsement_done, + SUM(CASE WHEN per.status != "Completed" THEN 1 ELSE 0 END) AS endorsement_pending + ') + ->where('per.agent_id', $id) + ->where('per.is_active', 1) + ->get()->getRowArray(); + + // ── Merge everything + $data = array_merge( + $agent, + [ + 'status' => $agent['is_active'] ? 'Active' : 'Inactive', + 'commission_rate' => 15, + ], + $policyStats ?? [], + $enquiryStats ?? [], + $endorseStats ?? [], + ); + + $ref['message'] = 'Partner details retrieved successfully.'; + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $data, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } } + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/policies +// partner_policy.agent_id = $id +// holder_name → pp.insured_name (the actual insured person, NOT agent) +// product → pp.product (varchar 50), falls back to pp.vehicle_type +// policy_no → pp.policy_number +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerPolicies($id) +{ + $ref = []; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $results = $this->db->table('partner_policy pp') + ->select(' + pp.policy_number AS policy_no, + pp.insured_name AS holder_name, + COALESCE(NULLIF(pp.product, ""), pp.vehicle_type) AS product, + pp.premium_amount AS premium, + DATE_FORMAT(pp.issued_date, "%d-%m-%Y") AS issued_date, + DATE_FORMAT(pp.end_date, "%d-%m-%Y") AS expiry_date + ') + ->where('pp.agent_id', $id) + ->where('pp.policy_number IS NOT NULL') + ->where('pp.premium_amount IS NOT NULL') + ->where('pp.is_active', 1) + ->orderBy('pp.issued_date', 'DESC') + ->get()->getResultArray(); + + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No policies found for this partner.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } +} + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/renewals?days=20 +// partner_policy.agent_id = $id +// holder_name → pp.insured_name +// premium → pp.premium_amount +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerRenewals($id) +{ + $ref = []; + $days = (int) ($this->request->getGet('days') ?? 20); + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $results = $this->db->table('partner_policy pp') + ->select(' + pp.policy_number AS policy_no, + pp.insured_name AS holder_name, + pp.premium_amount AS premium, + DATE_FORMAT(pp.end_date, "%d-%m-%Y") AS end_date, + DATEDIFF(pp.end_date, CURDATE()) AS days_left + ') + ->where('pp.agent_id', $id) + ->where('pp.is_active', 1) + ->where('pp.policy_number IS NOT NULL') + ->where('pp.premium_amount IS NOT NULL') + ->where('pp.end_date >= CURDATE()', null, false) + ->where("pp.end_date <= DATE_ADD(CURDATE(), INTERVAL {$days} DAY)", null, false) + ->orderBy('pp.end_date', 'ASC') + ->get()->getResultArray(); + + $ref['days_filter'] = $days; + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No renewals due within ' . $days . ' days.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } +} + + +// ══════════════════════════════════════════════════════════════════════════════ +// GET partner/{id}/earnings +// partner_policy.agent_id = $id +// Groups by issued_date month → month_key (YYYY-MM), month_label (Month YYYY) +// paid = MAX(is_data_accuracy_checked) — 1 if all policies in month are checked +// No month_key param → returns ALL months (FY filtering done client-side in Dart) +// ══════════════════════════════════════════════════════════════════════════════ +public function partnerEarnings($id) +{ + $ref = []; + $monthKey = $this->request->getGet('month_key') ?? null; + + try { + if (empty($id)) { + return $this->respond([ + 'status' => 'error', + 'code' => 400, + 'data' => [], + 'message' => 'Missing required parameter: id', + ], 200); + } + + $builder = $this->db->table('partner_policy pp'); + + $builder->select(" + DATE_FORMAT(pp.issued_date, '%Y-%m') AS month_key, + DATE_FORMAT(pp.issued_date, '%M %Y') AS month_label, + COUNT(pp.id) AS policies, + COALESCE(SUM(pp.premium_amount), 0) AS premium, + COALESCE(SUM(pp.premium_amount * 0.15), 0) AS commission, + COALESCE(SUM(pp.premium_amount * 0.15 * 0.10), 0) AS tds, + COALESCE(SUM(pp.premium_amount * 0.15 * 0.90), 0) AS net_payout, + MAX(pp.is_data_accuracy_checked) AS paid + "); + + $builder->where('pp.agent_id', $id); + $builder->where('pp.is_active', 1); + $builder->where('pp.policy_number IS NOT NULL'); + $builder->where('pp.premium_amount IS NOT NULL'); + + if (!empty($monthKey)) { + $builder->where("DATE_FORMAT(pp.issued_date, '%Y-%m')", $monthKey); + } + + $builder->groupBy("DATE_FORMAT(pp.issued_date, '%Y-%m')"); + $builder->orderBy('month_key', 'ASC'); + + $results = $builder->get()->getResultArray(); + + $ref['month_filter'] = $monthKey ?? 'all'; + $ref['total_records'] = count($results); + + if (empty($results)) { + throw new \RuntimeException('No earning data found for this partner.', 404); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $results, + 'ref' => $ref, + ], 200); + + } catch (\Throwable $e) { + + if ($e instanceof \RuntimeException && $e->getCode() === 404) { + $ref['message'] = 'No Data Found'; + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [], + 'message' => 'No Data Found', + 'ref' => $ref, + ], 200); + } + + $isDbError = $e instanceof \CodeIgniter\Database\Exceptions\DatabaseException + || $e instanceof \mysqli_sql_exception + || $e instanceof \PDOException; + + if ($isDbError) { + $ref['debug_info'] = $e->getFile() . ' / LN : ' . $e->getLine(); + $ref['message'] = 'Database Error Occurred.'; + } else { + $ref['message'] = 'An unexpected error occurred: ' . $e->getMessage(); + } + + return $this->respond([ + 'status' => 'error', + 'code' => 500, + 'data' => [], + 'ref' => $ref, + ], 200); + } + } +} \ No newline at end of file diff --git a/app/Controllers/EndorsementController.php b/app/Controllers/EndorsementController.php index 92f2594..5a36221 100644 --- a/app/Controllers/EndorsementController.php +++ b/app/Controllers/EndorsementController.php @@ -47,10 +47,24 @@ class EndorsementController extends ResourceController $builder = $this->EndorsementModel ->select('partner_endorsement_request.*, + CASE + WHEN partner_endorsement_request.policy_start_date = "0000-00-00" + OR YEAR(partner_endorsement_request.policy_start_date) < 2000 + THEN NULL + ELSE DATE_FORMAT(partner_endorsement_request.policy_start_date, "%Y-%m-%d") + END as policy_start_date, + CASE + WHEN partner_endorsement_request.policy_end_date = "0000-00-00" + OR YEAR(partner_endorsement_request.policy_end_date) < 2000 + THEN NULL + ELSE DATE_FORMAT(partner_endorsement_request.policy_end_date, "%Y-%m-%d") + END as policy_end_date, i.name as insurer_name, i.short_name as insurer_short_name, ib.branch_name as insurer_branch, c.client_name, c.phone as client_phone, c.email as client_email, + partner_endorsement_request.agent_id, + pa.agent_code as agent_code, pa.name as agent_name, pp.enquiry_id, etm.endorsement_type as endorsement_type_value, @@ -125,8 +139,14 @@ class EndorsementController extends ResourceController foreach ($data as $key => $row) { $data[$key]['created_at'] = date('d-m-Y h:i A', strtotime($row['created_at'])); $data[$key]['updated_at'] = date('d-m-Y h:i A', strtotime($row['updated_at'])); - $data[$key]['policy_start_date'] = date('d-m-Y', strtotime($row['policy_start_date'])); - $data[$key]['policy_end_date'] = date('d-m-Y', strtotime($row['policy_end_date'])); + // ✅ NULL-safe date formatting + $data[$key]['policy_start_date'] = (!empty($row['policy_start_date']) && $row['policy_start_date'] !== '0000-00-00') + ? date('d-m-Y', strtotime($row['policy_start_date'])) + : null; + + $data[$key]['policy_end_date'] = (!empty($row['policy_end_date']) && $row['policy_end_date'] !== '0000-00-00') + ? date('d-m-Y', strtotime($row['policy_end_date'])) + : null; } return $this->respond(['status' => 'success','code' => 200,'data' => $data], 200); @@ -268,26 +288,58 @@ class EndorsementController extends ResourceController if (!$endorsement) { return $this->respond([ 'status' => 'failed', 'code' => 404,'data' => 'Endorsement not found' ], 404); } // FILE UPLOAD - $uploadedCompletionFile = $endorsement['endorsement_file_name']; + $uploadedOriginalCompletionFile = $endorsement['endorsement_file_name']; - $uploadFile = $this->request->getFile('endorsement_file_name'); + $uploadOriginalFile = $this->request->getFile('endorsement_file_name'); - if ($uploadFile && $uploadFile->isValid()) { + if ($uploadOriginalFile && $uploadOriginalFile->isValid()) { - $uploadPath = WRITEPATH . 'uploads/endorsement/'; - if (!is_dir($uploadPath)) { - mkdir($uploadPath, 0777, true); + $uploadOriginalPath = WRITEPATH . 'uploads/endorsement/'; + if (!is_dir($uploadOriginalPath)) { + mkdir($uploadOriginalPath, 0777, true); } - $uploadedCompletionFile = time() . '_' . $uploadFile->getRandomName(); - $uploadFile->move($uploadPath, $uploadedCompletionFile); + $uploadedOriginalCompletionFile = time() . '_' . $uploadOriginalFile->getRandomName(); + $uploadOriginalFile->move($uploadOriginalPath, $uploadedOriginalCompletionFile); // ✅ fixed variable } - //COMMON UPDATE FIELDS + // FILE REVISED UPLOAD + $uploadedRevisedCompletionFile = $endorsement['endorsement_completion_file']; + $uploadRevisedFile = $this->request->getFile('endorsement_completion_file'); + + if ($uploadRevisedFile && $uploadRevisedFile->isValid()) { + $uploadRevisedPath = WRITEPATH . 'uploads/endorsement/endorsement_pdf/'; + if (!is_dir($uploadRevisedPath)) { + mkdir($uploadRevisedPath, 0777, true); + } + $uploadedRevisedCompletionFile = time() . '_' . $uploadRevisedFile->getRandomName(); + $uploadRevisedFile->move($uploadRevisedPath, $uploadedRevisedCompletionFile); + } + + /* + * COMMON UPDATE FIELDS + * Keep update mapping explicit so fields sent by frontend are not silently ignored. + */ $updateData = []; // Update only if provided (avoid null overwrite) + if (isset($reqData['policy_from'])) { + $updateData['policy_from'] = $reqData['policy_from']; + } + + if (isset($reqData['policy_number'])) { + $updateData['policy_number'] = $reqData['policy_number']; + } + + if (isset($reqData['manager_id'])) { + $updateData['manager_id'] = $reqData['manager_id']; + } + + if (isset($reqData['agent_id'])) { + $updateData['agent_id'] = $reqData['agent_id']; + } + if (isset($reqData['endorsement_type'])) { $updateData['endorsement_type'] = $reqData['endorsement_type']; } @@ -316,17 +368,26 @@ class EndorsementController extends ResourceController $updateData['endorsement_premium'] = $reqData['endorsement_premium']; } + if (isset($reqData['commission_amount'])) { + $updateData['commission_amount'] = $reqData['commission_amount']; + } + if (isset($reqData['pending_days'])) { $updateData['pending_days'] = $reqData['pending_days']; } - + + // Always update both files (new or existing) + $updateData['endorsement_file_name'] = $uploadedOriginalCompletionFile; + $updateData['endorsement_completion_file'] = $uploadedRevisedCompletionFile; // ✅ fixed variable - // Always update file (either old or new) - $updateData['endorsement_file_name'] = $uploadedCompletionFile; - - // External Policy Editable Fields - if ($endorsement['policy_from'] === 'External') { + /* + * External Policy Editable Fields + * Use incoming policy_from first (if provided), fallback to stored value. + * This prevents missing updates when stored value casing differs (external/External). + */ + $effectivePolicyFrom = $reqData['policy_from'] ?? $endorsement['policy_from'] ?? ''; + if (strcasecmp(trim((string)$effectivePolicyFrom), 'External') === 0) { if (isset($reqData['insurer_id'])) { $updateData['insurer_id'] = $reqData['insurer_id'] ?? null; @@ -372,6 +433,7 @@ class EndorsementController extends ResourceController if (!empty($staff) && $staff['role_id'] == 4) { $updateData['is_data_accuracy_checked'] = 1; + $updateData['status'] = "Closed" ; } } @@ -408,6 +470,30 @@ class EndorsementController extends ResourceController } } + public function deleteEndorsement() + { + try { + + $id = $this->request->getGet('id'); + + + // check if EndorsementModel exists + $file = $this->EndorsementModel->find((int)$id); + if (!$file) { + return $this->respond(['status' => 'failed','code' => 200, 'data' => 'Endorsement not found'], 200); + } + + // update status + $this->EndorsementModel->update($id, ['is_active' => 0]); + + return $this->respond([ + 'status' => 'success', 'code' => 200,'data' => "Endorsement Deleted"], 200); + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed','code' => 500,'data' => $e->getMessage()], 500); + } + } + // public function createEndorsement() // { @@ -556,6 +642,7 @@ class EndorsementController extends ResourceController { try { $id = $this->request->getGet('id'); + $type = $this->request->getGet('type'); // 'completion' or 'original' if (!$id) { return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'ID is required'], 200); @@ -568,7 +655,23 @@ class EndorsementController extends ResourceController return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found'], 200); } - $filePath = WRITEPATH . 'uploads/endorsement/' . $fileRecord['endorsement_file_name']; + // $filePath = WRITEPATH . 'uploads/endorsement/' . $fileRecord['endorsement_file_name']; + + // ✅ If type is null/empty → original (default) + // ✅ If type = 'completion' → endorsement_pdf/ subfolder + if (!$type || $type === 'original') { + $fileName = $fileRecord['endorsement_file_name'] ?? null; + $subPath = ''; + } else if ($type === 'completion') { + $fileName = $fileRecord['endorsement_completion_file'] ?? null; + $subPath = 'endorsement_pdf/'; + } + + if (!$fileName) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'No file uploaded yet'], 200); + } + + $filePath = WRITEPATH . 'uploads/endorsement/' . $subPath . $fileName; if (!file_exists($filePath)) { return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File missing on server'], 200); @@ -582,7 +685,7 @@ class EndorsementController extends ResourceController } } - public function EndorsementFilePath() + public function EndorsementFilePathGOWTHAM() { try { $endorsementId = $this->request->getGet('endorsement_id'); @@ -623,11 +726,138 @@ class EndorsementController extends ResourceController } } + public function EndorsementFilePath() + { + try { + $endorsementId = $this->request->getGet('endorsement_id'); + + if (!$endorsementId) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'endorsement_id is required'], 200); + } + // Fetch record from DB + $fileRecord = $this->EndorsementModel->where('is_active', 1)->find((int)$endorsementId); + if (!$fileRecord) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File not found in database'], 200); + } + // ✅ STEP 1: Check completion file first + $completionFile = $fileRecord['endorsement_completion_file'] ?? null; + $originalFile = $fileRecord['endorsement_file_name'] ?? null; - + if (!empty($completionFile)) { + // ✅ Use completion file path + $fileName = $completionFile; + $filePath = WRITEPATH . 'uploads/endorsement/endorsement_pdf/' . $fileName; + } elseif (!empty($originalFile)) { + // ✅ Fallback to original file path + $fileName = $originalFile; + $filePath = WRITEPATH . 'uploads/endorsement/' . $fileName; + } else { + // ❌ Neither file exists in DB + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'No file uploaded yet'], 200); + } + + // ✅ STEP 2: Check file exists on disk + if (!file_exists($filePath)) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'File missing on server'], 200); + } + + // ✅ STEP 3: Stream file to browser / Flutter + $mime = mime_content_type($filePath); + + return $this->response + ->setHeader('Content-Type', $mime) + ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') + ->setBody(file_get_contents($filePath)); + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'message' => $e->getMessage()], 500); + } + } + + public function uploadEndorsementFile() + { + try { + $data = $this->request->getPost(); + + // ✅ Validate required POST fields first + if (empty($data['id']) || empty($data['updated_by'])) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Missing required fields'], 400); + } + + $uploadPath = WRITEPATH . 'uploads/endorsement/'; + $allowedTypes = ['application/pdf']; + $pdfPath = $uploadPath . 'endorsement_pdf/'; + + $endorsementPdf = $this->request->getFile('endorsement_completion_file'); + + // ✅ Check file existence and validity BEFORE accessing its properties + if (!$endorsementPdf || !$endorsementPdf->isValid()) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'No valid file received'], 400); + } + + // ✅ MIME validation only after confirming file exists + if (!in_array($endorsementPdf->getMimeType(), $allowedTypes)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Only PDF files are allowed'], 400); + } + + // ✅ Fetch existing record to get old file name BEFORE uploading + $existingRecord = $this->EndorsementModel->find($data['id']); + $oldFileName = $existingRecord['endorsement_completion_file'] ?? null; + + // ✅ Ensure upload directory exists + if (!is_dir($pdfPath)) { + mkdir($pdfPath, 0777, true); + } + + // ✅ Upload new file + $pdfFileName = time() . '_' . $endorsementPdf->getRandomName(); + $endorsementPdf->move($pdfPath, $pdfFileName); + + // ✅ Guard: ensure file was actually saved on disk + if (!$pdfFileName || !file_exists($pdfPath . $pdfFileName)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'File upload failed'], 400); + } + + // ✅ Update DB with new file name + $updateData = [ + 'endorsement_completion_file' => $pdfFileName, + 'updated_by' => $data['updated_by'], + ]; + + $updated = $this->EndorsementModel->update($data['id'], $updateData); + + // ✅ DB update failed → delete the newly uploaded file to avoid orphan files + if (!$updated) { + if (file_exists($pdfPath . $pdfFileName)) { + unlink($pdfPath . $pdfFileName); + } + log_message('error', 'DB update failed for endorsement ID: ' . $data['id'] . '. New file removed.'); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => 'DB update failed, file not saved'], 500); + } + + // ✅ DB success → NOW safe to delete old file + if ($oldFileName) { + $oldFilePath = $pdfPath . $oldFileName; + if (file_exists($oldFilePath)) { + if (!unlink($oldFilePath)) { + log_message('warning', 'DB updated but failed to delete old file: ' . $oldFilePath); + } else { + log_message('info', 'Old file deleted after successful DB update: ' . $oldFilePath); + } + } + } + + log_message('info', 'Endorsement PDF uploaded successfully. ID: ' . $data['id'] . ', File: ' . $pdfFileName); + + return $this->respond(['status' => 'success', 'code' => 200, 'data' => $data['id']], 200); + + } catch (\Exception $e) { + log_message('error', 'uploadEndorsementFile error: ' . $e->getMessage()); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } - } diff --git a/app/Controllers/EnquiryController.php b/app/Controllers/EnquiryController.php index 22c2454..fcd535a 100644 --- a/app/Controllers/EnquiryController.php +++ b/app/Controllers/EnquiryController.php @@ -186,11 +186,13 @@ class EnquiryController extends ResourceController } //Get enquiry details - $enquiry = $this->enquiryModel->select('partner_enquiry.* , I.name as insurer_name , I.short_name , VT.vehicle_type as vehicle_type ,PB.name as broker_name ,PA.name as agent_name,PA.agent_code') + $enquiry = $this->enquiryModel->select('partner_enquiry.* , I.name as insurer_name , I.short_name , VT.vehicle_type as vehicle_type ,PB.name as broker_name ,PA.name as agent_name,PA.agent_code,PPMM.value as payment_mode_value') ->join('insurers I', 'I.id = partner_enquiry.insurer_id', 'left') ->join('vehicle_type VT', 'VT.id = partner_enquiry.vehicle_type_id', 'left') ->join('partner_brokers PB', 'PB.id = partner_enquiry.broker_id', 'left') ->join('partner_agent PA', 'PA.id = partner_enquiry.agent_id', 'left') + ->join('partner_quotation PQ', 'PQ.enquiry_id = partner_enquiry.id', 'left') + ->join('partner_payment_mode_master PPMM', 'PPMM.id = PQ.payment_mode_id', 'left') ->where('partner_enquiry.id', $enquiry_id) ->where('partner_enquiry.is_active', 1) ->first(); @@ -834,7 +836,7 @@ class EnquiryController extends ResourceController try { $data = $this->request->getJSON(true); - print_r($data);die; + // print_r($data);die; if (!isset($data['id'])) { return $this->respond(['status' => 'failed', 'code' => 200, 'error' => 'ID Required'], 200); diff --git a/app/Controllers/ExcelExportController.php b/app/Controllers/ExcelExportController.php index 7b6eec4..f8b3860 100644 --- a/app/Controllers/ExcelExportController.php +++ b/app/Controllers/ExcelExportController.php @@ -1546,11 +1546,12 @@ class ExcelExportController extends ResourceController 'Insured Name', 'Insurer', 'Broker', + 'Partner Code And Name', 'Contact Person', 'Remarks', 'Is Financial', 'Endorsement Premium Amount', - 'Commission Amount' + 'Commission Amount', ]; $finalData = []; @@ -2479,6 +2480,7 @@ class ExcelExportController extends ResourceController per.insured_name, I.name AS insurer_name, pb.name AS broker_name, + CONCAT(pa.agent_code, ' - ', pa.name) AS agent_details, per.contact_person, per.endorsement_description, per.financia_or_non_financial, @@ -2548,4 +2550,179 @@ class ExcelExportController extends ResourceController } } + // grid/download?role=Manager means all list + // grid/download?role=Manager&insurer=HDFC&vehicle_type=TWO%20WHEELER&plan_type=comp + // grid/download?role=Manager&insurer=HDFC&rto=TN&segment=BIKE&plan_type=comp + // grid/download?role=Manager&insurer=HDFC&segment=BIKE&vehicle_type=TWO%20WHEELER&plan_type=comp + public function downloadExcelGrid() + { + try { + $request = $this->request; + + // ✅ Inputs + $role = trim((string) ($request->getGet('role') ?? '')); + $fileId = trim((string) ($request->getGet('file_id') ?? '')); + $insurer = trim((string) ($request->getGet('insurer') ?? '')); + $rto = trim((string) ($request->getGet('rto') ?? '')); + $segment = trim((string) ($request->getGet('segment') ?? '')); + $vehicleType = trim((string) ($request->getGet('vehicle_type') ?? '')); + $planType = strtolower(trim((string) ($request->getGet('plan_type') ?? ''))); + $search = trim((string) ($request->getGet('search') ?? '')); + $loggedId = trim((string) ($request->getGet('logged_id') ?? '')); + + if ($role === '') { + return $this->response->setJSON([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'Role is required to export payout grid.', + ]); + } + + $isAgent = strtolower($role) === 'agent'; + + // ✅ Get retention rate (only for Agent) + $retentionRate = 0; + if ($isAgent && $loggedId !== '') { + $agent = $this->db->table('partner_agent') + ->select('retention_rate') + ->where('id', $loggedId) + ->get() + ->getRowArray(); + + $retentionRate = isset($agent['retention_rate']) ? (float)$agent['retention_rate'] : 0; + } + + // ✅ Main query + $builder = $this->db->table('partner_insurance_payout_grid'); + + if ($fileId !== '') { + $builder->where('partner_agent_incentive_file_id', $fileId); + } else { + $maxFileId = $this->db->table('partner_insurance_payout_grid') + ->selectMax('partner_agent_incentive_file_id') + ->get() + ->getRow() + ->partner_agent_incentive_file_id ?? null; + + if (!empty($maxFileId)) { + $builder->where('partner_agent_incentive_file_id', $maxFileId); + } + } + + // ✅ Filters + if ($insurer !== '') $builder->where('insurer', $insurer); + if ($rto !== '') $builder->where('rto', $rto); + if ($segment !== '') $builder->where('segment', $segment); + if ($vehicleType !== '') $builder->where('vehicle_type', $vehicleType); + + if (in_array($planType, ['comp', 'tp', 'od'])) { + $builder->where("$planType IS NOT NULL", null, false) + ->where("$planType !=", '') + ->where("$planType !=", '0'); + } + + // ✅ Search + if ($search !== '') { + $builder->groupStart() + ->like('insurer', $search) + ->orLike('vehicle_type', $search) + ->orLike('segment', $search) + ->orLike('rto', $search) + ->orLike('remarks', $search) + ->groupEnd(); + } + + // ✅ Fetch rows + $rows = $builder + ->select('id, insurer, vehicle_type, segment, rto, comp, tp, od, remarks') + ->orderBy('id', 'DESC') + ->get() + ->getResultArray(); + + // ========================= + // ✅ HEADERS + // ========================= + $headers = ['S.No', 'Insurer', 'Vehicle Type', 'Segment', 'RTO']; + + if ($isAgent) { + if ($planType === 'comp') { + $headers[] = 'Comp'; + } elseif ($planType === 'tp') { + $headers[] = 'TP'; + } elseif ($planType === 'od') { + $headers[] = 'OD'; + } + } else { + // ✅ Manager / Accounts + $headers[] = 'Comp'; + $headers[] = 'TP'; + $headers[] = 'OD'; + } + + $headers[] = 'Remarks'; + + // ========================= + // ✅ DATA + // ========================= + $data = []; + $sNo = 1; + + foreach ($rows as $row) { + + $comp = isset($row['comp']) ? (float)$row['comp'] : 0; + $tp = isset($row['tp']) ? (float)$row['tp'] : 0; + $od = isset($row['od']) ? (float)$row['od'] : 0; + + // ✅ Apply retention only for Agent + if ($isAgent) { + $comp -= $retentionRate; + $tp -= $retentionRate; + $od -= $retentionRate; + } + + $line = [ + $sNo++, + $row['insurer'] ?? '-', + $row['vehicle_type'] ?? '-', + $row['segment'] ?? '-', + $row['rto'] ?? '-', + ]; + + // ✅ Dynamic columns (Agent only) + if ($isAgent) { + if ($planType === 'comp') { + $line[] = $comp; + } elseif ($planType === 'tp') { + $line[] = $tp; + } elseif ($planType === 'od') { + $line[] = $od; + } + } else { + // ✅ Manager / Accounts → always all + $line[] = $comp; + $line[] = $tp; + $line[] = $od; + } + + $line[] = $row['remarks'] ?? '-'; + + $data[] = $line; + } + + // ✅ Export + $fileName = "grid_" . date('Ymd_His') . ".xlsx"; + + return $this->streamExcelFile($headers, $data, 'Payout Grid', $fileName, false); + + } catch (\Throwable $e) { + return $this->response->setJSON([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ]); + } + } + + + } diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 0f9d45d..3325e7d 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -8,7 +8,9 @@ use App\Models\QuotationModel; use App\Models\InvoiceModel; use App\Models\InvoiceItemModel; use App\Models\InvoiceUtrModel; +use App\Models\AgentIncentiveFileModel; use CodeIgniter\Database\Exceptions\DataException; +use PhpOffice\PhpSpreadsheet\IOFactory; class InvoiceController extends ResourceController { @@ -18,6 +20,7 @@ class InvoiceController extends ResourceController protected $InvoiceModel; protected $InvoiceItemModel; protected $InvoiceUtrModel; + protected $AgentIncentiveFileModel; protected $db; public function __construct() @@ -28,6 +31,7 @@ class InvoiceController extends ResourceController $this->InvoiceModel = new InvoiceModel(); $this->InvoiceItemModel = new InvoiceItemModel(); $this->InvoiceUtrModel = new InvoiceUtrModel(); + $this->AgentIncentiveFileModel = new AgentIncentiveFileModel(); $this->db = \Config\Database::connect(); } @@ -41,7 +45,37 @@ class InvoiceController extends ResourceController pos.name as pos_name, FORMAT(partner_invoice.invoice_amount, 2, "en_IN") AS invoice_amount_indian_format, DATE_FORMAT(partner_invoice.invoice_date, "%d-%m-%Y") AS invoice_date_ui_format, - ') + ( + SELECT GROUP_CONCAT(pa.name ORDER BY pa.id SEPARATOR ", ") + FROM partner_agent pa + WHERE JSON_SEARCH(partner_invoice.agent_id, "one", CAST(pa.id AS CHAR)) IS NOT NULL + ) AS partner_names, + ( + SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ) AS utr_numbers, + partner_invoice.invoice_amount AS invoiced_amount, + ( + COALESCE(( + SELECT SUM(piu.amount) + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ), 0.00) + ) AS payout_amount, + ( + partner_invoice.invoice_amount - ( + COALESCE(( + SELECT SUM(piu.amount) + FROM partner_invoice_utr piu + WHERE piu.invoice_id = partner_invoice.id + AND piu.is_active = 1 + ), 0.00) + ) + ) AS balance_amount', + false) ->join('partner_brokers PB', 'PB.id = partner_invoice.broker_id', 'left') ->join('partner_pos pos', 'pos.id = partner_invoice.pos_id', 'left') ->where('partner_invoice.is_active', 1) @@ -78,6 +112,25 @@ class InvoiceController extends ResourceController ->where('partner_invoice_items.is_active', 1) ->findAll(); + $paymentHistory = $this->InvoiceUtrModel + ->select('id, invoice_id, amount as paid_amount, utr_no, utr_date as paid_date, created_at, created_by') + ->where('invoice_id', $id) + ->where('is_active', 1) + ->where('amount >', 0) + ->orderBy('id', 'DESC') + ->findAll(); + + $utrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $id) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $totalPaidAmount = $utrPaidAmount; + $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); + $balanceAmount = max($invoiceAmount - $totalPaidAmount, 0); return $this->respond([ @@ -85,7 +138,10 @@ class InvoiceController extends ResourceController 'code' => 200, 'data' => [ 'invoice' => $invoice, - 'items' => $items + 'items' => $items, + 'payment_history' => $paymentHistory, + 'paid_amount' => $totalPaidAmount, + 'balance_amount' => $balanceAmount ] ], 200); @@ -101,12 +157,16 @@ class InvoiceController extends ResourceController try { $input = $this->request->getJSON(true); + // Normalize pos_id (client may send null/0/non-numeric like "w") + $posIdRaw = $input['pos_id'] ?? 0; + $posId = is_numeric($posIdRaw) ? (int)$posIdRaw : 0; + // Basic invoice data $invoiceData = [ 'invoice_amount' => $input['invoice_amount'] ?? 0, 'broker_id' => $input['broker_id'] ?? 0, 'agent_id' => json_encode($input['agent_id'] ?? []), - 'pos_id' => $input['pos_id'] ?? 0, + 'pos_id' => $posId, 'till_date' => $input['till_date'] ? date('Y-m-d', strtotime($input['till_date'])) : null, 'invoice_date' => $input['invoice_date'] ? date('Y-m-d', strtotime($input['invoice_date'])) : null, 'payout_status' => 1, @@ -116,11 +176,12 @@ class InvoiceController extends ResourceController ]; $invoiceId = $input['id'] ?? null; + $isCreate = empty($invoiceId); if (!$invoiceId) { // Generate invoice number only for CREATE - $invoiceData['invoice_no'] = $this->generateInvoiceNo($input['pos_id']); + $invoiceData['invoice_no'] = $this->generateInvoiceNo($posId); $invoiceData['created_at'] = date('Y-m-d H:i:s'); $invoiceData['created_by'] = $input['created_by'] ?? 0; @@ -171,6 +232,70 @@ class InvoiceController extends ResourceController } } + + + /* + * UTR save (Tempa) + */ + if ($isCreate && !empty($input['utrs']) && is_array($input['utrs'])) { + + $seenUtrs = []; + + foreach ($input['utrs'] as $utrRow) { + + $utrNo = trim((string)($utrRow['utr_no'] ?? '')); + + + if ($utrNo === '') continue; + + + // if (in_array($utrNo, $seenUtrs, true)) { + // $this->db->transRollback(); + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message'=> "Duplicate UTR in request: {$utrNo}" + // ], 400); + // } + + $seenUtrs[] = $utrNo; + + // $exists = $this->InvoiceUtrModel->where('utr_no', $utrNo)->where('is_active', 1)->first(); + + // if ($exists) { + // $this->db->transRollback(); + // return $this->respond([ + // 'status' => 'failed', + // 'code' => 400, + // 'message'=> "UTR already exists: {$utrNo}" + // ], 400); + // } + + // ✅ Format values + // $utrDate = !empty($utrRow['utr_date']) + // ? date('Y-m-d', strtotime($utrRow['utr_date'])) + // : null; + + // $amount = isset($utrRow['amount']) + // ? (float)$utrRow['amount'] + // : 0; + + $result_utr = $this->InvoiceUtrModel->insert([ + 'invoice_id' => $invoiceId, + 'utr_no' => $utrNo, + 'utr_date' => null, + 'amount' => 0, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => $input['created_by'] ?? 0 + ]); + + // if (!$result_utr) { + // print_r($this->InvoiceUtrModel->errors()); + // die; + // } + } + } if ($this->db->transStatus() === false) { $this->db->transRollback(); @@ -193,8 +318,14 @@ class InvoiceController extends ResourceController private function generateInvoiceNo($pos_id) { + $pos_id = !empty($pos_id) ? $pos_id : 0; $year = date('Y'); - $prefix = "NIIB/$pos_id/$year/"; + + // If pos_id is missing/invalid (0/null/"w"), generate invoice as MIG/Year/NNNNN + // Otherwise generate as NIIB/{pos_id}/Year/NNNNN + $prefix = ($pos_id > 0) + ? "NIIB/{$pos_id}/{$year}/" + : "MIG/{$year}/"; // Get last invoice of current year $lastInvoice = $this->InvoiceModel @@ -253,7 +384,13 @@ class InvoiceController extends ResourceController // Build the main query $query = $this->PolicyModel - ->select('partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id ,pa.name as agent_name,pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no') + ->select('partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id ,pa.name as agent_name,pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no, + COALESCE(( + SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") + FROM partner_invoice_utr piu + WHERE piu.invoice_id = pi.id + AND piu.is_active = 1 + ), "") as utr_no', false) ->join( 'partner_enquiry pe', 'pe.id = partner_policy.enquiry_id AND pe.broker_id = ' . (int)$input['broker_id'], @@ -354,7 +491,7 @@ class InvoiceController extends ResourceController ->join('partner_invoice_items pii', 'pii.policy_id = partner_policy.id', 'left') ->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'left') ->where('partner_policy.is_active', 1) - ->where('partner_policy.commission_amount >', 0) + ->where('partner_policy.commission_amount >=', 0) ->where('partner_policy.is_data_accuracy_checked', 1) ->where('partner_policy.manager_id', $manager_id) ->where('pii.policy_id IS NULL') // ❗ unused commission @@ -388,6 +525,809 @@ class InvoiceController extends ResourceController } } + public function updateUtrDetails() + { + $this->db->transBegin(); + + try { + $input = $this->request->getJSON(true); + + $invoiceId = (int)($input['invoice_id'] ?? 0); + $paidAmount = (float)($input['paid_amount'] ?? 0); + $paidDateRaw = $input['paid_date'] ?? null; + $utrId = (int)($input['utr_id'] ?? 0); + $utrNoInput = trim((string)($input['utr_no'] ?? '')); + + if ($invoiceId <= 0 || $paidAmount < 0 || empty($paidDateRaw)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id, paid_amount and paid_date are required' + ], 400); + } + + $invoice = $this->InvoiceModel + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } + + $paidDateTs = strtotime($paidDateRaw); + if ($paidDateTs === false) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Invalid paid_date format' + ], 400); + } + $paidDate = date('Y-m-d', $paidDateTs); + + $utrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $currentTotalPaid = $utrPaidAmount; + $invoiceAmount = (float) ($invoice['invoice_amount'] ?? 0); + $remainingBalance = max($invoiceAmount - $currentTotalPaid, 0); + + /* + * If utr_id is empty/0 from request: + * 1) create a fresh UTR row with given utr_no (or generated fallback), + * 2) keep amount = 0 first, + * 3) then continue existing update flow using the new utr_id. + */ + if ($utrId <= 0) { + $utrNo = $utrNoInput !== '' ? $utrNoInput : ('UTR' . date('YmdHis')); + while ( + !empty( + $this->InvoiceUtrModel + ->where('invoice_id', $invoiceId) + ->where('utr_no', $utrNo) + ->where('is_active', 1) + ->first() + ) + ) { + usleep(200000); + $utrNo = 'UTR' . date('YmdHis'); + } + + $insertedUtrId = $this->InvoiceUtrModel->insert([ + 'invoice_id' => $invoiceId, + 'utr_no' => $utrNo, + 'utr_date' => null, + 'amount' => 0, + 'is_active' => 1, + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => (int)($input['created_by'] ?? 0), + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); + + if ($insertedUtrId === false) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> 'Failed to create UTR', + 'error' => $this->InvoiceUtrModel->errors() + ], 500); + } + + $utrId = (int)$insertedUtrId; + } + + $utrQuery = $this->InvoiceUtrModel + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('id', $utrId); + $utrRow = $utrQuery->first(); + + if (empty($utrRow)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Selected UTR not found for this invoice' + ], 404); + } + + $existingUtrAmount = (float)($utrRow['amount'] ?? 0); + if ($paidAmount > ($remainingBalance + $existingUtrAmount)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount exceeds invoice balance' + ], 400); + } + if ($paidAmount < $existingUtrAmount) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount cannot be less than existing paid amount' + ], 400); + } + + $otherUtrPaidAmount = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('id !=', (int)$utrRow['id']) + ->first()['total'] ?? 0 + ); + $maxAllowedForSelectedUtr = max($invoiceAmount - $otherUtrPaidAmount, 0); + if ($paidAmount > $maxAllowedForSelectedUtr) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'Paid amount exceeds invoice balance' + ], 400); + } + + $updated = $this->InvoiceUtrModel->update((int)$utrRow['id'], [ + 'amount' => $paidAmount, + 'utr_date' => $paidDate, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); + + if ($updated === false) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> 'Failed to update UTR payment', + 'error' => $this->InvoiceUtrModel->errors() + ], 500); + } + + $latestTotalPaid = ($currentTotalPaid - $existingUtrAmount) + $paidAmount; + $newBalance = max($invoiceAmount - $latestTotalPaid, 0); + $payoutStatus = $newBalance == 0.0 ? 2 : 1; + + $this->InvoiceModel->update($invoiceId, [ + 'payout_status' => $payoutStatus, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => (int)($input['updated_by'] ?? 0), + ]); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'utr_id' => (int)$utrRow['id'], + 'utr_no' => (string)$utrRow['utr_no'], + 'invoice_id' => $invoiceId, + 'paid_amount' => $latestTotalPaid, + 'balance_amount' => $newBalance + ] + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } + + public function addInvoicePayment() + { + return $this->updateUtrDetails(); + } + + public function listUtrDetails() + { + try { + $invoiceId = (int)($this->request->getGet('invoice_id') ?? 0); + if ($invoiceId <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id is required' + ], 400); + } + + $invoice = $this->InvoiceModel + ->select('id, invoice_no, invoice_amount') + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } + + $history = $this->db->table('partner_invoice_utr piu') + ->select(' + piu.id, + piu.invoice_id, + pi.invoice_no, + pi.invoice_amount AS total_amount, + piu.amount AS paid_amount, + piu.utr_no, + piu.utr_date AS paid_date, + piu.created_at, + COALESCE(psu.name, psc.name, "-") AS createdby_name, + ( + pi.invoice_amount - COALESCE(( + SELECT SUM(piu2.amount) + FROM partner_invoice_utr piu2 + WHERE piu2.invoice_id = piu.invoice_id + AND piu2.is_active = 1 + AND piu2.amount > 0 + AND piu2.id <= piu.id + ), 0) + ) AS balance_amount + ', false) + ->join('partner_invoice pi', 'pi.id = piu.invoice_id', 'inner') + ->join('partner_staff psc', 'psc.id = piu.created_by', 'left') + ->join('partner_staff psu', 'psu.id = piu.updated_by', 'left') + ->where('piu.invoice_id', $invoiceId) + ->where('piu.is_active', 1) + ->where('piu.amount >', 0) + ->orderBy('piu.id', 'DESC') + ->get() + ->getResultArray(); + + // Return unique UTR list for this invoice (remove duplicates by utr_no). + // Keep only unpaid UTR rows (amount 0 or null). + $availableUtrs = $this->db->table('partner_invoice_utr') + ->select('MIN(id) as id, utr_no') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->where('(amount IS NULL OR amount <= 0)', null, false) + ->where('TRIM(COALESCE(utr_no, "")) !=', '') + ->groupBy('utr_no') + ->orderBy('MIN(id)', 'ASC', false) + ->get() + ->getResultArray(); + + $paidToDate = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + $balanceAmount = max(((float)$invoice['invoice_amount']) - $paidToDate, 0); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => $history, + 'available_utrs' => $availableUtrs, + 'invoice'=> [ + 'invoice_id' => (int)$invoice['id'], + 'invoice_no' => $invoice['invoice_no'], + 'total_amount' => (float)$invoice['invoice_amount'], + 'paid_to_date' => $paidToDate, + 'balance_amount' => $balanceAmount, + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } + + public function addPaymentHistory() + { + return $this->listUtrDetails(); + } + + public function utrDetails() + { + try { + $invoiceId = (int)($this->request->getGet('invoice_id') ?? 0); + if ($invoiceId <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'invoice_id is required' + ], 400); + } + + $invoice = $this->InvoiceModel + ->select('id, invoice_no, invoice_amount') + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (empty($invoice)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message'=> 'Invoice not found' + ], 404); + } + + $utrDetails = $this->InvoiceUtrModel + ->select('id, invoice_id, amount as paid_amount, utr_no, utr_date') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->orderBy('id', 'ASC') + ->findAll(); + + $paidToDate = (float) ( + $this->InvoiceUtrModel + ->selectSum('amount', 'total') + ->where('invoice_id', $invoiceId) + ->where('is_active', 1) + ->first()['total'] ?? 0 + ); + + $balanceAmount = max(((float)$invoice['invoice_amount']) - $paidToDate, 0); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'utrs_details' => $utrDetails, + 'invoice' => [ + 'invoice_id' => (int)$invoice['id'], + 'invoice_no' => $invoice['invoice_no'], + 'total_amount' => (float)$invoice['invoice_amount'], + 'paid_to_date' => $paidToDate, + 'balance_amount' => $balanceAmount, + ], + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message'=> $e->getMessage() + ], 500); + } + } + + public function bulkUploadCommission() + { + $this->db->transBegin(); + try { + $input = $this->request->getJSON(true); + if (!is_array($input)) { + $input = []; + } + + $post = $this->request->getPost(); + if (!is_array($post)) { + $post = []; + } + + $rows = $input['rows'] ?? []; + $updatedBy = (int)($input['updated_by'] ?? ($post['created_by'] ?? 0)); + + // Multipart flow: read excel on backend and store upload metadata. + $file = $this->request->getFile('file_name'); + if ($file && $file->isValid()) { + $month = date('Y-m-d'); + $uploadPath = WRITEPATH . 'uploads/agent/incentive_file/'; + if (!is_dir($uploadPath)) { + mkdir($uploadPath, 0777, true); + } + + $storedFileName = time() . '_' . $file->getRandomName(); + $file->move($uploadPath, $storedFileName); + + $this->AgentIncentiveFileModel->insert([ + 'incentive_month' => $month, + 'incentive_file_name' => $storedFileName, + 'file_type' => 'invoice', + 'is_active' => 1, + 'created_by' => $updatedBy > 0 ? $updatedBy : null, + ], true); + + $spreadsheet = IOFactory::load($uploadPath . $storedFileName); + $excelRows = $spreadsheet->getActiveSheet()->toArray(null, true, true, false); + + if (empty($excelRows)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Uploaded file is empty', + ], 400); + } + + $headerRow = $excelRows[0] ?? []; + $headerIndex = []; + foreach ($headerRow as $idx => $headerValue) { + $normalized = preg_replace('/[^a-z0-9]/', '', strtolower(trim((string)$headerValue))); + if (!empty($normalized)) { + $headerIndex[$normalized] = (int)$idx; + } + } + + $policyIdx = $headerIndex['policynumber'] ?? null; + $invoiceIdx = $headerIndex['invoicenumber'] ?? null; + $commissionIdx = $headerIndex['commission'] ?? ($headerIndex['commissionamount'] ?? null); + + if ($policyIdx === null || $commissionIdx === null) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Expected columns: Policy Number, Invoice Number, Commission or Commission Amount', + ], 400); + } + + $fallbackInvoiceNo = trim((string)($post['invoice_number'] ?? '')); + $rows = []; + foreach ($excelRows as $index => $row) { + if ($index === 0) { + continue; + } + + $policyNo = trim((string)($row[$policyIdx] ?? '')); + $invoiceNoFromFile = $invoiceIdx !== null ? trim((string)($row[$invoiceIdx] ?? '')) : ''; + $invoiceNo = $invoiceNoFromFile !== '' ? $invoiceNoFromFile : $fallbackInvoiceNo; + $commissionRaw = trim((string)($row[$commissionIdx] ?? '')); + + if ($policyNo === '' && $invoiceNo === '' && $commissionRaw === '') { + continue; + } + + $commission = (float)str_replace(',', '', $commissionRaw); + if ($policyNo === '' || $invoiceNo === '' || !is_numeric(str_replace(',', '', $commissionRaw))) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'Invalid data at line ' . ($index + 1), + ], 400); + } + + $rows[] = [ + 'line_no' => $index + 1, + 'policy_number' => $policyNo, + 'invoice_no' => $invoiceNo, + 'commission_amount' => $commission, + ]; + } + } + + if (empty($rows) || !is_array($rows)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'rows is required', + ], 400); + } + + $mismatchRows = []; + $validRows = []; + $skippedRows = 0; + + foreach ($rows as $row) { + $policyNo = trim((string)($row['policy_number'] ?? '')); + $invoiceNo = trim((string)($row['invoice_no'] ?? '')); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + $lineNo = (int)($row['line_no'] ?? 0); + + if ($policyNo === '' || $invoiceNo === '') { + $skippedRows++; + continue; + } + + $record = $this->db->table('partner_invoice_items pii') + ->select(' + pii.id as item_id, + pii.invoice_id, + pii.policy_id, + pii.policy_no, + pi.invoice_no, + pi.agent_id as invoice_agent_json, + pe.agent_id as policy_agent_id + ') + ->join('partner_invoice pi', 'pi.id = pii.invoice_id', 'inner') + ->join('partner_policy pp', 'pp.id = pii.policy_id', 'left') + ->join('partner_enquiry pe', 'pe.id = pp.enquiry_id', 'left') + ->where('pii.is_active', 1) + ->where('pi.is_active', 1) + ->where('pi.invoice_no', $invoiceNo) + ->where('pii.policy_no', $policyNo) + ->get() + ->getRowArray(); + + if (empty($record)) { + $skippedRows++; + continue; + } + + $invoiceAgentIds = $this->extractAgentIdsFromInvoice($record['invoice_agent_json'] ?? ''); + $policyAgentId = isset($record['policy_agent_id']) ? (int)$record['policy_agent_id'] : 0; + $targetAgentId = !empty($invoiceAgentIds) ? (int)$invoiceAgentIds[0] : 0; + $isMismatch = $policyAgentId > 0 && !in_array($policyAgentId, $invoiceAgentIds, true); + + if ($isMismatch) { + $mismatchRows[] = [ + 'line_no' => $lineNo, + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'policy_number' => $policyNo, + 'invoice_no' => $invoiceNo, + 'commission_amount' => $commissionAmount, + 'policy_agent_id' => $policyAgentId, + 'target_agent_id' => $targetAgentId, + ]; + continue; + } + + $validRows[] = [ + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'commission_amount' => $commissionAmount, + ]; + } + + if (!empty($mismatchRows)) { + $this->ensureBulkUploadStagingTable(); + $token = $this->generateProceedToken(); + + $this->db->table('invoice_bulk_upload_staging')->insert([ + 'proceed_token' => $token, + 'payload_json' => json_encode([ + 'rows' => $rows, + 'updated_by' => $updatedBy, + ]), + 'mismatch_json' => json_encode($mismatchRows), + 'created_at' => date('Y-m-d H:i:s'), + 'created_by' => $updatedBy, + 'is_active' => 1, + ]); + + // Keep staged token/payload so proceed API can continue later. + $this->db->transCommit(); + return $this->respond([ + 'status' => 'partner_mismatch', + 'code' => 200, + 'message' => 'Partner mismatch detected', + 'mismatch_count' => count($mismatchRows), + 'mismatches' => $mismatchRows, + 'proceed_token' => $token, + ], 200); + } + + $updatedCount = $this->applyBulkCommissionRows($validRows, $updatedBy); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Bulk upload completed', + 'updated_count' => $updatedCount, + 'skipped_count' => $skippedRows, + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message' => $e->getMessage(), + ], 500); + } + } + + public function bulkUploadCommissionProceed() + { + $this->db->transBegin(); + try { + $input = $this->request->getJSON(true); + $proceedToken = trim((string)($input['proceed_token'] ?? '')); + $forceReassign = (int)($input['force_reassign_partner'] ?? 0); + $updatedBy = (int)($input['updated_by'] ?? 0); + + if ($proceedToken === '' || $forceReassign !== 1) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message' => 'proceed_token and force_reassign_partner=1 are required', + ], 400); + } + + $this->ensureBulkUploadStagingTable(); + $staging = $this->db->table('invoice_bulk_upload_staging') + ->where('proceed_token', $proceedToken) + ->where('is_active', 1) + ->get() + ->getRowArray(); + + if (empty($staging)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message' => 'Invalid or expired proceed token', + ], 404); + } + + $payload = json_decode($staging['payload_json'] ?? '{}', true); + $rows = $payload['rows'] ?? []; + $mismatchRows = json_decode($staging['mismatch_json'] ?? '[]', true); + + foreach ($mismatchRows as $mismatch) { + $policyId = (int)($mismatch['policy_id'] ?? 0); + $targetAgentId = (int)($mismatch['target_agent_id'] ?? 0); + if ($policyId <= 0 || $targetAgentId <= 0) { + continue; + } + + $policy = $this->PolicyModel->select('enquiry_id')->where('id', $policyId)->first(); + if (!empty($policy['enquiry_id'])) { + $this->EnquiryModel->update((int)$policy['enquiry_id'], [ + 'agent_id' => $targetAgentId, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + } + } + + $finalRows = []; + foreach ($rows as $row) { + $policyNo = trim((string)($row['policy_number'] ?? '')); + $invoiceNo = trim((string)($row['invoice_no'] ?? '')); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + if ($policyNo === '' || $invoiceNo === '') { + continue; + } + + $record = $this->db->table('partner_invoice_items pii') + ->select('pii.id as item_id, pii.invoice_id, pii.policy_id') + ->join('partner_invoice pi', 'pi.id = pii.invoice_id', 'inner') + ->where('pii.is_active', 1) + ->where('pi.is_active', 1) + ->where('pi.invoice_no', $invoiceNo) + ->where('pii.policy_no', $policyNo) + ->get() + ->getRowArray(); + + if (empty($record)) { + continue; + } + + $finalRows[] = [ + 'item_id' => (int)$record['item_id'], + 'invoice_id' => (int)$record['invoice_id'], + 'policy_id' => (int)$record['policy_id'], + 'commission_amount' => $commissionAmount, + ]; + } + + $updatedCount = $this->applyBulkCommissionRows($finalRows, $updatedBy); + + $this->db->table('invoice_bulk_upload_staging') + ->where('id', (int)$staging['id']) + ->update([ + 'is_active' => 0, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + + if ($this->db->transStatus() === false) { + $this->db->transRollback(); + throw new DataException('Transaction failed'); + } + + $this->db->transCommit(); + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Bulk upload completed with partner reassignment', + 'updated_count' => $updatedCount, + ], 200); + } catch (\Exception $e) { + $this->db->transRollback(); + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message' => $e->getMessage(), + ], 500); + } + } + + private function applyBulkCommissionRows(array $rows, int $updatedBy): int + { + $updatedCount = 0; + foreach ($rows as $row) { + $itemId = (int)($row['item_id'] ?? 0); + $invoiceId = (int)($row['invoice_id'] ?? 0); + $policyId = (int)($row['policy_id'] ?? 0); + $commissionAmount = (float)($row['commission_amount'] ?? 0); + if ($itemId <= 0) { + continue; + } + + $affected = $this->db->table('partner_invoice_items') + ->where('id', $itemId) + ->where('invoice_id', $invoiceId) + ->where('policy_id', $policyId) + ->where('is_active', 1) + ->update([ + 'commission_amount' => $commissionAmount, + 'updated_at' => date('Y-m-d H:i:s'), + 'updated_by' => $updatedBy, + ]); + + if ($affected) { + $updatedCount++; + } + } + + return $updatedCount; + } + + private function extractAgentIdsFromInvoice($agentJson): array + { + if (is_array($agentJson)) { + return array_values(array_map('intval', $agentJson)); + } + if ($agentJson === null || $agentJson === '') { + return []; + } + + $decoded = json_decode((string)$agentJson, true); + if (is_array($decoded)) { + return array_values(array_map('intval', $decoded)); + } + + if (is_numeric($agentJson)) { + return [(int)$agentJson]; + } + + return []; + } + + private function generateProceedToken(): string + { + return bin2hex(random_bytes(16)); + } + + private function ensureBulkUploadStagingTable(): void + { + $sql = "CREATE TABLE IF NOT EXISTS invoice_bulk_upload_staging ( + id INT AUTO_INCREMENT PRIMARY KEY, + proceed_token VARCHAR(64) NOT NULL UNIQUE, + payload_json LONGTEXT NULL, + mismatch_json LONGTEXT NULL, + is_active TINYINT(1) NOT NULL DEFAULT 1, + created_at DATETIME NULL, + created_by INT NULL, + updated_at DATETIME NULL, + updated_by INT NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"; + $this->db->query($sql); + } + // public function getCommissionRateList() // { diff --git a/app/Controllers/PolicyController.php b/app/Controllers/PolicyController.php index 213088c..a9398c0 100644 --- a/app/Controllers/PolicyController.php +++ b/app/Controllers/PolicyController.php @@ -413,6 +413,82 @@ class PolicyController extends ResourceController } } + public function updatePolicyCommission() + { + try { + $data = $this->request->getJSON(true); + + if (empty($data['id'])) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'Policy id is required', + ], 422); + } + + if (!isset($data['commission_amount']) || $data['commission_amount'] === '') { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'commission_amount is required', + ], 422); + } + + $policyId = (int) $data['id']; + $policy = $this->PolicyModel->find($policyId); + if (!$policy) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Policy not found', + ], 404); + } + + $commission = (float) $data['commission_amount']; + if ($commission < 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => 'commission_amount must be positive', + ], 422); + } + + /* + * Commission-only update endpoint. + * Keeps the existing policy workflow unchanged and updates only required fields. + */ + $updateData = [ + 'commission_amount' => number_format($commission, 2, '.', ''), + 'updated_by' => $data['updated_by'] ?? null, + 'updated_on' => date('Y-m-d H:i:s'), + ]; + + if (!$this->PolicyModel->update($policyId, $updateData)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 422, + 'data' => $this->PolicyModel->errors(), + ], 422); + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'policy_id' => (string) $policyId, + 'commission_amount' => $updateData['commission_amount'], + 'message' => 'Commission updated successfully', + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + public function uploadPolicyFile() { try { diff --git a/app/Helpers/common_helper.php b/app/Helpers/common_helper.php index 7f3ae14..6d84858 100644 --- a/app/Helpers/common_helper.php +++ b/app/Helpers/common_helper.php @@ -181,8 +181,9 @@ if (!function_exists('createBDS')) { if (!function_exists('format_date_for_database')) { - function format_date_for_database(string $date): ?string + function format_date_for_database(?string $date): ?string { + if (empty($date)) return null; // ✅ handle null/empty $dt = DateTime::createFromFormat('d-m-Y', $date); if ($dt) { return $dt->format('Y-m-d'); diff --git a/app/Models/AgentIncentiveFileModel.php b/app/Models/AgentIncentiveFileModel.php index 735cf33..9a86903 100644 --- a/app/Models/AgentIncentiveFileModel.php +++ b/app/Models/AgentIncentiveFileModel.php @@ -15,6 +15,7 @@ class AgentIncentiveFileModel extends Model 'agent_id', 'incentive_month', 'incentive_file_name', + 'file_type', 'is_active', 'created_by', 'created_on', @@ -32,8 +33,9 @@ class AgentIncentiveFileModel extends Model // Validation rules (optional, add as per your need) protected $validationRules = [ - 'agent_id' => 'required|integer', - 'incentive_month' => 'required|valid_date', - 'incentive_file_name'=> 'required|string|max_length[150]' + 'agent_id' => 'permit_empty|integer', + 'incentive_month' => 'required|valid_date', + 'incentive_file_name' => 'required|string|max_length[150]', + 'file_type' => 'permit_empty|max_length[50]' ]; } diff --git a/app/Models/PartnerInsurancePayoutGridModel.php b/app/Models/PartnerInsurancePayoutGridModel.php new file mode 100644 index 0000000..bd7598c --- /dev/null +++ b/app/Models/PartnerInsurancePayoutGridModel.php @@ -0,0 +1,78 @@ + 'required|max_length[100]', + 'insurer' => 'required|max_length[45]', + 'segment' => 'required|max_length[100]', + ]; + + protected $validationMessages = [ + 'vehicle_type' => ['required' => 'Vehicle type is required.'], + 'insurer' => ['required' => 'Insurer is required.'], + 'segment' => ['required' => 'Segment is required.'], + ]; + + protected $skipValidation = false; + + // ------------------------------------------------------------------------- + // Fetch grid filtered by vehicle_type + // ------------------------------------------------------------------------- + public function getByVehicleType(string $vehicleType): array + { + return $this + ->where('vehicle_type', $vehicleType) + ->orderBy('insurer', 'ASC') + ->findAll(); + } + + // ------------------------------------------------------------------------- + // Fetch grid filtered by insurer + rto + // ------------------------------------------------------------------------- + public function getByInsurerAndRto(string $insurer, string $rto): array + { + return $this + ->where('insurer', $insurer) + ->where('rto', $rto) + ->orderBy('vehicle_type', 'ASC') + ->findAll(); + } + +} diff --git a/app/Models/PolicyModel.php b/app/Models/PolicyModel.php index b53c047..7dc61e5 100644 --- a/app/Models/PolicyModel.php +++ b/app/Models/PolicyModel.php @@ -30,6 +30,7 @@ class PolicyModel extends Model 'pt_oc_share_details_id', 'created_by', 'updated_by', + 'updated_on', // newly added 'tp', diff --git a/writable/uploads/sample_partner_grid_file.xls b/writable/uploads/sample_partner_grid_file.xls new file mode 100644 index 0000000..bdadf05 Binary files /dev/null and b/writable/uploads/sample_partner_grid_file.xls differ