diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 081b44cd..42aab688 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"); @@ -1010,6 +1011,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'); @@ -1056,6 +1060,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'); diff --git a/app/Controllers/FhplApiController.php b/app/Controllers/FhplApiController.php index dfa7db1b..2d6cc36c 100644 --- a/app/Controllers/FhplApiController.php +++ b/app/Controllers/FhplApiController.php @@ -1068,7 +1068,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..06876db1 100644 --- a/app/Controllers/LeadsController.php +++ b/app/Controllers/LeadsController.php @@ -571,6 +571,70 @@ class LeadsController extends BaseController } + // $leadFormType = (int) ($postData['lead_form_type'] ?? 1); + // $postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? [])); + // $isEbRenewalGpaClaimHistory = $leadFormType === 1 + // && (int) ($postData['lead_type'] ?? 0) === 2 + // && in_array(1, $postedPolicyTypeIds, true) + // && (int) ($postData['claim_history'] ?? 0) === 1; + + // if ($isEbRenewalGpaClaimHistory) { + // $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 +744,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 +1014,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 +1045,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 +2204,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 +2242,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 +2252,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 +2345,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 +2862,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 +4840,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..0f15f31d 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); diff --git a/app/Controllers/SalesController.php b/app/Controllers/SalesController.php index 195ace88..659299cd 100644 --- a/app/Controllers/SalesController.php +++ b/app/Controllers/SalesController.php @@ -371,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} @@ -757,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 @@ -1251,6 +1445,104 @@ class SalesController extends BaseController 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 ==================== @@ -1707,15 +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 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, l.client_name AS client_name, - CASE WHEN l.lead_type = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_type, + 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 diff --git a/app/Helpers/merge_pdf_helper.php b/app/Helpers/merge_pdf_helper.php index 4498b9f8..a192e33e 100644 --- a/app/Helpers/merge_pdf_helper.php +++ b/app/Helpers/merge_pdf_helper.php @@ -141,14 +141,8 @@ if (! function_exists('merge_ticket_pdfs')) { $src = $sourceFile['path']; try { if ($sourceFile['mime'] === 'application/pdf') { - $pageCount = $mpdf->setSourceFile($src); - 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); - $totalPages++; - } + $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)) { @@ -226,6 +220,81 @@ if (! function_exists('merge_ticket_pdfs')) { } } +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. 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/Views/insurer_statement_list.php b/app/Views/insurer_statement_list.php index e6d03e1a..5834ac98 100644 --- a/app/Views/insurer_statement_list.php +++ b/app/Views/insurer_statement_list.php @@ -302,7 +302,15 @@ $isl_col_width_px = nhance_dt_column_widths_px($isl_header_labels, $isl_col_max_ - + + + + + + + - + + + @@ -219,7 +220,7 @@

Client Information

-
+