Merge branch 'dev' of bitbucket.org:jubilian/nhance into dev

This commit is contained in:
VENKATESHWARAN 2025-11-20 11:02:10 +05:30
commit eaca6513ee
29 changed files with 2191 additions and 117 deletions

View File

@ -480,7 +480,6 @@ $routes->group("rfq", ["filter" => "authMVC"], function ($routes) {
$routes->get("driveListFiles", "GoogleDriveController::listFiles");
$routes->post('dmsSearch', 'PolicyTransactionController::dmsSearch', ['filter' => 'authMVC']);
$routes->get('dmsSearch', 'PolicyTransactionController::dmsSearch', ['filter' => 'authMVC']);
$routes->get('payouts', 'PolicyTransactionController::payouts', ['filter' => 'authMVC']);
$routes->get('downloadGdriveFile', 'GoogleDriveController::downloadGdriveFile', ['filter' => 'authMVC']);
$routes->get('cli/sendZeptoMail', 'MasterController::testZeptoSMTP');
@ -774,6 +773,10 @@ $routes->group('payout', function($routes) {
$routes->post('fetchUtrDetails',"PayoutController::fetchUtrDetails");
$routes->post('saveUtrDetails',"PayoutController::saveUtrDetails");
$routes->post('removeUtrDetails',"PayoutController::removeUtrDetails");
// invoice policy mapping
$routes->get('invoices', 'PayoutController::invoices');
$routes->post('invoices/save', 'PayoutController::saveInvoice');
$routes->get('invoices/history', 'PayoutController::auditHistory');
});
//PARTNER COMMISSION
@ -787,5 +790,6 @@ $routes->group('commission', function($routes) {
$routes->get('rules/list/(:any)',"RuleImportController::ruleList/$1");
$routes->post('rules/save/',"RuleImportController::saveRule");
$routes->post('rules/remove/',"RuleImportController::removeRule");
$routes->get('checkRuleUsage',"RuleImportController::checkRuleUsage");
});

View File

@ -13,7 +13,7 @@ class InsuranceCommissionController extends AdminController
public function __construct()
{
set_session_context('Client');
set_session_context('InsuranceCommissionController');
$this->myLogger = \Config\Services::mylogger();
// Load rules file if present in writable config path
// $rulesPath = WRITEPATH . 'config/insurance_rules.json';
@ -122,7 +122,10 @@ class InsuranceCommissionController extends AdminController
// Normalise department keys to lowercase for consistent lookups
$this->rules = [];
foreach ($parsed as $dept => $rules) {
$this->rules[strtolower($dept)] = $rules;
if($rules['is_deleted'] === false)
{
$this->rules[strtolower($dept)] = $rules;
}
}
// print_r($this->rules);die();

View File

@ -258,7 +258,7 @@ class NotificationController extends AdminController
$template_id = $this->request->getPost('template_id');
$test_mail = $this->request->getPost('test_mail');
// $test_mail = $this->request->getPost('test_mail');
$test_mail_list = $this->request->getPost('test_mail_list');
@ -268,27 +268,67 @@ class NotificationController extends AdminController
if(!empty($notification_data)){
$params = [
'client_data' => $client_data,
'notification_data' => $notification_data,
'test_mail' => $test_mail,
'test_mail_list' => $test_mail_list
];
// First Index as test mail all are testmaillist
// $params = [
// 'client_data' => $client_data,
// 'notification_data' => $notification_data,
// 'test_mail' => $test_mail,
// 'test_mail_list' => $test_mail_list
// ];
// $testMailData = sendMailNotification::sendMailNotificationForTesting($notification_data['template_name'], $params);
// if (!empty($testMailData)) {
// $mail_send_return1 = MailHelper::send_email($testMailData);
// $this->myLogger->logme("info", $mail_send_return1);
// $this->myLogger->logme("info", $mail_send_return1);
// return $this->respond(['status' => true,'code' => 200, 'respond' => json_decode($mail_send_return1)]);
// }else{
// return $this->respond(['status' => false,'code' => 200, 'message' => 'Test Mail Data Does Not Exist']);
// }
$testMailData = sendMailNotification::sendMailNotificationForTesting($notification_data['template_name'], $params);
// print_r($testMailData); die;
if (!empty($testMailData)) {
$mail_send_return1 = MailHelper::send_email($testMailData);
$this->myLogger->logme("info", $mail_send_return1);
$this->myLogger->logme("info", $mail_send_return1);
$mailArray = explode(',', $test_mail_list);
$successMails = [];
$failedMails = [];
return $this->respond(['status' => true,'code' => 200, 'respond' => json_decode($mail_send_return1)]);
foreach ($mailArray as $singleMail) {
$singleMail = trim($singleMail);
}else{
$params = [
'client_data' => $client_data,
'notification_data' => $notification_data,
'test_mail' => $singleMail,
'test_mail_list' => "" // optional
];
$testMailData = sendMailNotification::sendMailNotificationForTesting($notification_data['template_name'], $params);
if (!empty($testMailData)) {
$mail_send_return1 = MailHelper::send_email($testMailData);
if ($mail_send_return1) {
$successMails[] = $singleMail;
} else {
$failedMails[] = $singleMail;
}
$this->myLogger->logme("info", "Mail attempt to {$singleMail}: " . $mail_send_return1);
} else {
$failedMails[] = $singleMail;
$this->myLogger->logme("info", "Test Mail Data Does Not Exist for {$singleMail}");
}
}
// Prepare final response
if (!empty($successMails)) {
$responseMessage = "Count " . count($successMails) . " mail(s) sent successfully: \n" . implode("\n", $successMails);
if (!empty($failedMails)) {
$responseMessage .= "\nCount " . count($failedMails) . " mail(s) failed: \n" . implode("\n", $failedMails);
}
return $this->respond(['status' => true,'code' => 200,'message' => $responseMessage]);
} else {
return $this->respond(['status' => false,'code' => 200,'message' => 'All mails failed to send: ' . implode(", ", $failedMails)]);
}
return $this->respond(['status' => false,'code' => 200, 'message' => 'Test Mail Data Does Not Exist']);
}
}else{
return $this->respond(['status' => false,'code' => 200, 'message' => 'Notification Template not enabled']);

View File

@ -9,6 +9,7 @@ use App\Models\InvoiceItemModel;
use App\Models\InvoiceModel;
use App\Models\InvoiceUtrModel;
use App\Models\PolicyTransactionModel;
use App\Models\AuditHistoryModel;
class PayoutController extends BaseController
{
@ -20,21 +21,23 @@ class PayoutController extends BaseController
protected $policyTransactionModel;
protected $payout_status;
protected $auditHistory;
public function __construct()
{
set_session_context('PayoutController');
$this->myLogger = \Config\Services::mylogger();
$this->payout_status = [
1 => "Draft",
2 => "Pending",
3 => "Complete",
1 => "Pending",
2 => "Complete",
];
$this->invoiceItemModel = new InvoiceItemModel();
$this->invoiceModel = new InvoiceModel();
$this->invoiceUtrModel = new InvoiceUtrModel();
$this->policyTransactionModel = new PolicyTransactionModel();
$this->auditHistory = new AuditHistoryModel();
}
public function payoutList()
@ -189,4 +192,263 @@ class PayoutController extends BaseController
}
/*************************************************************************************************************/
//... Payout-invoice Mapping Commission's amount and Adjustment's Amount - Data Display
public function invoices()
{
$type = $this->request->getGet('type');
$title = $type === 'add' ? 'Add Invoices'
: ($type === 'edit' ? 'Edit Invoices'
: ($type === 'adjustment' ? 'Adjustment Invoices'
: 'Invoices'));
$data['tab_name'] = $title;
$data['page_name'] = $title;
$id = $this->request->getGet('id');
$data['agents'] = $this->invoiceModel->agentList(); // common both add and edit
// $data['checked_policy_numbers'] = [];
// $data['invoice'] = [];
// $data['extra_payouts'] = [];
//... Now Seperated add => 'policy_transaction_payouts1'
//... Now Seperated edit and adjustment => 'policy_transaction_payouts' old file
//... Reason : Due Datatable issues Export button Searching like that so seperated
if ($type === 'add') {
$invoiceNo = $this->generateInvoiceNumber();
$data['payouts'] = $this->invoiceModel->payoutList(1);
$data['invoice_number'] = $invoiceNo;
return $this->loadLayout('invoice_policy_mapping_add', $data);
}
if (($type === 'edit' || $type === 'adjustment') && !empty($id)) {
$invoice = $this->invoiceModel->where('id', $id)->first();
$data['freeze_edit'] = $this->auditHistory->where('table_name', 'partner_invoice')->where('pk', $id)->countAllResults();
$agentId = $invoice['agent_id'] ?? null;
$data['payouts'] = $this->invoiceModel->payoutList(2 ,$agentId,$id);
$data['extra_payouts'] = $agentId ? $this->invoiceModel->payoutList(3, $agentId) : [];
$invoice_items = $this->invoiceItemModel->where('invoice_id', $id)->findAll();
if (!$invoice) { return redirect()->to('payout/invoices')->with('error', 'Invoice not found'); }
$data['invoice'] = $invoice;
$data['invoice_items'] = $invoice_items;
// Initialize array for policy numbers
$data['checked_policy_numbers'] = array_column(array_filter($invoice_items, fn($ii) => isset($ii['is_active']) && $ii['is_active'] == 1),'policy_no');
$data['invoice_number']= $invoice['invoice_no'];
$data['type'] = $type;
return $this->loadLayout('invoice_policy_mapping', $data);
}
}
//... Payout-invoice Mapping - Save/update/soft Delete/Hard Delete Data
public function saveInvoice()
{
$json = $this->request->getJSON(true);
if (!$json) {
return $this->response->setJSON(['error' => 'Invalid JSON','message' => 'Invalid JSON received.'])->setStatusCode(400);
}
$id = $json['invoice_id'] ?? null;
try {
//... ADD Part
if (empty($id)) {
$invoiceData = [
'invoice_no' => $json['invoice_no'],
'agent_id' => $json['agent_id'],
'invoice_date' => $json['invoice_date'],
'invoice_amount' => $json['invoice_amount'],
'payout_status' => 1
];
$invoiceId = $this->invoiceModel->insert($invoiceData);
foreach ($json['policies'] as $p) {
$this->invoiceItemModel->insert([
'invoice_id' => $invoiceId,
'policy_id' => $p['policy_id'],
'policy_no' => $p['policy_no'],
'commission_amount' => $p['commission_amount'],
'is_active' => 1
]);
}
$message = "Invoice created successfully.\nInvoice No: " . $json['invoice_no'];
}
// ... EDIT part
if (!empty($id)) {
$invoiceId = $id;
$invoiceData = [
'invoice_no' => $json['invoice_no'],
'agent_id' => $json['agent_id'],
'invoice_date' => $json['invoice_date'],
'invoice_amount' => $json['invoice_amount'],
];
$this->invoiceModel->update($invoiceId, $invoiceData);
//... Fetch existing invoice item rows
$existingItems = $this->invoiceItemModel
->where('invoice_id', $invoiceId)
->findAll();
//... Create map by policy_no
$existingMap = [];
foreach ($existingItems as $item) {
$existingMap[$item['policy_no']] = $item;
}
$newPolicyNos = [];
//... Loop new JSON policies
foreach ($json['policies'] as $p) {
$newPolicyNos[] = $p['policy_no'];
if (isset($existingMap[$p['policy_no']])) {
//... Update existing item
$this->invoiceItemModel
->where('id', $existingMap[$p['policy_no']]['id'])
->set([
'commission_amount' => $p['commission_amount'],
'is_active' => 1
])
->update();
} else {
//... Insert new item
$this->invoiceItemModel->insert([
'invoice_id' => $invoiceId,
'policy_id' => $p['policy_id'],
'policy_no' => $p['policy_no'],
'commission_amount' => $p['commission_amount'],
'is_active' => 1,
]);
}
}
//... Delete items removed in JSON (hard delete)
foreach ($existingItems as $old) {
if (!in_array($old['policy_no'], $newPolicyNos)) {
$this->invoiceItemModel
->where('id', $old['id'])
->delete();
}
}
//... Delete items removed in JSON (soft delete REF : SVM )
// foreach ($existingItems as $old) {
// if (!in_array($old['policy_no'], $newPolicyNos)) {
// $this->invoiceItemModel
// ->where('id', $old['id'])
// ->set(['is_active' => 0])
// ->update();
// }
// }
$message = "Invoice updated successfully.";
}
return $this->response->setJSON([
'status' => 'success',
'message' => $message,
'invoice_id' => $invoiceId
]);
} catch (\Exception $e) {
return $this->response->setJSON([
'status' => 'error',
'message' => 'Unexpected error occurred: ' . $e->getMessage()
]);
}
}
//... Payout-invoice Mapping - Invoice number is auto-generated only for the Add mode.
// Note: It will change each time the page is reloaded.. (REF:MGW)
private function generateInvoiceNumber()
{
$year = date('Y');
$month = date('m');
do {
// random 3-digit number
$random = str_pad(rand(1, 999), 3, '0', STR_PAD_LEFT);
$invoiceNo = "INV{$year}{$month}{$random}";
// check main invoice table
$existsMain = $this->invoiceModel
->where('invoice_no', $invoiceNo)
->first();
// check partner invoice table
$existsPartner = $this->invoiceModel
->where('invoice_no', $invoiceNo)
->first();
} while ($existsMain || $existsPartner); // regenerate if duplicate found
return $invoiceNo;
}
//... Payout-invoice Mapping Audit History Based on "Adjustment" value (REF: KV,SVM)
public function auditHistory()
{
$iid = $this->request->getGet('id');
$details['invoice'] = $this->auditHistory
->select('auditing_history.*,partner_invoice.invoice_no, user_profiles.first_name as created_name')
->join('user_profiles', 'user_profiles.id = auditing_history.created_by', 'left')
->join('partner_invoice', 'partner_invoice.id = auditing_history.pk', 'left')
->where('auditing_history.table_name', 'partner_invoice') // ok
->where('auditing_history.pk', $iid)
->orderBy('auditing_history.created_at', 'desc')
->get()
->getResultArray();
$details['invoice_child'] = $this->auditHistory
->select('auditing_history.*,partner_invoice.invoice_no, user_profiles.first_name as created_name')
->join('user_profiles', 'user_profiles.id = auditing_history.created_by', 'left')
->join('partner_invoice_items', 'partner_invoice_items.id = auditing_history.pk', 'left')
->join('partner_invoice', 'partner_invoice.id = partner_invoice_items.invoice_id', 'left')
->where('auditing_history.table_name', 'partner_invoice_items') // FIXED
->where('partner_invoice.id', $iid)
->orderBy('auditing_history.created_at', 'desc')
->get()
->getResultArray();
return $this->response->setJSON([
'status' => 'success',
'data' => $details
]);
}
}

View File

@ -2811,20 +2811,6 @@
$this->loadLayout('dms_search', $data);
}
public function payouts()
{
$data['tab_name'] = 'Payouts';
$data['page_name'] = 'Payouts';
$data['payouts'] = [];
$data['agents'] = [ 1 => "Agent 1", 2 => "Agent 2", 3 => "Agent 3", 4 => "Agent 4", 5 => "Agent 5", 6 => "Agent 6", 7 => "Agent 7", 8 => "Agent 8"];
$data['brokers'] = [];
if ($this->request->is('post')) {}
if ($this->request->is('get')) {}
$this->loadLayout('policy_transaction_payouts', $data);
}
//---------------------------------------------------------------------------------------------------
public function getCoShareStatementDetails($pt_id)

View File

@ -4,6 +4,9 @@ namespace App\Controllers;
use CodeIgniter\API\ResponseTrait;
use App\Models\CommissionFilesModel;
use App\Models\InsurerModel;
use App\Models\PartnerPolicyModel;
class RuleImportController extends AdminController
{
@ -14,6 +17,7 @@ class RuleImportController extends AdminController
protected $departments;
protected $departmentFields;
protected $insurerModel;
protected $partnerPolicyModel;
public function __construct()
{
@ -21,7 +25,7 @@ class RuleImportController extends AdminController
$this->myLogger = \Config\Services::mylogger();
$this->ruleImportService = \Config\Services::ruleImportService();
$this->partnerPolicyModel = new partnerPolicyModel();
$this->commissionFilesModel = new CommissionFilesModel();
$this->insurerModel = new InsurerModel();
$this->departments = [
@ -208,6 +212,8 @@ class RuleImportController extends AdminController
'insurer_id' => (int)$insurerId,
'department' => $department,
'commission_month' => $commissionMonth,
'created_by' => (int)$createdBy,
];
$this->myLogger->logme('info', 'RuleImportController::upload - Calling ruleImportService->processUpload', ['payload' => $payload]);
@ -276,10 +282,22 @@ class RuleImportController extends AdminController
// handle existing JSON file based on $override (bool)
if (file_exists($jsonPath)) {
if ($overwrite) {
// delete existing file before writing (deterministic overwrite)
if (!@unlink($jsonPath)) {
$this->myLogger->logme('warning', 'RuleImportController::upload - Failed to delete existing JSON before overwrite', ['json_path' => $jsonPath]);
// proceed to overwrite anyway by writing to the same path
// rename existing file before overwrite
if (file_exists($jsonPath)) {
$backupPath = $jsonPath . '.' . date('YmdHis') . '.bak';
if (!@rename($jsonPath, $backupPath)) {
$this->myLogger->logme(
'warning',
'RuleImportController::upload - Failed to rename existing JSON before overwrite',
[
'json_path' => $jsonPath,
'backup_path' => $backupPath
]
);
// continue anyway; writing to same path will overwrite
}
}
$finalJson = $jsonData;
} else {
@ -797,5 +815,15 @@ class RuleImportController extends AdminController
return $this->respond(['status' => false, 'code' => 400, 'message' => $error_message], 200);
}
}
public function checkRuleUsage()
{
$rule_id = $this->request->getGet('rule_id');
$count = $this->partnerPolicyModel->where('commission_applied_rule', $rule_id)
->countAllResults();
return $this->respond(['status' => true, 'code' => 200, 'count' => $count, 'message' => ""], 200);
}
}

View File

@ -318,7 +318,7 @@ class MailHelper
$attachments = isset($params['attachments']) ? $params['attachments'] : [];
$common = isset($params['common']) ? $params['common'] : '';
$bcc = isset($params['bcc']) ? $params['bcc'] : '';
$cc = isset($params['cc']) ? $params['cc'] : '';
$cc = (isset($params['cc']) && !empty($params['cc'])) ? $params['cc'] : '';
$from_address = isset($params['from_mail']) && !empty($params['from_mail']) ? $params['from_mail'] : getenv('email.fromEmail');
// $from_address = "claims@nhanceindia.in";

View File

@ -62,6 +62,7 @@ class RuleImportService
protected array $expectedColumns;
protected array $columnValidators;
protected array $incomingData;
protected string $annotatedDir;
protected string $department;
protected string $uploadedCommissionFileID;
@ -148,7 +149,7 @@ class RuleImportService
{
// dd($params);
$startTime = microtime(true);
$this->incomingData = $params;
try {
$this->department = $params['department'];
$this->uploadedCommissionFileID = $params['id'];
@ -898,13 +899,29 @@ class RuleImportService
*/
protected function convertRowToRule(array $rowData): array
{
$temp_rule_id = $this->generateRuleId($rowData);
//rule_e07f52d4366a2_32_oct2025_3_com
$conditions = $this->buildConditions($rowData);
$temp_rule_id .= '_'.$this->incomingData['id'].'_'. strtolower(date('MY')).'_'.(count($conditions));
$calculation = $this->buildCalculation($rowData);
$temp_rule_id .= '_'.substr($calculation['type'], 0, 3);
$rule = [
'id' => $this->generateRuleId($rowData),
'id' => $temp_rule_id,
'name' => $this->sanitize($rowData[self::COL_RULE_NAME] ?? ''),
'department' => $this->department,
'is_deleted' => false,
'file_id' => $this->uploadedCommissionFileID,
'conditions' => $this->buildConditions($rowData),
'calculation' => $this->buildCalculation($rowData)
'conditions' => $conditions,
'calculation' => $calculation,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
'created_by' => $this->incomingData['created_by'],
'updated_by' => $this->incomingData['created_by'],
];
return $rule;

View File

@ -38,4 +38,29 @@ class InvoiceItemModel extends Model
protected $validationMessages = [];
protected $skipValidation = false;
protected $beforeInsert = ["checkAndAddCreatedByValue"];
protected $beforeUpdate = ["checkAndUpdateUpdatedByValue"];
protected function checkAndAddCreatedByValue(array $data)
{
// Check if 'updated_by' value is null or empty
if (empty($data['data']['created_by'])) {
// Set 'updated_by' value to the current session user ID
$data['data']['created_by'] = get_session_userid();
}
return $data;
}
protected function checkAndUpdateUpdatedByValue(array $data)
{
// Check if 'updated_by' value is null or empty
if (empty($data['data']['updated_by'])) {
// Set 'updated_by' value to the current session user ID
$data['data']['updated_by'] = get_session_userid();
}
return $data;
}
}

View File

@ -29,7 +29,7 @@ class InvoiceModel extends Model
protected $useTimestamps = false;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
// Validation (optional)
protected $validationRules = [
'invoice_no' => 'required|max_length[100]',
@ -187,4 +187,57 @@ class InvoiceModel extends Model
return $data;
}
public function payoutList($flag, $agentId = null, $invoiceId = null)
{
$builder = $this->db->table('policy_transaction pt')
->select('
pt.id,
pt.policy_no AS policyNo,
pt.agent_id AS agentId,
pp.insured_name AS customer,
pp.premium_amount AS premium,
pp.commission_amount AS commission,
pp.issued_date AS date_db,
DATE_FORMAT(pp.issued_date, "%d/%m/%Y") AS date,
pii.id AS invoiceItemId
')
->join('partner_policy pp','pt.policy_no = pp.policy_number AND pt.agent_id = pp.agent_id','left')
->where('pt.is_active',1)
->where('pt.agent_id IS NOT NULL', null, false);
if ($flag == 1) { // Add mode
// EXCLUDE all policies that exist in partner_invoice_items
$builder->join('partner_invoice_items pii','pii.policy_no = pt.policy_no','left');
$builder->where("pt.policy_no NOT IN (SELECT policy_no FROM partner_invoice_items)", null, false);
}
if ($flag == 2) { // Edit mode
// Policies belonging to a specific invoice
$builder->select('pii.commission_amount as paid_amount');
$builder->join('partner_invoice_items pii','pii.policy_no = pt.policy_no','inner'); // must exist
$builder->join('partner_invoice pi','pi.id = pii.invoice_id','inner'); // must exist
$builder->where('pii.is_active',1);
if ($invoiceId) {
$builder->where('pi.id', $invoiceId); // only policies of this invoice
}
if ($agentId) {
$builder->where('pt.agent_id', $agentId);
}
}
if ($flag == 3) { // Extra policies
// Policies not assigned to any invoice
$builder->select('pii.commission_amount as paid_amount');
$builder->join('partner_invoice_items pii','pii.policy_no = pt.policy_no','left');
$builder->where('pii.id IS NULL', null, false);
if ($agentId) {
$builder->where('pt.agent_id', $agentId);
}
}
return $builder->get()->getResultArray();
}
}

View File

@ -36,9 +36,9 @@ table.dataTable thead th {
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="tickets-table">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">Advertisement Image Name</th>
<th class="font-weight-medium">Status</th>
<th class="font-weight-medium">Action</th>
<th class="font-weight-medium"><div class="column-header">Advertisement Image Name</div></th>
<th class="font-weight-medium"><div class="column-header">Status</div></th>
<th class="font-weight-medium"><div class="column-header">Action</div></th>
</tr>
</thead>
@ -222,7 +222,8 @@ var table;
$(document).ready(function() {
table = $('#tickets-table').DataTable({
dom: "<'row'<'col-sm-1'f><'col-sm-11 text-right'B>>" + // Filter left, button right
// dom: "<'row'<'col-sm-1'f><'col-sm-11 text-right'B>>" + // Filter left, button right
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [

View File

@ -17,6 +17,8 @@ table.dataTable thead th {
max-width: 98% !important;
}
.column-header {margin-right: 10px;}
.custom-dropdown-menu {
display: none;
position: absolute;
@ -70,15 +72,15 @@ table.dataTable thead th {
<table data-custom-table-css="table" id="scroll-horizontal-datatable" class="table w-100 nowrap text-custom-black text-custom app-datatable">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">S.No&nbsp;</th>
<th class="font-weight-medium">Client Name&nbsp;</th>
<th class="font-weight-medium">Insurer Name&nbsp;</th>
<th class="font-weight-medium">Insurer Branch Name&nbsp;</th>
<th class="font-weight-medium">Opening Date&nbsp;</th>
<th class="font-weight-medium">CD Account No&nbsp;</th>
<th class="font-weight-medium">Opening Amount&nbsp;</th>
<th class="font-weight-medium">Date/User&nbsp;</th>
<th class="font-weight-medium">Action&nbsp;</th>
<th class="font-weight-medium"> <div class="column-header">S.No&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Client Name&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Insurer Name&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Insurer Branch Name&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Opening Date&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">CD Account No&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Opening Amount&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Date/User&nbsp; </div> </th>
<th class="font-weight-medium"> <div class="column-header">Action&nbsp; </div> </th>
</tr>
</thead>
<tbody class="app-table-body">
@ -215,7 +217,7 @@ table.dataTable thead th {
$('#scroll-horizontal-datatable').DataTable({
scrollX: true,
dom: "<'row'<'col-sm-2'f><'col-sm-10 text-right'B>>" +
dom: "<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [

View File

@ -103,9 +103,9 @@ $(document).ready(function() {
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'

View File

@ -344,9 +344,9 @@ $(document).ready(function()
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
style="position:absolute; right:11px; top:50%; transform:translateY(-53%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'

View File

@ -560,6 +560,20 @@ input:checked + .slider_blue::before {
searching: true,
autoWidth: false,
responsive: true,
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block; width:100%;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666; display:none; cursor:pointer;"></i>
</div>
`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
},
});
@ -2543,6 +2557,20 @@ $(document).ready(function () {
searching: true,
autoWidth: false,
responsive: true,
// language: {
// search: `
// <div class="datatable-search-wrapper" style="position:relative; display:inline-block; width:100%;">
// _INPUT_
// <i class="mdi mdi-magnify datatable-search-icon"
// style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666;"></i>
// <i class="mdi mdi-close-circle datatable-clear-icon"
// style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666; display:none; cursor:pointer;"></i>
// </div>
// `,
// searchPlaceholder: "Search",
// emptyTable: '<div class="text-center text-muted">No Data found</div>'
// },
});

View File

@ -173,7 +173,7 @@
if (ticketsTable.length) {
ticketsTable.DataTable({
scrollX: true,
dom: "<'row'<'col-sm-2'f><'col-sm-10 text-right'B>>" +
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [{
@ -197,6 +197,20 @@
}],
paging: true,
pageLength: 10,
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block; width:100%;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none; cursor:pointer;"></i>
</div>
`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
},
order: [
[0, 'desc']
]

View File

@ -102,8 +102,18 @@ $(document).ready(function () {
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
language: {
search: "_INPUT_",
searchPlaceholder: "Search..."
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block; width:100%;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:122px !important; top:50%; transform:translateY(-50%); color:#666; display:none; cursor:pointer;"></i>
</div>
`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
},
paging: true,
pageLength: 10,

View File

@ -164,6 +164,15 @@
margin-left: 5px;
border-radius: 6px;
}
th.no-sort {
pointer-events: none; /* disable click */
}
th.no-sort:before,
th.no-sort:after {
display: none !important; /* hide DataTables sorting arrows */
}
</style>
<div class="container-fluid-min">
<div class="row" id="inception_list">
@ -205,11 +214,12 @@
</div>
<div class="dataTables_length d-flex align-items-center">
</div>
<div>
<div class="table-responsive">
<table data-custom-table-css="table" class="table table-striped mb-0 nowrap" cellspacing="0" id="tickets-table">
<thead class="bg-light">
<tr>
<th></th>
<th class="no-sort"></th>
<th>
<div class="column-header">Insurer</div>
</th>
@ -1734,9 +1744,24 @@
"order": [], // Disable initial sorting if needed
"pageLength": 10,
// "dom": '<"top"lf>rt<"bottom"ip><"clear">', // This places length and filter controls at top
"language": {
"lengthMenu": "Show _MENU_ entries",
"search": "Search:"
// "language": {
// "lengthMenu": "Show _MENU_ entries",
// "search": "Search:"
// }
language: {
lengthMenu: "Show _MENU_ entries",
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block; width:100%;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none; cursor:pointer;"></i>
</div>
`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
}
});

View File

@ -0,0 +1,970 @@
<style>
/* ---- CSS cleaned/optimized ---- */
.table th, .table td { padding: 8px; }
table.dataTable tbody td { padding: 4px 4px !important; }
.col-12 { max-width: 98% !important; }
.dataTables_filter { position: absolute; }
.column-header { margin-right: 10px; }
.filter-inline { display: flex; align-items: center; gap: 10px; }
.filter-inline input[type="date"] { padding: 8px 12px; border:1px solid #ddd; border-radius:4px; font-size:13px; background:white; cursor:pointer; }
.filter-inline input[type="date"]:focus { outline:none; border-color:#00a9a3; }
.icon-btn { width:36px; height:36px; border:1px solid #ddd; background:white; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.2s; }
.icon-btn:hover { background:#f5f5f5; border-color:#00a9a3; }
.icon-btn svg { width:18px; height:18px; fill:#666; }
.icon-btn:hover svg { fill:#00a9a3; }
.policy-list { border:1px solid #e0e0e0; border-radius:4px; overflow:hidden; margin-top:0; }
.policy-header { background:#f8f9fa; padding:12px 15px; font-weight:600; border-bottom:1px solid #e0e0e0; display:flex; align-items:center; font-size:14px; }
.policy-header input[type="checkbox"] { width:18px; height:18px; margin-right:10px; cursor:pointer; }
.policy-item { border-bottom:1px solid #e0e0e0; padding:12px 15px; display:flex; align-items:center; background:white; transition:background 0.2s; }
.policy-item:hover { background:#f9f9f9; }
.policy-item:last-child { border-bottom:none; }
.policy-item.selected { background:#e8f5f4; }
.policy-checkbox { width:18px; height:18px; margin-right:15px; cursor:pointer; }
.policy-info { flex:1; display:flex; justify-content:space-between; align-items:center; }
.policy-details { flex:1; }
.policy-number { font-weight:600; color:#333; margin-bottom:4px; font-size:14px; }
.policy-meta { color:#666; font-size:13px; }
.policy-amount { font-size:16px; font-weight:600; color:#00a9a3; margin-left:20px; }
.badge { display:inline-block; padding:4px 10px; border-radius:12px; font-size:12px; font-weight:600; margin-left:10px; }
.badge-selected { background:#00a9a3; color:white; }
.summary-bar { position:fixed; bottom:0; left:0; right:0; width:100%; background:white; border-top:2px solid #00a9a3; padding:15px 30px; display:none; box-shadow:0 -2px 10px rgba(0,0,0,0.1); z-index:100; }
.summary-bar.show { display:flex; justify-content:flex-start; align-items:center; }
.summary-info { margin-left:auto; margin-right:0; display:flex; gap:40px; align-items:center; }
.summary-item { display:flex; flex-direction:column; }
.summary-label { font-size:12px; color:#666; margin-bottom:2px; }
.summary-value { font-size:18px; font-weight:600; color:#00a9a3; }
.summary-actions { display:flex; gap:10px; margin-left:auto; }
.btn-icon { background:#008b8b; border:none; border-radius:50%; width:32px; height:32px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
.btn-icon i { color:#fff !important; font-size:18px; }
.btn-icon:hover { background:#00a3a3; }
/* Responsive */
@media (max-width:1024px){ .filter-inline{flex-wrap:wrap;} .filter-inline input[type="date"]{font-size:12px;padding:6px 8px;} }
@media (max-width:768px){
.section-header{flex-direction:column;gap:10px;align-items:flex-start;}
.header-right{width:100%;justify-content:space-between;}
.filter-inline{flex:1;}
.summary-bar{left:0;flex-direction:column;gap:15px;padding:15px;}
.summary-info{width:100%;justify-content:space-around;}
.summary-actions{width:100%;}
}
.policy-adjustment::-webkit-inner-spin-button,
.policy-adjustment::-webkit-outer-spin-button {-webkit-appearance:none;margin:0;}
.policy-adjustment { -moz-appearance:textfield; }
</style>
<div class="row" id="invoices_details">
<div class="col-12">
<div id="invoices_accordion" class="ml-3">
<div class="card mb-1">
<h4 class="m-1">
<a href="#" onclick="history.back(); return false;">
<i style="font-size: 18px;" class="mdi mdi-chevron-left" title="Back"></i>
</a>
<span>Invoice Details</span>
<a id="toggleIcon" class="text-dark float-right" data-toggle="collapse" href="#collapseOne"
aria-expanded="true">
<i id="icon" class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
</a>
</h4>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#invoices_accordion">
<div class="card-body" style="padding-bottom: unset;">
<div class="form-row">
<div class="form-group col-md-3">
<label>Invoice Number </label>
<input class="form-control" type="text" id="invoiceNo" placeholder="Auto-generated" readonly
value="<?= isset($invoice['invoice_no']) ? $invoice['invoice_no'] : '' ?>">
</div>
<div class="form-group col-md-3">
<label>Invoice Date <span class="text-danger"></span></label>
<input class="form-control" type="date" id="invoiceDate" required value="<?= isset($invoice['invoice_date']) ? $invoice['invoice_date'] : '' ?>" >
</div>
<div class="form-group col-md-3">
<label for="agents"> Agents <span class="text-danger"></span></label>
<select class="form-control" id="agentSelect" name="agents_id" onchange="loadPolicies()" required>
<option value="">Select Agent</option>
<?php
if(isset($agents) && count($agents)) {
foreach($agents as $agent): ?>
<option value="<?= $agent['id'] ?>"
<?= isset($invoice['agent_id']) && $invoice['agent_id'] == $agent['id'] ? 'selected' : '' ?>>
<?= $agent['name'] ?> - <?= $agent['agent_code'] ?>
</option>
<?php endforeach;
}
?>
</select>
</div>
<div class="form-group col-md-3">
<label>Policy Till Date<span class="text-danger"></span></label>
<input class="form-control" type="date" id="policyTillDate" required onchange="loadPolicies()" max="<?= date('Y-m-d') ?>"
value="<?= isset($invoice['invoice_date']) ? $invoice['invoice_date'] : '' ?>" >
</div>
<div class="form-group col-md-3">
<input class="form-control" type="hidden" id="invoiceID" required value="<?= isset($invoice['id']) ? $invoice['id'] : '' ?>" >
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row" id="payouts_details">
<div class="col-12">
<div id="policy_accordion" class="ml-3">
<div class="card mb-1">
<div class="row align-items-center m-1" id="policy_filter">
<div class="col d-flex align-items-center">
<h4 class="mb-0">
Policy Selection
<span id="selectedCount" class="badge badge-selected" style="display:none;">0 selected</span>
</h4>
</div>
<div class="col-auto d-flex align-items-center">
<input type="date" class="form-control mr-2" id="fromDate" placeholder="From Date" title="From Date" style="width:170px;">
<input type="date" class="form-control mr-2" id="toDate" placeholder="To Date" title="To Date" style="width:170px;">
<button type="button" class="btn-icon mr-1" title="Reset Filters" onclick="resetFilters()"><i class="mdi mdi-refresh"></i></button>
<button type="button" class="btn-icon mr-1" title="Search" onclick="searchPolicies()"><i class="mdi mdi-magnify"></i></button>
<!-- <button class="btn app-btn-primary mr-2" onclick="resetFilters()" title="Reset Filters">
<i class="ri-reset-right-fill"></i>
</button>
<button class="btn app-btn-primary mr-2" onclick="searchPolicies()" title="Search">
<i class="ri-search-line"></i>
</button> -->
<?php if(isset($type) && $type === 'edit'): ?>
<button class="btn app-btn-primary mr-2 M" onclick="showMorePolicies()" title="More Policies">
More Policies
</button>
<?php endif; ?>
</div>
<div class="col-auto">
<a id="toggleIcon" class="text-dark" data-toggle="collapse" href="#collapseTwo">
<i id="icon" class="mdi mdi-chevron-down text-primary" style="font-size: 28px;"></i>
</a>
</div>
</div>
<div id="collapseTwo" class="collapse show" aria-labelledby="headingTwo" data-parent="#policy_accordion">
<div class="card-body" style="padding-top: unset !important; border: white !important; background: unset !important;">
<div>
<table data-custom-table-css="table" class="table table-striped mb-0 nowrap" cellspacing="0" id="policy_table">
<!-- <table data-custom-table-css="table" class="table table-borderless mb-0 nowrap" id="policy_table"> -->
<thead>
<tr>
<th style="text-align:center;"><input type="checkbox" class="CB" id="selectAll" onchange="toggleSelectAll(this)"></th>
<th><div class="column-header">Policy No</div></th><!-- 2 -->
<th><div class="column-header">Customer</div></th> <!-- 3 -->
<th><div class="column-header">Premium</div></th> <!-- 4 -->
<th><div class="column-header">Policy Issues Date</div></th> <!-- 5 -->
<th><div class="column-header">Commission Amount</div></th> <!-- 6 -->
<?php if(isset($type) && $type === 'adjustment'): ?>
<th><div class="column-header">Paid Amount</div></th> <!-- 7 -->
<?php endif; ?>
</tr>
</thead>
<tbody id="policyListBody"></tbody>
</table>
<div id="summaryBar" class="summary-bar">
<div class="summary-info">
<div class="summary-item">
<span class="summary-label">Selected Policies</span>
<span class="summary-value" id="totalPolicies">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Total Commission</span>
<span class="summary-value" id="totalAmount">₹0.00</span>
</div>
<?php if(isset($type) && $type === 'adjustment'): ?>
<div class="summary-item">
<span class="summary-label">Total Paid Amount</span>
<span class="summary-value" id="totalAdjustmentAmount">₹0.00</span>
</div>
<?php endif; ?>
<?php if(isset($type) && $type !== 'add'): ?>
<div class="summary-item">
<button type="button" class="btn-icon mr-1 text-warning" data-toggle="modal" data-target="#auditingModal" onclick="auditingHistory()" title="History"><i class="mdi mdi-history"></i></button>
</div>
<?php endif; ?>
</div>
<div class="summary-actions">
<button type="button" class="btn app-btn-outline-primary C" onclick="clearSelection()">Clear</button>
<button type="button" class="btn app-btn-secondary S" onclick="saveInvoice()">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- end col -->
</div>
<div class="modal fade " id="auditingModal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg" >
<!-- style="max-width: 775px !important;"> -->
<div class="modal-content" style="border-radius:15px;">
<div class="modal-header" style="border-bottom-width:0;">
<h4 class="modal-title title-text ">Auditing History</h4>
<button type="button" class="close" data-dismiss="modal" aria-hidden="true" data-bs-dismiss="modal">×</button>
</div>
<div class="modal-body card-scroll history-container" style="padding:5px 11px 11px 11px!important;">
</div>
</div>
</div>
</div>
<script>
// ---- Internal state ----
let policyTable = null;
const selectedPolicies = new Set();
let filteredPolicies = [];
window.allPolicies = <?php echo json_encode($payouts); ?> || [];
window.extraPolicies = <?php echo json_encode($extra_payouts); ?> || [];
window.checkedPolicyNumbers = <?php echo json_encode($checked_policy_numbers); ?> || [];
const invoiceType = <?php echo json_encode($type); ?>;
const colspan = invoiceType == "adjustment" ? 7 : 6;
// ---- Safe DOM getter ----
function getEl(selector) {
const el = document.getElementById(selector) || document.querySelector(selector);
return el || null;
}
// ---- Generate invoice number ----
function generateInvoiceNumber() {
const invoiceNoEl = getEl('invoiceNo');
if (invoiceNoEl && invoiceType == 'add') invoiceNoEl.value = "<?= $invoice_number ?>";
}
// ---- Toggle collapsible sections ----
function toggleSection(sectionId) {
const content = getEl(sectionId);
const icon = getEl(sectionId + 'Icon');
if (!content) return;
content.classList.toggle('collapsed');
if (icon) icon.classList.toggle('collapsed');
}
// ---- Reload Data table ----
function reloadDataTable() {
// Destroy existing table
if ($.fn.DataTable && $.fn.DataTable.isDataTable('#policy_table')) {
try { $('#policy_table').DataTable().destroy(); } catch(e) {}
$('#policy_table').find('thead').show();
}
// -------- EXPORT LOGIC --------
let exportColumns = ':not(:first-child)';
let exportFormat = null;
if (invoiceType === 'adjustment') {
exportFormat = function (data, row, column, node) {
const input = node.querySelector('input');
return input ? `₹${Number(input.value || 0).toFixed(2)}` : data;
};
}
// --------------------------------
policyTable = $('#policy_table').DataTable({
scrollX: true,
paging: true,
searching: true,
ordering: false,
autoWidth: false,
stateSave: false,
dom:
"<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [
{
extend: 'collection',
text: '<span class="btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
className: 'btn app-btn-secondary',
buttons: [
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited"></i><span class="btn-custom"> CSV </span>',
className: 'app-btn-primary',
title: getExportFileName(),
exportOptions: {
columns: exportColumns,
format: exportFormat ? { body: exportFormat } : undefined
}
},
{
extend: 'excel',
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
className: 'app-btn-primary',
title: getExportFileName(),
sheetName: getExportFileName(),
exportOptions: {
columns: exportColumns,
format: exportFormat ? { body: exportFormat } : undefined
}
}
]
}
],
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
}
});
// FIX alignment
setTimeout(() => {
if (policyTable) policyTable.columns.adjust().draw(false);
}, 300);
}
// ---- Render policies in table ----
function renderPolicies() {
if (!policyTable) {
reloadDataTable(); // initialize DataTable if not yet
}
// Clear existing rows
policyTable.clear();
if (!filteredPolicies || filteredPolicies.length === 0) {
// Add "No policies found" row
policyTable.row.add([
"No policies found"
]).draw();
$('#policy_table tbody tr td').attr('colspan', colspan).addClass('text-center');
} else {
filteredPolicies.forEach(p => {
const checked = selectedPolicies.has(String(p.id)) ? 'checked' : '';
const row = [
`<input type="checkbox" class="policy-checkbox CB" ${checked} onchange="togglePolicy(${p.id})">`,
p.policyNo || '',
p.customer || '',
`₹${Number(p.premium || 0).toFixed(2)}`,
p.date || '',
`₹${Number(p.commission || 0).toFixed(2)}`
];
// Add adjustment input if needed
if (invoiceType === 'adjustment') {
row.push(`<input type="number"
class="policy-adjustment"
data-invoiceItemId="${p.invoiceItemId}"
value="${p.paid_amount ?? p.commission ?? 0}"
min="0"
oninput="updateSummary();">`);
}
policyTable.row.add(row);
});
policyTable.draw();
$('#policy_table tbody tr td:first-child').css('text-align', 'center');
}
// Update Select All checkbox visibility
const selectAllEl = getEl('selectAll');
if (selectAllEl) selectAllEl.style.display = filteredPolicies.length > 0 ? '' : 'none';
// Update select-all checkbox state & summary
updateSelectAllCheckbox();
updateSummary();
// ---- Make checkboxes readonly in adjustment mode ----
if (invoiceType === 'adjustment') {
$('#policy_table').on('draw.dt', function() {
document.querySelectorAll('.CB').forEach(cb => cb.disabled = true);
});
}
}
// ---- Filter policies by agent/date ----
function filterPolicies() {
const fromDate = getEl('fromDate')?.value || '';
const toDate = getEl('toDate')?.value || '';
const agentId = getEl('agentSelect')?.value || '';
const policyTillDate = getEl('policyTillDate')?.value || '';
if (!agentId || agentId === '0') return toastr.warning('Please select an agent', 'Required');
filteredPolicies = window.allPolicies.filter(p => {
if (String(p.agentId) !== String(agentId)) return false;
if (policyTillDate && p.date_db > policyTillDate) return false;
if (fromDate && p.date_db < fromDate) return false;
if (toDate && p.date_db > toDate) return false;
return true;
});
renderPolicies();
}
// ---- Load policies for selected agent ----
function loadPolicies() {
const agentId = getEl('agentSelect')?.value || '';
const policyTillDate = getEl('policyTillDate')?.value || '';
const list = getEl('policyListBody');
if (!list) return;
selectedPolicies.clear();
// Destroy DataTable
if (policyTable && $.fn.DataTable.isDataTable('#policy_table')) {
try { policyTable.destroy(); } catch(e) {}
policyTable = null;
}
list.innerHTML = '';
if (!agentId || agentId === '0') {
list.innerHTML = `<tr><td colspan="${colspan}" class="text-center text-danger">Please select an agent</td></tr>`;
updateSummary();
reloadDataTable();
return;
}
// Filter policies for the selected agent
filteredPolicies = window.allPolicies.filter(p => {
if (String(p.agentId) !== String(agentId)) return false;
if (policyTillDate && p.date_db > policyTillDate) return false;
return true;
});
if (invoiceType != 'add') {
filteredPolicies.forEach(p => {
if (window.checkedPolicyNumbers.includes(p.policyNo)) {
selectedPolicies.add(String(p.id));
}
});
}
renderPolicies();
}
// ---- Toggle select-all checkbox ----
function toggleSelectAll() {
const selectAllBox = getEl('selectAll');
if (!selectAllBox) return;
const selectAll = selectAllBox.checked;
if (selectAll) {
filteredPolicies.forEach(p => selectedPolicies.add(String(p.id)));
} else {
filteredPolicies.forEach(p => selectedPolicies.delete(String(p.id)));
}
renderPolicies();
}
// ---- Toggle single policy ----
function togglePolicy(policyId) {
policyId = String(policyId);
if (selectedPolicies.has(policyId)) selectedPolicies.delete(policyId);
else selectedPolicies.add(policyId);
renderPolicies();
}
// ---- Update select-all header ----
function updateSelectAllCheckbox() {
const selectAllCheckbox = getEl('selectAll');
if (!selectAllCheckbox) return;
selectAllCheckbox.checked = filteredPolicies.length > 0 &&
filteredPolicies.every(p => selectedPolicies.has(String(p.id)));
}
// ---- Update summary footer ----
function updateSummary() {
const summaryBar = getEl('summaryBar');
const selectedCountBadge = getEl('selectedCount');
const totalPoliciesEl = getEl('totalPolicies');
const totalAmountEl = getEl('totalAmount');
let totalAdjustmentAmountEl;
if (invoiceType === 'adjustment') { totalAdjustmentAmountEl = getEl('totalAdjustmentAmount');}
if (!summaryBar || !totalPoliciesEl || !totalAmountEl) return;
if (selectedPolicies.size === 0) {
summaryBar.classList.remove('show')
const summaryBar = getEl('summaryBar');;
if (selectedCountBadge) selectedCountBadge.style.display = 'none';
totalPoliciesEl.textContent = '0';
totalAmountEl.textContent = '₹0.00';
if (invoiceType === 'adjustment') totalAdjustmentAmountEl.textContent = '₹0.00';
return;
}
if (selectedCountBadge) {
selectedCountBadge.style.display = 'inline-block';
selectedCountBadge.textContent = `${selectedPolicies.size} selected`;
}
summaryBar.classList.add('show');
// TOTAL COMMISSION AMOUNT
const totalAmount = filteredPolicies
.filter(p => selectedPolicies.has(String(p.id)))
.reduce((sum, p) => sum + Number(p.commission || 0), 0);
totalPoliciesEl.textContent = selectedPolicies.size;
totalAmountEl.textContent = `₹${totalAmount.toFixed(2)}`;
// TOTAL ADJUSTMENT AMOUNT
if (invoiceType === 'adjustment') {
// read all input boxes
let totalAdjustment = 0;
document.querySelectorAll('.policy-adjustment').forEach(input => {
const val = parseFloat(input.value) || 0;
totalAdjustment += val;
});
totalAdjustmentAmountEl.textContent = `₹${totalAdjustment.toFixed(2)}`;
// const difference = totalAmount - totalAdjustment;
// totalAdjustmentAmountEl.textContent = `₹${difference.toFixed(2)}`;
}
}
// ---- Clear selection ----
function clearSelection() {
const count = selectedPolicies.size;
if (count === 0) return toastr.info('No policies selected.', 'Info');
if (count === 1) return toastr.warning('At least one policy must remain selected.', 'Warning');
if (!confirm('Are you sure you want to clear all selections?')) return;
selectedPolicies.clear();
renderPolicies();
}
// ---- Reset filters ----
function resetFilters() {
['fromDate','toDate','from_date','to_date'].forEach(id => {
if (getEl(id)) getEl(id).value = '';
});
loadPolicies();
}
// ---- Show More Policies ----
function showMorePolicies() {
const agentId = getEl('agentSelect')?.value || '';
if (!agentId) return toastr.warning('Please select an agent', 'Required');
if (!window.extraPolicies || window.extraPolicies.length === 0) {
return toastr.info('No more policies available.', 'Information');
}
// Get existing policy IDs in filteredPolicies
const existingPolicyIds = new Set(filteredPolicies.map(p => p.id));
// Filter extraPolicies: same agent + not already in filteredPolicies
const newPolicies = window.extraPolicies.filter(p => {
return String(p.agentId) === String(agentId) && !existingPolicyIds.has(p.id);
});
if (newPolicies.length === 0) {
toastr.info('No more policies available for this agent.', 'Information');
return;
}
// Append new policies to filteredPolicies
filteredPolicies = [...filteredPolicies, ...newPolicies];
// In EDIT mode, pre-check any policies that already have invoiceItemId
if (invoiceType != 'add') {
filteredPolicies.forEach(p => {
if (p.invoiceItemId && !selectedPolicies.has(String(p.id))) {
selectedPolicies.add(String(p.id));
}
});
}
// Re-render table
renderPolicies();
}
// ---- Save invoice ----
function saveInvoice() {
const agentId = getEl('agentSelect')?.value || '';
const invoiceNo = getEl('invoiceNo')?.value || '';
const invoiceDate = getEl('invoiceDate')?.value || '';
const policyTillDate = getEl('policyTillDate')?.value || '';
const invoiceID = getEl('invoiceID')?.value || '';
if (!agentId || agentId === '0') return toastr.warning('Please select an agent', 'Required');
if (!invoiceDate) return toastr.warning('Please select invoice date', 'Required');
if (selectedPolicies.size === 0) return toastr.warning('Please select at least one policy', 'Required');
const selectedPolicyData = filteredPolicies.filter(p => selectedPolicies.has(String(p.id)));
// Map policies and compute totalAmount using adjustment if applicable
let totalAmount = 0;
const policies = selectedPolicyData.map(p => {
let finalAmount = Number(p.commission || 0);
if (invoiceType === 'adjustment') {
const adjustmentInput = document.querySelector(`.policy-adjustment[data-invoiceItemId="${p.invoiceItemId}"]`);
const adjustmentValue = adjustmentInput ? parseFloat(adjustmentInput.value) : null;
if (adjustmentValue !== null && !isNaN(adjustmentValue)) {
finalAmount = adjustmentValue;
}
}
totalAmount += finalAmount;
return {
policy_id: p.id,
policy_no: p.policyNo,
commission_amount: finalAmount
};
});
const invoiceData = {
invoice_id: invoiceID,
invoice_no: invoiceNo,
agent_id: agentId,
invoice_date: invoiceDate,
invoice_amount: totalAmount,
policies: policies
};
// console.log(invoiceData);return true;
$.ajax({
url: '<?= base_url('payout/invoices/save') ?>',
type: 'POST',
data: JSON.stringify(invoiceData),
contentType: 'application/json',
success: function(response) {
// alert(`Invoice created successfully!\n\nInvoice No: ${invoiceNo}`);
toastr.success(response.message, 'Success');
window.location.href = '<?= base_url('payout/list') ?>';
},
error: function() {
// alert('Error creating invoice');
toastr.error(response.message, 'Invoice Error');
}
});
}
// ---- Expose functions globally ----
window.loadPolicies = loadPolicies;
window.filterPolicies = filterPolicies;
window.resetFilters = resetFilters;
window.toggleSection = toggleSection;
window.clearSelection = clearSelection;
window.saveInvoice = saveInvoice;
window.generateInvoiceNumber = generateInvoiceNumber;
window.toggleSelectAll = toggleSelectAll;
window.togglePolicy = togglePolicy;
// ---- DOM Ready Initialization ----
document.addEventListener('DOMContentLoaded', function () {
selectedPolicies.clear();
filteredPolicies = [];
const today = new Date().toISOString().split("T")[0];
const invoiceDateEl = getEl('invoiceDate');
const policyTillDateEl = getEl('policyTillDate');
const agentSelectEl = getEl('agentSelect');
// Set max date (today)
if (invoiceDateEl) invoiceDateEl.max = today;
if (policyTillDateEl) policyTillDateEl.max = today;
if (invoiceType == 'add') {
// -------- ADD MODE --------
if (agentSelectEl) agentSelectEl.value = '';
if (invoiceDateEl) invoiceDateEl.required = true;
if (policyTillDateEl) policyTillDateEl.required = true;
if (agentSelectEl) agentSelectEl.required = true;
// Enable Select2
$('#agentSelect').prop('disabled', false).select2();
}
else {
// -------- EDIT,ADJUSTMENT MODE --------
if (agentSelectEl) agentSelectEl.readOnly = true;
// Disable Select2 (readonly)
$('#agentSelect').prop('disabled', true);
// Remove red star (*) from label
const agentLabel = document.querySelector('label[for="agents"] .text-danger');
if (agentLabel) agentLabel.remove();
}
generateInvoiceNumber();
if (invoiceType == 'add') {
if (getEl('invoiceDate')) getEl('invoiceDate').valueAsDate = new Date();
if (getEl('policyTillDate')) getEl('policyTillDate').valueAsDate = new Date();
}
const agentEl = getEl('agentSelect');
if (agentEl) {
agentEl.addEventListener('change', () => {
selectedPolicies.clear();
loadPolicies();
});
}
['fromDate','toDate'].forEach(id => {
const el = getEl(id);
if (el) el.addEventListener('change', filterPolicies);
});
if (policyTillDateEl) policyTillDateEl.addEventListener('change', loadPolicies);
if (agentEl && agentEl.value && agentEl.value !== '0') loadPolicies();
else {
const body = getEl('policyListBody');
if (body) body.innerHTML = `<tr><td colspan="${colspan}" class="text-center">Please select an agent</td></tr>`;
reloadDataTable();
}
});
</script>
<script>
document.addEventListener('DOMContentLoaded', function () {
const freeze = <?= isset($freeze_edit) ? (int)$freeze_edit : 0 ?>;
const type = '<?= isset($type) ? $type : '' ?>';
if (freeze > 0 && type === 'edit') {
console.log("Freeze edit active");
// Wait until DataTable is initialized
$('#policy_table').on('draw.dt', function() {
document.querySelectorAll('.CB').forEach(cb => cb.disabled = true);
});
// Hide summary bar actions
document.querySelector('.S').style.display = 'none'; // hides it
document.querySelector('.C').style.display = 'none';
document.querySelector('.M').style.display = 'none'; // hides it
}
});
</script>
<script>
function auditingHistory() {
const params = new URLSearchParams(window.location.search);
const InvoiceID = params.get('id'); // "12"
$.ajax({
url: "<?= base_url('payout/invoices/history?id=') ?>" + InvoiceID,
type: "GET",
dataType: "json",
success: function(response) {
let container = $(".history-container");
container.empty();
const invoiceArray = response?.data?.invoice || [];
const childArray = response?.data?.invoice_child || [];
// ---------- FIXED CONDITION ----------
if (
response.status === "success" &&
(invoiceArray.length > 0 || childArray.length > 0)
) {
let table = `
<div class="table-responsive">
<table data-custom-table-css="table"
class="table w-100 nowrap text-custom-black text-custom app-datatable">
<thead class="bg-light">
<!-- MAIN TITLE
<tr>
<th colspan="6" class="text-center"
style="font-size:22px; font-weight:700;">
Auditing History
</th>
</tr>
-->
<!-- INVOICE TITLE -->
<tr>
<th colspan="6" class="text-left"
style="font-size:18px; font-weight:600;">
Invoice No: ${invoiceArray[0]?.invoice_no ?? '-'}
</th>
</tr>
<tr>
<th>S.No</th>
<th>Field Name</th>
<th>Old Value</th>
<th>New Value</th>
<th>Updated By</th>
<th>Date/Time</th>
</tr>
</thead>
<tbody class="app-table-body">`;
// ============= MAIN INVOICE HISTORY =============
invoiceArray.forEach((row, index) => {
table += `
<tr>
<td>${index + 1}</td>
<td>${row.field_name.replace(/_/g, " ")}</td>
<td>${row.old_value ?? '-'}</td>
<td>${row.new_value ?? '-'}</td>
<td>${row.created_name ?? '-'}</td>
<td>${formatDate(row.created_at)}</td>
</tr>`;
});
// ============= CHILD ITEMS HISTORY =============
if (childArray.length > 0) {
table += `
<tr>
<th colspan="6" style="font-size:18px; font-weight:600;background:#eee;">
Invoice Item Changes
</th>
</tr>`;
childArray.forEach((row, index) => {
table += `
<tr>
<td>${index + 1}</td>
<td>${row.field_name.replace(/_/g, " ")}</td>
<td>${row.old_value ?? '-'}</td>
<td>${row.new_value ?? '-'}</td>
<td>${row.created_name ?? '-'}</td>
<td>${formatDate(row.created_at)}</td>
</tr>`;
});
}
table += `
</tbody>
</table>
</div>`;
container.html(table); // Render final table
}
else {
// ---------- NO DATA ----------
container.html(`
<div class="d-flex justify-content-center align-items-center"
style="height: 50vh; width:100%;">
<p class="text-center font-color-black">
<i>No history found</i>
</p>
</div>
`);
}
$("#auditingModal").modal("show");
},
error: function(xhr) {
$(".history-container").html(`
<div class="d-flex justify-content-center align-items-center"
style="height: 50vh; width:100%;">
<p class="text-center font-color-black"><i>No Data Found</i></p>
</div>
`);
$("#auditingModal").modal("show");
console.error(xhr.responseText);
}
});
}
// ----------------- DATE FORMAT -----------------
function formatDate(dateString) {
if (!dateString) return "-";
let dt = new Date(dateString);
let dateStr = dt.toLocaleDateString("en-GB", {
day: "2-digit",
month: "long",
year: "numeric"
});
let timeStr = dt.toLocaleTimeString("en-US", {
hour: "2-digit",
minute: "2-digit",
hour12: true
}).toLowerCase();
return `${dateStr} ${timeStr}`;
}
function getExportFileName() {
let d = new Date();
let day = String(d.getDate()).padStart(2, '0');
let month = String(d.getMonth() + 1).padStart(2, '0');
let year = d.getFullYear();
return `Invoice Policies List ${day}-${month}-${year}`;
}
</script>

View File

@ -0,0 +1,543 @@
<style>
/* ---- CSS cleaned/optimized ---- */
.table th, .table td { padding: 8px; }
table.dataTable tbody td { padding: 4px 4px !important; }
.col-12 { max-width: 98% !important; }
.dataTables_filter { position: absolute; }
.column-header { margin-right: 10px; }
.filter-inline { display: flex; align-items: center; gap: 10px; }
.filter-inline input[type="date"] { padding: 8px 12px; border:1px solid #ddd; border-radius:4px; font-size:13px; background:white; cursor:pointer; }
.filter-inline input[type="date"]:focus { outline:none; border-color:#00a9a3; }
.icon-btn { width:36px; height:36px; border:1px solid #ddd; background:white; border-radius:4px; cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.2s; }
.icon-btn:hover { background:#f5f5f5; border-color:#00a9a3; }
.icon-btn svg { width:18px; height:18px; fill:#666; }
.icon-btn:hover svg { fill:#00a9a3; }
.policy-list { border:1px solid #e0e0e0; border-radius:4px; overflow:hidden; margin-top:0; }
.policy-header { background:#f8f9fa; padding:12px 15px; font-weight:600; border-bottom:1px solid #e0e0e0; display:flex; align-items:center; font-size:14px; }
.policy-header input[type="checkbox"] { width:18px; height:18px; margin-right:10px; cursor:pointer; }
.policy-item { border-bottom:1px solid #e0e0e0; padding:12px 15px; display:flex; align-items:center; background:white; transition:background 0.2s; }
.policy-item:hover { background:#f9f9f9; }
.policy-item:last-child { border-bottom:none; }
.policy-item.selected { background:#e8f5f4; }
.policy-checkbox { width:18px; height:18px; margin-right:15px; cursor:pointer; }
.policy-info { flex:1; display:flex; justify-content:space-between; align-items:center; }
.policy-details { flex:1; }
.policy-number { font-weight:600; color:#333; margin-bottom:4px; font-size:14px; }
.policy-meta { color:#666; font-size:13px; }
.policy-amount { font-size:16px; font-weight:600; color:#00a9a3; margin-left:20px; }
.badge { display:inline-block; padding:4px 10px; border-radius:12px; font-size:12px; font-weight:600; margin-left:10px; }
.badge-selected { background:#00a9a3; color:white; }
.summary-bar { position:fixed; bottom:0; left:0; right:0; width:100%; background:white; border-top:2px solid #00a9a3; padding:15px 30px; display:none; box-shadow:0 -2px 10px rgba(0,0,0,0.1); z-index:100; }
.summary-bar.show { display:flex; justify-content:flex-start; align-items:center; }
.summary-info { margin-left:auto; margin-right:0; display:flex; gap:40px; align-items:center; }
.summary-item { display:flex; flex-direction:column; }
.summary-label { font-size:12px; color:#666; margin-bottom:2px; }
.summary-value { font-size:18px; font-weight:600; color:#00a9a3; }
.summary-actions { display:flex; gap:10px; margin-left:auto; }
.btn-icon { background:#008b8b; border:none; border-radius:50%; width:32px; height:32px; display:flex; align-items:center; justify-content:center; cursor:pointer; }
.btn-icon i { color:#fff !important; font-size:18px; }
.btn-icon:hover { background:#00a3a3; }
/* Responsive */
@media (max-width:1024px){ .filter-inline{flex-wrap:wrap;} .filter-inline input[type="date"]{font-size:12px;padding:6px 8px;} }
@media (max-width:768px){
.section-header{flex-direction:column;gap:10px;align-items:flex-start;}
.header-right{width:100%;justify-content:space-between;}
.filter-inline{flex:1;}
.summary-bar{left:0;flex-direction:column;gap:15px;padding:15px;}
.summary-info{width:100%;justify-content:space-around;}
.summary-actions{width:100%;}
}
</style>
<!-- ---- HTML structure remains mostly same ---- -->
<div class="row" id="invoices_details">
<div class="col-12">
<div id="invoices_accordion" class="ml-3">
<div class="card mb-1">
<h4 class="m-1">
<a href="#" onclick="history.back(); return false;">
<i style="font-size: 18px;" class="mdi mdi-chevron-left" title="Back"></i>
</a>
<span>Invoice Details</span>
<a id="toggleIcon" class="text-dark float-right" data-toggle="collapse" href="#collapseOne"
aria-expanded="true">
<i id="icon" class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
</a>
</h4>
<div id="collapseOne" class="collapse show" aria-labelledby="headingOne" data-parent="#invoices_accordion">
<div class="card-body" style="padding-bottom: unset;">
<div class="form-row">
<div class="form-group col-md-3">
<label>Invoice Number </label>
<input class="form-control" type="text" id="invoiceNo" placeholder="Auto-generated" readonly>
</div>
<div class="form-group col-md-3">
<label>Invoice Date <span class="text-danger"></span></label>
<input class="form-control" type="date" id="invoiceDate" required value="" >
</div>
<div class="form-group col-md-3">
<label for="agents"> Agents <span class="text-danger"></span></label>
<select class="form-control" id="agentSelect" name="agents_id" onchange="loadPolicies()" required>
<option value="">Select Agent</option>
<?php
if(isset($agents) && count($agents)) {
foreach($agents as $agent): ?>
<option value="<?= $agent['id'] ?>">
<?= $agent['name'] ?> - <?= $agent['agent_code'] ?>
</option>
<?php endforeach;
}
?>
</select>
</div>
<div class="form-group col-md-3">
<label>Policy Till Date<span class="text-danger"></span></label>
<input class="form-control" type="date" id="policyTillDate" required onchange="loadPolicies()" max="<?= date('Y-m-d') ?>" >
</div>
<div class="form-group col-md-3">
<input class="form-control" type="hidden" id="invoiceID" required value="<?= isset($invoice['id']) ? $invoice['id'] : '' ?>" >
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row" id="payouts_details">
<div class="col-12">
<div id="policy_accordion" class="ml-3">
<div class="card mb-1">
<div class="row align-items-center m-1" id="policy_filter">
<div class="col d-flex align-items-center">
<h4 class="mb-0">
Policy Selection
<span id="selectedCount" class="badge badge-selected" style="display:none;">0 selected</span>
</h4>
</div>
<div class="col-auto d-flex align-items-center">
<input type="date" class="form-control mr-2" id="fromDate" placeholder="From Date" title="From Date" style="width:170px;">
<input type="date" class="form-control mr-2" id="toDate" placeholder="To Date" title="To Date" style="width:170px;">
<button type="button" class="btn-icon mr-1" title="Reset Filters" onclick="resetFilters()"><i class="mdi mdi-refresh"></i></button>
<button type="button" class="btn-icon mr-1" title="Search" onclick="searchPolicies()"><i class="mdi mdi-magnify"></i></button>
</div>
<a id="toggleIcon" class="text-dark float-right" data-toggle="collapse" href="#collapseTwo" aria-expanded="true">
<i id="icon" class="mdi mdi-chevron-down mr-1 text-primary" style="font-size: 28px;"></i>
</a>
</div>
<div id="collapseTwo" class="collapse show" aria-labelledby="headingTwo" data-parent="#policy_accordion">
<div class="card-body" style="padding-top: unset !important; border: white !important; background: unset !important;">
<div>
<div>
<table data-custom-table-css="table" class="table table-striped mb-0 nowrap" cellspacing="0" id="tickets-table">
<thead class="bg-light">
<tr>
<th style="text-align:center;"><input type="checkbox" class="CB" id="selectAll" onchange="toggleSelectAll(this)"></th>
<th><div class="column-header">Policy No</div></th> <!-- 2 -->
<th><div class="column-header">Customer</div></th> <!-- 3 -->
<th><div class="column-header">Premium</div></th> <!-- 4 -->
<th><div class="column-header">Policy Issues Date</div></th> <!-- 5 -->
<th><div class="column-header">Commission Amount</div></th> <!-- 6 -->
</tr>
</thead>
<tbody id="ticketListBody">
</tbody>
</table>
</div>
<div id="summaryBar" class="summary-bar">
<div class="summary-info">
<div class="summary-item">
<span class="summary-label">Selected Policies</span>
<span class="summary-value" id="totalPolicies">0</span>
</div>
<div class="summary-item">
<span class="summary-label">Total Commission</span>
<span class="summary-value" id="totalAmount">₹0.00</span>
</div>
</div>
<div class="summary-actions">
<button type="button" class="btn app-btn-outline-primary C" onclick="clearSelection()">Clear</button>
<button type="button" class="btn app-btn-secondary S" onclick="saveInvoice()">Submit</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div><!-- end col -->
</div>
<script>
let selectedPolicies = new Set();
let filteredPolicies = [];
let allPolicies = <?php echo json_encode($payouts); ?> || [];
// Helper
function getEl(id) { return document.getElementById(id); }
$(document).ready(function(){
$('#agentSelect').select2 && $('#agentSelect').select2();
const today = new Date().toISOString().split("T")[0];
if (getEl('agentSelect')) getEl('agentSelect').value = "";
if (getEl('invoiceNo')) getEl('invoiceNo').value = "<?= $invoice_number ?>";
if (getEl('invoiceDate')) getEl('invoiceDate').max = today;
if (getEl('invoiceDate')) getEl('invoiceDate').valueAsDate = new Date();
if (getEl('policyTillDate')) getEl('policyTillDate').max = today;
if (getEl('policyTillDate')) getEl('policyTillDate').valueAsDate = new Date();
// Initial empty DataTable so buttons/search are available even before loading policies
if ($.fn.DataTable.isDataTable('#tickets-table')) {
$('#tickets-table').DataTable().clear().destroy();
}
$('#tickets-table').DataTable({
dom:
"<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [
{
extend: 'collection',
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
className: 'btn app-btn-secondary ',
buttons: [
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited"></i><span class="btn-custom"> CSV </span>',
className: 'app-btn-primary',
title: getExportFileName(),
exportOptions: {columns: ':not(:first-child)'}
},
{
extend: 'excel',
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
className: 'app-btn-primary',
title: getExportFileName(),
filename: getExportFileName(),
exportOptions: {columns: ':not(:first-child)'}
}
]
}
],
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
},
paging: false,
ordering: false,
});
});
// MAIN: load and render policies for selected agent / date
function loadPolicies() {
let agentEl = getEl('agentSelect');
let policyTillDateEl = getEl('policyTillDate');
let list = getEl('ticketListBody');
if (!list) return;
let agentId = agentEl ? agentEl.value : '';
let policyTillDate = policyTillDateEl ? policyTillDateEl.value : '';
selectedPolicies.clear();
if (!agentId || agentId === '' || agentId === '0') {
// Destroy DataTable so table returns to normal
if ($.fn.DataTable.isDataTable('#tickets-table')) {
$('#tickets-table').DataTable().clear().destroy();
}
list.innerHTML = `<tr><td colspan="6" class="text-center text-danger">Please select an agent</td></tr>`;
reinitDataTable();
updateSummary();
return;
}
filteredPolicies = allPolicies.filter(p => String(p.agentId) === String(agentId));
let rows = '';
filteredPolicies.forEach(p => {
const idStr = String(p.id);
const checked = selectedPolicies.has(idStr) ? 'checked' : '';
rows += `
<tr>
<td style="text-align:center;">
<input type="checkbox" class="policy-checkbox CB"
value="${idStr}" ${checked}
onchange="togglePolicy(this.value, this.checked)">
</td>
<td>${p.policyNo || ''}</td>
<td>${p.customer || ''}</td>
<td>${Number(p.premium || 0).toFixed(2)}</td>
<td>${p.date || ''}</td>
<td>${Number(p.commission || 0).toFixed(2)}</td>
</tr>
`;
});
// Destroy DataTable BEFORE inserting rows
if ($.fn.DataTable.isDataTable('#tickets-table')) {
$('#tickets-table').DataTable().clear().destroy();
}
// Insert rows
list.innerHTML = rows;
// Reinitialize AFTER writing rows
reinitDataTable();
updateSelectAllCheckbox();
updateSummary();
}
// DataTable re-init helper (destroy + create)
function reinitDataTable() {
if ($.fn.DataTable.isDataTable('#tickets-table')) {
try {
$('#tickets-table').DataTable().clear().destroy();
} catch (e) {
// ignore destroy errors
}
}
$('#tickets-table').DataTable({
dom:
"<'row'<'col-sm-6'f><'col-sm-6 text-right'B>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [
{
extend: 'collection',
text: '<span class=" btn-custom"> Export </span><i class="mdi mdi-menu-down"></i>',
className: 'btn app-btn-secondary ',
buttons: [
{
extend: 'csv',
text: '<i class="mdi mdi-file-delimited"></i><span class="btn-custom"> CSV </span>',
className: 'app-btn-primary',
title: getExportFileName(),
exportOptions: {columns: ':not(:first-child)'}
},
{
extend: 'excel',
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
className: 'app-btn-primary',
title: getExportFileName(),
filename: getExportFileName(),
exportOptions: {columns: ':not(:first-child)'}
}
]
}
],
language: {
search: `
<div class="datatable-search-wrapper" style="position:relative; display:inline-block;">
_INPUT_
<i class="mdi mdi-magnify datatable-search-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666;"></i>
<i class="mdi mdi-close-circle datatable-clear-icon"
style="position:absolute; right:10px; top:50%; transform:translateY(-50%); color:#666; display:none;"></i>
</div>`,
searchPlaceholder: "Search",
emptyTable: '<div class="text-center text-muted">No Data found</div>'
},
paging: false,
ordering: false,
// After init, align first column
initComplete: function() {
$('#tickets-table tbody tr td:first-child').css('text-align', 'center');
}
});
}
// Toggle single policy selection (called from checkbox onchange)
function togglePolicy(policyId, checked) {
policyId = String(policyId);
if (checked === undefined) {
// if called without the checked param, toggle based on presence
if (selectedPolicies.has(policyId)) selectedPolicies.delete(policyId);
else selectedPolicies.add(policyId);
} else {
if (checked) selectedPolicies.add(policyId);
else selectedPolicies.delete(policyId);
}
// update UI elements that reflect selection
updateSelectAllCheckbox();
updateSummary();
}
// Select / deselect all currently filtered policies
function toggleSelectAll(sourceCheckbox) {
if (!filteredPolicies || filteredPolicies.length === 0) return;
if (sourceCheckbox.checked) {
filteredPolicies.forEach(p => selectedPolicies.add(String(p.id)));
} else {
filteredPolicies.forEach(p => selectedPolicies.delete(String(p.id)));
}
// update all visible checkboxes to match selection
document.querySelectorAll('#ticketListBody .policy-checkbox').forEach(cb => {
cb.checked = selectedPolicies.has(cb.value);
});
updateSummary();
}
// Update header select-all checkbox state
function updateSelectAllCheckbox() {
const selectAllCheckbox = getEl('selectAll');
if (!selectAllCheckbox) return;
const allSelected = filteredPolicies.length > 0 && filteredPolicies.every(p => selectedPolicies.has(String(p.id)));
selectAllCheckbox.checked = !!allSelected;
}
// Update summary UI (footer/summaryBar)
function updateSummary() {
const summaryBar = getEl('summaryBar');
const selectedCountBadge = getEl('selectedCount');
const totalPoliciesEl = getEl('totalPolicies');
const totalAmountEl = getEl('totalAmount');
if (!totalPoliciesEl || !totalAmountEl) {
return;
}
if (selectedPolicies.size === 0) {
if (summaryBar) summaryBar.classList && summaryBar.classList.remove('show');
if (selectedCountBadge) selectedCountBadge.style.display = 'none';
totalPoliciesEl.textContent = '0';
totalAmountEl.textContent = '₹0.00';
return;
}
if (selectedCountBadge) {
selectedCountBadge.style.display = 'inline-block';
selectedCountBadge.textContent = `${selectedPolicies.size} selected`;
}
if (summaryBar) summaryBar.classList && summaryBar.classList.add('show');
const totalAmount = filteredPolicies
.filter(p => selectedPolicies.has(String(p.id)))
.reduce((sum, p) => sum + Number(p.commission || 0), 0);
totalPoliciesEl.textContent = String(selectedPolicies.size);
totalAmountEl.textContent = `₹${totalAmount.toFixed(2)}`;
}
// Clear selection
function clearSelection() {
if (!confirm('Are you sure you want to clear the selection?')) return;
selectedPolicies.clear();
// uncheck all checkboxes in view
document.querySelectorAll('#ticketListBody .policy-checkbox').forEach(cb => cb.checked = false);
updateSelectAllCheckbox();
updateSummary();
}
// Reset filters (keeps agent selection; clear others)
function resetFilters() {
['fromDate','toDate','from_date','to_date','policyTillDate'].forEach(id => {
const el = getEl(id);
if (el) el.value = '';
});
loadPolicies();
}
// Save invoice (AJAX)
function saveInvoice() {
const agentId = getEl('agentSelect')?.value || '';
const invoiceNo = getEl('invoiceNo')?.value || '';
const invoiceDate = getEl('invoiceDate')?.value || '';
const policyTillDate = getEl('policyTillDate')?.value || '';
const invoiceID = '';
if (!agentId || agentId === '0') return toastr.warning('Please select an agent', 'Required');
if (!invoiceDate) return toastr.warning('Please select invoice date', 'Required');
if (selectedPolicies.size === 0) return toastr.warning('Please select at least one policy', 'Required');
const selectedPolicyData = filteredPolicies.filter(p => selectedPolicies.has(String(p.id)));
let totalAmount = 0;
const policies = selectedPolicyData.map(p => {
let finalAmount = Number(p.commission || 0);
totalAmount += finalAmount;
return {
policy_id: p.id,
policy_no: p.policyNo,
commission_amount: finalAmount
};
});
const invoiceData = {
invoice_id: invoiceID,
invoice_no: invoiceNo,
agent_id: agentId,
invoice_date: invoiceDate,
invoice_amount: totalAmount,
policies: policies
};
$.ajax({
url: '<?= base_url('payout/invoices/save') ?>',
type: 'POST',
data: JSON.stringify(invoiceData),
contentType: 'application/json',
success: function(response) {
// toastr.success(`Invoice created successfully!<br>Invoice No: ${invoiceNo}`, 'Success');
toastr.success(response.message, 'Success');
window.location.href = '<?= base_url('payout/list') ?>';
},
error: function() {
toastr.error(response.message, 'Invoice Error');
// toastr.error('An error occurred while creating the invoice. Please try again.','Invoice Error');
}
});
}
function getExportFileName() {
let d = new Date();
let day = String(d.getDate()).padStart(2, '0');
let month = String(d.getMonth() + 1).padStart(2, '0');
let year = d.getFullYear();
return `Invoice Policies List ${day}-${month}-${year}`;
}
</script>

View File

@ -1368,7 +1368,7 @@
let test_mail_list = $('#common_mail').val();
let test_mail = test_mail_list.split(',')[0] ?? '';
let test_mail = test_mail_list.split(',')[0] ?? ''; //REF:SVM
if (test_mail !== null || test_mail !== "") {
@ -1400,30 +1400,19 @@
type: "POST",
data:{
template_id,
test_mail,
test_mail_list
test_mail_list,
},
dataType: 'json',
success: function(res) {
console.log('Test mail send function response', res)
console.log('Test mail send function response', res);
$('.loader').fadeOut();
$('.loader-mask').delay(350).fadeOut('slow');
if (res.status == true) {
let respond = res.respond;
console.log('Parsed respond', respond)
if (respond.status == 'success') {
toastr.success(respond.message, 'SUCCESS')
} else {
toastr.warning(respond.message, 'WARNING')
}
if (res.status === true) {
toastr.success(res.message, 'SUCCESS');
} else {
toastr.warning('Failed to sent mail', 'WARNING')
toastr.warning('Failed to send mail', 'WARNING');
}
},
error: function(xhr, status, error) {

View File

@ -182,11 +182,18 @@
<div class="col-12">
<div class="card">
<div class="card-body">
<!-- <div class="row" style="padding-bottom: 10px;">
<div class="row" style="padding-bottom: 10px;">
<div class="col-6" style="align-self: center;">
<h4 style="position: relative;">Payout List <span id="payout_title"></span></h4>
<!-- <h4 style="position: relative;">Payout List <span id="payout_title"></span></h4> -->
</div>
</div> -->
<div class="col-5" style="align-self: right;"></div>
<div class="col-1" style="align-self: right;">
<button class="btn app-btn-primary mr-2" type="button"
onclick="window.location.href='<?= base_url('payout/invoices?type=add') ?>'">
Add
</button>
</div>
</div>
<div class="table-responsive">
<table data-custom-table-css="table" id="scroll-horizontal-datatable" class="table w-100 nowrap">
<thead class="bg-light">
@ -219,8 +226,8 @@
<a href="javascript: void(0);" class="dropdown-toggle arrow-none btn btn-light btn-sm" data-toggle="dropdown"aria-expanded="false"><i class="mdi mdi-dots-horizontal"></i></a>
<div class="dropdown-menu dropdown-menu-right">
<a class="dropdown-item" onclick="fetchUtrDetails(<?= $row['id'] ?>, '<?= $row['invoice_no'] ?>')"><i class="mdi mdi-bank-transfer mr-2 text-muted font-18 vertical-middle"></i>UTR</a>
<a href="<?= base_url('payout/invoices/save?type="edit"') ?>" class="dropdown-item"><i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit</a>
<a href="<?= base_url('payout/invoices/save?type="adjustment"') ?>" class="dropdown-item"><i class="mdi mdi-tune mr-2 text-muted font-18 vertical-middle"></i>Adjustment</a>
<a href="<?= base_url('payout/invoices?type=edit&id=' . $row['id']) ?>" class="dropdown-item"><i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit</a>
<a href="<?= base_url('payout/invoices?type=adjustment&id=' . $row['id']) ?>" class="dropdown-item"><i class="mdi mdi-tune mr-2 text-muted font-18 vertical-middle"></i>Adjustment</a>
</div>
</div>
</td>

View File

@ -489,7 +489,8 @@ table.dataTable thead th {
if (ticketsTable.length) {
ticketsTable.DataTable({
scrollX: true,
dom: "<'row'<'col-sm-1'f><'col-sm-11 text-right'B>>" + // Filter left, buttons right
// dom: "<'row'<'col-sm-1'f><'col-sm-11 text-right'B>>" + // Filter left, buttons right
dom: "<'row'<'col-12 d-flex justify-content-between'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>", // Pagination at the bottom, without page length selector
buttons: [

View File

@ -561,7 +561,8 @@ $(document).ready(function() {
if (ticketsTable.length) {
ticketsTable.DataTable({
scrollX: true,
dom: "<'row'<'col-sm-1'f><'col-sm-11 text-right'B>>" + // Filter left, buttons right
// dom: "<'row'<'col-12'f><'col-sm-11 text-right'B>>" + // Filter left, buttons right
dom: "<'row'<'col-12 d-flex justify-content-between'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>", // Pagination at the bottom, without page length selector
buttons: [

View File

@ -43,7 +43,7 @@ table.dataTable tbody td {
<div class="card-body">
<div>
<div class="table-responsive">
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="tickets-table">
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="thz-table">
<thead class="bg-light">
<tr>
@ -315,7 +315,7 @@ table.dataTable tbody td {
// Datatable document ready
$(document).ready(function() {
var ticketsTable = $('#tickets-table');
var ticketsTable = $('#thz-table');
if (ticketsTable.length) {
ticketsTable.DataTable({
@ -349,7 +349,7 @@ table.dataTable tbody td {
orthogonal: 'sort'
},
className: 'app-btn-primary ',
title: 'Tickets'
title: 'Tickets',
sheetName: 'Tickets',
}
]
@ -374,6 +374,12 @@ table.dataTable tbody td {
} else {
console.error("Table not found.");
}
$(document).on('click', '.datatable-clear-icon', function () {
const input = $(this).closest('.datatable-search-wrapper').find('input');
input.val('').trigger('input');
$('#thz-table').DataTable().search('').draw();
});
});

View File

@ -69,7 +69,7 @@
<!-- Search by Mobile Number -->
<div>
<h4 onclick="toggleAccordion('mobileAccordion')">
Search by Mobile Number
Search by Employee ID / Mobile Number
<span class="arrow" id="mobileArrow"></span>
</h4>
<div class="accordion-content" id="mobileAccordion">

View File

@ -1,3 +1,54 @@
<style>
/* ----- Modal Size: 25% width + 25% height ----- */
#New_Ticket_modal .modal-dialog {
width: 15%;
max-width: 15%;
height: 25%;
max-height: 25%;
margin: auto !important;
}
/* Make modal-content fill the dialog & allow flex layout */
#New_Ticket_modal .modal-content {
height: 50%;
display: flex;
flex-direction: column;
}
/* Scroll only inside body */
#New_Ticket_modal .modal-body {
flex: 1;
overflow-y: auto;
padding: 10px;
}
/* Footer visible always */
#New_Ticket_modal .modal-footer {
position: sticky;
bottom: 0;
background: #fff;
padding: 10px;
}
/* ----- Small Height Screens Fix ----- */
@media (max-height: 300px) {
/* Make modal usable but still centered */
#New_Ticket_modal .modal-dialog {
width: 50%;
max-width: 50%;
height: 70%;
max-height: 70%;
}
/* Adjust body scroll area */
#New_Ticket_modal .modal-body {
max-height: calc(80vh - 120px);
}
}
</style>
<div class="modal fade" id="New_Ticket_modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">

View File

@ -15,6 +15,7 @@ table.dataTable tbody td {
.col-12 {
max-width: 98% !important;
}
.column-header {margin-right: 10px;}
.readonly-select {
pointer-events: none;
@ -74,14 +75,14 @@ table.dataTable tbody td {
<table data-custom-table-css="table" id="scroll-horizontal-datatable" class="table w-100 nowrap">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">S.No&nbsp;</th>
<th class="font-weight-medium">Owner Type&nbsp;</th>
<th class="font-weight-medium">Owner Name&nbsp;</th>
<th class="font-weight-medium">RC Book No&nbsp;</th>
<th class="font-weight-medium">Vehicle No&nbsp;</th>
<th class="font-weight-medium">Vehicle type&nbsp;</th>
<th class="font-weight-medium">Description&nbsp;</th>
<th class="font-weight-medium">Action&nbsp;</th>
<th class="font-weight-medium"><div class="column-header">S.No&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Owner Type&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Owner Name&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">RC Book No&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Vehicle No&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Vehicle type&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Description&nbsp;</div></th>
<th class="font-weight-medium"><div class="column-header">Action&nbsp;</div></th>
</tr>
</thead>
@ -465,9 +466,11 @@ $(document).ready(function() {
$(document).ready(function() {
$('#scroll-horizontal-datatable').DataTable({
var ticketsTable = $('#scroll-horizontal-datatable');
if (ticketsTable.length) {
ticketsTable.DataTable({
scrollX: true,
dom: "<'row'<'col-sm-2'f><'col-sm-10 text-right'B>>" +
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>",
buttons: [
@ -524,7 +527,10 @@ $(document).ready(function() {
paging: true,
pageLength: 10,
order: [[0, 'desc']]
});
});
} else {
console.error("Table atet found.");
}
});
function getEditVehicleDetailsData(input) {

View File

@ -2,6 +2,7 @@
.dataTables_filter {
position: absolute;
}
.column-header {margin-right: 10px;}
</style>
<!-- End ADD and EDIT Page HTML -->
@ -12,10 +13,10 @@
<table data-custom-table-css="table" class="table table-sm m-0 table-centered dt-responsive nowrap w-100" cellspacing="a" id="user-table">
<thead class="bg-light">
<tr>
<th class="font-weight-medium">S.No.</th>
<th class="font-weight-medium">Vehicle Type</th>
<th class="font-weight-medium"><div class="column-header">S.No.</div></th>
<th class="font-weight-medium"><div class="column-header">Vehicle Type</div></th>
<!-- <th class="font-weight-medium">Status</th> -->
<th class="font-weight-medium">Action</th>
<th class="font-weight-medium"><div class="column-header">Action</div></th>
</tr>
</thead>
<tbody>
@ -94,7 +95,8 @@
var ticketsTable = $('#user-table');
ticketsTable.DataTable({
scrollX: true,
dom: "<'row'<'col-sm-7'f><'col-sm-5 text-right'B>>" + // Filter left, buttons right
// dom: "<'row'<'col-sm-7'f><'col-sm-5 text-right'B>>" + // Filter left, buttons right
dom: "<'row'<'col-12 d-flex justify-content-between align-items-center'<'datatable-search dataTables_filter'f><'datatable-buttons dt-buttons'B>>>" +
"<'row'<'col-sm-12'tr>>" +
"<'row'<'col-sm-5'i><'col-sm-7'p>>", // Pagination at the bottom, without page length selector
buttons: [