diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 255275dc..406a099e 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -235,6 +235,7 @@ $routes->group("/client", ["filter" => "authMVC"], function ($routes) { $routes->group("/employee", ["filter" => "authMVC"], function ($routes) { $routes->get("list", "EmployeeController::list"); + $routes->get("pending-approvals", "EmployeeController::pendingApprovalsList"); $routes->get("search", "EmployeeController::search"); $routes->get("upload", "EmployeeController::employeesUplodWithEvents"); $routes->post("upload", "EmployeeController::employeesUplodWithEvents"); @@ -511,6 +512,7 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) { $routes->get('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); $routes->get('sync', 'ClaimReportDashboardController::sync'); + $routes->get('download-excel', 'ClaimReportDashboardController::downloadExcel'); }); $routes->group('enrollment-collection-v1', static function ($routes) { @@ -531,11 +533,12 @@ $routes->cli('cli/croneDailyActivityReport', 'DashboardController::croneDailyAct // Employee dependent APIs (no JWT / App-Signature; util uses authMVC only) $routes->get('relationshipList', 'EmployeeRestController::relationshipList'); -$routes->get('getEmployeeAndDependence', 'EmployeeRestController::getEmployeeAndDependence'); -$routes->post('editEmployeeAndDependence', 'EmployeeRestController::editEmployeeAndDependence'); $routes->post('addEmployeeAndDependencev2', 'EmployeeRestController::addEmployeeAndDependencev2'); +$routes->get('getEmployeeByPolicyv2', 'EmployeeRestController::getEmployeeByPolicyv2'); $routes->get('deleteDependence', 'EmployeeRestController::deleteDependence'); $routes->get('getEmployeeAndDependenceByClientId', 'EmployeeRestController::getEmployeeAndDependenceByClientId'); +$routes->get('getPendingApprovalDependents', 'EmployeeRestController::getPendingApprovalDependents'); +$routes->post('processDependentAdd', 'EmployeeRestController::processDependentAdd'); @@ -764,26 +767,29 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->post("ecardRequest", "ApiServiceController::ecardRequest"); $routes->get("getWellnessURL", "ApiServiceController::getWellnessURL"); - - $routes->post("logHrActivity", "RestAuthenticationController::logHrActivity"); // $routes->post("getVerifiedUserData", "RestAuthenticationController::getVerifiedUserData"); - $routes->post("getChatResponse", "ChatBotController::getChatResponse"); - - $routes->post("storeFireBase", "EmployeeRestController::storeFireBase"); $routes->get("getEmployeeProfile", "EmployeeRestController::getEmployeeProfile"); $routes->post("employeeUpload", "EmployeeRestController::employeeUpload"); $routes->get("relationshipList", "EmployeeRestController::relationshipList"); $routes->get("getEmployeeAndDependence", "EmployeeRestController::getEmployeeAndDependence"); $routes->post("editEmployeeAndDependence", "EmployeeRestController::editEmployeeAndDependence"); - $routes->post("addEmployeeAndDependence", "EmployeeRestController::addEmployeeAndDependence"); - $routes->get("getEmployeePolicy", "EmployeeRestController::getEmployeePolicy"); $routes->post("createOrUpdateEmployeePolicySiAmount", "EmployeeRestController::createOrUpdateEmployeePolicySiAmount"); - $routes->get("deleteDependence", "EmployeeRestController::deleteDependence"); $routes->get("getEmployeeAndDependenceByClientId", "EmployeeRestController::getEmployeeAndDependenceByClientId"); + + // $routes->post("addEmployeeAndDependence", "EmployeeRestController::addEmployeeAndDependence"); + // $routes->get("deleteDependence", "EmployeeRestController::deleteDependence"); + // $routes->get("getEmployeePolicy", "EmployeeRestController::getEmployeePolicy"); + + $routes->get("getEmployeePolicy", "EmployeeRestController::getEmployeePolicyv2"); + $routes->post("addEmployeeAndDependence", "EmployeeRestController::addEmployeeAndDependencev2"); + $routes->get("deleteDependence", "EmployeeRestController::deleteDependencev2"); + $routes->get("getPendingApprovalDependents", "EmployeeRestController::getPendingApprovalDependents"); + $routes->post('processDependentAdd', 'EmployeeRestController::processDependentAdd'); + $routes->get("getClientPolicy", "EmployeeRestController::getClientPolicy"); $routes->get("getClientRM", "EmployeeRestController::getClientRM"); $routes->get("getAddOnPolicy", "EmployeeRestController::getAddOnPolicy"); @@ -876,6 +882,7 @@ $routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratel $routes->get('all', 'ClaimReportDashboardController::all'); $routes->get('debug', 'ClaimReportDashboardController::debug'); $routes->get('debug/(:num)', 'ClaimReportDashboardController::debug/$1'); + $routes->get('download-excel', 'ClaimReportDashboardController::downloadExcel'); }); $routes->group('enrollment-collection-v1', static function ($routes) { diff --git a/app/Controllers/ClaimReportDashboardController.php b/app/Controllers/ClaimReportDashboardController.php index acbe8e8d..c57b97df 100644 --- a/app/Controllers/ClaimReportDashboardController.php +++ b/app/Controllers/ClaimReportDashboardController.php @@ -6,6 +6,9 @@ use App\Models\ClaimReportDashboardModel; use App\Models\ClaimsCollectionV2DashboardModel; use App\Models\ClaimDumpFileModel; use CodeIgniter\API\ResponseTrait; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; /** * Claims Collection dashboard API using claim_report. @@ -554,4 +557,199 @@ class ClaimReportDashboardController extends BaseController ->setBody($body); } + /** + * Download Excel of TPA dump rows linked from claim_report for a policy. + * + * Query: client_policy_id (or client_policy) — required. + * Uses claim_report.source_table + source_row_id to load dump rows. + * Excel columns = only fields AFTER master_reject_reason (that column and earlier are excluded). + * + * GET /util/claims-collection-report/download-excel?client_policy_id=123 + */ + public function downloadExcel() + { + @set_time_limit(0); + @ini_set('max_execution_time', '0'); + + $policyId = $this->resolvePolicyId(); + if ($policyId <= 0) { + return $this->respond([ + 'status' => false, + 'message' => 'client_policy_id is required.', + ], 422); + } + + $db = db_connect(); + + if (!$db->tableExists('claim_report')) { + return $this->respond([ + 'status' => false, + 'message' => 'Table claim_report does not exist.', + ], 500); + } + + $allowedDumpTables = [ + 'claims_dump_vidal', + 'claims_dump_abhi', + 'claims_dump_medi_assist', + 'claims_dump_fhpl', + 'claims_dump_reliance', + 'claims_dump_icici', + ]; + + $reportRows = $db->table('claim_report') + ->select('source_table, source_row_id') + ->where('client_policy_id', $policyId) + ->where('is_active', 1) + ->where('source_table IS NOT NULL', null, false) + ->where('source_row_id IS NOT NULL', null, false) + ->get() + ->getResultArray(); + + if ($reportRows === []) { + return $this->respond([ + 'status' => false, + 'message' => 'No claim_report records with dump linkage found for this policy.', + ], 404); + } + + // Group dump IDs by source_table + $idsByTable = []; + foreach ($reportRows as $row) { + $table = trim((string) ($row['source_table'] ?? '')); + $dumpId = (int) ($row['source_row_id'] ?? 0); + if ($table === '' || $dumpId <= 0 || ! in_array($table, $allowedDumpTables, true)) { + continue; + } + if (!$db->tableExists($table)) { + continue; + } + $idsByTable[$table][$dumpId] = $dumpId; + } + + if ($idsByTable === []) { + return $this->respond([ + 'status' => false, + 'message' => 'No valid TPA dump table references found for this policy.', + ], 404); + } + + $spreadsheet = new Spreadsheet(); + $spreadsheet->removeSheetByIndex(0); + $sheetIndex = 0; + $totalRows = 0; + + foreach ($idsByTable as $dumpTable => $dumpIds) { + $dumpIds = array_values($dumpIds); + $exportColumns = $this->getDumpColumnsAfterMasterReject($db, $dumpTable); + if ($exportColumns === []) { + continue; + } + + $dumpRows = $db->table($dumpTable) + ->whereIn('id', $dumpIds) + ->orderBy('id', 'ASC') + ->get() + ->getResultArray(); + + if ($dumpRows === []) { + continue; + } + + $sheetTitle = substr($dumpTable, 0, 31); + $sheet = $spreadsheet->createSheet($sheetIndex); + $sheet->setTitle($sheetTitle); + $sheetIndex++; + + // Headers + foreach ($exportColumns as $colIdx => $colName) { + $sheet->setCellValue( + Coordinate::stringFromColumnIndex($colIdx + 1) . '1', + $colName + ); + } + + // Data + $excelRow = 2; + foreach ($dumpRows as $dumpRow) { + foreach ($exportColumns as $colIdx => $colName) { + $sheet->setCellValue( + Coordinate::stringFromColumnIndex($colIdx + 1) . $excelRow, + $dumpRow[$colName] ?? null + ); + } + $excelRow++; + $totalRows++; + } + } + + if ($sheetIndex === 0 || $totalRows === 0) { + return $this->respond([ + 'status' => false, + 'message' => 'No dump table rows found for the linked claim_report records.', + ], 404); + } + + $spreadsheet->setActiveSheetIndex(0); + + $policy = $db->table('client_policy') + ->select('policy_no') + ->where('id', $policyId) + ->get() + ->getRowArray(); + + $policyNo = trim((string) ($policy['policy_no'] ?? '')); + if ($policyNo === '') { + $policyNo = (string) $policyId; + } + // Safe filename segment + $policyNoSafe = preg_replace('/[^A-Za-z0-9_\-]+/', '_', $policyNo) ?: (string) $policyId; + + $filename = 'claim_report_' . $policyNoSafe . '_' . date('Ymd_His') . '.xlsx'; + + ob_start(); + (new Xlsx($spreadsheet))->save('php://output'); + $excelOutput = ob_get_clean(); + + return $this->response + ->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setHeader('Cache-Control', 'max-age=0') + ->setBody($excelOutput); + } + + /** + * Return dump-table column names that appear AFTER master_reject_reason + * (master_reject_reason itself and all earlier columns are excluded). + * + * @return list + */ + protected function getDumpColumnsAfterMasterReject($db, string $table): array + { + $fields = $db->getFieldNames($table); + if ($fields === [] || $fields === false) { + return []; + } + + $marker = null; + foreach ($fields as $idx => $name) { + $lower = strtolower((string) $name); + if ( + $lower === 'master_reject_reason' + || $lower === 'master_rejected_reason' + || $lower === 'master_rejection_reason' + ) { + $marker = $idx; + break; + } + } + + // Marker not found — export nothing rather than leaking internal columns + if ($marker === null) { + return []; + } + + return array_values(array_slice($fields, $marker + 1)); + } + } diff --git a/app/Controllers/EmployeeController.php b/app/Controllers/EmployeeController.php index 0e6e5f93..b60561a2 100755 --- a/app/Controllers/EmployeeController.php +++ b/app/Controllers/EmployeeController.php @@ -164,6 +164,39 @@ class EmployeeController extends AdminController $this->loadLayout('employee_list', $data); } + public function pendingApprovalsList() + { + $data = []; + $filterData = $this->request->getGet() ?: []; + + $data['employees'] = $this->employeePolicyModel->getPendingApprovalDependents( + client_id: $filterData['client_id'] ?? null, + policy_id: $filterData['policy_id'] ?? null, + branch_id: $filterData['branch_id'] ?? null, + ); + $data['getData'] = [ + 'client_id' => $filterData['client_id'] ?? '0', + 'policy_id' => $filterData['policy_id'] ?? '0', + 'branch_id' => $filterData['branch_id'] ?? '0', + 'emp_code' => '', + 'emp_name' => '', + 'status' => ['pending_approval'], + ]; + + // AJAX filter submit → return table HTML only + if (count($this->request->getGet())) { + $html = view('employee_data_list', $data); + return $this->respond(['status' => true, 'html' => $html], 200); + } + + // Default page load → list all pending dependents + $data['tab_name'] = 'Pending Approvals'; + $data['page_name'] = 'Pending Approvals'; + $data['default_table_html'] = view('employee_data_list', $data); + + $this->loadLayout('employee_pending_approvals_list', $data); + } + public function getClientWithPolicies() { try { diff --git a/app/Controllers/EmployeeRestController.php b/app/Controllers/EmployeeRestController.php index 7d30450c..6586990e 100755 --- a/app/Controllers/EmployeeRestController.php +++ b/app/Controllers/EmployeeRestController.php @@ -327,12 +327,26 @@ class EmployeeRestController extends AdminController } } - public function createEmployeePolicyData($employee_id, $emp_code, $client_id, $client_policy_id, $basic_cover_si = null, $payable_employee = 0) + public function createEmployeePolicyData($employee_id, $emp_code, $client_id, $client_policy_id, $basic_cover_si = null, $payable_employee = 0, $extra_data = []) { + $client_policy = $this->clientPolicyModel->where('id', $client_policy_id)->where('client_id', $client_id)->first(); + $policy_terms = json_decode($client_policy['policy_terms']); + $dependent_effective_date = $extra_data['dependent_effective_date'] ?? null; + $hr_id = $extra_data['hr_id'] ?? null; + + $self_data = $this->employeeModel + ->select('employees.id') + ->join('employee_polices', 'employee_polices.employee_id = employees.id') + ->where('employee_polices.client_policy_id', $client_policy_id) + ->where('employees.emp_code', $emp_code) + ->where('employees.client_id', $client_id) + ->where('employees.relationship', 'self') + ->where('employees.is_active', 1) + ->where('employee_polices.is_active', 1) + ->first(); + if ($basic_cover_si == null) { - $client_policy = $this->clientPolicyModel->where('id', $client_policy_id)->where('client_id', $client_id)->first(); - $policy_terms = json_decode($client_policy['policy_terms']); $basic_cover_si = $policy_terms->sum_insured; } @@ -344,7 +358,7 @@ class EmployeeRestController extends AdminController ->where('client_policy_id', $client_policy_id) ->where('employee_id', $employee_id) ->where('is_active', 1) - ->set(['basic_cover_si' => $basic_cover_si]) + ->set(['basic_cover_si' => $basic_cover_si, 'updated_by' => $self_data['id'] ?? null]) ->update(); } else { @@ -352,8 +366,11 @@ class EmployeeRestController extends AdminController $data['client_policy_id'] = $client_policy_id; $data['basic_cover_si'] = $basic_cover_si; $data['payable_employee'] = $payable_employee; - $data['status'] = 'draft'; - $data['date_coverage'] = $this->getEmployeeCoverageDate($emp_code, $client_policy_id); + $data['policy_end_date'] = $client_policy['policy_end_date']; + $data['date_coverage'] = $this->convertDateFormatYMD($dependent_effective_date); + $data['status'] = 'pending_approval'; + $data['created_by'] = $hr_id ?? $self_data['id'] ?? null; + $data['emp_policy_created_by'] = $hr_id ? "HR" : 'USER'; $this->employeePolicyModel->insert($data); } @@ -2394,21 +2411,21 @@ class EmployeeRestController extends AdminController $slab_details = $this->policesModel->getPolicySlabRatesForEmpOnboard($client_policy_id, $client_id); // print_r($slab_details);die(); - $existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id, client_policy_id: $client_policy_id, emp_code: $emp_code, emp_status: ['draft', 'enrolled'], policy_status: ['draft', 'enrolled']); + $existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id, client_policy_id: $client_policy_id, emp_code: $emp_code, emp_status: ['draft', 'enrolled', 'pending_approval'], policy_status: ['draft', 'enrolled', 'pending_approval']); if (! count($existing_famility_details) && $policy_type == 2) //top up addon only { //get basepolicy id then pull emplist from base policy if only current policy is DA addon policy and emplist is zero // echo 'inside'; $client_policy_id = $base_policy; - $existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id, client_policy_id: $client_policy_id, emp_code: $emp_code, emp_status: ['draft', 'enrolled'], policy_status: ['draft', 'enrolled']); + $existing_famility_details = $this->employeeModel->getEmpFamilybyEmpCode(client_id: $client_id, client_policy_id: $client_policy_id, emp_code: $emp_code, emp_status: ['draft', 'enrolled', 'pending_approval'], policy_status: ['draft', 'enrolled', 'pending_approval']); } $file = ['id' => null, 'client_id' => $client_id, 'policy_id' => $client_policy_id, 'action' => 'inception']; // dd($this->employeeModel->getLastQuery()); // print_r($existing_famility_details);die(); $employee_data_group_by_family = data_group_by_family($existing_famility_details, $data_source = 'db'); - // print_r(($employee_data_group_by_family));//die(); + // print_r(($employee_data_group_by_family)); die(); $existing_units = $this->clientBranchModel->getExisitingUnits(client_id: $client_id, client_branch_id: $client_branch_id); @@ -6331,4 +6348,529 @@ class EmployeeRestController extends AdminController } } + + //--------------------------------------------------------------------------------------------------------------------------------------------- + // Employee Dependent Add API Endpoints + //--------------------------------------------------------------------------------------------------------------------------------------------- + + + + public function addEmployeeAndDependencev2() + { + try { + + $data = $this->request->getJSON(); + // print_r($data);die; + + if ($data) { + + $Count = 0; + foreach ($data as $item) { + + $self_data = $this->employeeModel + ->select('employees.id') + ->join('employee_polices', 'employee_polices.employee_id = employees.id') + ->where('employee_polices.client_policy_id', $item->client_policy_id) + ->where('employees.emp_code', $item->emp_code) + ->where('employees.client_id', $item->client_id) + ->where('employees.relationship', 'self') + ->where('employees.is_active', 1) + ->where('employee_polices.is_active', 1) + ->first(); + + $item->family_floater_key = $this->RelationshipMap($item->relationship); + $item->gender = $this->GenderMap($item->relationship, $item->emp_code); + $item->dob = $this->convertDateFormatYMD($item->dob); + $item->emp_status = 'pending_approval'; + $item->emp_created_by = isset($item->hr_id) ? 'HR' : 'USER'; + + if (isset($item->id)) { + + //update old data + $id = $item->id; + $item->updated_by = $item->hr_id ?? $self_data['id']; + $employee = $this->employeeModel->where('is_active', 1)->update($id, (array) $item); + if ($employee) { + $Count++; + } + + } else { + + //create new data + $item->created_by = $item->hr_id ?? $self_data['id']; + $employee = $this->employeeModel->insert($item); + + if ($employee) { + $Count++; + } + } + } + + if ($Count > 0) { + + if (isset($data[0]->id)) { + $this->createEmployeePolicyData($data[0]->id, $data[0]->emp_code, $data[0]->client_id, $data[0]->client_policy_id, $data[0]->basic_cover_si); + } else { + $payable_employee = $this->getEmployeePayableValue($data[0]->client_policy_id, $data[0]->relationship); + $this->createEmployeePolicyData($employee, $data[0]->emp_code, $data[0]->client_id, $data[0]->client_policy_id, $data[0]->basic_cover_si, $payable_employee, ['dependent_effective_date' => $data[0]->dependent_effective_date, 'hr_id' => $data[0]->hr_id ?? null]); + } + + $this->updatePremiumAmount($data[0]->client_policy_id, $data[0]->emp_code, $data[0]->client_branch_id); + + $result = []; + return $this->respond(['status' => 'success', 'code' => 200, 'data' => $result], 200); + } else { + $result = "No Matches"; + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => $result], 200); + } + }else{ + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => "Requested parameters are required"], 200); + } + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + + public function deleteDependencev2() + { + try { + + if ($this->request->getGet('id')) { + + $this->employeeModel->where('id', $this->request->getGet('id')) + ->where('is_active', 1) + ->set(['emp_status' => 'truncated', 'is_active' => 0]) + ->update(); + + $this->employeePolicyModel->where('employee_id', $this->request->getGet('id')) + ->where('is_active', 1) + ->set(['status' => 'truncated', 'is_active' => 0]) + ->update(); + + return $this->respond(['status' => 'success', 'code' => 200, 'data' => []], 200); + } else { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => []], 200); + } + + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + + public function getEmployeeByPolicyv2() + { + try { + + $id = $this->request->getGet('id') ?? $this->request->getPost('id'); + $emp_code = $this->request->getGet('emp_code') ?? $this->request->getPost('emp_code'); + $client_id = $this->request->getGet('client_id') ?? $this->request->getPost('client_id'); + $client_branch_id = $this->request->getGet('client_branch_id') ?? $this->request->getPost('client_branch_id'); + $client_policy_id = $this->request->getGet('client_policy_id') ?? $this->request->getPost('client_policy_id'); + + if (empty($id) || empty($emp_code) || empty($client_id) || empty($client_branch_id) || empty($client_policy_id)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'message' => 'Required parameters missing: id, emp_code, client_id, client_branch_id and client_policy_id are required', 'data' => []], 200); + } + + $keysToRemove = ["removable_keys"]; + + // Filtered by client_policy_id — returns at most one GMC policy + $empPolicyList = $this->employeeModel->getEmployeePolicy($id, $client_policy_id); + $array = $empPolicyList[0] ?? null; + + if (!$array) { + return $this->respond(['status' => 'failed', 'code' => 404, 'message' => 'Employee policy not found for the given employee and client policy', 'data' => []], 200); + } + + // GMC only (policy_type_id = 2) + if ((int) $array->policy_type_id !== 2) { + return $this->respond(['status' => 'failed', 'code' => 400, 'message' => 'Only GMC policy is supported for dependent add', 'data' => []], 200); + } + + $empData = $this->employeeModel + ->where('emp_code', $emp_code) + ->where('client_id', $client_id) + ->where('client_branch_id', $client_branch_id) + ->where('is_active', 1) + ->where('is_addon_value', 0) + ->groupStart() + ->whereIn('family_floater_key', ['self', 'spouse', 'child']) + ->orWhereIn('relationship', ['Self', 'Spouse', 'Son', 'Daughter']) + ->groupEnd() + ->findAll(); + + $decodedArray = json_decode($array->Policy_Terms); + $refusingData = (object) array_diff_key((array) $decodedArray, array_flip($keysToRemove)); + $array->Policy_Terms = $refusingData; + + $getSlabAndGridData = $this->policesModel->getPolicySlabRatesForEmpOnboard($array->ClientPolicyId, $array->ClientId); + $array->SlabRates = $getSlabAndGridData['slab_rates']; + $array->GridMaster = $getSlabAndGridData['grid_master']; + + $array->eCardDownload = $array->tpa_id != null + ? base_url('download-e-card/') . $array->rand_string . '/1' + : null; + + $familyFloates = $array->Policy_Terms->family_floaters ?? (object) []; + + // Only spouse + children (ignore parents / parents-in-law) + $hasSpouse = isset($familyFloates->spouse) && (int) $familyFloates->spouse > 0; + $hasChildren = isset($familyFloates->childrens) && (int) $familyFloates->childrens > 0; + $spouseCount = $hasSpouse ? (int) $familyFloates->spouse : 0; + $childrenCount = $hasChildren ? (int) $familyFloates->childrens : 0; + + $spouseChildrenTerms = (object) [ + 'self' => isset($familyFloates->self) ? (int) $familyFloates->self : 1, + 'spouse' => $spouseCount, + 'childrens' => $childrenCount, + ]; + + $array->notes = $this->FloterNotesConvertion($spouseChildrenTerms); + + $allowedRelationships = []; + if ($hasSpouse) { + $allowedRelationships[] = 'Spouse'; + } + if ($hasChildren) { + $allowedRelationships[] = 'Son'; + $allowedRelationships[] = 'Daughter'; + } + $array->allowed_relationships = $allowedRelationships; + + if (!empty($getSlabAndGridData['slab_rates'][0]) && ($getSlabAndGridData['slab_rates'][0]['premium_type'] == 1 || $getSlabAndGridData['slab_rates'][0]['premium_type'] == 3)) { + $array->floter_text_heading = 'Floater Sum Insured'; + $array->floter_text_description = 'This is a floater sum insured. A floater is a type of sum insured that provides coverage to more than one member ot a family at the same time. Simply put, its a single insurance cover for the entire family.'; + } else { + $array->floter_text_heading = 'Sum Insured'; + $array->floter_text_description = ''; + } + + $floters = $this->FloterConvertion($spouseChildrenTerms); + + $data = []; + $dependent_and_si_value = 0; + $dependent_and_si_premium_value = 0; + $dependent_and_si_gst_value = 0; + + foreach ($floters as $familyFloatesValue) { + $dependent = preg_replace('/\d/', '', $familyFloatesValue); + + if (!in_array($dependent, ['self', 'spouse', 'child'], true)) { + continue; + } + if ($dependent === 'spouse' && !$hasSpouse) { + continue; + } + if ($dependent === 'child' && !$hasChildren) { + continue; + } + + if (count($empData)) { + foreach ($empData as $key => $value) { + $empFloaterKey = !empty($value['family_floater_key']) + ? preg_replace('/\d/', '', strtolower(trim($value['family_floater_key']))) + : $this->RelationshipMap($value['relationship']); + + $relationship = strtolower(trim($value['relationship'] ?? '')); + $matches = ($empFloaterKey === $dependent) + || ($dependent === 'spouse' && $relationship === 'spouse') + || ($dependent === 'child' && in_array($relationship, ['son', 'daughter'], true)) + || ($dependent === 'self' && $relationship === 'self'); + + if ($matches) { + $employee_policy = $this->employeePolicyModel + ->where('employee_id', $value['id']) + ->where('client_policy_id', $array->ClientPolicyId) + ->where('is_active', 1) + ->get() + ->getRow(); + + if (isset($employee_policy->basic_cover_si)) { + $dependent_and_si_value = ($dependent_and_si_value == 0) ? $employee_policy->basic_cover_si : $dependent_and_si_value; + } + if (isset($employee_policy->payable_employee) && $employee_policy->payable_employee == 1) { + if (isset($employee_policy->rata_premimum)) { + $dependent_and_si_premium_value = $dependent_and_si_premium_value + $employee_policy->rata_premimum; + } + if (isset($employee_policy->gst)) { + $dependent_and_si_gst_value = $dependent_and_si_gst_value + $employee_policy->gst; + } + } + + $temp = []; + $temp['is_value_exist'] = true; + $temp['data']['family_floater_key'] = $familyFloatesValue; + $temp['data']['employee_id'] = $value['id']; + $temp['data']['relationship'] = $value['relationship']; + $temp['data']['name'] = $value['name']; + $temp['data']['dob'] = $this->convertDateFormatDMY($value['dob']); + $temp['data']['client_policy_id'] = $array->ClientPolicyId; + $temp['data']['form_type'] = $dependent; + $temp['data']['basic_cover_si'] = isset($employee_policy->basic_cover_si) ? $employee_policy->basic_cover_si : 0; + $temp['data']['age_validation'] = $this->getAgeRange($decodedArray, $familyFloatesValue); + + array_push($data, $temp); + unset($empData[$key]); + $floters = array_diff($floters, [$familyFloatesValue]); + break; + } + } + } + } + + // Remaining allowed spouse/child slots → Add buttons + if (count($floters)) { + foreach ($floters as $familyFloatesValue) { + $dependent = preg_replace('/\d/', '', $familyFloatesValue); + + if (!in_array($dependent, ['spouse', 'child'], true)) { + continue; + } + if ($dependent === 'spouse' && !$hasSpouse) { + continue; + } + if ($dependent === 'child' && !$hasChildren) { + continue; + } + + $temp2 = []; + $temp2['is_value_exist'] = false; + $temp2['data']['family_floater_key'] = $familyFloatesValue; + $temp2['data']['client_policy_id'] = $array->ClientPolicyId; + $temp2['data']['button_name'] = 'Add ' . ucfirst(str_replace('_', ' ', $dependent)); + $temp2['data']['form_type'] = $dependent; + $temp2['data']['age_validation'] = $this->getAgeRange($decodedArray, $familyFloatesValue); + $temp2['data']['allowed_relationships'] = $dependent === 'spouse' + ? ['Spouse'] + : ['Son', 'Daughter']; + + array_push($data, $temp2); + } + } + + $array->mapped_family_floaters = $data; + $array->type = 'GMC'; + $array->family_floaters_of_dependent_and_si_value = $dependent_and_si_value > 0 ? $dependent_and_si_value : 0; + $array->family_floaters_of_dependent_and_si_premium_value = $dependent_and_si_premium_value > 0 ? round($dependent_and_si_premium_value) : 0; + $array->family_floaters_of_dependent_and_gst_value = $dependent_and_si_gst_value > 0 ? round($dependent_and_si_gst_value) : 0; + + $employeePolicy = $this->getEmployeePolicyMembersForDependentAdd( + $emp_code, + $client_id, + $client_branch_id, + $client_policy_id + ); + + // Show add button when any spouse/child slot is still available + $addButtonShow = false; + foreach ($data as $floaterItem) { + if (empty($floaterItem['is_value_exist'])) { + $addButtonShow = true; + break; + } + } + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Employee policy and dependent details fetched successfully', + 'data' => $array, + 'EmployeePolicy' => $employeePolicy, + 'add_button_show' => $addButtonShow, + ], 200); + } catch (\Exception $e) { + return $this->respond(['status' => 'failed', 'code' => 500, 'message' => 'Something went wrong while fetching employee policy details', 'data' => $e->getMessage()], 500); + } + } + + /** + * Fetch Self / Spouse / Children employee+policy rows for a GMC client policy. + * Same data shape as EmployeePolicy in getEmployeeActiveOrInactivePolicy. + */ + private function getEmployeePolicyMembersForDependentAdd($emp_code, $client_id, $client_branch_id, $client_policy_id) + { + $employeeIds = $this->employeeModel + ->select('id') + ->where('emp_code', $emp_code) + ->where('client_id', $client_id) + ->where('client_branch_id', $client_branch_id) + ->where('is_active', 1) + ->where('is_addon_value', 0) + ->groupStart() + ->whereIn('family_floater_key', ['self', 'spouse', 'child']) + ->orWhereIn('relationship', ['Self', 'Spouse', 'Son', 'Daughter']) + ->groupEnd() + ->findColumn('id'); + + if (empty($employeeIds)) { + return []; + } + + return $this->employeePolicyModel + ->select('employees.*, employee_polices.employee_id, employee_polices.basic_cover_si, employee_polices.premium, employee_polices.gst, employee_polices.tpa_id, employee_polices.rand_string, employee_polices.uhid as uhid') + ->join('employees', 'employee_polices.employee_id = employees.id', 'left') + ->whereIn('employee_polices.employee_id', $employeeIds) + ->where('employee_polices.client_policy_id', $client_policy_id) + ->where('employee_polices.is_active', 1) + ->findAll(); + } + + public function processDependentAdd() + { + try { + $data = $this->request->getJSON(true); + + if (empty($data)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Request body is required'], 200); + } + + $employee_id = $data['employee_id'] ?? null; + $client_policy_id = $data['client_policy_id'] ?? null; + $hr_id = $data['hr_id'] ?? null; + $status = $data['status'] ?? "approved"; + $updated_by = $hr_id ?? get_session_userid() ?? null; + // Approver role: HR (app) or ACM (internal portal session user) + $approved_by_role = ! empty($hr_id) ? 'HR' : 'ACM'; + + if (empty($employee_id)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'employee_id is required'], 200); + } + + if (empty($client_policy_id)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'client_policy_id is required'], 200); + } + + if (empty($status)) { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'status is required'], 200); + } + + if ($status !== 'approved' && $status !== 'rejected') { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'status is invalid'], 200); + } + + $employee = $this->employeeModel + ->where('id', $employee_id) + ->where('is_active', 1) + ->first(); + + if (!$employee) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'Employee not found or inactive'], 200); + } + + if (($employee['emp_status'] ?? null) === 'active') { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Dependent is already approved'], 200); + } + + if (($employee['emp_status'] ?? null) !== 'pending_approval') { + return $this->respond(['status' => 'failed', 'code' => 400, 'data' => 'Dependent is not pending approval'], 200); + } + + $employee_policy = $this->employeePolicyModel + ->where('employee_id', $employee_id) + ->where('client_policy_id', $client_policy_id) + ->where('is_active', 1) + ->first(); + + if (!$employee_policy) { + return $this->respond(['status' => 'failed', 'code' => 404, 'data' => 'Employee policy not found or inactive'], 200); + } + + if ($status === 'approved') { + $employee_update_data = [ + 'updated_by' => $updated_by, + 'approved_by' => $approved_by_role, + 'emp_status' => 'active', + ]; + $policy_update_data = [ + 'updated_by' => $updated_by, + 'approved_by' => $approved_by_role, + 'status' => 'active', + ]; + } else { + $employee_update_data = [ + 'updated_by' => $updated_by, + 'approved_by' => $approved_by_role, + 'emp_status' => 'rejected', + 'is_active' => 0, + ]; + $policy_update_data = [ + 'updated_by' => $updated_by, + 'approved_by' => $approved_by_role, + 'status' => 'rejected', + 'is_active' => 0, + ]; + } + + $employee_updated = $this->employeeModel + ->where('id', $employee_id) + ->where('is_active', 1) + ->set($employee_update_data) + ->update(); + + if (!$employee_updated) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => 'Failed to update employee status'], 200); + } + + $policy_updated = $this->employeePolicyModel + ->where('employee_id', $employee_id) + ->where('client_policy_id', $client_policy_id) + ->where('is_active', 1) + ->set($policy_update_data) + ->update(); + + if (!$policy_updated) { + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => 'Failed to update employee policy status'], 200); + } + + return $this->respond(['status' => 'success', 'code' => 200, 'data' => []], 200); + } catch (\Exception $e) { + log_message('error', 'Error in processDependentAdd: ' . $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine()); + return $this->respond(['status' => 'failed', 'code' => 500, 'data' => $e->getMessage()], 500); + } + } + + /** + * List pending_approval dependents (API + mobile). + * Filters: client_id, client_branch_id / branch_id, client_policy_id / policy_id, optional search. + */ + public function getPendingApprovalDependents() + { + try { + $client_id = $this->request->getGet('client_id'); + $branch_id = $this->request->getGet('client_branch_id') ?? $this->request->getGet('branch_id'); + $policy_id = $this->request->getGet('client_policy_id') ?? $this->request->getGet('policy_id'); + $search = trim((string) ($this->request->getGet('search') ?? '')); + + $empData = $this->employeePolicyModel->getPendingApprovalDependents( + client_id: $client_id ?? 0, + policy_id: $policy_id ?? 0, + branch_id: $branch_id ?? 0, + search: $search + ); + + if ($empData) { + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'message' => 'Pending approval dependents fetched successfully', + 'data' => $empData, + ], 200); + } + + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'message' => 'No pending approval dependents found', + 'data' => [], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'message' => $e->getMessage(), + 'data' => [], + ], 500); + } + } + } diff --git a/app/Controllers/HrLoginDiagnoseController.php b/app/Controllers/HrLoginDiagnoseController.php index 0b7a1fa3..4ec2eb30 100644 --- a/app/Controllers/HrLoginDiagnoseController.php +++ b/app/Controllers/HrLoginDiagnoseController.php @@ -81,7 +81,11 @@ class HrLoginDiagnoseController extends AdminController 'HR_NOT_FOUND', 'high', 'No active HR found in post level_contacts for the given email/mobile (contact_type=client).', - 'Create/activate the HR contact under the correct client branch, or verify the email/mobile.' + [ + 'Option A: Verify email/mobile spelling and that level_contacts.contact_type = client and is_active = 1.', + 'Option B: Create/activate the HR contact under the correct client_branch (ref_id = branch id).', + 'Option C: If HR exists only in pre DB, create the matching post level_contacts row first.', + ] ); return [ @@ -89,6 +93,7 @@ class HrLoginDiagnoseController extends AdminController 'overall_status' => 'FAIL', 'summary' => 'HR not found in post DB.', 'issues' => $issues, + 'problems' => $issues, 'post' => ['contacts' => []], 'hr_access_control'=> [], 'pre' => ['reachable' => null, 'mappings' => []], @@ -102,6 +107,7 @@ class HrLoginDiagnoseController extends AdminController $accessRows = []; $preMaps = []; $loginCards = []; + $pairRows = []; $preReachable = null; try { @@ -115,7 +121,11 @@ class HrLoginDiagnoseController extends AdminController 'PRE_DB_UNREACHABLE', 'high', 'Could not connect to preDB: ' . $e->getMessage(), - 'Check preDB credentials in Database config / .env.' + [ + 'Option A: Check preDB hostname/database/username/password in app/Config/Database.php or .env.', + 'Option B: Confirm pre DB server is reachable from this host (network/firewall).', + 'Option C: Re-run diagnose after connectivity is restored to validate PRE mapping.', + ] ); } @@ -130,16 +140,24 @@ class HrLoginDiagnoseController extends AdminController $issues[] = $this->issue( 'BRANCH_INACTIVE', 'high', - "Branch inactive for post_hr_id={$contact['post_hr_id']} (branch #{$contact['post_branch_id']}).", - 'Activate client_branch.is_active=1 for this branch.' + "Branch inactive for post_hr_id={$contact['post_hr_id']} (branch #{$contact['post_branch_id']}, {$contact['branch_name']}).", + [ + "Option A: UPDATE client_branch SET is_active=1 WHERE id={$contact['post_branch_id']};", + 'Option B: In Client Onboarding, activate this branch and re-check login.', + 'Note: Login join requires client_branch.is_active=1.', + ] ); } if (!$flags['CLIENT_ACTIVE']) { $issues[] = $this->issue( 'CLIENT_INACTIVE', 'high', - "Client inactive for post_hr_id={$contact['post_hr_id']} (client #{$contact['post_client_id']}).", - 'Activate clients.is_active=1 for this client.' + "Client inactive for post_hr_id={$contact['post_hr_id']} (client #{$contact['post_client_id']}, {$contact['client_name']}).", + [ + "Option A: UPDATE clients SET is_active=1 WHERE id={$contact['post_client_id']};", + 'Option B: Activate the client in admin and re-run diagnose.', + 'Note: Login join requires clients.is_active=1.', + ] ); } @@ -160,8 +178,12 @@ class HrLoginDiagnoseController extends AdminController $issues[] = $this->issue( 'NO_HR_ACCESS_CONTROL_ROW', 'high', - "No hr_access_control row for post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']}.", - 'Open Client Onboarding → HR Access and save at least one module for this HR/branch.' + "No hr_access_control row for post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']} ({$contact['client_name']} / {$contact['branch_name']}).", + [ + 'Option A: Open Client Onboarding → HR Access for this client, select at least one module for this HR/branch, then Save.', + "Option B: Insert hr_access_control with post_hr_id={$contact['post_hr_id']}, post_branch_id={$contact['post_branch_id']}, post_client_id={$contact['post_client_id']} and allowed_modules JSON.", + 'Note: Without this row, getVerifiedHrData returns empty token for this card.', + ] ); } else { $modules = json_decode($access['allowed_modules'] ?? '[]', true); @@ -176,8 +198,12 @@ class HrLoginDiagnoseController extends AdminController $issues[] = $this->issue( 'EMPTY_ALLOWED_MODULES', 'high', - "hr_access_control id={$access['id']} exists but allowed_modules is empty.", - 'Assign pre/post modules in HR Access Control and save.' + "hr_access_control id={$access['id']} exists but allowed_modules is empty for post_hr_id={$contact['post_hr_id']}.", + [ + 'Option A: Open HR Access Control, tick PRE/POST modules for this HR, Save.', + "Option B: UPDATE hr_access_control SET allowed_modules='{\"pre\":[1],\"post\":[2,3,4]}' WHERE id={$access['id']}; (adjust module ids as needed).", + 'Note: Empty modules → token is treated as not ready.', + ] ); } } @@ -206,25 +232,52 @@ class HrLoginDiagnoseController extends AdminController if ($preReachable === true) { if (empty($preMap['PRE_BRANCH_LINKED'])) { + $postBranchId = (int) ($contact['post_branch_id'] ?? 0); $issues[] = $this->issue( 'PRE_BRANCH_NOT_LINKED', 'medium', - "Post branch #{$contact['post_branch_id']} has no valid pre_branch_id link.", - 'Set client_branch.pre_branch_id to the matching pre branch id.' + "Post branch #{$postBranchId} ({$contact['branch_name']}) has no valid pre_branch_id link (current pre_branch_id=" . ($contact['pre_branch_id'] ?? 'null') . ").", + [ + "Option A: Set client_branch.pre_branch_id on post branch #{$postBranchId} to the matching pre client_branch.id.", + 'Option B: Use Testing/mapping tools to sync post↔pre branch ids, then re-run diagnose.', + 'Option C: Confirm the pre branch exists and is_active=1 before linking.', + 'Note: Without this link, PRE HR/client mapping cannot be resolved.', + ] ); } elseif (empty($preMap['PRE_HR_MATCHED'])) { $issues[] = $this->issue( 'PRE_HR_NOT_MATCHED', 'medium', - "No pre level_contacts match for email+mobile under pre client #{$preMap['pre_client_id']}.", - 'Align email and mobile between pre and post HR contacts.' + "No pre level_contacts match for email/mobile under pre client #{$preMap['pre_client_id']} " + . "(post_hr_id={$contact['post_hr_id']}, email={$contact['hr_mail']}, mobile={$contact['hr_mobile']}).", + [ + 'Option A: Align email AND mobile between pre and post level_contacts for this HR.', + "Option B: Create/activate pre HR contact under pre client #{$preMap['pre_client_id']} with the same email/mobile.", + 'Option C: Check pre contact_type=client and is_active=1 on the matching branch.', + ] ); } elseif (empty($preMap['PRE_IDS_CONSISTENT']) && !empty($access)) { + $storedHr = (int) ($access['pre_hr_id'] ?? 0); + $storedClient = (int) ($access['pre_client_id'] ?? 0); + $storedBranch = (int) ($access['pre_branch_id'] ?? 0); + $liveHr = (int) ($preMap['pre_hr_id'] ?? 0); + $liveClient = (int) ($preMap['pre_client_id'] ?? 0); + $liveBranch = (int) ($preMap['pre_branch_id'] ?? 0); + $accessId = (int) ($access['id'] ?? 0); + $issues[] = $this->issue( 'PRE_IDS_INCONSISTENT', 'medium', - "hr_access_control pre ids do not match live pre data for post_hr_id={$contact['post_hr_id']}.", - 'Re-save HR Access Control so pre_hr_id / pre_client_id / pre_branch_id are refreshed.' + "hr_access_control pre ids do not match live pre data for post_hr_id={$contact['post_hr_id']} " + . "(client #{$contact['post_client_id']}, branch #{$contact['post_branch_id']}). " + . "Stored (hr_access_control id={$accessId}): pre_hr_id={$storedHr}, pre_client_id={$storedClient}, pre_branch_id={$storedBranch}. " + . "Live pre: pre_hr_id={$liveHr}, pre_client_id={$liveClient}, pre_branch_id={$liveBranch}.", + [ + 'Option A: Open Client Onboarding → HR Access for this client, select modules for this HR, Save (form must send live pre ids).', + "Option B (recommended): UPDATE hr_access_control SET pre_hr_id={$liveHr}, pre_client_id={$liveClient}, pre_branch_id={$liveBranch} WHERE id={$accessId};", + 'Option C: If UI re-save keeps old ids, auto-refresh in saveHrAccessData is commented out — enable it or use Option B.', + 'Note: POST login can still work; this mainly affects PRE modules/JWT pre mapping.', + ] ); } } @@ -244,6 +297,29 @@ class HrLoginDiagnoseController extends AdminController : (!$flags['CLIENT_ACTIVE'] ? 'CLIENT_INACTIVE' : 'UNKNOWN'))), ]; + $pairRows[] = [ + 'post' => [ + 'post_hr_id' => (int) $contact['post_hr_id'], + 'hr_name' => $contact['hr_name'] ?? '', + 'hr_mail' => $contact['hr_mail'] ?? '', + 'hr_mobile' => $contact['hr_mobile'] ?? '', + 'is_active' => (int) ($contact['is_active'] ?? 0), + 'post_branch_id' => (int) ($contact['post_branch_id'] ?? 0), + 'branch_name' => $contact['branch_name'] ?? '', + 'branch_active' => (int) ($contact['branch_active'] ?? 0), + 'pre_branch_id' => $contact['pre_branch_id'] ?? null, + 'post_client_id' => (int) ($contact['post_client_id'] ?? 0), + 'client_name' => $contact['client_name'] ?? '', + 'client_active' => (int) ($contact['client_active'] ?? 0), + 'has_access' => ! empty($access), + 'token_ready' => $tokenReady, + 'hr_active' => (int) ($contact['is_active'] ?? 0) === 1, + ], + 'pre' => $preMap, + 'access_id' => isset($access['id']) ? (int) $access['id'] : null, + 'has_token' => $canLoginCard, + ]; + $contact['hr_found'] = true; $contact['flags'] = $flags; } @@ -307,6 +383,26 @@ class HrLoginDiagnoseController extends AdminController $tokenCards = array_filter($loginCards, static fn ($c) => ! empty($c['has_token'])); $preMatched = array_filter($preMaps, static fn ($m) => ! empty($m['PRE_HR_MATCHED'])); + // Group PRE/POST pairs by post client for client-wise display. + $byClientMap = []; + foreach ($pairRows as $pair) { + $cid = (int) ($pair['post']['post_client_id'] ?? 0); + $key = (string) $cid; + if (! isset($byClientMap[$key])) { + $byClientMap[$key] = [ + 'post_client_id' => $cid, + 'client_name' => $pair['post']['client_name'] ?? '', + 'client_active' => (int) ($pair['post']['client_active'] ?? 0), + 'rows' => [], + ]; + } + $byClientMap[$key]['rows'][] = $pair; + } + $byClient = array_values($byClientMap); + usort($byClient, static function ($a, $b) { + return strcasecmp((string) ($a['client_name'] ?? ''), (string) ($b['client_name'] ?? '')); + }); + return [ 'input' => ['email' => $email, 'mobile' => $mobile], 'overall_status' => $overall, @@ -315,6 +411,7 @@ class HrLoginDiagnoseController extends AdminController 'summary' => $summary, 'issues' => $deduped, 'problems' => $deduped, + 'by_client' => $byClient, 'post' => [ 'contacts' => $postContacts, 'raw_found' => count($postContacts) > 0, @@ -355,6 +452,7 @@ class HrLoginDiagnoseController extends AdminController 'pre_matched' => count($preMatched), 'token_cards' => count($tokenCards), 'access_rows' => count(array_filter($accessRows, static fn ($a) => ! empty($a['id']))), + 'client_count' => count($byClient), 'would_return_both' => $anyToken && count($preMatched) > 0, 'winner' => ($anyToken && count($preMatched) > 0) ? 'both' : ($anyToken ? 'post' : (count($preMatched) > 0 ? 'pre' : 'none')), 'reason' => $summary, @@ -514,13 +612,25 @@ class HrLoginDiagnoseController extends AdminController return $result; } - private function issue(string $code, string $severity, string $message, string $fix = ''): array + /** + * @param string|array $fix + * @return array{code:string,severity:string,message:string,fix:string,fixes:array} + */ + private function issue(string $code, string $severity, string $message, $fix = ''): array { + if (is_array($fix)) { + $fixes = array_values(array_filter(array_map('strval', $fix))); + } else { + $fixes = preg_split("/\r\n|\n|\r/", (string) $fix) ?: []; + $fixes = array_values(array_filter(array_map('trim', $fixes))); + } + return [ 'code' => $code, 'severity' => $severity, 'message' => $message, - 'fix' => $fix, + 'fix' => implode("\n", $fixes), + 'fixes' => $fixes, ]; } } diff --git a/app/Database/Migrations/2026-08-01-043000_AddDependentApprovalTrackingColumns.php b/app/Database/Migrations/2026-08-01-043000_AddDependentApprovalTrackingColumns.php new file mode 100644 index 00000000..be63ed22 --- /dev/null +++ b/app/Database/Migrations/2026-08-01-043000_AddDependentApprovalTrackingColumns.php @@ -0,0 +1,81 @@ +db->fieldExists('emp_created_by', 'employees')) { + $this->forge->addColumn('employees', [ + 'emp_created_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + 'default' => null, + 'after' => 'emp_status', + 'comment' => 'Creator role: HR / USER', + ], + ]); + } + + if (! $this->db->fieldExists('approved_by', 'employees')) { + $this->forge->addColumn('employees', [ + 'approved_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + 'default' => null, + 'after' => 'emp_created_by', + 'comment' => 'Approver role: HR / ACM', + ], + ]); + } + + // employee_polices: who created the policy row, who approved (HR / ACM) + if (! $this->db->fieldExists('emp_policy_created_by', 'employee_polices')) { + $this->forge->addColumn('employee_polices', [ + 'emp_policy_created_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + 'default' => null, + 'after' => 'status', + 'comment' => 'Creator role: HR / USER', + ], + ]); + } + + if (! $this->db->fieldExists('approved_by', 'employee_polices')) { + $this->forge->addColumn('employee_polices', [ + 'approved_by' => [ + 'type' => 'VARCHAR', + 'constraint' => 50, + 'null' => true, + 'default' => null, + 'after' => 'emp_policy_created_by', + 'comment' => 'Approver role: HR / ACM', + ], + ]); + } + } + + public function down() + { + if ($this->db->fieldExists('approved_by', 'employees')) { + $this->forge->dropColumn('employees', 'approved_by'); + } + if ($this->db->fieldExists('emp_created_by', 'employees')) { + $this->forge->dropColumn('employees', 'emp_created_by'); + } + if ($this->db->fieldExists('approved_by', 'employee_polices')) { + $this->forge->dropColumn('employee_polices', 'approved_by'); + } + if ($this->db->fieldExists('emp_policy_created_by', 'employee_polices')) { + $this->forge->dropColumn('employee_polices', 'emp_policy_created_by'); + } + } +} diff --git a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php index 533394b1..adda4795 100644 --- a/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php +++ b/app/Libraries/TPAClaimsImportServices/BaseTpaClaimImportService.php @@ -808,7 +808,19 @@ abstract class BaseTpaClaimImportService } $ticketMasterModel = new TicketMasterModel(); - return $ticketMasterModel->updateBatch($payload, 'id') !== false; + $ok = $ticketMasterModel->updateBatch($payload, 'id') !== false; + if ($ok) { + $ids = []; + foreach ($payload as $row) { + if (!empty($row['id'])) { + $ids[] = (int) $row['id']; + } + } + // updateBatch does not fire model afterUpdate callbacks + (new \App\Models\ClaimReportModel())->syncFromTicketIds($ids); + } + + return $ok; } /** diff --git a/app/Models/ClaimReportModel.php b/app/Models/ClaimReportModel.php index df809eb1..86e0b74c 100644 --- a/app/Models/ClaimReportModel.php +++ b/app/Models/ClaimReportModel.php @@ -55,4 +55,148 @@ class ClaimReportModel extends Model 'created_at', 'updated_at', ]; + + /** + * Push ticket_master fields into matching claim_report row(s). + * Match by ticket_id first; fallback to (client_policy_id, claim_number). + * Never creates new claim_report rows — only updates existing ones. + * Failures are swallowed so ticket updates are never blocked. + */ + public function syncFromTicketId(int $ticketId): bool + { + if ($ticketId <= 0) { + return true; + } + + try { + if (!$this->db->tableExists('claim_report')) { + return true; + } + + $ticket = $this->db->table('ticket_master') + ->where('id', $ticketId) + ->get() + ->getRowArray(); + + if (empty($ticket)) { + return true; + } + + $update = $this->mapTicketToClaimReport($ticket); + if ($update === []) { + return true; + } + + $exists = $this->db->table('claim_report') + ->where('ticket_id', $ticketId) + ->countAllResults() > 0; + + if ($exists) { + $this->db->table('claim_report') + ->where('ticket_id', $ticketId) + ->update($update); + + return true; + } + + // Fallback: match by unique key when ticket_id not set on claim_report yet + $claimNumber = trim((string) ($ticket['claim_number'] ?? '')); + $clientPolicyId = (int) ($ticket['client_policy_id'] ?? 0); + if ($claimNumber === '' || $clientPolicyId <= 0) { + return true; + } + + $this->db->table('claim_report') + ->where('client_policy_id', $clientPolicyId) + ->where('claim_number', $claimNumber) + ->update($update); + + return true; + } catch (\Throwable $e) { + log_message('error', 'claim_report syncFromTicketId failed for ticket {id}: {msg}', [ + 'id' => $ticketId, + 'msg' => $e->getMessage(), + ]); + + return false; + } + } + + /** + * Sync multiple ticket IDs (e.g. after updateBatch). + * + * @param list $ticketIds + */ + public function syncFromTicketIds(array $ticketIds): void + { + $ids = array_values(array_unique(array_filter(array_map('intval', $ticketIds)))); + foreach ($ids as $id) { + $this->syncFromTicketId($id); + } + } + + /** + * @param array $ticket + * @return array + */ + protected function mapTicketToClaimReport(array $ticket): array + { + $now = date('Y-m-d H:i:s'); + + $relation = $ticket['relation'] ?? null; + if ($relation === null || $relation === '') { + $relation = $ticket['relationship'] ?? null; + } + + $approved = $ticket['approved_amount'] ?? null; + $claimed = $ticket['claim_amount'] ?? null; + $incurred = $ticket['incurred_amount'] ?? null; + if ($incurred === null || $incurred === '') { + $incurred = $approved !== null && $approved !== '' ? $approved : $claimed; + } + + $map = [ + 'tpa_id' => $ticket['tpa_id'] ?? null, + 'client_id' => $ticket['client_id'] ?? null, + 'client_policy_id' => $ticket['client_policy_id'] ?? null, + 'file_id' => $ticket['file_id'] ?? null, + 'ticket_id' => $ticket['id'] ?? null, + 'claim_number' => isset($ticket['claim_number']) ? trim((string) $ticket['claim_number']) : null, + 'emp_code' => $ticket['emp_code'] ?? null, + 'tpa_no' => $ticket['tpa_no'] ?? null, + 'emp_id' => $ticket['emp_id'] ?? null, + 'insured_emp_id' => $ticket['insured_emp_id'] ?? null, + 'claim_amount' => $claimed, + 'approved_amount' => $approved, + 'incurred_amount' => $incurred, + 'si_amt' => $ticket['si_amt'] ?? null, + 'tpa_claim_status' => $ticket['tpa_claim_status'] ?? null, + 'claim_status_id' => $ticket['claim_status_id'] ?? null, + 'tpa_claim_type' => $ticket['tpa_claim_type'] ?? null, + 'tpa_ailments' => $ticket['tpa_ailments'] ?? null, + 'doa' => $ticket['doa'] ?? null, + 'dod' => $ticket['dod'] ?? null, + 'date_of_intimat' => $ticket['date_of_intimat'] ?? null, + 'settled_date' => $ticket['settled_date'] ?? null, + 'approved_date' => $ticket['approved_date'] ?? null, + 'claim_dump_date' => $ticket['claim_dump_date'] ?? null, + 'hospital_name' => $ticket['hospital_name'] ?? null, + 'hospital_city' => $ticket['hospital_city'] ?? null, + 'hospital_state' => $ticket['hospital_state'] ?? null, + 'hospital_pin_code' => $ticket['hospital_pin_code'] ?? null, + 'hospital_address' => $ticket['hospital_address'] ?? null, + 'relation' => $relation, + 'is_active' => isset($ticket['is_active']) ? (int) $ticket['is_active'] : 1, + 'updated_at' => $now, + ]; + + // Do not overwrite claim_number with empty string + if ($map['claim_number'] === '') { + unset($map['claim_number']); + } + + // Keep source_table / source_row_id as-is (provenance from dump sync) + + return $map; + } } diff --git a/app/Models/EmployeeModel.php b/app/Models/EmployeeModel.php index 9f2663d7..bf869d82 100755 --- a/app/Models/EmployeeModel.php +++ b/app/Models/EmployeeModel.php @@ -31,6 +31,8 @@ class EmployeeModel extends Model "otp", "family_floater_key", "emp_status", + "emp_created_by", + "approved_by", "created_by", "updated_by", "updated_at", @@ -151,7 +153,7 @@ class EmployeeModel extends Model - public function getEmployeePolicy($id) + public function getEmployeePolicy($id, $client_policy_id = null) { return $this->db->table('employee_polices') @@ -176,6 +178,9 @@ class EmployeeModel extends Model ->where('employee_polices.status !=', 'truncated') ->where('employee_polices.is_active', 1) ->where('employee_polices.employee_id', $id) + ->when($client_policy_id, function($query) use ($client_policy_id){ + return $query->where('employee_polices.client_policy_id', $client_policy_id); + }) ->get() ->getResult(); } diff --git a/app/Models/EmployeePolicyModel.php b/app/Models/EmployeePolicyModel.php index 51392f54..1ffaf8d0 100755 --- a/app/Models/EmployeePolicyModel.php +++ b/app/Models/EmployeePolicyModel.php @@ -16,6 +16,8 @@ class EmployeePolicyModel extends Model "uhid", "batch_id", "status", + "emp_policy_created_by", + "approved_by", "pre_existing_alignments", "date_of_exit", "reason_for_exit", @@ -375,6 +377,105 @@ class EmployeePolicyModel extends Model return $res; } + /** + * Pending-approval dependents (excludes Self) for a client / branch / policy. + */ + public function getPendingApprovalDependents($client_id = 0, $policy_id = 0, $branch_id = 0, $search = '') + { + $ecard_download_link = " + CASE + WHEN + LOWER(emp.relationship) = 'self' AND + employee_polices.tpa_id IS NOT NULL AND employee_polices.tpa_id != '' AND + employee_polices.uhid IS NOT NULL AND employee_polices.uhid != '' + THEN + CONCAT('" . base_url('download-e-card/') . "', employee_polices.rand_string, '/1') + ELSE + NULL + END AS ecard_download_link + "; + + $result = $this->select([ + 'employee_polices.*', + 'policy_type.policy_type as policy_name', + 'im.short_name as insurer_short_name', + 'ib.branch_name as insurer_branch_name', + 'ib.branch_code as insurer_branch_code', + 'tpam.name as tpa_name', + 'tpam.short_name as tpa_short_name', + 'tpab.branch_code as tpa_branch_code', + 'cm.client_name', + 'cm.short_name as client_short_name', + 'emp.relationship', + 'emp.relationship_code', + 'emp.change_event', + 'emp.emp_code', + 'emp.name', + 'emp.email_corporate', + 'emp.dob', + 'DATE_FORMAT(emp.dob, "%d/%m/%Y") AS formatted_dob', + 'emp.gender', + 'emp.emp_status', + 'emp.is_active as emp_is_active', + 'emp.mobile as mobile', + 'emp.doj', + 'emp.basic_pay', + 'emp.band as grade', + 'policy_type.policy_type', + 'client_branch.branch_name as client_branch_name', + 'client_branch.branch_code as client_branch_code', + 'cp.policy_no', + 'cp.policy_type_id', + 'cp.is_addon', + '( + select id from employees + where emp_code = emp.emp_code + and client_id = emp.client_id + and lower(relationship) = "self" + and is_active = 1 + limit 1 + ) as self_employee_id', + $ecard_download_link + ]) + ->join('employees emp', 'employee_polices.employee_id = emp.id') + ->join('client_policy cp', 'employee_polices.client_policy_id = cp.id') + ->join('policies pm', 'cp.policy_id = pm.id', 'left') + ->join('policy_type', 'policy_type.id = cp.policy_type_id') + ->join('insurers im', 'cp.insurer_id = im.id') + ->join('insurer_branch ib', 'cp.insurer_branch_id = ib.id') + ->join('tpa tpam', 'cp.tpa_id = tpam.id', 'left') + ->join('tpa_branch tpab', 'cp.tpa_branch_id = tpab.id', 'left') + ->join('clients cm', 'cp.client_id = cm.id') + ->join('client_branch', 'emp.client_branch_id = client_branch.id') + ->where('employee_polices.is_active', 1) + ->where('emp.is_active', 1) + ->where('employee_polices.status', 'pending_approval') + ->where('emp.emp_status', 'pending_approval') + ->where("LOWER(emp.relationship) != 'self'", null, false) + ->orderBy('emp.emp_code', 'ASC') + ->orderBy('employee_polices.employee_id', 'ASC'); + + if (is_string($client_id) && preg_match('/^[a-f0-9]{32}$/i', $client_id)) { + $result->where('md5(emp.client_id)', $client_id); + } elseif ($client_id != 0 && ! empty($client_id)) { + $result->where('emp.client_id', $client_id); + } + + if ($branch_id != 0 && ! empty($branch_id)) { + $result->where('emp.client_branch_id', $branch_id); + } + + if ($policy_id != 0 && ! empty($policy_id)) { + $result->where('employee_polices.client_policy_id', $policy_id); + } + + if (! empty($search)) { + $this->applyEmployeeGlobalSearch($result, $search); + } + + return $result->findAll(); + } + /** * Global search across employee + policy fields (OR + LIKE). * Searches: emp_code, name, mobile, email, tpa_id, dob, relationship, status. diff --git a/app/Views/employee_data_list.php b/app/Views/employee_data_list.php index 56126102..e5947d4a 100755 --- a/app/Views/employee_data_list.php +++ b/app/Views/employee_data_list.php @@ -317,6 +317,9 @@ $gst_total = 0; case 'enrolled': echo '' . ucfirst($employee['status']) . ''; break; + case 'pending_approval': + echo 'Pending Approval'; + break; default: echo $employee['status']; break; @@ -434,8 +437,8 @@ $gst_total = 0;
- - + +
diff --git a/app/Views/employee_pending_approvals_list.php b/app/Views/employee_pending_approvals_list.php new file mode 100644 index 00000000..7aabf9cf --- /dev/null +++ b/app/Views/employee_pending_approvals_list.php @@ -0,0 +1,406 @@ + + +
+
+
+
+
+

+ Filter + + + +

+
+
+
+
+
+ +
+ +
+
+ +
+ +
+
+ +
+
+
+
+ Submit +
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/app/Views/hr_login_diagnose.php b/app/Views/hr_login_diagnose.php index 7df3c90a..5ed50f7f 100644 --- a/app/Views/hr_login_diagnose.php +++ b/app/Views/hr_login_diagnose.php @@ -132,8 +132,46 @@ .badge-high{background:var(--high-badge-bg);color:var(--high-badge-text);} .code{font-family:var(--mono);font-size:12.5px;color:var(--danger-code);font-weight:600;} .fix-text{color:var(--text-secondary);} + .fix-list{margin:0;padding-left:18px;} + .fix-list li{margin-bottom:6px;line-height:1.35;} + .fix-list li:last-child{margin-bottom:0;} + .problems-table td.fix-cell{min-width:320px;max-width:480px;} + .problems-table td.msg-cell{min-width:260px;} - .compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:20px;} + .compare-grid{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:12px;} + .client-block{margin-bottom:0;} + .client-block-head{ + display:flex;align-items:center;justify-content:space-between;gap:12px;flex-wrap:wrap; + padding:12px 16px;background:#F8F9FA;border-bottom:1px solid var(--border); + font-size:13.5px;font-weight:700; + } + .client-block-head .meta{font-weight:400;color:var(--text-muted);font-size:12.5px;} + .client-block .compare-grid{margin:0;padding:12px;background:#fff;border-radius:0;} + .client-block .compare-grid + .compare-grid{border-top:1px dashed var(--border);} + .branch-label{font-size:12px;color:var(--text-secondary);font-weight:600;margin:0 0 8px;padding:0 4px;} + + .client-tabs-wrap{margin-bottom:20px;} + .client-tabs{ + display:flex;gap:4px;border-bottom:1px solid var(--border);margin-bottom:0;padding:0 4px; + flex-wrap:nowrap;overflow-x:auto;-webkit-overflow-scrolling:touch; + } + .client-tab{ + padding:10px 14px;font-size:12.5px;font-weight:600;color:var(--text-secondary); + cursor:pointer;border-bottom:2px solid transparent;background:none;border-top:none;border-left:none;border-right:none; + font-family:var(--sans);white-space:nowrap;flex:0 0 auto;max-width:260px;overflow:hidden;text-overflow:ellipsis; + } + .client-tab.active{color:var(--teal-dark);border-bottom-color:var(--teal);} + .client-tab:hover:not(.active){color:var(--text-primary);} + .client-tab .badge-count{ + display:inline-block;margin-left:6px;font-size:10px;font-weight:700;padding:1px 6px;border-radius:999px; + background:var(--low-badge-bg);color:var(--low-badge-text); + } + .client-tab.active .badge-count{background:var(--teal-tint);color:var(--teal-dark);} + .client-panel{ + display:none;border:1px solid var(--border);border-top:none; + border-radius:0 0 var(--radius-md) var(--radius-md);background:#fff;overflow:hidden; + } + .client-panel.active{display:block;} .compare-card{border:1px solid var(--border);border-radius:var(--radius-md);overflow:hidden;background:#fff;} .compare-head{ padding:14px 20px;background:#F8F9FA;border-bottom:1px solid var(--border); @@ -246,6 +284,7 @@ $problems = $result['problems'] ?? ($result['issues'] ?? []); $accessRows = $result['hr_access_control'] ?? []; $loginSim = $result['login_simulation'] ?? []; + $byClient = $result['by_client'] ?? []; $postPrimary = $post['primary'] ?? []; $prePrimary = $pre['primary'] ?? []; $postFs = $post['filters_summary'] ?? []; @@ -265,87 +304,156 @@ - + - + - + - - + +
SeverityCodeCode MessageFixFix
No problems detected.
+ + + +
    + +
  1. + +
+ +
-
-
-
PRE (enrollment)
- -
- - - - - - - - - - - - - - - - - - - - - - - -
Raw found
Eligible (matched)
preDB reachable
PRE_BRANCH_LINKED
PRE_HR_MATCHED
PRE_IDS_CONSISTENT
PRE_CLIENT_MAPPED
pre_hr_id
name
email
mobile
client
branch
pre_client_id
pre_branch_id
-
+
Client-wise PRE / POST ( client)
+ +
+ -
-
POST (live)
- - - - - - - - - - - - - - - - - - - - -
Raw found - -
Eligible for login
hr active
branch active
client active
has access row
token ready
post_hr_id
name
email
mobile
client
branch
+ +
+

No client mappings to display.

-
+ +
+
+ $clientBlock): ?> + + + +
-
+ $clientBlock): ?> + +
+
+
+
+ + # +
+
+ branch/HR row + · token ready / + · client +
+
+ + + +
+
+
PRE (enrollment)
+

Branch:

+ + + + + + + + + + + + + +
PRE_BRANCH_LINKED
PRE_HR_MATCHED
PRE_IDS_CONSISTENT
PRE_CLIENT_MAPPED
pre_hr_id
name
email
mobile
client
branch
pre_client_id
pre_branch_id
+
+ +
+
POST (live)
+

Branch:

+ + + + + + + + + + + + + + +
Eligible for login
hr active
branch active
client active
has access row
token ready
post_hr_id
name
email
mobile
client
branch
access id
+
+
+ +
+
+ +
+ + +
@@ -359,6 +467,7 @@ POST eligible PRE eligible POST contacts + Clients mapped PRE matched Access rows with id Token-ready cards @@ -537,7 +646,20 @@