diff --git a/app/Config/Acl.php b/app/Config/Acl.php index f2937d2b..ce46852c 100644 --- a/app/Config/Acl.php +++ b/app/Config/Acl.php @@ -43,6 +43,7 @@ class Acl '#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]], '#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], '#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]], + '#^/viewClaimFile#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID]], diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e5378f79..74dddbef 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -508,6 +508,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) { $routes->post("saveInvoicePaymentDetails", "PolicyTransactionController::saveInvoicePaymentDetails"); $routes->get("deletePaymentEntry/(:any)", "PolicyTransactionController::deletePaymentEntry/$1"); $routes->get("downloadSampleInsurerStatement", "PolicyTransactionController::downloadSampleInsurerStatement"); + $routes->get("downloadInsurerStatement/(:num)", "PolicyTransactionController::downloadInsurerStatement/$1"); $routes->get("getFileErr/(:any)", "PolicyTransactionController::getFileErr/$1"); $routes->get("getInsurerStatementMonth", "PolicyTransactionController::getInsurerStatementMonth"); $routes->get("deleteStatement/(:any)", "PolicyTransactionController::deleteStatement/$1"); @@ -671,6 +672,9 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu $routes->post("getVerifiedRetailUserData", "RestAuthenticationController::getVerifiedRetailUserData"); $routes->post("updateRetailUserAuthDetails", "RestAuthenticationController::updateRetailUserAuthDetails"); + //samulss oauth login api's + $routes->get("getTokenforSamulssOAuthLogin", "RestAuthenticationController::getTokenforSamulssOAuthLogin"); + }); $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) { @@ -1009,6 +1013,9 @@ $routes->group('sales', function($routes) { // Get all leads with filters $routes->get('leads', 'SalesController::getLeads'); + + // Export leads by created date range + $routes->get('leads/export', 'SalesController::exportLeads'); // Get lead statistics $routes->get('leads/stats', 'SalesController::getLeadStats'); @@ -1055,6 +1062,9 @@ $routes->group('sales', function($routes) { // Get all activities with filters $routes->get('activities', 'SalesController::getActivities'); + + // Export activities by created date range + $routes->get('activities/export', 'SalesController::exportActivities'); // Get upcoming activities $routes->get('activities/upcoming', 'SalesController::getUpcomingActivities'); @@ -1118,6 +1128,6 @@ $routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controller $routes->get('client-policies', 'ExpenseController::clientPolicies'); }); - $routes->get('docs', 'Docs\DocsController::index'); - $routes->get('docs/(:segment)', 'Docs\DocsController::page/$1'); \ No newline at end of file + $routes->get('docs/(:segment)', 'Docs\DocsController::page/$1'); + diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index e8d27f3e..05873250 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -692,7 +692,8 @@ class EmployeeController extends AdminController return $this->response->download($filePath, null, $mimeType); } else { // File not found, show an error message or redirect - echo view('errors/html/production'); + echo view('errors/404.php', ['message' => 'Sample file not found']); + // return redirect()->back()->with('error', 'Sample file not found'); } } diff --git a/app/Controllers/EmployeeMultiEventServiceController.php b/app/Controllers/EmployeeMultiEventServiceController.php index 4f51f1ad..fef1e2f3 100644 --- a/app/Controllers/EmployeeMultiEventServiceController.php +++ b/app/Controllers/EmployeeMultiEventServiceController.php @@ -227,7 +227,7 @@ class EmployeeMultiEventServiceController extends BaseController 'format' => 'mobile', 'allowed_values' => null, 'custom' => 'check_dup_mobileno', - 'params' => ['row', 'existing_mobilenos'] + 'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key'] ], 'email' => [ 'col_idx' => 13, @@ -238,7 +238,7 @@ class EmployeeMultiEventServiceController extends BaseController 'format' => null, 'allowed_values' => null, 'custom' => 'check_dup_email', - 'params' => ['row', 'existing_mobilenos'] + 'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key'] ], 'pre_existing_ailments' => [ 'col_idx' => 14, diff --git a/app/Controllers/EmployeeServiceController.php b/app/Controllers/EmployeeServiceController.php index 064d1dc5..ecf2d49a 100755 --- a/app/Controllers/EmployeeServiceController.php +++ b/app/Controllers/EmployeeServiceController.php @@ -230,7 +230,7 @@ class EmployeeServiceController extends AdminController 'format' => 'mobile', 'allowed_values' => null, 'custom' => 'check_dup_mobileno', - 'params' => ['row', 'existing_mobilenos'] + 'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key'] ], 'email' => [ 'col_idx' => 13, @@ -241,7 +241,7 @@ class EmployeeServiceController extends AdminController 'format' => null, 'allowed_values' => null, 'custom' => 'check_dup_email', - 'params' => ['row', 'existing_mobilenos'] + 'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key'] ], 'pre_existing_ailments' => [ 'col_idx' => 14, diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php index 1cd91f3a..cbaf1115 100644 --- a/app/Controllers/FhplApiController.php +++ b/app/Controllers/FhplApiController.php @@ -503,12 +503,19 @@ class FhplApiController extends BaseController log_message('error', "Failed to update file table status."); } - log_message('error', 'FHPL - TPA ID Pull API FAILED | API failed: Empty menber data for this pull request'); + log_message('error', 'FHPL - TPA ID Pull API FAILED | API failed: Empty member data for this pull request'); + + $file_model = new BatchFileModel(); + $bfData = [ + 'error_data' => json_encode(['error_data' => 'API failed: Empty member data for this pull request.']), + 'status' => 'failed-7', + ]; + $file_model->where('id', $requestData['file_id'])->set($bfData)->update(); if($function_calling_type == "job"){ - return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request']; + return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request']; }else{ - return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request']); + return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request']); } } @@ -1068,7 +1075,7 @@ class FhplApiController extends BaseController 'is_active' => 1, 'created_by' => $file_info[0]['created_by'] ?? null, - 'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D' + 'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D', 'si' => $row['BASE_SUMINSURED'], 'doj' => change_date_format($row['DATE_OF_JOINING'],'Y-m-d\TH:i:s'), 'endorsement_no' => $row['ENDORSEMENT_NO'] diff --git a/app/Controllers/LeadsController.php b/app/Controllers/LeadsController.php index ce2dc8f0..7f347d6f 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -571,6 +571,72 @@ class LeadsController extends BaseController } + $leadFormType = (int) ($postData['lead_form_type'] ?? 1); + $postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? [])); + $claimHistoryPolicyTypes = [1, 6, 7]; + $hasClaimHistoryPolicy = count(array_intersect($postedPolicyTypeIds, $claimHistoryPolicyTypes)) > 0; + $isEbClaimHistory = $leadFormType === 1 + && in_array((int) ($postData['lead_type'] ?? 0), [2, 3], true) + && $hasClaimHistoryPolicy + && (int) ($postData['claim_history'] ?? 0) === 1; + + if ($isEbClaimHistory) { + $rules['first_year.*'] = [ + 'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]', + 'errors' => [ + 'required' => 'Claim Year is required for all entries.', + 'regex_match' => 'Year must be in format YYYY-YYYY.', + ], + ]; + $rules['emp_id.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee ID is required in Claim History.'], + ]; + $rules['emp_name.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Employee Name is required in Claim History.'], + ]; + $rules['gender.*'] = [ + 'rules' => 'required|in_list[Female,Male]', + 'errors' => [ + 'required' => 'Gender is required in Claim History.', + 'in_list' => 'Gender must be Female or Male.', + ], + ]; + $rules['designation.*'] = [ + 'rules' => 'required', + 'errors' => ['required' => 'Designation is required in Claim History.'], + ]; + $rules['sum_insured.*'] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Sum Insured is required in Claim History.', + 'numeric' => 'Sum Insured must be a number.', + ], + ]; + $rules['first_death_date.*'] = [ + 'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]', + 'errors' => [ + 'required' => 'Date of Death is required.', + 'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.', + ], + ]; + $rules['first_cause_of_death.*'] = [ + 'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]', + 'errors' => [ + 'required' => 'Nature/Cause Of Death is required.', + 'in_list' => 'Nature/Cause Of Death is invalid.', + ], + ]; + $rules['first_claim_amount.*'] = [ + 'rules' => 'required|numeric', + 'errors' => [ + 'required' => 'Claim/Settled Amount is required.', + 'numeric' => 'Claim/Settled Amount must be a number.', + ], + ]; + } + // 1. MANUALLY VALIDATE FILES BEFORE PROCESSING // $allFiles = $this->request->getFiles(); // foreach ($allFiles as $inputName => $files) { @@ -680,7 +746,7 @@ class LeadsController extends BaseController $data['client_code'] = generate_client_code('IC'); } - if (isset($data['lead_form_type'])) { + if ((int) ($data['lead_form_type'] ?? 1) === 2) { $data = $this->prepareSingleLeadData($data); } else { $data = $this->prepareMultipleLeadData($data); @@ -950,10 +1016,10 @@ class LeadsController extends BaseController $insertCount[] = $insert; $this->insertLeadStatus($insert, $value['status'], 3); - if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') { - $leadDataForClient = $this->leadsModel->find($insert); - $this->createClientWithLeadData($leadDataForClient); - } + // if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') { + // $leadDataForClient = $this->leadsModel->find($insert); + // $this->createClientWithLeadData($leadDataForClient); + // } if ($value['lead_form_type'] == 1) { //for this push the job to the calculateMembersDemography() function @@ -981,12 +1047,12 @@ class LeadsController extends BaseController $this->insertMultiFilesData($data[0]['multi_file_data'], $id, $data[0]['lead_form_type']); $this->insertLeadStatus($id, $data[0]['status'], 3); - if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') { - $leadDataForClient = $this->leadsModel->find($id); - if (empty($leadDataForClient['is_client_created'])) { - $this->createClientWithLeadData($leadDataForClient); - } - } + // if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') { + // $leadDataForClient = $this->leadsModel->find($id); + // if (empty($leadDataForClient['is_client_created'])) { + // $this->createClientWithLeadData($leadDataForClient); + // } + // } return $this->respond(['status' => true, 'lead_id' => $id, 'message' => "Opportunity updated successfully", 'data' => $data], 200); } @@ -2140,6 +2206,8 @@ class LeadsController extends BaseController { helper('excel_util_helper'); $rfq_data = $this->RFQModel->getRFQTableDataWithLeadIDAndType($lead_id, $type); + $claim_history = $rfq_data['claim_history'] ?? 0; + $is_placement = false; $length = 0; @@ -2176,7 +2244,7 @@ class LeadsController extends BaseController 'No of Dependents' => $rfq_data['incept_dept_count'], 'Total Lives' => $rfq_data['incept_no_of_lives'], - 'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", + // 'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", // 'Policy Run Days' => $rfq_data['policy_run_days'], @@ -2186,7 +2254,7 @@ class LeadsController extends BaseController 'Insured' => $rfq_data['client_name'], 'No of Employees at Inception' => $rfq_data['incept_emp_count'], // 'Total Sum Insured at Inception ' => $rfq_data['total_si_at_incept'], - 'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", + // 'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", 'Policy Type' => $this->leadType[$rfq_data['lead_type']] ?? "", @@ -2279,7 +2347,7 @@ class LeadsController extends BaseController 'Existing Insurer' => $rfq_data['insurer_name'], 'No of Employees at Renewal' => $rfq_data['renewal_emp_count'], 'Total Sum Insured at Renewal' => $rfq_data['total_si_at_renewal'], - 'Claims Experience for last 3 years' => "Mentioned in Claims sheet", + 'Claims Experience for last 3 years' => $claim_history == "1" ? "Mentioned in Claims sheet" : "Nil", // 'Insurer' => $rfq_data['insurer_name'] ?? " - ", // 'TPA' => $rfq_data['tpa_name'] ?? " - ", // 'Total Lives at Inception' => $rfq_data['total_lives_at_incept'], @@ -2796,7 +2864,7 @@ class LeadsController extends BaseController } //claim history new sheet; - if (! empty($rfq_data['fin_years_claims'])) { + if ($claim_history == 1 && !empty($rfq_data['fin_years_claims'])) { $claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? []; @@ -4774,7 +4842,18 @@ class LeadsController extends BaseController // 🔹 Client Details (Single Row) $data['actual_lead_client_details'] = $this->leadModel - ->select('sales_actual_leads.company_name, clients.short_name, clients.client_type, sales_actual_leads.email, sales_actual_leads.phone, sales_actual_leads.address, sales_actual_leads.website, sales_actual_leads.gst_number, sales_actual_leads.status, sales_actual_leads.assigned_to') + ->select(" + sales_actual_leads.company_name, + clients.short_name, + clients.client_type, + sales_actual_leads.email, + sales_actual_leads.phone, + sales_actual_leads.address, + sales_actual_leads.website, + COALESCE(NULLIF(sales_actual_leads.gst_number, ''), clients.gst) AS gst_number, + sales_actual_leads.status, + sales_actual_leads.assigned_to + ", false) ->join('clients', 'clients.id = sales_actual_leads.client_id', 'left') ->where('sales_actual_leads.lead_id', $actual_lead_id) ->first(); // first row only diff --git a/app/Controllers/PolicyTransactionController.php b/app/Controllers/PolicyTransactionController.php index 1c4c347b..26da53b2 100644 --- a/app/Controllers/PolicyTransactionController.php +++ b/app/Controllers/PolicyTransactionController.php @@ -4236,7 +4236,7 @@ class PolicyTransactionController extends BaseController return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); } - public function validateInsurerStatement($params) + public function validateInsurerStatementOLD1($params) { helper('excel_util_helper'); @@ -4392,8 +4392,7 @@ class PolicyTransactionController extends BaseController } } - - public function updateInsurerStatement($params) + public function updateInsurerStatementOLD($params) { helper('excel_util_helper'); //get file info @@ -4559,6 +4558,375 @@ class PolicyTransactionController extends BaseController return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); } + public function validateInsurerStatement($params) + { + /* + * Changes made: 12-06-2024 + * - Added safe file_id handling and DB file record validation before using file details. + * - Fixed physical file missing response and error message. + * - Skips empty Excel rows and collects unique policy numbers from the uploaded statement. + * - Fetches NHance source records only for those uploaded policy numbers. + * - Sanitizes policy and endorsement numbers before comparison to avoid hidden-space mismatch. + * - Validates each row by policy number + endorsement number combination. + * - Detects duplicate policy + endorsement rows and returns row-wise validation errors. + */ + helper('excel_util_helper'); + + $file_id = $params['file_id'] ?? 0; + + try { + + $error_data = ['error_code' => '', 'error_data' => []]; + $status = 'success'; + $ret_status = true; + + //get file info + $file = $this->insurerStatements->find((int)$file_id); + // dd($file); + + if (empty($file)) { + //file not found in DB + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => 'statement file not found in DB'])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name']; + + //check physical file + if (!file_exists($file_name_with_path)) { + //file not found update status and reason + $message = "Physical file not found"; + // echo $message; + $this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRow = $sheet->getHighestRow(); + $highestColumn = $sheet->getHighestColumn(); + + $excel_data = $sheet->rangeToArray('A1:' . $highestColumn . $highestRow); + unset($excel_data[0]); + $excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data); + // dd($excel_data); + + //get no of line items and update in DB + $line_items = 0; + + $policyNos = []; + + foreach ($excel_data as $row) { + if (check_row_is_empty_or_null($row)) { + continue; + } + + // row[1] => second column (B column) + $policyNo = $this->sanitizeStatementLookupValue($row[1] ?? ''); + + if ($policyNo !== '') { + $policyNos[] = $policyNo; + } + } + $policyNos = array_values(array_unique($policyNos)); + + // get uploaded month transactions data + // $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']); + $source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : []; + // dd($source_data); + + $source_lookup = []; + $source_policy_lookup = []; + $source_endorsement_lookup = []; + + foreach ($source_data as $source_row) { + $source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? ''); + $source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? ''); + $source_entry_key = $source_policy_no . '|' . $source_endorsement_no; + + $source_lookup[$source_entry_key] = true; + $source_policy_lookup[$source_policy_no] = true; + $source_endorsement_lookup[$source_policy_no][$source_endorsement_no] = true; + } + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + $matched_entry = []; + $error_messages = []; // row-wise error storage + + foreach ($excel_data as $excel_key => $excel_row) { + + $row_number = $excel_key; + $is_row_empty = check_row_is_empty_or_null($excel_row); + $policy_source_found = 0; + $endorsement_source_found = 0; + $duplicate_found = 0; + + if (!$is_row_empty) { + + $policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel + $endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel + $entry_key = $policy_no . '|' . $endorsement_no; + + if (isset($matched_entry[$entry_key])) { + $duplicate_found = 1; + } elseif (isset($source_lookup[$entry_key])) { + $line_items = $line_items + 1; + $matched_entry[$entry_key] = true; + continue; + } + + $policy_source_found = isset($source_policy_lookup[$policy_no]) ? 1 : 0; + $endorsement_source_found = isset($source_endorsement_lookup[$policy_no][$endorsement_no]) ? 1 : 0; + + // Policy number mismatch + if ($policy_source_found == 0) { + $error_messages[$row_number]['policy_no_mismatch'] = + "Policy number ({$policy_no}) not in NHance."; + } + + // Endorsement number mismatch + if ($policy_source_found == 1 && $endorsement_source_found == 0) { + $error_messages[$row_number]['endorsement_no_mismatch'] = + "Endorsement number ({$endorsement_no}) not in NHance for policy ({$policy_no})."; + } + + // Duplicate check + if ($duplicate_found == 1) { + $error_messages[$row_number]['duplicate'] = + "Duplicate entry found. This policy and endorsement ({$policy_no} | {$endorsement_no}) combination has already been matched."; + } + } + + } + // print_rr($error_messages);die; + + if (!empty($error_messages)) { + $error_data['error_code'] = 2; + $error_data['error_data'] = $error_messages; + $status = 'failed'; + $ret_status = false; + } + + //update in DB + $this->insurerStatements->where('id', $file_id)->set(['line_items' => $line_items, 'file_status' => $status, 'reason' => json_encode($error_data)])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); + } catch (\Throwable $th) { + + $errorData = [ + 'message' => $th->getMessage(), + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'code' => $th->getCode(), + 'trace' => $th->getTraceAsString(), + 'trace_array' => $th->getTrace(), // full array version (optional) + 'function' => $th->getTrace()[0]['function'] ?? null, + 'class' => $th->getTrace()[0]['class'] ?? null, + ]; + + $this->myLogger->logme("error", "POLICY-TRANSACTION-CONTROLLER - validateInsurerStatement: Exception: " . json_encode($errorData ?? [])); + $this->insurerStatements->where('id', $file_id)->set(['line_items' => 0, 'file_status' => 'failed', 'reason' => json_encode($errorData)])->update(); + return array('status' => false, 'error_code' => [], 'error_data' => $errorData); + } + } + + private function sanitizeStatementLookupValue($value): string + { + $value = trim((string)($value ?? '')); + $value = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $value); + return preg_replace('/[\x{200B}\x{200C}\x{200D}\x{FEFF}\x{00A0}\x{200E}\x{200F}\x{202A}-\x{202E}]/u', '', $value); + } + + public function updateInsurerStatement($params) + { + /* + * Changes made: 12-06-2024 + * - Added safe file_id handling and DB file record validation before using file details. + * - Fixed physical file missing response and error message. + * - Skips empty Excel rows and collects unique policy numbers from the uploaded statement. + * - Fetches NHance source records only for those uploaded policy numbers. + * - Uses the same sanitized policy number + endorsement number matching as validation. + * - Inserts statement details only for matched rows and skips empty or unmatched Excel rows. + * - Added safe default handling for amount and reward columns before calculation/insert. + */ + helper('excel_util_helper'); + $file_id = $params['file_id'] ?? 0; + + $error_data = ['error_code' => '', 'error_data' => []]; + $status = 'success'; + $ret_status = true; + + //get file info + $file = $this->insurerStatements->find((int)$file_id); + // dd($file); + + if (empty($file)) { + //file not found in DB + return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB'); + } + $file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name']; + + //check physical file + if (!file_exists($file_name_with_path)) { + //file not found update status and reason + $message = "Physical file not found"; + // echo $message; + $this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id)); + $this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update(); + return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found + } + + //get excel data to php array + $spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path); + $sheet = $spreadsheet->getActiveSheet(); + + $highestRowAndColumn = $sheet->getHighestRowAndColumn(); + // dd($highestRowAndColumn); + $excel_data = $sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']); + unset($excel_data[0]); + $excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data); + // dd($excel_data); + + $policyNos = []; + + foreach ($excel_data as $row) { + if (check_row_is_empty_or_null($row)) { + continue; + } + + $policyNo = $this->sanitizeStatementLookupValue($row[1] ?? ''); + + if ($policyNo !== '') { + $policyNos[] = $policyNo; + } + } + $policyNos = array_values(array_unique($policyNos)); + + // get uploaded month transactions data + // $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']); + $source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : []; + // Kint::dump($source_data);//die; + // Kint::dump($excel_data); + // die; + $source_lookup = []; + + foreach ($source_data as $source_row) { + $source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? ''); + $source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? ''); + $source_lookup[$source_policy_no . '|' . $source_endorsement_no][] = $source_row; + } + + // check policy no,insurer and etc in DB for this month + // if all good return true, otherwise return false with messssage + $data_to_update = []; + try { + foreach ($excel_data as $excel_row) { + if (check_row_is_empty_or_null($excel_row)) { + continue; + } + + // $policy_start_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[4]); //policy_start_date from excel + // $policy_end_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[5]); //policy_end_date from excel + $policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel + // $client_name = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[3]); //clientname from excel + $endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel + $entry_key = $policy_no . '|' . $endorsement_no; + + if (empty($source_lookup[$entry_key])) { + continue; + } + + $source_row = array_shift($source_lookup[$entry_key]); + + //calculate percentage first + $total_amt = 0; + + // $actual_bp_per = trim($excel_row[9]); //commented becoz this filed removed tfrom excel file + $actual_bp_per = 0; //set default value 0 for maintaining existing code flow + $actual_bp_brokerage = (int) trim((string)($excel_row[5] ?? 0)); + $actual_bp_amt = (int) trim((string)($excel_row[3] ?? 0)); + + if (($actual_bp_brokerage && $actual_bp_brokerage != 0 && $actual_bp_brokerage != "")) { + $total_amt += $actual_bp_brokerage; + //percentage reverse calculation + if (($actual_bp_per == 0 || $actual_bp_per == 0) && !empty($actual_bp_amt)) { + $actual_bp_per = (int) round(($actual_bp_brokerage / $actual_bp_amt) * 100, 2); + } + } else { + $actual_bp_brokerage = $actual_bp_amt * ($actual_bp_per / 100); + $total_amt += $actual_bp_brokerage; + } + + // $actual_tp_per = trim($excel_row[10]);//commented becoz this filed removed tfrom excel file + $actual_tp_per = 0;//set default value 0 for maintaining existing code flow + $actual_tp_brokerage = (int) trim((string)($excel_row[6] ?? 0)); + $actual_tp_amt = (int) trim((string)($excel_row[4] ?? 0)); + + if ($actual_tp_brokerage && $actual_tp_brokerage != 0 && $actual_tp_brokerage != "") { + $total_amt += $actual_tp_brokerage; + //percentage reverse calculation + if (($actual_tp_per == 0 || $actual_tp_per == "") && !empty($actual_tp_amt)) { + $actual_tp_per = (int) round(($actual_tp_brokerage / $actual_tp_amt) * 100, 2); + } + } else { + $actual_tp_brokerage = $actual_tp_amt * ($actual_tp_per / 100); + $total_amt += $actual_tp_brokerage; + } + + // $actual_tep_per = trim($excel_row[11]); + // $actual_tep_brokerage = trim($excel_row[14]); + // $actual_tep_amt = trim($excel_row[8]); + + $actual_tep_per = 0; + $actual_tep_brokerage = 0; + $actual_tep_amt = 0; + + if ($actual_tep_brokerage && $actual_tep_brokerage != 0 && $actual_tep_brokerage != "") { + $total_amt += $actual_tep_brokerage; + //percentage reverse calculation + if (($actual_tep_per == 0 || $actual_tep_per == "") && !empty($actual_tep_amt)) { + $actual_tep_per = ($actual_tep_brokerage / $actual_tep_amt) * 100; + } + } else { + $actual_tep_brokerage = $actual_tep_amt * ($actual_tep_per / 100); + $total_amt += $actual_tep_brokerage; + } + + //find variance + $variance_amt = $source_row['exp_amt'] - $total_amt; + + $data_to_update[] = ['co_share_id' => $source_row['id'], 'actual_bp_amt' => $actual_bp_amt, 'actual_tp_amt' => $actual_tp_amt, 'actual_tep_amt' => $actual_tep_amt, 'actual_bp_per' => $actual_bp_per, 'actual_tp_per' => $actual_tp_per, 'actual_tep_per' => $actual_tep_per, 'variance' => $variance_amt, 'actual_tep_brokerage_amt' => $actual_tep_brokerage, 'actual_tp_brokerage_amt' => $actual_tp_brokerage, 'actual_bp_brokerage_amt' => $actual_bp_brokerage, 'reward' => trim((string)($excel_row[7] ?? '')), 'statement_id' => $file_id]; + } + } catch (\Throwable $th) { + + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - updateInsurerStatement: Exception: " . $th->getMessage() . " --- Line: " . $th->getLine() . " --- Trace: " . $th->getTraceAsString()); + $errorData = [ + 'message' => $th->getMessage(), + 'file' => $th->getFile(), + 'line' => $th->getLine(), + 'code' => $th->getCode(), + 'trace' => $th->getTraceAsString(), + 'trace_array' => $th->getTrace(), // full array version (optional) + 'function' => $th->getTrace()[0]['function'] ?? null, + 'class' => $th->getTrace()[0]['class'] ?? null, + ]; + return ['status' => 'failed', 'code' => 500, 'message' => $th->getMessage(), 'error_data' => $errorData]; + } + // dd($data_to_update); + $this->coShareStmtDetailsModel->insertBatch($data_to_update, 'id'); + // dd($data_to_update); + // if($error_data['error_code']) + // { + // $status = 'failed'; + // $ret_status = false; + // } + //update in DB + $this->insurerStatements->where('id', $file_id)->set(['file_status' => $status, 'reason' => json_encode($error_data), 'invoice_status' => 'pending'])->update(); + return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']); + } + public function getInvoicePaymentDetails() { $statement_id = $this->request->getUri()->getSegment(4); @@ -4697,6 +5065,27 @@ class PolicyTransactionController extends BaseController } } + public function downloadInsurerStatement($id) + { + $statement = $this->insurerStatements + ->where('id', (int) $id) + ->where('is_active', 1) + ->first(); + + if (empty($statement) || empty($statement['file_name'])) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found'); + } + + $fileName = basename($statement['file_name']); + $filePath = WRITEPATH . 'uploads/statements/' . $fileName; + + if (!is_file($filePath)) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found'); + } + + return $this->response->download($filePath, null)->setFileName($fileName); + } + public function getFileErr() { $file_id = $this->request->getUri()->getSegment(4); @@ -4932,6 +5321,8 @@ class PolicyTransactionController extends BaseController try { + $bdsInstallmentData = []; + try { $bdsInstallmentData = $this->BdsPlacementModel->getClientInstallmentDetails(); @@ -4939,6 +5330,7 @@ class PolicyTransactionController extends BaseController $this->myLogger->logme('error', 'Exception: ' . $e->getMessage() . 'Page: ' . $e->getFile() . ' Line: ' . $e->getLine()); } + $this->myLogger->logme("error", "Data for BDS Installment " . json_encode($bdsInstallmentData)); // dd($bdsInstallmentData); diff --git a/app/Controllers/RestAuthenticationController.php b/app/Controllers/RestAuthenticationController.php index 4eae9ba0..15014b49 100755 --- a/app/Controllers/RestAuthenticationController.php +++ b/app/Controllers/RestAuthenticationController.php @@ -2446,4 +2446,25 @@ class RestAuthenticationController extends AdminController echo json_encode($result, JSON_UNESCAPED_SLASHES) . PHP_EOL; } + + public function getTokenforSamulssOAuthLogin() + { + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Function called"); + $this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Received payload = " . json_encode($this->request->getJSON() ?? [])); + + $id = $this->request->getGet('id') ?? null; + if(!$id){ + return $this->respond(['status' => 'failed','code' => 400,'message' => 'ID is required'], 200); + } + + $user = $this->employeeModel->where('id', $id)->first(); + if(!$user){ + return $this->respond(['status' => 'failed','code' => 400,'message' => 'User not found'], 200); + } + + $token = JWTToken::encode($user); + return $this->respond(['status' => 'success','code' => 200,'data' => $token], 200); + + } + } \ No newline at end of file diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index d42bea40..659299cd 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -40,6 +40,7 @@ class SalesController extends BaseController public function index() { $data = $this->getSalesStaffData(); + $data = array_merge($data, $this->getSalesFilterYears()); $data['tab_name'] = 'Leads'; $data['page_name'] = 'Leads'; @@ -48,6 +49,7 @@ class SalesController extends BaseController public function loadactivities(){ $data = $this->getSalesStaffData(); + $data = array_merge($data, $this->getSalesFilterYears()); $data['tab_name'] = 'Activities'; $data['page_name'] = 'Activities'; @@ -350,7 +352,9 @@ class SalesController extends BaseController 'status' => $this->request->getGet('status'), 'assigned_to' => $this->request->getGet('assigned_to'), 'search' => $this->request->getGet('search'), + 'financial_year' => $this->request->getGet('financial_year'), ]; + $filters = $this->applyFinancialYearDateRange($filters); $result = $this->leadModel->getLeadsWithFilters($filters, $limit, $offset); @@ -367,6 +371,102 @@ class SalesController extends BaseController } } + /** + * Export leads filtered by created_at date range. + * GET /sales/leads/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD + */ + public function exportLeads() + { + try { + $range = $this->getExportDateRange(); + } catch (\InvalidArgumentException $e) { + return $this->failValidationErrors($e->getMessage()); + } + + try { + $db = \Config\Database::connect(); + $builder = $db->table('sales_actual_leads sal') + ->select(' + sal.company_name, + c.short_name, + sal.address, + sal.website, + sal.gst_number, + sal.email, + sal.phone, + sal.status, + sal.created_at, + up.first_name as assigned_to_name, + COUNT(DISTINCT sa.activity_id) as total_activity + ') + ->join('clients c', 'c.id = sal.client_id', 'left') + ->join('user_profiles up', 'up.id = sal.assigned_to', 'left') + ->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left') + ->where("sal.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false); + + $status = $this->request->getGet('status'); + if (!empty($status)) { + $builder->where('sal.status', $status); + } + + $assignedToIds = $this->getExportAssignedToIds(); + if (!empty($assignedToIds)) { + $builder->whereIn('sal.assigned_to', $assignedToIds); + } + + $search = $this->request->getGet('search'); + if (!empty($search)) { + $builder->groupStart() + ->like('sal.company_name', $search) + ->orLike('c.short_name', $search) + ->orLike('sal.address', $search) + ->orLike('sal.website', $search) + ->orLike('sal.gst_number', $search) + ->orLike('sal.email', $search) + ->orLike('sal.phone', $search) + ->orLike('up.first_name', $search) + ->groupEnd(); + } + + $leads = $builder + ->groupBy('sal.lead_id, sal.company_name, c.short_name, sal.address, sal.website, sal.gst_number, sal.email, sal.phone, sal.status, sal.created_at, up.first_name') + ->orderBy('sal.created_at', 'DESC') + ->get() + ->getResultArray(); + + if (empty($leads)) { + return $this->exportNotFoundResponse("Leads Not Found {$range['display']}"); + } + + $rows = []; + $serialNo = 1; + foreach ($leads as $lead) { + $rows[] = [ + $serialNo++, + $lead['company_name'] ?? '', + $lead['short_name'] ?? '', + $lead['address'] ?? '', + $lead['website'] ?? '', + $lead['gst_number'] ?? '', + $lead['email'] ?? '', + $lead['phone'] ?? '', + $lead['status'] ?? '', + $lead['assigned_to_name'] ?? '', + $lead['total_activity'] ?? 0, + $this->formatExportDate($lead['created_at'] ?? ''), + ]; + } + + return $this->streamCsvDownload( + "sales-leads-{$range['from_label']}-to-{$range['to_label']}.csv", + ['S.No', 'Company', 'Company Short Name', 'Address', 'Website', 'GST', 'Email', 'Phone', 'Status', 'Assigned To', 'Total Activity', 'Created At'], + $rows + ); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + /** * Get single lead with complete details * GET /api/sales/leads/{id} @@ -732,8 +832,10 @@ class SalesController extends BaseController 'assigned_to' => $this->request->getGet('assigned_to'), 'date_from' => $this->request->getGet('date_from'), 'date_to' => $this->request->getGet('date_to'), + 'financial_year' => $this->request->getGet('financial_year'), 'search' => $this->request->getGet('search'), // ← ADD THIS ]; + $filters = $this->applyFinancialYearDateRange($filters); $result = $this->activityModel->getActivitiesWithFilters($filters, $limit, $offset); @@ -751,6 +853,104 @@ class SalesController extends BaseController } } + /** + * Export activities filtered by created_at date range. + * GET /sales/activities/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD + */ + public function exportActivities() + { + try { + $range = $this->getExportDateRange(); + } catch (\InvalidArgumentException $e) { + return $this->failValidationErrors($e->getMessage()); + } + + try { + $db = \Config\Database::connect(); + $builder = $db->table('sales_activities sa') + ->select(" + sa.activity_type, + sa.status, + sa.scheduled_date, + sa.created_at, + sa.notes, + sa.completion_notes, + sal.company_name, + c.short_name, + up1.first_name as assigned_to_name, + GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names + ") + ->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left') + ->join('clients c', 'c.id = sal.client_id', 'left') + ->join('user_profiles up1', 'up1.id = sa.assigned_to', 'left') + ->join('user_profiles up2', " + sa.additional_assigned_ids IS NOT NULL + AND sa.additional_assigned_ids != '' + AND sa.additional_assigned_ids != '[]' + AND JSON_VALID(sa.additional_assigned_ids) + AND JSON_CONTAINS(sa.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR))) + ", 'left') + ->where("sa.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false); + + $status = $this->request->getGet('status'); + if (!empty($status)) { + $builder->where('sa.status', $status); + } + + $assignedToIds = $this->getExportAssignedToIds(); + if (!empty($assignedToIds)) { + $builder->whereIn('sa.assigned_to', $assignedToIds); + } + + $search = $this->request->getGet('search'); + if (!empty($search)) { + $builder->groupStart() + ->like('sal.company_name', $search) + ->orLike('c.short_name', $search) + ->orLike('sa.status', $search) + ->orLike('sa.activity_type', $search) + ->orLike('up1.first_name', $search) + ->orLike('up2.first_name', $search) + ->groupEnd(); + } + + $activities = $builder + ->groupBy('sa.activity_id') + ->orderBy('sa.created_at', 'DESC') + ->get() + ->getResultArray(); + + if (empty($activities)) { + return $this->exportNotFoundResponse("Activities not Found {$range['display']}"); + } + + $rows = []; + $serialNo = 1; + foreach ($activities as $activity) { + $rows[] = [ + $serialNo++, + $activity['company_name'] ?? '', + $activity['short_name'] ?? '', + $activity['activity_type'] ?? '', + ucfirst((string) ($activity['status'] ?? '')), + $activity['assigned_to_name'] ?? '', + $activity['additional_assigned_names'] ?? '', + $this->formatExportDate($activity['scheduled_date'] ?? ''), + $this->formatExportDate($activity['created_at'] ?? ''), + $activity['notes'] ?: ($activity['completion_notes'] ?? ''), + ]; + } + + return $this->streamCsvDownload( + "sales-activities-{$range['from_label']}-to-{$range['to_label']}.csv", + ['S.No', 'Company', 'Company Short Name', 'Activity Type', 'Status', 'Assigned To', 'Additional Members', 'Scheduled Date', 'Created At', 'Notes'], + $rows + ); + } catch (\Exception $e) { + return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR); + } + } + /** * Get activities for a specific lead * GET /api/sales/leads/{leadId}/activities @@ -1205,6 +1405,144 @@ class SalesController extends BaseController return 1; } + private function getSalesFilterYears(): array + { + $currentFinYear = getCurrentFinancialYear(); + $db = \Config\Database::connect(); + + $finYearsRaw = $db->table('sales_target') + ->select('fy_year', false) + ->distinct() + ->orderBy('fy_year', 'DESC') + ->get() + ->getResultArray(); + + $finYears = array_column($finYearsRaw, 'fy_year'); + if (empty($finYears)) { + $finYears[] = $currentFinYear; + } + + if (!in_array($currentFinYear, $finYears, true)) { + array_unshift($finYears, $currentFinYear); + } + + return [ + 'fin_years' => $finYears, + 'current_fin_year' => $currentFinYear, + ]; + } + + private function applyFinancialYearDateRange(array $filters): array + { + if (empty($filters['financial_year'])) { + return $filters; + } + + $fyRange = $this->getFYDateRange((string) $filters['financial_year']); + $filters['date_from'] = $fyRange['start']; + $filters['date_to'] = $fyRange['end']; + + return $filters; + } + + private function getExportDateRange(): array + { + $fromDate = trim((string) $this->request->getGet('from_date')); + $toDate = trim((string) $this->request->getGet('to_date')); + + if ($fromDate === '' || $toDate === '') { + throw new \InvalidArgumentException('Please select from date and to date.'); + } + + $from = $this->normalizeExportDate($fromDate, '00:00:00'); + $to = $this->normalizeExportDate($toDate, '23:59:59'); + + if ($from === null || $to === null) { + throw new \InvalidArgumentException('Invalid date range. Use YYYY-MM-DD format.'); + } + + if (strtotime($from) > strtotime($to)) { + throw new \InvalidArgumentException('From date cannot be after to date.'); + } + + return [ + 'from' => $from, + 'to' => $to, + 'from_label' => date('Y-m-d', strtotime($from)), + 'to_label' => date('Y-m-d', strtotime($to)), + 'display' => date('d-m-Y', strtotime($from)) . ' to ' . date('d-m-Y', strtotime($to)), + ]; + } + + private function normalizeExportDate(string $date, string $time): ?string + { + $dateTime = \DateTime::createFromFormat('Y-m-d H:i:s', $date . ' ' . $time); + + if (!$dateTime || $dateTime->format('Y-m-d') !== $date) { + return null; + } + + return $dateTime->format('Y-m-d H:i:s'); + } + + private function getExportAssignedToIds(): array + { + $assignedTo = (string) $this->request->getGet('assigned_to'); + + if ($assignedTo === '') { + return []; + } + + return array_values(array_filter(array_map('trim', explode(',', $assignedTo)), static function ($id) { + return ctype_digit($id); + })); + } + + private function streamCsvDownload(string $filename, array $headers, array $rows) + { + $handle = fopen('php://temp', 'r+'); + fwrite($handle, "\xEF\xBB\xBF"); + fputcsv($handle, $headers); + + foreach ($rows as $row) { + fputcsv($handle, $row); + } + + rewind($handle); + $csv = stream_get_contents($handle); + fclose($handle); + + return $this->response + ->setHeader('Content-Type', 'text/csv; charset=UTF-8') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate') + ->setBody($csv); + } + + private function exportNotFoundResponse(string $message) + { + return $this->response + ->setStatusCode(ResponseInterface::HTTP_NOT_FOUND) + ->setJSON([ + 'status' => 'error', + 'message' => $message, + ]); + } + + private function formatExportDate(?string $value): string + { + if (empty($value)) { + return ''; + } + + $timestamp = strtotime($value); + if ($timestamp === false) { + return $value; + } + + return date('d/m/Y h:i A', $timestamp); + } + // ==================== Dashboard ==================== @@ -1392,6 +1730,7 @@ public function branchLevelDashboard($branchId, $branchwise_all_sales_team_ids, // Activities & opportunities also scoped to FY via CASE WHEN $leadsOverview = $db->table('sales_actual_leads sal') ->select("sal.lead_id, sal.company_name, sal.status, + sal.assigned_to AS assigned_to_id, up.first_name AS assigned_to, COUNT(DISTINCT CASE WHEN sa.scheduled_date >= '{$fyStart}' AND sa.scheduled_date <= '{$fyEnd}' THEN sa.activity_id END) AS activities, @@ -1403,7 +1742,7 @@ public function branchLevelDashboard($branchId, $branchwise_all_sales_team_ids, ->whereIn('sal.assigned_to', $branchwise_all_sales_team_ids) ->where('sal.created_at >=', $fyStart) ->where('sal.created_at <=', $fyEnd) - ->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name') + ->groupBy('sal.lead_id, sal.company_name, sal.status, sal.assigned_to, up.first_name') ->orderBy('sal.created_at', 'DESC') ->get() ->getResultArray(); @@ -1497,6 +1836,7 @@ private function buildTeamAchievement($db, array $sales_manager_ids, $branchId, ->get() ->getRowArray(); $targetAmt = (float)($targetRow['target_amount'] ?? 0); + $targetId = $targetRow['id'] ?? null; // Achieved (won leads in FY) $achievedAmt = (float)$this->getUserAchievedAmount($fy, $uid); @@ -1589,6 +1929,7 @@ private function buildTeamAchievement($db, array $sales_manager_ids, $branchId, 'role' => $m['role'], 'total_acts' => $totalActs, 'done_acts' => $doneActs, + 'target_id' => $targetId, 'target_amt' => $targetAmt, 'achieved_amt' => $achievedAmt, 'grad' => $palette['grad'], @@ -1658,12 +1999,23 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s // ── Won Leads for this user in FY (Table 2 in modal) ── // leads.actual_lead_id maps to sales_actual_leads.id - // leads.type: 1 = EB, else = Non-EB + // leads.lead_form_type: 1 = EB, else = Non-EB + // leads.lead_type: 1 = Fresh, 2 = Renewal, 3 = Roll Over $wonLeads = $db->query(" SELECT - sal.lead_id, + l.id AS opportunities_id, + l.actual_lead_id, + COALESCE(l.lead_form_type, 1) AS lead_form_type_id, + l.lead_type AS lead_type_id, sal.company_name AS company, - CASE WHEN l.lead_type = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_type, + l.client_name AS client_name, + CASE WHEN COALESCE(l.lead_form_type, 1) = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_form_type, + CASE + WHEN l.lead_type = 1 THEN 'Fresh' + WHEN l.lead_type = 2 THEN 'Renewal' + WHEN l.lead_type = 3 THEN 'Roll Over' + ELSE '' + END AS lead_type, l.created_at AS created_at, l.status FROM leads l @@ -1676,6 +2028,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s ORDER BY l.created_at DESC ", [$uid, $fyStart, $fyEnd])->getResultArray(); + // -- NOTE: AND TRIM(sal.company_name) = TRIM(l.client_name) $result[$uid] = [ 'total_policies' => $totalPolicies, 'total_exp_amt' => $totalExpAmt, diff --git a/app/Controllers/TicketController.php b/app/Controllers/TicketController.php index 6f64777c..bcfb183a 100644 --- a/app/Controllers/TicketController.php +++ b/app/Controllers/TicketController.php @@ -1162,28 +1162,28 @@ class TicketController extends BaseController // --- EMPLOYEE DETAILS --- 'emp_code' => [ 'label' => 'Employee ID', - 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]', + 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee ID is required.', - 'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).' + 'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).' ] ], 'emp_name' => [ 'label' => 'Employee Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee Name is required', 'min_length' => 'Employee Name must be at least 3 characters long', - 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'insured_name' => [ 'label' => 'Insured Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Insured Name is required', 'min_length' => 'Insured Name must be at least 3 characters long', - 'regex_match' => 'Insured Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Insured Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'relationship' => ['label' => 'Relationship','rules' => 'required', @@ -1393,19 +1393,19 @@ class TicketController extends BaseController // --- EMPLOYEE DETAILS --- 'emp_code' => [ 'label' => 'Employee ID', - 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]', + 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee ID is required.', - 'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).', + 'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).', ] ], 'emp_name' => [ 'label' => 'Employee Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee Name is required', 'min_length' => 'Employee Name must be at least 3 characters long', - 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]', @@ -1650,28 +1650,28 @@ class TicketController extends BaseController // --- EMPLOYEE DETAILS --- 'emp_code' => [ 'label' => 'Employee ID', - 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]', + 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee ID is required.', - 'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).', + 'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).', ] ], 'emp_name' => [ 'label' => 'Employee Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee Name is required', 'min_length' => 'Employee Name must be at least 3 characters long', - 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'insured_name' => [ 'label' => 'Insured Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Insured Name is required', 'min_length' => 'Insured Name must be at least 3 characters long', - 'regex_match' => 'Insured Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Insured Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'relationship' => ['label' => 'Relationship','rules' => 'required', @@ -1882,19 +1882,19 @@ class TicketController extends BaseController // --- EMPLOYEE DETAILS --- 'emp_code' => [ 'label' => 'Employee ID', - 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]', + 'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee ID is required.', - 'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).', + 'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).', ] ], 'emp_name' => [ 'label' => 'Employee Name', - 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]', + 'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]', 'errors' => [ 'required' => 'Employee Name is required', 'min_length' => 'Employee Name must be at least 3 characters long', - 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.' + 'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).' ] ], 'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]', diff --git a/app/Helpers/excel_util_helper.php b/app/Helpers/excel_util_helper.php index 0b7d0039..5aa68990 100755 --- a/app/Helpers/excel_util_helper.php +++ b/app/Helpers/excel_util_helper.php @@ -2107,8 +2107,8 @@ if (!function_exists('remap_default_age_ratio_into_relationship')) { } } -if (!function_exists('check_dup_mobileno')) { - function check_dup_mobileno(array $row, array $existing_mobilenos) +if (!function_exists('check_dup_mobileno_old')) { + function check_dup_mobileno_old(array $row, array $existing_mobilenos) { if(!empty($row['12'])){ foreach ($existing_mobilenos as $k => $value) { @@ -2122,6 +2122,41 @@ if (!function_exists('check_dup_mobileno')) { } } +if (!function_exists('check_dup_mobileno')) { + function check_dup_mobileno(array $row, array $existing_mobilenos, $excel_data = [], $row_key = null) + { + if(!empty($row['12'])){ + $current_relation = strtolower(trim($row['5'] ?? '')); + $current_mobile = trim((string) $row['12']); + + foreach ($existing_mobilenos as $k => $value) { + if ($current_relation == 'self' && $current_mobile == trim((string) ($value['mobile'] ?? ''))) { + return array('status' => false, 'error' => "Duplicate Mobile No"); + // break; + } + } + + foreach ($excel_data as $ed_row => $value) { + if ($row_key !== null && (string) $ed_row === (string) $row_key) { + continue; + } + + $excel_relation = strtolower(trim($value['5'] ?? '')); + $excel_mobile = trim((string) ($value['12'] ?? '')); + + if ($current_relation == 'self' && $excel_relation == 'self' && $excel_mobile !== '' && $current_mobile == $excel_mobile) { + $duplicate_row_no = is_numeric($ed_row) ? $ed_row + 1 : $ed_row; + $current_row_no = is_numeric($row_key) ? $row_key + 1 : $row_key; + return array('status' => false, 'error' => "This mobile number has already been used for another self in Excel at row no. " . ($duplicate_row_no) . " and " . ($current_row_no)); + // break; + } + } + + } + return array('status' => true); + } +} + // if (!function_exists('check_dup_email')) { // function check_dup_email(array $row, array $existing_mobilenos) // { @@ -2138,8 +2173,8 @@ if (!function_exists('check_dup_mobileno')) { // } // } -if (!function_exists('check_dup_email')) { - function check_dup_email(array $row, array $existing_mobilenos) +if (!function_exists('check_dup_email_old')) { + function check_dup_email_old(array $row, array $existing_mobilenos) { $errorMessages = []; @@ -2168,6 +2203,54 @@ if (!function_exists('check_dup_email')) { } } +if (!function_exists('check_dup_email')) { + function check_dup_email(array $row, array $existing_mobilenos, $excel_data = [], $row_key = null) + { + $errorMessages = []; + $current_relation = strtolower(trim($row['5'] ?? '')); + $current_email = trim((string) ($row['13'] ?? '')); + + // Validate email + $emailCheck = is_valid_or_empty_email($current_email); + if (!$emailCheck) { + $errorMessages[] = 'Invalid email format'; + } + + // Check duplicate only if email is not empty + if (!empty($current_email)) { + foreach ($existing_mobilenos as $value) { + if ($current_relation === 'self' && strcasecmp($current_email, trim((string) ($value['email_corporate'] ?? ''))) === 0) { + $errorMessages[] = 'Duplicate Email'; + break; + } + } + + foreach ($excel_data as $ed_row => $value) { + if ($row_key !== null && (string) $ed_row === (string) $row_key) { + continue; + } + + $excel_relation = strtolower(trim($value['5'] ?? '')); + $excel_email = trim((string) ($value['13'] ?? '')); + + if ($current_relation === 'self' && $excel_relation === 'self' && $excel_email !== '' && strcasecmp($current_email, $excel_email) === 0) { + $duplicate_row_no = is_numeric($ed_row) ? $ed_row + 1 : $ed_row; + $current_row_no = is_numeric($row_key) ? $row_key + 1 : $row_key; + $errorMessages[] = "This email has already been used for another self in Excel at row no. " . ($duplicate_row_no) . " and " . ($current_row_no); + break; + } + } + } + + // Prepare final return + if (!empty($errorMessages)) { + return ['status' => false, 'error' => implode(' & ', $errorMessages)]; + } + + return ['status' => true, 'error' => '']; + } +} + if (!function_exists('generate_family_relationship_array')) { function generate_family_relationship_array($family_structure_from_policy_terms) diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index 9316017f..a192e33e 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -13,22 +13,23 @@ if (! defined('MERGED_CLAIM_FILE_TYPE')) { if (! function_exists('merge_ticket_pdfs')) { /** - * Merge all active PDF rows in claim_files for a given ticket_master id + * Merge all active PDF/image rows in claim_files for a given ticket_master id * into one combined PDF and register that PDF as a new claim_files row * with file_type = MERGED_CLAIM_FILE_TYPE. * * Source rows are picked from claim_files where: * - ticket_id = $ticket_master_id * - is_active = 1 - * - mime_type = 'application/pdf' - * - file_type IN $opts['include_file_types'] (default [2, 3]) + * - mime_type IN $opts['include_mime_types'] (default PDF/JPG/PNG) + * - file_type IN $opts['include_file_types'] (default [1, 2, 3]) * * @param int $ticket_master_id * @param array $opts { * @var bool $replace Default true. Soft-delete previous merged row before re-creating. * @var int $created_by Override created_by user id on the inserted row. * @var int $ticket_type Default 1. Stored on the inserted claim_files row. - * @var array $include_file_types Default [2, 3]. + * @var array $include_file_types Default [1, 2, 3]. + * @var array $include_mime_types Default ['application/pdf', 'image/jpeg', 'image/png']. * } * @return array {status, merged_file_id, file_name, pages, source_count, message} */ @@ -38,7 +39,8 @@ if (! function_exists('merge_ticket_pdfs')) { 'replace' => true, 'created_by' => null, 'ticket_type' => 1, - 'include_file_types' => [2, 3], + 'include_file_types' => [1, 2], + 'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'], ]; $result = [ @@ -60,14 +62,13 @@ if (! function_exists('merge_ticket_pdfs')) { $rows = $claimFiles ->where('ticket_id', $ticket_master_id) ->where('is_active', 1) - ->where('mime_type', 'application/pdf') ->whereIn('file_type', $opts['include_file_types']) ->orderBy('id', 'ASC') ->findAll(); if (empty($rows)) { $result['status'] = true; - $result['message'] = 'No PDF files to merge'; + $result['message'] = 'No PDF/image files to merge'; return $result; } @@ -75,7 +76,7 @@ if (! function_exists('merge_ticket_pdfs')) { . 'uploads' . DIRECTORY_SEPARATOR . 'claim_files' . DIRECTORY_SEPARATOR; - $sourcePaths = []; + $sourceFiles = []; foreach ($rows as $row) { $name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? ''); if (empty($name)) { @@ -84,23 +85,47 @@ if (! function_exists('merge_ticket_pdfs')) { // url column may sometimes hold a full URL; we only care about the file basename on disk. $full = $uploadDir . basename($name); if (is_file($full) && is_readable($full)) { - $sourcePaths[] = $full; + $mime = merge_ticket_pdf_resolve_mime($full, (string) ($row['mime_type'] ?? '')); + if (! in_array($mime, $opts['include_mime_types'], true)) { + log_message('error', "merge_ticket_pdfs | unsupported source mime {$mime} | claim_file_id={$row['id']} | path={$full}"); + continue; + } + + $sourceFiles[] = [ + 'path' => $full, + 'mime' => $mime, + 'id' => $row['id'] ?? null, + ]; } else { - log_message('error', "merge_ticket_pdfs | missing PDF on disk | claim_file_id={$row['id']} | path={$full}"); + log_message('error', "merge_ticket_pdfs | missing file on disk | claim_file_id={$row['id']} | path={$full}"); } } - if (empty($sourcePaths)) { - $result['message'] = 'No readable PDF files on disk'; + if (empty($sourceFiles)) { + $result['message'] = 'No readable PDF/image files on disk'; return $result; } - $result['source_count'] = count($sourcePaths); + $result['source_count'] = count($sourceFiles); + log_message( + 'error', + 'merge_ticket_pdfs | source files resolved | ticket_id=' . $ticket_master_id . ' | sources=' . json_encode(array_map(static function ($sourceFile) { + return [ + 'id' => $sourceFile['id'], + 'mime' => $sourceFile['mime'], + 'file' => basename($sourceFile['path']), + ]; + }, $sourceFiles)) + ); $tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($tempDir)) { @mkdir($tempDir, 0775, true); } + $mpdfCacheDir = $tempDir . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfCacheDir)) { + @mkdir($mpdfCacheDir, 0775, true); + } $mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf'; $mergedPath = $uploadDir . $mergedName; @@ -112,18 +137,20 @@ if (! function_exists('merge_ticket_pdfs')) { ]); $totalPages = 0; - foreach ($sourcePaths as $src) { + foreach ($sourceFiles as $sourceFile) { + $src = $sourceFile['path']; try { - $pageCount = $mpdf->setSourceFile($src); - for ($p = 1; $p <= $pageCount; $p++) { - $tplId = $mpdf->importPage($p); - $size = $mpdf->getTemplateSize($tplId); - $mpdf->AddPageByArray([ - 'orientation' => ($size['width'] > $size['height']) ? 'L' : 'P', - 'sheet-size' => [$size['width'], $size['height']], - ]); - $mpdf->useTemplate($tplId); - $totalPages++; + if ($sourceFile['mime'] === 'application/pdf') { + $pageCount = merge_ticket_pdf_add_pdf_pages($mpdf, $src, $tempDir); + $totalPages += $pageCount; + log_message('error', "merge_ticket_pdfs | added PDF | file={$src} | pages={$pageCount}"); + } elseif (in_array($sourceFile['mime'], ['image/jpeg', 'image/png'], true)) { + if (merge_ticket_pdf_add_image_page($mpdf, $src)) { + $totalPages++; + log_message('error', "merge_ticket_pdfs | added image | file={$src} | mime={$sourceFile['mime']}"); + } + } else { + log_message('error', "merge_ticket_pdfs | unsupported source mime {$sourceFile['mime']} | {$src}"); } } catch (\Throwable $e) { log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage()); @@ -131,7 +158,7 @@ if (! function_exists('merge_ticket_pdfs')) { } if ($totalPages === 0) { - $result['message'] = 'All source PDFs failed to import'; + $result['message'] = 'All source PDF/image files failed to import'; return $result; } @@ -192,3 +219,173 @@ if (! function_exists('merge_ticket_pdfs')) { return $result; } } + +if (! function_exists('merge_ticket_pdf_add_pdf_pages')) { + /** + * Import PDF pages. If FPDI cannot import the source (commonly encrypted + * PDFs), normalize through Ghostscript and retry. + */ + function merge_ticket_pdf_add_pdf_pages(Mpdf $mpdf, string $pdfPath, string $tempDir): int + { + try { + return merge_ticket_pdf_import_pdf_pages($mpdf, $pdfPath); + } catch (\Throwable $e) { + log_message('error', "merge_ticket_pdfs | direct PDF import failed | file={$pdfPath} | " . $e->getMessage()); + + $normalizedPath = merge_ticket_pdf_normalize_with_ghostscript($pdfPath, $tempDir); + if ($normalizedPath === null) { + throw $e; + } + + try { + $pageCount = merge_ticket_pdf_import_pdf_pages($mpdf, $normalizedPath); + log_message('error', "merge_ticket_pdfs | Ghostscript normalized PDF imported | original={$pdfPath} | normalized={$normalizedPath} | pages={$pageCount}"); + @unlink($normalizedPath); + return $pageCount; + } catch (\Throwable $normalizedError) { + @unlink($normalizedPath); + throw $normalizedError; + } + } + } +} + +if (! function_exists('merge_ticket_pdf_import_pdf_pages')) { + function merge_ticket_pdf_import_pdf_pages(Mpdf $mpdf, string $pdfPath): int + { + $pageCount = $mpdf->setSourceFile($pdfPath); + for ($p = 1; $p <= $pageCount; $p++) { + $tplId = $mpdf->importPage($p); + $mpdf->AddPage(); + // adjustPageSize=true makes the output page match the imported page. + $mpdf->useTemplate($tplId, 0, 0, null, null, true); + } + + return $pageCount; + } +} + +if (! function_exists('merge_ticket_pdf_normalize_with_ghostscript')) { + function merge_ticket_pdf_normalize_with_ghostscript(string $pdfPath, string $tempDir): ?string + { + $ghostscript = is_executable('/usr/bin/gs') ? '/usr/bin/gs' : trim((string) @shell_exec('command -v gs 2>/dev/null')); + if ($ghostscript === '' || ! is_executable($ghostscript)) { + log_message('error', "merge_ticket_pdfs | Ghostscript not available for PDF normalization | file={$pdfPath}"); + return null; + } + + $normalizedPath = rtrim($tempDir, '/\\') . DIRECTORY_SEPARATOR . 'normalized_' . uniqid('', true) . '.pdf'; + $cmd = escapeshellarg($ghostscript) + . ' -q -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -dCompatibilityLevel=1.4' + . ' -sOutputFile=' . escapeshellarg($normalizedPath) + . ' ' . escapeshellarg($pdfPath) + . ' 2>&1'; + + $output = []; + $exitCode = 1; + @exec($cmd, $output, $exitCode); + + if ($exitCode !== 0 || ! is_file($normalizedPath) || filesize($normalizedPath) <= 0) { + log_message('error', 'merge_ticket_pdfs | Ghostscript normalization failed | file=' . $pdfPath . ' | exit=' . $exitCode . ' | output=' . implode(' ', $output)); + @unlink($normalizedPath); + return null; + } + + return $normalizedPath; + } +} + +if (! function_exists('merge_ticket_pdf_resolve_mime')) { + /** + * Resolve file type from stored MIME, filesystem MIME, and extension. + * Some uploads can be saved as image/jpg, empty MIME, or octet-stream in DB. + */ + function merge_ticket_pdf_resolve_mime(string $path, string $storedMime = ''): string + { + $storedMime = strtolower(trim($storedMime)); + if ($storedMime === 'image/jpg') { + return 'image/jpeg'; + } + if (in_array($storedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) { + return $storedMime; + } + + $detectedMime = ''; + if (function_exists('mime_content_type')) { + $detectedMime = strtolower((string) @mime_content_type($path)); + if ($detectedMime === 'image/jpg') { + return 'image/jpeg'; + } + if (in_array($detectedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) { + return $detectedMime; + } + } + + $ext = strtolower(pathinfo($path, PATHINFO_EXTENSION)); + if ($ext === 'pdf') { + return 'application/pdf'; + } + if (in_array($ext, ['jpg', 'jpeg'], true)) { + return 'image/jpeg'; + } + if ($ext === 'png') { + return 'image/png'; + } + + return $detectedMime ?: ($storedMime ?: 'application/octet-stream'); + } +} + +if (! function_exists('merge_ticket_pdf_add_image_page')) { + /** + * Add an uploaded image as a single PDF page, preserving portrait/landscape + * orientation and fitting the image proportionally inside the page. + */ + function merge_ticket_pdf_add_image_page(Mpdf $mpdf, string $imagePath): bool + { + $imageInfo = @getimagesize($imagePath); + $ext = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION)); + $imageType = $ext === 'png' || ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg'; + + $imageWidthPx = (int) ($imageInfo[0] ?? 0); + $imageHeightPx = (int) ($imageInfo[1] ?? 0); + $hasSize = $imageWidthPx > 0 && $imageHeightPx > 0; + $isLandscape = $hasSize && $imageWidthPx > $imageHeightPx; + + $pageWidth = $isLandscape ? 297 : 210; + $pageHeight = $isLandscape ? 210 : 297; + $margin = 0; + + $availableWidth = $pageWidth - ($margin * 2); + $availableHeight = $pageHeight - ($margin * 2); + if ($hasSize) { + $scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx); + $drawWidth = $imageWidthPx * $scale; + $drawHeight = $imageHeightPx * $scale; + $x = ($pageWidth - $drawWidth) / 2; + $y = ($pageHeight - $drawHeight) / 2; + } else { + // Some PNGs fail getimagesize(), but mPDF can still render them. + // Use a portrait A4 fallback and let mPDF calculate image height. + log_message('error', "merge_ticket_pdfs | image size unavailable, using fallback page | {$imagePath}"); + $drawWidth = $availableWidth; + $drawHeight = 0; + $x = $margin; + $y = $margin; + } + + $mpdf->AddPageByArray([ + 'orientation' => $isLandscape ? 'L' : 'P', + 'sheet-size' => 'A4', + 'margin-left' => 0, + 'margin-right' => 0, + 'margin-top' => 0, + 'margin-bottom' => 0, + 'margin-header' => 0, + 'margin-footer' => 0, + ]); + + $mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight, $imageType); + return true; + } +} diff --git a/app/Models/PTCOShareDetailsModel.php b/app/Models/PTCOShareDetailsModel.php index 59d53c9c..e619c69d 100644 --- a/app/Models/PTCOShareDetailsModel.php +++ b/app/Models/PTCOShareDetailsModel.php @@ -121,5 +121,41 @@ class PTCOShareDetailsModel extends Model ->get() ->getResultArray(); } + + public function getNonReconcileredPolicyTransactionByPolicyAndEndorsement(string $insurer_id, string $insurer_branch_id, array $policy_no) + { + $builder = $this->db->table('pt_co_share_details pt_co') + ->select(" + pt_co.id, + pt_co.pt_id, + pt_co.exp_amt, + pt.id AS policy_transaction_id, + pt.endorsement_no, + c.client_name, + pt.created_at, + pt_co.insurer_id, + pt_co.insurer_branch_id, + pt.policy_no, + pt.policy_issue_date, + pt.policy_start_date, + pt.policy_end_date, + pt.status, + pt_co.bp_amt, + pt_co.tp_amt, + pt_co.tep_amt, + pt_co.exp_amt, + pt_co.statement_id + ") + ->join('policy_transaction pt', 'pt_co.pt_id = pt.id') + ->join('clients c', 'pt.client_id = c.id') + ->where('pt_co.is_active', 1) + ->where('pt.is_active', 1) + ->where('pt_co.insurer_id', $insurer_id) + ->where('pt_co.insurer_branch_id', $insurer_branch_id) + ->whereIn('pt.policy_no', $policy_no); + + + return $builder->get()->getResultArray(); + } } diff --git a/app/Models/SalesActivityModel.php b/app/Models/SalesActivityModel.php index 9f8d4c7c..01c4ee3a 100644 --- a/app/Models/SalesActivityModel.php +++ b/app/Models/SalesActivityModel.php @@ -197,6 +197,14 @@ class SalesActivityModel extends Model $builder->whereIn('sales_activities.assigned_to', $assignedToIds); } + if (!empty($filters['date_from'])) { + $builder->where('sales_activities.created_at >=', $filters['date_from']); + } + + if (!empty($filters['date_to'])) { + $builder->where('sales_activities.created_at <=', $filters['date_to']); + } + if (!empty($filters['search'])) { // $builder->groupStart() $this->groupStart() @@ -243,6 +251,12 @@ class SalesActivityModel extends Model $ids = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']); $query->whereIn('assigned_to', $ids); } + if (!empty($filters['date_from'])) { + $query->where('created_at >=', $filters['date_from']); + } + if (!empty($filters['date_to'])) { + $query->where('created_at <=', $filters['date_to']); + } $counts[$status] = $query->countAllResults(); } $counts['all'] = $counts['pending'] + $counts['completed']; diff --git a/app/Models/SalesActualLeadModel.php b/app/Models/SalesActualLeadModel.php index d52fcd23..785e8cf4 100644 --- a/app/Models/SalesActualLeadModel.php +++ b/app/Models/SalesActualLeadModel.php @@ -112,6 +112,14 @@ class SalesActualLeadModel extends Model $this->whereIn('sales_actual_leads.assigned_to', $assignedToIds); } + if (!empty($filters['date_from'])) { + $this->where('sales_actual_leads.created_at >=', $filters['date_from']); + } + + if (!empty($filters['date_to'])) { + $this->where('sales_actual_leads.created_at <=', $filters['date_to']); + } + $this->orderBy('sales_actual_leads.created_at', 'DESC'); if (!empty($filters['search'])) { @@ -153,6 +161,14 @@ class SalesActualLeadModel extends Model $countQuery->whereIn('assigned_to', $assignedToIds); } + if (!empty($filters['date_from'])) { + $countQuery->where('created_at >=', $filters['date_from']); + } + + if (!empty($filters['date_to'])) { + $countQuery->where('created_at <=', $filters['date_to']); + } + $counts[$status] = $countQuery->where('status', $status)->countAllResults(); } diff --git a/app/Views/file_list.php b/app/Views/file_list.php index d7bc5bba..dc389326 100755 --- a/app/Views/file_list.php +++ b/app/Views/file_list.php @@ -298,6 +298,7 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len