MERGE_TEST_MINOR_CHANGES

This commit is contained in:
Ubuntu 2026-03-02 11:20:31 +05:30
commit ac0e159f5e
40 changed files with 3696 additions and 567 deletions

View File

@ -32,6 +32,8 @@ class Acl
'#^/metaTpaDashboardDemo#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID]],
'#^/sales/dashboard#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/sales#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
'#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],

View File

@ -444,7 +444,9 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
$routes->get('proceedExcelFileDataValidation', 'EmployeeController::proceedExcelFileDataValidation');
$routes->get('checkTpaApiEnable', 'EmployeeRestController::checkTpaApiEnable');
$routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable');
$routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
$routes->post('insufficientCdBalanceHrMailSend', 'EmployeeController::insufficientCdBalanceHrMailSend');
$routes->get('getTpaClaimDumpErrorData/(:any)', 'TicketServiceController::getTpaClaimDumpErrorData/$1');
});
$routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
@ -915,6 +917,8 @@ $routes->group('sales', function($routes) {
$routes->get('loadactivities', 'SalesController::loadactivities');
$routes->get('loadtargets', 'SalesController::loadtargets');
$routes->get('page/(:segment)', 'SalesController::noPage/$1');
// Get all leads with filters
@ -995,6 +999,13 @@ $routes->group('sales', function($routes) {
// Delete note
$routes->delete('notes/(:num)', 'SalesController::deleteNote/$1');
// ==================== TARGETS ROUTES ====================
$routes->get('targets', 'SalesController::getTargets');
$routes->get('targets/user/(:num)', 'SalesController::getTargetByUser/$1');
$routes->get('targets/fy/(:segment)','SalesController::getTargetByFY/$1');
$routes->post('targets', 'SalesController::createTarget');
$routes->put('targets/(:num)', 'SalesController::updateTarget/$1');
$routes->delete('targets/(:num)', 'SalesController::deleteTarget/$1');
//Dashboard
$routes->get('dashboard', 'SalesController::dashboard');
@ -1002,4 +1013,13 @@ $routes->group('sales', function($routes) {
$routes->get('salesManagerLevelDashboard', 'SalesController::salesManagerLevelDashboard');
});
// Expence Module Route Group
$routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controllers'], static function($routes) {
$routes->get('/', 'ExpenseController::index');
$routes->post('save', 'ExpenseController::save');
$routes->get('get/(:num)', 'ExpenseController::getExpense/$1');
$routes->post('delete/(:num)', 'ExpenseController::delete/$1');
$routes->get('client-policies', 'ExpenseController::clientPolicies');
});

View File

@ -62,32 +62,40 @@ class AppContentManagementController extends AdminController
$data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($data);
$id = ((int) $sanitized_post_data['add_image_id']) ?? null;
$id = (int)($sanitized_post_data['add_image_id'] ?? 0);
$rules = [
'client_id' => [
'rules' => 'required|integer',
'add_image_id' => [
'rules' => 'permit_empty|integer|is_natural',
'errors' => [
'required' => 'Client is required',
'integer' => 'Invalid client selected'
'integer' => 'Image ID must be a valid number',
'is_natural' => 'Image ID must be a non-negative number'
]
],
'client_id' => [
'rules' => 'required|integer|is_natural_no_zero',
'errors' => [
'required' => 'Client is required',
'integer' => 'Invalid client selected',
'is_natural_no_zero' => 'Invalid client selected'
]
],
'advertise_image' => [
'rules' => ($id === 0 ? 'uploaded[advertise_image]|' : '')
. 'is_image[advertise_image]'
. '|mime_in[advertise_image,image/jpg,image/jpeg,image/png]'
. '|max_size[advertise_image,200]'
. '|min_dims[advertise_image,1640,664]'
. '|max_dims[advertise_image,1640,664]',
'errors' => [
'uploaded' => 'Image is required',
'is_image' => 'File must be an image',
'mime_in' => 'Only JPG, JPEG, PNG allowed',
'max_size' => 'Image size must not exceed 200 KB',
'min_dims' => 'Image dimensions must be exactly 1640x664 pixels',
'max_dims' => 'Image dimensions must be exactly 1640x664 pixels',
]
],
];
$rules['advertise_image'] = [
'rules' => ($id === 0 ? 'uploaded[advertise_image]|' : '') // required only for ADD
. 'is_image[advertise_image]'
. '|mime_in[advertise_image,image/jpg,image/jpeg,image/png]'
. '|max_size[advertise_image,200]'
. '|min_dims[advertise_image,1640,664]'
. '|max_dims[advertise_image,1640,664]',
'errors' => [
'uploaded' => 'Image is required',
'is_image' => 'File must be an image',
'mime_in' => 'Only JPG, JPEG, PNG allowed',
'max_size' => 'Image size must not exceed 200 KB',
'min_dims' => 'Image dimensions must be exactly 1640x664 pixels',
'max_dims' => 'Image dimensions must be exactly 1640x664 pixels',
]
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
@ -100,7 +108,12 @@ class AppContentManagementController extends AdminController
$file = $this->request->getFile('advertise_image');
$client_id = $sanitized_post_data['client_id'] ?? null;
$client_id = (int)($sanitized_post_data['client_id'] ?? 0);
$clientExists = $this->clientModel->where('id', $client_id)->where('is_active', 1)->first();
if (!$clientExists) {
return $this->respond(['status' => false, 'message' => 'Invalid client selected.'], 400);
}
if (!$file || !$file->isValid()) {
return $this->respond(['status' => false, 'message' => 'No file uploaded or invalid file.'], 400);
@ -119,12 +132,15 @@ class AppContentManagementController extends AdminController
$file->move($uploadPath, $fileName);
$id = $sanitized_post_data['add_image_id'] ?? null;
$details = ['name' => $fileName,'client_id'=>$client_id];
if ($id == 0) {
if ($id === 0) {
$this->addImgModel->insert($details);
} else {
$existingRecord = $this->addImgModel->find($id);
if (!$existingRecord) {
return $this->respond(['status' => false, 'message' => 'Record not found for update.'], 404);
}
$this->addImgModel->update($id, $details);
}
@ -138,14 +154,34 @@ class AppContentManagementController extends AdminController
public function remove_advertise_image()
{
try {
$id = $this->request->getPost('add_image_id');
$rules = [
'add_image_id' => [
'rules' => 'required|integer|is_natural_no_zero',
'errors' => [
'required' => 'Image ID is required',
'integer' => 'Image ID must be a valid number',
'is_natural_no_zero' => 'Image ID must be a positive number'
]
]
];
if (!$id) {
return $this->respond(['status' => false, 'message' => 'ID missing'], 400);
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors()
]);
}
$data = ['is_active' => 0];
$this->addImgModel->update($id, $data);
$id = (int)$this->request->getPost('add_image_id');
$existing = $this->addImgModel->find($id);
if (!$existing) {
return $this->respond(['status' => false, 'message' => 'Record not found'], 404);
}
$this->addImgModel->update($id, ['is_active' => 0]);
return $this->respond(['status' => true, 'message' => 'Deleted successfully']);
@ -158,19 +194,27 @@ class AppContentManagementController extends AdminController
// Preview image Went Edit.
public function showAdvertiseImage($filename)
{
// $path = WRITEPATH . 'uploads/advertiseImage/' . $filename;
$path = ROOTPATH . 'public/uploads/add_image_upload/' . $filename;
$filename = basename($filename);
if (!file_exists($path)) {
if (!preg_match('/^[a-zA-Z0-9_\-]+\.(jpg|jpeg|png|gif|webp)$/i', $filename)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
// return $this->response->setStatusCode(404, 'File not found');
}
$mime = mime_content_type($path);
// header('Content-Type: ' . $mimeType);
// readfile($path);
// exit;
return $this->response->setHeader('Content-Type', $mime)->setBody(file_get_contents($path));
$path = ROOTPATH . 'public/uploads/add_image_upload/' . $filename;
$realPath = realpath($path);
$allowedDir = realpath(ROOTPATH . 'public/uploads/add_image_upload');
if (!$realPath || strpos($realPath, $allowedDir) !== 0 || !file_exists($realPath)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
$mime = mime_content_type($realPath);
$allowedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
if (!in_array($mime, $allowedMimes, true)) {
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound();
}
return $this->response->setHeader('Content-Type', $mime)->setBody(file_get_contents($realPath));
}
@ -183,39 +227,51 @@ class AppContentManagementController extends AdminController
if ($this->request->getMethod() === 'post') {
$rules = [
'type' => [
'rules' => 'required|max_length[255]',
'fe_id' => [
'rules' => 'permit_empty|integer|is_natural',
'errors' => [
'required' => 'Type is required',
'max_length' => 'Type cannot exceed 255 characters'
'integer' => 'ID must be a valid number',
'is_natural' => 'ID must be a non-negative number'
]
],
'type' => [
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
'errors' => [
'required' => 'Type is required',
'max_length' => 'Type cannot exceed 255 characters',
'regex_match' => 'Type contains invalid characters'
]
],
'content_section' => [
'rules' => 'required|max_length[255]',
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9_ \-]+$/]',
'errors' => [
'required' => 'Content Section is required',
'max_length' => 'Content Section cannot exceed 255 characters'
'required' => 'Content Section is required',
'max_length' => 'Content Section cannot exceed 255 characters',
'regex_match' => 'Content Section contains invalid characters'
]
],
'heading' => [
'rules' => 'required|max_length[255]',
'rules' => 'required|max_length[255]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/]+$/]',
'errors' => [
'required' => 'Heading is required',
'max_length' => 'Heading cannot exceed 255 characters'
'required' => 'Heading is required',
'max_length' => 'Heading cannot exceed 255 characters',
'regex_match' => 'Heading contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
]
],
'content' => [
'rules' => 'required|max_length[5000]',
'rules' => 'required|max_length[5000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
'errors' => [
'required' => 'Content is required',
'max_length' => 'Content cannot exceed 5000 characters'
'required' => 'Content is required',
'max_length' => 'Content cannot exceed 5000 characters',
'regex_match' => 'Content contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
]
],
'notes' => [
'rules' => 'required|max_length[1500]',
'rules' => 'required|max_length[1500]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
'errors' => [
'required' => 'Notes are required',
'max_length' => 'Notes cannot exceed 1500 characters'
'required' => 'Notes are required',
'max_length' => 'Notes cannot exceed 1500 characters',
'regex_match' => 'Notes contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
]
]
];
@ -230,8 +286,14 @@ class AppContentManagementController extends AdminController
}
$request_post_data = $this->request->getPost();
$data = sanitizeInputArrayAdvanced($request_post_data);
$id = $data['fe_id'];
$id = $data['fe_id'] ?? null;
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false, 'message' => 'Invalid ID format', 'code' => 400
]);
}
$id = !empty($id) ? (int)$id : null;
unset($data['fe_id']);
@ -244,6 +306,12 @@ class AppContentManagementController extends AdminController
$status = $this->feContentModel->insert($data);
$text = "Created";
} else {
$existing = $this->feContentModel->find($id);
if (!$existing) {
return $this->response->setStatusCode(404)->setJSON([
'status' => false, 'message' => 'Record not found', 'code' => 404
]);
}
$status = $this->feContentModel->update($id, $data);
$text = "Updated";
}
@ -260,6 +328,13 @@ class AppContentManagementController extends AdminController
$id = $this->request->getGet('fe_id') ?? null;
if (!empty($id)) {
if (!ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false, 'code' => 400, 'message' => 'Invalid ID format'
]);
}
$id = (int)$id;
$data['fe_list'] = $this->feContentModel->where('id', $id)->orderBy('id', 'DESC')->findAll();
@ -276,16 +351,24 @@ class AppContentManagementController extends AdminController
} elseif ($method === 'delete') {
// $input = $this->request->getRawInput();
$id = $this->request->getGet('fe_id'); // ✅ THIS
$id = $id ?? null;
$id = $this->request->getGet('fe_id') ?? null;
if (empty($id)) {
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
return $this->respond([
'status' => false,
'code' => 400,
'message' => 'Valid numeric ID is required for deletion'
], 400);
}
$id = (int)$id;
$existing = $this->feContentModel->find($id);
if (!$existing) {
return $this->respond([
'status' => false,
'code' => 404,
'message' => 'No ID provided for deletion'
], 200);
'message' => 'Record not found'
], 404);
}
$update_status = $this->feContentModel->where('id', $id)->set(['is_active' => 0])->update();
@ -309,10 +392,15 @@ class AppContentManagementController extends AdminController
}
private const ALLOWED_RETURN_TYPES = ['api', 'web'];
public function FAQ()
{
$method = strtolower($this->request->getMethod());
$returnType = strtolower($this->request->getGet('return_type') ?? 'api');
if (!in_array($returnType, self::ALLOWED_RETURN_TYPES, true)) {
$returnType = 'api';
}
$ref = ['timestamp' => date('Y-m-d H:i:s')];
try {
@ -320,33 +408,64 @@ class AppContentManagementController extends AdminController
if ($method === 'post') {
$rules = [
'category' => [
'rules' => 'required',
'rules' => 'required|max_length[100]|alpha_numeric_space',
'errors' => [
'required' => 'Category is required'
'required' => 'Category is required',
'max_length' => 'Category cannot exceed 100 characters',
'alpha_numeric_space' => 'Category contains invalid characters'
]
],
'question' => [
'rules' => 'required',
'rules' => 'required|max_length[1000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
'errors' => [
'required' => 'Question is required'
'required' => 'Question is required',
'max_length' => 'Question cannot exceed 1000 characters',
'regex_match' => 'Question contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
]
],
'answer' => [
'rules' => 'required',
'rules' => 'required|max_length[5000]|regex_match[/^[a-zA-Z0-9 _\-.,;:!?()&\/\r\n]+$/]',
'errors' => [
'required' => 'Answer is required'
'required' => 'Answer is required',
'max_length' => 'Answer cannot exceed 5000 characters',
'regex_match' => 'Answer contains invalid characters. Only letters, numbers, spaces and basic punctuation are allowed'
]
]
];
if (!$this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error',
'message' => 'Input validation failed',
'code' => 400,
'errors' => $this->validator->getErrors(),
'ref' => $ref
]);
}
$request_post_data = $this->request->getPost();
$sanitized_post_data = sanitizeInputArrayAdvanced($request_post_data);
$data = array_filter($sanitized_post_data, fn($v) => $v !== '' && $v !== null);
$id = $data['faq_id'];
$id = $data['faq_id'] ?? null;
if (!empty($id) && (!ctype_digit((string)$id) || (int)$id <= 0)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
]);
}
$id = !empty($id) ? (int)$id : null;
unset($data['faq_id']);
if (empty($id)) {
$status = $this->faqModel->insert($data);
$msg = "Created";
} else {
$existing = $this->faqModel->find($id);
if (!$existing) {
return $this->response->setStatusCode(404)->setJSON([
'status' => 'error', 'message' => 'FAQ not found', 'code' => 404, 'ref' => $ref
]);
}
$status = $this->faqModel->update($id, $data);
$msg = "Updated";
}
@ -372,10 +491,17 @@ class AppContentManagementController extends AdminController
$id = $this->request->getGet('faq_id');
if (!empty($id)) {
if (!ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setStatusCode(400)->setJSON([
'status' => 'error', 'message' => 'Invalid FAQ ID format', 'code' => 400, 'ref' => $ref
]);
}
$id = (int)$id;
if($returnType === 'web'){
$row = $this->faqModel->find((int)$id);
$row = $this->faqModel->find($id);
}else{
$row = $this->faqModel->where('is_active', 1)->find((int)$id);
$row = $this->faqModel->where('is_active', 1)->find($id);
}
$data['faq_list'] = $row ? [$row] : [];
@ -417,7 +543,15 @@ class AppContentManagementController extends AdminController
// --- 3. DELETE: SOFT DELETE ---
elseif ($method === 'delete') {
$id = $this->request->getGet('faq_id');
$status = (!empty($id)) ? $this->faqModel->update($id, ['is_active' => 0]) : false;
if (empty($id) || !ctype_digit((string)$id) || (int)$id <= 0) {
return $this->response->setJSON([
'status' => 'error', 'message' => 'Valid numeric FAQ ID is required', 'code' => 400, 'ref' => $ref
])->setStatusCode(400);
}
$id = (int)$id;
$status = $this->faqModel->find($id) ? $this->faqModel->update($id, ['is_active' => 0]) : false;
return $this->response->setJSON([
'status' => $status ? ($returnType === 'web' ? true : 'success') : ($returnType === 'web' ? false : 'error'),
@ -429,12 +563,12 @@ class AppContentManagementController extends AdminController
}
} catch (\Throwable $e) {
$msg = $e->getMessage();
log_message('error', 'FAQ error: ' . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine());
return $this->response->setJSON([
'status' => $returnType === 'web' ? false : 'error',
'code' => 500,
'message' => $msg,
'ref' => ['file' => $e->getFile(), 'line' => $e->getLine()]
'message' => 'An internal error occurred. Please try again later.',
'ref' => $ref
])->setStatusCode(500);
}
}

View File

@ -142,24 +142,38 @@ class ClientController extends AdminController
return $this->response->setJSON(['isDuplicate' => $isDuplicate]);
}
private const ALLOWED_DUPLICATE_CHECK_TABLES = [
'clients', 'client_branch', 'level_contacts', 'insurers', 'insurer_branch',
'tpa', 'tpa_branch', 'policies', 'client_policy', 'user_profiles',
];
public function checkDuplicateTableFieldValue()
{
$table = $this->request->getPost('table');
$field = $this->request->getPost('field');
$value = $this->request->getPost('value');
// Load the database if not already loaded
$db = db_connect();
if (!in_array($table, self::ALLOWED_DUPLICATE_CHECK_TABLES, true)) {
return $this->response->setJSON(['isDuplicate' => false, 'error' => 'Invalid table']);
}
$db = db_connect();
$tableFields = $db->getFieldNames($table);
// Perform the query
$builder = $db->table($table);
if (is_array($value)) {
$isDuplicate = $builder->where($value)->where('is_active', 1)->countAllResults() > 0;
$filteredValue = array_intersect_key($value, array_flip($tableFields));
if (empty($filteredValue)) {
return $this->response->setJSON(['isDuplicate' => false, 'error' => 'Invalid fields']);
}
$isDuplicate = $builder->where($filteredValue)->where('is_active', 1)->countAllResults() > 0;
} else {
if (!in_array($field, $tableFields, true)) {
return $this->response->setJSON(['isDuplicate' => false, 'error' => 'Invalid field']);
}
$isDuplicate = $builder->where($field, $value)->where('is_active', 1)->countAllResults() > 0;
}
// Return the result
return $this->response->setJSON(['isDuplicate' => $isDuplicate]);
}
@ -6955,16 +6969,21 @@ class ClientController extends AdminController
// $response = $ticketServiceController->getClaimExcelErrorData(["file_id" => 41]);
// $response = $ticketServiceController->claimDumpOnBoardProcess(["file_id" => 17]);
// $response = $ticketServiceController->extractExcelData("claims_dump_form_client.xlsx");
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 51]); //abhi
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 50]); //fhpl
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 53]); //icici
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 54]); //mediassist
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 52]); //reliance
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 48]); //vidal
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 4]); //abhi
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 53]); //icici
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 54]); //mediassist
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 52]); //reliance
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 48]); //vidal
// $response = $ticketServiceController->getTpaClaimDumpErrorData(["file_id" => 48]);
// dd($response);
// ---------- TICKET CONTROLLER --------------------------------------------------------------------------------

View File

@ -4360,13 +4360,15 @@ class EmpDataServiceController extends BaseController
{
$client_policy_id = $arrayData['client_policy_id'];
$cd_ac_pk = $this->clientPolicyModel->select('cd_ac_pk')->where('id',$client_policy_id)->first();
$sanitizedIds = array_map('intval', $arrayData['employeeIds']);
$placeholders = implode(',', array_fill(0, count($sanitizedIds), '?'));
$amount = $this->employeePolicyModel->query("
SELECT SUM(rata_premimum + gst) AS total_sum
FROM employee_polices
JOIN employees ON employees.id = employee_polices.employee_id
WHERE employees.unit = '$unit'
AND employee_polices.id IN (" . implode(',', $arrayData['employeeIds']) . ")
")->getRow();
WHERE employees.unit = ?
AND employee_polices.id IN ($placeholders)
", array_merge([$unit], $sanitizedIds))->getRow();
if($amount){
@ -4625,11 +4627,14 @@ class EmpDataServiceController extends BaseController
// AND employee_polices.id IN (" . implode(',', $arrayData['employeeIds']) . ")
// ")->getRow();
$sanitizedIds = array_map('intval', $arrayData['employeeIds']);
$placeholders = implode(',', array_fill(0, count($sanitizedIds), '?'));
$safeAddOneDay = (int)$add_one_day;
$amount = $this->employeePolicyModel->query("
SELECT
SUM(ROUND(
((employee_polices.rata_premimum * (DATEDIFF(employee_polices.policy_end_date, emp_endorsement.new_value) + '$add_one_day')) / 365) +
(((employee_polices.rata_premimum * (DATEDIFF(employee_polices.policy_end_date, emp_endorsement.new_value) + '$add_one_day')) / 365) * 0.18),
((employee_polices.rata_premimum * (DATEDIFF(employee_polices.policy_end_date, emp_endorsement.new_value) + ?)) / 365) +
(((employee_polices.rata_premimum * (DATEDIFF(employee_polices.policy_end_date, emp_endorsement.new_value) + ?)) / 365) * 0.18),
2
)) AS total_sum
FROM
@ -4639,14 +4644,14 @@ class EmpDataServiceController extends BaseController
JOIN
emp_endorsement ON emp_endorsement.pk = employee_polices.id
WHERE
employee_polices.id IN (" . implode(',', $arrayData['employeeIds']) . ")
AND emp_endorsement.pk IN (" . implode(',', $arrayData['employeeIds']) . ")
employee_polices.id IN ($placeholders)
AND emp_endorsement.pk IN ($placeholders)
AND employee_polices.claim_status = 0
AND emp_endorsement.field_name = 'date_of_exit'
AND emp_endorsement.is_active = 1
AND emp_endorsement.actions = 'd'
AND emp_endorsement.status != 'truncated';
")->getRow();
AND emp_endorsement.status != 'truncated'
", array_merge([$safeAddOneDay, $safeAddOneDay], $sanitizedIds, $sanitizedIds))->getRow();
// dd(db_connect()->getLastQuery(), $amount);

View File

@ -0,0 +1,412 @@
<?php
namespace App\Controllers;
use App\Models\ExpenseModel;
use App\Models\ClientModel;
use App\Models\ClientPolicyModel;
use CodeIgniter\API\ResponseTrait;
class ExpenseController extends AdminController
{
use ResponseTrait;
protected $myLogger;
protected $expenseModel;
protected $clientModel;
protected $clientPolicyModel;
public function __construct()
{
set_session_context('Expense');
$this->myLogger = \Config\Services::mylogger();
$this->expenseModel = new ExpenseModel();
$this->clientModel = new ClientModel();
$this->clientPolicyModel = new ClientPolicyModel();
}
/**
* Web list + form view
*/
public function index()
{
try {
$data['tab_name'] = 'Expense';
$data['page_name'] = 'Expense';
$descriptionPattern = '/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/u';
// Raw GET filters
$rawFilters = $this->request->getGet() ?? [];
$rawFilters = is_array($rawFilters) ? $rawFilters : [];
// Sanitize input array using existing helper
$sanitized = sanitizeInputArrayAdvanced($rawFilters);
$filters = [
'client_id' => trim($sanitized['client_id'] ?? ''),
'client_policy_id' => trim($sanitized['client_policy_id'] ?? ''),
'approved_by' => trim($sanitized['approved_by'] ?? ''),
'description' => trim($sanitized['description'] ?? ''),
'amount' => trim($sanitized['amount'] ?? ''),
'expense_date' => trim($sanitized['expense_date'] ?? ''),
];
$validationErrors = [];
// Basic type / format validation for filters
if ($filters['client_id'] !== '' && ! ctype_digit($filters['client_id'])) {
$validationErrors[] = 'Invalid client selected for search.';
$filters['client_id'] = '';
}
if ($filters['client_policy_id'] !== '' && ! ctype_digit($filters['client_policy_id'])) {
$validationErrors[] = 'Invalid policy selected for search.';
$filters['client_policy_id'] = '';
}
if ($filters['approved_by'] !== '' && ! ctype_digit($filters['approved_by'])) {
$validationErrors[] = 'Invalid approver selected for search.';
$filters['approved_by'] = '';
}
if ($filters['description'] !== '' && ! preg_match($descriptionPattern, $filters['description'])) {
$validationErrors[] = 'Description filter contains invalid characters.';
$filters['description'] = '';
}
if ($filters['amount'] !== '') {
if (! is_numeric($filters['amount']) || (float) $filters['amount'] < 0) {
$validationErrors[] = 'Amount filter must be a non-negative number.';
$filters['amount'] = '';
}
}
if ($filters['expense_date'] !== '') {
$dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
$errors = $dt ? \DateTime::getLastErrors() : ['warning_count' => 1, 'error_count' => 1];
if (! $dt || ! empty($errors['warning_count']) || ! empty($errors['error_count'])) {
$validationErrors[] = 'Expense Date filter must be in DD-MM-YYYY format.';
$filters['expense_date'] = '';
}
}
$data['filters'] = $filters;
$data['validation_errors'] = $validationErrors;
// Clients for dropdown
$data['clients'] = $this->clientModel
->select('id, client_name, short_name')
->where('is_active', 1)
->orderBy('client_name', 'ASC')
->findAll();
// Approved by (users) dropdown
$db = db_connect();
$data['approved_users'] = $db->table('user_profiles')
->select('id, first_name')
->where('is_active', 1)
->whereIn('id', [7, 8])
->orderBy('first_name', 'ASC')
->get()
->getResultArray();
// Policies for filter dropdown (when client filter is selected)
$data['policies_for_filter'] = [];
if ($filters['client_id'] !== '') {
$data['policies_for_filter'] = $this->clientPolicyModel
->select('id, policy_no')
->where('client_id', (int) $filters['client_id'])
->where('is_active', 1)
->orderBy('id', 'ASC')
->findAll();
}
// Existing expenses with optional filters
$builder = $this->expenseModel
->select('
expenses.*,
clients.client_name,
clients.short_name,
client_policy.policy_no,
user_profiles.first_name AS approved_by_name
')
->join('clients', 'clients.id = expenses.client_id')
->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
->where('expenses.is_active', 1);
if ($filters['client_id'] !== '') {
$builder->where('expenses.client_id', (int) $filters['client_id']);
}
if ($filters['client_policy_id'] !== '') {
$builder->where('expenses.client_policy_id', (int) $filters['client_policy_id']);
}
if ($filters['approved_by'] !== '') {
$builder->where('expenses.approved_by', (int) $filters['approved_by']);
}
if ($filters['description'] !== '') {
$builder->like('expenses.description', (string) $filters['description']);
}
if ($filters['amount'] !== '') {
$builder->where('expenses.amount', (float) $filters['amount']);
}
if ($filters['expense_date'] !== '') {
$dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']);
if ($dt) {
$builder->where('expenses.expense_date', $dt->format('Y-m-d'));
}
}
$data['expenses'] = $builder
->orderBy('expenses.id', 'DESC')
->findAll();
return $this->loadLayout('expense_list', $data);
} catch (\Throwable $e) {
return handle_exception($e, $this->myLogger, $this->response);
}
}
/**
* Create / update expense (AJAX)
*/
public function save()
{
try {
if ($this->request->getMethod() !== 'post') {
return $this->response
->setStatusCode(405)
->setJSON([
'status' => false,
'message' => 'Invalid request method',
]);
}
$rawData = $this->request->getPost();
$data = sanitizeInputArrayAdvanced($rawData);
$id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null;
$rules = [
'client_id' => [
'rules' => 'required|is_natural_no_zero',
'errors' => [
'required' => 'Client is required',
],
],
'client_policy_id' => [
'rules' => 'required|is_natural_no_zero',
'errors' => [
'required' => 'Policy is required',
],
],
'description' => [
'rules' => 'required|string|min_length[1]|max_length[2000]|regex_match[/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/]',
'errors' => [
'required' => 'Description is required',
'min_length' => 'Description cannot be empty',
'regex_match' => 'Description contains invalid characters.',
],
],
'expense_date' => [
'rules' => 'required|valid_date[d-m-Y]',
'errors' => [
'required' => 'Expense Date is required',
'valid_date' => 'Expense Date must be in DD-MM-YYYY format',
],
],
'approved_by' => [
'rules' => 'required|is_natural_no_zero',
'errors' => [
'required' => 'Approved By is required',
],
],
'amount' => [
'rules' => 'required|numeric|greater_than_equal_to[0]',
'errors' => [
'required' => 'Amount is required',
'numeric' => 'Amount must be numeric',
'greater_than_equal_to' => 'Amount cannot be negative',
],
],
];
if (! $this->validate($rules)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Input validation failed',
'errors' => $this->validator->getErrors(),
]);
}
$payload = [
'client_id' => (int) ($data['client_id'] ?? 0),
'client_policy_id' => (int) ($data['client_policy_id'] ?? 0),
'description' => $data['description'] ?? null,
'approved_by' => (int) ($data['approved_by'] ?? 0),
'amount' => $data['amount'] ?? null,
'is_active' => 1,
];
$expenseDate = $data['expense_date'] ?? null;
if (! empty($expenseDate)) {
$dt = \DateTime::createFromFormat('d-m-Y', $expenseDate);
$payload['expense_date'] = $dt ? $dt->format('Y-m-d') : null;
} else {
$payload['expense_date'] = null;
}
if ($id === null) {
$insertId = $this->expenseModel->insert($payload, true);
$success = ! empty($insertId);
$id = $insertId;
$message = $success
? 'Expense created successfully'
: 'Unable to create expense. Please try again.';
} else {
$success = $this->expenseModel->update($id, $payload);
$message = $success
? 'Expense updated successfully'
: 'Unable to update expense. Please try again.';
}
return $this->response
->setStatusCode($success ? 200 : 400)
->setJSON([
'status' => (bool) $success,
'message' => $message,
'id' => $id,
]);
} catch (\Throwable $e) {
return handle_exception($e, $this->myLogger, $this->response);
}
}
/**
* Get single expense (AJAX)
*/
public function getExpense($id = null)
{
try {
$id = (int) $id;
if (empty($id)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Invalid expense id',
]);
}
$expense = $this->expenseModel
->select('
expenses.*,
clients.client_name,
clients.short_name,
client_policy.policy_no,
user_profiles.first_name AS approved_by_name
')
->join('clients', 'clients.id = expenses.client_id', 'left')
->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left')
->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left')
->where('expenses.id', $id)
->where('expenses.is_active', 1)
->first();
if (empty($expense)) {
return $this->response->setStatusCode(404)->setJSON([
'status' => false,
'message' => 'Expense not found',
]);
}
return $this->response->setStatusCode(200)->setJSON([
'status' => true,
'data' => $expense,
]);
} catch (\Throwable $e) {
return handle_exception($e, $this->myLogger, $this->response);
}
}
/**
* Soft delete expense (AJAX)
*/
public function delete($id = null)
{
try {
if ($this->request->getMethod() !== 'post') {
return $this->response
->setStatusCode(405)
->setJSON([
'status' => false,
'message' => 'Invalid request method',
]);
}
$id = (int) $id;
if (empty($id)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Invalid expense id',
]);
}
$payload = [
'is_active' => 0,
];
$success = $this->expenseModel->update($id, $payload);
return $this->response
->setStatusCode($success ? 200 : 400)
->setJSON([
'status' => (bool) $success,
'message' => $success
? 'Expense deleted successfully'
: 'Unable to delete expense. Please try again.',
]);
} catch (\Throwable $e) {
return handle_exception($e, $this->myLogger, $this->response);
}
}
/**
* Get policies by client for dropdown (AJAX)
*/
public function clientPolicies()
{
try {
$clientId = (int) ($this->request->getGet('client_id') ?? 0);
if (empty($clientId)) {
return $this->response->setStatusCode(400)->setJSON([
'status' => false,
'message' => 'Client is required',
]);
}
$policies = $this->clientPolicyModel
->select('client_policy.id, client_policy.policy_no, policy_type.policy_type')
->join('policy_type', 'policy_type.id = client_policy.policy_type_id AND policy_type.is_active = 1')
->where('client_policy.client_id', $clientId)
->where('client_policy.is_active', 1)
->orderBy('client_policy.id', 'ASC')
->findAll();
return $this->response->setStatusCode(200)->setJSON([
'status' => true,
'data' => $policies,
]);
} catch (\Throwable $e) {
return handle_exception($e, $this->myLogger, $this->response);
}
}
}

View File

@ -844,6 +844,8 @@ class MediAssistApiController extends BaseController
->get()
->getResultArray();
log_message('error','MEDI_ASSIST - Claim Status started | for ticket count: ' . count($TicketData));
// dd($TicketData);
if (!$TicketData) {
@ -890,7 +892,11 @@ class MediAssistApiController extends BaseController
if ($response['status'] != true || empty($response['data']['claimsData'][0])) {
log_message('error','MEDI_ASSIST - Claim Status FAILED | for ticket ID: ' . $claimId.' | response: '.json_encode($response));
$error_data[$claimId][['status' => false,'message' => 'API call failed.','data' => $response]];
$error_data[$claimId] = [
'response' => $response,
'body' => $body
];
continue; // skip to next ticket
}
// Extract Claim Status

View File

@ -7,6 +7,7 @@ use App\Models\SalesActualLeadModel;
use App\Models\SalesContactPersonModel;
use App\Models\SalesActivityModel;
use App\Models\SalesLeadNoteModel;
use App\Models\SalesTargetModel;
use App\Models\UserModel;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\API\ResponseTrait;
@ -20,6 +21,8 @@ class SalesController extends BaseController
protected $activityModel;
protected $noteModel;
protected $userModel;
protected $targetModel;
public function __construct()
{
@ -28,6 +31,7 @@ class SalesController extends BaseController
$this->activityModel = new SalesActivityModel();
$this->noteModel = new SalesLeadNoteModel();
$this->userModel = new UserModel();
$this->targetModel = new SalesTargetModel();
}
@ -53,55 +57,73 @@ class SalesController extends BaseController
return $this->loadLayout('sales/activity_view', $data);
}
public function loadtargets(){
$data = $this->getSalesStaffData();
$data['tab_name'] = 'Sales Team Targets';
$data['page_name'] = 'Sales Team Targets';
return $this->loadLayout('sales/target_view', $data);
}
/**
* HELPER: Fetches Sales Managers based on the logged-in user's role and branch
*/
private function getSalesStaffData(): array
{
$db = \Config\Database::connect();
$db = \Config\Database::connect();
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
$role = get_role_id();
$team_id = user_team();
$data = [
'users' => [],
'sales_manager_ids' => []
'users' => [],
'sales_manager_ids'=> [],
'sales_role' => '',
'nhance_branch_id' => null,
'assigned_ids' => [],
];
$row = $db->table('user_profiles')->select('*')
->where('is_active', 1)->where('id', $logged_user_id)
->get()->getRow();
$nhance_branch_id = $row ? $row->nhance_branch_id : null;
// Is the logged-in user a Sales Manager? (Role 4, Team 5)
if ($role == 4 && in_array(5, $team_id)) {
$row = $db->table('user_profiles')->select('*')
->where('is_active', 1)->where('id', $logged_user_id)
->get()->getRow();
$nhance_branch_id = $row ? $row->nhance_branch_id : null;
$data['nhance_branch_id']= $nhance_branch_id;
// ── Sales Manager (Role 4, Team 5) ──────────────────────────
if ($role == 4 && in_array(5, $team_id)) {
$data['sales_role'] = 'Sales Manager';
$data['sales_manager_ids'] = [$logged_user_id];
$data['users'] = [
$data['assigned_ids'] = [$logged_user_id];
$data['users'] = [
[
'id' => $row->id,
'first_name' => $row->first_name,
'nhance_branch_id' => $nhance_branch_id
'first_name' => $row->first_name,
'last_name' => $row->last_name ?? '',
'nhance_branch_id' => $nhance_branch_id,
]
];
}
// Otherwise fetch ALL sales managers in this branch
elseif (in_array($role,[1,5])) {
$data['users'] = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id')
->join('user_teams ut', 'ut.user_id = up.id')
->where('up.is_active', 1)
->where('ut.is_active', 1)
->where('up.role', 4)
->where('ut.team_id', 5)
->where('up.nhance_branch_id', $nhance_branch_id)
->get()
->getResultArray();
// ── Sales Head (Role 1 or 5) ─────────────────────────────────
} elseif (in_array($role, [1, 5])) {
$data['sales_manager_ids'] = array_column($data['users'], 'id');
$data['sales_role'] = 'Sales Head';
$data['users'] = $db->table('user_profiles up')
->select('up.id, up.first_name, up.last_name, up.nhance_branch_id')
->join('user_teams ut', 'ut.user_id = up.id')
->where('up.is_active', 1)
->where('ut.is_active', 1)
->where('up.role', 4)
->where('ut.team_id', 5)
->where('up.nhance_branch_id', $nhance_branch_id)
->get()
->getResultArray();
$ids = array_column($data['users'], 'id');
$data['sales_manager_ids'] = $ids;
$data['assigned_ids'] = $ids; // same value, both available
}
return $data;
@ -751,6 +773,138 @@ class SalesController extends BaseController
}
}
// ==================== SALES TARGET APIs ====================
/**
* Get all sales targets
* GET /api/sales/targets
*/
public function getTargets()
{
try {
$targets = $this->targetModel->findAll();
return $this->respond([
'status' => 'success',
'data' => $targets
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Get sales target by user
* GET /api/sales/targets/user/{userId}
*/
public function getTargetByUser($userId)
{
try {
$targets = $this->targetModel->where('user_id', $userId)->findAll();
return $this->respond([
'status' => 'success',
'data' => $targets
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Get sales target by FY year
* GET /api/sales/targets/fy/{fyYear}
*/
public function getTargetByFY($fyYear)
{
try {
$targets = $this->targetModel->where('fy_year', $fyYear)->findAll();
return $this->respond([
'status' => 'success',
'data' => $targets
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Create sales target
* POST /api/sales/targets
*/
public function createTarget()
{
try {
$data = $this->request->getJSON(true);
$data['created_by'] = $this->getUserId();
$data['updated_by'] = $this->getUserId();
if (!$this->targetModel->insert($data)) {
return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
}
$targetId = $this->targetModel->getInsertID();
$target = $this->targetModel->find((int)$targetId);
return $this->respondCreated([
'status' => 'success',
'message' => 'Sales target created successfully',
'data' => $target
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Update sales target
* PUT /api/sales/targets/{id}
*/
public function updateTarget($id)
{
try {
$target = $this->targetModel->find((int)$id);
if (!$target) {
return $this->failNotFound('Sales target not found');
}
$data = $this->request->getJSON(true);
$data['updated_by'] = $this->getUserId();
if (!$this->targetModel->update($id, $data)) {
return $this->fail($this->targetModel->errors(), ResponseInterface::HTTP_BAD_REQUEST);
}
$updatedTarget = $this->targetModel->find((int)$id);
return $this->respond([
'status' => 'success',
'message' => 'Sales target updated successfully',
'data' => $updatedTarget
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
/**
* Delete sales target
* DELETE /api/sales/targets/{id}
*/
public function deleteTarget($id)
{
try {
$target = $this->targetModel->find((int)$id);
if (!$target) {
return $this->failNotFound('Sales target not found');
}
$this->targetModel->delete((int)$id);
return $this->respondDeleted([
'status' => 'success',
'message' => 'Sales target deleted successfully'
]);
} catch (\Exception $e) {
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
}
}
// ==================== HELPER METHODS ====================
/**
@ -773,52 +927,23 @@ class SalesController extends BaseController
// ==================== Dashboard ====================
public function dashboard(){
$logged_user_id = get_session_userid();
$role = get_role_id();
$team_id = user_team();
public function dashboard()
{
$payload = $this->request->getGet();
$base = $this->getSalesStaffData();
$salesRole = $base['sales_role'];
$salesManagerIds = $base['sales_manager_ids'];
$userId = get_session_userid();
$db = \Config\Database::connect();
// Get branch id from users array
$nhanceBranchId = $base['users'][0]['nhance_branch_id'] ?? null;
$row = $db->table('user_profiles')
->select('*')
->where('is_active', 1)
->where('id', $logged_user_id)
->get()
->getRowArray();
$nhance_branch_id = $row ? $row['nhance_branch_id'] : null;
// dd($logged_user_id, $nhance_branch_id, $role, $team_id );
if (in_array($role,[1,5])) {
$sales_manager_ids = array_column(
$db->table('user_profiles up')
->select('up.id')
->join('user_teams ut', 'ut.user_id = up.id')
->where([
'up.is_active' => 1,
'ut.is_active' => 1,
'up.role' => 4,
'ut.team_id' => 5,
'up.nhance_branch_id' => $nhance_branch_id
])
->get()
->getResultArray(),
'id'
);
$this->branchLevelDashboard($nhance_branch_id,$sales_manager_ids);
}
elseif ($role == 4 && in_array(5, $team_id)) {
$sales_manager_ids = [$logged_user_id];
$this->salesManagerLevelDashboard($logged_user_id,$sales_manager_ids, $payload);
if ($salesRole === 'Sales Head') {
$this->branchLevelDashboard($nhanceBranchId, $salesManagerIds);
} elseif ($salesRole === 'Sales Manager') {
$this->salesManagerLevelDashboard($userId, $salesManagerIds,$payload);
}
}
}
public function branchLevelDashboard($branchId,$sales_manager_ids)
{
@ -1001,7 +1126,7 @@ class SalesController extends BaseController
->findAll();
$data = [
'target' => $targetAmount,
'target_amt' => $targetAmount,
'achieved' => $achievedAmount,
'remaining' => $remainingAmount,
'percent' => $achievementPercent,
@ -1093,9 +1218,23 @@ class SalesController extends BaseController
/* ---------------- SUMMARY ---------------- */
$summary = ucfirst($input['activity_type']) .
' with ' .
$lead_data['company_name'];
$activityType = ucfirst($input['activity_type']);
$prepositionMap = [
'Email' => 'to',
'Call' => 'with',
'Meeting' => 'with',
'Visit' => 'to',
'Demo' => 'with',
'Share Docs' => 'to',
'To Do' => 'for'
];
$preposition = $prepositionMap[$activityType] ?? 'with';
$summary = "Activity scheduled : {$activityType} {$preposition} {$lead_data['company_name']}";
/* ---------------- GOOGLE PAYLOAD ---------------- */

View File

@ -30,8 +30,9 @@ use App\Models\TicketMasterModel;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Style\Fill;
class TicketServiceController extends BaseController
class TicketServiceController extends AdminController
{
use ResponseTrait;
@ -1840,39 +1841,86 @@ class TicketServiceController extends BaseController
{
}
// --------------------------------------------------------------------------------------------------------------------------------
public function tpaClaimDumpImporter($params)
/**
* Resolve and validate Claim Dump file metadata and physical file for TPA imports.
*
* @param int|null $fileId
* @return array{status:bool,message?:string,fileData?:array,filePath?:string}
*/
private function resolveTpaClaimDumpFile(?int $fileId): array
{
$file_id = $params['file_id'] ?? null; // Move outside try to ensure catch can see it
if (empty($fileId)) {
return [
'status' => false,
'message' => 'File ID is missing',
];
}
$fileData = $this->claimDumpFileModel->find((int) $fileId);
if (!$fileData) {
return [
'status' => false,
'message' => 'Invalid file ID. No file data found',
];
}
$filePath = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR . $fileData['file_name'];
if (!is_file($filePath)) {
return [
'status' => false,
'message' => 'Claim dump file not found',
];
}
return [
'status' => true,
'fileData' => $fileData,
'filePath' => $filePath,
];
}
/**
* First TPA-wise job parse Excel and push data into TPA staging table.
*
* @param array $params
* @return array
*/
public function tpaClaimDumpImporter(array $params)
{
$file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
try {
$file_path = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'claim_dump_excel' . DIRECTORY_SEPARATOR;
$resolved = $this->resolveTpaClaimDumpFile($file_id);
if (!$file_id) {
return ['status' => false, 'message' => 'File ID is missing'];
if ($resolved['status'] === false) {
return [
'status' => false,
'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
];
}
$fileData = $this->claimDumpFileModel->find((int)$file_id);
if (!$fileData) {
return ['status' => false, 'message' => 'Invalid file ID. No file data found'];
}
$file_full_path = $file_path . $fileData['file_name'];
if (!is_file($file_full_path)) {
return ['status' => false, 'message' => 'Claim dump file not found'];
}
$fileData = $resolved['fileData'];
$filePath = $resolved['filePath'];
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
$result = $handler->runTpaClaimDumpInsert($file_full_path, $file_id);
$result = $handler->runTpaClaimDumpInsert($filePath, $file_id);
if (!empty($result['status']) && $result['status'] === true) {
Jobs::addJob(['job_name' => 'tpaClaimDumpToTicketMasterImporters', 'payload' => ['file_id' => $file_id]]);
Jobs::addJob([
'job_name' => 'tpaClaimDumpToTicketMasterImporters',
'payload' => ['file_id' => $file_id],
]);
} else {
// FORCE FAIL LOGIC
$this->markAsFailed($file_id, $result['message'] ?? 'System error contact admin', $fileData['created_by'] ?? null);
$this->markAsFailed(
$file_id,
$result['message'] ?? 'System error contact admin',
$fileData['created_by'] ?? null
);
}
return $result;
@ -1884,9 +1932,9 @@ class TicketServiceController extends BaseController
}
return [
'status' => false,
'message' => 'TPA Claim dump import failed',
'error_data' => $th->getMessage()
'status' => false,
'message' => 'TPA Claim dump import failed',
'error_data' => $th->getMessage(),
];
}
}
@ -1908,30 +1956,37 @@ class TicketServiceController extends BaseController
return $this->claimDumpFileModel->update($file_id, $data);
}
public function tpaClaimDumpToTicketMasterImporters($params)
/**
* Second TPA-wise job move data from TPA staging into ticket_master.
*
* @param array $params
* @return array
*/
public function tpaClaimDumpToTicketMasterImporters(array $params)
{
$file_id = $params['file_id'] ?? null;
$file_id = isset($params['file_id']) ? (int) $params['file_id'] : null;
$fileData = null;
try {
if (!$file_id) {
return ['status' => false, 'message' => 'File ID is missing'];
$resolved = $this->resolveTpaClaimDumpFile($file_id);
if ($resolved['status'] === false) {
return [
'status' => false,
'message' => $resolved['message'] ?? 'Unable to resolve claim dump file',
];
}
$fileData = $this->claimDumpFileModel->where('id', $file_id)->first();
if (!$fileData) {
return ['status' => false, 'message' => 'Invalid file ID. No file data found to import'];
}
$fileData = $resolved['fileData'];
$handler = TpaClaimsImportFactory::make($fileData['tpa_id'] ?? 0);
$result = $handler->runTicketMasterInsert($params);
$result = $handler->runTicketMasterInsert($params);
if (!empty($result['status']) && $result['status'] === true) {
// Success: Update the status to success
$this->claimDumpFileModel->update($file_id, [
'status' => 'success',
'reason' => null
'reason' => null,
]);
} else {
// Logic failure: The runTicketMasterInsert returned status false
@ -1957,8 +2012,8 @@ class TicketServiceController extends BaseController
'message' => $th->getMessage(),
'error_data' => [
'line' => $th->getLine(),
'file' => $th->getFile()
]
'file' => $th->getFile(),
],
];
}
}
@ -2011,5 +2066,176 @@ class TicketServiceController extends BaseController
return $data;
}
public function getTpaClaimDumpErrorData($file_id)
{
$file_id = (int) $file_id;
// Ensure we always have a Response object, even if controller was instantiated manually
$response = $this->response ?? service('response');
if ($file_id <= 0) {
return $response
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
->setJSON(['status' => false, 'message' => 'Invalid file id']);
}
$fileData = $this->claimDumpFileModel->find($file_id);
if (!$fileData) {
return $response
->setStatusCode(ResponseInterface::HTTP_NOT_FOUND)
->setJSON(['status' => false, 'message' => 'File record not found']);
}
$tpaId = (int) ($fileData['tpa_id'] ?? 0);
if ($tpaId <= 0) {
return $response
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
->setJSON(['status' => false, 'message' => 'TPA not linked with this file']);
}
// Resolve TPA staging table based on configured TPA IDs
$tableName = match ($tpaId) {
(int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'claims_dump_vidal',
(int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'claims_dump_abhi',
(int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'claims_dump_medi_assist',
(int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'claims_dump_fhpl',
(int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'claims_dump_reliance',
(int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'claims_dump_icici',
default => null,
};
if ($tableName === null) {
return $response
->setStatusCode(ResponseInterface::HTTP_BAD_REQUEST)
->setJSON(['status' => false, 'message' => 'Unsupported TPA for error dump export']);
}
$db = \Config\Database::connect();
$builder = $db->table($tableName);
// Fetch only records belonging to this file and having a rejection reason
$rows = $builder
->where('file_id', $file_id)
->where('is_active', 1)
->where('master_reject_reason IS NOT NULL', null, false)
->get()
->getResultArray();
if (empty($rows)) {
// No error records return a small Excel file with just a message
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Errors');
$sheet->setCellValue('A1', 'Message');
$sheet->setCellValue('A2', 'No rejected records found for this file.');
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
ob_start();
$writer->save('php://output');
$excelOutput = ob_get_clean();
$filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
return $response
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setHeader('Cache-Control', 'max-age=0')
->setBody($excelOutput);
}
// Columns that must NOT be included in the Excel export
$excludedColumns = [
'id',
'client_id',
'client_policy_id',
'ticket_id',
'file_id',
'created_at',
'updated_at',
'created_by',
'updated_by',
'is_active',
];
$firstRow = $rows[0];
// Build header mapping and track the "Rejected Reason" column index
$dbColumnOrder = [];
$displayHeaderLabels = [];
$rejectedReasonColIdx = null; // 1-based index for Excel column
foreach ($firstRow as $columnName => $_) {
if (in_array($columnName, $excludedColumns, true)) {
continue;
}
$dbColumnOrder[] = $columnName;
if ($columnName === 'master_reject_reason' || $columnName === 'master_rejection_reason_key') {
$displayHeaderLabels[] = 'Rejected Reason';
$rejectedReasonColIdx = count($displayHeaderLabels); // current column index (1-based)
} else {
$displayHeaderLabels[] = $columnName;
}
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('Errors');
// Header row
$rowIndex = 1;
foreach ($displayHeaderLabels as $colIndex => $headerLabel) {
$columnLetter = Coordinate::stringFromColumnIndex($colIndex + 1);
$cellAddress = $columnLetter . $rowIndex;
$sheet->setCellValue($cellAddress, $headerLabel);
}
// Data rows
$rowIndex = 2;
foreach ($rows as $row) {
foreach ($dbColumnOrder as $i => $columnName) {
$colIndex = $i + 1;
$columnLetter = Coordinate::stringFromColumnIndex($colIndex);
$cellAddress = $columnLetter . $rowIndex;
$value = $row[$columnName] ?? null;
$sheet->setCellValue($cellAddress, $value);
// Highlight the "Rejected Reason" values
if ($rejectedReasonColIdx !== null && $colIndex === $rejectedReasonColIdx) {
$sheet->getStyle($cellAddress)
->getFill()
->setFillType(Fill::FILL_SOLID)
->getStartColor()
->setARGB('FFFFF4B2'); // light yellow
}
}
$rowIndex++;
}
// Autosize columns for better readability
$highestColumnIndex = count($displayHeaderLabels);
for ($col = 1; $col <= $highestColumnIndex; $col++) {
$columnLetter = Coordinate::stringFromColumnIndex($col);
$sheet->getColumnDimension($columnLetter)->setAutoSize(true);
}
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
ob_start();
$writer->save('php://output');
$excelOutput = ob_get_clean();
$filename = 'tpa_claim_dump_errors_' . $file_id . '.xlsx';
return $response
->setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setHeader('Cache-Control', 'max-age=0')
->setBody($excelOutput);
}
}

View File

@ -567,6 +567,8 @@ class VidalApiController extends BaseController
// dd($TicketData);
log_message('error', "VIDAL - Claim Status | Total tickets fetched for status update: " . count($TicketData));
if (!$TicketData) {
log_message('error', "VIDAL - Claim Status | Claims not found to update status");
return $this->response->setJSON(['status' => false,'message' => 'Claims not found' ]);
@ -599,7 +601,12 @@ class VidalApiController extends BaseController
if ($response['status'] != true || empty($response['data']['data']['claims'][0])) {
log_message('error', 'VIDAL - Claim Status FAILED | for ticket ID: ' . $claimId.' | response: '.json_encode($response));
$error_data[$claimId][['status' => false,'message' => 'API call failed.','data' => $response]];
$error_data[$claimId] = [
'status' => false,
'message' => 'API call failed.',
'data' => $response
];
continue;
}
// Extract claim status

View File

@ -766,18 +766,18 @@ if(!function_exists('query_construct_for_emp_policy_id')){
$gst_alise = '';
}
$subQuery .= "SELECT " . intval($index) . $row_id_alise;
$subQuery .= "'" . $employee[$col_index['emp_name']] . "'" . $emp_name_alise;
$subQuery .= "'" . $employee[$col_index['emp_code']] . "'" . $emp_code_alise;
$subQuery .= "'" . $employee[$col_index['emp_dob']] . "'" . $emp_dob_alise;
$subQuery .= "'" . $employee[$col_index['emp_gender']] . "'" . $emp_gender_alise;
$subQuery .= "'" . $employee[$col_index['pre_existing_alignments']] . "'" . $pre_existing_alignments_alise;
$subQuery .= "'" . $employee[$col_index['basic_cover_si']] . "'" . $basic_cover_si_alise;
$subQuery .= "'" . $employee[$col_index['emp_relationship']] . "'" . $emp_relationship_alise;
$subQuery .= "'" . $employee[$col_index['policy_end_date']] . "'" . $policy_end_date_alise;
$subQuery .= "'" . $employee[$col_index['days']] . "'" . $days_alise;
$subQuery .= "'" . $employee[$col_index['premium']] . "'" . $premium_alise;
$subQuery .= "'" . $employee[$col_index['rata_premimum']] . "'" . $rata_premimum_alise;
$subQuery .= "'" . $employee[$col_index['gst']] . "'" . $gst_alise;
$subQuery .= $db->escape($employee[$col_index['emp_name']]) . $emp_name_alise;
$subQuery .= $db->escape($employee[$col_index['emp_code']]) . $emp_code_alise;
$subQuery .= $db->escape($employee[$col_index['emp_dob']]) . $emp_dob_alise;
$subQuery .= $db->escape($employee[$col_index['emp_gender']]) . $emp_gender_alise;
$subQuery .= $db->escape($employee[$col_index['pre_existing_alignments']]) . $pre_existing_alignments_alise;
$subQuery .= $db->escape($employee[$col_index['basic_cover_si']]) . $basic_cover_si_alise;
$subQuery .= $db->escape($employee[$col_index['emp_relationship']]) . $emp_relationship_alise;
$subQuery .= $db->escape($employee[$col_index['policy_end_date']]) . $policy_end_date_alise;
$subQuery .= $db->escape($employee[$col_index['days']]) . $days_alise;
$subQuery .= $db->escape($employee[$col_index['premium']]) . $premium_alise;
$subQuery .= $db->escape($employee[$col_index['rata_premimum']]) . $rata_premimum_alise;
$subQuery .= $db->escape($employee[$col_index['gst']]) . $gst_alise;
}
// Complete the query
@ -893,18 +893,18 @@ if(!function_exists('query_construct_second_stage')){
$gst_alise = '';
}
$subQuery .= "SELECT " . intval($index) . $row_id_alise;
$subQuery .= "'" . $employee[$col_index['emp_name']] . "'" . $emp_name_alise;
$subQuery .= "'" . $employee[$col_index['emp_code']] . "'" . $emp_code_alise;
$subQuery .= "'" . $employee[$col_index['emp_dob']] . "'" . $emp_dob_alise;
$subQuery .= "'" . $employee[$col_index['emp_gender']] . "'" . $emp_gender_alise;
$subQuery .= "'" . $employee[$col_index['pre_existing_alignments']] . "'" . $pre_existing_alignments_alise;
$subQuery .= "'" . $employee[$col_index['basic_cover_si']] . "'" . $basic_cover_si_alise;
$subQuery .= "'" . $employee[$col_index['emp_relationship']] . "'" . $emp_relationship_alise;
$subQuery .= "'" . $employee[$col_index['policy_end_date']] . "'" . $policy_end_date_alise;
$subQuery .= "'" . $employee[$col_index['days']] . "'" . $days_alise;
$subQuery .= "'" . $employee[$col_index['premium']] . "'" . $premium_alise;
$subQuery .= "'" . $employee[$col_index['rata_premimum']] . "'" . $rata_premimum_alise;
$subQuery .= "'" . $employee[$col_index['gst']] . "'" . $gst_alise;
$subQuery .= $db->escape($employee[$col_index['emp_name']]) . $emp_name_alise;
$subQuery .= $db->escape($employee[$col_index['emp_code']]) . $emp_code_alise;
$subQuery .= $db->escape($employee[$col_index['emp_dob']]) . $emp_dob_alise;
$subQuery .= $db->escape($employee[$col_index['emp_gender']]) . $emp_gender_alise;
$subQuery .= $db->escape($employee[$col_index['pre_existing_alignments']]) . $pre_existing_alignments_alise;
$subQuery .= $db->escape($employee[$col_index['basic_cover_si']]) . $basic_cover_si_alise;
$subQuery .= $db->escape($employee[$col_index['emp_relationship']]) . $emp_relationship_alise;
$subQuery .= $db->escape($employee[$col_index['policy_end_date']]) . $policy_end_date_alise;
$subQuery .= $db->escape($employee[$col_index['days']]) . $days_alise;
$subQuery .= $db->escape($employee[$col_index['premium']]) . $premium_alise;
$subQuery .= $db->escape($employee[$col_index['rata_premimum']]) . $rata_premimum_alise;
$subQuery .= $db->escape($employee[$col_index['gst']]) . $gst_alise;
}
// Complete the query

View File

@ -65,12 +65,23 @@ class DataServiceSqlite
$this->sqliteDb->exec($sql);
}
private const ALLOWED_TABLES = ['http', 'log'];
public function insertData(array $data, string $tableName): bool
{
log_message('error', 'SQLite insert called: ');
if (!in_array($tableName, self::ALLOWED_TABLES, true)) {
log_message('error', 'SQLite insert rejected: invalid table name: ' . $tableName);
return false;
}
unset($data['context']);
$columns = implode(', ', array_keys($data));
// print_r($columns);
$safeColumns = array_map(function ($col) {
return preg_replace('/[^a-zA-Z0-9_]/', '', $col);
}, array_keys($data));
$columns = implode(', ', $safeColumns);
$placeholders = implode(', ', array_fill(0, count($data), '?'));
$sql = "INSERT INTO $tableName ($columns) VALUES ($placeholders)";

View File

@ -119,6 +119,19 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
'Settled' => 11,
'Rejected' => 8,
'Cancelled' => 13,
'Approved' => 9,
'Closed' => 12,
'Query' => 4,
'Required Information' => 4,
'In-Progress' => 5,
'Paid' => 11,
'Denied' => 8,
'Cancelled' => 13,
'Processed' => 61,
'Information Awaited' => 4,
'Denied Letter Sent' => 66,
'Cashless Document Awaited' => 3,
'RI Cancelled' => 13,
];
protected $dateColumns = [
@ -164,14 +177,30 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
$rows = $this->db->table('claims_dump_abhi cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
if (empty($rows)) {
return true;
}
$builder = $this->db->table('claims_dump_abhi');
return $builder->updateBatch($data, 'id');
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_abhi')->updateBatch($updateData, 'id') !== false;
}
@ -329,7 +358,7 @@ class AbhiClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']);
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;

View File

@ -81,7 +81,7 @@ abstract class BaseTpaClaimImportService
$this->db->transBegin();
try {
$file_id = $params['file_id'];
$file_id = $params['file_id'];
$ticketMasterData = $this->mapClaimMasterData($file_id);
// Check if mapping failed
@ -101,6 +101,12 @@ abstract class BaseTpaClaimImportService
$this->db->transRollback();
return ['status' => false, 'message' => 'Ticket Master Claim bulk insert failed'];
}
// Map newly created ticket IDs back to the TPA staging table
if (!$this->updateTicketIdInTPATable($file_id)) {
$this->db->transRollback();
return ['status' => false, 'message' => 'Updating ticket_id in TPA table failed'];
}
$message .= 'Ticket Master Claim bulk insert success. ';
$hasExecutedTask = true;
}else{
@ -328,6 +334,16 @@ abstract class BaseTpaClaimImportService
return $employeeData ?? [];
}
public function checkStatusMapping($statusArray, $statusString)
{
foreach ($statusArray as $key => $value) {
if (strtolower($statusString) == strtolower($key) || strtolower($statusString) == strtolower(trim($key))) {
return $value;
}
}
return null;
}
/**
* Map Excel rows to TPA table structure
*/
@ -349,9 +365,9 @@ abstract class BaseTpaClaimImportService
abstract protected function importClaimMaster(array $data): bool;
/**
* Update TPA table with ticket_master primary key
* Update TPA table with ticket_master primary key for a given file.
*/
abstract protected function updateTicketIdInTPATable(): bool;
abstract protected function updateTicketIdInTPATable(int $fileId): bool;
/**
* Update TPA table with ticket_master insert rejected reason

View File

@ -181,6 +181,13 @@ class FhplClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'Settled' => 11,
'Rejected' => 8,
'Paid' => 11,
'Approved' => 9,
'Closed' => 12,
'Under Process' => 5,
'Query' => 4,
'Required Information' => 4,
'In-Progress' => 5,
];
protected $dateColumns = [
@ -235,14 +242,31 @@ class FhplClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
// Join ticket_master with FHPL staging on file_id + claim_dump_ref_id -> id
$rows = $this->db->table('claims_dump_fhpl cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
if (empty($rows)) {
return true;
}
$builder = $this->db->table('claims_dump_fhpl');
return $builder->updateBatch($data, 'id');
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_fhpl')->updateBatch($updateData, 'id') !== false;
}
@ -400,7 +424,7 @@ class FhplClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
$item['claim_status_id'] = $statusMapping[$row['current_claim_status']] ?? 61;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['current_claim_status']) ?? 61;
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;

View File

@ -106,7 +106,13 @@ class IciciClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'PAID' => 11,
'SETTLED' => 11,
'REJECTED' => 8,
'APPROVED' => 9,
'CLOSED' => 12,
'QUERY' => 4,
'REQUIRED INFORMATION' => 4,
'IN-PROGRESS' => 5,
];
protected $dateColumns = [
@ -152,14 +158,30 @@ class IciciClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
$rows = $this->db->table('claims_dump_icici cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
if (empty($rows)) {
return true;
}
$builder = $this->db->table('claims_dump_icici');
return $builder->upsertBatch($data, 'id');
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_icici')->updateBatch($updateData, 'id') !== false;
}
@ -287,7 +309,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
$item['tpa_id'] = $client_policy_data['tpa_id'] ?? null;
$item['acm_id'] = $client_policy_data['acm_id'] ?? null;
$item['policy_no'] = $client_policy_data['policy_no'] ?? null;
$item['relationship'] = $this->convertRelation($row['relation_group'] ?? '');
$item['relationship'] = $this->convertRelation($row['relation'] ?? '');
$employee_data = $this->getEmployeeDetails($file_data['client_id'], $file_data['client_policy_id'], $row['employee_member_id'], $item['relationship']);
@ -317,7 +339,7 @@ class IciciClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
$item['claim_status_id'] = $statusMapping[$row['updated_status']] ?? 61;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['updated_status']) ?? 61;
$item['claim_dump_ref_id'] = $row['id'];
$item['file_id'] = $file_id;
$item['created_by'] = $file_data['created_by'] ?? null;

View File

@ -158,12 +158,15 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
protected $statusMapping = [
'Settled' => 11,
'Rejected' => 8,
'Paid' => 11,
'Denied' => 8,
'Cancelled' => 13,
'Processed' => 61,
'Information Awaited' => 4,
'Denied Letter Sent' => 66,
'Cashless Document Awaited' => 3,
'Approved' => 9,
'Closed' => 12,
];
/**
@ -192,16 +195,30 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
}
$builder = $this->db->table('claims_dump_medi_assist');
$builder->insertBatch($data);
$rows = $this->db->table('claims_dump_medi_assist cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
return true;
if (empty($rows)) {
return true;
}
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_medi_assist')->updateBatch($updateData, 'id') !== false;
}
@ -356,8 +373,8 @@ class MediAssistClaimImportService extends BaseTpaClaimImportService
// Meta fields
$item['claim_dump_ref_id'] = $row['id'];
$item['file_id'] = $file_id;
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
$item['file_id'] = $file_id;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
$item['created_by'] = $file_data['created_by'] ?? null;
$item['claim_type'] = 1;
$item['priority'] = 1;

View File

@ -96,14 +96,15 @@ class RcareClaimImportService extends BaseTpaClaimImportService
];
protected $statusMapping = [
'CL Paid with Settlement Letter' => 11,
'Settled' => 11,
'Paid' => 11,
'Rejected' => 8,
'Denied' => 8,
'Cancelled' => 13,
'Processed' => 61,
'Information Awaited' => 4,
'Denied Letter Sent' => 66,
'Cashless Document Awaited' => 3,
'Approved' => 9,
'Closed' => 12,
'CL Rejected' => 8,
'CL Approved' => 9,
'AL Closed' => 12,
];
protected $dateColumns = [
@ -143,14 +144,30 @@ class RcareClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
$rows = $this->db->table('claims_dump_reliance cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
if (empty($rows)) {
return true;
}
$builder = $this->db->table('claims_dump_reliance');
return $builder->updateBatch($data, 'id');
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_reliance')->updateBatch($updateData, 'id') !== false;
}
@ -307,7 +324,7 @@ class RcareClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
$item['claim_status_id'] = $statusMapping[$row['final_status']] ?? 61;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['final_status']) ?? 61;
$item['file_id'] = $file_id;
$item['created_by'] = $file_data['created_by'] ?? null;
$item['claim_dump_ref_id'] = $row['id'];

View File

@ -349,10 +349,17 @@ class VidalClaimImportService extends BaseTpaClaimImportService
];
protected $statusMapping = [
'CL Paid with Settlement Letter' => 11,
'CL Rejected' => 8,
'CL Approved' => 9,
'AL Closed' => 12,
'Settled' => 11,
'Rejected' => 8,
'Paid' => 11,
'Denied' => 8,
'Cancelled' => 13,
'Processed' => 61,
'Information Awaited' => 4,
'Denied Letter Sent' => 66,
'Cashless Document Awaited' => 3,
'Approved' => 9,
'Closed' => 12,
];
@ -382,14 +389,30 @@ class VidalClaimImportService extends BaseTpaClaimImportService
}
public function updateTicketIdInTPATable(): bool
public function updateTicketIdInTPATable(int $fileId): bool
{
if (empty($data)) {
return false;
$rows = $this->db->table('claims_dump_vidal cd')
->select('cd.id, tm.id AS ticket_id')
->join('ticket_master tm', 'tm.claim_dump_ref_id = cd.id AND tm.file_id = cd.file_id', 'inner')
->where('cd.is_active', 1)
->where('cd.file_id', $fileId)
->where('cd.ticket_id IS NULL')
->get()
->getResultArray();
if (empty($rows)) {
return true;
}
$builder = $this->db->table('claims_dump_vidal');
return $builder->updateBatch($data, 'id');
$updateData = [];
foreach ($rows as $row) {
$updateData[] = [
'id' => $row['id'],
'ticket_id' => $row['ticket_id'],
];
}
return $this->db->table('claims_dump_vidal')->updateBatch($updateData, 'id') !== false;
}
@ -542,7 +565,7 @@ class VidalClaimImportService extends BaseTpaClaimImportService
}
// Meta fields
$item['claim_status_id'] = $statusMapping[$row['claim_status']] ?? 61;
$item['claim_status_id'] = $this->checkStatusMapping($this->statusMapping, $row['claim_status']) ?? 61;
$item['file_id'] = $file_id;
$item['claim_dump_ref_id'] = $row['id'];
$item['created_by'] = $file_data['created_by'] ?? null;

View File

@ -228,8 +228,14 @@ class ClientModel extends Model
return $result;
}
private const ALLOWED_CONTACT_FIELDS = ['email', 'mobile', 'phone', 'name', 'contact_name', 'contact_email', 'contact_mobile'];
public function isDuplicateByClientBranch($value, $field, $clientId, $branchId)
{
if (!in_array($field, self::ALLOWED_CONTACT_FIELDS, true)) {
throw new \InvalidArgumentException("Invalid field name: {$field}");
}
$builder = $this->db->table('level_contacts lc')
->select('lc.id')
->join('client_branch cb', 'lc.ref_id = cb.id', 'left')

View File

@ -1146,6 +1146,8 @@ class EmployeePolicyModel extends Model
$endorsement_condition = "{$insurer_or_tpa}" === 'tpa' ? "AND (a.endorsement_id IS NULL OR a.endorsement_id = '')" : "AND (a.endorsement_id IS NULL OR a.endorsement_id = '')";
$add_one_day = (int)$add_one_day;
$query = $this->db->query("
SELECT DISTINCT
a.id as endorsement_primarykey,
@ -1688,30 +1690,29 @@ class EmployeePolicyModel extends Model
public function bulkUpdate($emp_details)
{
// Extract IDs, tpa_ids, and uhids
$ids = array_column($emp_details, 'id');
$ids = array_map('intval', array_column($emp_details, 'id'));
$tpa_ids = array_column($emp_details, 'tpa_id');
$uhids = array_column($emp_details, 'uhid');
// Escape values for SQL
$escapedIds = array_map([$this->db, 'escape'], $ids);
// Escape string values for SQL
$escapedTpaIds = array_map([$this->db, 'escape'], $tpa_ids);
$escapedUhids = array_map([$this->db, 'escape'], $uhids);
// Construct the CASE statements
// Construct the CASE statements with int-cast IDs
$caseTpaId = array_map(function($id, $tpa_id) {
return "WHEN id = $id THEN $tpa_id";
}, $escapedIds, $escapedTpaIds);
return "WHEN id = {$id} THEN {$tpa_id}";
}, $ids, $escapedTpaIds);
$caseUhid = array_map(function($id, $uhid) {
return "WHEN id = $id THEN $uhid";
}, $escapedIds, $escapedUhids);
return "WHEN id = {$id} THEN {$uhid}";
}, $ids, $escapedUhids);
// Convert cases to a string
$caseTpaIdString = implode(' ', $caseTpaId);
$caseUhidString = implode(' ', $caseUhid);
// Convert ids to a string
$idsString = implode(', ', $escapedIds);
$idsString = implode(', ', $ids);
// Construct the SQL query
$sql = "
@ -1837,18 +1838,19 @@ class EmployeePolicyModel extends Model
$groupKeys = [];
foreach ($endorsement_details as $row) {
$groupKey = $this->db->escape($row['group_key']);
$groupKey = (int)$row['group_key'];
$endorsementId = $this->db->escape($row['endorsement_id']);
$caseParts[] = "WHEN group_key = {$groupKey} THEN {$endorsementId}";
$groupKeys[] = $groupKey;
}
$groupKeysStr = implode(', ', $groupKeys);
$sql = "
UPDATE emp_endorsement
SET
endorsement_id = CASE " . implode(' ', $caseParts) . " END,
status = 'complete'
WHERE group_key IN (" . implode(', ', $groupKeys) . ")
WHERE group_key IN ({$groupKeysStr})
";
$this->db->query($sql);
@ -1866,26 +1868,37 @@ class EmployeePolicyModel extends Model
public function bulkUpdateForCorrection($emp_details){
$ids = [];
$caseStatements = [];
$employeeTableFields = $this->db->getFieldNames('employees');
foreach ($emp_details as $employee) {
$id = $this->db->escape($employee['id']);
$id = (int)$employee['id'];
$ids[] = $id;
foreach ($employee as $field => $value) {
if ($field === 'id') continue;
$safeField = preg_replace('/[^a-zA-Z0-9_]/', '', $field);
if (!in_array($safeField, $employeeTableFields, true)) {
continue;
}
$escapedValue = $this->db->escape($value);
if (!isset($caseStatements[$field])) {
$caseStatements[$field] = [];
if (!isset($caseStatements[$safeField])) {
$caseStatements[$safeField] = [];
}
$caseStatements[$field][] = "WHEN id = $id THEN $escapedValue";
$caseStatements[$safeField][] = "WHEN id = {$id} THEN {$escapedValue}";
}
}
// Construct the CASE strings
$caseStrings = [];
foreach ($caseStatements as $field => $cases) {
$caseStrings[] = "$field = CASE " . implode(' ', $cases) . " END";
$caseStrings[] = "{$field} = CASE " . implode(' ', $cases) . " END";
}
// Convert ids to a string
@ -1941,7 +1954,7 @@ class EmployeePolicyModel extends Model
->getRowArray();
// Determine if one day should be added for deletion
$add_one_day = (!empty($result) && $result['deletion_add_day'] == 1) ? 1 : 0;
$add_one_day = (int)((!empty($result) && $result['deletion_add_day'] == 1) ? 1 : 0);
$query = $this->db->query("
@ -2130,14 +2143,17 @@ class EmployeePolicyModel extends Model
$caseEndorsementId = "CASE ";
$ids = [];
foreach ($array as $item) {
$caseStatus .= "WHEN id = {$item['id']} THEN '{$item['status']}' ";
$caseEndorsementId .= "WHEN id = {$item['id']} THEN '{$item['endorsement_id']}' ";
$ids[] = $item['id'];
$safeId = (int)$item['id'];
$safeStatus = $this->db->escape($item['status']);
$safeEndorsementId = $this->db->escape($item['endorsement_id']);
$caseStatus .= "WHEN id = {$safeId} THEN {$safeStatus} ";
$caseEndorsementId .= "WHEN id = {$safeId} THEN {$safeEndorsementId} ";
$ids[] = $safeId;
}
$caseStatus .= "END";
$caseEndorsementId .= "END";
$ids = implode(',', $ids);
$query = "UPDATE emp_endorsement SET status = $caseStatus, endorsement_id = $caseEndorsementId WHERE id IN ($ids);"; // Execute the query
$query = "UPDATE emp_endorsement SET status = $caseStatus, endorsement_id = $caseEndorsementId WHERE id IN ($ids);";
$this->db->query($query);
}

View File

@ -0,0 +1,73 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class ExpenseModel extends Model
{
protected $table = 'expenses';
protected $primaryKey = 'id';
protected $DBGroup = 'default';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
'id',
'client_id',
'client_policy_id',
'description',
'approved_by',
'amount',
'expense_date',
'created_at',
'created_by',
'updated_at',
'updated_by',
'is_active',
];
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = ['setCreatedBy'];
protected $afterInsert = [];
protected $beforeUpdate = ['setUpdatedBy'];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
protected function setCreatedBy(array $data): array
{
if (empty($data['data']['created_by'])) {
$data['data']['created_by'] = get_session_userid();
}
if (empty($data['data']['created_at'])) {
$data['data']['created_at'] = date('Y-m-d H:i:s');
}
if (! isset($data['data']['is_active'])) {
$data['data']['is_active'] = 1;
}
return $data;
}
protected function setUpdatedBy(array $data): array
{
if (empty($data['data']['updated_by'])) {
$data['data']['updated_by'] = get_session_userid();
}
if (empty($data['data']['updated_at'])) {
$data['data']['updated_at'] = date('Y-m-d H:i:s');
}
return $data;
}
}

View File

@ -127,6 +127,22 @@
return $data;
}
private const ALLOWED_DATE_TYPES = [
'policy_issue_date',
'policy_start_date',
'policy_end_date',
'created_at',
'updated_at',
];
private function validateDateType($date_type)
{
if (!in_array($date_type, self::ALLOWED_DATE_TYPES, true)) {
throw new \InvalidArgumentException("Invalid date_type: {$date_type}");
}
return $date_type;
}
// BDS Report OLD Functin for QUERY
// public function getBDSReportList($client_id, $policy_id, $branch_id, $issuer)
public function getBDSReportListOld($start_date = 0, $end_date = 0, $client_id = 0, $insurer_id = 0, $policy_type_id = 0, $date_type = 0, $issuer = 0)
@ -282,11 +298,12 @@
// Check if the start date and end date are provided
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
} else {
// $fromDate = date('Y-m-d', strtotime('-30 days'));
@ -835,18 +852,16 @@
// Check if the start date and end date are provided
if ($start_date != 0 && $end_date != 0 && $date_type != 0 && $date_type != 'statement_month') {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
// $builder->where('policy_transaction.' . $date_type . '>=', $startDate)
// ->where('policy_transaction.' . $date_type . '<=', $endDate);
if($date_type == "policy_issue_date"){
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
}
@ -1064,6 +1079,7 @@
// Optimize Date Filtering
if (!empty($start_date) && !empty($end_date) && !empty($date_type)) {
$this->validateDateType($date_type);
$startDate = change_date_format($start_date, 'd/m/Y', 'Y-m-d 00:00:00');
$endDate = change_date_format($end_date, 'd/m/Y', 'Y-m-d 23:59:59');
@ -1165,6 +1181,7 @@
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = change_date_format($start_date, 'd/m/Y', 'Y-m-d 00:00:00');
$endDate = change_date_format($end_date, 'd/m/Y', 'Y-m-d 23:59:59');
@ -1172,8 +1189,8 @@
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
} else {
@ -1349,18 +1366,16 @@
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
// $builder->where('policy_transaction.' . $date_type . '>=', $startDate)
// ->where('policy_transaction.' . $date_type . '<=', $endDate);
if($date_type == "policy_issue_date"){
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
} else {
@ -1463,18 +1478,16 @@
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
// $builder->where('policy_transaction.' . $date_type . '>=', $startDate)
// ->where('policy_transaction.' . $date_type . '<=', $endDate);
if($date_type == "policy_issue_date"){
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
} else {
@ -1574,18 +1587,16 @@
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
// $builder->where('policy_transaction.' . $date_type . '>=', $startDate)
// ->where('policy_transaction.' . $date_type . '<=', $endDate);
if($date_type == "policy_issue_date"){
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
} else {
@ -1689,18 +1700,16 @@
// Date range filtering
if ($start_date != 0 && $end_date != 0 && $date_type != 0) {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
// $builder->where('policy_transaction.' . $date_type . '>=', $startDate)
// ->where('policy_transaction.' . $date_type . '<=', $endDate);
if($date_type == "policy_issue_date"){
$builder->where("pt_co_share_details.pt_policy_issue_date >=", $startDate)
->where("pt_co_share_details.pt_policy_issue_date <=", $endDate);
}else{
$builder->where('policy_transaction.' . $date_type . '>=', $startDate)
->where('policy_transaction.' . $date_type . '<=', $endDate);
$builder->where('policy_transaction.' . $date_type . ' >=', $startDate)
->where('policy_transaction.' . $date_type . ' <=', $endDate);
}
} else {
$fromDate = date('Y-m-d', strtotime('-90 days'));
@ -2364,6 +2373,7 @@
}
if ($start_date != 0 && $end_date != 0 && $date_type != 0 && $date_type != 'statement_month') {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
@ -2489,6 +2499,7 @@
// 3. Date condition (except statement_month)
if ($start_date != 0 && $end_date != 0 && $date_type != 0 && $date_type != 'statement_month') {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));
@ -3080,6 +3091,7 @@
// 3. Date condition (except statement_month)
if ($start_date != 0 && $end_date != 0 && $date_type != 0 && $date_type != 'statement_month') {
$this->validateDateType($date_type);
$startDate = date('Y-m-d 00:00:00', strtotime($start_date));
$endDate = date('Y-m-d 23:59:59', strtotime($end_date));

View File

@ -38,7 +38,7 @@ class SalesActivityModel extends Model
// Validation
protected $validationRules = [
'lead_id' => 'required|integer',
'activity_type' => 'required|in_list[Call,Email,Meeting,Demo,Share,Todo,Visit]',
'activity_type' => 'required|in_list[Call,Email,Meeting,Visit,Demo,Share Docs,To Do]',
'notes' => 'required',
'scheduled_date' => 'required',
'assigned_to' => 'required|integer',
@ -102,7 +102,7 @@ class SalesActivityModel extends Model
$assignedToIds = is_array($assigned_to) ? $assigned_to : explode(',', $assigned_to);
// Now it is guaranteed to be an array, making whereIn perfectly safe
$this->whereIn('sales_actual_leads.assigned_to', $assignedToIds);
$this->whereIn('sales_activities.assigned_to', $assignedToIds);
}

View File

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class SalesTargetModel extends Model
{
protected $table = 'sales_target';
protected $primaryKey = 'id';
protected $useAutoIncrement = true;
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $allowedFields = [
'user_id',
'fy_year',
'target_amount',
'created_by',
'updated_by'
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
protected $validationRules = [
'user_id' => 'required|integer',
'fy_year' => 'required|max_length[9]',
'target_amount' => 'required|decimal',
];
protected $validationMessages = [
'user_id' => ['required' => 'User is required'],
'fy_year' => ['required' => 'Financial year is required'],
'target_amount' => ['required' => 'Target amount is required', 'decimal' => 'Target amount must be a valid number'],
];
}

View File

@ -1,5 +1,4 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
@ -12,7 +11,7 @@ class TicketMasterModel extends Model
protected $returnType = 'array';
protected $useSoftDeletes = false;
protected $protectFields = true;
protected $allowedFields = [
protected $allowedFields = [
'id',
'ticket_type_id',
'feedback_json',
@ -79,9 +78,9 @@ class TicketMasterModel extends Model
'approved_description',
'file_id',
'denial_letter',
'manager_id',
'agent_id' ,
'agent_id',
'vehicle_id',
'hospital_address',
@ -98,17 +97,16 @@ class TicketMasterModel extends Model
];
// Callbacks
protected $allowCallbacks = true;
protected $beforeInsert = ["checkAndADDCreatedByValue"];
protected $afterInsert = [];
protected $beforeUpdate = ["checkAndUpdateUpdatedByValue"];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
protected $allowCallbacks = true;
protected $beforeInsert = ["checkAndADDCreatedByValue"];
protected $afterInsert = [];
protected $beforeUpdate = ["checkAndUpdateUpdatedByValue"];
protected $afterUpdate = [];
protected $beforeFind = [];
protected $afterFind = [];
protected $beforeDelete = [];
protected $afterDelete = [];
protected function checkAndADDCreatedByValue(array $data)
{
@ -150,15 +148,15 @@ class TicketMasterModel extends Model
public function getTemplateDataByTicketID($ticket_id)
{
$template_data = $this
->select("ticket_mail_template.*, CASE
WHEN employees.email_corporate IS NULL OR employees.email_corporate = ''
THEN ticket_master.emp_mail
ELSE employees.email_corporate
->select("ticket_mail_template.*, CASE
WHEN employees.email_corporate IS NULL OR employees.email_corporate = ''
THEN ticket_master.emp_mail
ELSE employees.email_corporate
END as emp_mail, ,ticket_master.claim_status_id")
->join('ticket_claim_status', 'ticket_master.claim_status_id = ticket_claim_status.id and ticket_claim_status.is_active = 1')
->join('ticket_mail_template', '
ticket_claim_status.trigger_type = ticket_mail_template.trigger_type
and ticket_claim_status.ticket_type = ticket_mail_template.ticket_type
ticket_claim_status.trigger_type = ticket_mail_template.trigger_type
and ticket_claim_status.ticket_type = ticket_mail_template.ticket_type
and ticket_mail_template.is_active = 1')
->join('employees', 'employees.id = ticket_master.emp_id', 'left')
->where('ticket_master.id', $ticket_id)
@ -173,11 +171,11 @@ class TicketMasterModel extends Model
{
$ticket_data = $this->select("
ticket_master.*,
CASE
WHEN employees.email_corporate IS NULL OR employees.email_corporate = ''
THEN ticket_master.emp_mail
ELSE employees.email_corporate
END as emp_mail,
CASE
WHEN employees.email_corporate IS NULL OR employees.email_corporate = ''
THEN ticket_master.emp_mail
ELSE employees.email_corporate
END as emp_mail,
user_profiles.first_name as acm,
user_profiles.mobile as acm_mobile,
@ -205,7 +203,7 @@ class TicketMasterModel extends Model
//get TAT report BAND wise Data
// public function getTATReport($ticket_type = null, $start_date = null, $end_date = null)
// {
// {
// //set default last 3 months data date
// $fromDate = date('Y-m-d', strtotime('-90 days'));
// $toDate = date('Y-m-d 23:59:59');
@ -222,9 +220,9 @@ class TicketMasterModel extends Model
// }
// // Fetch claim statuses from the `ticket_claim_status` table
// $statusQuery = " SELECT
// $statusQuery = " SELECT
// id, claim_status, ticket_type
// FROM ticket_claim_status
// FROM ticket_claim_status
// Where is_active = 1
// $ticket_type_data_1
// ";
@ -273,7 +271,6 @@ class TicketMasterModel extends Model
// $dynamicSelect = rtrim($dynamicSelect, ', ');
// // dd($dynamicSelect);
// // Construct the full SQL query
// $sql = "
// SELECT
@ -326,7 +323,7 @@ class TicketMasterModel extends Model
// WHERE
// tm.ticket_type_id = tcs.id
// $ticket_type_data_2
// AND tm.created_at >= '$fromDate'
// AND tm.created_at >= '$fromDate'
// AND tm.created_at <= '$toDate'
// AND tm.is_active = 1
// AND th.is_active = 1
@ -371,9 +368,9 @@ class TicketMasterModel extends Model
// }
// // Fetch claim statuses from the `ticket_claim_status` table
// $statusQuery = " SELECT
// $statusQuery = " SELECT
// id, claim_status, ticket_type
// FROM ticket_claim_status
// FROM ticket_claim_status
// Where is_active = 1
// $ticket_type_data_1
// ";
@ -422,7 +419,6 @@ class TicketMasterModel extends Model
// $dynamicSelect = rtrim($dynamicSelect, ', ');
// // dd($dynamicSelect);
// // Construct the full SQL query
// $sql = "
// SELECT
@ -451,20 +447,20 @@ class TicketMasterModel extends Model
// FROM
// ticket_master tm
// LEFT JOIN (
// SELECT
// th.ticket_id,
// SELECT
// th.ticket_id,
// th.new_value AS claim_status_id,
// MAX(th.created_at) AS change_date
// FROM
// ticket_history th
// WHERE
// FROM
// ticket_history th
// WHERE
// th.field_name = 'claim_status_id'
// AND th.is_active = 1
// GROUP BY
// GROUP BY
// th.ticket_id, th.new_value
// ) AS latest_status ON latest_status.ticket_id = tm.id
// WHERE tm.is_active = 1
// AND tm.created_at >= '$fromDate'
// AND tm.created_at >= '$fromDate'
// AND tm.created_at <= '$toDate'
// " . (!empty($ticket_type) ? " AND tm.ticket_type_id = $ticket_type" : "") . "
// GROUP BY tm.id, latest_status.claim_status_id, latest_status.change_date, tm.created_at
@ -493,33 +489,33 @@ class TicketMasterModel extends Model
{
// Set default date range (last 90 days)
$fromDate = date('Y-m-d', strtotime('-90 days'));
$toDate = date('Y-m-d 23:59:59');
if (!empty($start_date) && !empty($end_date)) {
$toDate = date('Y-m-d 23:59:59');
if (! empty($start_date) && ! empty($end_date)) {
$fromDate = change_date_format($start_date);
$toDate = change_date_format($end_date);
$toDate = change_date_format($end_date);
}
// Get all active claim statuses
$statusQuery = $this->db->table('ticket_claim_status')
->select('id, claim_status')
->where('is_active', 1)
->orderBy('id', 'ASC');
if (!empty($ticket_type)) {
if (! empty($ticket_type)) {
$statusQuery->where('ticket_type', $ticket_type);
}
$statusResult = $statusQuery->get()->getResultArray();
// Create a list of all TAT categories we want in the output
$tatCategories = [
'Above 20 Days',
'13-20 Days',
'13-20 Days',
'7-12 Days',
'0-6 Days'
'0-6 Days',
];
// Initialize the result array with all TAT categories
$result = [];
foreach ($tatCategories as $category) {
@ -529,39 +525,39 @@ class TicketMasterModel extends Model
}
$result[] = $row;
}
// Get the base query for ticket counts by status and TAT category
$query = $this->db->table('ticket_master tm')
->select([
'tcs.claim_status',
'CASE
WHEN DATEDIFF(CURDATE(), COALESCE(
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
AND th.field_name = "claim_status_id"
AND th.is_active = 1),
tm.created_at
)) BETWEEN 0 AND 6 THEN "0-6 Days"
WHEN DATEDIFF(CURDATE(), COALESCE(
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
AND th.field_name = "claim_status_id"
AND th.is_active = 1),
tm.created_at
)) BETWEEN 7 AND 12 THEN "7-12 Days"
WHEN DATEDIFF(CURDATE(), COALESCE(
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
(SELECT MAX(th.created_at)
FROM ticket_history th
WHERE th.ticket_id = tm.id
AND th.field_name = "claim_status_id"
AND th.is_active = 1),
tm.created_at
)) BETWEEN 13 AND 20 THEN "13-20 Days"
ELSE "Above 20 Days"
END AS tat_category',
'COUNT(*) AS count'
'COUNT(*) AS count',
])
->join('ticket_claim_status tcs', 'tcs.id = tm.claim_status_id AND tcs.is_active = 1')
->where('tm.is_active', 1)
@ -569,23 +565,23 @@ class TicketMasterModel extends Model
->where('tm.created_at <=', $toDate)
->groupBy('tcs.claim_status, tat_category')
->orderBy('FIELD(tat_category, "Above 20 Days", "13-20 Days", "7-12 Days", "0-6 Days")');
if (!empty($ticket_type)) {
if (! empty($ticket_type)) {
$query->where('tm.ticket_type_id', $ticket_type);
}
$countResults = $query->get()->getResultArray();
// Populate the result array with actual counts
foreach ($countResults as $row) {
foreach ($result as &$categoryRow) {
if ($categoryRow['TAT_Category'] === $row['tat_category']) {
$categoryRow[$row['claim_status']] = (int)$row['count'];
$categoryRow[$row['claim_status']] = (int) $row['count'];
break;
}
}
}
return $result;
}
@ -593,21 +589,21 @@ class TicketMasterModel extends Model
{
$ticket_type_data_1 = "";
$ticket_type_data_2 = "";
$statusBinds = [];
$statusBinds = [];
if (!empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
if (! empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
$statusBinds['policy_type'] = $policy_type;
}
// Fetch claim statuses dynamically
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusResult = $this->db->query($statusQuery, $statusBinds)->getResultArray();
// Initialize dynamic query parts
$dynamicSelect = '';
$dynamicTotal = '';
$dynamicSelect = '';
$dynamicTotal = '';
$dynamicClosedTotal = '';
// Loop through each claim status and generate the CASE statements
@ -624,7 +620,7 @@ class TicketMasterModel extends Model
'UNDER PROCESS - INVESTIGATION STATUS',
'UNDER PROCESS - QUERY DOCUMENT RECEIVED',
'APPROVED',
'PAYMENT INITIATED'
'PAYMENT INITIATED',
])) {
$dynamicTotal .= "COUNT(CASE WHEN master.claim_status_id = {$status['id']} THEN master.id END) + ";
}
@ -638,34 +634,34 @@ class TicketMasterModel extends Model
}
// Remove the trailing commas and `+` signs
$dynamicSelect = rtrim($dynamicSelect, ', ');
$dynamicTotal = rtrim($dynamicTotal, ' +');
$dynamicSelect = rtrim($dynamicSelect, ', ');
$dynamicTotal = rtrim($dynamicTotal, ' +');
$dynamicClosedTotal = rtrim($dynamicClosedTotal, ' +');
// print_rr($dynamicSelect);
// Construct the final SQL query
$sql = "
SELECT
SELECT
tpa.id as TPA_ID,
tpa.name AS TPA_NAME,
$dynamicSelect,
($dynamicTotal) AS TOTAL,
($dynamicClosedTotal) AS CLEARED_TOTAL
FROM
FROM
ticket_master master
JOIN
JOIN
tpa ON master.tpa_id = tpa.id AND tpa.is_active = 1
WHERE
WHERE
master.created_at BETWEEN :start_date: AND :end_date:
AND master.is_active = 1
$ticket_type_data_2
GROUP BY
GROUP BY
tpa.id;
";
// Execute the query
$binds = ["start_date"=>$start_date,"end_date"=>$end_date];
$binds = ["start_date" => $start_date, "end_date" => $end_date];
// Merge policy_type binds if present
if (!empty($statusBinds)) {
if (! empty($statusBinds)) {
$binds = array_merge($binds, $statusBinds);
}
$result = $this->db->query($sql, $binds)->getResultArray();
@ -680,28 +676,27 @@ class TicketMasterModel extends Model
if ($policy_type == 1) {
$ticket_type_data_1 = "";
$ticket_type_data_2 = "";
$statusBinds = [];
$statusBinds = [];
if (!empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
if (! empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
$statusBinds['policy_type'] = $policy_type;
}
// Fetch claim statuses dynamically
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusResult = $this->db->query($statusQuery,$statusBinds)->getResultArray();
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusResult = $this->db->query($statusQuery, $statusBinds)->getResultArray();
// Initialize dynamic query parts
$dynamicSelect = '';
$dynamicTotal = '';
$dynamicTotal = '';
// Loop through each claim status and generate the CASE statements
foreach ($statusResult as $status) {
$dynamicSelect .= "
COUNT(CASE WHEN master.claim_status_id = {$status['id']} THEN master.id END) AS `{$status['claim_status']}`, ";
// Include in total count
$dynamicTotal .= "COUNT(CASE WHEN master.claim_status_id = {$status['id']} THEN master.id END) + ";
@ -711,54 +706,54 @@ class TicketMasterModel extends Model
// Remove the trailing commas and `+` signs
$dynamicSelect = rtrim($dynamicSelect, ', ');
$dynamicTotal = rtrim($dynamicTotal, ' +');
$dynamicTotal = rtrim($dynamicTotal, ' +');
// print_rr($dynamicSelect);
// Construct the final SQL query
$sql = "
SELECT
SELECT
user_profiles.id as ACM_ID,
user_profiles.first_name AS ACM_NAME,
$dynamicSelect,
($dynamicTotal) AS TOTAL
FROM
FROM
ticket_master master
JOIN
JOIN
user_profiles ON master.acm_id = user_profiles.id AND user_profiles.is_active = 1
WHERE
WHERE
master.created_at BETWEEN :start_date: AND :end_date:
AND master.is_active = 1
$ticket_type_data_2
GROUP BY
GROUP BY
user_profiles.id;
";
$binds = ["start_date"=>$start_date,"end_date"=>$end_date];
// Merge policy_type binds if present
if (!empty($statusBinds)) {
$binds = ["start_date" => $start_date, "end_date" => $end_date];
// Merge policy_type binds if present
if (! empty($statusBinds)) {
$binds = array_merge($binds, $statusBinds);
}
$result = $this->db->query($sql,$binds)->getResultArray();
$result = $this->db->query($sql, $binds)->getResultArray();
// print_rr(count(($result)));
// print_rr($this->db->lastQuery);die();
return $result;
} else {
$ticket_type_data_1 = "";
$ticket_type_data_2 = "";
$statusBinds = [];
$statusBinds = [];
if (!empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
if (! empty($policy_type)) {
$ticket_type_data_1 = "AND ticket_type = :policy_type:";
$ticket_type_data_2 = "AND master.ticket_type_id = :policy_type:";
$statusBinds['policy_type'] = $policy_type;
}
// Fetch claim statuses dynamically
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusResult = $this->db->query($statusQuery,$statusBinds)->getResultArray();
$statusQuery = "SELECT id, claim_status FROM ticket_claim_status WHERE is_active = 1 $ticket_type_data_1";
$statusResult = $this->db->query($statusQuery, $statusBinds)->getResultArray();
// Initialize dynamic query parts
$dynamicSelect = '';
$dynamicTotal = '';
$dynamicSelect = '';
$dynamicTotal = '';
$dynamicClosedTotal = '';
// Loop through each claim status and generate the CASE statements
@ -773,7 +768,7 @@ class TicketMasterModel extends Model
'INSURER PENDING',
'INVESTIGATION',
'APPROVED',
'ON HOLD'
'ON HOLD',
])) {
$dynamicTotal .= "COUNT(CASE WHEN master.claim_status_id = {$status['id']} THEN master.id END) + ";
}
@ -787,52 +782,51 @@ class TicketMasterModel extends Model
}
// Remove the trailing commas and `+` signs
$dynamicSelect = rtrim($dynamicSelect, ', ');
$dynamicTotal = rtrim($dynamicTotal, ' +');
$dynamicSelect = rtrim($dynamicSelect, ', ');
$dynamicTotal = rtrim($dynamicTotal, ' +');
$dynamicClosedTotal = rtrim($dynamicClosedTotal, ' +');
// print_rr($dynamicSelect);
// Construct the final SQL query
$sql = "
SELECT
SELECT
user_profiles.id as ACM_ID,
user_profiles.first_name AS ACM_NAME,
$dynamicSelect,
($dynamicTotal) AS TOTAL,
($dynamicClosedTotal) AS CLEARED_TOTAL
FROM
FROM
ticket_master master
JOIN
JOIN
user_profiles ON master.acm_id = user_profiles.id AND user_profiles.is_active = 1
WHERE
WHERE
master.created_at BETWEEN :start_date: AND :end_date:
AND master.is_active = 1
$ticket_type_data_2
GROUP BY
GROUP BY
user_profiles.id;
";
$binds = ["start_date"=>$start_date,"end_date"=>$end_date];
$binds = ["start_date" => $start_date, "end_date" => $end_date];
// Execute the query
// Merge policy_type binds if present
if (!empty($statusBinds)) {
// Merge policy_type binds if present
if (! empty($statusBinds)) {
$binds = array_merge($binds, $statusBinds);
}
$result = $this->db->query($sql,$binds)->getResultArray();
$result = $this->db->query($sql, $binds)->getResultArray();
// print_rr($this->db->lastQuery);die();
return $result;
}
}
//api
public function get_ticket_data($emp_id, $returnType,$ticket_type = null, $ticket_id = null)
public function get_ticket_data($emp_id, $returnType, $ticket_type = null, $ticket_id = null)
{
$query = $this->select("
ticket_master.*,
tms.mail_subject as subject,
ticket_master.*,
tms.mail_subject as subject,
tms.id as ticket_message_id,
(
SELECT th1.old_value
@ -854,11 +848,11 @@ class TicketMasterModel extends Model
$query->whereIn('sender', ['staff', 'user']);
$query->where('ticket_master.emp_id', $emp_id);
if (!empty($ticket_id)) {
if (! empty($ticket_id)) {
$query->where('ticket_master.id', $ticket_id);
}
if (!empty($ticket_type)) {
if (! empty($ticket_type)) {
$query->where('ticket_master.ticket_type_id', $ticket_type);
}
@ -879,9 +873,9 @@ class TicketMasterModel extends Model
->select('tm.id, tm.ticket_type_id, tcs.claim_status, th.last_claim_status_change')
->join('ticket_claim_status tcs', 'tm.claim_status_id = tcs.id', 'left')
->join(
'(SELECT ticket_id, MAX(created_at) AS last_claim_status_change
FROM ticket_history
WHERE field_name = \'claim_status_id\'
'(SELECT ticket_id, MAX(created_at) AS last_claim_status_change
FROM ticket_history
WHERE field_name = \'claim_status_id\'
GROUP BY ticket_id) th',
'tm.id = th.ticket_id',
'left'
@ -898,8 +892,8 @@ class TicketMasterModel extends Model
$total = 0;
foreach ($statuses as $status) {
$alias = strtolower(str_replace(' ', '_', $status));
$summary[$alias] = 0;
$alias = strtolower(str_replace(' ', '_', $status));
$summary[$alias] = 0;
$summary[$alias . '_ids'] = '';
$dateLimit = $limit[$typeId][$status] ?? 3;
@ -911,19 +905,18 @@ class TicketMasterModel extends Model
if (
$row['claim_status'] === $status &&
(
!$row['last_claim_status_change'] ||
! $row['last_claim_status_change'] ||
$row['last_claim_status_change'] <= $threshold
)
) {
$summary[$alias]++;
$ticketIds[] = $row['id'];
if ($status == "APPROVED"|| $status == "SETTLED"|| $status == "CLOSED" ){
if ($status == "APPROVED" || $status == "SETTLED" || $status == "CLOSED") {
continue;
}
$total++;
}
}
@ -934,8 +927,8 @@ class TicketMasterModel extends Model
$summary['total'] = $total;
// Add approved but not settled IDs and count
$approvedNotSettled = $this->getNotSettledbutApprovedCount($typeId);
$summary['approved_not_settled'] = $approvedNotSettled['count'];
$approvedNotSettled = $this->getNotSettledbutApprovedCount($typeId);
$summary['approved_not_settled'] = $approvedNotSettled['count'];
$summary['approved_not_settled_ids'] = $approvedNotSettled['ticket_ids'];
$finalResults[$typeId] = $summary;
@ -960,13 +953,13 @@ class TicketMasterModel extends Model
$ids = $builder->get()->getRowArray();
// If no matching status IDs are found, return empty
if (!$ids) {
if (! $ids) {
return ['count' => 0, 'ticket_ids' => ''];
}
// Extract approved and settled status IDs
$approved_id = $ids['approved_id'];
$settled_id = $ids['settled_id'];
$settled_id = $ids['settled_id'];
// Subquery to get the latest approval time for each ticket
$subquery = $this->db->table('ticket_history')
@ -983,12 +976,12 @@ class TicketMasterModel extends Model
// Left join to find if there is a settled status for each ticket after approval
$builder->join(
'ticket_history th_settled',
"th_settled.ticket_id = tm.id
AND th_settled.field_name = 'claim_status_id'
AND th_settled.new_value = {$settled_id}
"th_settled.ticket_id = tm.id
AND th_settled.field_name = 'claim_status_id'
AND th_settled.new_value = {$settled_id}
AND th_settled.created_at > latest_approval.approved_time",
'left',
false // Important for raw ON condition
false// Important for raw ON condition
);
// Filtering conditions: not settled and approved time older than 12 days
@ -1001,7 +994,7 @@ class TicketMasterModel extends Model
$builder->select('tm.id');
// Execute the query
$query = $builder->get();
$query = $builder->get();
$ticketIds = array_column($query->getResultArray(), 'id'); // Extract the IDs
// Convert ticket IDs array to a comma-separated string
@ -1009,8 +1002,8 @@ class TicketMasterModel extends Model
// Return the count and comma-separated ticket IDs
return [
'count' => count($ticketIds),
'ticket_ids' => $ticketIdsString
'count' => count($ticketIds),
'ticket_ids' => $ticketIdsString,
];
}
@ -1024,8 +1017,8 @@ class TicketMasterModel extends Model
AND cp.is_active = 1
";
$binds = ["ticket_id"=>$ticket_id];
$query = $this->db->query($sql,$binds);
$binds = ["ticket_id" => $ticket_id];
$query = $this->db->query($sql, $binds);
if ($query && $query->getNumRows() > 0) {
$row = $query->getRowArray();
@ -1039,11 +1032,10 @@ class TicketMasterModel extends Model
public function getPolicyData($params)
{
$client_name = $params['client_name'] ?? null;
$client_name = $params['client_name'] ?? null;
$insurer_short_name = $params['insurer_short_name'] ?? null;
$tpa_short_name = $params['tpa_short_name'] ?? null;
$policy_no = $params['policy_no'] ?? null;
$tpa_short_name = $params['tpa_short_name'] ?? null;
$policy_no = $params['policy_no'] ?? null;
$builder = $this->db->table('client_policy');
$builder->select('client_policy.*');
@ -1055,19 +1047,19 @@ class TicketMasterModel extends Model
$builder->where('i.is_active', 1);
$builder->where('c.client_name', trim($client_name));
$builder->where('i.short_name', trim($insurer_short_name));
if(!empty($tpa_short_name)){
$builder->where('tpa.short_name', trim($tpa_short_name));
if (! empty($tpa_short_name)) {
$builder->where('tpa.short_name', trim($tpa_short_name));
}
$builder->where('client_policy.policy_no', trim($policy_no));
$query = $builder->get();
$query = $builder->get();
$result = $query->getResultArray();
// dd($this->db->getLastQuery());
return $result;
}
public function getTheClaimDetails($params)
{
{
$client_name = $params['client_name'] ?? null;
$policy_no = $params['policy_no'] ?? null;
$insurer_short_name = $params['insurer_short_name'] ?? null;
@ -1099,19 +1091,19 @@ class TicketMasterModel extends Model
$builder->where('ticket_master.claim_number', trim($claim_no));
$builder->where('ticket_master.relationship', trim($relationship));
if(!empty($tpa_short_name)){
$builder->where('tpa.short_name', trim($tpa_short_name));
if (! empty($tpa_short_name)) {
$builder->where('tpa.short_name', trim($tpa_short_name));
}
$query = $builder->get();
$query = $builder->get();
$result = $query->getResultArray();
return $result;
}
}
public function getEmployeeAndEmployeePolicyDetails($params)
{
{
$client_name = $params['client_name'] ?? null;
$emp_code = $params['emp_code'] ?? null;
$emp_name = $params['emp_name'] ?? null;
@ -1124,7 +1116,6 @@ class TicketMasterModel extends Model
// $emp_name = "Lokesh";
// $policy_no = "GMC-1999/2000/2021/2022";
$builder = $this->db->table('employees e');
$builder->select("
e.id as emp_id,
@ -1143,7 +1134,7 @@ class TicketMasterModel extends Model
cp.insurer_id,
cp.tpa_id,
cp.policy_no,
CASE
CASE
WHEN cp.policy_type_id = 2 THEN 1
WHEN cp.policy_type_id = 1 THEN 2
WHEN cp.policy_type_id = 6 THEN 3
@ -1165,7 +1156,7 @@ class TicketMasterModel extends Model
$builder->where('cp.policy_no', trim($policy_no));
$builder->where('LOWER(e.relationship)', strtolower('self'));
$query = $builder->get();
$query = $builder->get();
$result = $query->getRowArray();
return $result;
@ -1181,8 +1172,8 @@ class TicketMasterModel extends Model
$builder = $this->db->table('user_profiles');
$builder->select("user_profiles.*");
$builder->where('user_profiles.is_active', 1);
$builder->where('user_profiles.mobile', trim($acm_mobile));
$query = $builder->get();
$builder->where('user_profiles.mobile', trim($acm_mobile));
$query = $builder->get();
$result = $query->getRowArray();
return $result;
@ -1193,9 +1184,9 @@ class TicketMasterModel extends Model
public function getInsuredDetails($params)
{
$client_id = $params['client_id'] ?? null;
$client_id = $params['client_id'] ?? null;
$emp_code = $params['emp_code'] ?? null;
$policy_id = $params['client_policy_id'] ?? null;
$policy_id = $params['client_policy_id'] ?? null;
$insured_name = $params['insured_name'] ?? null;
$builder = $this->db->table('employees e');
@ -1220,7 +1211,7 @@ class TicketMasterModel extends Model
$builder->where('e.client_id', trim($client_id));
$builder->where('ep.client_policy_id', trim($policy_id));
$query = $builder->get();
$query = $builder->get();
$result = $query->getRowArray();
// dd($this->db->getLastQuery());
return $result;

View File

@ -245,7 +245,7 @@
<img src="<?= base_url() . "public"; ?>/assets/images/active_leads_and_bds_renewal.png" alt="Logo" height="14"
class="active_leads " style="display:none;">
&nbsp;&nbsp;
<span class="d-none d-sm-inline-block dash-tab dash-tab-font">Leads and BDS Renewals</span>
<span class="d-none d-sm-inline-block dash-tab dash-tab-font">Opportunities and BDS Renewals</span>
</a>
</li>
&nbsp;&nbsp;&nbsp;&nbsp;

View File

@ -24,6 +24,66 @@
.dataTables_length label {height: 21px !important;}
.readonly-select { background-color: #f3f3f3 !important; cursor: not-allowed; pointer-events: none; }
.custom-tooltip {
position: relative;
display: inline-block;
cursor: pointer;
}
.info-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
background-color: #007bff;
color: #fff;
border-radius: 50%;
font-size: 14px;
font-style: italic;
font-weight: bold;
font-family: Georgia, serif;
line-height: 1;
vertical-align: middle;
}
.tooltiptext {
visibility: hidden;
opacity: 0;
position: fixed; /* fixed instead of absolute to escape modal overflow */
background: #333;
color: #fff;
padding: 10px 14px;
border-radius: 6px;
white-space: normal; /* allow text wrapping */
width: 320px; /* fixed width for multiline */
font-size: 12px;
font-weight: normal;
font-style: normal;
line-height: 1.6;
z-index: 99999;
transition: opacity 0.2s ease;
pointer-events: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
top: 20px !important;
left: 155px !important;
}
.tooltiptext::after {
content: "";
position: absolute;
top: 100%;
left: 20px;
border: 6px solid transparent;
border-top-color: #333;
}
.custom-tooltip:hover .tooltiptext {
visibility: visible;
opacity: 1;
}
</style>
<!-- <div class="row">
@ -117,6 +177,16 @@
class="mdi mdi-dots-horizontal"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a target="_blank" class="dropdown-item" href="<?= base_url("util/download_claim_dump_file/") . $file['file_id']; ?>"><i class="mdi mdi-download mr-2 text-muted font-18 vertical-middle"></i>Download</a>
<?php if ($file['status'] === 'failed') : ?>
<a href="javascript:void(0);"
class="dropdown-item"
onclick="downloadTpaErrorFile(<?= (int) $file['file_id']; ?>)">
<i class="mdi mdi-file-excel mr-2 text-danger font-18 vertical-middle"></i>
Download Error File
</a>
<?php endif; ?>
<!-- <a data-id="<?php echo $file['file_id'] ?>" data-toggle="modal" data-target="#full-width-modal-emp-list" class="dropdown-item view_emp_list" href="#"><i class="mdi mdi-eye mr-2 text-muted font-18 vertical-middle"></i>View</a> -->
</div>
</div>
@ -151,7 +221,16 @@
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title" id="myCenterModalLabel">Claim Dump Upload</h4>
<h4 class="modal-title" id="myCenterModalLabel">
Claim Dump Upload
<i class="info-icon"
data-toggle="tooltip"
data-placement="right"
data-html="true"
title="1. If you want to upload a TPA-wise claim dump, select the client and policy, then upload the TPA's Excel file. <br><br> 2. The second option without selecting the client and policy allows you to upload multiple claims. You can download the sample file provided below, fill it in, and upload it. This is for bulk dump upload.">
i
</i>
</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
</div>
<div class="modal-body">
@ -219,6 +298,20 @@
let policyListByClient = null;
let client_list = null;
$(function () {
$('[data-toggle="tooltip"]').tooltip();
});
document.querySelectorAll('.custom-tooltip').forEach(function(tooltip) {
tooltip.addEventListener('mouseenter', function() {
var icon = this.querySelector('.info-icon');
var tip = this.querySelector('.tooltiptext');
var rect = icon.getBoundingClientRect();
tip.style.left = rect.left + 'px';
tip.style.top = (rect.top - tip.offsetHeight - 10) + 'px';
});
});
$(document).ready(function() {
getClientAndBranchAndPolicy();
@ -232,10 +325,17 @@
let client_id = $('#client_id').val() ?? null;
let client_policy_id = $('#client_policy_id').val() ?? null;
let tpa_id = $('#tpa_id').val() ?? null;
if(client_id){
if(!client_policy_id){
toastr.warning('Please select the client policy', 'Warning');
toastr.warning('Please select the client policy', 'WARNING');
return;
}
if(!tpa_id){
toastr.warning('TPA is required', 'WARNING');
return;
}
}
@ -251,9 +351,15 @@
'application/vnd.oasis.opendocument.spreadsheet'
];
if (!fileInput.files.length) {
toastr.warning('Please select an Excel file', 'WARNING');
return false;
}
var selectedFile = fileInput.files[0];
if (!allowedTypes.includes(selectedFile.type)) {
alert('Please upload a valid Excel file (.xlsx, .xls, .ods)');
toastr.warning('Please upload a valid Excel file (.xlsx, .xls, .ods)', 'WARNING');
return false;
}
@ -500,6 +606,70 @@
}
function downloadTpaErrorFile(file_id) {
if (!file_id) {
toastr.error('Invalid file reference for error download', 'Error');
return;
}
var url = '<?= base_url("util/getTpaClaimDumpErrorData"); ?>/' + file_id;
$.ajax({
url: url,
type: 'GET',
xhrFields: { responseType: 'blob' },
success: function (data, status, xhr) {
var disposition = xhr.getResponseHeader('Content-Disposition') || '';
var contentType = xhr.getResponseHeader('Content-Type') || '';
// If server sent JSON (error), parse it and show via Toastr
if (contentType.indexOf('application/json') !== -1) {
var reader = new FileReader();
reader.onload = function () {
try {
var json = JSON.parse(reader.result);
var msg = json.message || 'Unable to download error file.';
toastr.error(msg, 'Error');
} catch (e) {
toastr.error('Unable to download error file.', 'Error');
}
};
reader.readAsText(data);
return;
}
// Otherwise assume it is Excel and trigger a download
var filename = 'tpa_claim_dump_errors_' + file_id + '.xlsx';
var match = /filename="?([^"]+)"?/.exec(disposition);
if (match && match[1]) {
filename = match[1];
}
var blob = new Blob([data], {
type: contentType || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
});
var link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toastr.success('Error file download started', 'Success');
},
error: function (xhr) {
var msg = 'Unable to download error file.';
try {
var json = JSON.parse(xhr.responseText);
if (json.message) {
msg = json.message;
}
} catch (e) {}
toastr.error(msg, 'Error');
}
});
}
$('.close').click(function(){
$('#modal_body').empty()
let html = `<div class="spinner-border text-primary" role="status" style="position: relative; left: 200px;"></div>`

586
app/Views/expense_list.php Normal file
View File

@ -0,0 +1,586 @@
<div class="row" id="expense_module">
<div class="col-12">
<div class="card">
<div class="card-body">
<div class="row mb-3">
<div class="col-6 d-flex align-items-center">
<!-- <h4 class="mb-0">Expense</h4> -->
</div>
<div class="col-6 text-right">
<!-- <button type="button" class="btn app-btn-secondary" id="btnResetExpense">
<i class="mdi mdi-refresh mr-1"></i>Reset
</button> -->
</div>
</div>
<div class="custom-form mb-4">
<form role="form" class="parsley-examples" method="get" id="expense_form">
<input type="hidden" value="<?= csrf_hash() ?>" name="<?= csrf_token() ?>" />
<input type="hidden" name="id" id="expense_id" />
<div class="form-row">
<div class="form-group col-md-4">
<label for="client_id">Client Name <span class="text-danger">*</span></label>
<select class="form-control" id="client_id" name="client_id" required>
<option value="">Select Client</option>
<?php if (! empty($clients)) : ?>
<?php foreach ($clients as $client) : ?>
<option value="<?= $client['id']; ?>" <?= ! empty($filters['client_id']) && (int) $filters['client_id'] === (int) $client['id'] ? 'selected' : ''; ?>>
<?= esc($client['client_name']); ?>
<?= ! empty($client['short_name']) ? ' (' . esc($client['short_name']) . ')' : ''; ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="form-group col-md-4">
<label for="client_policy_id">Policy No <span class="text-danger">*</span></label>
<select class="form-control" id="client_policy_id" name="client_policy_id" required>
<option value="">Select Policy</option>
<?php if (! empty($policies_for_filter)) : ?>
<?php foreach ($policies_for_filter as $policy) : ?>
<option value="<?= $policy['id']; ?>" <?= ! empty($filters['client_policy_id']) && (int) $filters['client_policy_id'] === (int) $policy['id'] ? 'selected' : ''; ?>>
<?= ! empty($policy['policy_type']) ? esc($policy['policy_type']) . ' - ' : ''; ?> <?= esc($policy['policy_no']); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="form-group col-md-4">
<label for="approved_by">Approved By <span class="text-danger">*</span></label>
<select class="form-control" id="approved_by" name="approved_by" required>
<option value="">Select User</option>
<?php if (! empty($approved_users)) : ?>
<?php foreach ($approved_users as $user) : ?>
<option value="<?= $user['id']; ?>" <?= ! empty($filters['approved_by']) && (int) $filters['approved_by'] === (int) $user['id'] ? 'selected' : ''; ?>>
<?= esc($user['first_name']); ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
</div>
<div class="form-row">
<div class="form-group col-md-4">
<label for="expense_date">Expense Date <span class="text-danger">*</span></label>
<input
type="text"
class="form-control"
id="expense_date"
name="expense_date"
placeholder="DD-MM-YYYY"
value="<?= isset($filters['expense_date']) ? esc($filters['expense_date']) : ''; ?>"
required
>
</div>
<div class="form-group col-md-4">
<label for="amount">Amount <span class="text-danger">*</span></label>
<input type="number" step="0.01" min="0" class="form-control" id="amount" name="amount" placeholder="Enter amount" value="<?= isset($filters['amount']) ? esc($filters['amount']) : ''; ?>" required>
</div>
<div class="form-group col-md-4">
<label for="description">Description <span class="text-danger">*</span></label>
<textarea class="form-control" id="description" name="description" rows="3" placeholder="Enter description" required><?= isset($filters['description']) ? esc($filters['description']) : ''; ?></textarea>
</div>
</div>
<div class="form-group text-right m-b-0">
<!-- <button type="button" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSearchExpense">
Search
</button> -->
<button type="submit" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSubmitExpense">
Save
</button>
<button type="button" class="btn btn-secondary waves-effect" id="btnCancelExpense">
Reset
</button>
</div>
</form>
</div>
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="expense-table">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">S.No</th>
<th class="font-weight-medium">Client</th>
<th class="font-weight-medium">Policy No</th>
<th class="font-weight-medium">Description</th>
<th class="font-weight-medium">Approved By</th>
<th class="font-weight-medium">Amount</th>
<th class="font-weight-medium">Expense Date</th>
<th class="font-weight-medium">Action</th>
</tr>
</thead>
<tbody>
<?php if (! empty($expenses)) : ?>
<?php foreach ($expenses as $index => $row) : ?>
<tr>
<td class="text-center"><?= $index + 1; ?></td>
<td>
<?= esc($row['client_name'] ?? ''); ?>
<?= ! empty($row['short_name']) ? ' (' . esc($row['short_name']) . ')' : ''; ?>
</td>
<td><?= esc($row['policy_no'] ?? ''); ?></td>
<td><?= esc($row['description'] ?? ''); ?></td>
<td><?= esc($row['approved_by_name'] ?? ''); ?></td>
<td><?= number_format((float) ($row['amount'] ?? 0), 2); ?></td>
<td>
<?php if (! empty($row['expense_date'])): ?>
<?= date('d-m-Y', strtotime($row['expense_date'])); ?>
<?php endif; ?>
</td>
<td>
<div class="btn-group dropdown">
<a href="javascript:void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown" aria-expanded="false">
<i class="mdi mdi-dots-horizontal"></i>
</a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" href="javascript:void(0);" onclick="editExpense(<?= (int) $row['id']; ?>)">
<i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit
</a>
<a class="dropdown-item" href="javascript:void(0);" onclick="deleteExpense(<?= (int) $row['id']; ?>)">
<i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete
</a>
</div>
</div>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const expenseValidationErrors = <?= ! empty($validation_errors) ? json_encode(array_values($validation_errors)) : '[]'; ?>;
let expensePoliciesCache = {};
function resetExpenseForm() {
$('#client_id').val('').trigger('change');
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
$('#approved_by').val('').trigger('change');
$('#description').val('');
$('#amount').val('');
$('#expense_date').val('');
$('#btnSubmitExpense').text('Save');
window.location.href = '<?= base_url("expense"); ?>';
}
function validateExpenseSave() {
const errors = [];
const clientId = $('#client_id').val();
const policyId = $('#client_policy_id').val();
const approvedBy = $('#approved_by').val();
const desc = ($('#description').val() || '').trim();
const amount = ($('#amount').val() || '').trim();
const expenseDate= ($('#expense_date').val() || '').trim();
const idPattern = /^[0-9]+$/;
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
if (!clientId) {
errors.push('Client is required.');
} else if (!idPattern.test(clientId)) {
errors.push('Invalid client selected.');
}
if (!policyId) {
errors.push('Policy is required.');
} else if (!idPattern.test(policyId)) {
errors.push('Invalid policy selected.');
}
if (!approvedBy) {
errors.push('Approved By is required.');
} else if (!idPattern.test(approvedBy)) {
errors.push('Invalid approver selected.');
}
if (!desc) {
errors.push('Description is required.');
} else if (!descPattern.test(desc)) {
errors.push('Description contains invalid characters.');
}
if (!amount) {
errors.push('Amount is required.');
} else if (isNaN(amount) || Number(amount) < 0) {
errors.push('Amount must be a non-negative number.');
}
if (!expenseDate) {
errors.push('Expense Date is required.');
} else if (!datePattern.test(expenseDate)) {
errors.push('Expense Date must be in DD-MM-YYYY format.');
}
if (errors.length) {
toastr.warning(errors.join('<br>'), 'Validation');
return false;
}
return true;
}
function validateExpenseSearchFilters() {
const errors = [];
const clientId = $('#client_id').val();
const policyId = $('#client_policy_id').val();
const approvedBy = $('#approved_by').val();
const desc = ($('#description').val() || '').trim();
const amount = ($('#amount').val() || '').trim();
const expenseDate= ($('#expense_date').val() || '').trim();
const idPattern = /^[0-9]+$/;
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
if (clientId && !idPattern.test(clientId)) {
errors.push('Invalid client selected for search.');
}
if (policyId && !idPattern.test(policyId)) {
errors.push('Invalid policy selected for search.');
}
if (approvedBy && !idPattern.test(approvedBy)) {
errors.push('Invalid approver selected for search.');
}
if (desc && !descPattern.test(desc)) {
errors.push('Description filter contains invalid characters.');
}
if (amount) {
if (isNaN(amount) || Number(amount) < 0) {
errors.push('Amount filter must be a non-negative number.');
}
}
if (expenseDate && !datePattern.test(expenseDate)) {
errors.push('Expense Date filter must be in DD-MM-YYYY format.');
}
if (errors.length) {
toastr.warning(errors.join('<br>'), 'Validation');
return false;
}
return true;
}
function populatePolicies(clientId, selectedPolicyId = null) {
if (!clientId) {
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
return;
}
if (expensePoliciesCache[clientId]) {
const policies = expensePoliciesCache[clientId];
let optionsHtml = '<option value=\"\">Select Policy</option>';
policies.forEach(function (p) {
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
});
$('#client_policy_id').html(optionsHtml);
if (selectedPolicyId) {
$('#client_policy_id').val(selectedPolicyId).trigger('change');
}
return;
}
$.ajax({
url: '<?= base_url("expense/client-policies"); ?>',
type: 'GET',
dataType: 'json',
data: {client_id: clientId},
success: function (res) {
if (res.status) {
expensePoliciesCache[clientId] = res.data || [];
let optionsHtml = '<option value=\"\">Select Policy</option>';
(res.data || []).forEach(function (p) {
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
});
$('#client_policy_id').html(optionsHtml);
if (selectedPolicyId) {
$('#client_policy_id').val(selectedPolicyId).trigger('change');
}
} else {
toastr.warning(res.message || 'Unable to load policies', 'WARNING');
}
},
error: function () {
toastr.error('Unexpected error while loading policies', 'ERROR');
}
});
}
function editExpense(id) {
if (!id) {
return;
}
$.ajax({
url: '<?= base_url("expense/get"); ?>/' + id,
type: 'GET',
dataType: 'json',
success: function (res) {
if (res.status && res.data) {
const data = res.data;
$('#expense_id').val(data.id);
$('#description').val(data.description || '');
$('#amount').val(data.amount || '');
if (data.expense_date) {
const parts = (data.expense_date || '').split('-'); // Y-m-d
if (parts.length === 3) {
const formatted = parts[2] + '-' + parts[1] + '-' + parts[0];
$('#expense_date').val(formatted);
if (window.expenseDatePicker) {
window.expenseDatePicker.setDate(formatted, true, 'd-m-Y');
}
}
}
if (data.approved_by) {
$('#approved_by').val(data.approved_by).trigger('change');
}
if (data.client_id) {
$('#client_id').val(data.client_id).trigger('change');
populatePolicies(data.client_id, data.client_policy_id || null);
}
$('#btnSubmitExpense').text('Update');
$('html, body').animate({
scrollTop: $('#expense_form').offset().top - 100
}, 400);
} else {
toastr.warning(res.message || 'Expense not found', 'WARNING');
}
},
error: function () {
toastr.error('Unable to fetch expense details', 'ERROR');
}
});
}
function deleteExpense(id) {
if (!id) {
return;
}
Swal.fire({
title: "Are you sure?",
text: "You want to delete this expense",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#3085d6",
cancelButtonColor: "#d33",
confirmButtonText: "Yes",
}).then((result) => {
if (!result.isConfirmed) {
return;
}
const postData = {};
postData['<?= csrf_token() ?>'] = '<?= csrf_hash() ?>';
$.ajax({
url: '<?= base_url("expense/delete"); ?>/' + id,
type: 'POST',
dataType: 'json',
data: postData,
success: function (res) {
if (res.status) {
toastr.success(res.message || 'Expense deleted successfully', 'SUCCESS');
setTimeout(function () {
location.reload();
}, 800);
} else {
toastr.warning(res.message || 'Unable to delete expense', 'WARNING');
}
},
error: function () {
toastr.error('Unable to delete expense', 'ERROR');
}
});
});
}
$(document).ready(function () {
$('#client_id').select2();
$('#client_policy_id').select2();
$('#approved_by').select2();
window.expenseDatePicker = flatpickr('#expense_date', {
dateFormat: 'd-m-Y',
allowInput: true
});
$('#expense-table').DataTable({
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
buttons: [
{
extend: 'collection',
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
className: 'btn app-btn-secondary ',
buttons: [
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited"></i><span class="btn-custom"> CSV </span>',
title: 'Expense List',
className: 'app-btn-primary ',
exportOptions: {
columns: ':not(:last-child)'
}
},
{
extend: 'excel',
title: 'Expense-List',
sheetName: 'Expense-List',
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
className: 'app-btn-primary ',
exportOptions: {
orthogonal: 'sort',
columns: ':not(:last-child)'
}
},
{
extend: 'pdf',
text: '<i class="mdi mdi-file-pdf"></i><span class="btn-custom"> PDF </span>',
title: 'Expense List',
className: 'app-btn-primary ',
exportOptions: {
columns: ':not(:last-child)'
}
}
]
}
],
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
}
});
$('.dataTables_length label').css('height', '21px');
// Show any backend validation errors from search
if (Array.isArray(expenseValidationErrors) && expenseValidationErrors.length) {
expenseValidationErrors.forEach(function (msg) {
toastr.warning(msg, 'Validation');
});
}
$('#client_id').on('change', function () {
const clientId = $(this).val();
populatePolicies(clientId);
});
$('#btnSearchExpense').on('click', function () {
if (!validateExpenseSearchFilters()) {
return;
}
const params = {};
const clientId = $('#client_id').val();
const policyId = $('#client_policy_id').val();
const approvedBy = $('#approved_by').val();
const desc = $('#description').val();
const amount = $('#amount').val();
const expenseDate = $('#expense_date').val();
if (clientId) {
params.client_id = clientId;
}
if (policyId) {
params.client_policy_id = policyId;
}
if (approvedBy) {
params.approved_by = approvedBy;
}
if (desc) {
params.description = desc;
}
if (amount) {
params.amount = amount;
}
if (expenseDate) {
params.expense_date = expenseDate;
}
const query = $.param(params);
const baseUrl = '<?= base_url("expense"); ?>';
window.location.href = query ? baseUrl + '?' + query : baseUrl;
});
$('#btnSubmitExpense').on('click', function (e) {
e.preventDefault();
if (!validateExpenseSave()) {
return;
}
const formData = $('#expense_form').serialize();
$.ajax({
url: '<?= base_url("expense/save"); ?>',
type: 'POST',
dataType: 'json',
data: formData,
success: function (res) {
if (res.status) {
toastr.success(res.message || 'Expense saved successfully', 'SUCCESS');
setTimeout(function () {
location.reload();
}, 800);
} else {
const errors = res.errors || {};
let errorMsg = res.message || 'Unable to save expense';
if (Object.keys(errors).length) {
errorMsg = Object.values(errors).join('<br>');
}
toastr.warning(errorMsg, 'WARNING');
}
},
error: function () {
toastr.error('Unexpected error while saving expense', 'ERROR');
}
});
});
$('#btnResetExpense, #btnCancelExpense').on('click', function () {
resetExpenseForm();
});
});
</script>

View File

@ -1876,6 +1876,17 @@
</li>
<?php } ?>
<!-- Expence -->
<?php if (in_array(get_role_id(), [1, 5])) { ?>
<li class="li-seperate" id="clients-li">
<a href="<?= base_url('expense') ?>" class="img-inactive">
<img style="border-radius: 5px;"
src="<?= base_url() . "public"; ?>/assets/images/expense_icon.png" alt="Logo" height="24">
<span> Expense </span>
</a>
</li>
<?php } ?>
<!-- BDS -->
<?php if ((get_role_id() == 1 || get_role_id() == 5 || get_role_id() == 4) || (in_array(MANAGEMENT_TEAM_ID, user_team()) || in_array(FINANCE_TEAM_ID, user_team()) || in_array(BUSINESS_TEAM_ID, user_team()) || in_array(POS_TEAM_ID, user_team()))) { ?>
<li class="li-seperate" id="policy-transactions-li">
@ -2104,6 +2115,11 @@
<!-- <li>
<a href="<?= base_url('sales/page/Team') ?>"><i class="ri-bar-chart-line"></i> Sales Team </a>
</li> -->
<?php if (in_array(get_role_id(), [1, 5])){ ?>
<li>
<a href="<?= base_url('sales/loadtargets') ?>"><i class="ri-group-2-fill"></i>Sales Team Targets </a>
</li>
<?php } ?>
</ul>
</div>
</li>

View File

@ -347,7 +347,7 @@ $isActive = get_role_id() == STAFF_ROLE_ID && in_array(ENROLLMENT_TEAM_ID, user_
if (type == 1 && leadType != null && leadType == 1) {
$('.leadStatusTitle_1').show();
$('#main_tile').text(" Leads");
$('#main_tile').text("Opportunities");
}
if (type == 1 && leadType != null && leadType == 2) {

View File

@ -452,6 +452,33 @@
policyEnd.attr("id", end_date_id);
$("label[for='policy_end_date'], label[for^='policy_end_date_']").attr("for", end_date_id);
}, 3000);
$(document).ready(function() {
$('#salse_person_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#client_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#client_branch_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
});
</script>
<script>
@ -1241,6 +1268,15 @@
var lead_type = $('#lead_type').val();
leadTypeBsedHideAndShow(lead_type)
$('#policy_type_id_' + increment).parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#insurer_' + increment).select2();
$('#tpa_' + increment).select2();
$('#proposed_insurer_' + increment).select2();

View File

@ -432,6 +432,42 @@
<script>
$(document).ready(function() {
$('#salse_person_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#client_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#client_branch_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
$('#policy_type_id').parsley({
errorsContainer: function(ParsleyField) {
if (ParsleyField.$element.hasClass('select2-hidden-accessible')) {
return ParsleyField.$element.siblings('.select2-container');
}
return ParsleyField.$element;
}
});
});
$(document).ready(function() {

View File

@ -4,8 +4,8 @@
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.btn-primary { background: #ff6b35; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #ff5722; transform: translateY(-1px); }
.btn-primary { background: #02a8b5; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #02a8b5; transform: translateY(-1px); }
.btn-complete { background: #4caf50; color: white; border: none;padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
.btn-complete:hover { background: #4caf50; transform: translateY(-1px); }
.btn-view { background: #f0f0f0; color: #666; border: none; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; transition: all 0.2s;}
@ -14,7 +14,7 @@
/* Filter Tabs */
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
.tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
.tab.active { background: #ff6b35; color: white; border-color: #ff6b35; }
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
/* Leads Grid */
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
@ -256,8 +256,8 @@
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share Docs" onclick="selectType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="To Do" onclick="selectType('To Do', this)"> To Do</button>
</div>
</div>
<div class="form-group">
@ -326,8 +326,8 @@
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('To Do', this)"> To Do</button>
</div>
@ -406,7 +406,7 @@ $(document).ready(function() {
$('.searchable').each(function() {
let parentModal = $(this).closest('.modal');
$(this).select2({
placeholder: "Select User",
placeholder: "Select..",
dropdownParent: parentModal.length ? parentModal : $(document.body)
});
});
@ -429,7 +429,15 @@ function resetFlatpicker(){
});
}
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "" };
const activityIcons = {
"Call": "📞",
"Email": "✉️",
"Meeting": "📅",
"Visit": "🚗",
"Demo": "🖥️",
"Share Docs": "📄",
"To Do": ""
};
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
const API = '<?= base_url('sales') ?>';
@ -441,6 +449,7 @@ let selectedFollowUpActivityType = '';
let currentPage = 1;
let limit = 10;
let currentOffset = 0;
const department = '<?= $sales_role ?>';
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
@ -461,6 +470,7 @@ function closeModal(id) {
selectedType = 'Call';
$('#typeButtons .d_activity_type').removeClass('active');
$('#typeButtons .d_activity_type[data-type="Call"]').addClass('active');
fetchActivities(); // now i called because of Refresh the page .......
}
// Specific cleanup for "Complete" modal (hidden follow-up sections)
@ -652,7 +662,6 @@ async function fetchActivities(isLoadMore = false) {
// 2. Detail Logic
async function viewDetail(id) {
console.log("i am here");
lead_id = id;
let res = await fetch(`${API}/leads/${id}`);
let json = await res.json();
@ -740,6 +749,7 @@ function renderCard(opps) {
function renderTimeline(acts) {
const cont = document.getElementById('timelineContainer');
if (acts.length === 0) {
cont.classList.add('no-line');
@ -786,6 +796,7 @@ function renderTimeline(acts) {
}
function openActivityModal() {
document.getElementById('act_owner').value = global_lead_assigned_to;
document.getElementById('act_owner').dispatchEvent(new Event('change'));
document.getElementById('act_lead_id').value = lead_id;
@ -908,7 +919,7 @@ document.getElementById('activityForm').onsubmit = async (e) => {
toastr.success('Activity Created Successfully');
closeModal('activityModal');
if (flag === "frompopup") {
viewDetail(document.getElementById('act_lead_id').value);
viewDetail(actLead);
} else {
window.location.reload();
}

View File

@ -135,8 +135,8 @@
'Meeting' => '📅',
'Visit' => '🚗',
'Demo' => '🖥️',
'Share' => '📄',
'Todo' => '✓'
'Share Docs' => '📄',
'To Do' => '✓'
];
$icon = $activityIcons[$a['activity_type']] ?? '📌';
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
@ -217,8 +217,8 @@
'Meeting' => ['color' => '#4299e1', 'icon' => '📅'],
'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'],
'Demo' => ['color' => '#9f7aea', 'icon' => '🖥️'],
'Share' => ['color' => '#ed8936', 'icon' => '📄'],
'Todo' => ['color' => '#718096', 'icon' => '✓'],
'Share Docs' => ['color' => '#ed8936', 'icon' => '📄'],
'To Do' => ['color' => '#718096', 'icon' => '✓'],
];
?>
@ -248,14 +248,14 @@
<div class="card" style="grid-column: 1 / -1; width: 100%; box-sizing: border-box;">
<div class="table-header">
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview <?php echo !empty($leads_overview) ? "<i>(".count($leads_overview).")</i>" : ""; ?></h3>
<!-- <span style="color: #718096; font-size: 12px; font-weight: 600;">Click a lead to see details</span> -->
<h3 style="font-size: 16px; font-weight: 700;">All Leads Overview </h3>
<!-- <span style="color: #718096; font-size: 12px; font-weight: 600;">Click a lead to see details</span>
<a href="<?= base_url('sales') ?>"
style="color: #718096; font-size: 12px; font-weight: 600; text-decoration: none; padding: 4px 8px; transition: color 0.2s ease; display: inline-block; cursor: pointer;"
onmouseover="this.style.color='#ff6b35';"
onmouseout="this.style.color='#718096';">
Click a lead to see details
</a>
</a> -->
</div>
<table>
<thead>

View File

@ -86,7 +86,7 @@
<div class="target-card">
<div style="font-size:11px; color:#999; letter-spacing: 1px; font-weight: 600;">YEARLY TARGET</div>
<div class="target-amount"><?= number_format($target / 100000, 1) ?>L</div>
<div class="target-amount"><?= number_format($target_amt / 100000, 1) ?>L</div>
<div style="font-size:12px; color:#777;"><?= $display_fin_years ?></div>
<div class="chart-circle" style="position: absolute; right: 40px; top: 35px; width: 75px; height: 75px;">
@ -134,7 +134,7 @@
</div>
<?php endforeach; ?> -->
<?php foreach($upcoming as $u):
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share' => '📄', 'Todo' => '✓' ];
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ];
$icon = $activityIcons[$u['activity_type']] ?? '📌';
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($u['scheduled_date']));
?>

View File

@ -0,0 +1,602 @@
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; }
/* ═══════════════════════════════════════════
LAYOUT Page Shell
═══════════════════════════════════════════ */
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
.top-bar {
display: flex;
justify-content: space-between; /* pushes subtitle left, search right */
align-items: center; /* vertically centers both */
padding: 14px 28px;
background: white;
/* border-bottom: 1px solid #e0e0e0; */
}
/* ═══════════════════════════════════════════
SEARCH INPUT
═══════════════════════════════════════════ */
.search-input { padding: 9px 14px; border: 1px solid #e0e0e0; border-radius: 8px; font-size: 14px; width: 240px; outline: none; background: #f5f5f5; transition: border-color .2s;}
.search-input:focus { border-color: #ff6b35; }
/* ═══════════════════════════════════════════
CONTENT AREA
═══════════════════════════════════════════ */
.content-area { width: 100%; }
.content { padding: 10px; width: 100%; max-width: 100%;}
.subtitle { font-size: 13px; color: #999; margin-bottom: 20px; }
.view-container { display: none; }
.view-container.active { display: block; }
/* ═══════════════════════════════════════════
MAIN TABLE
═══════════════════════════════════════════ */
.tt-table-wrapper { border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; width: 100%;}
.tt-main-table { width: 100%;border-collapse: collapse;table-layout: fixed;}
.tt-main-table thead { display: table; width: 100%; table-layout: fixed; }
.tt-main-table thead th { background: #f4f4f4; padding: 11px 14px; text-align: left;font-size: 12px;font-weight: 700;color: #777;text-transform: uppercase;letter-spacing: .4px;border-bottom: 1px solid #e0e0e0;}
.tt-main-table tbody { display: block; max-height: 530px; overflow-y: auto;}
.tt-main-table tbody tr { display: table; width: 100%; table-layout: fixed; }
.tt-main-table tbody td { padding: 12px 14px; font-size: 14px; border-bottom: 1px solid #f0f0f0; vertical-align: middle;}
.tt-main-table tbody tr:last-child td { border-bottom: none; }
.tt-main-table tbody tr:hover td { background: #fafafa; }
/* Empty row — must stay as table-row (not display:table override) */
.tt-main-table tbody tr.empty-row { display: table-row; width: 100%; }
.tt-main-table tbody tr.empty-row td { width: 100%; text-align: center; }
/* ═══════════════════════════════════════════
MEMBER AVATAR (used inside main table)
═══════════════════════════════════════════ */
.tt-person-cell { display: flex; align-items: center; gap: 1px;}
/* .tt-avatar { width: 38px; height: 38px; border-radius: 50%; background: #ff6b35; color: white; font-size: 14px; font-weight: 700; display: flex; align-items: center; justify-content: center; flex-shrink: 0;} */
/* ═══════════════════════════════════════════
EMPTY STATE
═══════════════════════════════════════════ */
.empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 40px 20px; color: #aaa; gap: 10px; }
.empty-icon { font-size: 36px; opacity: 0.5; line-height: 1;}
.empty-state p { margin: 0; font-size: 14px; font-weight: 500; color: #bbb;}
.empty-row td { padding: 0 !important; border: none !important; background: transparent !important;}
/* ═══════════════════════════════════════════
MODAL Overlay & Box
═══════════════════════════════════════════ */
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 2000; align-items: center; justify-content: center;}
.modal.active { display: flex; }
.modal-box { background: white; border-radius: 14px; width: 92%; max-width: 640px; max-height: 90vh; display: flex; flex-direction: column; overflow: hidden; box-shadow: 0 8px 32px rgba(0, 0, 0, .18); animation: slideUp .2s ease;}
.modal-header { padding: 6px 25px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 25px; flex: 1; overflow-y: auto;}
/* ═══════════════════════════════════════════
MODAL Form Fields
═══════════════════════════════════════════ */
.form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 15px;}
.form-group { margin-bottom: 20px; }
.form-label { display: block; margin-bottom: 8px; font-size: 14px; font-weight: 500; color: #333;}
.form-input,
.form-textarea,
.form-select {
width: 100%;
padding: 10px 15px;
border: 1px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
font-family: inherit;
background: #f5f5f5; /* grey background */
color: #333;
transition: border-color .2s, background .2s;
}
.form-textarea { resize: vertical; min-height: 100px;}
.form-input:focus,
.form-textarea:focus,
.form-select:focus { outline: none; border-color: #ff6b35; background: #fff;}
/* ═══════════════════════════════════════════
MODAL Detail Table (targets list)
═══════════════════════════════════════════ */
.table-wrap { border: 1px solid #e0e0e0; border-radius: 10px; overflow: hidden; margin-top: 16px;}
.table-wrap table { width: 100%; border-collapse: collapse; table-layout: fixed; }
.table-wrap thead { display: table; width: 100%; table-layout: fixed; }
.table-wrap thead th { background: #f8f8f8; padding: 12px 16px; text-align: left; font-size: 13px; font-weight: 600; color: #666; border-bottom: 1px solid #e0e0e0;}
.table-wrap tbody { display: block; max-height: 220px; overflow-y: auto; }
.table-wrap tbody tr { display: table; width: 100%; table-layout: fixed; }
.table-wrap tbody td { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; font-size: 14px; color: #333; vertical-align: middle;}
.table-wrap tbody tr:last-child td { border-bottom: none; }
.table-wrap tbody tr:hover td { background: #fafafa; }
/* Empty row fix for modal table */
.table-wrap tbody tr.empty-row { display: table-row; }
.table-wrap tbody tr.empty-row td { padding: 0 !important; border: none !important; text-align: center;}
/* ═══════════════════════════════════════════
BUTTONS
═══════════════════════════════════════════ */
/* Primary action — View Target (main table) */
.btn-tt-view {
background: #ff6b35;
color: white;
border: none;
padding: 7px 16px;
border-radius: 7px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background .2s;
}
.btn-tt-view:hover { background: #ff5722; }
/* Close button — modal header */
.close-btn {
background: none;
border: none;
font-size: 22px;
cursor: pointer;
color: #888;
width: 32px;
height: 32px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
transition: background .2s;
}
.close-btn:hover { background: #f0f0f0; color: #333; }
/* Edit row — modal detail table */
.btn-edit {
background: #e8f4ff;
color: #1976d2;
border: none;
padding: 5px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
margin-right: 6px;
transition: background .2s;
}
.btn-edit:hover { background: #bbdefb; }
/* Remove row — modal detail table */
.btn-remove {
background: #ffeaea;
color: #d32f2f;
border: none;
padding: 5px 12px;
border-radius: 5px;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background .2s;
}
.btn-remove:hover { background: #ffcdd2; }
/* ═══════════════════════════════════════════
ANIMATIONS
═══════════════════════════════════════════ */
@keyframes slideUp {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
/* ═══════════════════════════════════════════
RESPONSIVE
═══════════════════════════════════════════ */
@media (max-width: 540px) {
.form-row { grid-template-columns: 1fr; }
.search-input { width: 100%; }
}
/* ═══════════════════════════════════════════
GREY BOX Input Section Container
═══════════════════════════════════════════ */
.grey-box {
background: #f5f5f5;
border: 1px solid #e0e0e0;
border-radius: 10px;
padding: 16px 18px;
margin-bottom: 20px;
}
.grey-box .form-row {
display: grid;
grid-template-columns: 1fr 1fr auto; /* 2 fields + button */
gap: 15px;
align-items: flex-end; /* aligns button to bottom of fields */
}
.grey-box .form-group {
margin-bottom: 0; /* remove default gap inside grey box */
}
.grey-box .btn {
white-space: nowrap;
height: 40px; /* match input height */
width: 100%;
}
@media (max-width: 540px) {
.grey-box .form-row {
grid-template-columns: 1fr; /* stack vertically on mobile */
}
}
@keyframes fadeHighlight {
0% { background-color: #fffbcc; }
100% { background-color: transparent; }
}
.row-highlight {
animation: fadeHighlight 2s ease forwards;
}
</style>
<div class="main-content">
<hr class="my-0">
<div class="top-bar">
<p class="subtitle" id="memberCount"></p>
<input type="text" class="search-input" id="searchInput" placeholder="Search member…" oninput="filterMembers(this.value)">
</div>
<!-- Content -->
<div class="content">
<div class="content-area view-container active" id="teamtargetView">
<div class="tt-table-wrapper">
<table class="tt-main-table">
<thead>
<tr>
<th>Sales Manager</th>
<th>Action</th>
</tr>
</thead>
<tbody id="ttMainTableBody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal" id="targetModal">
<div class="modal-box">
<div class="modal-header">
<span class="modal-title" id="modalTitle"></span>
<button class="close-btn" onclick="closeModal()" title="Close">×</button>
</div>
<div class="modal-body">
<input type="hidden" id="recordId">
<input type="hidden" id="currentUserId">
<div class="grey-box">
<div class="form-row">
<div class="form-group">
<label class="form-label">Financial Year</label>
<select class="form-select" id="fyYear"></select>
</div>
<div class="form-group">
<label class="form-label">Amount ()</label>
<input type="text" class="form-input" id="targetAmount" placeholder="e.g. 500000"
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
</div>
<div class="form-group">
<button type="button" class="btn btn-primary waves-effect btn-sm w-100" id="btn-save" onclick="saveRecord()">Save Target</button>
</div>
</div>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Financial Year</th>
<th>Amount</th>
<th>Action</th>
</tr>
</thead>
<tbody id="detailTableBody">
<tr class="empty-row"><td colspan="3">No targets added yet.</td></tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const users = <?= json_encode($users ?? []) ?>;
/* ──────────────────────────────────────────
DATA
────────────────────────────────────────── */
const teamMembers = users.map(user => ({
id: user.id,
name: user.first_name + ' ' + user.last_name
}));
const API = '<?= base_url('sales') ?>';
let activeUserId = null;
/* ──────────────────────────────────────────
RENDER MEMBER LIST
────────────────────────────────────────── */
function renderMemberList(filter = '') {
const tbody = document.getElementById('ttMainTableBody');
const countEl = document.getElementById('memberCount');
const term = filter.trim().toLowerCase();
const filtered = teamMembers.filter(m =>
m.name.toLowerCase().includes(term)
);
// countEl.textContent = `${filtered.length} member${filtered.length !== 1 ? 's' : ''}`;
countEl.textContent = ``;
tbody.innerHTML = '';
if (filtered.length === 0) {
tbody.innerHTML = `
<tr class="empty-row">
<td colspan="2">
<div class="empty-state">
<div class="empty-icon">👤</div>
<p>No Member found</p>
</div>
</td>
</tr>
`;
return;
}
filtered.forEach(m => {
// const initials = m.name.split(' ').map(w => w[0]).join('').substring(0, 2).toUpperCase();
const tr = document.createElement('tr');
tr.innerHTML = `
<td id="card_${m.id}">
<div class="tt-person-cell">
<div class="avatar-md">
<div class="avatar-title bg-soft-primary rounded-circle text-primary font-20 font-weight-semibold"
style="background-color: #f1f3fa; width: 48px; height: 48px; display: flex; align-items: center; justify-content: center;">
${(m.name || '?').charAt(0).toUpperCase()}
</div>
</div>
<span>${m.name}</span>
</div>
</td>
<td>
<button class="btn btn-primary waves-effect waves-light" onclick="openModal(${m.id})">View Target</button>
</td>
`;
tbody.appendChild(tr);
});
}
function filterMembers(val) {
renderMemberList(val);
}
/* ──────────────────────────────────────────
FY OPTIONS
────────────────────────────────────────── */
function populateFY(selectedFY = null) {
const sel = document.getElementById('fyYear');
const today = new Date();
const year = today.getFullYear();
const month = today.getMonth(); // 0-11
// Determine current FY start year
let currentFYStart;
if (month >= 3) { // April (3) or later
currentFYStart = year;
} else { // Jan, Feb, Mar
currentFYStart = year - 1;
}
sel.innerHTML = '';
for (let i = 0; i < 10; i++) {
const startYear = currentFYStart - i;
const label = `${startYear}-${startYear + 1}`;
const opt = document.createElement('option');
opt.value = label;
opt.textContent = label;
if (label === selectedFY) opt.selected = true;
sel.appendChild(opt);
}
}
/* ──────────────────────────────────────────
MODAL
────────────────────────────────────────── */
function openModal(userId) {
activeUserId = userId;
const member = teamMembers.find(m => Number(m.id) === Number(userId));
document.getElementById('modalTitle').textContent = member ? member.name : '';
document.getElementById('currentUserId').value = userId;
document.getElementById('recordId').value = '';
document.getElementById('targetAmount').value = '';
populateFY();
renderDetailTable(userId);
document.getElementById('targetModal').classList.add('active');
}
function closeModal() {
document.getElementById('targetModal').classList.remove('active');
activeUserId = null;
}
document.getElementById('targetModal').addEventListener('click', function(e) {
if (e.target === this) closeModal();
});
/* ──────────────────────────────────────────
RENDER DETAIL TABLE
────────────────────────────────────────── */
async function renderDetailTable(userId) {
const tbody = document.getElementById('detailTableBody');
tbody.innerHTML = '<tr><td colspan="3">Loading...</td></tr>';
try {
const res = await fetch(`${API}/targets/user/${userId}`);
const json = await res.json();
const records = json.data || [];
if (records.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="3">No targets added yet.</td></tr>';
return;
}
// records.sort((a, b) => b.id - a.id);
records.sort((a, b) => {
let startYearA = parseInt(a.fy_year.split('-')[0], 10);
let startYearB = parseInt(b.fy_year.split('-')[0], 10);
return startYearB - startYearA;
});
tbody.innerHTML = records.map(r => `
<tr id="row_${r.id}">
<td>${r.fy_year}</td>
<td> ${formatNum(r.target_amount)}</td>
<td>
<button class="btn-edit" onclick="editRecord(${r.id}, '${r.fy_year}', ${r.target_amount})">Edit</button>
<button class="btn-remove" onclick="removeRecord(${r.id})">Remove</button>
</td>
</tr>
`).join('');
} catch (err) {
tbody.innerHTML = '<tr><td colspan="3">Failed to load targets.</td></tr>';
console.error(err);
}
}
/* ──────────────────────────────────────────
SAVE (INSERT / UPDATE)
────────────────────────────────────────── */
async function saveRecord() {
const userId = activeUserId;
const recordId = document.getElementById('recordId').value.trim();
const fyYear = document.getElementById('fyYear').value;
const amtRaw = document.getElementById('targetAmount').value.trim();
// Validation
if (!fyYear) { toastr.error('Please select a Financial Year.'); return; }
if (!amtRaw) { toastr.error('Please enter an amount.'); return; }
const amount = parseInt(amtRaw, 10);
if (isNaN(amount) || amount <= 0) { toastr.error('Enter a valid positive amount.'); return; }
const existingRows = document.querySelectorAll('#detailTableBody tr[id^="row_"]');
for (const row of existingRows) {
const rowId = row.id.replace('row_', '');
const rowFY = row.cells[0].textContent.trim();
// Skip the row being edited
if (recordId && rowId === recordId) continue;
if (rowFY === fyYear) {
toastr.error(`A target for ${fyYear} already exists.`);
return;
}
}
const saveBtn = document.getElementById('btn-save');
const originalBtnText = saveBtn.innerText;
saveBtn.disabled = true;
saveBtn.innerText = recordId ? 'Updating...' : 'Saving...';
try {
const payload = {
user_id: userId,
fy_year: fyYear,
target_amount: amount
};
const isUpdate = !!recordId;
const url = isUpdate ? `${API}/targets/${recordId}` : `${API}/targets`;
const method = isUpdate ? 'PUT' : 'POST';
const res = await fetch(url, {
method: method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const result = await res.json();
if (res.ok) {
toastr.success(isUpdate ? 'Target updated!' : 'Target saved!');
// Reset form
document.getElementById('recordId').value = '';
document.getElementById('targetAmount').value = '';
populateFY();
await renderDetailTable(userId);
const savedId = result.data?.id || recordId;
const targetRow = document.getElementById(`row_${savedId}`);
if (targetRow) {
targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
targetRow.classList.add('row-highlight');
setTimeout(() => targetRow.classList.remove('row-highlight'), 3000);
}
} else {
toastr.error(result.message || 'Failed to save target.');
}
} catch (err) {
console.error(err);
toastr.error('A network error occurred.');
} finally {
saveBtn.disabled = false;
saveBtn.innerText = originalBtnText;
}
}
/* ──────────────────────────────────────────
EDIT populate form from table row data
────────────────────────────────────────── */
function editRecord(id, fyYear, amount) {
document.getElementById('recordId').value = id;
document.getElementById('targetAmount').value = amount;
populateFY(fyYear);
}
/* ──────────────────────────────────────────
REMOVE
────────────────────────────────────────── */
async function removeRecord(id) {
if (!confirm('Are you sure you want to remove this target?')) return;
try {
const res = await fetch(`${API}/targets/${id}`, { method: 'DELETE' });
const result = await res.json();
if (res.ok) {
toastr.success('Target removed.');
renderDetailTable(activeUserId);
} else {
toastr.error(result.message || 'Failed to remove target.');
}
} catch (err) {
console.error(err);
toastr.error('A network error occurred.');
}
}
/* ──────────────────────────────────────────
HELPERS
────────────────────────────────────────── */
function formatNum(n) {
return Number(n).toLocaleString('en-IN');
}
/* ──────────────────────────────────────────
INIT
────────────────────────────────────────── */
renderMemberList();
</script>

View File

@ -5,13 +5,13 @@
.main-content { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
/* .top-bar { background: white; padding: 15px 30px; border-bottom: 1px solid #e0e0e0; display: flex; justify-content: space-between; align-items: center; } */
.btn-primary { background: #ff6b35; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #ff5722; transform: translateY(-1px); }
.btn-primary { background: #02a8b5; color: white; border: none; padding: 10px 20px; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s; }
.btn-primary:hover { background: #02a8b5; transform: translateY(-1px); }
/* Filter Tabs */
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
.tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
.tab.active { background: #ff6b35; color: white; border-color: #ff6b35; }
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
/* Leads Grid */
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
@ -285,8 +285,8 @@
<button type="button" class="activity-type-btn d_activity_type" data-type="Meeting" onclick="selectType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Visit" onclick="selectType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Demo" onclick="selectType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share" onclick="selectType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Todo" onclick="selectType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="Share Docs" onclick="selectType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn d_activity_type" data-type="To Do" onclick="selectType('To Do', this)"> To Do</button>
</div>
<div class="form-group">
@ -354,8 +354,8 @@
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Meeting', this)">📅 Meeting</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Visit', this)">🚗 Visit</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Demo', this)">🖥️ Demo</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Todo', this)"> To Do</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('Share Docs', this)">📄 Share Docs</button>
<button type="button" class="activity-type-btn f_activity_type" onclick="selectFollowUpActivityType('To Do', this)"> To Do</button>
</div>
@ -604,7 +604,7 @@ $(document).ready(function() {
$('.searchable').each(function() {
let parentModal = $(this).closest('.modal');
$(this).select2({
placeholder: "Select User",
placeholder: "Select..",
dropdownParent: parentModal.length ? parentModal : $(document.body)
});
});
@ -936,7 +936,15 @@ function renderCard(opps) {
}
function renderTimeline(acts) {
const cont = document.getElementById('timelineContainer');
const activityIcons = { Call: "📞", Email: "✉️", Meeting: "📅", Visit: "🚗",Demo: "🖥️", Share: "📄", Todo: "" };
const activityIcons = {
"Call": "📞",
"Email": "✉️",
"Meeting": "📅",
"Visit": "🚗",
"Demo": "🖥️",
"Share Docs": "📄",
"To Do": ""
};
if (acts.length === 0) {
cont.classList.add('no-line');
@ -1592,7 +1600,7 @@ if (btnSaveContact) {
// Use .value instead of .val()
let name = document.getElementById('contact_name').value.trim();
let mobile = document.getElementById('contact_mobile').value.trim();
let lead_id = document.getElementById('lead_id').value.trim();
let lead_id = document.getElementById('hidden_lead_id').value.trim();
if (!name || !mobile) {
return toastr.warning('Please enter both contact person name and mobile number.');
@ -1607,7 +1615,7 @@ if (btnSaveContact) {
body: JSON.stringify(payload)
});
const result = await res.json(); // Get the response body
const contactResult = await res.json(); // Get the response body
if (res.ok) {
// Clear inputs
@ -1620,16 +1628,16 @@ if (btnSaveContact) {
// Create the HTML string
let contactHtml = `
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-2"
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
style="background: #f8f8f8; border-radius: 8px;"
data-id="${result.data.contact_id}">
data-id="${contactResult.data.contact_id}">
<div>
<div class="fw-bold">${result.data.name}</div>
<div class="text-muted">${result.data.mobile}</div>
<div class="fw-bold">${contactResult.data.name}</div>
<div class="text-muted">${contactResult.data.mobile}</div>
</div>
<button type="button"
class="btn btn-danger btn-sm btnRemoveContact"
data-id="${result.data.contact_id}">
data-id="${contactResult.data.contact_id}">
Remove
</button>
</div>
@ -1637,19 +1645,19 @@ if (btnSaveContact) {
// <button type="button"
// class="btn btn-secondary btn-sm btnEditContact"
// data-id="${result.data.contact_id}">
// data-id="${contactResult.data.contact_id}">
// Update
// </button>
// Append to container using Vanilla JS
document.getElementById('savedContactsContainer').insertAdjacentHTML('beforeend', contactHtml);
toastr.success('Contact saved successfully');
} else {
let err = await res.json();
if (res.status === 400) {
let errorMessages = "";
let seenMessages = [];
if (err.messages) {
Object.entries(err.messages).forEach(([field, message]) => {
if (contactResult.messages) {
Object.entries(contactResult.messages).forEach(([field, message]) => {
let inputElement = $('[name="' + field + '"]');
if (inputElement.length > 0) {
@ -1673,11 +1681,11 @@ if (btnSaveContact) {
});
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
} else {
toastr.warning(err.message || 'Validation failed', 'Warning');
toastr.warning(contactResult.message || 'Validation failed', 'Warning');
}
}
else {
toastr.error(err.message || 'Error adding lead');
toastr.error(contactResult.message || 'Error adding lead');
}
}
} catch (error) {

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@ -0,0 +1,399 @@
<?php
namespace PhpOption {
/**
* Lightweight stub of PhpOption\Option used only for testing env() handling.
* This is intentionally minimal and should not be relied on in application code.
*/
class Option
{
private $value;
public function __construct($value)
{
$this->value = $value;
}
public static function fromValue($value): self
{
return new self($value);
}
public function map($callback): self
{
if ($this->value !== null) {
$this->value = $callback($this->value);
}
return $this;
}
public function getOrCall($callback)
{
return $this->value !== null ? $this->value : $callback();
}
public function getOrThrow($exception)
{
if ($this->value === null) {
throw $exception;
}
return $this->value;
}
}
}
namespace App\Filters {
use CodeIgniter\Filters\FilterInterface;
use CodeIgniter\HTTP\RequestInterface;
use CodeIgniter\HTTP\ResponseInterface;
// Test stubs for application filters to bypass logging, ACL, and security checks during tests.
class AclFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) { return null; }
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
}
class HttpRequestLog implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) { return null; }
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
}
class SecurityInputFilter implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) { return null; }
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
}
class GlobalPostFileUploadGuard implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) { return null; }
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
}
class Cors implements FilterInterface
{
public function before(RequestInterface $request, $arguments = null) { return null; }
public function after(RequestInterface $request, ResponseInterface $response, $arguments = null) { return; }
}
}
namespace Dotenv\Repository {
interface RepositoryInterface
{
public function get($name);
public function set($name, $value);
}
class RepositoryBuilder
{
public static function createWithDefaultAdapters(): self
{
return new self();
}
public function addAdapter($adapter): self
{
// No-op for testing.
return $this;
}
public function immutable(): self
{
return $this;
}
public function make(): RepositoryInterface
{
return new class() implements RepositoryInterface {
public function get($name)
{
$value = getenv($name);
return $value === false ? null : $value;
}
public function set($name, $value)
{
putenv("$name=$value");
}
};
}
}
}
namespace Dotenv\Repository\Adapter {
class PutenvAdapter
{
// Stub class used only to satisfy Illuminate\Support\Env references.
}
}
namespace Tests\unit {
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\FeatureTestTrait;
class ExpenseControllerTest extends CIUnitTestCase
{
use FeatureTestTrait;
protected function setUp(): void
{
parent::setUp();
// Override routes for testing to avoid filters/middleware.
$this->withRoutes([
['get', 'expense', 'ExpenseController::index'],
['post', 'expense/save', 'ExpenseController::save'],
['get', 'expense/get/(:num)', 'ExpenseController::getExpense/$1'],
['post', 'expense/delete/(:num)','ExpenseController::delete/$1'],
['get', 'expense/client-policies', 'ExpenseController::clientPolicies'],
]);
// Clean up any test data from previous runs
$db = db_connect('default');
$db->table('expenses')->like('description', 'CI4_TEST_', 'after')->delete();
}
/**
* Helper to insert an expense row directly into the database.
*/
private function createExpenseRecord(array $overrides = []): int
{
$db = db_connect('default');
$data = array_merge([
'client_id' => 1,
'client_policy_id' => 1,
'description' => 'CI4_TEST_' . uniqid('', true),
'approved_by' => 1,
'amount' => 100.00,
'created_at' => date('Y-m-d H:i:s'),
'is_active' => 1,
], $overrides);
$db->table('expenses')->insert($data);
return (int) $db->insertID();
}
public function testCreateExpenseSuccess(): void
{
$payload = [
'client_id' => '1',
'client_policy_id' => '1',
'approved_by' => '1',
'description' => 'CI4_TEST_Valid Description 123-ABC',
'expense_date' => '12-01-2026',
'amount' => '250.75',
];
$result = $this->post('expense/save', $payload);
$result->assertStatus(200);
$json = json_decode($result->getJSON(), true);
$this->assertIsArray($json);
$this->assertTrue($json['status'] ?? false);
$this->assertNotEmpty($json['id'] ?? null);
$insertedId = (int) $json['id'];
$db = db_connect('default');
$row = $db->table('expenses')
->where('description', $payload['description'])
->orderBy('id', 'DESC')
->get()
->getRowArray();
$this->assertNotEmpty($row);
$this->assertSame(1, (int) $row['is_active']);
$this->assertSame($payload['description'], $row['description']);
$this->assertEquals(250.75, (float) $row['amount']);
}
public function testCreateExpenseValidationFailureMissingFields(): void
{
$payload = [
// all required fields missing / empty
];
$result = $this->post('expense/save', $payload);
$result->assertStatus(400);
$json = json_decode($result->getJSON(), true);
$this->assertFalse($json['status'] ?? true);
$this->assertSame('Input validation failed', $json['message'] ?? '');
$this->assertArrayHasKey('client_id', $json['errors'] ?? []);
$this->assertArrayHasKey('client_policy_id', $json['errors'] ?? []);
$this->assertArrayHasKey('approved_by', $json['errors'] ?? []);
$this->assertArrayHasKey('description', $json['errors'] ?? []);
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
$this->assertArrayHasKey('expense_date', $json['errors'] ?? []);
}
public function testCreateExpenseValidationFailureInvalidDescription(): void
{
$payload = [
'client_id' => '1',
'client_policy_id' => '1',
'approved_by' => '1',
'description' => 'Invalid @ Description <>', // invalid characters
'expense_date' => '12-01-2026',
'amount' => '100.00',
];
$result = $this->post('expense/save', $payload);
$result->assertStatus(400);
$json = json_decode($result->getJSON(), true);
$this->assertFalse($json['status'] ?? true);
$this->assertArrayHasKey('description', $json['errors'] ?? []);
$this->assertStringContainsString(
'invalid characters',
strtolower($json['errors']['description'] ?? '')
);
}
public function testUpdateExpenseSuccess(): void
{
$id = $this->createExpenseRecord([
'description' => 'CI4_TEST_Original Description',
'amount' => 50.00,
]);
$payload = [
'id' => (string) $id,
'client_id' => '1',
'client_policy_id'=> '1',
'approved_by' => '1',
'description' => 'Updated Description 456',
'expense_date' => '13-01-2026',
'amount' => '75.50',
];
$result = $this->post('expense/save', $payload);
$result->assertStatus(200);
$json = json_decode($result->getJSON(), true);
$this->assertTrue($json['status'] ?? false);
$this->assertSame($id, (int) ($json['id'] ?? 0));
$db = db_connect('default');
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
$this->assertNotEmpty($row);
$this->assertSame('Updated Description 456', $row['description']);
$this->assertEquals(75.50, (float) $row['amount']);
}
public function testUpdateExpenseValidationFailureInvalidAmount(): void
{
$id = $this->createExpenseRecord();
$payload = [
'id' => (string) $id,
'client_id' => '1',
'client_policy_id'=> '1',
'approved_by' => '1',
'description' => 'Another Valid Description',
'expense_date' => '14-01-2026',
'amount' => '-10', // invalid negative
];
$result = $this->post('expense/save', $payload);
$result->assertStatus(400);
$json = json_decode($result->getJSON(), true);
$this->assertFalse($json['status'] ?? true);
$this->assertArrayHasKey('amount', $json['errors'] ?? []);
$this->assertStringContainsString(
'cannot be negative',
strtolower($json['errors']['amount'] ?? '')
);
}
public function testDeleteExpenseSoftDeleteSuccess(): void
{
$id = $this->createExpenseRecord();
$result = $this->post('expense/delete/' . $id);
$result->assertStatus(200);
$json = json_decode($result->getJSON(), true);
$this->assertTrue($json['status'] ?? false);
$db = db_connect();
$row = $db->table('expenses')->where('id', $id)->get()->getRowArray();
$this->assertNotEmpty($row);
$this->assertSame(0, (int) $row['is_active']);
}
public function testDeleteExpenseInvalidId(): void
{
$result = $this->post('expense/delete/0');
$result->assertStatus(400);
$json = json_decode($result->getJSON(), true);
$this->assertFalse($json['status'] ?? true);
$this->assertSame('Invalid expense id', $json['message'] ?? '');
}
public function testSearchFilterByClientAndDescription(): void
{
// Create two distinct expenses
$this->createExpenseRecord([
'client_id' => 10,
'description' => 'FilterMatch Description',
]);
$this->createExpenseRecord([
'client_id' => 20,
'description' => 'Other Description',
]);
$result = $this->get('expense?client_id=10&description=FilterMatch');
$result->assertStatus(200);
$body = $result->getBody();
$this->assertStringContainsString('FilterMatch Description', $body);
$this->assertStringNotContainsString('Other Description', $body);
}
public function testSearchFilterValidationInvalidCharacters(): void
{
$result = $this->get('expense?description=<script>alert(1)</script>');
$result->assertStatus(200);
$body = $result->getBody();
$this->assertStringContainsString('Description filter contains invalid characters.', $body);
}
}
}