Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
ced690d86b
@ -43,6 +43,7 @@ class Acl
|
||||
'#^/expense#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, STAFF_ROLE_ID]],
|
||||
'#^/sendDataToTPA#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
|
||||
'#^/swagger#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID]],
|
||||
'#^/viewClaimFile#' => ['roles' => [ADMIN_ROLE_ID, HEAD_ROLE_ID, ACCOUNT_MANAGER_ROLE_ID, MANAGER_ROLE_ID, STAFF_ROLE_ID]],
|
||||
|
||||
|
||||
|
||||
|
||||
@ -508,6 +508,7 @@ $routes->group("policy_tranction", ["filter" => "authMVC"], function ($routes) {
|
||||
$routes->post("saveInvoicePaymentDetails", "PolicyTransactionController::saveInvoicePaymentDetails");
|
||||
$routes->get("deletePaymentEntry/(:any)", "PolicyTransactionController::deletePaymentEntry/$1");
|
||||
$routes->get("downloadSampleInsurerStatement", "PolicyTransactionController::downloadSampleInsurerStatement");
|
||||
$routes->get("downloadInsurerStatement/(:num)", "PolicyTransactionController::downloadInsurerStatement/$1");
|
||||
$routes->get("getFileErr/(:any)", "PolicyTransactionController::getFileErr/$1");
|
||||
$routes->get("getInsurerStatementMonth", "PolicyTransactionController::getInsurerStatementMonth");
|
||||
$routes->get("deleteStatement/(:any)", "PolicyTransactionController::deleteStatement/$1");
|
||||
@ -671,6 +672,9 @@ $routes->group("employeeRest", ['filter' => ['ratelimit' , 'appSignature'] ], fu
|
||||
$routes->post("getVerifiedRetailUserData", "RestAuthenticationController::getVerifiedRetailUserData");
|
||||
$routes->post("updateRetailUserAuthDetails", "RestAuthenticationController::updateRetailUserAuthDetails");
|
||||
|
||||
//samulss oauth login api's
|
||||
$routes->get("getTokenforSamulssOAuthLogin", "RestAuthenticationController::getTokenforSamulssOAuthLogin");
|
||||
|
||||
});
|
||||
|
||||
$routes->group("employeeRest", ["filter" => ['GlobalPostFileUploadGuard', 'ratelimit' , 'appSignature', 'authJWT']], function ($routes) {
|
||||
@ -1009,6 +1013,9 @@ $routes->group('sales', function($routes) {
|
||||
|
||||
// Get all leads with filters
|
||||
$routes->get('leads', 'SalesController::getLeads');
|
||||
|
||||
// Export leads by created date range
|
||||
$routes->get('leads/export', 'SalesController::exportLeads');
|
||||
|
||||
// Get lead statistics
|
||||
$routes->get('leads/stats', 'SalesController::getLeadStats');
|
||||
@ -1055,6 +1062,9 @@ $routes->group('sales', function($routes) {
|
||||
|
||||
// Get all activities with filters
|
||||
$routes->get('activities', 'SalesController::getActivities');
|
||||
|
||||
// Export activities by created date range
|
||||
$routes->get('activities/export', 'SalesController::exportActivities');
|
||||
|
||||
// Get upcoming activities
|
||||
$routes->get('activities/upcoming', 'SalesController::getUpcomingActivities');
|
||||
@ -1118,6 +1128,6 @@ $routes->group('expense', ["filter" => "authMVC", 'namespace' => 'App\Controller
|
||||
$routes->get('client-policies', 'ExpenseController::clientPolicies');
|
||||
});
|
||||
|
||||
|
||||
$routes->get('docs', 'Docs\DocsController::index');
|
||||
$routes->get('docs/(:segment)', 'Docs\DocsController::page/$1');
|
||||
$routes->get('docs/(:segment)', 'Docs\DocsController::page/$1');
|
||||
|
||||
|
||||
@ -692,7 +692,8 @@ class EmployeeController extends AdminController
|
||||
return $this->response->download($filePath, null, $mimeType);
|
||||
} else {
|
||||
// File not found, show an error message or redirect
|
||||
echo view('errors/html/production');
|
||||
echo view('errors/404.php', ['message' => 'Sample file not found']);
|
||||
// return redirect()->back()->with('error', 'Sample file not found');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -227,7 +227,7 @@ class EmployeeMultiEventServiceController extends BaseController
|
||||
'format' => 'mobile',
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_dup_mobileno',
|
||||
'params' => ['row', 'existing_mobilenos']
|
||||
'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key']
|
||||
],
|
||||
'email' => [
|
||||
'col_idx' => 13,
|
||||
@ -238,7 +238,7 @@ class EmployeeMultiEventServiceController extends BaseController
|
||||
'format' => null,
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_dup_email',
|
||||
'params' => ['row', 'existing_mobilenos']
|
||||
'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key']
|
||||
],
|
||||
'pre_existing_ailments' => [
|
||||
'col_idx' => 14,
|
||||
|
||||
@ -230,7 +230,7 @@ class EmployeeServiceController extends AdminController
|
||||
'format' => 'mobile',
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_dup_mobileno',
|
||||
'params' => ['row', 'existing_mobilenos']
|
||||
'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key']
|
||||
],
|
||||
'email' => [
|
||||
'col_idx' => 13,
|
||||
@ -241,7 +241,7 @@ class EmployeeServiceController extends AdminController
|
||||
'format' => null,
|
||||
'allowed_values' => null,
|
||||
'custom' => 'check_dup_email',
|
||||
'params' => ['row', 'existing_mobilenos']
|
||||
'params' => ['row', 'existing_mobilenos', 'excel_data', 'row_key']
|
||||
],
|
||||
'pre_existing_ailments' => [
|
||||
'col_idx' => 14,
|
||||
|
||||
@ -503,12 +503,19 @@ class FhplApiController extends BaseController
|
||||
log_message('error', "Failed to update file table status.");
|
||||
}
|
||||
|
||||
log_message('error', 'FHPL - TPA ID Pull API FAILED | API failed: Empty menber data for this pull request');
|
||||
log_message('error', 'FHPL - TPA ID Pull API FAILED | API failed: Empty member data for this pull request');
|
||||
|
||||
$file_model = new BatchFileModel();
|
||||
$bfData = [
|
||||
'error_data' => json_encode(['error_data' => 'API failed: Empty member data for this pull request.']),
|
||||
'status' => 'failed-7',
|
||||
];
|
||||
$file_model->where('id', $requestData['file_id'])->set($bfData)->update();
|
||||
|
||||
if($function_calling_type == "job"){
|
||||
return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request'];
|
||||
return ['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request'];
|
||||
}else{
|
||||
return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty menber data for this pull request']);
|
||||
return $this->respond(['status' => false, 'message' => 'API call failed', 'data' => 'Empty member data for this pull request']);
|
||||
}
|
||||
|
||||
}
|
||||
@ -1068,7 +1075,7 @@ class FhplApiController extends BaseController
|
||||
|
||||
'is_active' => 1,
|
||||
'created_by' => $file_info[0]['created_by'] ?? null,
|
||||
'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D'
|
||||
'action_flag_status' => $row['IsActive'] == 1 ? 'A' : 'D',
|
||||
'si' => $row['BASE_SUMINSURED'],
|
||||
'doj' => change_date_format($row['DATE_OF_JOINING'],'Y-m-d\TH:i:s'),
|
||||
'endorsement_no' => $row['ENDORSEMENT_NO']
|
||||
|
||||
@ -571,6 +571,72 @@ class LeadsController extends BaseController
|
||||
|
||||
}
|
||||
|
||||
$leadFormType = (int) ($postData['lead_form_type'] ?? 1);
|
||||
$postedPolicyTypeIds = array_map('intval', (array) ($postData['policy_type_id'] ?? []));
|
||||
$claimHistoryPolicyTypes = [1, 6, 7];
|
||||
$hasClaimHistoryPolicy = count(array_intersect($postedPolicyTypeIds, $claimHistoryPolicyTypes)) > 0;
|
||||
$isEbClaimHistory = $leadFormType === 1
|
||||
&& in_array((int) ($postData['lead_type'] ?? 0), [2, 3], true)
|
||||
&& $hasClaimHistoryPolicy
|
||||
&& (int) ($postData['claim_history'] ?? 0) === 1;
|
||||
|
||||
if ($isEbClaimHistory) {
|
||||
$rules['first_year.*'] = [
|
||||
'rules' => 'required|regex_match[/^\d{4}-\d{4}$/]',
|
||||
'errors' => [
|
||||
'required' => 'Claim Year is required for all entries.',
|
||||
'regex_match' => 'Year must be in format YYYY-YYYY.',
|
||||
],
|
||||
];
|
||||
$rules['emp_id.*'] = [
|
||||
'rules' => 'required',
|
||||
'errors' => ['required' => 'Employee ID is required in Claim History.'],
|
||||
];
|
||||
$rules['emp_name.*'] = [
|
||||
'rules' => 'required',
|
||||
'errors' => ['required' => 'Employee Name is required in Claim History.'],
|
||||
];
|
||||
$rules['gender.*'] = [
|
||||
'rules' => 'required|in_list[Female,Male]',
|
||||
'errors' => [
|
||||
'required' => 'Gender is required in Claim History.',
|
||||
'in_list' => 'Gender must be Female or Male.',
|
||||
],
|
||||
];
|
||||
$rules['designation.*'] = [
|
||||
'rules' => 'required',
|
||||
'errors' => ['required' => 'Designation is required in Claim History.'],
|
||||
];
|
||||
$rules['sum_insured.*'] = [
|
||||
'rules' => 'required|numeric',
|
||||
'errors' => [
|
||||
'required' => 'Sum Insured is required in Claim History.',
|
||||
'numeric' => 'Sum Insured must be a number.',
|
||||
],
|
||||
];
|
||||
$rules['first_death_date.*'] = [
|
||||
'rules' => 'required|regex_match[/^[0-9]{2}-([0-9]{2}|[A-Za-z]{3})-[0-9]{4}$/]',
|
||||
'errors' => [
|
||||
'required' => 'Date of Death is required.',
|
||||
'regex_match' => 'Date of Death must be in DD-MM-YYYY or DD-MMM-YYYY format.',
|
||||
],
|
||||
];
|
||||
$rules['first_cause_of_death.*'] = [
|
||||
'rules' => 'required|in_list[natural_death,suicide,accident,cardiac_arrest,septic_shock,heart_attack]',
|
||||
'errors' => [
|
||||
'required' => 'Nature/Cause Of Death is required.',
|
||||
'in_list' => 'Nature/Cause Of Death is invalid.',
|
||||
],
|
||||
];
|
||||
$rules['first_claim_amount.*'] = [
|
||||
'rules' => 'required|numeric',
|
||||
'errors' => [
|
||||
'required' => 'Claim/Settled Amount is required.',
|
||||
'numeric' => 'Claim/Settled Amount must be a number.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
// 1. MANUALLY VALIDATE FILES BEFORE PROCESSING
|
||||
// $allFiles = $this->request->getFiles();
|
||||
// foreach ($allFiles as $inputName => $files) {
|
||||
@ -680,7 +746,7 @@ class LeadsController extends BaseController
|
||||
$data['client_code'] = generate_client_code('IC');
|
||||
}
|
||||
|
||||
if (isset($data['lead_form_type'])) {
|
||||
if ((int) ($data['lead_form_type'] ?? 1) === 2) {
|
||||
$data = $this->prepareSingleLeadData($data);
|
||||
} else {
|
||||
$data = $this->prepareMultipleLeadData($data);
|
||||
@ -950,10 +1016,10 @@ class LeadsController extends BaseController
|
||||
$insertCount[] = $insert;
|
||||
$this->insertLeadStatus($insert, $value['status'], 3);
|
||||
|
||||
if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') {
|
||||
$leadDataForClient = $this->leadsModel->find($insert);
|
||||
$this->createClientWithLeadData($leadDataForClient);
|
||||
}
|
||||
// if (in_array((int) $value['lead_type'], [1, 3], true) && $value['status'] == 'won') {
|
||||
// $leadDataForClient = $this->leadsModel->find($insert);
|
||||
// $this->createClientWithLeadData($leadDataForClient);
|
||||
// }
|
||||
|
||||
if ($value['lead_form_type'] == 1) {
|
||||
//for this push the job to the calculateMembersDemography() function
|
||||
@ -981,12 +1047,12 @@ class LeadsController extends BaseController
|
||||
$this->insertMultiFilesData($data[0]['multi_file_data'], $id, $data[0]['lead_form_type']);
|
||||
$this->insertLeadStatus($id, $data[0]['status'], 3);
|
||||
|
||||
if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') {
|
||||
$leadDataForClient = $this->leadsModel->find($id);
|
||||
if (empty($leadDataForClient['is_client_created'])) {
|
||||
$this->createClientWithLeadData($leadDataForClient);
|
||||
}
|
||||
}
|
||||
// if (in_array((int) $data[0]['lead_type'], [1, 3], true) && $data[0]['status'] == 'won') {
|
||||
// $leadDataForClient = $this->leadsModel->find($id);
|
||||
// if (empty($leadDataForClient['is_client_created'])) {
|
||||
// $this->createClientWithLeadData($leadDataForClient);
|
||||
// }
|
||||
// }
|
||||
|
||||
return $this->respond(['status' => true, 'lead_id' => $id, 'message' => "Opportunity updated successfully", 'data' => $data], 200);
|
||||
}
|
||||
@ -2140,6 +2206,8 @@ class LeadsController extends BaseController
|
||||
{
|
||||
helper('excel_util_helper');
|
||||
$rfq_data = $this->RFQModel->getRFQTableDataWithLeadIDAndType($lead_id, $type);
|
||||
$claim_history = $rfq_data['claim_history'] ?? 0;
|
||||
|
||||
$is_placement = false;
|
||||
$length = 0;
|
||||
|
||||
@ -2176,7 +2244,7 @@ class LeadsController extends BaseController
|
||||
'No of Dependents' => $rfq_data['incept_dept_count'],
|
||||
'Total Lives' => $rfq_data['incept_no_of_lives'],
|
||||
|
||||
'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided",
|
||||
// 'Period of Insurance ' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided",
|
||||
// 'Insurer' => $rfq_data['insurer_name'] ?? " - ",
|
||||
// 'TPA' => $rfq_data['tpa_name'] ?? " - ",
|
||||
// 'Policy Run Days' => $rfq_data['policy_run_days'],
|
||||
@ -2186,7 +2254,7 @@ class LeadsController extends BaseController
|
||||
'Insured' => $rfq_data['client_name'],
|
||||
'No of Employees at Inception' => $rfq_data['incept_emp_count'],
|
||||
// 'Total Sum Insured at Inception ' => $rfq_data['total_si_at_incept'],
|
||||
'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided",
|
||||
// 'Policy Period' => ! empty($rfq_data['policy_start_date']) && ! empty($rfq_data['policy_end_date']) ? date('d-m-Y', strtotime($rfq_data['policy_start_date'])) . ' to ' . date('d-m-Y', strtotime($rfq_data['policy_end_date'])) : "To be decided",
|
||||
// 'Insurer' => $rfq_data['insurer_name'] ?? " - ",
|
||||
// 'TPA' => $rfq_data['tpa_name'] ?? " - ",
|
||||
'Policy Type' => $this->leadType[$rfq_data['lead_type']] ?? "",
|
||||
@ -2279,7 +2347,7 @@ class LeadsController extends BaseController
|
||||
'Existing Insurer' => $rfq_data['insurer_name'],
|
||||
'No of Employees at Renewal' => $rfq_data['renewal_emp_count'],
|
||||
'Total Sum Insured at Renewal' => $rfq_data['total_si_at_renewal'],
|
||||
'Claims Experience for last 3 years' => "Mentioned in Claims sheet",
|
||||
'Claims Experience for last 3 years' => $claim_history == "1" ? "Mentioned in Claims sheet" : "Nil",
|
||||
// 'Insurer' => $rfq_data['insurer_name'] ?? " - ",
|
||||
// 'TPA' => $rfq_data['tpa_name'] ?? " - ",
|
||||
// 'Total Lives at Inception' => $rfq_data['total_lives_at_incept'],
|
||||
@ -2796,7 +2864,7 @@ class LeadsController extends BaseController
|
||||
}
|
||||
|
||||
//claim history new sheet;
|
||||
if (! empty($rfq_data['fin_years_claims'])) {
|
||||
if ($claim_history == 1 && !empty($rfq_data['fin_years_claims'])) {
|
||||
|
||||
$claim_details = json_decode($rfq_data['fin_years_claims'], true) ?? [];
|
||||
|
||||
@ -4774,7 +4842,18 @@ class LeadsController extends BaseController
|
||||
|
||||
// 🔹 Client Details (Single Row)
|
||||
$data['actual_lead_client_details'] = $this->leadModel
|
||||
->select('sales_actual_leads.company_name, clients.short_name, clients.client_type, sales_actual_leads.email, sales_actual_leads.phone, sales_actual_leads.address, sales_actual_leads.website, sales_actual_leads.gst_number, sales_actual_leads.status, sales_actual_leads.assigned_to')
|
||||
->select("
|
||||
sales_actual_leads.company_name,
|
||||
clients.short_name,
|
||||
clients.client_type,
|
||||
sales_actual_leads.email,
|
||||
sales_actual_leads.phone,
|
||||
sales_actual_leads.address,
|
||||
sales_actual_leads.website,
|
||||
COALESCE(NULLIF(sales_actual_leads.gst_number, ''), clients.gst) AS gst_number,
|
||||
sales_actual_leads.status,
|
||||
sales_actual_leads.assigned_to
|
||||
", false)
|
||||
->join('clients', 'clients.id = sales_actual_leads.client_id', 'left')
|
||||
->where('sales_actual_leads.lead_id', $actual_lead_id)
|
||||
->first(); // first row only
|
||||
|
||||
@ -4236,7 +4236,7 @@ class PolicyTransactionController extends BaseController
|
||||
return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']);
|
||||
}
|
||||
|
||||
public function validateInsurerStatement($params)
|
||||
public function validateInsurerStatementOLD1($params)
|
||||
{
|
||||
helper('excel_util_helper');
|
||||
|
||||
@ -4392,8 +4392,7 @@ class PolicyTransactionController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function updateInsurerStatement($params)
|
||||
public function updateInsurerStatementOLD($params)
|
||||
{
|
||||
helper('excel_util_helper');
|
||||
//get file info
|
||||
@ -4559,6 +4558,375 @@ class PolicyTransactionController extends BaseController
|
||||
return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']);
|
||||
}
|
||||
|
||||
public function validateInsurerStatement($params)
|
||||
{
|
||||
/*
|
||||
* Changes made: 12-06-2024
|
||||
* - Added safe file_id handling and DB file record validation before using file details.
|
||||
* - Fixed physical file missing response and error message.
|
||||
* - Skips empty Excel rows and collects unique policy numbers from the uploaded statement.
|
||||
* - Fetches NHance source records only for those uploaded policy numbers.
|
||||
* - Sanitizes policy and endorsement numbers before comparison to avoid hidden-space mismatch.
|
||||
* - Validates each row by policy number + endorsement number combination.
|
||||
* - Detects duplicate policy + endorsement rows and returns row-wise validation errors.
|
||||
*/
|
||||
helper('excel_util_helper');
|
||||
|
||||
$file_id = $params['file_id'] ?? 0;
|
||||
|
||||
try {
|
||||
|
||||
$error_data = ['error_code' => '', 'error_data' => []];
|
||||
$status = 'success';
|
||||
$ret_status = true;
|
||||
|
||||
//get file info
|
||||
$file = $this->insurerStatements->find((int)$file_id);
|
||||
// dd($file);
|
||||
|
||||
if (empty($file)) {
|
||||
//file not found in DB
|
||||
$this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => 'statement file not found in DB'])])->update();
|
||||
return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB');
|
||||
}
|
||||
$file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name'];
|
||||
|
||||
//check physical file
|
||||
if (!file_exists($file_name_with_path)) {
|
||||
//file not found update status and reason
|
||||
$message = "Physical file not found";
|
||||
// echo $message;
|
||||
$this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id));
|
||||
$this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update();
|
||||
return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found
|
||||
}
|
||||
|
||||
//get excel data to php array
|
||||
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$highestRow = $sheet->getHighestRow();
|
||||
$highestColumn = $sheet->getHighestColumn();
|
||||
|
||||
$excel_data = $sheet->rangeToArray('A1:' . $highestColumn . $highestRow);
|
||||
unset($excel_data[0]);
|
||||
$excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data);
|
||||
// dd($excel_data);
|
||||
|
||||
//get no of line items and update in DB
|
||||
$line_items = 0;
|
||||
|
||||
$policyNos = [];
|
||||
|
||||
foreach ($excel_data as $row) {
|
||||
if (check_row_is_empty_or_null($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// row[1] => second column (B column)
|
||||
$policyNo = $this->sanitizeStatementLookupValue($row[1] ?? '');
|
||||
|
||||
if ($policyNo !== '') {
|
||||
$policyNos[] = $policyNo;
|
||||
}
|
||||
}
|
||||
$policyNos = array_values(array_unique($policyNos));
|
||||
|
||||
// get uploaded month transactions data
|
||||
// $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']);
|
||||
$source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : [];
|
||||
// dd($source_data);
|
||||
|
||||
$source_lookup = [];
|
||||
$source_policy_lookup = [];
|
||||
$source_endorsement_lookup = [];
|
||||
|
||||
foreach ($source_data as $source_row) {
|
||||
$source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? '');
|
||||
$source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? '');
|
||||
$source_entry_key = $source_policy_no . '|' . $source_endorsement_no;
|
||||
|
||||
$source_lookup[$source_entry_key] = true;
|
||||
$source_policy_lookup[$source_policy_no] = true;
|
||||
$source_endorsement_lookup[$source_policy_no][$source_endorsement_no] = true;
|
||||
}
|
||||
|
||||
// check policy no,insurer and etc in DB for this month
|
||||
// if all good return true, otherwise return false with messssage
|
||||
$matched_entry = [];
|
||||
$error_messages = []; // row-wise error storage
|
||||
|
||||
foreach ($excel_data as $excel_key => $excel_row) {
|
||||
|
||||
$row_number = $excel_key;
|
||||
$is_row_empty = check_row_is_empty_or_null($excel_row);
|
||||
$policy_source_found = 0;
|
||||
$endorsement_source_found = 0;
|
||||
$duplicate_found = 0;
|
||||
|
||||
if (!$is_row_empty) {
|
||||
|
||||
$policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel
|
||||
$endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel
|
||||
$entry_key = $policy_no . '|' . $endorsement_no;
|
||||
|
||||
if (isset($matched_entry[$entry_key])) {
|
||||
$duplicate_found = 1;
|
||||
} elseif (isset($source_lookup[$entry_key])) {
|
||||
$line_items = $line_items + 1;
|
||||
$matched_entry[$entry_key] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$policy_source_found = isset($source_policy_lookup[$policy_no]) ? 1 : 0;
|
||||
$endorsement_source_found = isset($source_endorsement_lookup[$policy_no][$endorsement_no]) ? 1 : 0;
|
||||
|
||||
// Policy number mismatch
|
||||
if ($policy_source_found == 0) {
|
||||
$error_messages[$row_number]['policy_no_mismatch'] =
|
||||
"Policy number <strong>({$policy_no}) </strong> not in NHance.";
|
||||
}
|
||||
|
||||
// Endorsement number mismatch
|
||||
if ($policy_source_found == 1 && $endorsement_source_found == 0) {
|
||||
$error_messages[$row_number]['endorsement_no_mismatch'] =
|
||||
"Endorsement number <strong>({$endorsement_no})</strong> not in NHance for policy <strong>({$policy_no})</strong>.";
|
||||
}
|
||||
|
||||
// Duplicate check
|
||||
if ($duplicate_found == 1) {
|
||||
$error_messages[$row_number]['duplicate'] =
|
||||
"Duplicate entry found. This policy and endorsement <strong>({$policy_no} | {$endorsement_no})</strong> combination has already been matched.";
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// print_rr($error_messages);die;
|
||||
|
||||
if (!empty($error_messages)) {
|
||||
$error_data['error_code'] = 2;
|
||||
$error_data['error_data'] = $error_messages;
|
||||
$status = 'failed';
|
||||
$ret_status = false;
|
||||
}
|
||||
|
||||
//update in DB
|
||||
$this->insurerStatements->where('id', $file_id)->set(['line_items' => $line_items, 'file_status' => $status, 'reason' => json_encode($error_data)])->update();
|
||||
return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']);
|
||||
} catch (\Throwable $th) {
|
||||
|
||||
$errorData = [
|
||||
'message' => $th->getMessage(),
|
||||
'file' => $th->getFile(),
|
||||
'line' => $th->getLine(),
|
||||
'code' => $th->getCode(),
|
||||
'trace' => $th->getTraceAsString(),
|
||||
'trace_array' => $th->getTrace(), // full array version (optional)
|
||||
'function' => $th->getTrace()[0]['function'] ?? null,
|
||||
'class' => $th->getTrace()[0]['class'] ?? null,
|
||||
];
|
||||
|
||||
$this->myLogger->logme("error", "POLICY-TRANSACTION-CONTROLLER - validateInsurerStatement: Exception: " . json_encode($errorData ?? []));
|
||||
$this->insurerStatements->where('id', $file_id)->set(['line_items' => 0, 'file_status' => 'failed', 'reason' => json_encode($errorData)])->update();
|
||||
return array('status' => false, 'error_code' => [], 'error_data' => $errorData);
|
||||
}
|
||||
}
|
||||
|
||||
private function sanitizeStatementLookupValue($value): string
|
||||
{
|
||||
$value = trim((string)($value ?? ''));
|
||||
$value = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $value);
|
||||
return preg_replace('/[\x{200B}\x{200C}\x{200D}\x{FEFF}\x{00A0}\x{200E}\x{200F}\x{202A}-\x{202E}]/u', '', $value);
|
||||
}
|
||||
|
||||
public function updateInsurerStatement($params)
|
||||
{
|
||||
/*
|
||||
* Changes made: 12-06-2024
|
||||
* - Added safe file_id handling and DB file record validation before using file details.
|
||||
* - Fixed physical file missing response and error message.
|
||||
* - Skips empty Excel rows and collects unique policy numbers from the uploaded statement.
|
||||
* - Fetches NHance source records only for those uploaded policy numbers.
|
||||
* - Uses the same sanitized policy number + endorsement number matching as validation.
|
||||
* - Inserts statement details only for matched rows and skips empty or unmatched Excel rows.
|
||||
* - Added safe default handling for amount and reward columns before calculation/insert.
|
||||
*/
|
||||
helper('excel_util_helper');
|
||||
$file_id = $params['file_id'] ?? 0;
|
||||
|
||||
$error_data = ['error_code' => '', 'error_data' => []];
|
||||
$status = 'success';
|
||||
$ret_status = true;
|
||||
|
||||
//get file info
|
||||
$file = $this->insurerStatements->find((int)$file_id);
|
||||
// dd($file);
|
||||
|
||||
if (empty($file)) {
|
||||
//file not found in DB
|
||||
return array('status' => false, 'error_code' => 0, 'error_data' => 'statement file not found in DB');
|
||||
}
|
||||
$file_name_with_path = WRITEPATH . "/uploads/statements/" . $file['file_name'];
|
||||
|
||||
//check physical file
|
||||
if (!file_exists($file_name_with_path)) {
|
||||
//file not found update status and reason
|
||||
$message = "Physical file not found";
|
||||
// echo $message;
|
||||
$this->myLogger->logme('error', ($message . ' for statement file id ' . $file_id));
|
||||
$this->insurerStatements->where('id', $file_id)->set(['file_status' => 'failed', 'reason' => json_encode(['error_code' => 0, 'error_data' => $message])])->update();
|
||||
return array('status' => false, 'error_code' => 0, 'error_data' => $message); //0 - Physcial file not found
|
||||
}
|
||||
|
||||
//get excel data to php array
|
||||
$spreadsheet = \PhpOffice\PhpSpreadsheet\IOFactory::load($file_name_with_path);
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$highestRowAndColumn = $sheet->getHighestRowAndColumn();
|
||||
// dd($highestRowAndColumn);
|
||||
$excel_data = $sheet->rangeToArray('A1:' . $highestRowAndColumn['column'] . $highestRowAndColumn['row']);
|
||||
unset($excel_data[0]);
|
||||
$excel_data = ExcelSanitizeHelper::sanitizeArrayData($excel_data);
|
||||
// dd($excel_data);
|
||||
|
||||
$policyNos = [];
|
||||
|
||||
foreach ($excel_data as $row) {
|
||||
if (check_row_is_empty_or_null($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$policyNo = $this->sanitizeStatementLookupValue($row[1] ?? '');
|
||||
|
||||
if ($policyNo !== '') {
|
||||
$policyNos[] = $policyNo;
|
||||
}
|
||||
}
|
||||
$policyNos = array_values(array_unique($policyNos));
|
||||
|
||||
// get uploaded month transactions data
|
||||
// $source_data = $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactions(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], month: $file['month']);
|
||||
$source_data = !empty($policyNos) ? $this->PTCOShareDetailsModel->getNonReconcileredPolicyTransactionByPolicyAndEndorsement(insurer_id: $file['insurer_id'], insurer_branch_id: $file['branch_id'], policy_no: $policyNos) : [];
|
||||
// Kint::dump($source_data);//die;
|
||||
// Kint::dump($excel_data);
|
||||
// die;
|
||||
$source_lookup = [];
|
||||
|
||||
foreach ($source_data as $source_row) {
|
||||
$source_policy_no = $this->sanitizeStatementLookupValue($source_row['policy_no'] ?? '');
|
||||
$source_endorsement_no = $this->sanitizeStatementLookupValue($source_row['endorsement_no'] ?? '');
|
||||
$source_lookup[$source_policy_no . '|' . $source_endorsement_no][] = $source_row;
|
||||
}
|
||||
|
||||
// check policy no,insurer and etc in DB for this month
|
||||
// if all good return true, otherwise return false with messssage
|
||||
$data_to_update = [];
|
||||
try {
|
||||
foreach ($excel_data as $excel_row) {
|
||||
if (check_row_is_empty_or_null($excel_row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// $policy_start_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[4]); //policy_start_date from excel
|
||||
// $policy_end_date = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[5]); //policy_end_date from excel
|
||||
$policy_no = $this->sanitizeStatementLookupValue($excel_row[1] ?? ''); //policy_number from excel
|
||||
// $client_name = preg_replace('/^\p{Z}+|\p{Z}+$/u', '', $excel_row[3]); //clientname from excel
|
||||
$endorsement_no = $this->sanitizeStatementLookupValue($excel_row[2] ?? ''); //endorsement number from excel
|
||||
$entry_key = $policy_no . '|' . $endorsement_no;
|
||||
|
||||
if (empty($source_lookup[$entry_key])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source_row = array_shift($source_lookup[$entry_key]);
|
||||
|
||||
//calculate percentage first
|
||||
$total_amt = 0;
|
||||
|
||||
// $actual_bp_per = trim($excel_row[9]); //commented becoz this filed removed tfrom excel file
|
||||
$actual_bp_per = 0; //set default value 0 for maintaining existing code flow
|
||||
$actual_bp_brokerage = (int) trim((string)($excel_row[5] ?? 0));
|
||||
$actual_bp_amt = (int) trim((string)($excel_row[3] ?? 0));
|
||||
|
||||
if (($actual_bp_brokerage && $actual_bp_brokerage != 0 && $actual_bp_brokerage != "")) {
|
||||
$total_amt += $actual_bp_brokerage;
|
||||
//percentage reverse calculation
|
||||
if (($actual_bp_per == 0 || $actual_bp_per == 0) && !empty($actual_bp_amt)) {
|
||||
$actual_bp_per = (int) round(($actual_bp_brokerage / $actual_bp_amt) * 100, 2);
|
||||
}
|
||||
} else {
|
||||
$actual_bp_brokerage = $actual_bp_amt * ($actual_bp_per / 100);
|
||||
$total_amt += $actual_bp_brokerage;
|
||||
}
|
||||
|
||||
// $actual_tp_per = trim($excel_row[10]);//commented becoz this filed removed tfrom excel file
|
||||
$actual_tp_per = 0;//set default value 0 for maintaining existing code flow
|
||||
$actual_tp_brokerage = (int) trim((string)($excel_row[6] ?? 0));
|
||||
$actual_tp_amt = (int) trim((string)($excel_row[4] ?? 0));
|
||||
|
||||
if ($actual_tp_brokerage && $actual_tp_brokerage != 0 && $actual_tp_brokerage != "") {
|
||||
$total_amt += $actual_tp_brokerage;
|
||||
//percentage reverse calculation
|
||||
if (($actual_tp_per == 0 || $actual_tp_per == "") && !empty($actual_tp_amt)) {
|
||||
$actual_tp_per = (int) round(($actual_tp_brokerage / $actual_tp_amt) * 100, 2);
|
||||
}
|
||||
} else {
|
||||
$actual_tp_brokerage = $actual_tp_amt * ($actual_tp_per / 100);
|
||||
$total_amt += $actual_tp_brokerage;
|
||||
}
|
||||
|
||||
// $actual_tep_per = trim($excel_row[11]);
|
||||
// $actual_tep_brokerage = trim($excel_row[14]);
|
||||
// $actual_tep_amt = trim($excel_row[8]);
|
||||
|
||||
$actual_tep_per = 0;
|
||||
$actual_tep_brokerage = 0;
|
||||
$actual_tep_amt = 0;
|
||||
|
||||
if ($actual_tep_brokerage && $actual_tep_brokerage != 0 && $actual_tep_brokerage != "") {
|
||||
$total_amt += $actual_tep_brokerage;
|
||||
//percentage reverse calculation
|
||||
if (($actual_tep_per == 0 || $actual_tep_per == "") && !empty($actual_tep_amt)) {
|
||||
$actual_tep_per = ($actual_tep_brokerage / $actual_tep_amt) * 100;
|
||||
}
|
||||
} else {
|
||||
$actual_tep_brokerage = $actual_tep_amt * ($actual_tep_per / 100);
|
||||
$total_amt += $actual_tep_brokerage;
|
||||
}
|
||||
|
||||
//find variance
|
||||
$variance_amt = $source_row['exp_amt'] - $total_amt;
|
||||
|
||||
$data_to_update[] = ['co_share_id' => $source_row['id'], 'actual_bp_amt' => $actual_bp_amt, 'actual_tp_amt' => $actual_tp_amt, 'actual_tep_amt' => $actual_tep_amt, 'actual_bp_per' => $actual_bp_per, 'actual_tp_per' => $actual_tp_per, 'actual_tep_per' => $actual_tep_per, 'variance' => $variance_amt, 'actual_tep_brokerage_amt' => $actual_tep_brokerage, 'actual_tp_brokerage_amt' => $actual_tp_brokerage, 'actual_bp_brokerage_amt' => $actual_bp_brokerage, 'reward' => trim((string)($excel_row[7] ?? '')), 'statement_id' => $file_id];
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - updateInsurerStatement: Exception: " . $th->getMessage() . " --- Line: " . $th->getLine() . " --- Trace: " . $th->getTraceAsString());
|
||||
$errorData = [
|
||||
'message' => $th->getMessage(),
|
||||
'file' => $th->getFile(),
|
||||
'line' => $th->getLine(),
|
||||
'code' => $th->getCode(),
|
||||
'trace' => $th->getTraceAsString(),
|
||||
'trace_array' => $th->getTrace(), // full array version (optional)
|
||||
'function' => $th->getTrace()[0]['function'] ?? null,
|
||||
'class' => $th->getTrace()[0]['class'] ?? null,
|
||||
];
|
||||
return ['status' => 'failed', 'code' => 500, 'message' => $th->getMessage(), 'error_data' => $errorData];
|
||||
}
|
||||
// dd($data_to_update);
|
||||
$this->coShareStmtDetailsModel->insertBatch($data_to_update, 'id');
|
||||
// dd($data_to_update);
|
||||
// if($error_data['error_code'])
|
||||
// {
|
||||
// $status = 'failed';
|
||||
// $ret_status = false;
|
||||
// }
|
||||
//update in DB
|
||||
$this->insurerStatements->where('id', $file_id)->set(['file_status' => $status, 'reason' => json_encode($error_data), 'invoice_status' => 'pending'])->update();
|
||||
return array('status' => $ret_status, 'error_code' => $error_data['error_code'], 'error_data' => $error_data['error_data']);
|
||||
}
|
||||
|
||||
public function getInvoicePaymentDetails()
|
||||
{
|
||||
$statement_id = $this->request->getUri()->getSegment(4);
|
||||
@ -4697,6 +5065,27 @@ class PolicyTransactionController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
public function downloadInsurerStatement($id)
|
||||
{
|
||||
$statement = $this->insurerStatements
|
||||
->where('id', (int) $id)
|
||||
->where('is_active', 1)
|
||||
->first();
|
||||
|
||||
if (empty($statement) || empty($statement['file_name'])) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found');
|
||||
}
|
||||
|
||||
$fileName = basename($statement['file_name']);
|
||||
$filePath = WRITEPATH . 'uploads/statements/' . $fileName;
|
||||
|
||||
if (!is_file($filePath)) {
|
||||
throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound('Statement file not found');
|
||||
}
|
||||
|
||||
return $this->response->download($filePath, null)->setFileName($fileName);
|
||||
}
|
||||
|
||||
public function getFileErr()
|
||||
{
|
||||
$file_id = $this->request->getUri()->getSegment(4);
|
||||
@ -4932,6 +5321,8 @@ class PolicyTransactionController extends BaseController
|
||||
|
||||
try {
|
||||
|
||||
$bdsInstallmentData = [];
|
||||
|
||||
try {
|
||||
|
||||
$bdsInstallmentData = $this->BdsPlacementModel->getClientInstallmentDetails();
|
||||
@ -4939,6 +5330,7 @@ class PolicyTransactionController extends BaseController
|
||||
|
||||
$this->myLogger->logme('error', 'Exception: ' . $e->getMessage() . 'Page: ' . $e->getFile() . ' Line: ' . $e->getLine());
|
||||
}
|
||||
|
||||
$this->myLogger->logme("error", "Data for BDS Installment " . json_encode($bdsInstallmentData));
|
||||
// dd($bdsInstallmentData);
|
||||
|
||||
|
||||
@ -2446,4 +2446,25 @@ class RestAuthenticationController extends AdminController
|
||||
echo json_encode($result, JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
}
|
||||
|
||||
|
||||
public function getTokenforSamulssOAuthLogin()
|
||||
{
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Function called");
|
||||
$this->myLogger->logme("error", "REST-AUTH-CONTROLLER - getTokenforSamulssOAuthLogin: Received payload = " . json_encode($this->request->getJSON() ?? []));
|
||||
|
||||
$id = $this->request->getGet('id') ?? null;
|
||||
if(!$id){
|
||||
return $this->respond(['status' => 'failed','code' => 400,'message' => 'ID is required'], 200);
|
||||
}
|
||||
|
||||
$user = $this->employeeModel->where('id', $id)->first();
|
||||
if(!$user){
|
||||
return $this->respond(['status' => 'failed','code' => 400,'message' => 'User not found'], 200);
|
||||
}
|
||||
|
||||
$token = JWTToken::encode($user);
|
||||
return $this->respond(['status' => 'success','code' => 200,'data' => $token], 200);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -40,6 +40,7 @@ class SalesController extends BaseController
|
||||
|
||||
public function index() {
|
||||
$data = $this->getSalesStaffData();
|
||||
$data = array_merge($data, $this->getSalesFilterYears());
|
||||
|
||||
$data['tab_name'] = 'Leads';
|
||||
$data['page_name'] = 'Leads';
|
||||
@ -48,6 +49,7 @@ class SalesController extends BaseController
|
||||
|
||||
public function loadactivities(){
|
||||
$data = $this->getSalesStaffData();
|
||||
$data = array_merge($data, $this->getSalesFilterYears());
|
||||
|
||||
$data['tab_name'] = 'Activities';
|
||||
$data['page_name'] = 'Activities';
|
||||
@ -350,7 +352,9 @@ class SalesController extends BaseController
|
||||
'status' => $this->request->getGet('status'),
|
||||
'assigned_to' => $this->request->getGet('assigned_to'),
|
||||
'search' => $this->request->getGet('search'),
|
||||
'financial_year' => $this->request->getGet('financial_year'),
|
||||
];
|
||||
$filters = $this->applyFinancialYearDateRange($filters);
|
||||
|
||||
$result = $this->leadModel->getLeadsWithFilters($filters, $limit, $offset);
|
||||
|
||||
@ -367,6 +371,102 @@ class SalesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export leads filtered by created_at date range.
|
||||
* GET /sales/leads/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD
|
||||
*/
|
||||
public function exportLeads()
|
||||
{
|
||||
try {
|
||||
$range = $this->getExportDateRange();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->failValidationErrors($e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('sales_actual_leads sal')
|
||||
->select('
|
||||
sal.company_name,
|
||||
c.short_name,
|
||||
sal.address,
|
||||
sal.website,
|
||||
sal.gst_number,
|
||||
sal.email,
|
||||
sal.phone,
|
||||
sal.status,
|
||||
sal.created_at,
|
||||
up.first_name as assigned_to_name,
|
||||
COUNT(DISTINCT sa.activity_id) as total_activity
|
||||
')
|
||||
->join('clients c', 'c.id = sal.client_id', 'left')
|
||||
->join('user_profiles up', 'up.id = sal.assigned_to', 'left')
|
||||
->join('sales_activities sa', 'sa.lead_id = sal.lead_id', 'left')
|
||||
->where("sal.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false);
|
||||
|
||||
$status = $this->request->getGet('status');
|
||||
if (!empty($status)) {
|
||||
$builder->where('sal.status', $status);
|
||||
}
|
||||
|
||||
$assignedToIds = $this->getExportAssignedToIds();
|
||||
if (!empty($assignedToIds)) {
|
||||
$builder->whereIn('sal.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
$search = $this->request->getGet('search');
|
||||
if (!empty($search)) {
|
||||
$builder->groupStart()
|
||||
->like('sal.company_name', $search)
|
||||
->orLike('c.short_name', $search)
|
||||
->orLike('sal.address', $search)
|
||||
->orLike('sal.website', $search)
|
||||
->orLike('sal.gst_number', $search)
|
||||
->orLike('sal.email', $search)
|
||||
->orLike('sal.phone', $search)
|
||||
->orLike('up.first_name', $search)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$leads = $builder
|
||||
->groupBy('sal.lead_id, sal.company_name, c.short_name, sal.address, sal.website, sal.gst_number, sal.email, sal.phone, sal.status, sal.created_at, up.first_name')
|
||||
->orderBy('sal.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($leads)) {
|
||||
return $this->exportNotFoundResponse("Leads Not Found {$range['display']}");
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$serialNo = 1;
|
||||
foreach ($leads as $lead) {
|
||||
$rows[] = [
|
||||
$serialNo++,
|
||||
$lead['company_name'] ?? '',
|
||||
$lead['short_name'] ?? '',
|
||||
$lead['address'] ?? '',
|
||||
$lead['website'] ?? '',
|
||||
$lead['gst_number'] ?? '',
|
||||
$lead['email'] ?? '',
|
||||
$lead['phone'] ?? '',
|
||||
$lead['status'] ?? '',
|
||||
$lead['assigned_to_name'] ?? '',
|
||||
$lead['total_activity'] ?? 0,
|
||||
$this->formatExportDate($lead['created_at'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->streamCsvDownload(
|
||||
"sales-leads-{$range['from_label']}-to-{$range['to_label']}.csv",
|
||||
['S.No', 'Company', 'Company Short Name', 'Address', 'Website', 'GST', 'Email', 'Phone', 'Status', 'Assigned To', 'Total Activity', 'Created At'],
|
||||
$rows
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get single lead with complete details
|
||||
* GET /api/sales/leads/{id}
|
||||
@ -732,8 +832,10 @@ class SalesController extends BaseController
|
||||
'assigned_to' => $this->request->getGet('assigned_to'),
|
||||
'date_from' => $this->request->getGet('date_from'),
|
||||
'date_to' => $this->request->getGet('date_to'),
|
||||
'financial_year' => $this->request->getGet('financial_year'),
|
||||
'search' => $this->request->getGet('search'), // ← ADD THIS
|
||||
];
|
||||
$filters = $this->applyFinancialYearDateRange($filters);
|
||||
|
||||
$result = $this->activityModel->getActivitiesWithFilters($filters, $limit, $offset);
|
||||
|
||||
@ -751,6 +853,104 @@ class SalesController extends BaseController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export activities filtered by created_at date range.
|
||||
* GET /sales/activities/export?from_date=YYYY-MM-DD&to_date=YYYY-MM-DD
|
||||
*/
|
||||
public function exportActivities()
|
||||
{
|
||||
try {
|
||||
$range = $this->getExportDateRange();
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return $this->failValidationErrors($e->getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
$db = \Config\Database::connect();
|
||||
$builder = $db->table('sales_activities sa')
|
||||
->select("
|
||||
sa.activity_type,
|
||||
sa.status,
|
||||
sa.scheduled_date,
|
||||
sa.created_at,
|
||||
sa.notes,
|
||||
sa.completion_notes,
|
||||
sal.company_name,
|
||||
c.short_name,
|
||||
up1.first_name as assigned_to_name,
|
||||
GROUP_CONCAT(DISTINCT up2.first_name SEPARATOR ', ') as additional_assigned_names
|
||||
")
|
||||
->join('sales_actual_leads sal', 'sal.lead_id = sa.lead_id', 'left')
|
||||
->join('clients c', 'c.id = sal.client_id', 'left')
|
||||
->join('user_profiles up1', 'up1.id = sa.assigned_to', 'left')
|
||||
->join('user_profiles up2', "
|
||||
sa.additional_assigned_ids IS NOT NULL
|
||||
AND sa.additional_assigned_ids != ''
|
||||
AND sa.additional_assigned_ids != '[]'
|
||||
AND JSON_VALID(sa.additional_assigned_ids)
|
||||
AND JSON_CONTAINS(sa.additional_assigned_ids, JSON_QUOTE(CAST(up2.id AS CHAR)))
|
||||
", 'left')
|
||||
->where("sa.created_at BETWEEN {$db->escape($range['from'])} AND {$db->escape($range['to'])}", null, false);
|
||||
|
||||
$status = $this->request->getGet('status');
|
||||
if (!empty($status)) {
|
||||
$builder->where('sa.status', $status);
|
||||
}
|
||||
|
||||
$assignedToIds = $this->getExportAssignedToIds();
|
||||
if (!empty($assignedToIds)) {
|
||||
$builder->whereIn('sa.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
$search = $this->request->getGet('search');
|
||||
if (!empty($search)) {
|
||||
$builder->groupStart()
|
||||
->like('sal.company_name', $search)
|
||||
->orLike('c.short_name', $search)
|
||||
->orLike('sa.status', $search)
|
||||
->orLike('sa.activity_type', $search)
|
||||
->orLike('up1.first_name', $search)
|
||||
->orLike('up2.first_name', $search)
|
||||
->groupEnd();
|
||||
}
|
||||
|
||||
$activities = $builder
|
||||
->groupBy('sa.activity_id')
|
||||
->orderBy('sa.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
if (empty($activities)) {
|
||||
return $this->exportNotFoundResponse("Activities not Found {$range['display']}");
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
$serialNo = 1;
|
||||
foreach ($activities as $activity) {
|
||||
$rows[] = [
|
||||
$serialNo++,
|
||||
$activity['company_name'] ?? '',
|
||||
$activity['short_name'] ?? '',
|
||||
$activity['activity_type'] ?? '',
|
||||
ucfirst((string) ($activity['status'] ?? '')),
|
||||
$activity['assigned_to_name'] ?? '',
|
||||
$activity['additional_assigned_names'] ?? '',
|
||||
$this->formatExportDate($activity['scheduled_date'] ?? ''),
|
||||
$this->formatExportDate($activity['created_at'] ?? ''),
|
||||
$activity['notes'] ?: ($activity['completion_notes'] ?? ''),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->streamCsvDownload(
|
||||
"sales-activities-{$range['from_label']}-to-{$range['to_label']}.csv",
|
||||
['S.No', 'Company', 'Company Short Name', 'Activity Type', 'Status', 'Assigned To', 'Additional Members', 'Scheduled Date', 'Created At', 'Notes'],
|
||||
$rows
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail($e->getMessage(), ResponseInterface::HTTP_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get activities for a specific lead
|
||||
* GET /api/sales/leads/{leadId}/activities
|
||||
@ -1205,6 +1405,144 @@ class SalesController extends BaseController
|
||||
return 1;
|
||||
}
|
||||
|
||||
private function getSalesFilterYears(): array
|
||||
{
|
||||
$currentFinYear = getCurrentFinancialYear();
|
||||
$db = \Config\Database::connect();
|
||||
|
||||
$finYearsRaw = $db->table('sales_target')
|
||||
->select('fy_year', false)
|
||||
->distinct()
|
||||
->orderBy('fy_year', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
|
||||
$finYears = array_column($finYearsRaw, 'fy_year');
|
||||
if (empty($finYears)) {
|
||||
$finYears[] = $currentFinYear;
|
||||
}
|
||||
|
||||
if (!in_array($currentFinYear, $finYears, true)) {
|
||||
array_unshift($finYears, $currentFinYear);
|
||||
}
|
||||
|
||||
return [
|
||||
'fin_years' => $finYears,
|
||||
'current_fin_year' => $currentFinYear,
|
||||
];
|
||||
}
|
||||
|
||||
private function applyFinancialYearDateRange(array $filters): array
|
||||
{
|
||||
if (empty($filters['financial_year'])) {
|
||||
return $filters;
|
||||
}
|
||||
|
||||
$fyRange = $this->getFYDateRange((string) $filters['financial_year']);
|
||||
$filters['date_from'] = $fyRange['start'];
|
||||
$filters['date_to'] = $fyRange['end'];
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
private function getExportDateRange(): array
|
||||
{
|
||||
$fromDate = trim((string) $this->request->getGet('from_date'));
|
||||
$toDate = trim((string) $this->request->getGet('to_date'));
|
||||
|
||||
if ($fromDate === '' || $toDate === '') {
|
||||
throw new \InvalidArgumentException('Please select from date and to date.');
|
||||
}
|
||||
|
||||
$from = $this->normalizeExportDate($fromDate, '00:00:00');
|
||||
$to = $this->normalizeExportDate($toDate, '23:59:59');
|
||||
|
||||
if ($from === null || $to === null) {
|
||||
throw new \InvalidArgumentException('Invalid date range. Use YYYY-MM-DD format.');
|
||||
}
|
||||
|
||||
if (strtotime($from) > strtotime($to)) {
|
||||
throw new \InvalidArgumentException('From date cannot be after to date.');
|
||||
}
|
||||
|
||||
return [
|
||||
'from' => $from,
|
||||
'to' => $to,
|
||||
'from_label' => date('Y-m-d', strtotime($from)),
|
||||
'to_label' => date('Y-m-d', strtotime($to)),
|
||||
'display' => date('d-m-Y', strtotime($from)) . ' to ' . date('d-m-Y', strtotime($to)),
|
||||
];
|
||||
}
|
||||
|
||||
private function normalizeExportDate(string $date, string $time): ?string
|
||||
{
|
||||
$dateTime = \DateTime::createFromFormat('Y-m-d H:i:s', $date . ' ' . $time);
|
||||
|
||||
if (!$dateTime || $dateTime->format('Y-m-d') !== $date) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $dateTime->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
private function getExportAssignedToIds(): array
|
||||
{
|
||||
$assignedTo = (string) $this->request->getGet('assigned_to');
|
||||
|
||||
if ($assignedTo === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_values(array_filter(array_map('trim', explode(',', $assignedTo)), static function ($id) {
|
||||
return ctype_digit($id);
|
||||
}));
|
||||
}
|
||||
|
||||
private function streamCsvDownload(string $filename, array $headers, array $rows)
|
||||
{
|
||||
$handle = fopen('php://temp', 'r+');
|
||||
fwrite($handle, "\xEF\xBB\xBF");
|
||||
fputcsv($handle, $headers);
|
||||
|
||||
foreach ($rows as $row) {
|
||||
fputcsv($handle, $row);
|
||||
}
|
||||
|
||||
rewind($handle);
|
||||
$csv = stream_get_contents($handle);
|
||||
fclose($handle);
|
||||
|
||||
return $this->response
|
||||
->setHeader('Content-Type', 'text/csv; charset=UTF-8')
|
||||
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
|
||||
->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate')
|
||||
->setBody($csv);
|
||||
}
|
||||
|
||||
private function exportNotFoundResponse(string $message)
|
||||
{
|
||||
return $this->response
|
||||
->setStatusCode(ResponseInterface::HTTP_NOT_FOUND)
|
||||
->setJSON([
|
||||
'status' => 'error',
|
||||
'message' => $message,
|
||||
]);
|
||||
}
|
||||
|
||||
private function formatExportDate(?string $value): string
|
||||
{
|
||||
if (empty($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$timestamp = strtotime($value);
|
||||
if ($timestamp === false) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return date('d/m/Y h:i A', $timestamp);
|
||||
}
|
||||
|
||||
|
||||
// ==================== Dashboard ====================
|
||||
|
||||
@ -1392,6 +1730,7 @@ public function branchLevelDashboard($branchId, $branchwise_all_sales_team_ids,
|
||||
// Activities & opportunities also scoped to FY via CASE WHEN
|
||||
$leadsOverview = $db->table('sales_actual_leads sal')
|
||||
->select("sal.lead_id, sal.company_name, sal.status,
|
||||
sal.assigned_to AS assigned_to_id,
|
||||
up.first_name AS assigned_to,
|
||||
COUNT(DISTINCT CASE WHEN sa.scheduled_date >= '{$fyStart}'
|
||||
AND sa.scheduled_date <= '{$fyEnd}' THEN sa.activity_id END) AS activities,
|
||||
@ -1403,7 +1742,7 @@ public function branchLevelDashboard($branchId, $branchwise_all_sales_team_ids,
|
||||
->whereIn('sal.assigned_to', $branchwise_all_sales_team_ids)
|
||||
->where('sal.created_at >=', $fyStart)
|
||||
->where('sal.created_at <=', $fyEnd)
|
||||
->groupBy('sal.lead_id, sal.company_name, sal.status, up.first_name')
|
||||
->groupBy('sal.lead_id, sal.company_name, sal.status, sal.assigned_to, up.first_name')
|
||||
->orderBy('sal.created_at', 'DESC')
|
||||
->get()
|
||||
->getResultArray();
|
||||
@ -1497,6 +1836,7 @@ private function buildTeamAchievement($db, array $sales_manager_ids, $branchId,
|
||||
->get()
|
||||
->getRowArray();
|
||||
$targetAmt = (float)($targetRow['target_amount'] ?? 0);
|
||||
$targetId = $targetRow['id'] ?? null;
|
||||
|
||||
// Achieved (won leads in FY)
|
||||
$achievedAmt = (float)$this->getUserAchievedAmount($fy, $uid);
|
||||
@ -1589,6 +1929,7 @@ private function buildTeamAchievement($db, array $sales_manager_ids, $branchId,
|
||||
'role' => $m['role'],
|
||||
'total_acts' => $totalActs,
|
||||
'done_acts' => $doneActs,
|
||||
'target_id' => $targetId,
|
||||
'target_amt' => $targetAmt,
|
||||
'achieved_amt' => $achievedAmt,
|
||||
'grad' => $palette['grad'],
|
||||
@ -1658,12 +1999,23 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
|
||||
|
||||
// ── Won Leads for this user in FY (Table 2 in modal) ──
|
||||
// leads.actual_lead_id maps to sales_actual_leads.id
|
||||
// leads.type: 1 = EB, else = Non-EB
|
||||
// leads.lead_form_type: 1 = EB, else = Non-EB
|
||||
// leads.lead_type: 1 = Fresh, 2 = Renewal, 3 = Roll Over
|
||||
$wonLeads = $db->query("
|
||||
SELECT
|
||||
sal.lead_id,
|
||||
l.id AS opportunities_id,
|
||||
l.actual_lead_id,
|
||||
COALESCE(l.lead_form_type, 1) AS lead_form_type_id,
|
||||
l.lead_type AS lead_type_id,
|
||||
sal.company_name AS company,
|
||||
CASE WHEN l.lead_type = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_type,
|
||||
l.client_name AS client_name,
|
||||
CASE WHEN COALESCE(l.lead_form_type, 1) = 1 THEN 'EB' ELSE 'Non-EB' END AS lead_form_type,
|
||||
CASE
|
||||
WHEN l.lead_type = 1 THEN 'Fresh'
|
||||
WHEN l.lead_type = 2 THEN 'Renewal'
|
||||
WHEN l.lead_type = 3 THEN 'Roll Over'
|
||||
ELSE ''
|
||||
END AS lead_type,
|
||||
l.created_at AS created_at,
|
||||
l.status
|
||||
FROM leads l
|
||||
@ -1676,6 +2028,7 @@ private function buildOppAchievement($db, array $sales_manager_ids, $branchId, s
|
||||
ORDER BY l.created_at DESC
|
||||
", [$uid, $fyStart, $fyEnd])->getResultArray();
|
||||
|
||||
// -- NOTE: AND TRIM(sal.company_name) = TRIM(l.client_name)
|
||||
$result[$uid] = [
|
||||
'total_policies' => $totalPolicies,
|
||||
'total_exp_amt' => $totalExpAmt,
|
||||
|
||||
@ -1162,28 +1162,28 @@ class TicketController extends BaseController
|
||||
// --- EMPLOYEE DETAILS ---
|
||||
'emp_code' => [
|
||||
'label' => 'Employee ID',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee ID is required.',
|
||||
'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).'
|
||||
'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).'
|
||||
]
|
||||
],
|
||||
'emp_name' => [
|
||||
'label' => 'Employee Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee Name is required',
|
||||
'min_length' => 'Employee Name must be at least 3 characters long',
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'insured_name' => [
|
||||
'label' => 'Insured Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Insured Name is required',
|
||||
'min_length' => 'Insured Name must be at least 3 characters long',
|
||||
'regex_match' => 'Insured Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Insured Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'relationship' => ['label' => 'Relationship','rules' => 'required',
|
||||
@ -1393,19 +1393,19 @@ class TicketController extends BaseController
|
||||
// --- EMPLOYEE DETAILS ---
|
||||
'emp_code' => [
|
||||
'label' => 'Employee ID',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee ID is required.',
|
||||
'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).',
|
||||
'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).',
|
||||
]
|
||||
],
|
||||
'emp_name' => [
|
||||
'label' => 'Employee Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee Name is required',
|
||||
'min_length' => 'Employee Name must be at least 3 characters long',
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
|
||||
@ -1650,28 +1650,28 @@ class TicketController extends BaseController
|
||||
// --- EMPLOYEE DETAILS ---
|
||||
'emp_code' => [
|
||||
'label' => 'Employee ID',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee ID is required.',
|
||||
'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).',
|
||||
'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).',
|
||||
]
|
||||
],
|
||||
'emp_name' => [
|
||||
'label' => 'Employee Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee Name is required',
|
||||
'min_length' => 'Employee Name must be at least 3 characters long',
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'insured_name' => [
|
||||
'label' => 'Insured Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Insured Name is required',
|
||||
'min_length' => 'Insured Name must be at least 3 characters long',
|
||||
'regex_match' => 'Insured Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Insured Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'relationship' => ['label' => 'Relationship','rules' => 'required',
|
||||
@ -1882,19 +1882,19 @@ class TicketController extends BaseController
|
||||
// --- EMPLOYEE DETAILS ---
|
||||
'emp_code' => [
|
||||
'label' => 'Employee ID',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9_-]+$/]',
|
||||
'rules' => 'required|min_length[2]|max_length[20]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee ID is required.',
|
||||
'regex_match' => 'Employee ID cannot contain spaces or special characters (only letters, numbers, hyphens and underscores. allowed).',
|
||||
'regex_match' => 'Employee ID can only contain letters, numbers, spaces, slashes (/), hyphens (-) and underscores (_).',
|
||||
]
|
||||
],
|
||||
'emp_name' => [
|
||||
'label' => 'Employee Name',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s_-]+$/]',
|
||||
'rules' => 'required|min_length[3]|regex_match[/^[a-zA-Z0-9\s\/_-]+$/]',
|
||||
'errors' => [
|
||||
'required' => 'Employee Name is required',
|
||||
'min_length' => 'Employee Name must be at least 3 characters long',
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, hyphens, and underscores.'
|
||||
'regex_match' => 'Employee Name can only contain letters, numbers, spaces, slashes (/), hyphens (-), and underscores (_).'
|
||||
]
|
||||
],
|
||||
'emp_mobile' => ['label' => 'Employee Mobile No','rules' => 'required|numeric|exact_length[10]',
|
||||
|
||||
@ -2107,8 +2107,8 @@ if (!function_exists('remap_default_age_ratio_into_relationship')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('check_dup_mobileno')) {
|
||||
function check_dup_mobileno(array $row, array $existing_mobilenos)
|
||||
if (!function_exists('check_dup_mobileno_old')) {
|
||||
function check_dup_mobileno_old(array $row, array $existing_mobilenos)
|
||||
{
|
||||
if(!empty($row['12'])){
|
||||
foreach ($existing_mobilenos as $k => $value) {
|
||||
@ -2122,6 +2122,41 @@ if (!function_exists('check_dup_mobileno')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('check_dup_mobileno')) {
|
||||
function check_dup_mobileno(array $row, array $existing_mobilenos, $excel_data = [], $row_key = null)
|
||||
{
|
||||
if(!empty($row['12'])){
|
||||
$current_relation = strtolower(trim($row['5'] ?? ''));
|
||||
$current_mobile = trim((string) $row['12']);
|
||||
|
||||
foreach ($existing_mobilenos as $k => $value) {
|
||||
if ($current_relation == 'self' && $current_mobile == trim((string) ($value['mobile'] ?? ''))) {
|
||||
return array('status' => false, 'error' => "Duplicate Mobile No");
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($excel_data as $ed_row => $value) {
|
||||
if ($row_key !== null && (string) $ed_row === (string) $row_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$excel_relation = strtolower(trim($value['5'] ?? ''));
|
||||
$excel_mobile = trim((string) ($value['12'] ?? ''));
|
||||
|
||||
if ($current_relation == 'self' && $excel_relation == 'self' && $excel_mobile !== '' && $current_mobile == $excel_mobile) {
|
||||
$duplicate_row_no = is_numeric($ed_row) ? $ed_row + 1 : $ed_row;
|
||||
$current_row_no = is_numeric($row_key) ? $row_key + 1 : $row_key;
|
||||
return array('status' => false, 'error' => "This mobile number has already been used for another self in Excel at row no. " . ($duplicate_row_no) . " and " . ($current_row_no));
|
||||
// break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return array('status' => true);
|
||||
}
|
||||
}
|
||||
|
||||
// if (!function_exists('check_dup_email')) {
|
||||
// function check_dup_email(array $row, array $existing_mobilenos)
|
||||
// {
|
||||
@ -2138,8 +2173,8 @@ if (!function_exists('check_dup_mobileno')) {
|
||||
// }
|
||||
// }
|
||||
|
||||
if (!function_exists('check_dup_email')) {
|
||||
function check_dup_email(array $row, array $existing_mobilenos)
|
||||
if (!function_exists('check_dup_email_old')) {
|
||||
function check_dup_email_old(array $row, array $existing_mobilenos)
|
||||
{
|
||||
$errorMessages = [];
|
||||
|
||||
@ -2168,6 +2203,54 @@ if (!function_exists('check_dup_email')) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!function_exists('check_dup_email')) {
|
||||
function check_dup_email(array $row, array $existing_mobilenos, $excel_data = [], $row_key = null)
|
||||
{
|
||||
$errorMessages = [];
|
||||
$current_relation = strtolower(trim($row['5'] ?? ''));
|
||||
$current_email = trim((string) ($row['13'] ?? ''));
|
||||
|
||||
// Validate email
|
||||
$emailCheck = is_valid_or_empty_email($current_email);
|
||||
if (!$emailCheck) {
|
||||
$errorMessages[] = 'Invalid email format';
|
||||
}
|
||||
|
||||
// Check duplicate only if email is not empty
|
||||
if (!empty($current_email)) {
|
||||
foreach ($existing_mobilenos as $value) {
|
||||
if ($current_relation === 'self' && strcasecmp($current_email, trim((string) ($value['email_corporate'] ?? ''))) === 0) {
|
||||
$errorMessages[] = 'Duplicate Email';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($excel_data as $ed_row => $value) {
|
||||
if ($row_key !== null && (string) $ed_row === (string) $row_key) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$excel_relation = strtolower(trim($value['5'] ?? ''));
|
||||
$excel_email = trim((string) ($value['13'] ?? ''));
|
||||
|
||||
if ($current_relation === 'self' && $excel_relation === 'self' && $excel_email !== '' && strcasecmp($current_email, $excel_email) === 0) {
|
||||
$duplicate_row_no = is_numeric($ed_row) ? $ed_row + 1 : $ed_row;
|
||||
$current_row_no = is_numeric($row_key) ? $row_key + 1 : $row_key;
|
||||
$errorMessages[] = "This email has already been used for another self in Excel at row no. " . ($duplicate_row_no) . " and " . ($current_row_no);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare final return
|
||||
if (!empty($errorMessages)) {
|
||||
return ['status' => false, 'error' => implode(' & ', $errorMessages)];
|
||||
}
|
||||
|
||||
return ['status' => true, 'error' => ''];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!function_exists('generate_family_relationship_array')) {
|
||||
function generate_family_relationship_array($family_structure_from_policy_terms)
|
||||
|
||||
@ -13,22 +13,23 @@ if (! defined('MERGED_CLAIM_FILE_TYPE')) {
|
||||
|
||||
if (! function_exists('merge_ticket_pdfs')) {
|
||||
/**
|
||||
* Merge all active PDF rows in claim_files for a given ticket_master id
|
||||
* Merge all active PDF/image rows in claim_files for a given ticket_master id
|
||||
* into one combined PDF and register that PDF as a new claim_files row
|
||||
* with file_type = MERGED_CLAIM_FILE_TYPE.
|
||||
*
|
||||
* Source rows are picked from claim_files where:
|
||||
* - ticket_id = $ticket_master_id
|
||||
* - is_active = 1
|
||||
* - mime_type = 'application/pdf'
|
||||
* - file_type IN $opts['include_file_types'] (default [2, 3])
|
||||
* - mime_type IN $opts['include_mime_types'] (default PDF/JPG/PNG)
|
||||
* - file_type IN $opts['include_file_types'] (default [1, 2, 3])
|
||||
*
|
||||
* @param int $ticket_master_id
|
||||
* @param array $opts {
|
||||
* @var bool $replace Default true. Soft-delete previous merged row before re-creating.
|
||||
* @var int $created_by Override created_by user id on the inserted row.
|
||||
* @var int $ticket_type Default 1. Stored on the inserted claim_files row.
|
||||
* @var array $include_file_types Default [2, 3].
|
||||
* @var array $include_file_types Default [1, 2, 3].
|
||||
* @var array $include_mime_types Default ['application/pdf', 'image/jpeg', 'image/png'].
|
||||
* }
|
||||
* @return array {status, merged_file_id, file_name, pages, source_count, message}
|
||||
*/
|
||||
@ -38,7 +39,8 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
'replace' => true,
|
||||
'created_by' => null,
|
||||
'ticket_type' => 1,
|
||||
'include_file_types' => [2, 3],
|
||||
'include_file_types' => [1, 2],
|
||||
'include_mime_types' => ['application/pdf', 'image/jpeg', 'image/png'],
|
||||
];
|
||||
|
||||
$result = [
|
||||
@ -60,14 +62,13 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
$rows = $claimFiles
|
||||
->where('ticket_id', $ticket_master_id)
|
||||
->where('is_active', 1)
|
||||
->where('mime_type', 'application/pdf')
|
||||
->whereIn('file_type', $opts['include_file_types'])
|
||||
->orderBy('id', 'ASC')
|
||||
->findAll();
|
||||
|
||||
if (empty($rows)) {
|
||||
$result['status'] = true;
|
||||
$result['message'] = 'No PDF files to merge';
|
||||
$result['message'] = 'No PDF/image files to merge';
|
||||
return $result;
|
||||
}
|
||||
|
||||
@ -75,7 +76,7 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
. 'uploads' . DIRECTORY_SEPARATOR
|
||||
. 'claim_files' . DIRECTORY_SEPARATOR;
|
||||
|
||||
$sourcePaths = [];
|
||||
$sourceFiles = [];
|
||||
foreach ($rows as $row) {
|
||||
$name = ! empty($row['url']) ? $row['url'] : ($row['file_name'] ?? '');
|
||||
if (empty($name)) {
|
||||
@ -84,23 +85,47 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
// url column may sometimes hold a full URL; we only care about the file basename on disk.
|
||||
$full = $uploadDir . basename($name);
|
||||
if (is_file($full) && is_readable($full)) {
|
||||
$sourcePaths[] = $full;
|
||||
$mime = merge_ticket_pdf_resolve_mime($full, (string) ($row['mime_type'] ?? ''));
|
||||
if (! in_array($mime, $opts['include_mime_types'], true)) {
|
||||
log_message('error', "merge_ticket_pdfs | unsupported source mime {$mime} | claim_file_id={$row['id']} | path={$full}");
|
||||
continue;
|
||||
}
|
||||
|
||||
$sourceFiles[] = [
|
||||
'path' => $full,
|
||||
'mime' => $mime,
|
||||
'id' => $row['id'] ?? null,
|
||||
];
|
||||
} else {
|
||||
log_message('error', "merge_ticket_pdfs | missing PDF on disk | claim_file_id={$row['id']} | path={$full}");
|
||||
log_message('error', "merge_ticket_pdfs | missing file on disk | claim_file_id={$row['id']} | path={$full}");
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($sourcePaths)) {
|
||||
$result['message'] = 'No readable PDF files on disk';
|
||||
if (empty($sourceFiles)) {
|
||||
$result['message'] = 'No readable PDF/image files on disk';
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['source_count'] = count($sourcePaths);
|
||||
$result['source_count'] = count($sourceFiles);
|
||||
log_message(
|
||||
'error',
|
||||
'merge_ticket_pdfs | source files resolved | ticket_id=' . $ticket_master_id . ' | sources=' . json_encode(array_map(static function ($sourceFile) {
|
||||
return [
|
||||
'id' => $sourceFile['id'],
|
||||
'mime' => $sourceFile['mime'],
|
||||
'file' => basename($sourceFile['path']),
|
||||
];
|
||||
}, $sourceFiles))
|
||||
);
|
||||
|
||||
$tempDir = rtrim(WRITEPATH, '/\\') . DIRECTORY_SEPARATOR . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
|
||||
if (! is_dir($tempDir)) {
|
||||
@mkdir($tempDir, 0775, true);
|
||||
}
|
||||
$mpdfCacheDir = $tempDir . DIRECTORY_SEPARATOR . 'mpdf';
|
||||
if (! is_dir($mpdfCacheDir)) {
|
||||
@mkdir($mpdfCacheDir, 0775, true);
|
||||
}
|
||||
|
||||
$mergedName = 'merged_' . $ticket_master_id . '_' . time() . '_' . bin2hex(random_bytes(5)) . '.pdf';
|
||||
$mergedPath = $uploadDir . $mergedName;
|
||||
@ -112,18 +137,20 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
]);
|
||||
|
||||
$totalPages = 0;
|
||||
foreach ($sourcePaths as $src) {
|
||||
foreach ($sourceFiles as $sourceFile) {
|
||||
$src = $sourceFile['path'];
|
||||
try {
|
||||
$pageCount = $mpdf->setSourceFile($src);
|
||||
for ($p = 1; $p <= $pageCount; $p++) {
|
||||
$tplId = $mpdf->importPage($p);
|
||||
$size = $mpdf->getTemplateSize($tplId);
|
||||
$mpdf->AddPageByArray([
|
||||
'orientation' => ($size['width'] > $size['height']) ? 'L' : 'P',
|
||||
'sheet-size' => [$size['width'], $size['height']],
|
||||
]);
|
||||
$mpdf->useTemplate($tplId);
|
||||
$totalPages++;
|
||||
if ($sourceFile['mime'] === 'application/pdf') {
|
||||
$pageCount = merge_ticket_pdf_add_pdf_pages($mpdf, $src, $tempDir);
|
||||
$totalPages += $pageCount;
|
||||
log_message('error', "merge_ticket_pdfs | added PDF | file={$src} | pages={$pageCount}");
|
||||
} elseif (in_array($sourceFile['mime'], ['image/jpeg', 'image/png'], true)) {
|
||||
if (merge_ticket_pdf_add_image_page($mpdf, $src)) {
|
||||
$totalPages++;
|
||||
log_message('error', "merge_ticket_pdfs | added image | file={$src} | mime={$sourceFile['mime']}");
|
||||
}
|
||||
} else {
|
||||
log_message('error', "merge_ticket_pdfs | unsupported source mime {$sourceFile['mime']} | {$src}");
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', "merge_ticket_pdfs | failed to import {$src} | " . $e->getMessage());
|
||||
@ -131,7 +158,7 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
}
|
||||
|
||||
if ($totalPages === 0) {
|
||||
$result['message'] = 'All source PDFs failed to import';
|
||||
$result['message'] = 'All source PDF/image files failed to import';
|
||||
return $result;
|
||||
}
|
||||
|
||||
@ -192,3 +219,173 @@ if (! function_exists('merge_ticket_pdfs')) {
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('merge_ticket_pdf_add_pdf_pages')) {
|
||||
/**
|
||||
* Import PDF pages. If FPDI cannot import the source (commonly encrypted
|
||||
* PDFs), normalize through Ghostscript and retry.
|
||||
*/
|
||||
function merge_ticket_pdf_add_pdf_pages(Mpdf $mpdf, string $pdfPath, string $tempDir): int
|
||||
{
|
||||
try {
|
||||
return merge_ticket_pdf_import_pdf_pages($mpdf, $pdfPath);
|
||||
} catch (\Throwable $e) {
|
||||
log_message('error', "merge_ticket_pdfs | direct PDF import failed | file={$pdfPath} | " . $e->getMessage());
|
||||
|
||||
$normalizedPath = merge_ticket_pdf_normalize_with_ghostscript($pdfPath, $tempDir);
|
||||
if ($normalizedPath === null) {
|
||||
throw $e;
|
||||
}
|
||||
|
||||
try {
|
||||
$pageCount = merge_ticket_pdf_import_pdf_pages($mpdf, $normalizedPath);
|
||||
log_message('error', "merge_ticket_pdfs | Ghostscript normalized PDF imported | original={$pdfPath} | normalized={$normalizedPath} | pages={$pageCount}");
|
||||
@unlink($normalizedPath);
|
||||
return $pageCount;
|
||||
} catch (\Throwable $normalizedError) {
|
||||
@unlink($normalizedPath);
|
||||
throw $normalizedError;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('merge_ticket_pdf_import_pdf_pages')) {
|
||||
function merge_ticket_pdf_import_pdf_pages(Mpdf $mpdf, string $pdfPath): int
|
||||
{
|
||||
$pageCount = $mpdf->setSourceFile($pdfPath);
|
||||
for ($p = 1; $p <= $pageCount; $p++) {
|
||||
$tplId = $mpdf->importPage($p);
|
||||
$mpdf->AddPage();
|
||||
// adjustPageSize=true makes the output page match the imported page.
|
||||
$mpdf->useTemplate($tplId, 0, 0, null, null, true);
|
||||
}
|
||||
|
||||
return $pageCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('merge_ticket_pdf_normalize_with_ghostscript')) {
|
||||
function merge_ticket_pdf_normalize_with_ghostscript(string $pdfPath, string $tempDir): ?string
|
||||
{
|
||||
$ghostscript = is_executable('/usr/bin/gs') ? '/usr/bin/gs' : trim((string) @shell_exec('command -v gs 2>/dev/null'));
|
||||
if ($ghostscript === '' || ! is_executable($ghostscript)) {
|
||||
log_message('error', "merge_ticket_pdfs | Ghostscript not available for PDF normalization | file={$pdfPath}");
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalizedPath = rtrim($tempDir, '/\\') . DIRECTORY_SEPARATOR . 'normalized_' . uniqid('', true) . '.pdf';
|
||||
$cmd = escapeshellarg($ghostscript)
|
||||
. ' -q -dNOPAUSE -dBATCH -dSAFER -sDEVICE=pdfwrite -dCompatibilityLevel=1.4'
|
||||
. ' -sOutputFile=' . escapeshellarg($normalizedPath)
|
||||
. ' ' . escapeshellarg($pdfPath)
|
||||
. ' 2>&1';
|
||||
|
||||
$output = [];
|
||||
$exitCode = 1;
|
||||
@exec($cmd, $output, $exitCode);
|
||||
|
||||
if ($exitCode !== 0 || ! is_file($normalizedPath) || filesize($normalizedPath) <= 0) {
|
||||
log_message('error', 'merge_ticket_pdfs | Ghostscript normalization failed | file=' . $pdfPath . ' | exit=' . $exitCode . ' | output=' . implode(' ', $output));
|
||||
@unlink($normalizedPath);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $normalizedPath;
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('merge_ticket_pdf_resolve_mime')) {
|
||||
/**
|
||||
* Resolve file type from stored MIME, filesystem MIME, and extension.
|
||||
* Some uploads can be saved as image/jpg, empty MIME, or octet-stream in DB.
|
||||
*/
|
||||
function merge_ticket_pdf_resolve_mime(string $path, string $storedMime = ''): string
|
||||
{
|
||||
$storedMime = strtolower(trim($storedMime));
|
||||
if ($storedMime === 'image/jpg') {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if (in_array($storedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) {
|
||||
return $storedMime;
|
||||
}
|
||||
|
||||
$detectedMime = '';
|
||||
if (function_exists('mime_content_type')) {
|
||||
$detectedMime = strtolower((string) @mime_content_type($path));
|
||||
if ($detectedMime === 'image/jpg') {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if (in_array($detectedMime, ['application/pdf', 'image/jpeg', 'image/png'], true)) {
|
||||
return $detectedMime;
|
||||
}
|
||||
}
|
||||
|
||||
$ext = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
if ($ext === 'pdf') {
|
||||
return 'application/pdf';
|
||||
}
|
||||
if (in_array($ext, ['jpg', 'jpeg'], true)) {
|
||||
return 'image/jpeg';
|
||||
}
|
||||
if ($ext === 'png') {
|
||||
return 'image/png';
|
||||
}
|
||||
|
||||
return $detectedMime ?: ($storedMime ?: 'application/octet-stream');
|
||||
}
|
||||
}
|
||||
|
||||
if (! function_exists('merge_ticket_pdf_add_image_page')) {
|
||||
/**
|
||||
* Add an uploaded image as a single PDF page, preserving portrait/landscape
|
||||
* orientation and fitting the image proportionally inside the page.
|
||||
*/
|
||||
function merge_ticket_pdf_add_image_page(Mpdf $mpdf, string $imagePath): bool
|
||||
{
|
||||
$imageInfo = @getimagesize($imagePath);
|
||||
$ext = strtolower(pathinfo($imagePath, PATHINFO_EXTENSION));
|
||||
$imageType = $ext === 'png' || ((int) ($imageInfo[2] ?? 0) === IMAGETYPE_PNG) ? 'png' : 'jpg';
|
||||
|
||||
$imageWidthPx = (int) ($imageInfo[0] ?? 0);
|
||||
$imageHeightPx = (int) ($imageInfo[1] ?? 0);
|
||||
$hasSize = $imageWidthPx > 0 && $imageHeightPx > 0;
|
||||
$isLandscape = $hasSize && $imageWidthPx > $imageHeightPx;
|
||||
|
||||
$pageWidth = $isLandscape ? 297 : 210;
|
||||
$pageHeight = $isLandscape ? 210 : 297;
|
||||
$margin = 0;
|
||||
|
||||
$availableWidth = $pageWidth - ($margin * 2);
|
||||
$availableHeight = $pageHeight - ($margin * 2);
|
||||
if ($hasSize) {
|
||||
$scale = min($availableWidth / $imageWidthPx, $availableHeight / $imageHeightPx);
|
||||
$drawWidth = $imageWidthPx * $scale;
|
||||
$drawHeight = $imageHeightPx * $scale;
|
||||
$x = ($pageWidth - $drawWidth) / 2;
|
||||
$y = ($pageHeight - $drawHeight) / 2;
|
||||
} else {
|
||||
// Some PNGs fail getimagesize(), but mPDF can still render them.
|
||||
// Use a portrait A4 fallback and let mPDF calculate image height.
|
||||
log_message('error', "merge_ticket_pdfs | image size unavailable, using fallback page | {$imagePath}");
|
||||
$drawWidth = $availableWidth;
|
||||
$drawHeight = 0;
|
||||
$x = $margin;
|
||||
$y = $margin;
|
||||
}
|
||||
|
||||
$mpdf->AddPageByArray([
|
||||
'orientation' => $isLandscape ? 'L' : 'P',
|
||||
'sheet-size' => 'A4',
|
||||
'margin-left' => 0,
|
||||
'margin-right' => 0,
|
||||
'margin-top' => 0,
|
||||
'margin-bottom' => 0,
|
||||
'margin-header' => 0,
|
||||
'margin-footer' => 0,
|
||||
]);
|
||||
|
||||
$mpdf->Image($imagePath, $x, $y, $drawWidth, $drawHeight, $imageType);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -121,5 +121,41 @@ class PTCOShareDetailsModel extends Model
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
public function getNonReconcileredPolicyTransactionByPolicyAndEndorsement(string $insurer_id, string $insurer_branch_id, array $policy_no)
|
||||
{
|
||||
$builder = $this->db->table('pt_co_share_details pt_co')
|
||||
->select("
|
||||
pt_co.id,
|
||||
pt_co.pt_id,
|
||||
pt_co.exp_amt,
|
||||
pt.id AS policy_transaction_id,
|
||||
pt.endorsement_no,
|
||||
c.client_name,
|
||||
pt.created_at,
|
||||
pt_co.insurer_id,
|
||||
pt_co.insurer_branch_id,
|
||||
pt.policy_no,
|
||||
pt.policy_issue_date,
|
||||
pt.policy_start_date,
|
||||
pt.policy_end_date,
|
||||
pt.status,
|
||||
pt_co.bp_amt,
|
||||
pt_co.tp_amt,
|
||||
pt_co.tep_amt,
|
||||
pt_co.exp_amt,
|
||||
pt_co.statement_id
|
||||
")
|
||||
->join('policy_transaction pt', 'pt_co.pt_id = pt.id')
|
||||
->join('clients c', 'pt.client_id = c.id')
|
||||
->where('pt_co.is_active', 1)
|
||||
->where('pt.is_active', 1)
|
||||
->where('pt_co.insurer_id', $insurer_id)
|
||||
->where('pt_co.insurer_branch_id', $insurer_branch_id)
|
||||
->whereIn('pt.policy_no', $policy_no);
|
||||
|
||||
|
||||
return $builder->get()->getResultArray();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -197,6 +197,14 @@ class SalesActivityModel extends Model
|
||||
$builder->whereIn('sales_activities.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$builder->where('sales_activities.created_at >=', $filters['date_from']);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$builder->where('sales_activities.created_at <=', $filters['date_to']);
|
||||
}
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
// $builder->groupStart()
|
||||
$this->groupStart()
|
||||
@ -243,6 +251,12 @@ class SalesActivityModel extends Model
|
||||
$ids = is_array($filters['assigned_to']) ? $filters['assigned_to'] : explode(',', $filters['assigned_to']);
|
||||
$query->whereIn('assigned_to', $ids);
|
||||
}
|
||||
if (!empty($filters['date_from'])) {
|
||||
$query->where('created_at >=', $filters['date_from']);
|
||||
}
|
||||
if (!empty($filters['date_to'])) {
|
||||
$query->where('created_at <=', $filters['date_to']);
|
||||
}
|
||||
$counts[$status] = $query->countAllResults();
|
||||
}
|
||||
$counts['all'] = $counts['pending'] + $counts['completed'];
|
||||
|
||||
@ -112,6 +112,14 @@ class SalesActualLeadModel extends Model
|
||||
$this->whereIn('sales_actual_leads.assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$this->where('sales_actual_leads.created_at >=', $filters['date_from']);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$this->where('sales_actual_leads.created_at <=', $filters['date_to']);
|
||||
}
|
||||
|
||||
$this->orderBy('sales_actual_leads.created_at', 'DESC');
|
||||
|
||||
if (!empty($filters['search'])) {
|
||||
@ -153,6 +161,14 @@ class SalesActualLeadModel extends Model
|
||||
$countQuery->whereIn('assigned_to', $assignedToIds);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_from'])) {
|
||||
$countQuery->where('created_at >=', $filters['date_from']);
|
||||
}
|
||||
|
||||
if (!empty($filters['date_to'])) {
|
||||
$countQuery->where('created_at <=', $filters['date_to']);
|
||||
}
|
||||
|
||||
$counts[$status] = $countQuery->where('status', $status)->countAllResults();
|
||||
}
|
||||
|
||||
|
||||
@ -298,6 +298,7 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form class="parsley-examples" id="uploadForm" action="<?php echo base_url() . 'employee/upload' ?>"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<input type="hidden" id="file_client_id" name="client_id">
|
||||
<input type="hidden" id="file_policy_id" name="policy_id">
|
||||
@ -439,6 +440,9 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
title: "Deleted!",
|
||||
icon: "success"
|
||||
});
|
||||
|
||||
window.location.reload(true);
|
||||
|
||||
} else if (response.code === 404 && response.dataStatus === false) {
|
||||
console.error('No data found', response);
|
||||
handleNoDataFound(response, fileId);
|
||||
@ -455,10 +459,8 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
console.error(xhr.responseText);
|
||||
console.error(status, error);
|
||||
|
||||
setTimeout(function() {
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, 1000);
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
|
||||
console.error('Error fetching data from API:', error);
|
||||
toastr.error('Something went wrong! Try later', 'Error');
|
||||
@ -501,6 +503,9 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
title: "Deleted!",
|
||||
icon: "success"
|
||||
});
|
||||
|
||||
window.location.reload(true);
|
||||
|
||||
} else if (response.code === 404 && response.dataStatus === false) {
|
||||
console.error('No data found', response);
|
||||
Swal.fire({
|
||||
@ -537,12 +542,14 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
}
|
||||
}
|
||||
|
||||
$('#uploadForm').submit(function() {
|
||||
$('#uploadForm').submit(function(event) {
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
var isValid = $('#uploadForm').parsley().validate();
|
||||
if (!isValid) {
|
||||
console.log('Form is Empty', 'Warning');
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create FormData object
|
||||
@ -595,6 +602,8 @@ $fl_col_width_px = nhance_dt_column_widths_px($fl_header_labels, $fl_col_max_len
|
||||
window.location.reload(true);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
})
|
||||
|
||||
$('body').on('click', '.reload', function() {
|
||||
|
||||
@ -1358,10 +1358,17 @@
|
||||
toastr.error(response.message || 'Unable to fetch data', 'Error');
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
|
||||
}, function(xhr, status, error) {
|
||||
console.error('Error fetching data:', error);
|
||||
console.error(xhr.responseText);
|
||||
toastr.error('An error occurred while fetching TPA ID.', 'Error');
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
@ -1421,10 +1428,18 @@
|
||||
toastr.error(response.message || 'Unable to fetch data', 'Error');
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
|
||||
}, function(xhr, status, error) {
|
||||
console.error('Error fetching data:', error);
|
||||
console.error(xhr.responseText);
|
||||
toastr.error('An error occurred while pushing.', 'Error');
|
||||
|
||||
setTimeout(function() {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -302,7 +302,15 @@ $isl_col_width_px = nhance_dt_column_widths_px($isl_header_labels, $isl_col_max_
|
||||
<td><?php echo $row['short_name'] . '-' . $row['branch_code']; ?></td>
|
||||
<td><?php echo change_date_format($row['month'], 'Y-m-d', 'M-Y'); ?></td>
|
||||
<td><?php echo $row['stmt_sno'] ?> </td>
|
||||
<td><?php echo $row['file_name'] ?> </td>
|
||||
<td>
|
||||
<?php if (!empty($row['file_name'])) { ?>
|
||||
<a href="<?php echo base_url('policy_tranction/statement/downloadInsurerStatement/' . $row['id']); ?>" class="text-primary" title="Download <?php echo esc($row['file_name']); ?>">
|
||||
<?php echo esc($row['file_name']); ?>
|
||||
</a>
|
||||
<?php } else { ?>
|
||||
-
|
||||
<?php } ?>
|
||||
</td>
|
||||
<td><?php echo $row['line_items'] ?></td>
|
||||
<td><?php echo $row['file_status'];
|
||||
if ($row['file_status'] == 'failed') {
|
||||
|
||||
@ -166,6 +166,7 @@
|
||||
enctype="multipart/form-data">
|
||||
|
||||
<input type="hidden" name="id" id="leads_primarykey">
|
||||
<input type="hidden" name="lead_form_type" id="lead_form_type_id" value="<?= isset($selected_lead_type) ? $selected_lead_type : 1 ?>">
|
||||
<input type="hidden" name="actual_lead_id" id="actual_lead_id" value="<?= isset($actual_lead_id) ? $actual_lead_id : 0 ?>">
|
||||
|
||||
|
||||
@ -219,7 +220,7 @@
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h4 class="card-title mb-0">Client Information</h4>
|
||||
<div class="form-group freshFields mb-0 d-flex align-items-center">
|
||||
<div class="form-group freshFields existing-client-toggle mb-0 d-flex align-items-center">
|
||||
<label for="exixting_client" class="mb-0 mr-2">Existing Client</label>
|
||||
<label class="switch mb-0">
|
||||
<input id="exixting_client" type="checkbox" name="exixting_client">
|
||||
@ -480,8 +481,30 @@
|
||||
<script>
|
||||
var actualLeadClientName = '';
|
||||
var actualLeadShortName = '';
|
||||
var actualLeadGstNumber = '';
|
||||
var actualLeadContactDetails = null;
|
||||
let claimIndex = 1;
|
||||
|
||||
function restoreActualLeadGstNumber() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) {
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreActualLeadContactDetails(force = false) {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadContactDetails) {
|
||||
if (force || !$('#contact_person_name').val()) {
|
||||
$('#contact_person_name').val(actualLeadContactDetails.name || '');
|
||||
}
|
||||
if (force || !$('#contact_person_mobile').val()) {
|
||||
$('#contact_person_mobile').val(actualLeadContactDetails.mobile || '');
|
||||
}
|
||||
if (force || !$('#contact_person_email').val()) {
|
||||
$('#contact_person_email').val(actualLeadContactDetails.email || '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
var policyStart = $(".policy_start_date");
|
||||
var policyEnd = $(".policy_end_date");
|
||||
@ -744,7 +767,7 @@
|
||||
|
||||
}
|
||||
|
||||
leadTypeBsedHideAndShow(lead_type)
|
||||
leadTypeBsedHideAndShow(lead_type, false, policy_type_id)
|
||||
|
||||
|
||||
if (lead_type == 1) {
|
||||
@ -822,11 +845,15 @@
|
||||
$('#lost_reason').val(res.data.lost_reason || '');
|
||||
$('#notes').val(res.data.notes);
|
||||
|
||||
let existingClientId = res.data.client_id || res.data.is_client_created || 0;
|
||||
const isExistingClient = res.data.client_id !== undefined
|
||||
&& res.data.client_id !== null
|
||||
&& String(res.data.client_id) !== ''
|
||||
&& String(res.data.client_id) !== '0';
|
||||
let existingClientId = isExistingClient ? res.data.client_id : 0;
|
||||
console.log('existingClientId', existingClientId);
|
||||
|
||||
if (lead_type == 3) {
|
||||
if (existingClientId && existingClientId != 0) {
|
||||
if (lead_type == 1 || lead_type == 3) {
|
||||
if (isExistingClient) {
|
||||
$('#exixting_client').prop('checked', true).trigger('change');
|
||||
} else {
|
||||
$('#exixting_client').prop('checked', false).trigger('change');
|
||||
@ -851,6 +878,7 @@
|
||||
.contact_person_email);
|
||||
$('#policy_type_id_1').val(res.data.policy_type_id)
|
||||
.select2();
|
||||
togglePolicyDateFields(lead_type, res.data.policy_type_id);
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
}, 1000)
|
||||
@ -1093,6 +1121,7 @@
|
||||
|
||||
var lead_type = $('#lead_type').val();
|
||||
console.log("lead_type", lead_type);
|
||||
togglePolicyDateFields(lead_type);
|
||||
|
||||
let url = '<?= base_url('util/getPolicyTypeFields/') ?>';
|
||||
|
||||
@ -1133,7 +1162,7 @@
|
||||
|
||||
if (lead_type == 1) {
|
||||
$('.claim-row').hide();
|
||||
} else {
|
||||
} else if (!shouldShowClaimHistorySwitch(lead_type, policy_type_id)) {
|
||||
$('.claim-row').show();
|
||||
|
||||
if (policy_type_id == 1) {
|
||||
@ -1145,6 +1174,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
if ($('#claim_history').length && typeof claimHistoryToggle === 'function') {
|
||||
claimHistoryToggle();
|
||||
}
|
||||
|
||||
if (lead_type != 1 && policy_type_id != 1 && policy_type_id != 6 && policy_type_id != 7) {
|
||||
if (lead_type == 1) {
|
||||
updateRenewalFields(dataIncrement);
|
||||
@ -1281,7 +1314,7 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<div class="form-group col-md-4 policy-date-field">
|
||||
<label for="policy_start_date">Date of Commencement <span class="text-danger">*</span></label>
|
||||
<div class="input-icon">
|
||||
<input type="text" class="form-control policy_start_date" id="policy_start_date_${increment}" name="policy_start_date[]" placeholder="Enter DOC" onchange="calculatePolicyMetrics(this)" autocomplete="off" >
|
||||
@ -1290,7 +1323,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-4">
|
||||
<div class="form-group col-md-4 policy-date-field">
|
||||
<label for="policy_end_date">Date of Expiry <span class="text-danger">*</span></label>
|
||||
<div class="input-icon">
|
||||
<input type="text" class="form-control policy_end_date" id="policy_end_date_${increment}" name="policy_end_date[]" placeholder="Enter DOE" autocomplete="off">
|
||||
@ -1776,6 +1809,8 @@
|
||||
const finyearJsonString = gatherClaimExperienceData(policy_type_ids);
|
||||
console.log('finyearJsonString', finyearJsonString);
|
||||
formData.append('finyear', finyearJsonString);
|
||||
let claimHistoryStatus = $("#claim_history").length > 0 && $("#claim_history").prop("checked") ? 1 : 0;
|
||||
formData.set('claim_history', claimHistoryStatus);
|
||||
|
||||
$.ajax({
|
||||
data: formData,
|
||||
@ -1898,6 +1933,7 @@
|
||||
$('#policy_start_date').val("");
|
||||
$('#policy_end_date').val("");
|
||||
$("#appendArea_1").empty();
|
||||
togglePolicyDateFields(value);
|
||||
|
||||
// $('#source_policy_start_date').val("");
|
||||
// $('#source_policy_end_date').val("");
|
||||
@ -1938,6 +1974,8 @@
|
||||
// --- Re-populate Actual Lead data if it was cleared ---
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0) {
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
// Re-generate short name from the stored client name to ensure consistency
|
||||
if (actualLeadClientName.trim() !== '') {
|
||||
let baseName = actualLeadClientName
|
||||
@ -2019,6 +2057,12 @@
|
||||
|
||||
let claimData = [];
|
||||
|
||||
if ($("#claim_history").length > 0 && !$("#claim_history").prop("checked")) {
|
||||
return JSON.stringify({
|
||||
"finyear": claimData
|
||||
});
|
||||
}
|
||||
|
||||
$(".claim-row").each(function() {
|
||||
|
||||
let year = $(this).find("[name='first_year[]']").val();
|
||||
@ -2148,6 +2192,14 @@
|
||||
// toggleRequiredFields();
|
||||
// }
|
||||
|
||||
function isClaimHistoryPolicyType(policyTypeId) {
|
||||
return ['1', '6', '7'].includes(String(policyTypeId));
|
||||
}
|
||||
|
||||
function shouldShowClaimHistorySwitch(leadType, policyTypeId) {
|
||||
return (leadType == 2 || leadType == 3) && isClaimHistoryPolicyType(policyTypeId);
|
||||
}
|
||||
|
||||
function appendThreeYearsClaims(count) {
|
||||
|
||||
// let count = $('#appendAreaForClaim').data('count');
|
||||
@ -2156,15 +2208,23 @@
|
||||
let policy_type_id = $('#policy_type_id_' + count).val()
|
||||
console.log('policy_type_id', policy_type_id);
|
||||
|
||||
let lead_type = $('#lead_type').val();
|
||||
let showClaimHistorySwitch = shouldShowClaimHistorySwitch(lead_type, policy_type_id);
|
||||
|
||||
console.log('claimIndex from parent', claimIndex);
|
||||
let increment = claimIndex;
|
||||
|
||||
let claimsFields = `
|
||||
let claimsFields = `${showClaimHistorySwitch && !document.querySelector('#claim_history') ? `<div class="custom-control custom-switch">
|
||||
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" checked>
|
||||
<label class="custom-control-label" for="claim_history">Claims History</label>
|
||||
</div><br>` : ``}`;
|
||||
|
||||
claimsFields += `
|
||||
<div class="row claim-row">
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_year_${increment}">Year<span class="text-danger">*</span></label>
|
||||
<select class="form-control first_year_" id="first_year_${increment}" name="first_year[]">
|
||||
<select class="form-control first_year_ claim-input" id="first_year_${increment}" name="first_year[]">
|
||||
<option value="">Select Year</option>
|
||||
<?php foreach ($lastFiveYears as $year) {
|
||||
echo "<option value='$year'>$year</option>";
|
||||
@ -2174,18 +2234,18 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="emp_id_${increment}">Emp ID<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="emp_id_${increment}" name="emp_id[]">
|
||||
<input type="text" class="form-control claim-input" id="emp_id_${increment}" name="emp_id[]">
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="emp_name_${increment}">Employee Name<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="emp_name_${increment}" name="emp_name[]">
|
||||
<input type="text" class="form-control claim-input" id="emp_name_${increment}" name="emp_name[]">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="gender_${increment}">Gender<span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="gender_${increment}" name="gender[]">
|
||||
<select class="form-control claim-input" id="gender_${increment}" name="gender[]">
|
||||
<option value="">Select Gender</option>
|
||||
<option value="Female">Female</option>
|
||||
<option value="Male">Male</option>
|
||||
@ -2194,25 +2254,25 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="designation_${increment}">Designation <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="designation_${increment}" name="designation[]">
|
||||
<input type="text" class="form-control claim-input" id="designation_${increment}" name="designation[]">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="sum_insured_${increment}">Sum Insured <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="sum_insured_${increment}" name="sum_insured[]">
|
||||
<input type="text" class="form-control claim-input" id="sum_insured_${increment}" name="sum_insured[]">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_death_date_${increment}">Date of Death<span class="text-danger">*</span></label>
|
||||
<div class="input-icon">
|
||||
<input type="text" class="form-control death_date flatpickr-date" id="first_death_date_${increment}" name="first_death_date[]" autocomplete="off">
|
||||
<input type="text" class="form-control death_date flatpickr-date claim-input" id="first_death_date_${increment}" name="first_death_date[]" autocomplete="off">
|
||||
<i class="mdi mdi-calendar-blank-outline additional-icon"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_cause_of_death_${increment}">Nature/Cause Of Death <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="first_cause_of_death_${increment}" name="first_cause_of_death[]">
|
||||
<select class="form-control claim-input" id="first_cause_of_death_${increment}" name="first_cause_of_death[]">
|
||||
<option value="">Select Cause of Death</option>
|
||||
<?php foreach ($causeOfDeath as $cause => $death_value) {
|
||||
echo "<option value='$cause'>$death_value</option>";
|
||||
@ -2222,7 +2282,7 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_claim_amount_${increment}">Claim/Settled Amount<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="first_claim_amount_${increment}" name="first_claim_amount[]">
|
||||
<input type="text" class="form-control claim-input" id="first_claim_amount_${increment}" name="first_claim_amount[]">
|
||||
</div>
|
||||
|
||||
<!-- <div class="form-group col-md-2">
|
||||
@ -2283,7 +2343,13 @@
|
||||
maxDate: 'today', // Optional: disable future dates
|
||||
});
|
||||
|
||||
if (showClaimHistorySwitch) {
|
||||
claimHistoryToggle();
|
||||
}
|
||||
|
||||
toggleRequiredFields();
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
@ -2297,7 +2363,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
function leadTypeBsedHideAndShow(value, resetValues = false) {
|
||||
function claimHistoryToggle() {
|
||||
let claimHistoryStatus = $("#claim_history").prop("checked") ? 1 : 0;
|
||||
|
||||
if (claimHistoryStatus == 1) {
|
||||
$(".claim-row").show();
|
||||
$(".claim-input").attr("required", true);
|
||||
} else {
|
||||
$(".claim-row").hide();
|
||||
$(".claim-input").removeAttr("required").val("");
|
||||
$(".claim-input").each(function() {
|
||||
if ($(this).hasClass('select2-hidden-accessible')) {
|
||||
$(this).val(null).trigger('change');
|
||||
}
|
||||
if ($(this).parsley) {
|
||||
$(this).parsley().reset();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleRequiredFields();
|
||||
}
|
||||
|
||||
function togglePolicyDateFields(value, fallbackPolicyTypeId = null) {
|
||||
const policyTypesToHide = ['1', '2', '3', '4', '5', '6', '7'];
|
||||
|
||||
$('.dynamic-form-row').each(function(index) {
|
||||
const selectedPolicyTypeId = $(this).find('[id^="policy_type_id_"]').val();
|
||||
const policyTypeId = String(selectedPolicyTypeId || (index === 0 ? fallbackPolicyTypeId || '' : ''));
|
||||
const shouldHide = value == 1 && policyTypesToHide.includes(policyTypeId);
|
||||
|
||||
$(this).find('.policy-date-field').toggle(!shouldHide);
|
||||
});
|
||||
}
|
||||
|
||||
function leadTypeBsedHideAndShow(value, resetValues = false, fallbackPolicyTypeId = null) {
|
||||
|
||||
togglePolicyDateFields(value, fallbackPolicyTypeId);
|
||||
if (value == 2) {
|
||||
$('.existing-client-toggle').removeClass('d-flex').addClass('d-none');
|
||||
} else {
|
||||
$('.existing-client-toggle').removeClass('d-none').addClass('d-flex');
|
||||
}
|
||||
|
||||
if (value == 1) {
|
||||
$('.btnDiv').show();
|
||||
@ -2337,7 +2444,6 @@
|
||||
} else if (value == 3) {
|
||||
|
||||
$('.btnDiv').hide();
|
||||
$('.claim-row').show();
|
||||
|
||||
$('.emp_title_text').text('No of Employees at Inception')
|
||||
$('.depnd_title_text').text(' No of Dependents at Inception')
|
||||
@ -2363,6 +2469,10 @@
|
||||
|
||||
|
||||
$('.rolloverhide').hide();
|
||||
$('.rolloverhide').find('select, input, textarea').removeAttr('required').prop('required', false);
|
||||
$('#source_policy_id, #source_policy_start_date, #source_policy_end_date')
|
||||
.removeAttr('required')
|
||||
.prop('required', false);
|
||||
|
||||
|
||||
if (resetValues) {
|
||||
@ -2447,7 +2557,13 @@
|
||||
|
||||
}
|
||||
|
||||
if ($('#claim_history').length && typeof claimHistoryToggle === 'function') {
|
||||
claimHistoryToggle();
|
||||
}
|
||||
|
||||
toggleRequiredFields();
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
function toggleRequiredFields() {
|
||||
@ -2561,6 +2677,8 @@
|
||||
$("#client_name").prop("required", true);
|
||||
$('#client_name').closest('.form-group').show();
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
});
|
||||
|
||||
|
||||
@ -2576,7 +2694,8 @@
|
||||
actualLeadClientName = actual_lead_client_details.company_name || '';
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || '';
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') {
|
||||
$('#client_type').val(String(actual_lead_client_details.client_type));
|
||||
}
|
||||
@ -2627,12 +2746,8 @@
|
||||
// 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 || '');
|
||||
actualLeadContactDetails = actual_lead_contact_person_details;
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -831,22 +831,31 @@ if (isset($selected_lead_type)) {
|
||||
|
||||
let policy_type_id = data.policy_type_id || null;
|
||||
let lead_type = data.lead_type || null;
|
||||
const isExistingClient = data.client_id !== undefined
|
||||
&& data.client_id !== null
|
||||
&& String(data.client_id) !== ''
|
||||
&& String(data.client_id) !== '0';
|
||||
|
||||
if ([1, 6, 7].includes(policy_type_id)) {
|
||||
let newId = 'appendAreaForClaim_' + dataIncrement;
|
||||
$('#appendAreaForClaim').attr('id', newId);
|
||||
}
|
||||
|
||||
leadTypeBsedHideAndShow(lead_type);
|
||||
leadTypeBsedHideAndShow(lead_type, false, policy_type_id);
|
||||
if ($('#claim_history').length && typeof claimHistoryToggle === 'function') {
|
||||
claimHistoryToggle();
|
||||
}
|
||||
|
||||
$('.loader').fadeIn();
|
||||
$('.loader-mask').fadeIn();
|
||||
|
||||
|
||||
if (lead_type == 1 || lead_type == 3) {
|
||||
$('.claim-row').hide();
|
||||
if (lead_type == 1 || !shouldShowClaimHistorySwitch(lead_type, policy_type_id)) {
|
||||
$('.claim-row').hide();
|
||||
}
|
||||
|
||||
if(data.is_client_created == null || data.is_client_created == 0 || data.is_client_created == ""){
|
||||
if(!isExistingClient){
|
||||
$('#exixting_client').prop('checked', false);
|
||||
$('#exixting_client').prop('disabled', true);
|
||||
|
||||
@ -935,6 +944,10 @@ if (isset($selected_lead_type)) {
|
||||
}
|
||||
}
|
||||
|
||||
if ($('#claim_history').length && typeof claimHistoryToggle === 'function') {
|
||||
claimHistoryToggle();
|
||||
}
|
||||
|
||||
if(lead_type == 3){
|
||||
let incurred_claim_date_id = 'incurred_claim_date';
|
||||
|
||||
@ -1110,7 +1123,12 @@ if (isset($selected_lead_type)) {
|
||||
|
||||
leadTypeBsedHideAndShow(lead_type);
|
||||
|
||||
if(data.client_id == 0 || data.client_id == null){
|
||||
const isExistingClient = data.client_id !== undefined
|
||||
&& data.client_id !== null
|
||||
&& String(data.client_id) !== ''
|
||||
&& String(data.client_id) !== '0';
|
||||
|
||||
if(!isExistingClient){
|
||||
temp_client_id = 0;
|
||||
}else{
|
||||
temp_client_id = data.client_id
|
||||
@ -1118,7 +1136,7 @@ if (isset($selected_lead_type)) {
|
||||
|
||||
if(lead_type == 1 || lead_type == 3){
|
||||
|
||||
if(data.is_client_created == null || data.is_client_created == 0 || data.is_client_created == ""){
|
||||
if(!isExistingClient){
|
||||
$('#exixting_client').prop('checked', false);
|
||||
$('#exixting_client').prop('disabled', true);
|
||||
|
||||
@ -1324,6 +1342,7 @@ if (isset($selected_lead_type)) {
|
||||
$('#contact_person_email').val(data.contact_person_email || '');
|
||||
$('#policy_type_id').val(data.policy_type_id || '').select2();
|
||||
$('#policy_type_id_1').val(data.policy_type_id || '').select2();
|
||||
togglePolicyDateFields(lead_type, data.policy_type_id || '');
|
||||
|
||||
$('.loader').fadeOut();
|
||||
$('.loader-mask').delay(350).fadeOut('slow');
|
||||
|
||||
@ -493,6 +493,22 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
<script>
|
||||
var actualLeadClientName = '';
|
||||
var actualLeadShortName = '';
|
||||
var actualLeadGstNumber = '';
|
||||
var actualLeadContactDetails = null;
|
||||
|
||||
function restoreActualLeadGstNumber() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadGstNumber) {
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
}
|
||||
}
|
||||
|
||||
function restoreActualLeadContactDetails() {
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0 && actualLeadContactDetails) {
|
||||
$('#contact_person_name').val(actualLeadContactDetails.name || '');
|
||||
$('#contact_person_mobile').val(actualLeadContactDetails.mobile || '');
|
||||
$('#contact_person_email').val(actualLeadContactDetails.email || '');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
$('#rfq_qcr_viewers').parsley({
|
||||
@ -561,6 +577,22 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
addFileField(1);
|
||||
})
|
||||
|
||||
function togglePolicyDateFields(value, fallbackPolicyTypeId = null) {
|
||||
const policyTypesToHide = ['1', '2', '3', '4', '5', '6', '7'];
|
||||
const selectedPolicyTypeId = $('#policy_type_id').val();
|
||||
const policyTypeId = String(selectedPolicyTypeId || fallbackPolicyTypeId || '');
|
||||
const shouldHide = value == 1 && policyTypesToHide.includes(policyTypeId);
|
||||
const $policyDateFields = $('#policy_start_date, #policy_end_date').closest('.form-group');
|
||||
|
||||
$policyDateFields.toggle(!shouldHide);
|
||||
|
||||
if (shouldHide) {
|
||||
$policyDateFields.find('input').prop('required', false).val('');
|
||||
} else if (value == 1 || value == 3) {
|
||||
$policyDateFields.find('input').prop('required', true);
|
||||
}
|
||||
}
|
||||
|
||||
function getPolicyTypeFields(input) {
|
||||
|
||||
console.log('getPolicyTypeFields', input);
|
||||
@ -855,6 +887,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
// --- Re-populate Actual Lead data if it was cleared ---
|
||||
if ($('#actual_lead_id').val() && $('#actual_lead_id').val() != 0) {
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
if (actualLeadShortName.trim() !== '') {
|
||||
$('#client_short_name').val(actualLeadShortName);
|
||||
} else if (actualLeadClientName.trim() !== '') {
|
||||
@ -955,6 +989,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
}
|
||||
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
var allContacts = "";
|
||||
function getBranchData(input,inputType) {
|
||||
@ -1348,6 +1384,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
$("#client_id").prop("required", false);
|
||||
$("#client_branch_id").prop("required", false);
|
||||
}
|
||||
restoreActualLeadGstNumber();
|
||||
restoreActualLeadContactDetails();
|
||||
});
|
||||
|
||||
$(document).ready(function() {
|
||||
@ -1362,7 +1400,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
actualLeadClientName = actual_lead_client_details.company_name || '';
|
||||
$('#client_name').val(actualLeadClientName);
|
||||
|
||||
$('#gst').val(actual_lead_client_details.gst_number || '');
|
||||
actualLeadGstNumber = actual_lead_client_details.gst_number || actual_lead_client_details.gst || '';
|
||||
$('#gst').val(actualLeadGstNumber);
|
||||
if (actual_lead_client_details.client_type !== undefined && actual_lead_client_details.client_type !== null && actual_lead_client_details.client_type !== '') {
|
||||
$('#client_type').val(String(actual_lead_client_details.client_type));
|
||||
}
|
||||
@ -1411,12 +1450,8 @@ var pageBackButton = '<a href="<?= base_url('leads/list') ?>" class="topbar-icon
|
||||
// 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 || '');
|
||||
actualLeadContactDetails = actual_lead_contact_person_details;
|
||||
restoreActualLeadContactDetails();
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -41,10 +41,23 @@
|
||||
<hr>
|
||||
|
||||
<?php if(isset($lead_edit_data)) { ?>
|
||||
<?php
|
||||
$savedClaims = !empty($lead_edit_data['fin_years_claims_array']) ? $lead_edit_data['fin_years_claims_array'] : [];
|
||||
$isClaimHistoryChecked = (isset($lead_edit_data['claim_history']) && (int) $lead_edit_data['claim_history'] === 1) || !empty($savedClaims);
|
||||
$showClaimHistorySwitch = isset($lead_edit_data['lead_type'], $lead_edit_data['policy_type_id'])
|
||||
&& in_array((int) $lead_edit_data['lead_type'], [2, 3], true)
|
||||
&& in_array((int) $lead_edit_data['policy_type_id'], [1, 6, 7], true);
|
||||
?>
|
||||
<?php if ($showClaimHistorySwitch) { ?>
|
||||
<div class="custom-control custom-switch">
|
||||
<input type="checkbox" onchange="claimHistoryToggle()" class="custom-control-input" id="claim_history" name="claim_history" <?= $isClaimHistoryChecked ? 'checked' : '' ?>>
|
||||
<label class="custom-control-label" for="claim_history">Claims History</label>
|
||||
</div><br>
|
||||
<?php } ?>
|
||||
<div class="form-row" id="appendAreaForClaim_1">
|
||||
<?php
|
||||
|
||||
$claims = !empty($lead_edit_data['fin_years_claims_array']) ? $lead_edit_data['fin_years_claims_array'] : [ ['year' => '', 'claim_amount' => '', 'status' => '', 'claim_type' => '', 'cause_of_death' => '', 'death_date' => ''] ];
|
||||
$claims = !empty($savedClaims) ? $savedClaims : [ ['year' => '', 'claim_amount' => '', 'status' => '', 'claim_type' => '', 'cause_of_death' => '', 'death_date' => ''] ];
|
||||
|
||||
foreach ($claims as $key => $value) { ?>
|
||||
|
||||
@ -52,7 +65,7 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_year">Year<span class="text-danger">*</span></label>
|
||||
<select class="form-control first_year_" id="first_year" name="first_year[]">
|
||||
<select class="form-control first_year_ claim-input" id="first_year" name="first_year[]">
|
||||
<option value="">Select Year</option>
|
||||
<?php foreach ($lastFiveYears as $year) {
|
||||
$selected = ($year == $value['year']) ? 'selected' : '';
|
||||
@ -63,17 +76,17 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="emp_id">Emp ID<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="emp_id" name="emp_id[]" value="<?= htmlspecialchars(isset($value['emp_id']) ? $value['emp_id'] : '-' ) ?>">
|
||||
<input type="text" class="form-control claim-input" id="emp_id" name="emp_id[]" value="<?= htmlspecialchars(isset($value['emp_id']) ? $value['emp_id'] : '-' ) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="emp_name">Employee Name<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="emp_name" name="emp_name[]" value="<?= htmlspecialchars(isset($value['emp_name']) ? $value['emp_name'] : '-' ) ?>">
|
||||
<input type="text" class="form-control claim-input" id="emp_name" name="emp_name[]" value="<?= htmlspecialchars(isset($value['emp_name']) ? $value['emp_name'] : '-' ) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="gender">Gender<span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="gender" name="gender[]">
|
||||
<select class="form-control claim-input" id="gender" name="gender[]">
|
||||
<option value="">Select Gender</option>
|
||||
<option value="Female" <?= (isset($value['gender']) && $value['gender'] == 'Female') ? 'selected' : '' ?>>Female</option>
|
||||
<option value="Male" <?= (isset($value['gender']) && $value['gender'] == 'Male') ? 'selected' : '' ?>>Male</option>
|
||||
@ -83,22 +96,22 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="designation">Designation <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="designation" name="designation[]" value="<?= htmlspecialchars(isset($value['designation']) ? $value['designation'] : '-' ) ?>">
|
||||
<input type="text" class="form-control claim-input" id="designation" name="designation[]" value="<?= htmlspecialchars(isset($value['designation']) ? $value['designation'] : '-' ) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="sum_insured">Sum Insured <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="sum_insured" name="sum_insured[]" value="<?= htmlspecialchars(isset($value['sum_insured']) ? $value['sum_insured'] : '-' ) ?>">
|
||||
<input type="text" class="form-control claim-input" id="sum_insured" name="sum_insured[]" value="<?= htmlspecialchars(isset($value['sum_insured']) ? $value['sum_insured'] : '-' ) ?>">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_death_date">Date of Death<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control flatpickr-date" id="first_death_date" name="first_death_date[]" value="<?= htmlspecialchars($value['death_date']) ?>" autocomplete="off">
|
||||
<input type="text" class="form-control flatpickr-date claim-input" id="first_death_date" name="first_death_date[]" value="<?= htmlspecialchars($value['death_date']) ?>" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_cause_of_death">Nature/Cause Of Death <span class="text-danger">*</span></label>
|
||||
<select class="form-control" id="first_cause_of_death" name="first_cause_of_death[]">
|
||||
<select class="form-control claim-input" id="first_cause_of_death" name="first_cause_of_death[]">
|
||||
<option value="">Select Cause of Death</option>
|
||||
<?php foreach ($causeOfDeath as $cause => $death_value) {
|
||||
$selected = ($cause == $value['cause_of_death']) ? 'selected' : '';
|
||||
@ -109,7 +122,7 @@
|
||||
|
||||
<div class="form-group col-md-2">
|
||||
<label for="first_claim_amount">Claim/Settled Amount<span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="first_claim_amount" name="first_claim_amount[]" value="<?= htmlspecialchars(isset($value['claim_amount']) ? $value['claim_amount'] : ( isset($value['settled']) ? $value['settled'] : '-' )) ?>">
|
||||
<input type="text" class="form-control claim-input" id="first_claim_amount" name="first_claim_amount[]" value="<?= htmlspecialchars(isset($value['claim_amount']) ? $value['claim_amount'] : ( isset($value['settled']) ? $value['settled'] : '-' )) ?>">
|
||||
</div>
|
||||
|
||||
<!-- <div class="form-group col-md-2">
|
||||
|
||||
@ -15,12 +15,40 @@
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
.tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
|
||||
.tab { padding: 10px 14px; border-radius: 20px; cursor: pointer; font-size: 12px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; }
|
||||
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
|
||||
|
||||
/* Leads Grid */
|
||||
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
|
||||
.lead-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.sales-toolbar { flex-wrap: nowrap; padding: 16px 15px; gap: 12px; overflow-x: auto; }
|
||||
.sales-toolbar .filter-tabs { padding: 0; flex: 0 0 auto; min-width: max-content; overflow: visible; }
|
||||
.sales-toolbar .lead-actions { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; }
|
||||
.sales-toolbar .search-input { width: 230px !important; height: 38px; padding: 9px 12px; border: 1px solid #ddd !important; border-radius: 6px !important; flex: 0 0 230px; }
|
||||
.sales-toolbar .toolbar-select { width: 175px !important; min-width: 175px; padding: 9px 12px; }
|
||||
.sales-toolbar .toolbar-select + .select2-container { width: 175px !important; min-width: 175px; }
|
||||
.sales-toolbar .financial-year-select { width: 122px !important; min-width: 122px; }
|
||||
.sales-toolbar .financial-year-select + .select2-container { width: 122px !important; min-width: 122px; flex: 0 0 122px; }
|
||||
.sales-toolbar .select2-container .select2-selection--single { height: 38px; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; }
|
||||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 36px; }
|
||||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
|
||||
.sales-toolbar #memberFilter + .select2-container { width: 175px !important; min-width: 175px; flex: 0 0 175px; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection--single,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection--single { height: 38px !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 6px !important; display: flex; align-items: center; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__rendered,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__rendered { flex: 1; line-height: 36px !important; padding-left: 12px; padding-right: 28px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow b,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow b { border-width: 4px 3px 0 3px !important; margin-left: -3px; margin-top: -2px; }
|
||||
.sales-toolbar #memberFilter + .select2-container--open .select2-selection__arrow b,
|
||||
.sales-toolbar #financialYearFilter + .select2-container--open .select2-selection__arrow b { border-width: 0 3px 4px 3px !important; }
|
||||
.toolbar-icon-btn { width: 38px; height: 38px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.2s; }
|
||||
.toolbar-icon-btn:hover { transform: translateY(-1px); filter: brightness(0.96); }
|
||||
.toolbar-icon-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(2, 168, 181, 0.25); }
|
||||
.toolbar-icon-btn i { font-size: 20px; line-height: 1; }
|
||||
.toolbar-icon-btn.export-btn { background: #1f9d55; }
|
||||
.toolbar-icon-btn.add-btn { background: #02a8b5; }
|
||||
/* This forces 2 equal columns on desktop */
|
||||
.activities-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; padding: 30px; }
|
||||
|
||||
@ -172,21 +200,35 @@
|
||||
|
||||
<div class="main-content">
|
||||
<hr style="margin-bottom: 0 !important;">
|
||||
<div class="lead-header">
|
||||
<div class="lead-header sales-toolbar">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="filter-tabs">
|
||||
<div class="tab active" data-filter="all" onclick="setFilter('all', this)">All</div>
|
||||
<div class="tab" data-filter="Pending" onclick="setFilter('Pending', this)">Pending</div>
|
||||
<div class="tab" data-filter="Completed" onclick="setFilter('Completed', this)">Completed</div>
|
||||
<div class="tab" data-filter="Pending" onclick="setFilter('pending', this)">Pending</div>
|
||||
<div class="tab" data-filter="Completed" onclick="setFilter('completed', this)">Completed</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openMainActivityModal()">
|
||||
+ Add Activity
|
||||
placeholder="Search activities..." onkeyup="fetchActivities(false)">
|
||||
<select id="memberFilter" class="toolbar-select searchable" onchange="fetchActivities(false)">
|
||||
<option value="" selected>All Member</option>
|
||||
<?php foreach(($sales_manager_with_head ?? []) as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= esc($sm['first_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="financialYearFilter" class="toolbar-select financial-year-select searchable" onchange="fetchActivities(false)">
|
||||
<?php foreach(($fin_years ?? []) as $year): ?>
|
||||
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="toolbar-icon-btn export-btn" id="activityExportExcelBtn" title="Export Excel" aria-label="Export activities">
|
||||
<i class="mdi mdi-file-excel"></i>
|
||||
</button>
|
||||
<button class="toolbar-icon-btn add-btn" onclick="openMainActivityModal()" title="Add Activity" aria-label="Add activity">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -448,12 +490,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
const select2Options = {
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
};
|
||||
if (el.id !== 'memberFilter') {
|
||||
select2Options.placeholder = "Select..";
|
||||
}
|
||||
$(el).select2(select2Options);
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
@ -509,6 +554,7 @@ const activityIcons = {
|
||||
const salesManagerWithHeadIds = <?= json_encode($sales_manager_with_head_ids ?? []) ?>;
|
||||
|
||||
const API = '<?= base_url('sales') ?>';
|
||||
const defaultFinancialYear = '<?= esc($current_fin_year ?? '') ?>';
|
||||
let filter = 'all';
|
||||
let lead_id = null;
|
||||
let global_lead_assigned_to = null;
|
||||
@ -519,6 +565,58 @@ let limit = 10;
|
||||
let currentOffset = 0;
|
||||
const department = '<?= $sales_role ?>';
|
||||
|
||||
function getCurrentFY() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
const startYear = month >= 3 ? year : year - 1;
|
||||
return startYear + '-' + String(startYear + 1);
|
||||
}
|
||||
|
||||
function initSalesToolbarFilters() {
|
||||
const fySelect = document.getElementById('financialYearFilter');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (fySelect) {
|
||||
let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
|
||||
const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
|
||||
if (!matched && fySelect.options.length > 0) {
|
||||
fy = fySelect.options[0].value;
|
||||
}
|
||||
fySelect.value = fy;
|
||||
}
|
||||
|
||||
const statusParam = (params.get('status') || 'all').toLowerCase();
|
||||
const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
|
||||
.find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
|
||||
if (statusTab) {
|
||||
document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
|
||||
statusTab.classList.add('active');
|
||||
filter = statusParam;
|
||||
}
|
||||
|
||||
const memberSelect = document.getElementById('memberFilter');
|
||||
const memberId = params.get('member');
|
||||
if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
|
||||
memberSelect.value = memberId;
|
||||
if (window.jQuery) {
|
||||
$(memberSelect).trigger('change.select2');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedMemberIds(defaultIds) {
|
||||
const memberFilter = document.getElementById('memberFilter');
|
||||
if (memberFilter && memberFilter.value) {
|
||||
return [memberFilter.value];
|
||||
}
|
||||
return Array.isArray(defaultIds) ? defaultIds : [];
|
||||
}
|
||||
|
||||
function getSelectedFinancialYear() {
|
||||
return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
|
||||
}
|
||||
|
||||
|
||||
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
|
||||
|
||||
@ -583,7 +681,11 @@ function setFilter(val, el) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
filter = val;
|
||||
fetchActivities();
|
||||
const searchInput = document.getElementById('mainSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
fetchActivities(false);
|
||||
}
|
||||
|
||||
async function fetchActivities(isLoadMore = false) {
|
||||
@ -592,6 +694,7 @@ async function fetchActivities(isLoadMore = false) {
|
||||
const btnLoadMore = document.getElementById('btn-load-more');
|
||||
const spinner = document.getElementById('load-more-spinner');
|
||||
const text = document.getElementById('load-more-text');
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
|
||||
|
||||
|
||||
// 1. Define the empty state HTML early so we can use it immediately if needed
|
||||
@ -606,7 +709,7 @@ async function fetchActivities(isLoadMore = false) {
|
||||
console.log(salesManagerWithHeadIds);
|
||||
// return;
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerWithHeadIds === 'undefined' || salesManagerWithHeadIds.length === 0) {
|
||||
if (selectedMemberIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -631,11 +734,15 @@ 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 salesManagerWithHeadIds !== 'undefined' && salesManagerWithHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerWithHeadIds.join(',')}`;
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: q,
|
||||
limit,
|
||||
offset: currentOffset,
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
financial_year: getSelectedFinancialYear()
|
||||
});
|
||||
let url = `${API}/activities?${params.toString()}`;
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
|
||||
@ -1289,6 +1396,126 @@ function convertDBFormatted(input) {
|
||||
);
|
||||
}
|
||||
|
||||
function csvEscape(value) {
|
||||
const text = String(value ?? '').replace(/"/g, '""');
|
||||
return `"${text}"`;
|
||||
}
|
||||
|
||||
function formatIndianDate(value) {
|
||||
if (!value) return '';
|
||||
const date = new Date(String(value).replace(' ', 'T'));
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
|
||||
return date.toLocaleString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).replace(',', '').toUpperCase();
|
||||
}
|
||||
|
||||
function downloadCsv(filename, rows) {
|
||||
const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function getExportFilename(response, fallbackFilename) {
|
||||
const disposition = response.headers.get('content-disposition') || '';
|
||||
const match = disposition.match(/filename="?([^"]+)"?/i);
|
||||
return match ? match[1] : fallbackFilename;
|
||||
}
|
||||
|
||||
async function downloadExportFile(url, fallbackFilename) {
|
||||
const response = await fetch(url);
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (!response.ok || contentType.includes('application/json')) {
|
||||
let message = 'Export failed. Please try again.';
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.message || json.messages?.error || message;
|
||||
} catch (err) {
|
||||
// Keep the generic message when the response cannot be parsed.
|
||||
}
|
||||
|
||||
return response.status === 404 ? toastr.warning(message) : toastr.error(message);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = getExportFilename(response, fallbackFilename);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function initActivityExportDateRangePicker() {
|
||||
const $button = $('#activityExportExcelBtn');
|
||||
if (!$button.length) return;
|
||||
|
||||
if (typeof moment === 'undefined' || typeof $.fn.daterangepicker === 'undefined') {
|
||||
$button.on('click', () => toastr.error('Date range picker is not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
$button.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
startDate: moment().startOf('month'),
|
||||
endDate: moment(),
|
||||
maxDate: moment(),
|
||||
opens: 'left',
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: 'DD-MM-YYYY',
|
||||
applyLabel: 'Generate Excel',
|
||||
cancelLabel: 'Cancel'
|
||||
},
|
||||
ranges: {
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment()],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
}
|
||||
});
|
||||
|
||||
$button.on('apply.daterangepicker', function (ev, picker) {
|
||||
exportActivitiesCsv(
|
||||
picker.startDate.format('YYYY-MM-DD'),
|
||||
picker.endDate.format('YYYY-MM-DD')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportActivitiesCsv(fromDate, toDate) {
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerWithHeadIds);
|
||||
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
|
||||
if (!fromDate || !toDate) return toastr.warning('Please select a date range');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: document.getElementById('mainSearch')?.value || '',
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
from_date: fromDate,
|
||||
to_date: toDate
|
||||
});
|
||||
|
||||
await downloadExportFile(
|
||||
`${API}/activities/export?${params.toString()}`,
|
||||
`sales-activities-${fromDate}-to-${toDate}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('load', initActivityExportDateRangePicker);
|
||||
initSalesToolbarFilters();
|
||||
fetchActivities();
|
||||
|
||||
</script>
|
||||
@ -67,6 +67,7 @@
|
||||
.card-hero:nth-child(3)::before { background: linear-gradient(90deg, #fda085, #f6d365); }
|
||||
.card-hero:nth-child(4)::before { background: linear-gradient(90deg, #43e97b, #38f9d7); }
|
||||
.card-hero:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0,0,0,0.09); }
|
||||
.card-hero.stat-link { cursor: pointer; }
|
||||
.stat-icon { width: 42px; height: 42px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; margin-bottom: 14px; }
|
||||
.stat-val { font-size: 30px; font-weight: 800; color: #1a202c; line-height: 1; }
|
||||
.stat-label { color: #8896aa; font-size: 13px; margin-top: 6px; font-weight: 600; }
|
||||
@ -103,6 +104,8 @@
|
||||
th { text-align: left; padding: 10px 12px; color: #8896aa; font-size: 11px; text-transform: uppercase; letter-spacing: 0.07em; border-bottom: 1px solid #edf2f7; background: white; font-weight: 700; }
|
||||
td { padding: 13px 12px; border-bottom: 1px solid #f4f6fb; font-size: 14px; vertical-align: middle; }
|
||||
tbody tr:hover { background: #fafbfe; }
|
||||
.won-opportunity-row { cursor: pointer; }
|
||||
.won-opportunity-row:hover { background: #eff6ff !important; }
|
||||
|
||||
/* ── Status Pills ── */
|
||||
.status-pill { padding: 4px 12px; border-radius: 20px; font-size: 11px; font-weight: 700; display: inline-block; }
|
||||
@ -114,6 +117,8 @@
|
||||
.status-not-a-prospects { background: #ffebee; color: #d32f2f; }
|
||||
.leads-table-wrap { max-height: 480px; overflow-y: auto; }
|
||||
.leads-table-wrap thead th { position: sticky; top: 0; background: white; z-index: 1; }
|
||||
.activity-count-link { display: inline-block; color: #2563eb; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.activity-count-muted { display: inline-block; color: #8896aa; cursor: help; }
|
||||
.empty-state { text-align: center; padding: 50px 20px; color: #aaa; }
|
||||
.empty-icon { width: 70px; height: 70px; margin: 0 auto 16px; background: #f5f5f5; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32px; }
|
||||
|
||||
@ -178,6 +183,14 @@
|
||||
.ms-num { font-size: 19px; font-weight: 800; }
|
||||
.ms-lbl { font-size: 10px; color: #7c8db0; font-weight: 700; letter-spacing: .05em; margin-top: 2px; }
|
||||
.modal-body { padding: 22px 24px; overflow-y: auto; flex: 1; }
|
||||
.target-edit-panel { display: grid; grid-template-columns: 1fr 170px 120px; gap: 12px; align-items: end; padding: 16px; margin-bottom: 18px; border: 1px solid #bfdbfe; border-radius: 14px; background: #eff6ff; }
|
||||
.target-edit-title { font-size: 13px; font-weight: 800; color: #1e3a8a; margin-bottom: 4px; }
|
||||
.target-edit-help { font-size: 11px; color: #64748b; font-weight: 600; }
|
||||
.target-edit-input { width: 100%; height: 38px; border: 1px solid #93c5fd; border-radius: 9px; padding: 8px 12px; font-size: 14px; font-weight: 700; color: #0f172a; background: #fff; }
|
||||
.target-edit-input:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.14); }
|
||||
.target-edit-btn { height: 38px; border: none; border-radius: 9px; background: #2563eb; color: #fff; font-size: 13px; font-weight: 800; cursor: pointer; transition: background .2s, transform .2s; }
|
||||
.target-edit-btn:hover { background: #1d4ed8; transform: translateY(-1px); }
|
||||
.target-edit-btn:disabled { background: #94a3b8; cursor: not-allowed; transform: none; }
|
||||
|
||||
/* ── Modal Tabs ── */
|
||||
.modal-tabs { display: flex; gap: 4px; background: #f1f5f9; border-radius: 10px; padding: 4px; margin-bottom: 20px; }
|
||||
@ -219,7 +232,7 @@
|
||||
.opp-card-lbl { font-size: 11px; color: #7c8db0; font-weight: 700; margin-top: 4px; letter-spacing: .04em; }
|
||||
|
||||
@media(max-width:900px) { .list-col-head,.achieve-row { grid-template-columns: 220px 1fr 110px 110px 110px; } .col-lbl:nth-child(6),.col-lbl:nth-child(7),.achieve-row>*:nth-child(6),.achieve-row>*:nth-child(7) { display: none; } }
|
||||
@media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
|
||||
@media(max-width:640px) { .list-col-head { display: none; } .achieve-row { grid-template-columns: 1fr auto; gap: 12px; padding: 14px 16px; } .achieve-row>*:not(:nth-child(1)):not(:nth-child(7)) { display: none; } .section-head { flex-direction: column; align-items: flex-start; gap: 12px; } .modal-summary { grid-template-columns: repeat(2,1fr); } .target-edit-panel { grid-template-columns: 1fr; } .opp-summary-cards { grid-template-columns: 1fr 1fr; } }
|
||||
</style>
|
||||
|
||||
<div class="dash-container">
|
||||
@ -243,22 +256,22 @@
|
||||
|
||||
<!-- ── Stat Cards ── -->
|
||||
<div class="stat-cards">
|
||||
<div class="card-hero">
|
||||
<div class="card-hero stat-link" onclick="goSalesPage('leads', 'all')" title="View all leads">
|
||||
<div class="stat-icon" style="background:#ede9fe;">👥</div>
|
||||
<div class="stat-val"><?php echo $total_leads; ?></div>
|
||||
<div class="stat-label">Total Leads</div>
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'all')" title="View all activities">
|
||||
<div class="stat-icon" style="background:#fce7f3;">⚡</div>
|
||||
<div class="stat-val"><?php echo $total_acts ?></div>
|
||||
<div class="stat-label">Total Activities</div>
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'pending')" title="View pending activities">
|
||||
<div class="stat-icon" style="background:#fef3c7;">⏳</div>
|
||||
<div class="stat-val"><?php echo $total_pending_acts ?></div>
|
||||
<div class="stat-label">Pending Activities</div>
|
||||
</div>
|
||||
<div class="card-hero">
|
||||
<div class="card-hero stat-link" onclick="goSalesPage('activities', 'completed')" title="View completed activities">
|
||||
<div class="stat-icon" style="background:#d1fae5;">✅</div>
|
||||
<div class="stat-val"><?php echo $total_completed_acts ?></div>
|
||||
<div class="stat-label">Completed Activities</div>
|
||||
@ -349,9 +362,14 @@
|
||||
</div>
|
||||
<div class="team-members-scroll">
|
||||
<?php
|
||||
if (!empty($team)):
|
||||
$visibleTeam = array_values(array_filter($team ?? [], function ($member) {
|
||||
return !(stripos($member['role'] ?? '', 'admin') !== false
|
||||
&& (int) ($member['total_acts'] ?? 0) === 0
|
||||
&& (int) ($member['done_acts'] ?? 0) === 0);
|
||||
}));
|
||||
if (!empty($visibleTeam)):
|
||||
$colors = ['#ff6b35','#667eea','#48bb78','#ed8936','#9f7aea'];
|
||||
foreach ($team as $member):
|
||||
foreach ($visibleTeam as $member):
|
||||
$firstLetter = strtoupper(substr($member['first_name'], 0, 1));
|
||||
$fullName = $member['first_name'];
|
||||
$color = $colors[abs(crc32($member['first_name'])) % count($colors)];
|
||||
@ -425,10 +443,29 @@
|
||||
<?php foreach ($leads_overview as $lead): ?>
|
||||
<?php $statusClass = strtolower(str_replace(' ', '-', $lead['status'])); ?>
|
||||
<tr>
|
||||
<td><strong><?= esc($lead['company_name']) ?></strong></td>
|
||||
<td>
|
||||
<strong>
|
||||
<a href="javascript:void(0);"
|
||||
onclick="goLeadDetail('<?= esc($lead['lead_id'] ?? '', 'attr') ?>')"
|
||||
title="View lead details"
|
||||
style="color:#0f172a; text-decoration:underline; text-underline-offset:2px;">
|
||||
<?= esc($lead['company_name']) ?>
|
||||
</a>
|
||||
</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;">
|
||||
<?php if ((int) $lead['activities'] > 0): ?>
|
||||
<span class="activity-count-link"
|
||||
title="View activities"
|
||||
onclick="goLeadActivities('<?= esc($lead['assigned_to_id'] ?? '', 'attr') ?>')"><?= $lead['activities'] ?></span>
|
||||
<?php else: ?>
|
||||
<span class="activity-count-muted" title="No activities"><?= $lead['activities'] ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</td>
|
||||
<td><div style="text-align:center; font-weight:700;"><?= $lead['opportunities'] ?></div></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
@ -470,11 +507,165 @@ const OPP_DATA = <?php echo json_encode($opp_achievement ?? []); ?>;
|
||||
/* ── JS Helpers ── */
|
||||
const ACT_ICONS = {Call:'📞',Email:'✉️',Meeting:'📅',Visit:'🚗',Demo:'🖥️','Share Docs':'📄','To Do':'✓'};
|
||||
const ACT_COLORS = {Call:'#4f46e5',Email:'#10b981',Meeting:'#06b6d4',Visit:'#f59e0b',Demo:'#ec4899','Share Docs':'#f97316','To Do':'#64748b'};
|
||||
const fmt = v => '₹' + (v / 100000).toFixed(1) + 'L';
|
||||
const fmtIndianFull = value => '₹' + (Number(value) || 0).toLocaleString('en-IN', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2
|
||||
});
|
||||
const fmt = value => {
|
||||
const amount = Number(value) || 0;
|
||||
let display = '₹0';
|
||||
if (Math.abs(amount) >= 100000) {
|
||||
display = '₹' + (amount / 100000).toFixed(1) + 'L';
|
||||
} else if (Math.abs(amount) >= 1000) {
|
||||
display = '₹' + (amount / 1000).toFixed(1) + 'K';
|
||||
} else if (amount !== 0) {
|
||||
display = '₹' + amount.toLocaleString('en-IN', { maximumFractionDigits: 0 });
|
||||
}
|
||||
return '<span title="' + fmtIndianFull(amount) + '">' + display + '</span>';
|
||||
};
|
||||
const fmtFull = v => '₹' + Number(v).toLocaleString('en-IN');
|
||||
const pct = (a, t) => t > 0 ? Math.min(100, Math.round(a / t * 100)) : 0;
|
||||
const badge = p => p >= 100 ? ['b-achieved','🏆 Achieved'] : p >= 75 ? ['b-ontrack','🎯 On Track'] : p >= 50 ? ['b-behind','⚡ Behind'] : ['b-atrisk','⚠️ At Risk'];
|
||||
const progColor= p => p >= 100 ? 'linear-gradient(90deg,#10b981,#34d399)' : p >= 75 ? 'linear-gradient(90deg,#06b6d4,#67e8f9)' : p >= 50 ? 'linear-gradient(90deg,#f97316,#fbbf24)' : 'linear-gradient(90deg,#ef4444,#fca5a5)';
|
||||
const SALES_LEADS_URL = '<?= base_url('sales') ?>';
|
||||
const SALES_ACTIVITIES_URL = '<?= base_url('sales/loadactivities') ?>';
|
||||
const SALES_TARGETS_URL = '<?= base_url('sales/targets') ?>';
|
||||
const OPPORTUNITY_DETAIL_BASE = '<?= base_url('/util/getLeadNonEB/') ?>';
|
||||
|
||||
function getSelectedDashboardFY() {
|
||||
return document.getElementById('financial_year')?.value || getCurrentFY();
|
||||
}
|
||||
|
||||
function redirectToOpportunity(leadTypeId, actualLeadId, opportunityId) {
|
||||
if (!leadTypeId || !actualLeadId || !opportunityId) return;
|
||||
|
||||
window.location.href = OPPORTUNITY_DETAIL_BASE
|
||||
+ encodeURIComponent(leadTypeId) + '/'
|
||||
+ encodeURIComponent(actualLeadId) + '/'
|
||||
+ encodeURIComponent(opportunityId);
|
||||
}
|
||||
|
||||
function goSalesPage(page, status, memberId) {
|
||||
const url = new URL(page === 'activities' ? SALES_ACTIVITIES_URL : SALES_LEADS_URL, window.location.origin);
|
||||
url.searchParams.set('fy', getSelectedDashboardFY());
|
||||
url.searchParams.set('status', status || 'all');
|
||||
if (memberId) {
|
||||
url.searchParams.set('member', memberId);
|
||||
}
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function goLeadActivities(memberId) {
|
||||
if (!memberId) return;
|
||||
goSalesPage('activities', 'all', memberId);
|
||||
}
|
||||
|
||||
function goLeadDetail(leadId) {
|
||||
if (!leadId) return;
|
||||
const url = new URL(SALES_LEADS_URL, window.location.origin);
|
||||
url.searchParams.set('fy', getSelectedDashboardFY());
|
||||
url.searchParams.set('status', 'all');
|
||||
url.searchParams.set('lead_id', leadId);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
|
||||
function showToast(type, message) {
|
||||
if (window.toastr && typeof toastr[type] === 'function') {
|
||||
toastr[type](message);
|
||||
return;
|
||||
}
|
||||
alert(message);
|
||||
}
|
||||
|
||||
function refreshTargetInDashboard(userId, targetId, targetAmount) {
|
||||
const member = TEAM.find(item => item.id == userId);
|
||||
if (!member) return;
|
||||
|
||||
member.target_id = targetId || member.target_id;
|
||||
member.target_amt = targetAmount;
|
||||
if (Array.isArray(member.splits)) {
|
||||
member.splits.forEach(split => {
|
||||
split.target = Number((targetAmount / 4).toFixed(2));
|
||||
});
|
||||
}
|
||||
|
||||
if (OPP_DATA[userId]) {
|
||||
OPP_DATA[userId].target_amt = targetAmount;
|
||||
}
|
||||
|
||||
renderBranchSummary();
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function getSavedTargetRecord(userId, fyYear, fallbackTarget, forceFetch = false) {
|
||||
if (!forceFetch && fallbackTarget?.id && fallbackTarget?.target_amount) {
|
||||
return fallbackTarget;
|
||||
}
|
||||
|
||||
const res = await fetch(`${SALES_TARGETS_URL}/user/${userId}`);
|
||||
if (!res.ok) {
|
||||
return fallbackTarget || {};
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return (json.data || []).find(record => record.fy_year === fyYear) || fallbackTarget || {};
|
||||
}
|
||||
|
||||
async function saveModalTarget(targetId, userId, fyYear) {
|
||||
const input = document.getElementById('modalTargetAmount');
|
||||
const button = document.getElementById('btnSaveModalTarget');
|
||||
const amount = parseInt(input?.value || '0', 10);
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
input?.focus();
|
||||
showToast('warning', 'Please enter a valid target amount.');
|
||||
return;
|
||||
}
|
||||
|
||||
const originalText = button ? button.textContent : '';
|
||||
if (button) {
|
||||
button.disabled = true;
|
||||
button.textContent = 'Saving...';
|
||||
}
|
||||
|
||||
try {
|
||||
const isUpdate = !!targetId;
|
||||
const res = await fetch(isUpdate ? `${SALES_TARGETS_URL}/${targetId}` : SALES_TARGETS_URL, {
|
||||
method: isUpdate ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
fy_year: fyYear,
|
||||
target_amount: amount
|
||||
})
|
||||
});
|
||||
const result = await res.json();
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(result?.message || 'Unable to update target amount.');
|
||||
}
|
||||
|
||||
const activeTabIndex = Array.from(document.querySelectorAll('.modal-tab'))
|
||||
.findIndex(tab => tab.classList.contains('active'));
|
||||
const updatedTarget = await getSavedTargetRecord(userId, fyYear, result?.data || {}, !isUpdate);
|
||||
const updatedAmount = Number(updatedTarget.target_amount ?? amount);
|
||||
|
||||
refreshTargetInDashboard(userId, updatedTarget.id || targetId, updatedAmount);
|
||||
openModal(userId);
|
||||
if (activeTabIndex > 0) {
|
||||
const tabButton = document.querySelectorAll('.modal-tab')[activeTabIndex];
|
||||
if (tabButton) switchTab(activeTabIndex, tabButton);
|
||||
}
|
||||
|
||||
showToast('success', result?.message || (isUpdate ? 'Target amount updated successfully.' : 'Target amount created successfully.'));
|
||||
} catch (err) {
|
||||
showToast('error', err.message || 'Unable to save target amount.');
|
||||
if (button) {
|
||||
button.disabled = false;
|
||||
button.textContent = originalText || 'Save Target';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Date formatters ── */
|
||||
const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
||||
@ -608,6 +799,19 @@ function openModal(id) {
|
||||
+ '<div class="ms-box"><div class="ms-num" style="color:#f97316;">' + fmt(rem) + '</div><div class="ms-lbl">REMAINING</div></div>'
|
||||
+ '<div class="ms-box"><div class="ms-num" style="color:' + (p>=75?'#10b981':p>=50?'#f97316':'#ef4444') + ';">' + p + '%</div><div class="ms-lbl">ACHIEVED %</div></div>';
|
||||
|
||||
const hasTargetRecord = !!m.target_id;
|
||||
const targetEditorHtml =
|
||||
'<div class="target-edit-panel">'
|
||||
+ '<div>'
|
||||
+ '<div class="target-edit-title">' + (hasTargetRecord ? 'Edit Target Amount' : 'Create Target Amount') + '</div>'
|
||||
+ '<div class="target-edit-help">' + (hasTargetRecord ? 'Update' : 'Create') + ' target for ' + m.first_name + ' - ' + currentFY + '</div>'
|
||||
+ '</div>'
|
||||
+ '<div>'
|
||||
+ '<input type="text" id="modalTargetAmount" class="target-edit-input" value="' + Math.round(m.target_amt || 0) + '" placeholder="Target amount" oninput="this.value=this.value.replace(/[^0-9]/g, \'\')">'
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="target-edit-btn" id="btnSaveModalTarget" onclick="saveModalTarget(' + (m.target_id || 0) + ', ' + m.id + ', \'' + currentFY + '\')" title="' + (hasTargetRecord ? 'Update target amount' : 'Create target amount') + '">' + (hasTargetRecord ? 'Update Target' : 'Create Target') + '</button>'
|
||||
+ '</div>';
|
||||
|
||||
/* ── Overall progress block (shared) ── */
|
||||
const overallHtml =
|
||||
'<div class="overall-prog">'
|
||||
@ -738,12 +942,27 @@ function openModal(id) {
|
||||
/* ── Won Leads Table (Table 2) ── */
|
||||
const wonLeads = opp.won_leads || [];
|
||||
const wonRows = wonLeads.map(function(l) {
|
||||
const typeBadgeClass = l.lead_type === 'EB'
|
||||
// const opportunityType = [
|
||||
// l.lead_form_type || '',
|
||||
// l.lead_type || ''
|
||||
// ].filter(Boolean).join('/').toUpperCase();
|
||||
const opportunityType = [
|
||||
l.lead_form_type || '',
|
||||
].filter(Boolean).join('').toUpperCase();
|
||||
const typeBadgeClass = l.lead_form_type === 'EB'
|
||||
? 'style="background:#eff6ff;color:#2563eb;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"'
|
||||
: 'style="background:#f0fdf4;color:#16a34a;padding:3px 10px;border-radius:99px;font-size:11px;font-weight:700;"';
|
||||
return '<tr>'
|
||||
const canRedirect = l.lead_form_type_id && l.actual_lead_id && l.opportunities_id;
|
||||
const rowAttrs = canRedirect
|
||||
? ' class="won-opportunity-row" title="Open opportunity" onclick=\'redirectToOpportunity('
|
||||
+ JSON.stringify(String(l.lead_form_type_id)) + ','
|
||||
+ JSON.stringify(String(l.actual_lead_id)) + ','
|
||||
+ JSON.stringify(String(l.opportunities_id)) + ')\''
|
||||
: '';
|
||||
|
||||
return '<tr' + rowAttrs + '>'
|
||||
+ '<td style="font-weight:700;color:#0f172a;">' + (l.company || '—') + '</td>'
|
||||
+ '<td><span ' + typeBadgeClass + '>' + (l.lead_type || '—') + '</span></td>'
|
||||
+ '<td><span ' + typeBadgeClass + '>' + (opportunityType || '—') + '</span></td>'
|
||||
+ '<td style="font-size:12px;color:#64748b;">' + fmtCreatedAt(l.created_at) + '</td>'
|
||||
+ '</tr>';
|
||||
}).join('');
|
||||
@ -767,7 +986,8 @@ function openModal(id) {
|
||||
|
||||
/* ── Inject tabs ── */
|
||||
document.getElementById('modalBody').innerHTML =
|
||||
'<div class="modal-tabs">'
|
||||
targetEditorHtml
|
||||
+ '<div class="modal-tabs">'
|
||||
+ '<button class="modal-tab active" onclick="switchTab(0,this)">📊 ' + fyLabel + ' Split-wise Achievement</button>'
|
||||
+ '<button class="modal-tab" onclick="switchTab(1,this)">🏆 Opportunities Achievement</button>'
|
||||
+ '</div>'
|
||||
|
||||
@ -12,12 +12,40 @@
|
||||
|
||||
/* Filter Tabs */
|
||||
.filter-tabs { display: flex; gap: 10px; padding: 20px 30px; }
|
||||
.tab { padding: 10px 20px; border-radius: 20px; cursor: pointer; font-size: 14px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; }
|
||||
.tab { padding: 10px 14px; border-radius: 20px; cursor: pointer; font-size: 12px; background: white; color: #666; border: 1px solid #e0e0e0; transition: all 0.2s; white-space: nowrap; flex: 0 0 auto; }
|
||||
.tab.active { background: #02a8b5; color: white; border-color: #02a8b5; }
|
||||
|
||||
/* Leads Grid */
|
||||
.lead-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; /* responsive */ gap: 15px; }
|
||||
.lead-actions { display: flex; align-items: center; gap: 10px; }
|
||||
.lead-actions { display: flex; align-items: center; gap: 4px; }
|
||||
.sales-toolbar { flex-wrap: nowrap; padding: 16px 15px; gap: 12px; overflow-x: auto; }
|
||||
.sales-toolbar .filter-tabs { padding: 0; flex: 0 0 auto; min-width: max-content; overflow: visible; }
|
||||
.sales-toolbar .lead-actions { flex: 1 1 auto; min-width: 0; flex-wrap: nowrap; justify-content: flex-end; }
|
||||
.sales-toolbar .search-input { width: 230px !important; height: 38px; padding: 9px 12px; border: 1px solid #ddd !important; border-radius: 6px !important; flex: 0 0 230px; }
|
||||
.sales-toolbar .toolbar-select { width: 175px !important; min-width: 175px; padding: 9px 12px; }
|
||||
.sales-toolbar .toolbar-select + .select2-container { width: 175px !important; min-width: 175px; }
|
||||
.sales-toolbar .financial-year-select { width: 122px !important; min-width: 122px; }
|
||||
.sales-toolbar .financial-year-select + .select2-container { width: 122px !important; min-width: 122px; flex: 0 0 122px; }
|
||||
.sales-toolbar .select2-container .select2-selection--single { height: 38px; border: 1px solid #ddd; border-radius: 6px; display: flex; align-items: center; }
|
||||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__rendered { line-height: 36px; }
|
||||
.sales-toolbar .select2-container--default .select2-selection--single .select2-selection__arrow { height: 36px; }
|
||||
.sales-toolbar #memberFilter + .select2-container { width: 175px !important; min-width: 175px; flex: 0 0 175px; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection--single,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection--single { height: 38px !important; background: #fff !important; border: 1px solid #ddd !important; border-radius: 6px !important; display: flex; align-items: center; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__rendered,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__rendered { flex: 1; line-height: 36px !important; padding-left: 12px; padding-right: 28px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow { height: 25px !important; width: 20px; top: 1px; right: 6px; }
|
||||
.sales-toolbar #memberFilter + .select2-container .select2-selection__arrow b,
|
||||
.sales-toolbar #financialYearFilter + .select2-container .select2-selection__arrow b { border-width: 4px 3px 0 3px !important; margin-left: -3px; margin-top: -2px; }
|
||||
.sales-toolbar #memberFilter + .select2-container--open .select2-selection__arrow b,
|
||||
.sales-toolbar #financialYearFilter + .select2-container--open .select2-selection__arrow b { border-width: 0 3px 4px 3px !important; }
|
||||
.toolbar-icon-btn { width: 38px; height: 38px; padding: 0; display: inline-flex; align-items: center; justify-content: center; border: none; border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.2s; }
|
||||
.toolbar-icon-btn:hover { transform: translateY(-1px); filter: brightness(0.96); }
|
||||
.toolbar-icon-btn:focus { outline: none; box-shadow: 0 0 0 2px rgba(2, 168, 181, 0.25); }
|
||||
.toolbar-icon-btn i { font-size: 20px; line-height: 1; }
|
||||
.toolbar-icon-btn.export-btn { background: #1f9d55; }
|
||||
.toolbar-icon-btn.add-btn { background: #02a8b5; }
|
||||
.leads-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; padding: 0 30px 30px; overflow-y: auto; }
|
||||
.lead-card { background: white; border-radius: 12px; padding: 20px; border: 1px solid #e0e0e0; cursor: pointer; transition: all 0.2s; }
|
||||
.lead-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.1); transform: translateY(-2px); }
|
||||
@ -146,7 +174,7 @@
|
||||
<div class="tab" data-filter="Not a Prospects" onclick="setFilter('Not a Prospects', this)">Not a Prospects</div>
|
||||
</div> -->
|
||||
<hr style="margin-bottom: 0 !important;">
|
||||
<div class="lead-header">
|
||||
<div class="lead-header sales-toolbar">
|
||||
|
||||
<!-- LEFT SIDE -->
|
||||
<div class="filter-tabs">
|
||||
@ -160,9 +188,23 @@
|
||||
<!-- RIGHT SIDE -->
|
||||
<div class="lead-actions">
|
||||
<input type="text" class="search-input" id="mainSearch"
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)" style="width: 300px !important;">
|
||||
<button class="btn-primary" onclick="openModal('addLeadModal')">
|
||||
+ Add Lead
|
||||
placeholder="Search leads..." onkeyup="fetchLeads(false)">
|
||||
<select id="memberFilter" class="toolbar-select searchable" onchange="fetchLeads(false)">
|
||||
<option value="" selected>All Member</option>
|
||||
<?php foreach(($sales_manager_with_head ?? []) as $sm): ?>
|
||||
<option value="<?= $sm['id'] ?>"><?= esc($sm['first_name']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<select id="financialYearFilter" class="toolbar-select financial-year-select searchable" onchange="fetchLeads(false)">
|
||||
<?php foreach(($fin_years ?? []) as $year): ?>
|
||||
<option value="<?= esc($year) ?>" <?= (($current_fin_year ?? '') === $year) ? 'selected' : '' ?>><?= esc($year) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<button class="toolbar-icon-btn export-btn" id="leadExportExcelBtn" title="Export Excel" aria-label="Export leads">
|
||||
<i class="mdi mdi-file-excel"></i>
|
||||
</button>
|
||||
<button class="toolbar-icon-btn add-btn" onclick="openModal('addLeadModal')" title="Add Lead" aria-label="Add lead">
|
||||
<i class="mdi mdi-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -630,31 +672,64 @@
|
||||
/* Reset Bootstrap's default position: absolute if present */
|
||||
position: static;
|
||||
}
|
||||
.contact-person-entry-row .form-control-sm {
|
||||
width: 100% !important;
|
||||
height: 31px;
|
||||
}
|
||||
.contact-person-entry-row {
|
||||
align-items: flex-start !important;
|
||||
}
|
||||
.contact-person-entry-row .contact-field-col {
|
||||
flex: 0 0 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
.contact-person-entry-row .form-label {
|
||||
min-height: 18px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
@media (max-width: 767.98px) {
|
||||
.contact-person-entry-row .contact-field-col {
|
||||
flex: 0 0 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
.contact-action-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
height: 31px;
|
||||
}
|
||||
.contact-action-buttons .btn {
|
||||
width: 36px;
|
||||
min-width: 36px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
</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">
|
||||
<div class="row g-0-5 align-items-end ml-1 contact-person-entry-row">
|
||||
<div class="col-md-3 col-12 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Person Name <span class="text-danger">*</span></label>
|
||||
<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, ''); validateContactField('name')">
|
||||
<small id="contact_name_error" class="text-danger d-block mt-1 field-error"></small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-3 col-6 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Mobile <span class="text-danger">*</span></label>
|
||||
<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); validateContactField('mobile')">
|
||||
<small id="contact_mobile_error" class="text-danger d-block mt-1 field-error"></small>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-6">
|
||||
<div class="col-md-3 col-6 mb-2 mb-md-0 contact-field-col">
|
||||
<label class="form-label">Designation </label>
|
||||
<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="col-md-1 col-3 mb-2 mb-md-0">
|
||||
<label class="form-label"> </label>
|
||||
<div class="primary-container">
|
||||
<label for="contact_is_primary" class="primary-label">Primary</label>
|
||||
@ -668,13 +743,13 @@
|
||||
✔ Save
|
||||
</button>
|
||||
</div> -->
|
||||
<div class="col-md-2 col-6">
|
||||
<div class="col-md-2 col-6 mb-2 mb-md-0">
|
||||
<label class="form-label"> </label>
|
||||
<div style="display:flex; gap:2px;">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" style="width:42%" title="Save">
|
||||
<div class="contact-action-buttons">
|
||||
<button type="button" id="btnSaveContact" class="btn btn-sm btn-info text-white" title="Save">
|
||||
✔
|
||||
</button>
|
||||
<button type="button" id="btnClearContact" class="btn btn-sm btn-danger text-white" style="width:42%" title="Clear">
|
||||
<button type="button" id="btnClearContact" class="btn btn-sm btn-danger text-white" title="Clear">
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
@ -729,12 +804,15 @@ document.addEventListener('DOMContentLoaded', function () {
|
||||
// Single selects
|
||||
document.querySelectorAll('.searchable:not(.multi-searchable)').forEach(function(el) {
|
||||
let parentModal = el.closest('.modal');
|
||||
$(el).select2({
|
||||
placeholder: "Select..",
|
||||
const select2Options = {
|
||||
allowClear: false,
|
||||
minimumResultsForSearch: 0,
|
||||
dropdownParent: parentModal ? $(parentModal) : $(document.body)
|
||||
});
|
||||
};
|
||||
if (el.id !== 'memberFilter') {
|
||||
select2Options.placeholder = "Select..";
|
||||
}
|
||||
$(el).select2(select2Options);
|
||||
});
|
||||
|
||||
// Multi selects
|
||||
@ -771,6 +849,7 @@ const API = '<?= base_url('sales') ?>';
|
||||
const SALES_CHECK_DUPLICATE_URL = '<?= base_url('sales/checkDuplicate') ?>';
|
||||
/** Normalized company name when edit modal opened — same name retyped after clear must not count as duplicate */
|
||||
let trackerEditLeadOriginalCompanyNorm = '';
|
||||
const defaultFinancialYear = '<?= esc($current_fin_year ?? '') ?>';
|
||||
let filter = 'all';
|
||||
let lead_id = null;
|
||||
let global_lead_assigned_to = null;
|
||||
@ -780,6 +859,58 @@ let currentPage = 1;
|
||||
let limit = 9;
|
||||
let currentOffset = 0;
|
||||
|
||||
function getCurrentFY() {
|
||||
const today = new Date();
|
||||
const year = today.getFullYear();
|
||||
const month = today.getMonth();
|
||||
const startYear = month >= 3 ? year : year - 1;
|
||||
return startYear + '-' + String(startYear + 1);
|
||||
}
|
||||
|
||||
function initSalesToolbarFilters() {
|
||||
const fySelect = document.getElementById('financialYearFilter');
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (fySelect) {
|
||||
let fy = params.get('fy') || defaultFinancialYear || getCurrentFY();
|
||||
const matched = Array.from(fySelect.options).some(opt => opt.value === fy);
|
||||
if (!matched && fySelect.options.length > 0) {
|
||||
fy = fySelect.options[0].value;
|
||||
}
|
||||
fySelect.value = fy;
|
||||
}
|
||||
|
||||
const statusParam = (params.get('status') || 'all').toLowerCase();
|
||||
const statusTab = Array.from(document.querySelectorAll('.filter-tabs .tab'))
|
||||
.find(tab => (tab.dataset.filter || '').toLowerCase() === statusParam);
|
||||
if (statusTab) {
|
||||
document.querySelectorAll('.filter-tabs .tab').forEach(tab => tab.classList.remove('active'));
|
||||
statusTab.classList.add('active');
|
||||
filter = statusTab.dataset.filter || 'all';
|
||||
}
|
||||
|
||||
const memberSelect = document.getElementById('memberFilter');
|
||||
const memberId = params.get('member');
|
||||
if (memberSelect && memberId && Array.from(memberSelect.options).some(opt => opt.value === memberId)) {
|
||||
memberSelect.value = memberId;
|
||||
if (window.jQuery) {
|
||||
$(memberSelect).trigger('change.select2');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSelectedMemberIds(defaultIds) {
|
||||
const memberFilter = document.getElementById('memberFilter');
|
||||
if (memberFilter && memberFilter.value) {
|
||||
return [memberFilter.value];
|
||||
}
|
||||
return Array.isArray(defaultIds) ? defaultIds : [];
|
||||
}
|
||||
|
||||
function getSelectedFinancialYear() {
|
||||
return document.getElementById('financialYearFilter')?.value || defaultFinancialYear || getCurrentFY();
|
||||
}
|
||||
|
||||
function openModal(id) { document.getElementById(id).classList.add('active'); resetFlatpicker(); }
|
||||
// function closeModal(id) { document.getElementById(id).classList.remove('active'); }
|
||||
|
||||
@ -940,7 +1071,11 @@ function setFilter(val, el) {
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
filter = val;
|
||||
fetchLeads();
|
||||
const searchInput = document.getElementById('mainSearch');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
fetchLeads(false);
|
||||
}
|
||||
|
||||
// 1. Leads Grid Logic
|
||||
@ -970,6 +1105,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
const btnLoadMore = document.getElementById('btn-load-more');
|
||||
const spinner = document.getElementById('load-more-spinner');
|
||||
const text = document.getElementById('load-more-text');
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
|
||||
|
||||
// 1. Define the empty state HTML early so we can use it immediately if needed
|
||||
const emptyStateHTML = `<div style="display: contents;">
|
||||
@ -980,7 +1116,7 @@ async function fetchLeads(isLoadMore = false) {
|
||||
</div>`;
|
||||
|
||||
// 2. 🛑 SHORT-CIRCUIT: If no sales manager IDs exist, show empty state and stop!
|
||||
if (typeof salesManagerHeadIds === 'undefined' || salesManagerHeadIds.length === 0) {
|
||||
if (selectedMemberIds.length === 0) {
|
||||
console.log("No Sales Manager IDs found. Skipping API call.");
|
||||
grid.innerHTML = emptyStateHTML;
|
||||
btnLoadMore.style.display = 'none'; // Hide the load more button
|
||||
@ -1005,11 +1141,15 @@ 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=${salesManagerHeadIds.join(',')}`; // We already proved it exists above!
|
||||
if (typeof salesManagerHeadIds !== 'undefined' && salesManagerHeadIds.length > 0) {
|
||||
url += `&assigned_to=${salesManagerHeadIds.join(',')}`;
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: q,
|
||||
limit,
|
||||
offset: currentOffset,
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
financial_year: getSelectedFinancialYear()
|
||||
});
|
||||
let url = `${API}/leads?${params.toString()}`;
|
||||
|
||||
console.log("Fetching API:", url);
|
||||
|
||||
@ -1154,8 +1294,9 @@ async function viewDetail(id) {
|
||||
function renderCard(opps) {
|
||||
const opp_cont = document.getElementById('opportunitiesContainer');
|
||||
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
let lead_type = o.lead_type == 1 ? 'EB' : 'Non-EB';
|
||||
opp_cont.innerHTML = opps.length ? opps.map(o => {
|
||||
const opportunityFormType = Number(o.lead_form_type || 1);
|
||||
let lead_form_type = opportunityFormType === 1 ? 'EB' : 'Non-EB';
|
||||
let status = o.status?.toLowerCase();
|
||||
let statusClass = {
|
||||
won: 'status-text-won',
|
||||
@ -1180,7 +1321,7 @@ function renderCard(opps) {
|
||||
<div class="opportunities-list" id="leadOpportunitiesList">
|
||||
<div class="opportunity-card" style="bottom:2px;">
|
||||
<div class="opportunity-header">
|
||||
<div class="opportunity-title">${lead_type}</div>
|
||||
<div class="opportunity-title">${lead_form_type}</div>
|
||||
</div>
|
||||
<div class="opportunity-details">
|
||||
<div class="opportunity-detail-item">
|
||||
@ -2084,12 +2225,12 @@ function submitToRedirectwithactualLeadIDUrl(){
|
||||
|
||||
|
||||
let actual_lead_id = document.getElementById('opp_lead_id').value;
|
||||
let lead_type = $('input[name="lead_form_type"]:checked').val();
|
||||
let lead_form_type = $('input[name="lead_form_type"]:checked').val();
|
||||
|
||||
// URL: /type / actual_lead_id
|
||||
let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_type + '/' + actual_lead_id ;
|
||||
let url = '<?=base_url('/util/getLeadNonEB/')?>' + lead_form_type + '/' + actual_lead_id ;
|
||||
|
||||
console.log('lead_type ', lead_type);
|
||||
console.log('lead_form_type ', lead_form_type);
|
||||
console.log('opp_lead_id ', actual_lead_id);
|
||||
console.log('url ', url);
|
||||
|
||||
@ -2138,7 +2279,8 @@ document.getElementById('do_follow').addEventListener('change', function () {
|
||||
});
|
||||
|
||||
|
||||
async function openEditLeadModal(id) {
|
||||
async function openEditLeadModal(id, preservedFields = {}) {
|
||||
preservedFields = preservedFields || {};
|
||||
|
||||
// 1. Reset UI State
|
||||
document.getElementById('hidden_lead_id').value = id;
|
||||
@ -2183,9 +2325,12 @@ async function openEditLeadModal(id) {
|
||||
form.querySelector('[name="company_name"]').value = lead.company_name || '';
|
||||
form.querySelector('[name="email"]').value = lead.email || '';
|
||||
form.querySelector('[name="phone"]').value = lead.phone || '';
|
||||
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="address"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'address') ? preservedFields.address : (lead.address || '');
|
||||
form.querySelector('[name="website"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'website') ? preservedFields.website : (lead.website || '');
|
||||
form.querySelector('[name="gst_number"]').value =
|
||||
Object.prototype.hasOwnProperty.call(preservedFields, 'gst_number') ? preservedFields.gst_number : (lead.gst_number || '');
|
||||
// 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 || '';
|
||||
@ -2277,15 +2422,27 @@ document.getElementById('contactPersonsList').onclick = async (e) => {
|
||||
if (!confirm('Are you sure you want to remove this contact?')) return;
|
||||
|
||||
try {
|
||||
const preservedFields = getEditLeadPreservedFields();
|
||||
const res = await fetch(`${API}/contacts/${contactId}`, { method: 'DELETE' });
|
||||
if (res.ok) {
|
||||
toastr.success('Removed successfully');
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value); // Refresh
|
||||
openEditLeadModal(document.getElementById('hidden_lead_id').value, preservedFields); // Refresh
|
||||
}
|
||||
} catch (err) { console.error(err); }
|
||||
}
|
||||
};
|
||||
|
||||
function getEditLeadPreservedFields() {
|
||||
const form = document.getElementById('editLeadForm');
|
||||
if (!form) return {};
|
||||
|
||||
return {
|
||||
address: form.querySelector('[name="address"]')?.value || '',
|
||||
website: form.querySelector('[name="website"]')?.value || '',
|
||||
gst_number: form.querySelector('[name="gst_number"]')?.value || ''
|
||||
};
|
||||
}
|
||||
|
||||
// --- 2. SAVE / UPDATE LOGIC ---
|
||||
const btnSaveContact = document.getElementById('btnSaveContact');
|
||||
if (btnSaveContact) {
|
||||
@ -2310,6 +2467,7 @@ if (btnSaveContact) {
|
||||
const method = editingId ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const preservedFields = getEditLeadPreservedFields();
|
||||
const res = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@ -2323,7 +2481,7 @@ if (btnSaveContact) {
|
||||
resetContactForm();
|
||||
|
||||
// Refresh List
|
||||
openEditLeadModal(leadId);
|
||||
openEditLeadModal(leadId, preservedFields);
|
||||
} else {
|
||||
const err = await res.json();
|
||||
if (err.messages?.name) setContactFieldError('name', err.messages.name);
|
||||
@ -2394,7 +2552,138 @@ function convertDBFormatted(input) {
|
||||
seconds.padStart(2, '0')
|
||||
);
|
||||
}
|
||||
fetchLeads();
|
||||
|
||||
function csvEscape(value) {
|
||||
const text = String(value ?? '').replace(/"/g, '""');
|
||||
return `"${text}"`;
|
||||
}
|
||||
|
||||
function capitalizeStatus(value) {
|
||||
const text = String(value ?? '').trim();
|
||||
return text ? text.charAt(0).toUpperCase() + text.slice(1).toLowerCase() : '';
|
||||
}
|
||||
|
||||
function formatIndianDate(value) {
|
||||
if (!value) return '';
|
||||
const date = new Date(String(value).replace(' ', 'T'));
|
||||
if (Number.isNaN(date.getTime())) return value;
|
||||
|
||||
return date.toLocaleString('en-IN', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
}).replace(',', '').toUpperCase();
|
||||
}
|
||||
|
||||
function downloadCsv(filename, rows) {
|
||||
const csv = rows.map(row => row.map(csvEscape).join(',')).join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function getExportFilename(response, fallbackFilename) {
|
||||
const disposition = response.headers.get('content-disposition') || '';
|
||||
const match = disposition.match(/filename="?([^"]+)"?/i);
|
||||
return match ? match[1] : fallbackFilename;
|
||||
}
|
||||
|
||||
async function downloadExportFile(url, fallbackFilename) {
|
||||
const response = await fetch(url);
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
|
||||
if (!response.ok || contentType.includes('application/json')) {
|
||||
let message = 'Export failed. Please try again.';
|
||||
try {
|
||||
const json = await response.json();
|
||||
message = json.message || json.messages?.error || message;
|
||||
} catch (err) {
|
||||
// Keep the generic message when the response cannot be parsed.
|
||||
}
|
||||
|
||||
return response.status === 404 ? toastr.warning(message) : toastr.error(message);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const link = document.createElement('a');
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.download = getExportFilename(response, fallbackFilename);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
}
|
||||
|
||||
function initLeadExportDateRangePicker() {
|
||||
const $button = $('#leadExportExcelBtn');
|
||||
if (!$button.length) return;
|
||||
|
||||
if (typeof moment === 'undefined' || typeof $.fn.daterangepicker === 'undefined') {
|
||||
$button.on('click', () => toastr.error('Date range picker is not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
$button.daterangepicker({
|
||||
autoUpdateInput: false,
|
||||
startDate: moment().startOf('month'),
|
||||
endDate: moment(),
|
||||
maxDate: moment(),
|
||||
opens: 'left',
|
||||
drops: 'down',
|
||||
locale: {
|
||||
format: 'DD-MM-YYYY',
|
||||
applyLabel: 'Generate Excel',
|
||||
cancelLabel: 'Cancel'
|
||||
},
|
||||
ranges: {
|
||||
'Today': [moment(), moment()],
|
||||
'Yesterday': [moment().subtract(1, 'days'), moment().subtract(1, 'days')],
|
||||
'Last 7 Days': [moment().subtract(6, 'days'), moment()],
|
||||
'Last 30 Days': [moment().subtract(29, 'days'), moment()],
|
||||
'This Month': [moment().startOf('month'), moment()],
|
||||
'Last Month': [moment().subtract(1, 'month').startOf('month'), moment().subtract(1, 'month').endOf('month')]
|
||||
}
|
||||
});
|
||||
|
||||
$button.on('apply.daterangepicker', function (ev, picker) {
|
||||
exportLeadsCsv(
|
||||
picker.startDate.format('YYYY-MM-DD'),
|
||||
picker.endDate.format('YYYY-MM-DD')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function exportLeadsCsv(fromDate, toDate) {
|
||||
const selectedMemberIds = getSelectedMemberIds(salesManagerHeadIds);
|
||||
if (selectedMemberIds.length === 0) return toastr.warning('No members available to export');
|
||||
if (!fromDate || !toDate) return toastr.warning('Please select a date range');
|
||||
|
||||
const params = new URLSearchParams({
|
||||
status: filter === 'all' ? '' : filter,
|
||||
search: document.getElementById('mainSearch')?.value || '',
|
||||
assigned_to: selectedMemberIds.join(','),
|
||||
from_date: fromDate,
|
||||
to_date: toDate
|
||||
});
|
||||
|
||||
await downloadExportFile(
|
||||
`${API}/leads/export?${params.toString()}`,
|
||||
`sales-leads-${fromDate}-to-${toDate}.csv`
|
||||
);
|
||||
}
|
||||
|
||||
window.addEventListener('load', initLeadExportDateRangePicker);
|
||||
initSalesToolbarFilters();
|
||||
fetchLeads().then(() => {
|
||||
const leadId = new URLSearchParams(window.location.search).get('lead_id');
|
||||
if (leadId) {
|
||||
viewDetail(leadId);
|
||||
}
|
||||
});
|
||||
|
||||
// ================================================================
|
||||
// COMPANY SEARCH — Odoo-style autocomplete
|
||||
|
||||
@ -884,13 +884,13 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="policy_start_date">Policy Start Date</label>
|
||||
<input type="text" class="form-control" id="policy_start_date" name="policy_start_date" placeholder="DD/MM/YYY" value="<?= isset($lead_data['policy_start_date']) ? date('d-m-Y', strtotime($lead_data['policy_start_date'])) : "" ?>">
|
||||
<label for="policy_start_date">Policy Start Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="policy_start_date" name="policy_start_date" placeholder="DD/MM/YYY" value="<?= isset($lead_data['policy_start_date']) ? date('d-m-Y', strtotime($lead_data['policy_start_date'])) : "" ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
<label for="policy_end_date">Policy End Date</label>
|
||||
<input type="text" class="form-control" id="policy_end_date" name="policy_end_date" placeholder="DD/MM/YYY" value="<?= isset($lead_data['policy_end_date']) ? date('d-m-Y', strtotime($lead_data['policy_end_date'])) : "" ?>">
|
||||
<label for="policy_end_date">Policy End Date <span class="text-danger">*</span></label>
|
||||
<input type="text" class="form-control" id="policy_end_date" name="policy_end_date" placeholder="DD/MM/YYY" value="<?= isset($lead_data['policy_end_date']) ? date('d-m-Y', strtotime($lead_data['policy_end_date'])) : "" ?>" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group col-md-3">
|
||||
@ -4429,6 +4429,18 @@ function constructURL_ForPlacementMailSend(return_type = false) {
|
||||
let acm_pk = $('#acm_id option:selected').data('id');
|
||||
let is_installment = $("#is_installment_switch").is(":checked") ? 1 : 0;
|
||||
|
||||
if (!policy_start_date) {
|
||||
toastr.warning('Policy Start Date is required.', 'Warning');
|
||||
$('#policy_start_date').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!policy_end_date) {
|
||||
toastr.warning('Policy End Date is required.', 'Warning');
|
||||
$('#policy_end_date').focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Process `to` field
|
||||
to = to.split(',').map(email => email.trim());
|
||||
// cc = cc.map(Number); // or split if it's a string: `cc.split(',').map(email => email.trim())`
|
||||
@ -7552,6 +7564,9 @@ function appendMultiFileData(data) {
|
||||
|
||||
// Data to send in the AJAX request
|
||||
let requestData = constructURL_ForPlacementMailSend(true);
|
||||
if (requestData === false) {
|
||||
return false;
|
||||
}
|
||||
console.log('requestData', requestData);
|
||||
|
||||
// Show loader
|
||||
|
||||
Loading…
Reference in New Issue
Block a user