Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev
This commit is contained in:
commit
beb8620926
10
.env.sample
10
.env.sample
@ -129,4 +129,12 @@ HEALTH_INDIA_TOKEN_URL =
|
||||
HEALTH_INDIA_USERNAME =
|
||||
HEALTH_INDIA_PASSWORD =
|
||||
|
||||
HEALTH_INDIA_PRIMARY_KEY_CONSTANT =
|
||||
HEALTH_INDIA_PRIMARY_KEY_CONSTANT =
|
||||
|
||||
#For sending mail for leads RFQ/QCR
|
||||
LEAD_INSURER_FROM_MAIL_ID =
|
||||
LEAD_CLIENT_FROM_MAIL_ID =
|
||||
|
||||
# BDS Daily Report Emails Configuration
|
||||
bds.dailyReportEmails =
|
||||
|
||||
|
||||
@ -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]],
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
51
app/Config/RfqConfig.php
Normal file
51
app/Config/RfqConfig.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
namespace Config;
|
||||
|
||||
use CodeIgniter\Config\BaseConfig;
|
||||
|
||||
class RfqConfig extends BaseConfig
|
||||
{
|
||||
/**
|
||||
* Google Drive parent folder IDs for RFQ and QCR sheets.
|
||||
*/
|
||||
public string $rfqParentFolderId = '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1';
|
||||
public string $qcrParentFolderId = '1MnYh5PTPDlc9mYMGsjmTv02y8EZ-BQf1';
|
||||
|
||||
/**
|
||||
* Default permissions for created sheets.
|
||||
* Copied from GoogleSheetController::$config.
|
||||
*/
|
||||
public array $permissions = [
|
||||
'editors' => [
|
||||
'vitvelz@gmail.com',
|
||||
'velz1990@gmail.com',
|
||||
'venkateshraman786@gmail.com',
|
||||
],
|
||||
'viewers' => [],
|
||||
];
|
||||
|
||||
/**
|
||||
* Default protections for RFQ sheets.
|
||||
* Copied from GoogleSheetController::$config.
|
||||
*/
|
||||
public array $protections = [
|
||||
[
|
||||
'range' => 'RFQ Page!B12:C12',
|
||||
'users' => [
|
||||
'velz1990@gmail.com',
|
||||
'vitvelz@gmail.com',
|
||||
'firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com',
|
||||
],
|
||||
'groups' => [],
|
||||
],
|
||||
[
|
||||
'range' => 'Claims Page!A1',
|
||||
'users' => [
|
||||
'velz1990@gmail.com',
|
||||
'venkateshraman786@gmail.com',
|
||||
'firebase-adminsdk-mcdfe@nhance-ee8d1.iam.gserviceaccount.com',
|
||||
],
|
||||
'groups' => [],
|
||||
],
|
||||
];
|
||||
}
|
||||
@ -444,11 +444,17 @@ $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->get('croneDailyActivityReport', 'DashboardController::croneDailyActivityReport');
|
||||
});
|
||||
|
||||
$routes->post("policy_tranction/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
|
||||
$routes->cli("cli/sendInstallmentRemainderMail","PolicyTransactionController::sendInstallmentRemainderMail");
|
||||
$routes->cli("cli/cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport");
|
||||
$routes->cli('cli/croneDailyActivityReport', 'DashboardController::croneDailyActivityReport');
|
||||
|
||||
|
||||
|
||||
$routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
|
||||
|
||||
@ -496,6 +502,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
|
||||
//$routes->post('failedStatement',"PolicyTransactionController::failedStatementList");
|
||||
});
|
||||
|
||||
$routes->get("cronDailyBDSReport", "PolicyTransactionController::cronDailyBDSReport");
|
||||
});
|
||||
|
||||
$routes->group("leads", ["filter" => "authMVC"], function ($routes) {
|
||||
@ -503,6 +510,8 @@ $routes->group("leads", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->match(['get', 'post'],"list", "LeadsController::viewLeadsList");
|
||||
$routes->post("create", "LeadsController::createLead");
|
||||
$routes->get("list/(:any)", "LeadsController::getLeadDataForEdit/$1");
|
||||
$routes->get("createRfqSheet", "LeadsController::createRfqSheet");
|
||||
$routes->get("mailTemplate", "LeadsController::getLeadMailTemplate");
|
||||
$routes->get("sendMail", "LeadsController::sendMailWithAttachement");
|
||||
$routes->post("sendMail", "LeadsController::sendMailWithAttachement");
|
||||
$routes->get("exportQCRandRFQ/(:any)", "LeadsController::exportQCRandRFQ/$1");
|
||||
@ -516,6 +525,10 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->post("createQCR", "LeadsController::createQCR");
|
||||
$routes->get("list/(:any)", "LeadsController::viewRFQ/$1");
|
||||
$routes->get("nonEB","LeadsController::rfqNonEB");
|
||||
// Non-EB dedicated RFQ/QCR endpoints (do not alter existing ones)
|
||||
$routes->get("nonEB/rfq/(:any)","LeadsController::viewNonEbRFQFromList/$1");
|
||||
$routes->get("nonEB/qcr/(:any)","LeadsController::viewNonEbQCRFromList/$1");
|
||||
$routes->get("placementData/(:num)", "LeadsController::getPlacementData/$1");
|
||||
});
|
||||
|
||||
|
||||
@ -545,9 +558,18 @@ $routes->cli("cli/sendCroneRemainderMail", "DashboardController::sendCroneRemain
|
||||
$routes->cli('cli/update-emp-policy-status', 'ClientController::updateEmpAndPolicyStatus');
|
||||
$routes->cli('cli/insurerRFQRemainder', 'LeadsController::remainderForQcr');
|
||||
$routes->cli('cli/sendMailWithAutoQuery','TicketController::sendMailWithAutoQuery');
|
||||
|
||||
// Claim Status Update every 2 hours
|
||||
$routes->cli('cli/MediAssit-ClaimStatusUpdate','MediAssistApiController::ClaimStatusUpdate');
|
||||
$routes->cli('cli/Vidal-ClaimStatusUpdate','VidalApiController::ClaimStatusUpdate');
|
||||
$routes->cli('cli/Fhpl-ClaimStatusUpdate','FhplApiController::ClaimStatusUpdate');
|
||||
$routes->cli('cli/HealthIndia-ClaimStatusUpdate','HealthIndiaApiController::ClaimStatusUpdate');
|
||||
|
||||
// Sync TPA Claims to Nhance
|
||||
$routes->cli('cli/MediAssit-syncTpaClaimToNhance','MediAssistApiController::syncTpaClaimToNhance');
|
||||
$routes->cli('cli/Fhpl-syncTpaClaimToNhance','FhplApiController::syncFhplClaimsToNhance');
|
||||
$routes->cli('cli/HealthIndia-syncTpaClaimToNhance','HealthIndiaApiController::syncHealthIndiaClaimsToNhance');
|
||||
|
||||
$routes->cli('cli/thzReminderCrone','ThzController::getOpenTicketsOlderThan24HoursAndAssignNextLevel');
|
||||
|
||||
|
||||
@ -764,6 +786,7 @@ $routes->group("/ticket", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->get('fetchVehiclePolicy/(:any)','TicketController::fetchVehiclePolicy/$1');
|
||||
$routes->post('saveIRDocsJson',"TicketController::saveIRDocsJson");
|
||||
$routes->get('getTpaClaimStatus',"ApiServiceController::getClaimStatus");
|
||||
$routes->post('manualTpaClaimPush',"ApiServiceController::manualTpaClaimPush");
|
||||
});
|
||||
|
||||
$routes->group("/claim_mis", ["filter" => "authMVC"], function ($routes) {
|
||||
@ -1011,4 +1034,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');
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -485,6 +485,35 @@ class ApiServiceController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual TPA Claim Push - accepts claim_id via POST and delegates to pushClaims().
|
||||
* Returns the response from pushClaims() as the API response.
|
||||
*/
|
||||
public function manualTpaClaimPush()
|
||||
{
|
||||
$claimId = $this->request->getPost('claim_id');
|
||||
|
||||
if (empty($claimId)) {
|
||||
return $this->response->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'claim_id is required'
|
||||
]);
|
||||
}
|
||||
|
||||
$result = $this->pushClaims($claimId);
|
||||
|
||||
if ($result !== null && is_array($result)) {
|
||||
return $this->response->setJSON([
|
||||
'status' => $result['status'] ?? false,
|
||||
'message' => $result['message'] ?? ($result['status'] ? 'Claim pushed successfully' : 'Claim push failed')
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => false,
|
||||
'message' => 'Claim push failed or TPA has no API service enabled for this ticket.'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
// public function getWellnessUrl()
|
||||
|
||||
@ -6969,16 +6969,23 @@ 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->tpaClaimDumpImporter(["file_id" => 48]); //vidal`
|
||||
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 57]); //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 --------------------------------------------------------------------------------
|
||||
|
||||
@ -7,6 +7,7 @@ use CodeIgniter\HTTP\RequestInterface;
|
||||
use CodeIgniter\HTTP\ResponseInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use App\Helpers\sendMailNotification;
|
||||
use App\Helpers\MailHelper;
|
||||
|
||||
use CodeIgniter\API\ResponseTrait;
|
||||
|
||||
@ -24,6 +25,11 @@ use App\Controllers\EmpDataServiceController;
|
||||
use App\Models\TicketMasterModel;
|
||||
use App\Models\LeadsModel;
|
||||
use App\Models\TicketClaimStatusModel;
|
||||
use App\Models\FileModel;
|
||||
use App\Models\BatchFileModel;
|
||||
use App\Models\SalesActivityModel;
|
||||
use App\Models\SalesActualLeadModel;
|
||||
|
||||
|
||||
|
||||
class DashboardController extends AdminController
|
||||
@ -43,6 +49,13 @@ class DashboardController extends AdminController
|
||||
protected $policyStatus;
|
||||
protected $colorShades;
|
||||
protected $claimDashLimit;
|
||||
protected $filesModel;
|
||||
protected $batchFilesModel;
|
||||
protected $leadsModel;
|
||||
protected $ticketMasterModel;
|
||||
protected $ticketClaimStatusModel;
|
||||
protected $salesActivityModel;
|
||||
protected $salesActualModel;
|
||||
|
||||
protected $myLogger;
|
||||
|
||||
@ -59,6 +72,13 @@ class DashboardController extends AdminController
|
||||
$this->ticketModel = new TicketMasterModel();
|
||||
$this->leadModel = new LeadsModel();
|
||||
$this->ticketStatusModel = new TicketClaimStatusModel();
|
||||
$this->filesModel = new FileModel();
|
||||
$this->batchFilesModel = new BatchFileModel();
|
||||
$this->leadsModel = new LeadsModel();
|
||||
$this->ticketMasterModel = new TicketMasterModel();
|
||||
$this->ticketClaimStatusModel = new TicketClaimStatusModel();
|
||||
$this->salesActivityModel = new SalesActivityModel();
|
||||
$this->salesActualModel = new SalesActualLeadModel();
|
||||
|
||||
$this->myLogger = \Config\Services::mylogger();
|
||||
|
||||
@ -787,4 +807,298 @@ class DashboardController extends AdminController
|
||||
$data['ticket_type_id'] = $ticketTypeId;
|
||||
return $this->respond(['status' => "success", "data" => $data], 200);
|
||||
}
|
||||
|
||||
public function croneDailyActivityReport()
|
||||
{
|
||||
|
||||
// $today = date('Y-m-d');
|
||||
$today = date('Y-m-d', strtotime('-1 day'));
|
||||
|
||||
// Inception & endorsement counts (file uploads)
|
||||
$total_inception_count = $this->filesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('action', 'inception')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
$total_endorsement_count = $this->filesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('action !=', 'inception')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
// TPA & Insurer batch file counts
|
||||
$total_tpa_incetion_count = $this->batchFilesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('event_type', 'inception')
|
||||
->where('insurer_or_tpa', 'tpa')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
$total_tpa_endorsement_count = $this->batchFilesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('event_type !=', 'inception')
|
||||
->where('insurer_or_tpa', 'tpa')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
$total_insurer_incetion_count = $this->batchFilesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('event_type', 'inception')
|
||||
->where('insurer_or_tpa', 'insurer')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
$total_insurer_endorsement_count = $this->batchFilesModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('event_type !=', 'inception')
|
||||
->where('insurer_or_tpa', 'insurer')
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
// Claim counts
|
||||
$total_claim_count = $this->ticketMasterModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->countAllResults();
|
||||
|
||||
$total_gmc_status_wise_claim_count = $this->ticketMasterModel
|
||||
->select('tcs.claim_status, count(*) as count')
|
||||
->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
|
||||
->where('ticket_master.is_active', 1)
|
||||
->where('ticket_master.ticket_type_id', 1)
|
||||
// ->where('DATE(ticket_master.created_at)', $today)
|
||||
->where('tcs.is_active', 1)
|
||||
->groupBy('tcs.claim_status')
|
||||
->findAll();
|
||||
|
||||
$total_gpa_status_wise_claim_count = $this->ticketMasterModel
|
||||
->select('tcs.claim_status, count(*) as count')
|
||||
->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
|
||||
->where('ticket_master.is_active', 1)
|
||||
->where('ticket_master.ticket_type_id', 2)
|
||||
// ->where('DATE(ticket_master.created_at)', $today)
|
||||
->where('tcs.is_active', 1)
|
||||
->groupBy('tcs.claim_status')
|
||||
->findAll();
|
||||
|
||||
$total_edli_status_wise_claim_count = $this->ticketMasterModel
|
||||
->select('tcs.claim_status, count(*) as count')
|
||||
->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
|
||||
->where('ticket_master.is_active', 1)
|
||||
->where('ticket_master.ticket_type_id', 3)
|
||||
// ->where('DATE(ticket_master.created_at)', $today)
|
||||
->where('tcs.is_active', 1)
|
||||
->groupBy('tcs.claim_status')
|
||||
->findAll();
|
||||
|
||||
$total_gtli_status_wise_claim_count = $this->ticketMasterModel
|
||||
->select('tcs.claim_status, count(*) as count')
|
||||
->join('ticket_claim_status tcs', 'tcs.id = ticket_master.claim_status_id', 'left')
|
||||
->where('ticket_master.is_active', 1)
|
||||
->where('ticket_master.ticket_type_id', 4)
|
||||
// ->where('DATE(ticket_master.created_at)', $today)
|
||||
->where('tcs.is_active', 1)
|
||||
->groupBy('tcs.claim_status')
|
||||
->findAll();
|
||||
|
||||
// Sales / lead counts
|
||||
$total_opportunity_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->countAllResults();
|
||||
|
||||
$total_rfq_created_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('status', 'rfq_created')
|
||||
->countAllResults();
|
||||
|
||||
$total_rfq_insurer_send_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('status', 'rfq_sent')
|
||||
->countAllResults();
|
||||
|
||||
$total_qcr_created_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('status', 'qcr_created')
|
||||
->countAllResults();
|
||||
|
||||
$total_qcr_client_send_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('status', 'qcr_sent')
|
||||
->countAllResults();
|
||||
|
||||
$total_placement_count = $this->leadsModel
|
||||
->where('is_active', 1)
|
||||
->where('DATE(created_at)', $today)
|
||||
->where('status', 'won')
|
||||
->countAllResults();
|
||||
|
||||
$total_activity_count = $this->salesActivityModel
|
||||
->where('DATE(created_at)', $today)
|
||||
->countAllResults();
|
||||
|
||||
$total_lead_count = $this->salesActualModel
|
||||
->where('DATE(created_at)', $today)
|
||||
->countAllResults();
|
||||
|
||||
$total_bds_count = $this->policyTransactionModel
|
||||
->select('pt_co_share_details.*')
|
||||
->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
|
||||
->where('DATE(policy_transaction.created_at)', $today)
|
||||
->where('pt_co_share_details.is_active', 1)
|
||||
->where('policy_transaction.is_active', 1)
|
||||
->countAllResults();
|
||||
|
||||
$total_bds_policy_wise_count = $this->policyTransactionModel
|
||||
->select('pt_co_share_details.*')
|
||||
->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
|
||||
->where('DATE(policy_transaction.created_at)', $today)
|
||||
->where('pt_co_share_details.is_active', 1)
|
||||
->where('policy_transaction.action_type', 'inception')
|
||||
->where('policy_transaction.is_active', 1)
|
||||
->countAllResults();
|
||||
|
||||
|
||||
$total_bds_endorsement_wise_count = $this->policyTransactionModel
|
||||
->select('pt_co_share_details.*')
|
||||
->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
|
||||
->where('DATE(policy_transaction.created_at)', $today)
|
||||
->where('pt_co_share_details.is_active', 1)
|
||||
->where('policy_transaction.action_type !=', 'inception')
|
||||
->where('policy_transaction.is_active', 1)
|
||||
->countAllResults();
|
||||
|
||||
$total_bds_policy_type_wise_count = $this->policyTransactionModel
|
||||
->select('policy_type.policy_type, count(*) as count')
|
||||
->join('pt_co_share_details', 'policy_transaction.id = pt_co_share_details.pt_id')
|
||||
->join('policy_type', 'policy_transaction.policy_type_id = policy_type.id')
|
||||
->where('DATE(policy_transaction.created_at)', $today)
|
||||
->where('pt_co_share_details.is_active', 1)
|
||||
->where('policy_transaction.is_active', 1)
|
||||
->groupBy('policy_transaction.policy_type_id')
|
||||
->findAll();
|
||||
|
||||
// Connect Pre DB for Employee Enrollment Count
|
||||
|
||||
$db2 = \Config\Database::connect('preDB');
|
||||
|
||||
$builder = $db2->table('employees');
|
||||
$total_employee_draft_count = $builder->join('employee_polices', 'employees.id = employee_polices.employee_id')
|
||||
->select('employee_polices.status, count(*) as count')
|
||||
->where('DATE(employee_polices.created_at)', $today)
|
||||
->where('employee_polices.is_active', 1)
|
||||
->where('employees.is_active', 1)
|
||||
->whereIn('employee_polices.status', ['draft'])
|
||||
->groupBy('employee_polices.status')
|
||||
->countAllResults();
|
||||
|
||||
|
||||
$builder1 = $db2->table('employees');
|
||||
$total_employee_enrolled_count = $builder1->join('employee_polices', 'employees.id = employee_polices.employee_id')
|
||||
->select('employee_polices.status, count(*) as count')
|
||||
->where('DATE(employee_polices.created_at)', $today)
|
||||
->where('employee_polices.is_active', 1)
|
||||
->where('employees.is_active', 1)
|
||||
->whereIn('employee_polices.status', ['enrolled'])
|
||||
->groupBy('employee_polices.status')
|
||||
->countAllResults();
|
||||
|
||||
$builder2 = $db2->table('files');
|
||||
$total_open_for_enrollemnt_policy_count = $builder2
|
||||
->select('COUNT(*) as count')
|
||||
->where('enrollment_open_date <=', $today)
|
||||
->where('enrollment_close_date >=', $today)
|
||||
->where('is_active', 1)
|
||||
->where('status', 'success')
|
||||
->countAllResults();
|
||||
|
||||
$data = [
|
||||
'total_inception_count' => $total_inception_count,
|
||||
'total_endorsement_count' => $total_endorsement_count,
|
||||
'total_tpa_incetion_count' => $total_tpa_incetion_count,
|
||||
'total_tpa_endorsement_count' => $total_tpa_endorsement_count,
|
||||
'total_insurer_incetion_count' => $total_insurer_incetion_count,
|
||||
'total_insurer_endorsement_count' => $total_insurer_endorsement_count,
|
||||
'total_claim_count' => $total_claim_count,
|
||||
'total_gmc_status_wise_claim_count' => $total_gmc_status_wise_claim_count,
|
||||
'total_gpa_status_wise_claim_count' => $total_gpa_status_wise_claim_count,
|
||||
'total_edli_status_wise_claim_count' => $total_edli_status_wise_claim_count,
|
||||
'total_gtli_status_wise_claim_count' => $total_gtli_status_wise_claim_count,
|
||||
'total_opportunity_count' => $total_opportunity_count,
|
||||
'total_rfq_created_count' => $total_rfq_created_count,
|
||||
'total_rfq_insurer_send_count' => $total_rfq_insurer_send_count,
|
||||
'total_qcr_created_count' => $total_qcr_created_count,
|
||||
'total_qcr_client_send_count' => $total_qcr_client_send_count,
|
||||
'total_placement_count' => $total_placement_count,
|
||||
'total_activity_count' => $total_activity_count,
|
||||
'total_lead_count' => $total_lead_count,
|
||||
'total_bds_count' => $total_bds_count,
|
||||
'total_bds_policy_wise_count' => $total_bds_policy_wise_count,
|
||||
'total_bds_endorsement_wise_count' => $total_bds_endorsement_wise_count,
|
||||
'total_bds_policy_type_wise_count' => $total_bds_policy_type_wise_count,
|
||||
'total_employee_draft_count' => $total_employee_draft_count,
|
||||
'total_employee_enrolled_count' => $total_employee_enrolled_count,
|
||||
'total_open_for_enrollemnt_policy_count' => $total_open_for_enrollemnt_policy_count,
|
||||
];
|
||||
|
||||
// dd($data);
|
||||
|
||||
$today = date('d-m-Y', strtotime($today));
|
||||
$data['today'] = $today;
|
||||
|
||||
// Render the HTML email using the dedicated view
|
||||
$message = view('daily_report_email_template', $data);
|
||||
// return $message;
|
||||
|
||||
// Recipients: prefer dedicated env, fallback to BDS report emails if not set
|
||||
$emailList = getenv('activity.dailyReportEmails') ?: getenv('bds.dailyReportEmails') ?: '';
|
||||
$recipientEmails = array_filter(array_map('trim', explode(',', $emailList)));
|
||||
|
||||
if (empty($recipientEmails)) {
|
||||
$this->myLogger->logme('error', 'croneDailyActivityReport: No recipients configured (set activity.dailyReportEmails in .env).');
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Daily activity data prepared but no recipients configured.',
|
||||
'data' => $data,
|
||||
], 200);
|
||||
}
|
||||
|
||||
$subject = "Daily Activity Report - {$today}";
|
||||
|
||||
$res = MailHelper::send_email([
|
||||
'mail' => $recipientEmails,
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
]);
|
||||
|
||||
$resDecoded = is_string($res) ? json_decode($res, true) : $res;
|
||||
|
||||
if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
|
||||
$this->myLogger->logme('error', 'croneDailyActivityReport: Report email sent to ' . count($recipientEmails) . ' recipients');
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Daily activity report emailed successfully.',
|
||||
'recipients' => count($recipientEmails),
|
||||
], 200);
|
||||
}
|
||||
|
||||
$this->myLogger->logme('error', 'croneDailyActivityReport: Email send failed - ' . json_encode($resDecoded));
|
||||
return $this->respond([
|
||||
'status' => false,
|
||||
'message' => 'Daily activity data prepared but email send failed.',
|
||||
'data' => $data,
|
||||
], 500);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -2418,6 +2418,11 @@ class EmployeeRestController extends AdminController
|
||||
["ticket_type" => "4", "type_name" => "GTLI"],
|
||||
];
|
||||
|
||||
$claim_type = $this->ticketController->claimType;
|
||||
unset($claim_type[1][2]);
|
||||
unset($claim_type[1][4]);
|
||||
$data['claim_type'] = $claim_type;
|
||||
|
||||
return $this->respond(['status' => (count($data) ? 'success' : 'failed'), 'code' => (count($data) ? 200 : 404), 'data' => $data], 200);
|
||||
}
|
||||
|
||||
@ -2581,61 +2586,90 @@ class EmployeeRestController extends AdminController
|
||||
|
||||
$required_docs = $this->ticketMaster->select('required_docs')->where('id', $ticket_id)->first();
|
||||
$data['required_docs'] = json_decode($required_docs['required_docs'] ?? '{}', true) ?? [];
|
||||
// $ticketData = $data['ticket_data'];
|
||||
// $ticketHistory = $data['ticket_history'];
|
||||
// print_r($ticketHistory); die;
|
||||
// print_rr($data['ticket_history']); die;
|
||||
|
||||
$filteredArray = array_filter($data['ticket_history'], function($item) {
|
||||
return $item['field_name'] == 'claim_status_id';
|
||||
});
|
||||
|
||||
$filteredArray = array_reverse(array_values($filteredArray));
|
||||
|
||||
$currentClaimStatus = $this->claimStatusModel->select("claim_status")->where('id', $data['claims_data']['claim_status_id'])->where('is_active', 1)->first();
|
||||
$ticketClaimStatus = $this->claimStatusModel->select('claim_status, display_name')->where('display_name is not null')->where('ticket_type', $data['claims_data']['ticket_type_id'])->where('is_active', 1)->findAll();
|
||||
$status_list = array_column($ticketClaimStatus, 'display_name', 'claim_status');
|
||||
// print_r($currentClaimStatus); die;
|
||||
// dd($filteredArray, $status_list); die;
|
||||
|
||||
$data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []);
|
||||
$filtered_history = [];
|
||||
$counter = 0;
|
||||
|
||||
foreach ($filteredArray as $key => $value) {
|
||||
foreach ($status_list as $status => $display_name) {
|
||||
if ($value['new_value'] == $status) {
|
||||
|
||||
// Loop through ticket_data
|
||||
foreach ($data['ticket_data'] as $status => &$fields) {
|
||||
// Search ticket_history for matching old_status_value
|
||||
foreach ($data['ticket_history'] as $history) {
|
||||
if($display_name == 'Under Process'){
|
||||
$unique_key = $display_name . str_repeat("\u{200B}", $counter++);
|
||||
}else{
|
||||
$unique_key = $display_name;
|
||||
}
|
||||
|
||||
if ($history['old_status_value'] === $status) {
|
||||
// Attach modified_by and created_at
|
||||
// $fields['modified_by'] = $history['modified_by'];
|
||||
$fields['modified_by'] = "";
|
||||
$fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at']));
|
||||
// Break after first match (assuming latest entry is enough)
|
||||
break;
|
||||
$filtered_history[$unique_key] = [
|
||||
'modified_by' => "",
|
||||
'modified_at' => date('d-m-Y h:i A', strtotime($value['created_at'])),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
// dd($filteredArray, $status_list, $filtered_history); die;
|
||||
|
||||
$data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = "";
|
||||
$data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at']));
|
||||
|
||||
$new_ticket_data = [];
|
||||
|
||||
foreach ($data['ticket_data'] as $oldKey => $value) {
|
||||
// $data['ticket_data'] = array_fill_keys(array_keys($data['ticket_data']), []);
|
||||
|
||||
// Only process if key exists in status_list
|
||||
if (! isset($status_list[$oldKey])) {
|
||||
continue; // skip and do NOT add to new array
|
||||
}
|
||||
// // Loop through ticket_data
|
||||
// foreach ($data['ticket_data'] as $status => &$fields) {
|
||||
// // Search ticket_history for matching old_status_value
|
||||
// foreach ($data['ticket_history'] as $history) {
|
||||
|
||||
// Get new key based on mapping
|
||||
$newKey = $status_list[$oldKey];
|
||||
// if ($history['old_status_value'] === $status) {
|
||||
// // Attach modified_by and created_at
|
||||
// // $fields['modified_by'] = $history['modified_by'];
|
||||
// $fields['modified_by'] = "";
|
||||
// $fields['modified_at'] = date('d-m-Y h:i A', strtotime($history['created_at']));
|
||||
// // Break after first match (assuming latest entry is enough)
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// Avoid duplicates
|
||||
if (! isset($new_ticket_data[$newKey])) {
|
||||
$new_ticket_data[$newKey] = $value;
|
||||
}
|
||||
}
|
||||
// $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_by'] = "";
|
||||
// $data['ticket_data'][$currentClaimStatus['claim_status']]['modified_at'] = $formatted = date('d-m-Y h:i A', strtotime($data['claims_data']['updated_at']));
|
||||
|
||||
uasort($new_ticket_data, function ($a, $b) {
|
||||
$timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']);
|
||||
$timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']);
|
||||
return $timeA <=> $timeB; // Ascending
|
||||
});
|
||||
// $new_ticket_data = [];
|
||||
|
||||
$data['ticket_data'] = $new_ticket_data;
|
||||
// foreach ($data['ticket_data'] as $oldKey => $value) {
|
||||
|
||||
// // Only process if key exists in status_list
|
||||
// if (! isset($status_list[$oldKey])) {
|
||||
// continue; // skip and do NOT add to new array
|
||||
// }
|
||||
|
||||
// // Get new key based on mapping
|
||||
// $newKey = $status_list[$oldKey];
|
||||
|
||||
// // Avoid duplicates
|
||||
// if (! isset($new_ticket_data[$newKey])) {
|
||||
// $new_ticket_data[$newKey] = $value;
|
||||
// }
|
||||
// }
|
||||
|
||||
// uasort($new_ticket_data, function ($a, $b) {
|
||||
// $timeA = \DateTime::createFromFormat('d-m-Y h:i A', $a['modified_at']);
|
||||
// $timeB = \DateTime::createFromFormat('d-m-Y h:i A', $b['modified_at']);
|
||||
// return $timeA <=> $timeB; // Ascending
|
||||
// });
|
||||
|
||||
// $data['ticket_data'] = $new_ticket_data;
|
||||
$data['ticket_data'] = $filtered_history;
|
||||
|
||||
$ticketMesssageModel = new TicketMessageModel();
|
||||
$ticket_message = $ticketMesssageModel
|
||||
@ -5451,7 +5485,7 @@ class EmployeeRestController extends AdminController
|
||||
'dashboard' => $database_id,
|
||||
],
|
||||
'exp' => time() + (10 * 60),
|
||||
'params' => (object) [],
|
||||
'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase
|
||||
];
|
||||
|
||||
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
|
||||
|
||||
412
app/Controllers/ExpenseController.php
Normal file
412
app/Controllers/ExpenseController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,7 @@ class FhplApiController extends BaseController
|
||||
|
||||
if (count($data) && $data['filePath'] == null) {
|
||||
log_message('error', "FHPL - Claim Push FAILED | claimId: '.$claimId.' - Claim or File Missing");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | Claim or File Missing'];
|
||||
}
|
||||
|
||||
// Build absolute file path
|
||||
@ -108,7 +108,7 @@ class FhplApiController extends BaseController
|
||||
|
||||
if (!file_exists($pdfPath)) {
|
||||
log_message('error', "FHPL - Claim Push FAILED | claimId: '.$claimId.' - PDF not found on server");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | PDF not found on server'];
|
||||
}
|
||||
|
||||
// Convert PDF to Base64
|
||||
@ -118,7 +118,7 @@ class FhplApiController extends BaseController
|
||||
$tokenResponse = json_decode($this->generateAuthToken()->getBody(), true);
|
||||
if (empty($tokenResponse['data']['access_token'])) {
|
||||
log_message('error', "FHPL - Claim Push FAILED | claimId: '.$claimId.' - FHPL Token generation failed");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | FHPL Token generation failed'];
|
||||
}
|
||||
$token = $tokenResponse['data']['access_token'];
|
||||
|
||||
@ -163,7 +163,7 @@ class FhplApiController extends BaseController
|
||||
$this->db->table('ticket_master')
|
||||
->where('id',$claimId)
|
||||
->update([ 'tpa_push_response' => json_encode($response) ]);
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | API call failed'];
|
||||
}
|
||||
|
||||
|
||||
@ -185,14 +185,14 @@ class FhplApiController extends BaseController
|
||||
]);
|
||||
|
||||
log_message('error', 'FHPL - Claim Push SUCCESS | claimId: '.$claimId.' | claimNO: '.$fhplClaimNo);
|
||||
|
||||
return ['status' => true, 'message' => 'Claim Push SUCCESS', 'response' => $response];
|
||||
}else {
|
||||
log_message('error', 'FHPL - Claim Push API SUCCESS BUT claimsInfo EMPTY | response: '.json_encode($response));
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API SUCCESS BUT claimsInfo EMPTY'];
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API SUCCESS BUT claimsInfo EMPTY'];
|
||||
// return $this->response->setJSON($response);
|
||||
}
|
||||
|
||||
@ -288,7 +288,10 @@ class FhplApiController extends BaseController
|
||||
$tickets = $this->db->table('ticket_master tm')
|
||||
->select("tm.id,tm.tpa_claim_id,cp.policy_no")
|
||||
->join('client_policy cp','tm.client_policy_id=cp.id')
|
||||
->where('tm.tpa_claim_id IS NOT NULL')
|
||||
->where('tm.tpa_claim_push_reference_no IS NOT NULL')
|
||||
->whereNotIn('tm.claim_status_id', [8, 11, 12, 13, 66, 22, 24, 47, 49, 32, 34, 53, 55, 42, 44, 58, 60])
|
||||
->where('tm.is_active', 1)
|
||||
->where('cp.tpa_id', $this->fhplTpaId)
|
||||
->get()->getResultArray();
|
||||
|
||||
$count=0;
|
||||
@ -714,7 +717,7 @@ class FhplApiController extends BaseController
|
||||
// ];
|
||||
// }
|
||||
|
||||
public function syncFhplClaimsToNhance()
|
||||
public function syncFhplClaimsToNhanceOld()
|
||||
{
|
||||
helper('api');
|
||||
|
||||
@ -790,6 +793,201 @@ class FhplApiController extends BaseController
|
||||
return ['status'=>true,'total'=>count($finalResult)];
|
||||
}
|
||||
|
||||
public function syncFhplClaimsToNhance()
|
||||
{
|
||||
helper('api');
|
||||
|
||||
try {
|
||||
|
||||
// Generate FHPL Token
|
||||
$tokenResponse = json_decode($this->generateAuthToken()->getBody(), true);
|
||||
|
||||
if (empty($tokenResponse['data']['access_token'])) {
|
||||
return $this->response->setJSON(['status' => false,'message' => 'FHPL Token generation failed']);
|
||||
}
|
||||
|
||||
$token = $tokenResponse['data']['access_token'];
|
||||
|
||||
$url = getenv('FHPL_BASE_URL')."/api/GetTPA_ClaimsDetails";
|
||||
|
||||
$headers = [
|
||||
"Authorization: Bearer ".$token,
|
||||
"Content-Type: application/json"
|
||||
];
|
||||
|
||||
$policies = $this->db->table('client_policy')
|
||||
->where('tpa_id',$this->fhplTpaId)
|
||||
->get()->getResultArray();
|
||||
|
||||
$finalResult=[];
|
||||
foreach($policies as $policy){
|
||||
|
||||
$body = [
|
||||
"UserName" => getenv('FHPL_USER_NAME'),
|
||||
"Password" => getenv('FHPL_PASSWORD'),
|
||||
"PolicyNumber" => $policy['policy_no'],
|
||||
"Fromdate" => $policy['policy_start_date'],
|
||||
"Todate" => $policy['policy_end_date']
|
||||
];
|
||||
|
||||
$response = call_third_party_api($url,'POST',$headers,$body);
|
||||
|
||||
if(!empty($response['data'])){
|
||||
log_message('error', 'FHPL - Sync TPA Claims | Exception thrown while calling GetTPA_ClaimsDetails API: ' . json_encode($response));
|
||||
$finalResult = array_merge($finalResult,$response['data']);
|
||||
}
|
||||
}
|
||||
|
||||
$insertedCount = 0;
|
||||
|
||||
// Insert into ticket_master with mandatory columns (reference: MediAssist syncTpaClaimToNhance)
|
||||
foreach($finalResult as $row){
|
||||
|
||||
$status = $row['CLAIM_STATUS'] ?? null;
|
||||
|
||||
$map = [
|
||||
"Under Process"=>5,
|
||||
"Paid"=>11,
|
||||
"Rejected"=>8,
|
||||
"Approved"=>8
|
||||
];
|
||||
|
||||
$claimStatus = $map[$status] ?? 61;
|
||||
|
||||
// Derive relationship (default to self)
|
||||
$relationship = map_relationship(trim($row['RELATION'] ?? 'self'));
|
||||
|
||||
// Fetch client policy details
|
||||
$clientpolicy = $this->db->table('client_policy cp')
|
||||
->select("
|
||||
cp.id as client_policy_id,
|
||||
cp.client_id ,
|
||||
cp.insurer_id ,
|
||||
cp.tpa_id ,
|
||||
client_rm.id as acm_id
|
||||
")
|
||||
->join('client_rm', 'client_rm.client_id = cp.client_id AND client_rm.level = 3', 'left')
|
||||
->where('cp.policy_no', $row['POLICY_NO'] ?? null)
|
||||
->orderBy('client_rm.id','DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$clientpolicy) {
|
||||
log_message(
|
||||
'error',
|
||||
'FHPL - Sync TPA Claims | Client policy not found for policy_no: ' . ($row['POLICY_NO'] ?? 'N/A')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch employee / insured details
|
||||
$employee = $this->db->table('employees e')
|
||||
->select("
|
||||
e.id as emp_id,
|
||||
e.emp_code ,
|
||||
e.name as emp_name,
|
||||
e2.id as insured_emp_id,
|
||||
e2.name as insured_emp_name,
|
||||
ep.tpa_id as tpa_no
|
||||
")
|
||||
->join(
|
||||
'employees e2',
|
||||
"e2.emp_code = e.emp_code AND e2.relationship = ".$this->db->escape($relationship),
|
||||
'left'
|
||||
)
|
||||
->join( 'employee_polices ep', "ep.employee_id = e2.id ", 'left' )
|
||||
->where('e.emp_code', $row['EMPLOYEE_NO'] ?? null)
|
||||
->where('e.client_id', $clientpolicy['client_id'] ?? null)
|
||||
->where('e.relationship', 'self')
|
||||
->where('e.is_active', 1)
|
||||
->where('e2.is_active', 1)
|
||||
->where('ep.is_active', 1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$employee) {
|
||||
log_message(
|
||||
'error',
|
||||
'FHPL - Sync TPA Claims | Employee data not found for emp_code: ' . ($row['EMPLOYEE_NO'] ?? 'N/A') .
|
||||
' | policy_no: ' . ($row['POLICY_NO'] ?? 'N/A') .
|
||||
' | relationship: ' . $relationship
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$claimData = [
|
||||
// Core
|
||||
'ticket_type_id' => 1,
|
||||
'claim_status_id' => $claimStatus,
|
||||
'policy_no' => $row['POLICY_NO'] ?? null,
|
||||
'claim_number' => $row['CLAIM_ID'] ?? null,
|
||||
'tpa_claim_id' => $row['CLAIM_ID'] ?? null,
|
||||
|
||||
// local primary ids
|
||||
'tpa_id' => $clientpolicy['tpa_id'] ?? $this->fhplTpaId,
|
||||
'insurer_id' => $clientpolicy['insurer_id'] ?? null,
|
||||
'client_policy_id' => $clientpolicy['client_policy_id'] ?? null,
|
||||
'client_id' => $clientpolicy['client_id'] ?? null,
|
||||
'acm_id' => $clientpolicy['acm_id'] ?? null,
|
||||
|
||||
// Employee / Insured
|
||||
'emp_id' => $employee['emp_id'] ?? null,
|
||||
'insured_emp_id' => $employee['insured_emp_id'] ?? null,
|
||||
'tpa_no' => $employee['tpa_no'] ?? null,
|
||||
'emp_code' => $row['EMPLOYEE_NO'] ?? null,
|
||||
'emp_name' => $employee['emp_name'] ?? null,
|
||||
'insured_name' => $row['insured_emp_name'] ?? null,
|
||||
'relationship' => $relationship,
|
||||
|
||||
// Claim info
|
||||
'claim_type' => 1,
|
||||
'mode_of_intimation' => 5,
|
||||
'claim_amount' => $row['CLAIM_AMOUNT'] ?? null,
|
||||
|
||||
// Dates
|
||||
'doa' => change_date_format($row['DATE_OF_ADMISSION'] ?? '', null, 'Y-m-d') ?? null,
|
||||
'dod' => change_date_format($row['DATE_OF_DISCHARGE'] ?? '', null, 'Y-m-d') ?? null,
|
||||
|
||||
// Hospital
|
||||
'hospital_name' => $row['HOSPITAL_NAME'] ?? null,
|
||||
'hospital_state' => $row['HOSPITAL_STATE'] ?? null,
|
||||
'hospital_city' => $row['HOSPITAL_CITY'] ?? null,
|
||||
'hospital_address' => $row['Hospital Address'] ?? null,
|
||||
'hospital_pincode' => $row['Hospital Pincode'] ?? null,
|
||||
|
||||
'registration_date' => $row['CLAIM_REGISTERED_DATE'] ?? null,
|
||||
|
||||
// Others
|
||||
'tpa_claim_type' => $row['CLAIM_TYPE'] ?? null,
|
||||
'tpa_ailments' => $row['AILMENT'] ?? null,
|
||||
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$this->db->table('ticket_master')->insert($claimData);
|
||||
|
||||
$insertedCount++;
|
||||
}
|
||||
|
||||
log_message('error', 'FHPL - Sync TPA Claims | Fetched Data | Inserted Data: ' . count($finalResult) . ' | Inserted: ' . $insertedCount);
|
||||
return ['status'=>true,'total'=>count($finalResult), 'inserted'=>$insertedCount];
|
||||
|
||||
} catch (\Throwable $th) {
|
||||
$errorData = [
|
||||
'message' => $th->getMessage(),
|
||||
'file' => $th->getFile(),
|
||||
'line' => $th->getLine(),
|
||||
'code' => $th->getCode(),
|
||||
'trace' => $th->getTraceAsString(),
|
||||
'trace_array' => $th->getTrace(), // full array version (optional)
|
||||
'function' => $th->getTrace()[0]['function'] ?? null,
|
||||
'class' => $th->getTrace()[0]['class'] ?? null,
|
||||
];
|
||||
log_message('error', 'FHPL - Sync TPA Claims | Exception thrown: ' . json_encode($errorData));
|
||||
return ['status'=>false,'message'=>$th->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function saveFhplAPIData($array)
|
||||
{
|
||||
$file_id = $array['file_id'];
|
||||
|
||||
@ -123,12 +123,12 @@ class HealthIndiaApiController extends BaseController
|
||||
|
||||
if (!$data) {
|
||||
log_message('error', "HEALTH_INDIA - Claim Push FAILED | claimId: {$claimId} - Claim not found");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | Claim not found'];
|
||||
}
|
||||
|
||||
if ($data['filePath'] == null) {
|
||||
log_message('error', "HEALTH_INDIA - Claim Push FAILED | claimId: {$claimId} - File Missing");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | File Missing'];
|
||||
}
|
||||
|
||||
// Build absolute file path
|
||||
@ -137,7 +137,7 @@ class HealthIndiaApiController extends BaseController
|
||||
|
||||
if (!file_exists($pdfPath)) {
|
||||
log_message('error', "HEALTH_INDIA - Claim Push FAILED | claimId: {$claimId} - PDF not found on server at path: {$pdfPath}");
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | PDF not found on server'];
|
||||
}
|
||||
|
||||
// Convert PDF to Base64
|
||||
@ -217,7 +217,7 @@ class HealthIndiaApiController extends BaseController
|
||||
$this->db->table('ticket_master')
|
||||
->where('id', $claimId)
|
||||
->update(['tpa_push_response' => json_encode($response)]);
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | API call failed'];
|
||||
}
|
||||
|
||||
if ($response['status'] === true && !empty($response['data']['result'][0]['ccn'])) {
|
||||
@ -234,12 +234,13 @@ class HealthIndiaApiController extends BaseController
|
||||
]);
|
||||
|
||||
log_message('error', 'HEALTH_INDIA - Claim Push SUCCESS | claimId: ' . $claimId . ' | CCN: ' . $ccn . ' | CCN_EXT: ' . $ccnExt);
|
||||
return ['status' => true, 'message' => 'Claim Push SUCCESS', 'response' => $response];
|
||||
} else {
|
||||
log_message('error', 'HEALTH_INDIA - Claim Push API SUCCESS BUT CCN EMPTY | claimId: ' . $claimId . ' | response: ' . json_encode($response));
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API SUCCESS BUT CCN EMPTY'];
|
||||
}
|
||||
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API SUCCESS BUT CCN EMPTY'];
|
||||
}
|
||||
|
||||
public function ClaimDetail($claimId = null)
|
||||
@ -355,7 +356,9 @@ class HealthIndiaApiController extends BaseController
|
||||
$tickets = $this->db->table('ticket_master tm')
|
||||
->select("tm.id, tm.tpa_claim_id, cp.policy_no")
|
||||
->join('client_policy cp', 'tm.client_policy_id=cp.id')
|
||||
->where('tm.tpa_claim_id IS NOT NULL')
|
||||
->where('tm.tpa_claim_push_reference_no IS NOT NULL')
|
||||
->whereNotIn('tm.claim_status_id', [8, 11, 12, 13, 66, 22, 24, 47, 49, 32, 34, 53, 55, 42, 44, 58, 60])
|
||||
->where('tm.is_active', 1)
|
||||
->where('cp.tpa_id', $this->healthIndiaTpaId)
|
||||
->get()->getResultArray();
|
||||
|
||||
@ -722,7 +725,7 @@ class HealthIndiaApiController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function syncHealthIndiaClaimsToNhance()
|
||||
public function syncHealthIndiaClaimsToNhanceOld()
|
||||
{
|
||||
helper('api');
|
||||
|
||||
@ -820,6 +823,217 @@ class HealthIndiaApiController extends BaseController
|
||||
]);
|
||||
}
|
||||
|
||||
public function syncHealthIndiaClaimsToNhance()
|
||||
{
|
||||
helper('api');
|
||||
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Started');
|
||||
|
||||
// Generate Health India Token
|
||||
$tokenResponse = json_decode($this->generateAuthToken()->getBody(), true);
|
||||
|
||||
if (empty($tokenResponse['data']['result'][0]['access_token'])) {
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims FAILED | Token generation failed');
|
||||
return $this->response->setJSON(['status' => false, 'message' => 'Token generation failed']);
|
||||
}
|
||||
|
||||
$token = $tokenResponse['data']['result'][0]['access_token'];
|
||||
|
||||
$url = getenv('HEALTH_INDIA_BASE_URL') . "/ClaimsMIS/GetClaimsMIS";
|
||||
|
||||
$headers = [
|
||||
"Authorization: Bearer " . $token,
|
||||
"Content-Type: application/json"
|
||||
];
|
||||
|
||||
$policies = $this->db->table('client_policy')
|
||||
->where('tpa_id', $this->healthIndiaTpaId)
|
||||
->get()->getResultArray();
|
||||
|
||||
$finalResult = [];
|
||||
|
||||
foreach ($policies as $policy) {
|
||||
$body = [
|
||||
"policY_NUMBER" => $policy['policy_no']
|
||||
];
|
||||
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Fetching for policy: ' . $policy['policy_no']);
|
||||
|
||||
$response = call_third_party_api($url, 'POST', $headers, $body);
|
||||
|
||||
if (!empty($response['data']['result'])) {
|
||||
$finalResult = array_merge($finalResult, $response['data']['result']);
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Fetched ' . count($response['data']['result']) . ' claims for policy: ' . $policy['policy_no']);
|
||||
}
|
||||
}
|
||||
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Total claims fetched: ' . count($finalResult));
|
||||
|
||||
$insertedCount = 0;
|
||||
|
||||
foreach ($finalResult as $row) {
|
||||
$status = $row['Claim_Status'] ?? 'Under Process';
|
||||
|
||||
$map = [
|
||||
"Under Process" => 5,
|
||||
"Pending for Bill Entry" => 5,
|
||||
"Paid" => 11,
|
||||
"Rejected" => 8,
|
||||
"Approved" => 8,
|
||||
"Outstanding" => 5,
|
||||
];
|
||||
|
||||
$claimStatus = $map[$status] ?? 1;
|
||||
|
||||
// Check if claim already exists
|
||||
$existing = $this->db->table('ticket_master')
|
||||
->where('tpa_claim_id', $row['CLAIM_NUMBER'])
|
||||
->get()->getRowArray();
|
||||
|
||||
if ($existing) {
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Skipped existing claim: ' . ($row['CLAIM_NUMBER'] ?? 'N/A'));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Derive relationship (default to self)
|
||||
$relationship = 'self';
|
||||
$rawRelation = $row['RELATION_NAME'] ?? null;
|
||||
if (!empty($rawRelation)) {
|
||||
if ($rawRelation === 'Employee') {
|
||||
$relationship = 'self';
|
||||
} elseif (strtoupper($rawRelation) === 'WIFE') {
|
||||
$relationship = 'spouse';
|
||||
} else {
|
||||
$relationship = strtolower($rawRelation);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch client policy details
|
||||
$clientpolicy = $this->db->table('client_policy cp')
|
||||
->select("
|
||||
cp.id as client_policy_id,
|
||||
cp.client_id,
|
||||
cp.insurer_id,
|
||||
cp.tpa_id,
|
||||
client_rm.id as acm_id
|
||||
")
|
||||
->join('client_rm', 'client_rm.client_id = cp.client_id AND client_rm.level = 3', 'left')
|
||||
->where('cp.policy_no', $row['Policy_No'] ?? null)
|
||||
->orderBy('client_rm.id', 'DESC')
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$clientpolicy) {
|
||||
log_message(
|
||||
'error',
|
||||
'HEALTH_INDIA - Sync TPA Claims | Client policy not found for policy_no: ' . ($row['Policy_No'] ?? 'N/A')
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch employee / insured details
|
||||
$employee = $this->db->table('employees e')
|
||||
->select("
|
||||
e.id as emp_id,
|
||||
e.emp_code,
|
||||
e.name as emp_name,
|
||||
e2.id as insured_emp_id,
|
||||
e2.name as insured_emp_name,
|
||||
ep.tpa_id as tpa_no,
|
||||
e.mobile as emp_mobile,
|
||||
e.email_corporate as emp_mail
|
||||
")
|
||||
->join(
|
||||
'employees e2',
|
||||
"e2.emp_code = e.emp_code AND e2.relationship = " . $this->db->escape($relationship),
|
||||
'left'
|
||||
)
|
||||
->join('employee_polices ep', "ep.employee_id = e2.id ", 'left')
|
||||
->where('e.emp_code', $row['Employee_Code'] ?? null)
|
||||
->where('e.client_id', $clientpolicy['client_id'] ?? null)
|
||||
->where('e.relationship', 'self')
|
||||
->where('e.is_active', 1)
|
||||
->where('e2.is_active', 1)
|
||||
->where('ep.is_active', 1)
|
||||
->get()
|
||||
->getRowArray();
|
||||
|
||||
if (!$employee) {
|
||||
log_message(
|
||||
'error',
|
||||
'HEALTH_INDIA - Sync TPA Claims | Employee data not found for emp_code: ' . ($row['Employee_Code'] ?? 'N/A') .
|
||||
' | policy_no: ' . ($row['Policy_No'] ?? 'N/A') .
|
||||
' | relationship: ' . $relationship
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$claimData = [
|
||||
// Core
|
||||
'ticket_type_id' => 1,
|
||||
'claim_status_id' => $claimStatus,
|
||||
'policy_no' => $row['Policy_No'] ?? null,
|
||||
'claim_number' => $row['CLAIM_NUMBER'] ?? null,
|
||||
'tpa_claim_id' => $row['CLAIM_NUMBER'] ?? null,
|
||||
|
||||
// Local primary/foreign keys
|
||||
'tpa_id' => $clientpolicy['tpa_id'] ?? $this->healthIndiaTpaId,
|
||||
'insurer_id' => $clientpolicy['insurer_id'] ?? null,
|
||||
'client_policy_id' => $clientpolicy['client_policy_id'] ?? null,
|
||||
'client_id' => $clientpolicy['client_id'] ?? null,
|
||||
'acm_id' => $clientpolicy['acm_id'] ?? null,
|
||||
|
||||
// Employee / Insured
|
||||
'emp_id' => $employee['emp_id'] ?? null,
|
||||
'insured_emp_id' => $employee['insured_emp_id'] ?? null,
|
||||
'tpa_no' => $employee['tpa_no'] ?? null,
|
||||
'emp_code' => $row['Employee_Code'] ?? null,
|
||||
'emp_name' => $employee['emp_name'] ?? null,
|
||||
'insured_name' => $employee['insured_emp_name'] ?? ($row['PATIENT_NAME'] ?? null),
|
||||
'relationship' => $relationship,
|
||||
'emp_mobile' => $employee['emp_mobile'] ?? null,
|
||||
'emp_mail' => $employee['emp_mail'] ?? null,
|
||||
|
||||
// Claim info
|
||||
'claim_type' => 1,
|
||||
'mode_of_intimation' => 5,
|
||||
'claim_amount' => $row['INTIMATED_AMOUNT'] ?? 0,
|
||||
|
||||
// Dates
|
||||
'doa' => !empty($row['DATEOF_ADMISSION']) ? date('Y-m-d', strtotime($row['DATEOF_ADMISSION'])) : null,
|
||||
'dod' => !empty($row['DATEOF_DISCHARGE']) ? date('Y-m-d', strtotime($row['DATEOF_DISCHARGE'])) : null,
|
||||
|
||||
// Hospital
|
||||
'hospital_name' => $row['HOSPITAL_NAME'] ?? null,
|
||||
'hospital_address' => $row['Hospital_address'] ?? null,
|
||||
'hospital_pincode' => $row['HOSPITAL_Pincode'] ?? null,
|
||||
'hospital_state' => $row['HOSPITAL_STATE'] ?? null,
|
||||
'hospital_city' => $row['HOSPITAL_CITY'] ?? null,
|
||||
|
||||
// TPA extras
|
||||
'tpa_claim_status' => $status,
|
||||
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$this->db->table('ticket_master')->insert($claimData);
|
||||
$insertedCount++;
|
||||
|
||||
log_message(
|
||||
'error',
|
||||
'HEALTH_INDIA - Sync TPA Claims | Inserted claim: ' . ($row['CLAIM_NUMBER'] ?? 'N/A')
|
||||
);
|
||||
}
|
||||
|
||||
log_message('error', 'HEALTH_INDIA - Sync TPA Claims | Completed | Total: ' . count($finalResult) . ' | Inserted: ' . $insertedCount);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => true,
|
||||
'total' => count($finalResult),
|
||||
'inserted' => $insertedCount
|
||||
]);
|
||||
}
|
||||
|
||||
public function saveHealthIndiaAPIData($array)
|
||||
{
|
||||
$file_id = $array['file_id'];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -141,7 +141,7 @@ class MediAssistApiController extends BaseController
|
||||
$this->db->table('ticket_master')
|
||||
->where('id',$claimId)
|
||||
->update([ 'tpa_push_response' => json_encode($response) ]);
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED', 'response' => $response];
|
||||
}
|
||||
|
||||
// return $this->response->setJSON($response);
|
||||
@ -156,11 +156,11 @@ class MediAssistApiController extends BaseController
|
||||
->where('id',$claimId)
|
||||
->update([ 'tpa_claim_push_reference_no' => $claimRef ]);
|
||||
|
||||
return;
|
||||
return ['status' => true, 'message' => 'Claim Push SUCCESS', 'response' => $response];
|
||||
|
||||
} else {
|
||||
log_message('error','MEDI_ASSIST - Claim Push API Failed | claimReferenceNo EMPTY | claimId: '.$claimId.' | response: '.json_encode($response));
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API Failed', 'response' => $response];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -317,8 +317,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'Q',
|
||||
'col_name' => 'Base Premium',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -329,8 +329,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'R',
|
||||
'col_name' => 'Non commission permium Amount',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -341,8 +341,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'S',
|
||||
'col_name' => 'TP Premium',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -353,8 +353,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'T',
|
||||
'col_name' => 'IGST',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_gst_percentage',
|
||||
'params' => ['row']
|
||||
@ -365,8 +365,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'U',
|
||||
'col_name' => 'CGST',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_gst_percentage',
|
||||
'params' => ['row']
|
||||
@ -377,8 +377,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'V',
|
||||
'col_name' => 'SGST',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_gst_percentage',
|
||||
'params' => ['row']
|
||||
@ -390,8 +390,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'W',
|
||||
'col_name' => 'Stamp Duty',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -414,8 +414,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'X',
|
||||
'col_name' => 'Agreed Amount',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -426,8 +426,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'Y',
|
||||
'col_name' => 'Agreed BP Percentage',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -438,8 +438,8 @@ class PolicyTransactionController extends BaseController
|
||||
'col_cell_name' => 'Z',
|
||||
'col_name' => 'Agreed TP Percentage',
|
||||
'is_mandatory' => false,
|
||||
'data_type' => '',
|
||||
'format' => null,
|
||||
'data_type' => 'positive_number',
|
||||
'format' => 'positive_number',
|
||||
'allowed_values' => null,
|
||||
'custom' => null,
|
||||
'params' => null
|
||||
@ -6098,8 +6098,189 @@ class PolicyTransactionController extends BaseController
|
||||
return array('status' => false, 'message' => 'file_id is required');
|
||||
}
|
||||
|
||||
$this->policyTransactionModel->select()->findAll();
|
||||
$data['is_active'] = 0;
|
||||
$this->policyTransactionModel->where('file_id', $file_id)->set($data)->update();
|
||||
$this->PTCOShareDetailsModel->where('file_id', $file_id)->set($data)->update();
|
||||
|
||||
return $this->respond(['status' => true, 'code' => 200, 'message' => 'BDS bulk upload data truncated successfully'], 200);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Cron job: Daily BDS Report
|
||||
* Fetches today's BDS entries, generates Excel (matching report_bds.php column order), and emails to recipients from .env.
|
||||
* Recipients: comma-separated emails in bds.reportEmails
|
||||
* File stored temporarily in writable/tmp/ and deleted after sending.
|
||||
*/
|
||||
public function cronDailyBDSReport()
|
||||
{
|
||||
|
||||
helper('excel_import_export_helper');
|
||||
|
||||
$filePath = null;
|
||||
try {
|
||||
|
||||
$today = date('Y-m-d', strtotime('-1 day'));
|
||||
$reportList = $this->policyTransactionModel->getBDSReportList($today, $today,0,0,0,'created_at',0,0,0,0,0,[]);
|
||||
|
||||
if (empty($reportList)) {
|
||||
$this->myLogger->logme('error', "cronDailyBDSReport: No BDS records for {$today}");
|
||||
return $this->respond(['status' => 'success', 'message' => 'No BDS records for today. No report sent.'], 200);
|
||||
}
|
||||
|
||||
// Column headers exactly matching report_bds.php order (including display:none columns)
|
||||
$headers = [
|
||||
'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type',
|
||||
'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch',
|
||||
'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks',
|
||||
'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %',
|
||||
'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount',
|
||||
'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date',
|
||||
'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager',
|
||||
'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)',
|
||||
'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST',
|
||||
'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount',
|
||||
'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount',
|
||||
'CD Account No'
|
||||
];
|
||||
|
||||
$excelData = [];
|
||||
foreach ($reportList as $idx => $row) {
|
||||
$totalIrda = (float)($row['total_irda_amt'] ?? 0);
|
||||
$billedAmt = (float)($row['billed_amt'] ?? 0);
|
||||
$unbilledAmt = $totalIrda - $billedAmt;
|
||||
if ($totalIrda == 0) {
|
||||
$unbilledAmt = abs($unbilledAmt);
|
||||
}
|
||||
$unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt;
|
||||
|
||||
$hasIrda = ($totalIrda != 0);
|
||||
|
||||
$excelData[] = [
|
||||
$idx + 1,
|
||||
$row['user_name'] ?? 'N/A',
|
||||
$row['policy_issue_month'] ?? 'N/A',
|
||||
$row['revenue_type'] ?? 'N/A',
|
||||
$row['client_type'] ?? 'N/A',
|
||||
$row['client_name'] ?? 'N/A',
|
||||
$row['action_type'] ?? 'N/A',
|
||||
$row['policy_type'] ?? 'N/A',
|
||||
$row['bap'] ?? 'N/A',
|
||||
$row['vehicle_no'] ?? 'N/A',
|
||||
$row['policy_no'] ?? 'N/A',
|
||||
$row['endorsement_no'] ?? 'N/A',
|
||||
$row['insurer_branch_name'] ?? 'N/A',
|
||||
!empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['ref'] ?? 'N/A',
|
||||
$row['remarks'] ?? 'N/A',
|
||||
$hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%',
|
||||
$hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%',
|
||||
$row['reward'] ?? '0.00',
|
||||
$row['total_irda_amt'] ?? '0.00',
|
||||
!empty($row['billed_amt']) ? $row['billed_amt'] : '0.00',
|
||||
number_format((float)$unbilledAmt, 2, '.', ''),
|
||||
$row['salse_person_name'] ?? 'N/A',
|
||||
$row['service_person_name'] ?? 'N/A',
|
||||
$row['nhance_branch'] ?? 'N/A',
|
||||
$row['installment'] ?? 'N/A',
|
||||
!empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['co_share'] ?? 'No',
|
||||
$row['bro_payable_by'] ?? 'No',
|
||||
$row['salse_manager_name'] ?? 'N/A',
|
||||
$row['service_manager_name'] ?? 'N/A',
|
||||
$row['service_branch'] ?? 'N/A',
|
||||
!empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['policy_holder_name'] ?? 'N/A',
|
||||
$row['same_as_proposer'] ?? 'No',
|
||||
$row['follower_policy_no'] ?? 'N/A',
|
||||
number_format((float)($row['co_share_per'] ?? 0), 2),
|
||||
number_format((float)($row['non_comm_per_amt'] ?? 0), 2),
|
||||
number_format((float)($row['bp_cgst'] ?? 0), 2),
|
||||
number_format((float)($row['bp_sgst'] ?? 0), 2),
|
||||
number_format((float)($row['bp_igst'] ?? 0), 2),
|
||||
number_format((float)($row['stamp_duty'] ?? 0), 2),
|
||||
number_format((float)($row['standerd_bp_per'] ?? 0), 2),
|
||||
number_format((float)($row['standerd_tp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_bp_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_bp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2),
|
||||
$row['cd_ac_no'] ?? 'N/A'
|
||||
];
|
||||
}
|
||||
|
||||
$today = date('d-m-Y', strtotime($today));
|
||||
$fileName = 'BDS_Daily_Report_' . $today . '.xlsx';
|
||||
$tmpDir = WRITEPATH . 'tmp' . DIRECTORY_SEPARATOR;
|
||||
if (!is_dir($tmpDir)) {
|
||||
mkdir($tmpDir, 0755, true);
|
||||
}
|
||||
$filePath = $tmpDir . $fileName;
|
||||
|
||||
$generated = generate_excel($headers, $excelData, $filePath);
|
||||
if (!$generated) {
|
||||
$this->myLogger->logme('error', 'cronDailyBDSReport: Excel generation failed');
|
||||
return $this->respond(['status' => false, 'message' => 'Excel generation failed'], 500);
|
||||
}
|
||||
|
||||
$emailList = getenv('bds.dailyReportEmails') ?: '';
|
||||
$recipientEmails = array_filter(array_map('trim', explode(',', $emailList)));
|
||||
if (empty($recipientEmails)) {
|
||||
@unlink($filePath);
|
||||
$this->myLogger->logme('error', 'cronDailyBDSReport: No recipients in bds.dailyReportEmails. Report generated but not sent.');
|
||||
return $this->respond([
|
||||
'status' => 'success',
|
||||
'message' => 'Report generated. No recipients configured (set bds.dailyReportEmails in .env).',
|
||||
'file' => $fileName
|
||||
], 200);
|
||||
}
|
||||
|
||||
$subject = "BDS Report Up to - {$today}";
|
||||
$message = "<p>Please find attached the BDS report up to {$today}.</p>";
|
||||
$message .= "<p>Total records: " . count($reportList) . "</p>";
|
||||
|
||||
$attachments = [
|
||||
['filePath' => $filePath, 'fileName' => $fileName]
|
||||
];
|
||||
|
||||
$res = MailHelper::send_email([
|
||||
'mail' => $recipientEmails,
|
||||
'subject' => $subject,
|
||||
'message' => $message,
|
||||
'attachments' => $attachments
|
||||
]);
|
||||
|
||||
@unlink($filePath);
|
||||
|
||||
$resDecoded = is_string($res) ? json_decode($res, true) : $res;
|
||||
if (isset($resDecoded['status']) && $resDecoded['status'] === 'success') {
|
||||
$this->myLogger->logme('error', "cronDailyBDSReport: Report sent to " . count($recipientEmails) . " recipients");
|
||||
return $this->respond([
|
||||
'status' => true,
|
||||
'message' => 'Report generated and emailed successfully.',
|
||||
'recipients' => count($recipientEmails)
|
||||
], 200);
|
||||
}
|
||||
|
||||
$this->myLogger->logme('error', 'cronDailyBDSReport: Email send failed - ' . json_encode($resDecoded));
|
||||
return $this->respond(['status' => false, 'message' => 'Report generated but email send failed'], 500);
|
||||
} catch (Exception $e) {
|
||||
if ($filePath && is_file($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
$this->myLogger->logme('error', 'cronDailyBDSReport Exception: ' . $e->getMessage());
|
||||
return $this->respond(['status' => false, 'message' => $e->getMessage()], 500);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -1097,10 +1097,12 @@ class TestingController extends BaseController
|
||||
'dashboard' => $database_id
|
||||
],
|
||||
'exp' => time() + (10 * 60), // 10 minutes
|
||||
'params' => (object)[]
|
||||
'params' => (object) ['client_policy' => $policy_id ], // MUST be object for Metabase
|
||||
|
||||
];
|
||||
|
||||
$token = JWT::encode($payload, $METABASE_SECRET_KEY, 'HS256');
|
||||
// dd($token);
|
||||
|
||||
if ($this->request->getGet('api') == 1) {
|
||||
return $this->respond([
|
||||
|
||||
@ -959,6 +959,8 @@ class TicketController extends BaseController
|
||||
// Redirect or show a 404 to prevent "Undefined array key" errors
|
||||
return redirect()->to(base_url('ticket/list'))->with('error', 'Ticket not found');
|
||||
}
|
||||
|
||||
$ticket_data['is_tpa_api_service_enabled'] = $this->ticketMasterModel->isTpaApiServiceEnabled($ticket_id);
|
||||
|
||||
$ticket_data = $this->formatDateForClaim($ticket_data, 'd/m/Y');
|
||||
|
||||
@ -1173,9 +1175,13 @@ class TicketController extends BaseController
|
||||
'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).'
|
||||
]
|
||||
],
|
||||
'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [
|
||||
'regex_match' => 'TPA ID can only contain letters and numbers.'
|
||||
]],
|
||||
'tpa_no' => [
|
||||
'label' => 'TPA ID',
|
||||
'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]',
|
||||
'errors' => [
|
||||
'regex_match' => 'TPA ID can only contain letters, numbers, and /.'
|
||||
]
|
||||
],
|
||||
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
|
||||
'errors' => [
|
||||
'required' => 'Mobile number is required',
|
||||
@ -1522,9 +1528,13 @@ class TicketController extends BaseController
|
||||
'regex_match' => 'Policy Number can only contain letters, numbers, spaces, hyphens(-) underscores(_), and slashes(/).'
|
||||
]
|
||||
],
|
||||
'tpa_no' => ['label' => 'TPA ID','rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9]+$/]','errors' => [
|
||||
'regex_match' => 'TPA ID can only contain letters and numbers.'
|
||||
]],
|
||||
'tpa_no' => [
|
||||
'label' => 'TPA ID',
|
||||
'rules' => 'permit_empty|regex_match[/^[a-zA-Z0-9\/]+$/]',
|
||||
'errors' => [
|
||||
'regex_match' => 'TPA ID can only contain letters, numbers, and /.'
|
||||
]
|
||||
],
|
||||
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
|
||||
'errors' => [
|
||||
'required' => 'Mobile number is required',
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -164,7 +164,7 @@ class VidalApiController extends BaseController
|
||||
|
||||
if (count($data) && $data['filePath'] == null) {
|
||||
log_message('error', "VIDAL - Claim Push | Submit claim failed - Claim or File Missing");
|
||||
return $this->response->setJSON(['status' => false,'message' => 'Claim or File Missing', ]);
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | Claim or File Missing'];
|
||||
}
|
||||
|
||||
$filePath = $data['filePath'] ?? '';
|
||||
@ -271,7 +271,7 @@ class VidalApiController extends BaseController
|
||||
$this->db->table('ticket_master')
|
||||
->where('id',$claimId)
|
||||
->update([ 'tpa_push_response' => json_encode($response) ]);
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push FAILED | API call failed'];
|
||||
}
|
||||
|
||||
// return $this->response->setJSON($response);
|
||||
@ -289,16 +289,17 @@ class VidalApiController extends BaseController
|
||||
->where('id',$claimId)
|
||||
->update([ 'tpa_claim_push_reference_no' => $claimInwardNO , 'tpa_claim_id' => $claimNO , 'claim_number' => $claimNO ]);
|
||||
|
||||
return;
|
||||
return ['status' => true, 'message' => 'Claim Push SUCCESS'];
|
||||
|
||||
} else {
|
||||
log_message('error', 'VIDAL - Claim Push API SUCCESS BUT claimNO,claimInwardNO EMPTY | claimId: '.$claimId.' | response: '.json_encode($response));
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API SUCCESS BUT claimNO,claimInwardNO EMPTY'];
|
||||
}
|
||||
}else{
|
||||
log_message('error', 'VIDAL - Claim Push API FAILED | claimId: '.$claimId.' | response: '.json_encode($response));
|
||||
return;
|
||||
return ['status' => false, 'message' => 'Claim Push API FAILED'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function getWellnessSSORedirectUrl($email = 'test@getvisitapp.com')
|
||||
|
||||
@ -3023,6 +3023,9 @@ if (!function_exists('validate_excel_value')) {
|
||||
case 'vehicle':
|
||||
return validate_indian_vehicle_number($value);
|
||||
|
||||
case 'positive_number':
|
||||
return validate_positive_number_value($value);
|
||||
|
||||
default:
|
||||
return [
|
||||
'status' => true,
|
||||
@ -3066,6 +3069,24 @@ if (!function_exists('validate_mobile_value')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('validate_positive_number_value')) {
|
||||
function validate_positive_number_value($value)
|
||||
{
|
||||
if ($value === "" || $value === null) {
|
||||
return ['status' => true, 'error' => null];
|
||||
}
|
||||
|
||||
if (is_numeric($value) && $value >= 0) {
|
||||
return ['status' => true, 'error' => null];
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => false,
|
||||
'error' => "Value must be a positive number"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('validate_email_value')) {
|
||||
function validate_email_value($value)
|
||||
{
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
<?php namespace App\Libraries;
|
||||
<?php
|
||||
namespace App\Libraries;
|
||||
|
||||
use Google_Client;
|
||||
use Google_Service_Sheets;
|
||||
use Google_Service_Drive;
|
||||
use Google_Service_Sheets_ValueRange;
|
||||
use Google_Service_Sheets;
|
||||
use Google_Service_Sheets_BatchUpdateSpreadsheetRequest;
|
||||
use Google_Service_Sheets_ValueRange;
|
||||
|
||||
class GoogleSheetLib
|
||||
{
|
||||
@ -12,7 +13,6 @@ class GoogleSheetLib
|
||||
protected Google_Service_Sheets $sheets;
|
||||
protected Google_Service_Drive $drive;
|
||||
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->client = new Google_Client();
|
||||
@ -28,7 +28,7 @@ class GoogleSheetLib
|
||||
// Required scopes
|
||||
$this->client->addScope([
|
||||
Google_Service_Drive::DRIVE,
|
||||
Google_Service_Sheets::SPREADSHEETS
|
||||
Google_Service_Sheets::SPREADSHEETS,
|
||||
]);
|
||||
|
||||
// Init services
|
||||
@ -53,7 +53,7 @@ class GoogleSheetLib
|
||||
public function write(string $spreadsheetId, array $values, string $range = 'Sheet1')
|
||||
{
|
||||
$body = new Google_Service_Sheets_ValueRange([
|
||||
'values' => $values
|
||||
'values' => $values,
|
||||
]);
|
||||
|
||||
$this->sheets
|
||||
@ -81,7 +81,6 @@ class GoogleSheetLib
|
||||
return $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
|
||||
/* ================= COPY TEMPLATE ================= */
|
||||
|
||||
public function copyTemplate(string $templateId, string $name, string $folderId): string
|
||||
@ -92,10 +91,10 @@ class GoogleSheetLib
|
||||
'name' => $name,
|
||||
'parents' => [$folderId],
|
||||
|
||||
]),[
|
||||
'supportsAllDrives' => true,
|
||||
'fields' => 'id, name, parents'
|
||||
]
|
||||
]), [
|
||||
'supportsAllDrives' => true,
|
||||
'fields' => 'id, name, parents',
|
||||
]
|
||||
);
|
||||
|
||||
return $file->id;
|
||||
@ -116,7 +115,7 @@ class GoogleSheetLib
|
||||
|
||||
private function createPermission(string $fileId, string $email, string $role)
|
||||
{
|
||||
$type = str_starts_with($email, 'group:') ? 'group' : 'user';
|
||||
$type = str_starts_with($email, 'group:') ? 'group' : 'user';
|
||||
$email = str_replace('group:', '', $email);
|
||||
|
||||
$this->drive->permissions->create(
|
||||
@ -124,9 +123,9 @@ class GoogleSheetLib
|
||||
new \Google_Service_Drive_Permission([
|
||||
'type' => $type,
|
||||
'role' => $role,
|
||||
'emailAddress' => $email
|
||||
'emailAddress' => $email,
|
||||
]),
|
||||
['sendNotificationEmail' => false,'supportsAllDrives' => true]
|
||||
['sendNotificationEmail' => false, 'supportsAllDrives' => true]
|
||||
);
|
||||
}
|
||||
|
||||
@ -135,7 +134,7 @@ class GoogleSheetLib
|
||||
public function applyProtectionsold(string $spreadsheetId, array $ranges)
|
||||
{
|
||||
$spreadsheet = $this->sheets->spreadsheets->get($spreadsheetId);
|
||||
$sheetId = $spreadsheet->getSheets()[0]->getProperties()->getSheetId();
|
||||
$sheetId = $spreadsheet->getSheets()[0]->getProperties()->getSheetId();
|
||||
|
||||
$requests = [];
|
||||
|
||||
@ -145,96 +144,95 @@ class GoogleSheetLib
|
||||
$requests[] = [
|
||||
'addProtectedRange' => [
|
||||
'protectedRange' => [
|
||||
'range' => [
|
||||
'sheetId' => $sheetId
|
||||
'range' => [
|
||||
'sheetId' => $sheetId,
|
||||
],
|
||||
'warningOnly' => false
|
||||
]
|
||||
]
|
||||
'warningOnly' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$this->sheets->spreadsheets->batchUpdate(
|
||||
$spreadsheetId,
|
||||
new Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
||||
'requests' => $requests
|
||||
'requests' => $requests,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
public function applyProtections(string $spreadsheetId, array $protections)
|
||||
{
|
||||
// Fetch spreadsheet metadata
|
||||
$spreadsheet = $this->sheets->spreadsheets->get(
|
||||
$spreadsheetId,
|
||||
['fields' => 'sheets(properties(sheetId,title,gridProperties))']
|
||||
);
|
||||
|
||||
// Map sheet names
|
||||
$sheetMap = [];
|
||||
foreach ($spreadsheet->getSheets() as $sheet) {
|
||||
$props = $sheet->getProperties();
|
||||
$sheetMap[$props->getTitle()] = [
|
||||
'sheetId' => $props->getSheetId(),
|
||||
'rowCount' => $props->getGridProperties()->getRowCount(),
|
||||
'colCount' => $props->getGridProperties()->getColumnCount(),
|
||||
];
|
||||
}
|
||||
|
||||
$requests = [];
|
||||
|
||||
foreach ($protections as $protection) {
|
||||
|
||||
$rangeStr = $protection['range'];
|
||||
|
||||
if (!str_contains($rangeStr, '!')) {
|
||||
throw new \Exception("Invalid range format: {$rangeStr}");
|
||||
}
|
||||
|
||||
[$sheetName, $a1] = explode('!', $rangeStr, 2);
|
||||
|
||||
if (!isset($sheetMap[$sheetName])) {
|
||||
throw new \Exception("Sheet not found: {$sheetName}");
|
||||
}
|
||||
|
||||
$sheetMeta = $sheetMap[$sheetName];
|
||||
|
||||
$gridRange = $this->convertA1ToGridRange(
|
||||
$a1,
|
||||
$sheetMeta['sheetId'],
|
||||
$sheetMeta['rowCount'],
|
||||
$sheetMeta['colCount']
|
||||
{
|
||||
// Fetch spreadsheet metadata
|
||||
$spreadsheet = $this->sheets->spreadsheets->get(
|
||||
$spreadsheetId,
|
||||
['fields' => 'sheets(properties(sheetId,title,gridProperties))']
|
||||
);
|
||||
|
||||
$protectedRange = [
|
||||
'range' => $gridRange,
|
||||
'description' => 'RFQ Protected Area',
|
||||
'warningOnly' => false,
|
||||
'editors' => [
|
||||
'users' => $protection['users'] ?? [],
|
||||
'groups' => $protection['groups'] ?? []
|
||||
]
|
||||
];
|
||||
// Map sheet names
|
||||
$sheetMap = [];
|
||||
foreach ($spreadsheet->getSheets() as $sheet) {
|
||||
$props = $sheet->getProperties();
|
||||
$sheetMap[$props->getTitle()] = [
|
||||
'sheetId' => $props->getSheetId(),
|
||||
'rowCount' => $props->getGridProperties()->getRowCount(),
|
||||
'colCount' => $props->getGridProperties()->getColumnCount(),
|
||||
];
|
||||
}
|
||||
|
||||
$requests[] = [
|
||||
'addProtectedRange' => [
|
||||
'protectedRange' => $protectedRange
|
||||
]
|
||||
];
|
||||
$requests = [];
|
||||
|
||||
foreach ($protections as $protection) {
|
||||
|
||||
$rangeStr = $protection['range'];
|
||||
|
||||
if (! str_contains($rangeStr, '!')) {
|
||||
throw new \Exception("Invalid range format: {$rangeStr}");
|
||||
}
|
||||
|
||||
[$sheetName, $a1] = explode('!', $rangeStr, 2);
|
||||
|
||||
if (! isset($sheetMap[$sheetName])) {
|
||||
throw new \Exception("Sheet not found: {$sheetName}");
|
||||
}
|
||||
|
||||
$sheetMeta = $sheetMap[$sheetName];
|
||||
|
||||
$gridRange = $this->convertA1ToGridRange(
|
||||
$a1,
|
||||
$sheetMeta['sheetId'],
|
||||
$sheetMeta['rowCount'],
|
||||
$sheetMeta['colCount']
|
||||
);
|
||||
|
||||
$protectedRange = [
|
||||
'range' => $gridRange,
|
||||
'description' => 'RFQ Protected Area',
|
||||
'warningOnly' => false,
|
||||
'editors' => [
|
||||
'users' => $protection['users'] ?? [],
|
||||
'groups' => $protection['groups'] ?? [],
|
||||
],
|
||||
];
|
||||
|
||||
$requests[] = [
|
||||
'addProtectedRange' => [
|
||||
'protectedRange' => $protectedRange,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
if (! empty($requests)) {
|
||||
$batch = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
||||
'requests' => $requests,
|
||||
]);
|
||||
|
||||
$this->sheets->spreadsheets->batchUpdate($spreadsheetId, $batch);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!empty($requests)) {
|
||||
$batch = new \Google_Service_Sheets_BatchUpdateSpreadsheetRequest([
|
||||
'requests' => $requests
|
||||
]);
|
||||
|
||||
$this->sheets->spreadsheets->batchUpdate($spreadsheetId, $batch);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ================= URL ================= */
|
||||
|
||||
public function sheetUrl(string $sheetId): string
|
||||
@ -242,43 +240,41 @@ class GoogleSheetLib
|
||||
return "https://docs.google.com/spreadsheets/d/{$sheetId}/edit";
|
||||
}
|
||||
|
||||
private function convertA1ToGridRange($a1, $sheetId, $maxRows, $maxCols)
|
||||
{
|
||||
if (preg_match('/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i', $a1, $m)) {
|
||||
|
||||
private function convertA1ToGridRange($a1, $sheetId, $maxRows, $maxCols)
|
||||
{
|
||||
if (preg_match('/^([A-Z]+)(\d+)(?::([A-Z]+)(\d+))?$/i', $a1, $m)) {
|
||||
$startCol = $this->colToIndex($m[1]);
|
||||
$startRow = intval($m[2]) - 1;
|
||||
|
||||
$startCol = $this->colToIndex($m[1]);
|
||||
$startRow = intval($m[2]) - 1;
|
||||
if (! empty($m[3])) {
|
||||
$endCol = $this->colToIndex($m[3]) + 1;
|
||||
$endRow = intval($m[4]);
|
||||
} else {
|
||||
$endCol = $startCol + 1;
|
||||
$endRow = $startRow + 1;
|
||||
}
|
||||
|
||||
if (!empty($m[3])) {
|
||||
$endCol = $this->colToIndex($m[3]) + 1;
|
||||
$endRow = intval($m[4]);
|
||||
} else {
|
||||
$endCol = $startCol + 1;
|
||||
$endRow = $startRow + 1;
|
||||
return [
|
||||
'sheetId' => $sheetId,
|
||||
'startRowIndex' => $startRow,
|
||||
'endRowIndex' => $endRow,
|
||||
'startColumnIndex' => $startCol,
|
||||
'endColumnIndex' => $endCol,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'sheetId' => $sheetId,
|
||||
'startRowIndex' => $startRow,
|
||||
'endRowIndex' => $endRow,
|
||||
'startColumnIndex' => $startCol,
|
||||
'endColumnIndex' => $endCol
|
||||
];
|
||||
throw new \Exception("Unsupported A1 format: {$a1}");
|
||||
}
|
||||
|
||||
throw new \Exception("Unsupported A1 format: {$a1}");
|
||||
}
|
||||
|
||||
private function colToIndex($letters)
|
||||
{
|
||||
$letters = strtoupper($letters);
|
||||
$index = 0;
|
||||
for ($i = 0; $i < strlen($letters); $i++) {
|
||||
$index = $index * 26 + (ord($letters[$i]) - 64);
|
||||
private function colToIndex($letters)
|
||||
{
|
||||
$letters = strtoupper($letters);
|
||||
$index = 0;
|
||||
for ($i = 0; $i < strlen($letters); $i++) {
|
||||
$index = $index * 26 + (ord($letters[$i]) - 64);
|
||||
}
|
||||
return $index - 1;
|
||||
}
|
||||
return $index - 1;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -16,7 +16,7 @@ class MyGoogleDrive
|
||||
{
|
||||
$this->myLogger = \Config\Services::mylogger();
|
||||
$this->client = new Google_Client();
|
||||
$this->client->setAuthConfig(ROOTPATH . 'nhance-app-google-drive.json'); // App credentials
|
||||
$this->client->setAuthConfig(ROOTPATH . 'nhance-ee8d1-e3c5269b1ec7.json'); // App credentials
|
||||
// putenv('GOOGLE_APPLICATION_CREDENTIALS=' . ROOTPATH . 'nhance-app-google-drive.json');
|
||||
$this->client->useApplicationDefaultCredentials();
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -9,17 +9,30 @@ use App\Models\TicketMasterModel;
|
||||
use App\Models\EmployeeModel;
|
||||
use App\Models\ClaimDumpFileModel;
|
||||
use App\Models\ClaimsDumpFhplModel;
|
||||
use App\Models\ClientPolicyModel;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
abstract class BaseTpaClaimImportService
|
||||
{
|
||||
protected BaseConnection $db;
|
||||
protected $claimDumpFileModel;
|
||||
protected $clientPolicyModel;
|
||||
protected $policyNumberMapping;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->db = db_connect();
|
||||
$this->claimDumpFileModel = new ClaimDumpFileModel();
|
||||
$this->clientPolicyModel = new ClientPolicyModel();
|
||||
$this->policyNumberMapping = [
|
||||
(int) env('VIDAL_PRIMARY_KEY_CONSTANT') => 'Insurer Policy Number',
|
||||
(int) env('ABHI_PRIMARY_KEY_CONSTANT') => 'Policy Number',
|
||||
(int) env('MEDI_ASSIST_PRIMARY_KEY_CONSTANT') => 'policy_no',
|
||||
(int) env('FHPL_PRIMARY_KEY_CONSTANT') => 'Policy No',
|
||||
(int) env('R_CARE_PRIMARY_KEY_CONSTANT') => 'Policy Number',
|
||||
(int) env('ICICI_PRIMARY_KEY_CONSTANT') => 'POLICY_NO',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -33,6 +46,8 @@ abstract class BaseTpaClaimImportService
|
||||
try {
|
||||
$fileData = $this->claimDumpFileModel->where('id', $fileId)->first();
|
||||
|
||||
$client_policy_data = $this->clientPolicyModel->where('id', $fileData['client_policy_id'])->first();
|
||||
|
||||
// Determine sheet name logic...
|
||||
if (env('FHPL_PRIMARY_KEY_CONSTANT') == $fileData['tpa_id']) {
|
||||
$rows = $this->readExcelBySheetName($filePath, 'Claims&Preauth');
|
||||
@ -47,6 +62,18 @@ abstract class BaseTpaClaimImportService
|
||||
return ['status' => false, 'message' => 'Excel file contains no data or wrong file upload'];
|
||||
}
|
||||
|
||||
if(isset($this->policyNumberMapping[$fileData['tpa_id']]) && !empty($this->policyNumberMapping[$fileData['tpa_id']])){
|
||||
$policy_number_column = $this->policyNumberMapping[$fileData['tpa_id']];
|
||||
}else{
|
||||
$policy_number_column = 'policy_no';
|
||||
}
|
||||
|
||||
|
||||
if($client_policy_data['policy_no'] != ($rows[0][$policy_number_column] ?? '')){
|
||||
$this->db->transRollback();
|
||||
return ['status' => false, 'message' => 'Policy number mismatch in the file and in the system'];
|
||||
}
|
||||
|
||||
$tpaInsertData = $this->mapTPAData($rows, $fileId);
|
||||
|
||||
if (empty($tpaInsertData)) {
|
||||
@ -81,7 +108,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 +128,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 +361,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 +392,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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
|
||||
73
app/Models/ExpenseModel.php
Normal file
73
app/Models/ExpenseModel.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class LeadsModel extends Model
|
||||
{
|
||||
protected $table = 'leads';
|
||||
protected $primaryKey = 'id';
|
||||
protected $table = 'leads';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
'id',
|
||||
'actual_lead_id',
|
||||
@ -47,6 +46,7 @@ class LeadsModel extends Model
|
||||
'proposel_data',
|
||||
'status',
|
||||
'notes',
|
||||
'lost_reason',
|
||||
'created_at',
|
||||
'created_by',
|
||||
'updated_at',
|
||||
@ -99,7 +99,7 @@ class LeadsModel extends Model
|
||||
'no_of_installment',
|
||||
'is_policy_created',
|
||||
'claim_history',
|
||||
|
||||
|
||||
'total_lives_at_incept',
|
||||
'premium_at_incept',
|
||||
|
||||
@ -108,20 +108,19 @@ class LeadsModel extends Model
|
||||
'quote_received_insurer',
|
||||
'acm_id',
|
||||
'policy_with_correction',
|
||||
'agreed_percentage',
|
||||
'agreed_percentage', 'misc',
|
||||
];
|
||||
|
||||
|
||||
// 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)
|
||||
{
|
||||
@ -160,7 +159,7 @@ class LeadsModel extends Model
|
||||
FROM rfq
|
||||
WHERE type = 2
|
||||
AND is_active = 1 and lead_id = leads.id
|
||||
) AS qcr_count,
|
||||
) AS qcr_count,
|
||||
|
||||
(
|
||||
SELECT COUNT(*) AS rfq_count
|
||||
@ -176,7 +175,7 @@ class LeadsModel extends Model
|
||||
->join('lead_files', 'leads.id = lead_files.lead_id AND lead_files.type = 2 AND lead_files.is_active = 1', 'left')
|
||||
->where('leads.is_active', 1);
|
||||
|
||||
if (!empty($where)) {
|
||||
if (! empty($where)) {
|
||||
$data->where($where);
|
||||
}
|
||||
|
||||
@ -185,7 +184,7 @@ class LeadsModel extends Model
|
||||
|
||||
public function getLeadForInsertClientList($type = null, $client_id = null)
|
||||
{
|
||||
$query = $this->db->table('leads')
|
||||
$query = $this->db->table('leads')
|
||||
->select('leads.*, user_profiles.first_name as user_name')
|
||||
->join('user_profiles', 'leads.created_by = user_profiles.id')
|
||||
->where('leads.is_active', 1)
|
||||
@ -194,8 +193,6 @@ class LeadsModel extends Model
|
||||
->where("(leads.is_client_created = '' OR leads.is_client_created IS NULL)")
|
||||
->where("(leads.is_policy_created = '' OR leads.is_policy_created IS NULL)");
|
||||
|
||||
|
||||
|
||||
if ($type) {
|
||||
$query->where('leads.lead_type', $type);
|
||||
}
|
||||
@ -242,8 +239,8 @@ class LeadsModel extends Model
|
||||
// $builder->select($select);
|
||||
|
||||
// // Auditing subquery
|
||||
// $subquery = "(SELECT pk, MAX(created_at) AS last_claim_status_change
|
||||
// FROM auditing_history
|
||||
// $subquery = "(SELECT pk, MAX(created_at) AS last_claim_status_change
|
||||
// FROM auditing_history
|
||||
// WHERE table_name = 'leads' AND field_name = 'status'
|
||||
// GROUP BY pk)";
|
||||
|
||||
@ -281,7 +278,7 @@ class LeadsModel extends Model
|
||||
// // if (!str_ends_with($key, '_ids') && $key != "won") {
|
||||
// // $total += (int) $value;
|
||||
// // }
|
||||
|
||||
|
||||
// if (!str_ends_with($key, '_ids')) {
|
||||
// $total += (int) $value;
|
||||
// }
|
||||
@ -291,12 +288,10 @@ class LeadsModel extends Model
|
||||
// $lead_data[0]['total'] = $total;
|
||||
// $lead_data[0]['policy_with_correction'] = $policy_with_correction_count;
|
||||
|
||||
|
||||
// // dd($lead_data[0]);
|
||||
// return $lead_data[0];
|
||||
// }
|
||||
|
||||
|
||||
public function getDashData()
|
||||
{
|
||||
// Get all unique statuses
|
||||
@ -321,7 +316,7 @@ class LeadsModel extends Model
|
||||
$selectParts = [];
|
||||
foreach ($statuses as $row) {
|
||||
$status = $row['status'];
|
||||
$alias = strtolower(str_replace(' ', '_', $status));
|
||||
$alias = strtolower(str_replace(' ', '_', $status));
|
||||
|
||||
$selectParts[] = "SUM(CASE WHEN status = '{$status}' THEN 1 ELSE 0 END) AS `{$alias}`";
|
||||
$selectParts[] = "GROUP_CONCAT(CASE WHEN status = '{$status}' THEN id END) AS `{$alias}_ids`";
|
||||
@ -339,19 +334,17 @@ class LeadsModel extends Model
|
||||
// Calculate total
|
||||
$total = 0;
|
||||
foreach ($lead_data as $key => $val) {
|
||||
if (!str_ends_with($key, '_ids')) {
|
||||
if (! str_ends_with($key, '_ids')) {
|
||||
$total += (int) $val;
|
||||
}
|
||||
}
|
||||
|
||||
$lead_data['total'] = $total;
|
||||
$lead_data['policy_with_correction'] = $policy_with_correction_data['count'] ?? 0;
|
||||
$lead_data['total'] = $total;
|
||||
$lead_data['policy_with_correction'] = $policy_with_correction_data['count'] ?? 0;
|
||||
$lead_data['policy_with_correction_ids'] = $policy_with_correction_data['ids'] ?? null;
|
||||
|
||||
// dd($lead_data);
|
||||
return $lead_data;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -3098,6 +3098,8 @@
|
||||
if ($date_type === "policy_issue_date") {
|
||||
$conditions .= " AND pcsd.pt_policy_issue_date >= '$startDate' ";
|
||||
$conditions .= " AND pcsd.pt_policy_issue_date <= '$endDate' ";
|
||||
} else if ($date_type === "created_at") {
|
||||
$conditions .= " AND pt.created_at <= '$endDate' ";
|
||||
} else {
|
||||
$conditions .= " AND pt.$date_type >= '$startDate' ";
|
||||
$conditions .= " AND pt.$date_type <= '$endDate' ";
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Model;
|
||||
|
||||
class PolicyTypeModel extends Model
|
||||
{
|
||||
protected $table = 'policy_type';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
protected $table = 'policy_type';
|
||||
protected $primaryKey = 'id';
|
||||
protected $allowedFields = [
|
||||
"id",
|
||||
"policy_type",
|
||||
"bap",
|
||||
@ -22,6 +21,6 @@ class PolicyTypeModel extends Model
|
||||
"etp",
|
||||
"iep",
|
||||
"itp",
|
||||
"question_json",'itep','etep', 'policy_category',
|
||||
"question_json", 'itep', 'etep', 'policy_category', 'misc',
|
||||
];
|
||||
}
|
||||
|
||||
@ -25,6 +25,7 @@ class SalesActivityModel extends Model
|
||||
'completion_notes',
|
||||
'completed_date',
|
||||
'parent_activity_id',
|
||||
'additional_assigned_ids',
|
||||
'created_by',
|
||||
'updated_by'
|
||||
];
|
||||
@ -64,8 +65,14 @@ class SalesActivityModel extends Model
|
||||
*/
|
||||
public function getActivitiesByLead($leadId, $status = null)
|
||||
{
|
||||
$builder = $this->select('sales_activities.*, user_profiles.first_name as assigned_to_name')
|
||||
->join('user_profiles', 'user_profiles.id = sales_activities.assigned_to', 'left')
|
||||
// Convert the INT id to a string, then wrap it in JSON quotes to match ["5", "11"]
|
||||
$subQuery = "(SELECT GROUP_CONCAT(up2.first_name SEPARATOR ', ')
|
||||
FROM user_profiles up2
|
||||
WHERE JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
) as additional_assigned_names";
|
||||
|
||||
$builder = $this->select("sales_activities.*, up1.first_name as assigned_to_name, $subQuery")
|
||||
->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left')
|
||||
->where('sales_activities.lead_id', $leadId);
|
||||
|
||||
if ($status) {
|
||||
@ -78,7 +85,7 @@ class SalesActivityModel extends Model
|
||||
/**
|
||||
* Get all sales_activities with filters
|
||||
*/
|
||||
public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0)
|
||||
public function getActivitiesWithFiltersOLD($filters = [], $limit = 10, $offset = 0)
|
||||
{
|
||||
$this->select('sales_activities.*, sales_actual_leads.company_name, user_profiles.first_name as assigned_to_name')
|
||||
->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left')
|
||||
@ -102,7 +109,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);
|
||||
}
|
||||
|
||||
|
||||
@ -139,6 +146,90 @@ class SalesActivityModel extends Model
|
||||
// ];
|
||||
}
|
||||
|
||||
public function getActivitiesWithFilters($filters = [], $limit = 10, $offset = 0)
|
||||
{
|
||||
// 1. Initial Selection
|
||||
$builder = $this->select("
|
||||
sales_activities.*,
|
||||
sales_actual_leads.company_name,
|
||||
up1.first_name as assigned_to_name,
|
||||
GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names
|
||||
")
|
||||
->join('sales_actual_leads', 'sales_actual_leads.lead_id = sales_activities.lead_id', 'left')
|
||||
->join('user_profiles as up1', 'up1.id = sales_activities.assigned_to', 'left');
|
||||
|
||||
// 2. The JSON Join for additional names
|
||||
// Only attempts join if the string looks like a JSON array
|
||||
$builder->join('user_profiles as up2', "
|
||||
sales_activities.additional_assigned_ids IS NOT NULL
|
||||
AND sales_activities.additional_assigned_ids != ''
|
||||
AND sales_activities.additional_assigned_ids != '[]'
|
||||
AND JSON_VALID(sales_activities.additional_assigned_ids)
|
||||
AND JSON_CONTAINS(sales_activities.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
", 'left');
|
||||
|
||||
// 3. Apply Filters
|
||||
if (!empty($filters['status'])) {
|
||||
$builder->where('sales_activities.status', $filters['status']);
|
||||
}
|
||||
|
||||
if (!empty($filters['activity_type'])) {
|
||||
$builder->where('sales_activities.activity_type', $filters['activity_type']);
|
||||
}
|
||||
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$assignedToIds = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']);
|
||||
$builder->whereIn('sales_activities.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
$builder->groupStart()
|
||||
->like('sales_actual_leads.company_name', $filters['search'])
|
||||
->orLike('up1.first_name', $filters['search'])
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
// 4. Grouping & Ordering
|
||||
$builder->groupBy('sales_activities.activity_id');
|
||||
$builder->orderBy('sales_activities.created_at', 'DESC');
|
||||
|
||||
// 5. Calculate Counts (using a clean builder to avoid the syntax error)
|
||||
$counts = $this->getActivityStatusCounts($filters);
|
||||
|
||||
// 6. Get Data and Total
|
||||
// Use true for countAllResults to get an accurate count of grouped rows
|
||||
$totalCountQuery = clone $builder;
|
||||
$total = $totalCountQuery->countAllResults(false);
|
||||
|
||||
$data = $builder->findAll($limit, $offset);
|
||||
|
||||
return [
|
||||
'data' => $data,
|
||||
'total' => $total,
|
||||
'counts' => $counts
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get counts without breaking the main query syntax
|
||||
*/
|
||||
private function getActivityStatusCounts($filters)
|
||||
{
|
||||
$validStatuses = ['pending', 'completed'];
|
||||
$counts = ['all' => 0, 'pending' => 0, 'completed' => 0];
|
||||
|
||||
foreach ($validStatuses as $status) {
|
||||
$query = $this->db->table('sales_activities')->where('status', $status);
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$ids = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']);
|
||||
$query->whereIn('assigned_to', $ids);
|
||||
}
|
||||
$counts[$status] = $query->countAllResults();
|
||||
}
|
||||
$counts['all'] = $counts['pending'] + $counts['completed'];
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete an activity
|
||||
*/
|
||||
|
||||
@ -123,7 +123,38 @@ class SalesActualLeadModel extends Model
|
||||
|
||||
$data = $this->findAll($limit, $offset);
|
||||
|
||||
return ['data' => $data,'total' => $total];
|
||||
$counts = $this->getLeadStatusCounts($filters);
|
||||
|
||||
return ['data' => $data,'total' => $total,'counts' => $counts];
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get counts without breaking the main query syntax
|
||||
*/
|
||||
private function getLeadStatusCounts($filters)
|
||||
{
|
||||
|
||||
$validStatuses = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
|
||||
$counts = ['all' => 0, 'New' => 0, 'Potential' => 0, 'Prospects' => 0, 'Not a Prospects' => 0];
|
||||
|
||||
foreach ($validStatuses as $status) {
|
||||
$countQuery = $this->db->table('sales_actual_leads')
|
||||
->whereIn('status', $validStatuses);
|
||||
|
||||
// Apply assigned_to filter to counts too
|
||||
if (!empty($filters['assigned_to'])) {
|
||||
$assignedToIds = is_array($filters['assigned_to'])
|
||||
? $filters['assigned_to']
|
||||
: explode(',', $filters['assigned_to']);
|
||||
$countQuery->whereIn('assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
$counts[$status] = $countQuery->where('status', $status)->countAllResults();
|
||||
}
|
||||
|
||||
$counts['all'] = array_sum($counts);
|
||||
|
||||
return $counts;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1218,6 +1218,21 @@ class TicketMasterModel extends Model
|
||||
|
||||
}
|
||||
|
||||
public function isTpaApiServiceEnabled($ticket_id)
|
||||
{
|
||||
$result = $this->db->table('ticket_master tm')
|
||||
->select('tas.*')
|
||||
->join('client_policy cp', 'tm.client_policy_id = cp.id')
|
||||
->join('tpa_api_services tas', 'cp.tpa_id = tas.tpa_id')
|
||||
->where('tm.id', $ticket_id)
|
||||
->where('tm.is_active', 1)
|
||||
->where('cp.is_active', 1)
|
||||
->where('tas.is_active', 1)
|
||||
->get()->getRowArray();
|
||||
|
||||
return count($result ?? []) > 0 ? true : false; // true if any API service is enabled, false if no API service is enabled
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------
|
||||
|
||||
}
|
||||
|
||||
@ -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>`
|
||||
|
||||
347
app/Views/daily_report_email_template.php
Normal file
347
app/Views/daily_report_email_template.php
Normal file
@ -0,0 +1,347 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
/* Base */
|
||||
body {
|
||||
font-family: 'Segoe UI', Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background-color: #f4f7f6;
|
||||
color: #333;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.06);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #1f618d, #21618c, #117a65);
|
||||
color: #ffffff;
|
||||
padding: 24px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.header-subtitle {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
padding: 20px;
|
||||
}
|
||||
.report-date {
|
||||
color: #7f8c8d;
|
||||
font-size: 13px;
|
||||
margin-bottom: 16px;
|
||||
text-align: right;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 17px;
|
||||
color: #2c3e50;
|
||||
border-bottom: 2px solid #3498db;
|
||||
display: inline-block;
|
||||
margin: 18px 0 12px;
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
/* Stats Grid - stacked for better email support */
|
||||
.stats-grid {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.stat-card {
|
||||
width: 100%;
|
||||
display: block;
|
||||
background: #f8f9fa;
|
||||
padding: 12px 14px;
|
||||
border-radius: 6px;
|
||||
border-left: 4px solid #3498db;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.stat-label {
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
color: #7f8c8d;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.stat-value {
|
||||
font-size: 18px;
|
||||
color: #2c3e50;
|
||||
margin-top: 4px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.stat-note {
|
||||
font-size: 11px;
|
||||
color: #95a5a6;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table-container {
|
||||
width: 100%;
|
||||
}
|
||||
.status-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.status-table th {
|
||||
background-color: #ecf0f1;
|
||||
text-align: left;
|
||||
padding: 10px 8px;
|
||||
font-size: 13px;
|
||||
border-bottom: 2px solid #bdc3c7;
|
||||
}
|
||||
.status-table td {
|
||||
padding: 10px 8px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
background: #ecf0f1;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background: #f8f9fa;
|
||||
padding: 14px 18px;
|
||||
text-align: center;
|
||||
font-size: 11px;
|
||||
color: #95a5a6;
|
||||
}
|
||||
|
||||
/* Mobile */
|
||||
@media only screen and (max-width: 480px) {
|
||||
.content {
|
||||
padding: 14px;
|
||||
}
|
||||
.header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: 12px;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 15px;
|
||||
}
|
||||
.stat-card {
|
||||
min-width: 100%;
|
||||
}
|
||||
.report-date {
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container" style="width:100%;max-width:680px;margin:0 auto;background:#ffffff;border-radius:8px;overflow:hidden;box-shadow:0 4px 10px rgba(0,0,0,0.06);">
|
||||
<div class="header" style="background:#1f618d;color:#ffffff;padding:24px 16px;text-align:center;">
|
||||
<h1>Daily Activity Report</h1>
|
||||
<div class="header-subtitle">
|
||||
Key operations, sales and claim metrics for the day
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content" style="padding:20px;">
|
||||
<div class="report-date" style="color:#7f8c8d;font-size:13px;margin-bottom:16px;text-align:right;">Date: <?= $today ?></div>
|
||||
|
||||
<!-- File Operations -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">Inception & Endorsement (File Upload)</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #3498db;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Total Inception</div>
|
||||
<div class="stat-value"><?= $total_inception_count ?></div>
|
||||
<div class="stat-note">Employee file uploads marked as inception</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #3498db;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Total Endorsement</div>
|
||||
<div class="stat-value"><?= $total_endorsement_count ?></div>
|
||||
<div class="stat-note">All non‑inception successful uploads</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Employee Enrollment (new data points as cards only) -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">Employee Enrollment</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #3498db;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Draft / Enrolled </div>
|
||||
<div class="stat-value"><?= $total_employee_draft_count ?? 0 ?> / <?= $total_employee_enrolled_count ?? 0 ?></div>
|
||||
<div class="stat-note">Employees in draft / enrolled status for the day</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #9b59b6;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Open for Enrollment Policies</div>
|
||||
<div class="stat-value"><?= $total_open_for_enrollemnt_policy_count ?? 0 ?></div>
|
||||
<div class="stat-note">Policies currently open for enrollment</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TPA / Insurer -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">TPA & Insurer Batch Summary</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #e67e22;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">TPA (Inception / Endorsement)</div>
|
||||
<div class="stat-value"><?= $total_tpa_incetion_count ?> / <?= $total_tpa_endorsement_count ?></div>
|
||||
<div class="stat-note">Successful TPA events</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #9b59b6;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Insurer (Inception / Endorsement)</div>
|
||||
<div class="stat-value"><?= $total_insurer_incetion_count ?> / <?= $total_insurer_endorsement_count ?></div>
|
||||
<div class="stat-note">Successful insurer events</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sales Funnel -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">Sales Funnel & Leads</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #27ae60;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Opportunities & Won</div>
|
||||
<div class="stat-value"><?= $total_opportunity_count ?> / <?= $total_placement_count ?></div>
|
||||
<div class="stat-note">Total opportunities created vs converted</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #16a085;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">RFQ (Created / Sent to Insurer)</div>
|
||||
<div class="stat-value"><?= $total_rfq_created_count ?> / <?= $total_rfq_insurer_send_count ?></div>
|
||||
<div class="stat-note">Movement from opportunity to RFQ</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #2980b9;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">QCR (Created / Sent to Client)</div>
|
||||
<div class="stat-value"><?= $total_qcr_created_count ?> / <?= $total_qcr_client_send_count ?></div>
|
||||
<div class="stat-note">Quotes prepared and shared</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #8e44ad;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Total Leads & Activities</div>
|
||||
<div class="stat-value"><?= $total_lead_count ?> / <?= $total_activity_count ?></div>
|
||||
<div class="stat-note">Lead entries and logged touchpoints</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BDS / Policy Transactions -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">BDS Policy Transactions</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #d35400;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Total BDS Transactions</div>
|
||||
<div class="stat-value"><?= $total_bds_count ?></div>
|
||||
<div class="stat-note">All policy transactions processed</div>
|
||||
</div>
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #c0392b;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">BDS (Policy / Endorsement)</div>
|
||||
<div class="stat-value"><?= $total_bds_policy_wise_count ?> / <?= $total_bds_endorsement_wise_count ?></div>
|
||||
<div class="stat-note">Split of inceptions vs endorsements</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Claims -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">Claims</h3>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card" style="width:100%;display:block;background:#f8f9fa;padding:12px 14px;border-radius:6px;border-left:4px solid #d35400;box-sizing:border-box;margin-bottom:10px;">
|
||||
<div class="stat-label">Total Claims Registered for the day</div>
|
||||
<div class="stat-value"><?= $total_claim_count ?></div>
|
||||
<div class="stat-note"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- BDS by Policy Type -->
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">BDS by Policy Type</h3>
|
||||
<div class="table-container" style="width:100%;">
|
||||
<table class="status-table" style="width:100%;border-collapse:collapse;margin-top:8px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color:#ecf0f1;text-align:left;padding:10px 8px;font-size:13px;border-bottom:2px solid #bdc3c7;">Policy Type</th>
|
||||
<th style="background-color:#ecf0f1;text-align:right;padding:10px 8px;font-size:13px;border-bottom:2px solid #bdc3c7;">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($total_bds_policy_type_wise_count)): ?>
|
||||
<?php foreach ($total_bds_policy_type_wise_count as $row): ?>
|
||||
<tr>
|
||||
<td style="padding:10px 8px;border-bottom:1px solid #eee;font-size:13px;">
|
||||
<span class="badge" style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#ecf0f1;color:#2c3e50;"><?= esc($row['policy_type']) ?></span>
|
||||
</td>
|
||||
<td style="padding:10px 8px;border-bottom:1px solid #eee;font-size:13px;text-align:right;font-weight:bold;"><?= $row['count'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="2" style="padding:10px 8px;border-bottom:1px solid #eee;font-size:12px;text-align:center;color:#7f8c8d;">
|
||||
No BDS activity recorded for today.
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<h3 class="section-title" style="font-size:17px;color:#2c3e50;border-bottom:2px solid #3498db;display:inline-block;margin:18px 0 12px;padding-bottom:4px;">Claim Status Breakdown</h3>
|
||||
<div style="padding:10px 8px;border:1px solid #eee;border-radius:4px;font-size:12px;text-align:center;color:#7f8c8d;margin-top:8px;">
|
||||
The total claims received so far are listed ticket type-wise.
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$claimSections = [
|
||||
'GMC Claims' => $total_gmc_status_wise_claim_count ?? [],
|
||||
'GPA Claims' => $total_gpa_status_wise_claim_count ?? [],
|
||||
'EDLI Claims' => $total_edli_status_wise_claim_count ?? [],
|
||||
'GTLI Claims' => $total_gtli_status_wise_claim_count ?? [],
|
||||
];
|
||||
$hasAnyClaimData = false;
|
||||
?>
|
||||
|
||||
<?php foreach ($claimSections as $sectionTitle => $rows): ?>
|
||||
<?php if (!empty($rows)): ?>
|
||||
<?php $hasAnyClaimData = true; ?>
|
||||
<h4 style="font-size:14px;color:#34495e;margin:14px 0 6px;"><?= esc($sectionTitle) ?></h4>
|
||||
<div class="table-container" style="width:100%;margin-bottom:10px;">
|
||||
<table class="status-table" style="width:100%;border-collapse:collapse;margin-top:4px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="background-color:#ecf0f1;text-align:left;padding:10px 8px;font-size:13px;border-bottom:2px solid #bdc3c7;">Status</th>
|
||||
<th style="background-color:#ecf0f1;text-align:right;padding:10px 8px;font-size:13px;border-bottom:2px solid #bdc3c7;">Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($rows as $row): ?>
|
||||
<tr>
|
||||
<td style="padding:10px 8px;border-bottom:1px solid #eee;font-size:13px;">
|
||||
<span class="badge" style="display:inline-block;padding:2px 8px;border-radius:10px;font-size:11px;background:#ecf0f1;color:#2c3e50;"><?= esc($row['claim_status']) ?></span>
|
||||
</td>
|
||||
<td style="padding:10px 8px;border-bottom:1px solid #eee;font-size:13px;text-align:right;font-weight:bold;"><?= $row['count'] ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="footer" style="background:#f8f9fa;padding:14px 18px;text-align:center;font-size:11px;color:#95a5a6;">
|
||||
This is an automated system generated report. Please do not reply to this email.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
586
app/Views/expense_list.php
Normal file
586
app/Views/expense_list.php
Normal 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>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h3>Google Sheet Editor</h3>
|
||||
<h3>Edit RFQ </h3>
|
||||
|
||||
<button onclick="save()">💾 Save</button>
|
||||
<button onclick="download()">⬇ Download</button>
|
||||
@ -44,9 +44,9 @@ function createRfq() {
|
||||
<table id="sheet"></table>
|
||||
|
||||
<script>
|
||||
alert('first');
|
||||
// alert('first');
|
||||
const sheetId = "<?= esc($sheetId) ?>";
|
||||
alert('second');
|
||||
// alert('second');
|
||||
/* ---------- LOAD ---------- */
|
||||
fetch('<?php echo base_url() ?>' + `sheet/${sheetId}/fetch`)
|
||||
.then(r => r.json())
|
||||
@ -54,7 +54,7 @@ fetch('<?php echo base_url() ?>' + `sheet/${sheetId}/fetch`)
|
||||
.then(render);
|
||||
|
||||
function render(data) {
|
||||
alert('data');
|
||||
// alert('data');
|
||||
console.log('data');
|
||||
console.log(data);
|
||||
const table = document.getElementById('sheet');
|
||||
|
||||
@ -121,7 +121,7 @@
|
||||
|
||||
<!-- Sweet Alert CDN -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap-multiselect@1.1.0/dist/js/bootstrap-multiselect.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
|
||||
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-multiselect/0.9.13/css/bootstrap-multiselect.css" />
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/toastr@2.1.4/toastr.min.js"></script>
|
||||
@ -146,7 +146,7 @@
|
||||
|
||||
|
||||
<script src="https://cdn.datatables.net/plug-ins/2.0.8/sorting/scientific.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.8.0/jszip.min.js"></script>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -401,6 +401,12 @@
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-row" id="lost_reason_div" style="display: none;">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="lost_reason">Lost Reason <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="lost_reason" name="lost_reason" rows="4" placeholder="Please specify why this opportunity was lost"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="notes">Remarks</label>
|
||||
@ -778,7 +784,8 @@
|
||||
$('#client_name').val(res.data.client_name);
|
||||
$('#client_short_name').val(res.data.client_short_name);
|
||||
$('#entity_type_id').val(res.data.entity_type_id);
|
||||
$('#lead_status').val(res.data.status);
|
||||
$('#lead_status').val(res.data.status).trigger('change');
|
||||
$('#lost_reason').val(res.data.lost_reason || '');
|
||||
$('#notes').val(res.data.notes);
|
||||
|
||||
setTimeout(function() {
|
||||
@ -2431,5 +2438,66 @@
|
||||
});
|
||||
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var actual_lead_client_details = <?= json_encode($actual_lead_client_details ?? null) ?>;
|
||||
var actual_lead_contact_person_details = <?= json_encode($actual_lead_contact_person_details ?? null) ?>;
|
||||
|
||||
// -------------------------------
|
||||
// CLIENT DETAILS AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_client_details) {
|
||||
|
||||
$('#client_name').val(actual_lead_client_details.company_name || '');
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
|
||||
// 🔥 Important:
|
||||
// Only auto-generate short name IF empty (avoid overwrite in edit)
|
||||
if (!$('#client_short_name').val()) {
|
||||
$('#client_name').trigger('input');
|
||||
} else {
|
||||
// Run duplicate validation once
|
||||
validateInput($('#client_short_name')[0], "clients", "short_name");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// CONTACT PERSON AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_contact_person_details) {
|
||||
|
||||
$('#contact_person_name').val(actual_lead_contact_person_details.name || '');
|
||||
|
||||
$('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || '');
|
||||
|
||||
$('#contact_person_email').val(actual_lead_contact_person_details.email || '');
|
||||
}
|
||||
|
||||
|
||||
$('#lead_status').change(function() {
|
||||
if ($(this).val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
// Check on page load
|
||||
if ($('#lead_status').val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
// Ensure it's hidden and not required if the initial value is not 'lost'
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
//-----------------------------------------------------------------------------------------------------------
|
||||
</script>
|
||||
File diff suppressed because it is too large
Load Diff
@ -410,6 +410,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-row" id="lost_reason_div" style="display: none;">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="lost_reason">Lost Reason <span class="text-danger">*</span></label>
|
||||
<textarea class="form-control" id="lost_reason" name="lost_reason" rows="4" placeholder="Please specify why this opportunity was lost"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group col-md-12">
|
||||
<label for="notes">Remarks</label>
|
||||
@ -1258,4 +1264,63 @@
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
|
||||
var actual_lead_client_details = <?= json_encode($actual_lead_client_details ?? null) ?>;
|
||||
var actual_lead_contact_person_details = <?= json_encode($actual_lead_contact_person_details ?? null) ?>;
|
||||
|
||||
// -------------------------------
|
||||
// CLIENT DETAILS AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_client_details) {
|
||||
|
||||
$('#client_name').val(actual_lead_client_details.company_name || '');
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
|
||||
// 🔥 Important:
|
||||
// Only auto-generate short name IF empty (avoid overwrite in edit)
|
||||
if (!$('#client_short_name').val()) {
|
||||
$('#client_name').trigger('input');
|
||||
} else {
|
||||
// Run duplicate validation once
|
||||
validateInput($('#client_short_name')[0], "clients", "short_name");
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------
|
||||
// CONTACT PERSON AUTO FILL
|
||||
// -------------------------------
|
||||
if (actual_lead_contact_person_details) {
|
||||
|
||||
$('#contact_person_name').val(actual_lead_contact_person_details.name || '');
|
||||
|
||||
$('#contact_person_mobile').val(actual_lead_contact_person_details.mobile || '');
|
||||
|
||||
$('#contact_person_email').val(actual_lead_contact_person_details.email || '');
|
||||
}
|
||||
|
||||
|
||||
$('#lead_status').change(function() {
|
||||
if ($(this).val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
|
||||
// Check on page load
|
||||
if ($('#lead_status').val() === 'lost') {
|
||||
$('#lost_reason_div').show();
|
||||
$('#lost_reason').attr('required', 'required');
|
||||
} else {
|
||||
// Ensure it's hidden and not required if the initial value is not 'lost'
|
||||
$('#lost_reason_div').hide();
|
||||
$('#lost_reason').val("");
|
||||
$('#lost_reason').removeAttr('required');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -10,6 +10,8 @@
|
||||
.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;}
|
||||
.btn-view:hover { background: #f0f0f0; transform: translateY(-1px); }
|
||||
.btn-close { 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; }
|
||||
.btn-close:hover { background: #f0f0f0; color: #333; }
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
@ -68,6 +70,7 @@
|
||||
.activity-meta { display: flex; gap: 20px; font-size: 13px; color: #999; margin-top: 10px;}
|
||||
|
||||
.lead-status { display: inline-block; padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500; margin-top: 5px; }
|
||||
.opportunity-status-badge { padding: 4px 10px; border-radius: 12px; font-size: 11px; font-weight: 500;}
|
||||
|
||||
.status-new { background: #e3f2fd; color: #1976d2; }
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
@ -76,6 +79,10 @@
|
||||
.status-pending { background: #fff3e0; color: #f57c00; }
|
||||
.status-completed { background: #e8f5e9; color: #388e3c; }
|
||||
.status-unknown { background: #000; color: #fff; }
|
||||
.status-text-unknown { color: #000; font-weight: bold; }
|
||||
.status-text-lost { color: #d32f2f; font-weight: bold; }
|
||||
.status-text-won { color: #388e3c; font-weight: bold; }
|
||||
|
||||
|
||||
/* Modals */
|
||||
.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; }
|
||||
@ -124,6 +131,9 @@
|
||||
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
||||
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
|
||||
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
|
||||
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
|
||||
.notes { color: #555; }
|
||||
|
||||
/* Base style for both tabs */
|
||||
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
|
||||
@ -142,6 +152,21 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.select2-container--default
|
||||
.select2-selection--multiple
|
||||
.select2-selection__choice {
|
||||
background-color: #02a8b5 !important;
|
||||
border: none !important;
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #fff !important;
|
||||
}
|
||||
.modal .select2-container--default .select2-selection--multiple {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
@ -159,7 +184,7 @@
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)">
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openMainActivityModal()">
|
||||
+ Add Activity
|
||||
</button>
|
||||
@ -191,7 +216,7 @@
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body">
|
||||
@ -233,7 +258,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Activity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -276,11 +301,19 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="act_owner" class="search-input searchable" style="width:100%" required>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner</label>
|
||||
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -297,7 +330,7 @@
|
||||
<div class="modal-content" style="max-width: 675px;">
|
||||
<div class="modal-header">
|
||||
<h3>Complete Activity</h3>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -346,11 +379,19 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
|
||||
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -368,7 +409,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
<button class="btn-close" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
@ -401,18 +442,45 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
resetFlatpicker();
|
||||
$('.searchable').each(function() {
|
||||
let parentModal = $(this).closest('.modal');
|
||||
$(this).select2({
|
||||
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Search and select members...",
|
||||
allowClear: false,
|
||||
closeOnSelect: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||||
width: '100%'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// $(document).ready(function() {
|
||||
// resetFlatpicker();
|
||||
// $('.searchable').each(function() {
|
||||
// let parentModal = $(this).closest('.modal');
|
||||
// $(this).select2({
|
||||
// placeholder: "Select..",
|
||||
// dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
function resetFlatpicker(){
|
||||
document.querySelectorAll(".forschedule").forEach(el => {
|
||||
if (el._flatpickr) {
|
||||
@ -438,7 +506,7 @@ const activityIcons = {
|
||||
"Share Docs": "📄",
|
||||
"To Do": "✓"
|
||||
};
|
||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
||||
const salesManagerWithHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
let filter = 'all';
|
||||
@ -535,10 +603,10 @@ async function fetchActivities(isLoadMore = false) {
|
||||
</div>`;
|
||||
|
||||
console.log("function here");
|
||||
console.log(salesManagerIds);
|
||||
console.log(salesManagerWithHeadIds);
|
||||
// return;
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
|
||||
if (typeof salesManagerWithHeadIds === 'undefined' || salesManagerWithHeadIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -565,8 +633,8 @@ async function fetchActivities(isLoadMore = false) {
|
||||
// 5. Build URL with dynamic offset
|
||||
let url = `${API}/activities?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
||||
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
||||
if (typeof salesManagerWithHeadIds !== 'undefined' && salesManagerWithHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerWithHeadIds.join(',')}`;
|
||||
}
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
@ -578,6 +646,15 @@ async function fetchActivities(isLoadMore = false) {
|
||||
const activityData = json.data || [];
|
||||
const totalrecords = json.total || 0;
|
||||
console.log("API Response:", activityData);
|
||||
|
||||
const tabLeadsCount = json.counts || {};
|
||||
|
||||
// ✅ Update tab counts
|
||||
if (tabLeadsCount) {
|
||||
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
|
||||
document.querySelector('[data-filter="Pending"]').innerText = `Pending (${tabLeadsCount.pending ?? 0})`;
|
||||
document.querySelector('[data-filter="Completed"]').innerText = `Completed (${tabLeadsCount.completed ?? 0})`;
|
||||
}
|
||||
|
||||
const html = activityData
|
||||
.sort((a, b) => Number(b.activity_id) - Number(a.activity_id))
|
||||
@ -613,9 +690,18 @@ async function fetchActivities(isLoadMore = false) {
|
||||
<div class="activity-lead-name">${a.company_name}</div>
|
||||
<div class="activity-notes" title="${displayNote}">${displayNote}</div>
|
||||
<div class="activity-meta">
|
||||
<span>📅 ${formattedCreatedDate}</span>
|
||||
<span>🗓️ ${formattedCreatedDate}</span>
|
||||
<span>👤 ${a.assigned_to_name}</span>
|
||||
</div>
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-start; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
|
||||
<div class="activity-actions">
|
||||
@ -706,10 +792,17 @@ async function viewDetail(id) {
|
||||
}
|
||||
|
||||
function renderCard(opps) {
|
||||
const cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
cont.innerHTML = opps.length ? opps.map(o => {
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
lost: 'status-text-lost'
|
||||
}[status] || 'status-text-unknown';
|
||||
|
||||
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
|
||||
|
||||
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@ -736,11 +829,21 @@ function renderCard(opps) {
|
||||
<strong>Created At:</strong> ${formattedDate}
|
||||
</div>
|
||||
<div class="opportunity-detail-item">
|
||||
<strong>Status:</strong> ${o.status}
|
||||
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opportunity-footer">
|
||||
${o.notes || 'N/A'}
|
||||
${ `${o.status === 'lost' ? `
|
||||
<div class="lost-reason">
|
||||
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
|
||||
</div>
|
||||
<div class="footer-divider"></div>
|
||||
` : ``}
|
||||
<div class="notes">
|
||||
<strong>Remarks:</strong> ${o.notes || 'N/A'}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -774,24 +877,35 @@ function renderTimeline(acts) {
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||
<div class="timeline-content">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
||||
<b>${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; color:#444;">
|
||||
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : '<div style="margin-bottom:8px;"></div>'}
|
||||
|
||||
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
|
||||
${a.notes}
|
||||
</div>
|
||||
<div style="font-size:13px; color:#444;"></div>
|
||||
|
||||
${a.status === 'pending' ?
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id},'frompopup')">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||
}
|
||||
|
||||
@ -906,7 +1020,8 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
notes: actNotes, // Reused the trimmed variable from above
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res = await fetch(`${API}/activities`, {
|
||||
@ -1023,7 +1138,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
notes: document.getElementById('f_notes').value,
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: document.getElementById('f_assigned_to').value,
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res2 = await fetch(`${API}/activities`, {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
294
app/Views/sales/branch_level_dashboard_view_1mar.php
Normal file
294
app/Views/sales/branch_level_dashboard_view_1mar.php
Normal file
@ -0,0 +1,294 @@
|
||||
<style>
|
||||
.dash-container { padding: 25px; background: #f8f9fa; font-family: 'Segoe UI', sans-serif; }
|
||||
.top-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; }
|
||||
|
||||
.stat-cards { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
|
||||
.card { background: white; padding: 25px; border-radius: 12px; border: 1px solid #edf2f7; box-shadow: 0 2px 4px rgba(0,0,0,0.02); }
|
||||
.stat-val { font-size: 28px; font-weight: 700; color: #1a202c; }
|
||||
.stat-label { color: #718096; font-size: 14px; margin-top: 4px; font-weight: 500; }
|
||||
.stat-change { font-size: 11px; margin-top: 8px; font-weight: 600; }
|
||||
.text-success { color: #48bb78; }
|
||||
|
||||
.card-hero {
|
||||
background: white;
|
||||
padding: 25px;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #edf2f7;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.05);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* The Gradient Overlay Effect */
|
||||
.card-hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 4px; /* Thin line at the top */
|
||||
background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
|
||||
}
|
||||
|
||||
.card-hero:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 10px 15px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Optional: Subtle Background Gradient */
|
||||
.card-hero.gradient-bg {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8faff 100%);
|
||||
}
|
||||
|
||||
.main-grid { display: grid; grid-template-columns: 2fr 1fr; gap: 20px; margin-bottom: 30px; }
|
||||
.table-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
|
||||
.team-member { display: flex; align-items: center; padding: 15px 0; border-bottom: 1px solid #f1f5f9; }
|
||||
.member-img { width: 40px; height: 40px; border-radius: 50%; background: #ff6b35; color: white; margin-right: 12px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; }
|
||||
.act-stats { text-align: right; }
|
||||
.act-stats .total { font-weight: 700; font-size: 13px; color: #1a202c; }
|
||||
.act-stats .done { font-size: 11px; color: #38a169; font-weight: 600; margin-top: 2px; }
|
||||
|
||||
.breakdown-card { margin-top: 20px; }
|
||||
.breakdown-item { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; font-size: 13px; }
|
||||
.dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 8px; }
|
||||
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th { text-align: left; padding: 12px; color: #718096; font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em; border-bottom: 1px solid #edf2f7; }
|
||||
td { padding: 15px 12px; border-bottom: 1px solid #f1f5f9; font-size: 14px; vertical-align: middle; }
|
||||
|
||||
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
||||
.status-completed { background: #f0fff4; color: #38a169; }
|
||||
.status-pending { background: #fffaf0; color: #dd6b20; }
|
||||
.status-new { background: #e3f2fd; color: #1976d2; }
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
|
||||
.empty-state { text-align: center; padding: 60px 20px; color: #999; }
|
||||
.empty-icon { width: 80px; height: 80px; margin: 0 auto 20px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 36px; }
|
||||
|
||||
</style>
|
||||
|
||||
<div class="dash-container">
|
||||
<!-- <div class="top-header">
|
||||
<div>
|
||||
<h2 style="font-weight: 800; color: #1a202c; font-size: 24px;">Dashboard</h2>
|
||||
<p style="color: #718096; font-size: 14px; margin-top: 4px;">Overview of your branch</p>
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<input type="text" placeholder="Search leads, activities....." style="background:white; padding:12px 20px; border-radius:10px; border:1px solid #e2e8f0; width: 350px; font-size: 13px;">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="stat-cards">
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_leads; ?></div>
|
||||
<div class="stat-label">Total Leads</div>
|
||||
<!-- <div class="stat-change text-success">↑ 12% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_acts ?></div>
|
||||
<div class="stat-label">Total Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 5% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
||||
<div class="stat-label">Pending Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
||||
<div class="stat-label">Completed Activities</div>
|
||||
<!-- <div class="stat-change text-success">↑ 15% this month</div> -->
|
||||
</div>
|
||||
<!-- <div class="card">
|
||||
<div class="stat-val">₹15.0L</div>
|
||||
<div class="stat-label">Pipeline Value</div>
|
||||
<div class="stat-change text-success">↑ 18% this month</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="main-grid">
|
||||
<div class="card">
|
||||
<div class="table-header">
|
||||
<h3 style="font-size: 16px; font-weight: 700;">Pending Activities - All Team</h3>
|
||||
<span style="color: #718096; font-size: 12px; font-weight: 600;"><?= count($pending_acts) . ' ' . (count($pending_acts) == 1 ? 'activity' : 'activities') ?></span>
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th>Activity & Assigned</th>
|
||||
<th>Date</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($pending_acts)): ?>
|
||||
<?php foreach ($pending_acts as $a): ?>
|
||||
<?php
|
||||
$activityIcons = [
|
||||
'Call' => '📞',
|
||||
'Email' => '✉️',
|
||||
'Meeting' => '📅',
|
||||
'Visit' => '🚗',
|
||||
'Demo' => '🖥️',
|
||||
'Share Docs' => '📄',
|
||||
'To Do' => '✓'
|
||||
];
|
||||
$icon = $activityIcons[$a['activity_type']] ?? '📌';
|
||||
$statusClass = strtolower(str_replace(' ', '-', $a['status']));
|
||||
$formattedscheduledDate = date('M d, Y, h:i A', strtotime($a['scheduled_date']));
|
||||
?>
|
||||
<tr>
|
||||
<td><strong><?= esc($a['company_name']) ?></strong></td>
|
||||
<td>
|
||||
<div style="color: #ff6b35; font-weight: 700; font-size: 11px;">
|
||||
<?= $icon ?> <?= strtoupper(esc($a['activity_type'])) ?>
|
||||
</div>
|
||||
<div style="font-size: 12px; color: #718096;">
|
||||
<?= esc($a['assigned_to_name'] ?? 'ID: ' . $a['assigned_to']) ?>
|
||||
</div>
|
||||
<!-- <div style="font-size: 12px; color: #718096;">
|
||||
<?= esc($a['notes'] ?? 'ID: ' . $a['assigned_to']) ?>
|
||||
</div> -->
|
||||
</td>
|
||||
<td style="color: #4a5568; font-weight: 500;"><?= $formattedscheduledDate ?></td>
|
||||
<td><span class="status-pill status-<?= $statusClass ?>"><?= ucfirst(esc($a['status'])) ?></span></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="4">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✓</div> No activities found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="card">
|
||||
|
||||
<h3 style="font-size: 16px; font-weight: 700; margin-bottom: 15px;">Sales Team Performance</h3>
|
||||
<?php foreach ($team as $member) {
|
||||
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
||||
$fullName = $member['first_name'] . ' ' . $member['last_name'];
|
||||
$totalActs = $member['total_acts'];
|
||||
$doneActs = $member['done_acts'];
|
||||
$role = $member['role']; // fix your DB first
|
||||
|
||||
// Random or consistent color based on name
|
||||
$colors = ['#ff6b35', '#667eea', '#48bb78', '#ed8936', '#9f7aea'];
|
||||
$colorIndex = abs(crc32($member['first_name'])) % count($colors);
|
||||
$color = $colors[$colorIndex];
|
||||
|
||||
echo "
|
||||
<div class='team-member'>
|
||||
<div class='member-img' style='background: {$color};'>{$firstLetter}</div>
|
||||
<div style='flex: 1;'>
|
||||
<div style='font-weight: 700; font-size: 14px;'>{$fullName}</div>
|
||||
<div style='font-size: 11px; color: #718096;'>{$role}</div>
|
||||
</div>
|
||||
<div class='act-stats'>
|
||||
<div class='total'>{$totalActs} acts</div>
|
||||
<div class='done'>{$doneActs} done</div>
|
||||
</div>
|
||||
</div>";
|
||||
} ?>
|
||||
|
||||
|
||||
<div class="card breakdown-card">
|
||||
<h3 style="font-size: 15px; font-weight: 700; margin-bottom: 20px;">Activity Breakdown</h3>
|
||||
<!-- <div class="breakdown-item"><span><span class="dot" style="background: #ff6b35;"></span> Call</span><strong>42%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #48bb78;"></span> Email</span><strong>25%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #4299e1;"></span> Meeting</span><strong>18%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #ecc94b;"></span> Visit</span><strong>10%</strong></div>
|
||||
<div class="breakdown-item"><span><span class="dot" style="background: #9f7aea;"></span> Demo</span><strong>5%</strong></div> -->
|
||||
<?php
|
||||
$activityConfig = [
|
||||
'Call' => ['color' => '#ff6b35', 'icon' => '📞'],
|
||||
'Email' => ['color' => '#48bb78', 'icon' => '✉️'],
|
||||
'Meeting' => ['color' => '#4299e1', 'icon' => '📅'],
|
||||
'Visit' => ['color' => '#ecc94b', 'icon' => '🚗'],
|
||||
'Demo' => ['color' => '#9f7aea', 'icon' => '🖥️'],
|
||||
'Share Docs' => ['color' => '#ed8936', 'icon' => '📄'],
|
||||
'To Do' => ['color' => '#718096', 'icon' => '✓'],
|
||||
];
|
||||
?>
|
||||
|
||||
<?php if (!empty($activity_breakdown)): ?>
|
||||
<?php foreach ($activity_breakdown as $item): ?>
|
||||
<?php
|
||||
$type = $item['activity_type'];
|
||||
$color = $activityConfig[$type]['color'] ?? '#718096';
|
||||
$icon = $activityConfig[$type]['icon'] ?? '📌';
|
||||
?>
|
||||
<div class="breakdown-item">
|
||||
<span>
|
||||
<span style="background: <?= $color ?>;"></span>
|
||||
<?= $icon ?> <?= esc($type) ?>
|
||||
</span>
|
||||
<strong><?= $item['percentage'] ?>%</strong>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">✓</div> No activities found
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 </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> -->
|
||||
</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Company</th>
|
||||
<th>Status</th>
|
||||
<th>Assigned To</th>
|
||||
<th style="text-align: center !important;">Activities</th>
|
||||
<th style="text-align: center !important;">Opportunities</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if (!empty($leads_overview)): ?>
|
||||
<?php foreach ($leads_overview as $lead): ?>
|
||||
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
|
||||
<tr>
|
||||
<td><strong><?= esc($lead['company_name']) ?></strong></td>
|
||||
<td><span class="status-pill status-<?= $statusClass ?>"><?= esc($lead['status']) ?></span></td>
|
||||
<td><?= esc($lead['assigned_to'] ?? 'Unassigned') ?></td>
|
||||
<td><div style="text-align: center; font-weight: 700;"><?= $lead['activities'] ?></div></td>
|
||||
<td><div style="text-align: center; font-weight: 700;"><?= $lead['opportunities'] ?></div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php else: ?>
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon">👤</div> No leads found
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@ -74,7 +74,7 @@
|
||||
<p style="color:#666; font-size:13px; margin-top: 4px;">Welcome back, <?= $user_name ?></p> -->
|
||||
</div>
|
||||
<div style="position: relative;">
|
||||
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)" style="background:#f5f5f5; padding:10px 20px; border-radius:8px; border:none; width: 250px; font-size: 13px; cursor: pointer;">
|
||||
<select id="financial_year" name="financial_year" onchange="onFinancialYearChange(this)" style="width: 250px; font-size: 13px; cursor: pointer;">
|
||||
<?php if(!empty($fin_years)): ?>
|
||||
<?php foreach($fin_years as $year): ?>
|
||||
<option value="<?= $year; ?>"><?= $year; ?></option>
|
||||
@ -133,6 +133,9 @@
|
||||
<button class="badge-done" onclick="openComp(<?= $u['activity_id'] ?>,<?= $u['lead_id'] ?>)">✓ Mark as completed</button>
|
||||
</div>
|
||||
<?php endforeach; ?> -->
|
||||
<?php if (!empty($upcoming)) : ?>
|
||||
|
||||
|
||||
<?php foreach($upcoming as $u):
|
||||
$activityIcons = [ 'Call' => '📞', 'Email' => '✉️', 'Meeting' => '📅', 'Visit' => '🚗', 'Demo' => '🖥️', 'Share Docs' => '📄', 'To Do' => '✓' ];
|
||||
$icon = $activityIcons[$u['activity_type']] ?? '📌';
|
||||
@ -160,11 +163,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="text-center text-muted py-3">
|
||||
No Upcoming Activities for this Financial Year.
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="list-card">
|
||||
<h3 style="font-size:16px; font-weight: 700; margin-bottom:20px;">My Recent Leads</h3>
|
||||
<?php if (!empty($recent_leads)) : ?>
|
||||
<?php foreach($recent_leads as $rl): ?>
|
||||
<!-- <div class="list-item">
|
||||
<div style="display:flex; gap:12px; align-items:center;">
|
||||
@ -206,6 +217,13 @@
|
||||
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
<?php else : ?>
|
||||
|
||||
<div class="text-center text-muted py-3">
|
||||
No Leads Found for this Financial Year.
|
||||
</div>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -307,12 +307,12 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const users = <?= json_encode($users ?? []) ?>;
|
||||
const sales_manager = <?= json_encode($sales_manager ?? []) ?>;
|
||||
|
||||
/* ──────────────────────────────────────────
|
||||
DATA
|
||||
────────────────────────────────────────── */
|
||||
const teamMembers = users.map(user => ({
|
||||
const teamMembers = sales_manager.map(user => ({
|
||||
id: user.id,
|
||||
name: user.first_name + ' ' + user.last_name
|
||||
}));
|
||||
|
||||
@ -7,6 +7,8 @@
|
||||
|
||||
.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-close { 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; }
|
||||
.btn-close:hover { background: #f0f0f0; color: #333; }
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
@ -24,6 +26,11 @@
|
||||
.status-potential { background: #fff3e0; color: #f57c00; }
|
||||
.status-prospects { background: #e8f5e9; color: #388e3c; }
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
.status-unknown { background: #000; color: #fff; }
|
||||
.status-text-unknown { color: #000; font-weight: bold; }
|
||||
.status-text-lost { color: #d32f2f; font-weight: bold; }
|
||||
.status-text-won { color: #388e3c; font-weight: bold; }
|
||||
|
||||
|
||||
/* Modals */
|
||||
.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; }
|
||||
@ -72,6 +79,9 @@
|
||||
.opportunity-details { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; margin-top: 15px;}
|
||||
.opportunity-detail-item { font-size: 13px; color: #666;}
|
||||
.opportunity-footer { margin-top: 15px; padding-top: 15px; border-top: 1px solid #f0f0f0; font-size: 13px; color: #666; }
|
||||
.lost-reason { margin-bottom: 8px; color: #c0392b; /* soft red for lost */ }
|
||||
.footer-divider { border-top: 1px dashed #ddd; margin: 8px 0; }
|
||||
.notes { color: #555; }
|
||||
|
||||
/* Base style for both tabs */
|
||||
.tab-item { cursor: pointer; padding-bottom: 10px; margin: 0; font-size: 16px; color: #999; /* Default grey for unselected */ border-bottom: 2px solid transparent; transition: all 0.2s ease; }
|
||||
@ -105,7 +115,20 @@
|
||||
border-color: #ff4d4d !important;
|
||||
}
|
||||
|
||||
|
||||
.select2-container--default
|
||||
.select2-selection--multiple
|
||||
.select2-selection__choice {
|
||||
background-color: #02a8b5 !important;
|
||||
border: none !important;
|
||||
border-color: #fff !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
.select2-container--default .select2-selection--multiple .select2-selection__choice__remove {
|
||||
color: #fff !important;
|
||||
}
|
||||
.modal .select2-container--default .select2-selection--multiple {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="main-content">
|
||||
@ -137,7 +160,7 @@
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)">
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openModal('addLeadModal')">
|
||||
+ Add Lead
|
||||
</button>
|
||||
@ -166,7 +189,7 @@
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Add Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('addLeadModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('addLeadModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="addLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -203,8 +226,8 @@
|
||||
<label class="form-label">Assign To<span class="text-danger">*</span></label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@ -228,7 +251,7 @@
|
||||
<h2 id="det_company">Lead Detail</h2>
|
||||
<span id="det_status_badge" class="lead-status" style="margin-top: unset;"></span>
|
||||
</div>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('leadDetailModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('leadDetailModal')" title="Close">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body">
|
||||
@ -270,7 +293,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Activity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('activityModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('activityModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="activityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -305,12 +328,20 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To</label>
|
||||
<select id="act_owner" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner</label>
|
||||
<select id="act_multiple_owner" class="search-input searchable multi-searchable" style="width:100%;" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group text-right mb-0">
|
||||
@ -326,7 +357,7 @@
|
||||
<div class="modal-content" style="max-width: 675px;">
|
||||
<div class="modal-header">
|
||||
<h3>Complete Activity</h3>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('completeModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('completeModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="completeForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -374,12 +405,20 @@
|
||||
<div class="form-group">
|
||||
<label class="form-label">Assigned To <span class="text-danger">*</span></label>
|
||||
<select id="f_assigned_to" class="search-input searchable" style="width:100%">
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?></option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Additional Assigner <span class="text-danger">*</span></label>
|
||||
<select id="f_multiple_owner" class="search-input searchable multi-searchable" style="width:100%" multiple>
|
||||
<?php foreach($sales_team as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@ -397,7 +436,7 @@
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Select Opportunity Type</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('opportunityModal')" title="Close">×</button>
|
||||
</div>
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
@ -429,61 +468,12 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div class="modal" id="opportunityModal">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<div class="modal-header">
|
||||
<h2>Add Opportunity</h2>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('opportunityModal')">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="OpportunityForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
<div class="modal-body p-4">
|
||||
|
||||
<input type="hidden" id="opp_lead_id">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Opportunity Title</label>
|
||||
<input type="text" name="title" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group ml-1">
|
||||
<label class="form-label">Expected Worth Amount</label>
|
||||
<input type="number" name="amount" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group mr-1">
|
||||
<label class="form-label">Policy Type</label>
|
||||
<input type="text" name="policy_type" class="search-input" style="width:100%">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Employee Count</label>
|
||||
<input type="number" name="count" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Expected Close Date</label>
|
||||
<input type="datetime-local" id="e_date" class="search-input" style="width:100%">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="opp_notes" class="search-input" style="width:100%; height:100px;" placeholder="Add notes about this opportunity..." required></textarea>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="form-group text-right mb-0">
|
||||
<button type="button" class="btn-primary" style="background:#eee; color:#333; margin-right:10px;" onclick="closeModal('opportunityModal')">Cancel</button>
|
||||
<button type="submit" class="btn-primary" >Create Opportunity</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="modal" id="editLeadModal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">Edit Lead</h4>
|
||||
<button class="btn-primary" style="background:none; color:#666; font-size:24px;" title="Close" onclick="closeModal('editLeadModal')">×</button>
|
||||
<button class="btn-close" onclick="closeModal('editLeadModal')" title="Close">×</button>
|
||||
</div>
|
||||
<form class="parsley-examples" id="editLeadForm" enctype="multipart/form-data">
|
||||
<hr class="my-0">
|
||||
@ -525,46 +515,75 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mb-1" style="padding-left: 25px;">
|
||||
<label class="form-label">Contact Persons</label>
|
||||
<div id="contactPersonsContainer">
|
||||
<div id="savedContactsContainer"> <span id="NoData"> <center><i> No contact persons added yet </i> </center> </span> </div>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.g-0-5 { --bs-gutter-x: 0.3rem; }
|
||||
.g-0-5 > * { padding-right: calc(var(--bs-gutter-x) * .5); padding-left: calc(var(--bs-gutter-x) * .5); }
|
||||
|
||||
<div class="col-12 mb-1 ml-1 ">
|
||||
|
||||
<div class="row g-2 align-items-center">
|
||||
|
||||
<div class="col-md-5" style="padding-left: 20px;">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="contact_name"
|
||||
placeholder="Contact Person Name"
|
||||
oninput="this.value=this.value.replace(/[^A-Za-z\s]/g,'')">
|
||||
.primary-container {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Ensures the checkbox container height matches the standard input height */
|
||||
height: 31px;
|
||||
}
|
||||
.primary-label {
|
||||
font-size: 12px !important;
|
||||
font-weight: bold;
|
||||
/* Forces the text to take up exactly its own height */
|
||||
line-height: 1;
|
||||
margin-top: 2px;
|
||||
color: #888;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* This is the most important part */
|
||||
.primary-container .form-check-input {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
cursor: pointer;
|
||||
/* Reset Bootstrap's default position: absolute if present */
|
||||
position: static;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="col-12 ml-1">
|
||||
<label class="form-label fw-bold small ml-1">Contact Persons</label>
|
||||
<input type="hidden" id="editing_contact_id" value="">
|
||||
<div class="row g-0-5 align-items-end ml-1">
|
||||
<div class="col-md-3 col-12 mb-2 mb-md-0">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_name" placeholder="Person Name" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
id="contact_mobile"
|
||||
placeholder="Contact Mobile Number"
|
||||
maxlength="10"
|
||||
oninput="this.value=this.value.replace(/[^0-9]/g,'')">
|
||||
<div class="col-md-3 col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_mobile" placeholder="Mobile" maxlength="10" oninput="this.value = this.value.replace(/[^0-9]/g, '').substring(0, 10);">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3" style="padding-right: 4%;">
|
||||
<button type="button"
|
||||
id="btnSaveContact"
|
||||
class="btn btn-sm w-100"
|
||||
style="background:#ff6a3d;border:none;color:white;">
|
||||
+ Save Contact
|
||||
<div class="col-md-3 col-6">
|
||||
<input type="text" class="form-control form-control-sm" id="contact_designation" placeholder="Designation" oninput="this.value = this.value.replace(/[^A-Za-z\s]/g, '')">
|
||||
</div>
|
||||
|
||||
<div class="col-md-1 col-3">
|
||||
<div class="primary-container">
|
||||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||||
<input class="form-check-input" type="checkbox" id="contact_is_primary">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-2 col-6">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:85%">
|
||||
✔ Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="contactPersonsList" class="p-1">
|
||||
<div id="noContactPersonsData" class="py-1 text-center">
|
||||
<small class="text-muted fst-italic">No contact persons added yet</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-row mb-1">
|
||||
<div class="col-md-12 ml-3" style="width: 97%;">
|
||||
@ -580,8 +599,8 @@
|
||||
<label class="form-label">Assign To <span class="text-danger">*</span> </label>
|
||||
<select name="assigned_to" class="form-control searchable" style="width: 100%;" required>
|
||||
<option value="">Select User</option>
|
||||
<?php foreach($users as $user): ?>
|
||||
<option value="<?= $user['id'] ?>"><?= $user['first_name'] ?> </option>
|
||||
<?php foreach($sales_manager_with_head as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= $sm['first_name'] ?> </option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
@ -599,16 +618,32 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
resetFlatpicker();
|
||||
$('.searchable').each(function() {
|
||||
let parentModal = $(this).closest('.modal');
|
||||
$(this).select2({
|
||||
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
dropdownParent: parentModal.length ? parentModal : $(document.body)
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
document.querySelectorAll('.multi-searchable').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Search and select members...",
|
||||
allowClear: false,
|
||||
closeOnSelect: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body),
|
||||
width: '100%'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function resetFlatpicker(){
|
||||
@ -626,7 +661,7 @@ function resetFlatpicker(){
|
||||
allowInput: false,
|
||||
});
|
||||
}
|
||||
const salesManagerIds = <?= json_encode($sales_manager_ids ?? []) ?>;
|
||||
const salesManagerHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
let filter = 'all';
|
||||
let lead_id = null;
|
||||
@ -648,7 +683,12 @@ function closeModal(id) {
|
||||
const form = modal.querySelector('form'); // Find the form inside this specific modal
|
||||
if (form) {
|
||||
form.reset(); // Resets standard inputs (text, email, select)
|
||||
$(form).find('.searchable').val('').trigger('change'); // Clear jQuery/Select2/Searchable dropdowns if you use them
|
||||
form.querySelectorAll('.searchable').forEach(function(el) {
|
||||
// Reset native select
|
||||
Array.from(el.options).forEach(o => o.selected = false);
|
||||
// Notify Select2 to refresh its UI
|
||||
$(el).trigger('change');
|
||||
});
|
||||
$(form).find('.is-invalid').removeClass('is-invalid'); // Remove any "is-invalid" red borders from previous validation errors
|
||||
}
|
||||
|
||||
@ -672,6 +712,15 @@ function closeModal(id) {
|
||||
switchTab('activity'); // Reset the tab back to 'activity'
|
||||
document.getElementById('btn_add_opportunity').style.display = 'none';
|
||||
}
|
||||
|
||||
if (id === 'editLeadModal') {
|
||||
// const Eform = document.getElementById('editLeadForm');
|
||||
// if (!Eform) return; // safety check
|
||||
// Eform.reset();
|
||||
form.querySelectorAll('[name="status"] option')
|
||||
.forEach(opt => opt.hidden = false);
|
||||
form.querySelector('[name="status"]').value = 'New';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -733,7 +782,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
</div>`;
|
||||
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerIds === 'undefined' || salesManagerIds.length === 0) {
|
||||
if (typeof salesManagerHeadIds === 'undefined' || salesManagerHeadIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -759,9 +808,9 @@ async function fetchLeads(isLoadMore = false) {
|
||||
|
||||
// 5. Build URL with dynamic offset
|
||||
let url = `${API}/leads?status=${filter === 'all' ? '' : filter}&search=${q}&limit=${limit}&offset=${currentOffset}`;
|
||||
// url += `&assigned_to=${salesManagerIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerIds !== 'undefined' && salesManagerIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerIds.join(',')}`;
|
||||
// url += `&assigned_to=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
|
||||
}
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
@ -773,6 +822,17 @@ async function fetchLeads(isLoadMore = false) {
|
||||
const leadsData = json.data || [];
|
||||
const totalrecords = json.total || 0;
|
||||
console.log("API Response:", leadsData);
|
||||
|
||||
const tabLeadsCount = json.counts || {};
|
||||
|
||||
// ✅ Update tab counts
|
||||
if (tabLeadsCount) {
|
||||
document.querySelector('[data-filter="all"]').innerText = `All (${tabLeadsCount.all ?? 0})`;
|
||||
document.querySelector('[data-filter="New"]').innerText = `New (${tabLeadsCount.New ?? 0})`;
|
||||
document.querySelector('[data-filter="Potential"]').innerText = `Potential (${tabLeadsCount.Potential ?? 0})`;
|
||||
document.querySelector('[data-filter="Prospects"]').innerText = `Prospects (${tabLeadsCount.Prospects ?? 0})`;
|
||||
document.querySelector('[data-filter="Not a Prospects"]').innerText = `Not a Prospects (${tabLeadsCount['Not a Prospects'] ?? 0})`;
|
||||
}
|
||||
|
||||
const html = leadsData
|
||||
.sort((a, b) => Number(b.lead_id) - Number(a.lead_id))
|
||||
@ -801,7 +861,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
</div>
|
||||
<div class="dropdown">
|
||||
<a class="text-body dropdown-toggle" href="#" data-toggle="dropdown">
|
||||
<i class="mdi mdi-dots-vertical font-20" title="Options"></i>
|
||||
<i class="mdi mdi-dots-vertical font-20 btn-close" title="Options"></i>
|
||||
</a>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a class="dropdown-item" href="javascript:void(0);" onclick="viewDetail(${l.lead_id})"> 👁 View</a>
|
||||
@ -894,10 +954,21 @@ async function viewDetail(id) {
|
||||
}
|
||||
|
||||
function renderCard(opps) {
|
||||
const cont = document.getElementById('opportunitiesContainer');
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
cont.innerHTML = opps.length ? opps.map(o => {
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
lost: 'status-text-lost'
|
||||
}[status] || 'status-text-unknown';
|
||||
|
||||
let opp_status_value = o.status?.replace(/[-_']/g, ' ').toUpperCase();
|
||||
|
||||
|
||||
|
||||
|
||||
let formattedDate = new Date(o.created_at).toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
@ -924,11 +995,21 @@ function renderCard(opps) {
|
||||
<strong>Created At:</strong> ${formattedDate}
|
||||
</div>
|
||||
<div class="opportunity-detail-item">
|
||||
<strong>Status:</strong> ${o.status}
|
||||
<strong>Status:</strong> <span class="${statusClass}">${opp_status_value}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="opportunity-footer">
|
||||
${o.notes || 'N/A'}
|
||||
${ `${o.status === 'lost' ? `
|
||||
<div class="lost-reason">
|
||||
<strong>Reason for Loss:</strong> ${o.lost_reason || 'N/A'}
|
||||
</div>
|
||||
<div class="footer-divider"></div>
|
||||
` : ``}
|
||||
<div class="notes">
|
||||
<strong>Remarks:</strong> ${o.notes || 'N/A'}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@ -936,15 +1017,7 @@ function renderCard(opps) {
|
||||
}
|
||||
function renderTimeline(acts) {
|
||||
const cont = document.getElementById('timelineContainer');
|
||||
const activityIcons = {
|
||||
"Call": "📞",
|
||||
"Email": "✉️",
|
||||
"Meeting": "📅",
|
||||
"Visit": "🚗",
|
||||
"Demo": "🖥️",
|
||||
"Share Docs": "📄",
|
||||
"To Do": "✓"
|
||||
};
|
||||
const activityIcons = { "Call": "📞", "Email": "✉️", "Meeting": "📅", "Visit": "🚗", "Demo": "🖥️", "Share Docs": "📄", "To Do": "✓"};
|
||||
|
||||
if (acts.length === 0) {
|
||||
cont.classList.add('no-line');
|
||||
@ -969,29 +1042,41 @@ function renderTimeline(acts) {
|
||||
// <span style="font-size:11px; color:#999">${formattedDate}</span>
|
||||
|
||||
return `
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-dot ${a.status==='completed'?'completed':''}"></div>
|
||||
<div class="timeline-content">
|
||||
<div style="display:flex; justify-content:space-between; margin-bottom:5px;">
|
||||
<b>${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999"> 👤 ${a.assigned_to_name} | 🗓 ${formattedDate}</span>
|
||||
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:2px;">
|
||||
<b style="white-space: nowrap; margin-right: 15px;">${icon} ${formattedType}</b>
|
||||
<span style="font-size:11px; color:#999; text-align:right;"> 👤 ${a.assigned_to_name} | 🗓️ ${formattedDate}</span>
|
||||
</div>
|
||||
<div style="display:flex; justify-content:space-between; font-size:13px; color:#444;">
|
||||
|
||||
${a.additional_assigned_names ? `
|
||||
<div style="display:flex; justify-content:flex-end; margin-bottom:5px;">
|
||||
<span style="font-size:11px; color:#999; text-align: right;
|
||||
max-width: 75%; /* Forces long lists to wrap cleanly on the right side */
|
||||
display: inline-block;">
|
||||
👥 ${a.additional_assigned_names}
|
||||
</span>
|
||||
</div>
|
||||
` : '<div style="margin-bottom:8px;"></div>'}
|
||||
|
||||
<div style="font-size:13px; color:#444; margin-bottom: 8px;">
|
||||
${a.notes}
|
||||
</div>
|
||||
<div style="font-size:13px; color:#444;"></div>
|
||||
|
||||
${a.status === 'pending' ?
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:8px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:8px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
`<button class="btn-primary" style="padding:4px 10px; font-size:11px; margin-top:4px;" onclick="openComp(${a.activity_id},${a.assigned_to},${a.lead_id})">Mark Complete</button>` :
|
||||
`<div style="font-size:12px; color:#388e3c; margin-top:4px; font-weight:500;">✓ Outcome: ${a.completion_notes}</div>`
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
</div>`;
|
||||
}).join('') : `<div class="empty-state"><div class="empty-icon">✓</div> No activities yet. Add one to get started!</div>`;
|
||||
}
|
||||
function openActivityModal() {
|
||||
document.getElementById('act_owner').value = global_lead_assigned_to;
|
||||
document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||||
$(document.getElementById('act_owner')).trigger('change'); // tells Select2 to update display
|
||||
// document.getElementById('act_owner').dispatchEvent(new Event('change'));
|
||||
document.getElementById('act_lead_id').value = lead_id;
|
||||
openModal('activityModal');
|
||||
}
|
||||
@ -1004,7 +1089,8 @@ function openComp(id, assigned_to, lead_id) {
|
||||
document.getElementById('comp_id').value = id;
|
||||
document.getElementById('lead_id').value = lead_id;
|
||||
document.getElementById('f_assigned_to').value = assigned_to;
|
||||
document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
|
||||
$(document.getElementById('f_assigned_to')).trigger('change'); // tells Select2 to update display
|
||||
// document.getElementById('f_assigned_to').dispatchEvent(new Event('change'));
|
||||
document.getElementById('f_typeButtons').querySelectorAll('.f_activity_type')
|
||||
.forEach(btn => btn.classList.remove('active'));
|
||||
selectedFollowUpActivityType = '';
|
||||
@ -1250,11 +1336,14 @@ document.getElementById('editLeadForm').onsubmit = async (e) => {
|
||||
|
||||
if(res.ok) {
|
||||
toastr.success('Leads Updated Successfully');
|
||||
e.target.reset();
|
||||
closeModal('editLeadModal');
|
||||
fetchLeads();
|
||||
e.target.reset();
|
||||
$('#savedContactsContainer').empty();
|
||||
$('#NoData').show();
|
||||
// CLEANUP: Reset the contact UI for the next time it opens
|
||||
$('#contactPersonsList').find('.contact-card').remove();
|
||||
$('#noContactPersonsData').show();
|
||||
document.getElementById('editing_contact_id').value = '';
|
||||
document.getElementById('btnSaveContact').innerHTML = "✔ Save";
|
||||
} else {
|
||||
let err = await res.json();
|
||||
if (res.status === 400) {
|
||||
@ -1346,7 +1435,9 @@ document.getElementById('activityForm').onsubmit = async (e) => {
|
||||
notes: actNotes, // Reused the trimmed variable from above
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: actOwner, // Reused the trimmed variable from above
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('act_multiple_owner').selectedOptions).map(o => o.value)
|
||||
|
||||
};
|
||||
|
||||
const res = await fetch(`${API}/activities`, {
|
||||
@ -1483,7 +1574,8 @@ document.getElementById('completeForm').onsubmit = async (e) => {
|
||||
notes: document.getElementById('f_notes').value,
|
||||
scheduled_date: dbFormat,
|
||||
assigned_to: document.getElementById('f_assigned_to').value,
|
||||
status: 'pending'
|
||||
status: 'pending',
|
||||
additional_assigned_ids : Array.from(document.getElementById('f_multiple_owner').selectedOptions).map(o => o.value)
|
||||
};
|
||||
|
||||
const res2 = await fetch(`${API}/activities`, {
|
||||
@ -1594,146 +1686,11 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
}
|
||||
});
|
||||
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
btnSaveContact.onclick = async (e) => {
|
||||
// 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('hidden_lead_id').value.trim();
|
||||
|
||||
if (!name || !mobile) {
|
||||
return toastr.warning('Please enter both contact person name and mobile number.');
|
||||
}
|
||||
|
||||
let payload = { lead_id: lead_id, name: name, mobile: mobile };
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
const contactResult = await res.json(); // Get the response body
|
||||
|
||||
if (res.ok) {
|
||||
// Clear inputs
|
||||
document.getElementById('contact_name').value = '';
|
||||
document.getElementById('contact_mobile').value = '';
|
||||
|
||||
// Hide "No Data" message
|
||||
const noData = document.getElementById('NoData');
|
||||
if(noData) noData.style.display = 'none';
|
||||
|
||||
// Create the HTML string
|
||||
let contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${contactResult.data.contact_id}">
|
||||
<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="${contactResult.data.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// <button type="button"
|
||||
// class="btn btn-secondary btn-sm btnEditContact"
|
||||
// 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 {
|
||||
|
||||
if (res.status === 400) {
|
||||
let errorMessages = "";
|
||||
let seenMessages = [];
|
||||
if (contactResult.messages) {
|
||||
Object.entries(contactResult.messages).forEach(([field, message]) => {
|
||||
let inputElement = $('[name="' + field + '"]');
|
||||
|
||||
if (inputElement.length > 0) {
|
||||
// Make the field box turn red so the user sees it immediately
|
||||
inputElement.addClass('is-invalid');
|
||||
|
||||
// Focus on the first error field
|
||||
if (isFirstError) {
|
||||
// The 100ms delay safely bypasses Bootstrap's modal focus block
|
||||
setTimeout(function() {
|
||||
inputElement.focus();
|
||||
}, 100);
|
||||
|
||||
isFirstError = false;
|
||||
}
|
||||
}
|
||||
if (!seenMessages.includes(message)) {
|
||||
errorMessages += `• ${message}<br>`;
|
||||
seenMessages.push(message);
|
||||
}
|
||||
});
|
||||
toastr.error(errorMessages, 'Validation Error', { allowHtml: true });
|
||||
} else {
|
||||
toastr.warning(contactResult.message || 'Validation failed', 'Warning');
|
||||
}
|
||||
}
|
||||
else {
|
||||
toastr.error(contactResult.message || 'Error adding lead');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('An error occurred');
|
||||
}
|
||||
};
|
||||
}
|
||||
const savedContactsContainer = document.getElementById('savedContactsContainer');
|
||||
if (savedContactsContainer) {
|
||||
savedContactsContainer.onclick = async (e) => {
|
||||
|
||||
// Check if the clicked element is a Remove button
|
||||
if (e.target.classList.contains('btnRemoveContact')) {
|
||||
const btn = e.target;
|
||||
const contactId = btn.dataset.id; // Get data-id
|
||||
const card = btn.closest('.contact-card'); // Find the parent card
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
card.remove(); // Remove from DOM
|
||||
toastr.success('Contact removed successfully');
|
||||
|
||||
// Check if container is empty to show "No Data"
|
||||
const container = document.getElementById('savedContactsContainer');
|
||||
if (container.querySelectorAll('.contact-card').length === 0) {
|
||||
const noData = document.getElementById('NoData');
|
||||
if(noData) noData.style.display = 'block';
|
||||
}
|
||||
} else {
|
||||
toastr.error('Failed to remove contact');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
toastr.error('An error occurred');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
const container = $('#savedContactsContainer');
|
||||
const container = $('#contactPersonsList');
|
||||
// Remove only previous contact cards, keep the NoData span for now
|
||||
container.find('.contact-card').remove();
|
||||
|
||||
@ -1757,35 +1714,49 @@ async function openEditLeadModal(id) {
|
||||
form.querySelector('[name="address"]').value = lead.address || '';
|
||||
form.querySelector('[name="website"]').value = lead.website || '';
|
||||
form.querySelector('[name="gst_number"]').value = lead.gst_number || '';
|
||||
form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
// form.querySelector('[name="status"]').value = lead.status || 'New';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
form.querySelector('[name="assigned_to"]').value = lead.assigned_to || '';
|
||||
$(form.querySelector('[name="assigned_to"]')).trigger('change');
|
||||
const statusSelect = form.querySelector('[name="status"]');
|
||||
statusSelect.value = lead.status || 'New';
|
||||
updateStatusOptions(statusSelect.value);
|
||||
|
||||
// 4. Handle Contact Persons (Looping through the nested array)
|
||||
const contacts = lead.contact_persons; // Array from your JSON
|
||||
|
||||
if (contacts && contacts.length > 0) {
|
||||
$('#NoData').hide();
|
||||
|
||||
$('#noContactPersonsData').hide();
|
||||
contacts.forEach(contact => {
|
||||
const isPrimary = contact.is_primary == 1;
|
||||
const primaryBadge = isPrimary ? `<div style="margin-top:2px;"><span class="badge bg-warning text-dark ml-1" style="font-size:9px;">PRIMARY</span></div>` : '';
|
||||
|
||||
let contactHtml = `
|
||||
<div class="contact-card d-flex justify-content-between align-items-center mb-2 p-3"
|
||||
style="background: #f8f8f8; border-radius: 8px;"
|
||||
data-id="${contact.contact_id}">
|
||||
<div>
|
||||
<div class="fw-bold">${contact.name}</div>
|
||||
<div class="text-muted">${contact.mobile}</div>
|
||||
<div class="contact-card d-flex align-items-center mb-2 p-2 px-3"
|
||||
style="background: ${isPrimary ? '#fff9c4' : '#f8f8f8'}; border: 1px solid ${isPrimary ? '#fbc02d' : '#eee'}; border-radius: 8px; gap: 12px;">
|
||||
|
||||
<div style="flex: 2; min-width: 0;">
|
||||
<div class="fw-bold text-truncate" style="font-size:14px;">${contact.name} ${primaryBadge}</div>
|
||||
</div>
|
||||
<button type="button"
|
||||
class="btn btn-danger btn-sm btnRemoveContact"
|
||||
data-id="${contact.contact_id}">
|
||||
Remove
|
||||
</button>
|
||||
</div>`;
|
||||
|
||||
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;">${contact.mobile}</div>
|
||||
<div style="flex: 2; min-width: 0; color:#666; font-size:13px;"><div class="text-truncate">${contact.designation}</div></div>
|
||||
|
||||
<div class="d-flex" style="gap:5px;">
|
||||
<button type="button" class="btnEditContact" data-id="${contact.contact_id}" data-info='${JSON.stringify(contact)}' title="Edit Contact Person"
|
||||
style="background:none; border:none; cursor:pointer; color:#1976d2; font-size:18px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
|
||||
onmouseover="this.style.background='#e3f2fd'" onmouseout="this.style.background='none'">✏️</button>
|
||||
<button type="button" class="btnRemoveContact" data-id="${contact.contact_id}" title="Remove Contact Person"
|
||||
style="background:none; border:none; cursor:pointer; color:#1976d2; font-size:18px; flex-shrink:0; padding:4px 6px; border-radius:6px; transition:background 0.2s;"
|
||||
onmouseover="this.style.background='#ffebee'" onmouseout="this.style.background='none'">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.append(contactHtml);
|
||||
});
|
||||
} else {
|
||||
$('#NoData').show();
|
||||
}
|
||||
else {
|
||||
$('#noContactPersonsData').show();
|
||||
}
|
||||
|
||||
// 5. Open the Modal
|
||||
@ -1797,6 +1768,125 @@ async function openEditLeadModal(id) {
|
||||
}
|
||||
}
|
||||
|
||||
// --- 1. HANDLE EDIT & REMOVE (Event Delegation) ---
|
||||
document.getElementById('contactPersonsList').onclick = async (e) => {
|
||||
const btnEdit = e.target.closest('.btnEditContact');
|
||||
const btnRemove = e.target.closest('.btnRemoveContact');
|
||||
|
||||
// EDIT LOGIC
|
||||
if (btnEdit) {
|
||||
const card = btnEdit.closest('.contact-card');
|
||||
const contactData = JSON.parse(btnEdit.dataset.info);
|
||||
|
||||
// Fill Form
|
||||
document.getElementById('editing_contact_id').value = contactData.contact_id;
|
||||
document.getElementById('contact_name').value = contactData.name;
|
||||
document.getElementById('contact_mobile').value = contactData.mobile;
|
||||
document.getElementById('contact_designation').value = contactData.designation;
|
||||
document.getElementById('contact_is_primary').checked = contactData.is_primary == 1;
|
||||
|
||||
// Change Button UI
|
||||
const saveBtn = document.getElementById('btnSaveContact');
|
||||
saveBtn.innerHTML = "Update";
|
||||
|
||||
document.getElementById('contact_name').focus();
|
||||
}
|
||||
|
||||
// REMOVE LOGIC
|
||||
if (btnRemove) {
|
||||
const contactId = btnRemove.dataset.id;
|
||||
if (!confirm('Are you sure you want to remove this contact?')) return;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
toastr.success('Removed successfully');
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
};
|
||||
|
||||
// --- 2. SAVE / UPDATE LOGIC ---
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
btnSaveContact.onclick = async (e) => {
|
||||
const editingId = document.getElementById('editing_contact_id').value;
|
||||
const leadId = document.getElementById('hidden_lead_id').value;
|
||||
|
||||
let payload = {
|
||||
lead_id: leadId,
|
||||
name: document.getElementById('contact_name').value.trim(),
|
||||
mobile: document.getElementById('contact_mobile').value.trim(),
|
||||
designation: document.getElementById('contact_designation').value.trim(),
|
||||
is_primary: document.getElementById('contact_is_primary').checked ? 1 : 0
|
||||
};
|
||||
|
||||
if (!payload.name || !payload.mobile) {
|
||||
return toastr.warning('Name and Mobile are required');
|
||||
}
|
||||
|
||||
// Determine if we POST (new) or PUT (edit)
|
||||
const url = editingId ? `${API}/contacts/${editingId}` : `${API}/contacts`;
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
toastr.success(editingId ? 'Contact Updated' : 'Contact Saved');
|
||||
|
||||
// Reset Form UI
|
||||
document.getElementById('editing_contact_id').value = '';
|
||||
document.getElementById('contact_name').value = '';
|
||||
document.getElementById('contact_mobile').value = '';
|
||||
document.getElementById('contact_designation').value = '';
|
||||
document.getElementById('contact_is_primary').checked = false;
|
||||
|
||||
const saveBtn = document.getElementById('btnSaveContact');
|
||||
saveBtn.innerHTML = "✔ Save";
|
||||
// Refresh List
|
||||
openEditLeadModal(leadId);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
toastr.error(err.message || 'Error processing request');
|
||||
}
|
||||
} catch (error) { console.error(error); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
const statusOrder = ['New', 'Potential', 'Prospects', 'Not a Prospects'];
|
||||
|
||||
function updateStatusOptions(currentStatus) {
|
||||
|
||||
let Eform = document.getElementById('editLeadForm');
|
||||
if (!Eform) return; // safety check
|
||||
|
||||
|
||||
let select = Eform.querySelector('[name="status"]');
|
||||
let currentIndex = statusOrder.indexOf(currentStatus);
|
||||
|
||||
// First show all
|
||||
select.querySelectorAll('option').forEach(opt => {
|
||||
opt.hidden = false;
|
||||
});
|
||||
|
||||
// Hide previous ones
|
||||
if (currentIndex > -1) {
|
||||
statusOrder.slice(0, currentIndex).forEach(status => {
|
||||
const opt = select.querySelector(`option[value="${status}"]`);
|
||||
if (opt) opt.hidden = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
function convertDBFormatted(input) {
|
||||
|
||||
if (!input) return null;
|
||||
|
||||
@ -46,6 +46,17 @@
|
||||
|
||||
<div class="col-auto d-flex align-items-center">
|
||||
|
||||
|
||||
<?php if (
|
||||
$ticket_data['is_tpa_api_service_enabled'] == true &&
|
||||
empty($ticket_data['tpa_claim_push_reference_no']) &&
|
||||
empty($ticket_data['claim_number'])
|
||||
) : ?>
|
||||
<a href="#" class="btn btn-success mr-2" onclick="manualTpaClaimPush(); return false;">
|
||||
Manual TPA Claim Push
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if(isset($ticket_data['tpa_claim_push_reference_no']) && !empty($ticket_data['tpa_claim_push_reference_no'])) : ?>
|
||||
<a href="#" class="btn btn-success mr-2" onclick="fetchTpaClaimStatus()">
|
||||
Fetch Claim Status
|
||||
@ -385,6 +396,57 @@
|
||||
});
|
||||
}
|
||||
|
||||
function manualTpaClaimPush() {
|
||||
let ticket_master_id = $('#ticket_master_id').val();
|
||||
if (!ticket_master_id) {
|
||||
toastr.warning('Claim ID not found. Please ensure the form is loaded.', 'Warning');
|
||||
return;
|
||||
}
|
||||
|
||||
Swal.fire({
|
||||
title: 'Manual TPA Claim Push',
|
||||
text: 'Do you want to push this claim to the TPA now?',
|
||||
icon: 'question',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#00999E',
|
||||
cancelButtonColor: '#6c757d',
|
||||
confirmButtonText: 'Yes, push claim',
|
||||
cancelButtonText: 'Cancel'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
$('.loader').fadeIn();
|
||||
$('.loader-mask').fadeIn();
|
||||
|
||||
let requestData = { claim_id: ticket_master_id };
|
||||
let url = '<?= base_url('ticket/manualTpaClaimPush') ?>';
|
||||
|
||||
sendAjaxRequestForGlobal(url, 'POST', requestData, function(response) {
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
console.log('Manual TPA claim push response:', response);
|
||||
if (response.status) {
|
||||
toastr.success(response.message || 'Claim pushed to TPA successfully', 'Success');
|
||||
window.location.reload(true);
|
||||
} else {
|
||||
toastr.warning(response.message || 'Claim push failed', 'Warning');
|
||||
}
|
||||
}, function(xhr, status, error) {
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
console.error('Error pushing claim:', error);
|
||||
console.error(xhr.responseText);
|
||||
let msg = 'An error occurred while pushing the claim to TPA.';
|
||||
if (xhr.responseJSON && xhr.responseJSON.message) {
|
||||
msg = xhr.responseJSON.message;
|
||||
} else if (xhr.status === 404) {
|
||||
msg = 'Manual TPA Claim Push endpoint is not configured. Please contact support.';
|
||||
}
|
||||
toastr.error(msg, 'Error');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function fetchTpaClaimStatus(){
|
||||
|
||||
let ticket_master_id = $('#ticket_master_id').val();
|
||||
|
||||
BIN
public/assets/images/expense_icon.png
Normal file
BIN
public/assets/images/expense_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
399
tests/unit/ExpenseControllerTest.php
Normal file
399
tests/unit/ExpenseControllerTest.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
278
tests/unit/PolicyTransactionControllerTest.php
Normal file
278
tests/unit/PolicyTransactionControllerTest.php
Normal file
@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\unit;
|
||||
|
||||
use CodeIgniter\Test\CIUnitTestCase;
|
||||
use App\Controllers\PolicyTransactionController;
|
||||
use App\Models\PolicyTransactionModel;
|
||||
|
||||
class PolicyTransactionControllerTest extends CIUnitTestCase
|
||||
{
|
||||
/**
|
||||
* Helper to build a fully initialised controller instance.
|
||||
*/
|
||||
protected function makeController(): PolicyTransactionController
|
||||
{
|
||||
$controller = new PolicyTransactionController();
|
||||
|
||||
$request = \Config\Services::request();
|
||||
$response = \Config\Services::response();
|
||||
$logger = \Config\Services::logger();
|
||||
|
||||
$controller->initController($request, $response, $logger);
|
||||
|
||||
return $controller;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to inject a stubbed PolicyTransactionModel into the controller.
|
||||
*
|
||||
* @param PolicyTransactionController $controller
|
||||
* @param array $stubData
|
||||
*/
|
||||
protected function injectBdsStubModel(PolicyTransactionController $controller, array $stubData): void
|
||||
{
|
||||
$stubModel = new class($stubData)
|
||||
{
|
||||
private array $data;
|
||||
|
||||
public function __construct(array $data)
|
||||
{
|
||||
$this->data = $data;
|
||||
}
|
||||
|
||||
public function getBDSReportList(
|
||||
$start_date = 0,
|
||||
$end_date = 0,
|
||||
$client_id = 0,
|
||||
$insurer_id = 0,
|
||||
$policy_type_id = 0,
|
||||
$date_type = 0,
|
||||
$issuer = 0,
|
||||
$client_branch_id = 0,
|
||||
$insurer_branch_id = 0,
|
||||
$client_policy_id = 0,
|
||||
$user_id = 0,
|
||||
$where = [],
|
||||
) {
|
||||
return $this->data;
|
||||
}
|
||||
};
|
||||
|
||||
$refClass = new \ReflectionClass($controller);
|
||||
$prop = $refClass->getProperty('policyTransactionModel');
|
||||
$prop->setAccessible(true);
|
||||
$prop->setValue($controller, $stubModel);
|
||||
}
|
||||
|
||||
public function testCronDailyBDSReportReturnsUnauthorizedWithoutKey(): void
|
||||
{
|
||||
putenv('cron.secretKey=unit-test-secret');
|
||||
|
||||
$_GET = [];
|
||||
|
||||
$controller = $this->makeController();
|
||||
|
||||
$response = $controller->cronDailyBDSReport();
|
||||
|
||||
$this->assertSame(401, $response->getStatusCode());
|
||||
|
||||
$body = json_decode($response->getBody(), true);
|
||||
$this->assertIsArray($body);
|
||||
$this->assertSame('error', $body['status'] ?? null);
|
||||
}
|
||||
|
||||
public function testCronDailyBDSReportNoRecordsReturnsSuccessMessage(): void
|
||||
{
|
||||
putenv('cron.secretKey=unit-test-secret');
|
||||
putenv('bds.reportEmails='); // no recipients for this test
|
||||
|
||||
$_GET['key'] = 'unit-test-secret';
|
||||
|
||||
$controller = $this->makeController();
|
||||
|
||||
$this->injectBdsStubModel($controller, []);
|
||||
|
||||
$response = $controller->cronDailyBDSReport();
|
||||
|
||||
$this->assertSame(200, $response->getStatusCode());
|
||||
|
||||
$body = json_decode($response->getBody(), true);
|
||||
$this->assertIsArray($body);
|
||||
$this->assertSame('success', $body['status'] ?? null);
|
||||
$this->assertStringContainsString(
|
||||
'No BDS records for today',
|
||||
$body['message'] ?? ''
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration-style test: generate an Excel file for today's BDS data
|
||||
* and store it in the user's Downloads folder as BDS_DAILY_REPORT_TEST_CASE.xlsx.
|
||||
*
|
||||
* This reuses the same transformation logic as cronDailyBDSReport.
|
||||
*/
|
||||
public function testGenerateDailyBdsReportExcelToDownloads(): void
|
||||
{
|
||||
helper(['excel_import_export_helper', 'utility_helper']);
|
||||
|
||||
// Sample BDS row to generate a test Excel (no DB dependency)
|
||||
$today = date('Y-m-d');
|
||||
$reportList = [
|
||||
[
|
||||
'user_name' => 'Test User',
|
||||
'policy_issue_month' => 'Mar 2026',
|
||||
'revenue_type' => 'Fresh',
|
||||
'client_type' => 'Group',
|
||||
'client_name' => 'Test Client',
|
||||
'action_type' => 'Policy',
|
||||
'policy_type' => 'Health',
|
||||
'bap' => 'BAP-GRP',
|
||||
'vehicle_no' => 'TN01AB1234',
|
||||
'policy_no' => 'P-TEST-001',
|
||||
'endorsement_no' => 'E-TEST-001',
|
||||
'insurer_branch_name' => 'Chennai Branch',
|
||||
'endorse_eff_date' => $today,
|
||||
'policy_start_date' => $today,
|
||||
'policy_end_date' => $today,
|
||||
'ref' => 'REF-001',
|
||||
'remarks' => 'Test remark',
|
||||
'bp_amt' => 1000,
|
||||
'tp_or_ter' => 500,
|
||||
'premium_wo_gst' => 1500,
|
||||
'total_premium' => 1770,
|
||||
'agreed_bp_per' => 10,
|
||||
'agreed_tp_or_ter_per' => 5,
|
||||
'reward' => 100,
|
||||
'total_irda_amt' => 800,
|
||||
'billed_amt' => 300,
|
||||
'salse_person_name' => 'Sales Person',
|
||||
'service_person_name' => 'Service Person',
|
||||
'nhance_branch' => 'Nhance Chennai',
|
||||
'installment' => '1',
|
||||
'data_received_date' => $today,
|
||||
'renewal_date' => $today,
|
||||
'co_share' => 'No',
|
||||
'bro_payable_by' => 'No',
|
||||
'salse_manager_name' => 'Sales Manager',
|
||||
'service_manager_name' => 'Service Manager',
|
||||
'service_branch' => 'Service Branch',
|
||||
'rollover_date' => $today,
|
||||
'policy_holder_name' => 'Holder Name',
|
||||
'same_as_proposer' => 'Yes',
|
||||
'follower_policy_no' => 'F-TEST-001',
|
||||
'co_share_per' => 0,
|
||||
'non_comm_per_amt' => 0,
|
||||
'bp_cgst' => 0,
|
||||
'bp_sgst' => 0,
|
||||
'bp_igst' => 0,
|
||||
'stamp_duty' => 0,
|
||||
'standerd_bp_per' => 0,
|
||||
'standerd_tp_per' => 0,
|
||||
'actual_bp_amt' => 0,
|
||||
'actual_tp_amt' => 0,
|
||||
'actual_bp_per' => 0,
|
||||
'actual_tp_per' => 0,
|
||||
'actual_tep_brokerage_amt' => 0,
|
||||
'actual_tp_brokerage_amt' => 0,
|
||||
'cd_ac_no' => 'CD-001',
|
||||
],
|
||||
];
|
||||
|
||||
$headers = [
|
||||
'S. No', 'User', 'Month', 'Business Type', 'Client Type', 'Insured Name', 'Transaction Type',
|
||||
'Policy Type', 'BAP Group', 'Vehicle Number', 'Policy No', 'Endorsement No', 'Insurer Branch',
|
||||
'Endorsement Effective Date', 'Policy Effective Date', 'Policy Expiry Date', 'Reference', 'Remarks',
|
||||
'BP Premium', 'TP Premium', 'Premium (without GST)', 'Total Premium', 'Agreed BP %', 'Agreed TP %',
|
||||
'Rewards', 'Agreed Amount', 'Invoiced Amount', 'Outstanding Amount',
|
||||
'Salse Person', 'Service Person', 'Salse Person Branch', 'Installment', 'Data Received Date', 'Renewal Date',
|
||||
'Co-Premium', 'Remuneration Pay By Leader', 'Salse Person Manager', 'Service Person Manager',
|
||||
'Service Person Branch', 'Rollover Date', 'Policyholder Name', 'Insured (Same as Proposer)',
|
||||
'Follower Policy No', 'Co-Share %', 'Non Commissional Premium Amount', 'CGST', 'SGST', 'IGST',
|
||||
'Stamp Duty', 'Standard BP %', 'Standard TP %', 'Actual BP Amount', 'Actual TP Amount',
|
||||
'Actual BP %', 'Actual TP %', 'Actual BP Remuneration Amount', 'Actual TP Remuneration Amount',
|
||||
'CD Account No'
|
||||
];
|
||||
|
||||
$excelData = [];
|
||||
foreach ($reportList as $idx => $row) {
|
||||
$totalIrda = (float)($row['total_irda_amt'] ?? 0);
|
||||
$billedAmt = (float)($row['billed_amt'] ?? 0);
|
||||
$unbilledAmt = $totalIrda - $billedAmt;
|
||||
if ($totalIrda == 0) {
|
||||
$unbilledAmt = abs($unbilledAmt);
|
||||
}
|
||||
$unbilledAmt = ($unbilledAmt == 0 && $billedAmt == 0) ? $totalIrda : $unbilledAmt;
|
||||
|
||||
$hasIrda = ($totalIrda != 0);
|
||||
|
||||
$excelData[] = [
|
||||
$idx + 1,
|
||||
$row['user_name'] ?? 'N/A',
|
||||
$row['policy_issue_month'] ?? 'N/A',
|
||||
$row['revenue_type'] ?? 'N/A',
|
||||
$row['client_type'] ?? 'N/A',
|
||||
$row['client_name'] ?? 'N/A',
|
||||
$row['action_type'] ?? 'N/A',
|
||||
$row['policy_type'] ?? 'N/A',
|
||||
$row['bap'] ?? 'N/A',
|
||||
$row['vehicle_no'] ?? 'N/A',
|
||||
$row['policy_no'] ?? 'N/A',
|
||||
$row['endorsement_no'] ?? 'N/A',
|
||||
$row['insurer_branch_name'] ?? 'N/A',
|
||||
!empty($row['endorse_eff_date']) ? change_date_format($row['endorse_eff_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['policy_start_date']) ? change_date_format($row['policy_start_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['policy_end_date']) ? change_date_format($row['policy_end_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['ref'] ?? 'N/A',
|
||||
$row['remarks'] ?? 'N/A',
|
||||
$hasIrda ? ($row['bp_amt'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['tp_or_ter'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['premium_wo_gst'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['total_premium'] ?? '0.00') : '0.00',
|
||||
$hasIrda ? ($row['agreed_bp_per'] ?? '0.00') . '%' : '0.00%',
|
||||
$hasIrda ? ($row['agreed_tp_or_ter_per'] ?? '0.00') . '%' : '0.00%',
|
||||
$row['reward'] ?? '0.00',
|
||||
$row['total_irda_amt'] ?? '0.00',
|
||||
!empty($row['billed_amt']) ? $row['billed_amt'] : '0.00',
|
||||
number_format((float)$unbilledAmt, 2, '.', ''),
|
||||
$row['salse_person_name'] ?? 'N/A',
|
||||
$row['service_person_name'] ?? 'N/A',
|
||||
$row['nhance_branch'] ?? 'N/A',
|
||||
$row['installment'] ?? 'N/A',
|
||||
!empty($row['data_received_date']) ? change_date_format($row['data_received_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
!empty($row['renewal_date']) ? change_date_format($row['renewal_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['co_share'] ?? 'No',
|
||||
$row['bro_payable_by'] ?? 'No',
|
||||
$row['salse_manager_name'] ?? 'N/A',
|
||||
$row['service_manager_name'] ?? 'N/A',
|
||||
$row['service_branch'] ?? 'N/A',
|
||||
!empty($row['rollover_date']) ? change_date_format($row['rollover_date'], 'Y-m-d', 'd/m/Y') : 'N/A',
|
||||
$row['policy_holder_name'] ?? 'N/A',
|
||||
$row['same_as_proposer'] ?? 'No',
|
||||
$row['follower_policy_no'] ?? 'N/A',
|
||||
number_format((float)($row['co_share_per'] ?? 0), 2),
|
||||
number_format((float)($row['non_comm_per_amt'] ?? 0), 2),
|
||||
number_format((float)($row['bp_cgst'] ?? 0), 2),
|
||||
number_format((float)($row['bp_sgst'] ?? 0), 2),
|
||||
number_format((float)($row['bp_igst'] ?? 0), 2),
|
||||
number_format((float)($row['stamp_duty'] ?? 0), 2),
|
||||
number_format((float)($row['standerd_bp_per'] ?? 0), 2),
|
||||
number_format((float)($row['standerd_tp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_bp_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_bp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_per'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tep_brokerage_amt'] ?? 0), 2),
|
||||
number_format((float)($row['actual_tp_brokerage_amt'] ?? 0), 2),
|
||||
$row['cd_ac_no'] ?? 'N/A'
|
||||
];
|
||||
}
|
||||
|
||||
$filePath = '/home/venkat/Downloads/BDS_DAILY_REPORT_TEST_CASE.xlsx';
|
||||
|
||||
$generated = generate_excel($headers, $excelData, $filePath);
|
||||
|
||||
$this->assertTrue($generated, 'Failed to generate BDS_DAILY_REPORT_TEST_CASE.xlsx in Downloads');
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user