MERGE_TEST_NONEB_FLOW&OTRS

This commit is contained in:
Ubuntu 2026-03-06 11:27:50 +05:30
commit 2277546932
40 changed files with 8265 additions and 2437 deletions

View File

@ -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 =

51
app/Config/RfqConfig.php Normal file
View 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' => [],
],
];
}

View File

@ -446,11 +446,15 @@ $routes->group("/util", ["filter" => "authMVC"], function ($routes) {
$routes->get('generateDemographyDataTable', 'LeadsController::generateDemographyDataTable');
$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) {
@ -498,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) {
@ -505,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");
@ -518,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");
});
@ -547,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');
@ -766,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) {

View File

@ -476,6 +476,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()

View File

@ -6975,7 +6975,9 @@ class ClientController extends AdminController
// $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->tpaClaimDumpImporter(["file_id" => 48]); //vidal`
// $response = $ticketServiceController->tpaClaimDumpImporter(["file_id" => 57]); //mediassist
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 51]); //abhi
// $response = $ticketServiceController->tpaClaimDumpToTicketMasterImporters(["file_id" => 50]); //fhpl

View File

@ -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);
}
}

View File

@ -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');

View File

@ -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'];

View File

@ -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

View File

@ -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];
}
}

View File

@ -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

View File

@ -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([

View File

@ -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',

View File

@ -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')

View File

@ -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)
{

View File

@ -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;
}
}

View File

@ -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();

View File

@ -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)) {

View File

@ -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;
}
}

View File

@ -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' ";

View File

@ -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',
];
}

View File

@ -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')
@ -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
*/

View File

@ -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;
}
/**

View File

@ -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
}
// -----------------------------------------------------------------------------------------------------
}

View 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 &amp; 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 noninception 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 &amp; 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 &amp; 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 &amp; 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 &amp; 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>

View File

@ -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');

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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')">&times;</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')">&times;</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')">&times;</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')">&times;</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

View 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>

View File

@ -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>

View File

@ -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
}));

View File

@ -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')">&times;</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')">&times;</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')">&times;</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')">&times;</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')">&times;</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')">&times;</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')">&times;</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;

View File

@ -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();

View 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');
}
}