diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 57f24fe..1c3a596 100755 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -213,6 +213,7 @@ $routes->group('api', ['filter' => ["jwtAuth:1,2,3,4,agent","appSignature"] ], f //invoice $routes->get('invoice/list', 'InvoiceController::invoiceList'); $routes->get('invoice/details', 'InvoiceController::findInvoiceWithItems'); + $routes->get('invoice/view', 'InvoiceController::invoiceView'); $routes->post('invoice/create-or-update', 'InvoiceController::createOrUpdateInvoice'); $routes->get('invoice/utrDetails', 'InvoiceController::utrDetails'); $routes->post('invoice/add-payment', 'InvoiceController::addInvoicePayment'); // tempo diff --git a/app/Controllers/InvoiceController.php b/app/Controllers/InvoiceController.php index 816594c..22474f9 100644 --- a/app/Controllers/InvoiceController.php +++ b/app/Controllers/InvoiceController.php @@ -38,8 +38,36 @@ class InvoiceController extends ResourceController public function invoiceList() { try { + $fromDateRaw = $this->request->getGet('from_date'); + $toDateRaw = $this->request->getGet('to_date'); - $data = $this->InvoiceModel + $fromParsed = $this->parseOptionalDmYDate($fromDateRaw); + $toParsed = $this->parseOptionalDmYDate($toDateRaw); + + if ($fromParsed['error'] !== null) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => $fromParsed['error'], + ], 400); + } + if ($toParsed['error'] !== null) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => $toParsed['error'], + ], 400); + } + + if ($fromParsed['ymd'] !== null && $toParsed['ymd'] !== null && $fromParsed['ymd'] > $toParsed['ymd']) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'from_date must be on or before to_date', + ], 400); + } + + $query = $this->InvoiceModel ->select('partner_invoice.*, PB.name as broker_name, pos.name as pos_name, @@ -78,9 +106,16 @@ class InvoiceController extends ResourceController false) ->join('partner_brokers PB', 'PB.id = partner_invoice.broker_id', 'left') ->join('partner_pos pos', 'pos.id = partner_invoice.pos_id', 'left') - ->where('partner_invoice.is_active', 1) - ->orderBy('partner_invoice.id', 'DESC') - ->findAll(); + ->where('partner_invoice.is_active', 1); + + if ($fromParsed['ymd'] !== null) { + $query->where('DATE(partner_invoice.invoice_date) >=', $fromParsed['ymd']); + } + if ($toParsed['ymd'] !== null) { + $query->where('DATE(partner_invoice.invoice_date) <=', $toParsed['ymd']); + } + + $data = $query->orderBy('partner_invoice.id', 'DESC')->findAll(); return $this->respond(['status' => 'success', 'code' => 200,'data' => $data ], 200); @@ -89,6 +124,32 @@ class InvoiceController extends ResourceController } } + /** + * Optional invoice list date filter: format d-m-Y (e.g. 10-03-2026). Empty / null = no filter. + * + * @return array{ymd: ?string, error: ?string} + */ + private function parseOptionalDmYDate($value): array + { + if ($value === null) { + return ['ymd' => null, 'error' => null]; + } + $v = trim((string) $value); + if ($v === '') { + return ['ymd' => null, 'error' => null]; + } + + $dt = \DateTime::createFromFormat('d-m-Y', $v); + if ($dt === false || $dt->format('d-m-Y') !== $v) { + return [ + 'ymd' => null, + 'error' => 'Invalid date format; use d-m-Y (e.g. 10-03-2026)', + ]; + } + + return ['ymd' => $dt->format('Y-m-d'), 'error' => null]; + } + public function findInvoiceWithItems() { try { @@ -150,6 +211,84 @@ class InvoiceController extends ResourceController } } + /** + * GET invoice_id, manager_id — invoice header + active line items (policies under this manager only). + */ + public function invoiceView() + { + try { + $invoiceId = (int) $this->request->getGet('invoice_id'); + $managerId = (int) $this->request->getGet('manager_id'); + + if ($invoiceId <= 0 || $managerId <= 0) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'data' => 'invoice_id and manager_id are required', + ], 400); + } + + $invoice = $this->InvoiceModel + ->where('id', $invoiceId) + ->where('is_active', 1) + ->first(); + + if (! $invoice) { + return $this->respond([ + 'status' => 'failed', + 'code' => 404, + 'data' => 'Invoice not found', + ], 404); + } + + $forbidden = $this->db->query( + 'SELECT 1 AS x FROM partner_invoice_items pii ' + . 'INNER JOIN partner_policy pp ON pp.id = pii.policy_id ' + . 'WHERE pii.invoice_id = ? AND pii.is_active = 1 ' + . 'AND (pp.manager_id IS NULL OR pp.manager_id != ?) ' + . 'LIMIT 1', + [$invoiceId, $managerId] + )->getRow(); + + if ($forbidden !== null) { + return $this->respond([ + 'status' => 'failed', + 'code' => 403, + 'data' => 'You do not have access to this invoice', + ], 403); + } + + $items = $this->InvoiceItemModel + ->select( + 'partner_invoice_items.*, pp.issued_date, pe.name AS customer_name, pp.premium_amount, ' + . 'pa.agent_code, pa.name AS agent_name, CONCAT_WS(" - ", pa.agent_code, pa.name) AS agent_code_name', + false + ) + ->join('partner_policy pp', 'pp.id = partner_invoice_items.policy_id', 'left') + ->join('partner_enquiry pe', 'pe.id = pp.enquiry_id', 'left') + ->join('partner_quotation pq', 'pq.id = pp.quotation_id', 'left') + ->join('partner_agent pa', 'pa.id = pp.agent_id', 'left') + ->where('partner_invoice_items.invoice_id', $invoiceId) + ->where('partner_invoice_items.is_active', 1) + ->findAll(); + + return $this->respond([ + 'status' => 'success', + 'code' => 200, + 'data' => [ + 'invoice' => $invoice, + 'items' => $items, + ], + ], 200); + } catch (\Exception $e) { + return $this->respond([ + 'status' => 'failed', + 'code' => 500, + 'data' => $e->getMessage(), + ], 500); + } + } + public function createOrUpdateInvoice() { $this->db->transBegin(); @@ -388,138 +527,177 @@ class InvoiceController extends ResourceController public function getCommissionRateList() { try { - $input = $this->request->getJSON(true); - - // if (empty($input['broker_id'])) { - // return $this->respond([ - // 'status' => 'failed', - // 'code' => 400, - // 'message'=> 'broker_id is required' - // ], 400); - // } - - $invoice_id = isset($input['invoice_id']) ? $input['invoice_id'] : ''; - - // Build the main query - $query = $this->PolicyModel - ->select('partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id ,pa.name as agent_name,pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no, - COALESCE(( - SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") - FROM partner_invoice_utr piu - WHERE piu.invoice_id = pi.id - AND piu.is_active = 1 - ), "") as utr_no', false) - // ->join( - // 'partner_enquiry pe', - // 'pe.id = partner_policy.enquiry_id AND pe.broker_id = ' . (int)$input['broker_id'], - // 'left' - // ) - ->join('partner_enquiry pe','pe.id = partner_policy.enquiry_id','left') - ->join('partner_quotation pq', 'pq.id = partner_policy.quotation_id', 'left') - ->join('partner_invoice_items pii', 'pii.policy_id = partner_policy.id', 'left') - ->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'left') - ->join('partner_agent pa', 'pa.id = pe.agent_id', 'left') - ->where('partner_policy.is_active', 1) - ->where('partner_policy.commission_amount > 0') - ->where('partner_policy.is_data_accuracy_checked', 1) - ->where('partner_policy.manager_id', $input['manager_id']); - - // Apply agent_id filter (supports multiple agents) - if (!empty($input['agent_id'])) { - if (is_array($input['agent_id'])) { - $query->whereIn('pe.agent_id', $input['agent_id']); - } else { - $query->where('pe.agent_id', $input['agent_id']); - } + if (! is_array($input)) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'JSON body is required', + ], 400); } - // Apply date range filters - if (!empty($input['from_date'])) { - $query->where('partner_policy.issued_date >=', date('Y-m-d', strtotime($input['from_date']))); + if (empty($input['manager_id'])) { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'manager_id is required', + ], 400); } - if (!empty($input['to_date'])) { - $query->where('partner_policy.issued_date <=', date('Y-m-d', strtotime($input['to_date']))); - } + $managerId = (int) $input['manager_id']; + $invoiceIdRaw = $input['invoice_id'] ?? null; + $invoiceId = ($invoiceIdRaw !== null && $invoiceIdRaw !== '' && (int) $invoiceIdRaw > 0) + ? (int) $invoiceIdRaw + : 0; - // Apply POS filter if provided - if (!empty($input['pos_id'])) { - $query->where('partner_policy.pos_id', $input['pos_id']); - } - - // Filter based on invoice_id or policies not yet invoiced - if (empty($invoice_id)) { - $query->where('pii.policy_id IS NULL'); + // Unused policies: no active invoice line on an active invoice (same as getAgentUnusedCommissionList). + // Do not LEFT JOIN all invoice_items — inactive rows duplicate policies and break "unused" detection. + if ($invoiceId === 0) { + $query = $this->PolicyModel + ->select( + 'partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, ' + . 'partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id, ' + . 'pa.name as agent_name, pa.agent_code, partner_policy.premium_amount, ' + . "NULL AS invoice_id, NULL AS invoice_no, '' AS utr_no", + false + ) + ->join('partner_enquiry pe', 'pe.id = partner_policy.enquiry_id', 'left') + ->join('partner_quotation pq', 'pq.id = partner_policy.quotation_id', 'left') + ->join('partner_agent pa', 'pa.id = pe.agent_id', 'left') + ->where('partner_policy.is_active', 1) + // ->where('partner_policy.commission_amount >', 0) + // ->where('partner_policy.is_data_accuracy_checked', 1) + ->where('partner_policy.manager_id', $managerId) + ->where('partner_policy.policy_number IS NOT NULL', null, false) + ->where( + 'NOT EXISTS (SELECT 1 FROM partner_invoice_items pii2 ' + . 'INNER JOIN partner_invoice pi2 ON pi2.id = pii2.invoice_id AND pi2.is_active = 1 ' + . 'WHERE pii2.policy_id = partner_policy.id AND pii2.is_active = 1)', + null, + false + ); } else { - $query->where('pi.id', $invoice_id); + $query = $this->PolicyModel + ->select( + 'partner_policy.policy_number as policy_no, partner_policy.id as policy_id, partner_policy.issued_date, ' + . 'partner_policy.commission_amount, partner_policy.insured_name as customer_name, pe.agent_id, ' + . 'pa.name as agent_name, pa.agent_code, partner_policy.premium_amount, pi.id as invoice_id, pi.invoice_no, ' + . 'COALESCE((' + . 'SELECT GROUP_CONCAT(piu.utr_no ORDER BY piu.id SEPARATOR ", ") ' + . 'FROM partner_invoice_utr piu ' + . 'WHERE piu.invoice_id = pi.id AND piu.is_active = 1' + . "), '') as utr_no", + false + ) + ->join('partner_enquiry pe', 'pe.id = partner_policy.enquiry_id', 'left') + ->join('partner_quotation pq', 'pq.id = partner_policy.quotation_id', 'left') + ->join('partner_agent pa', 'pa.id = pe.agent_id', 'left') + ->join( + 'partner_invoice_items pii', + 'pii.policy_id = partner_policy.id AND pii.is_active = 1 AND pii.invoice_id = ' . $invoiceId, + 'inner' + ) + ->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'inner') + ->where('partner_policy.is_active', 1) + ->where('partner_policy.commission_amount >', 0) + ->where('partner_policy.is_data_accuracy_checked', 1) + ->where('partner_policy.manager_id', $managerId) + ->where('partner_policy.policy_number IS NOT NULL', null, false) + ->where('pi.id', $invoiceId) + ->distinct(); } + $this->applyCommissionRateListPayloadFilters($query, $input); + + $query->orderBy('partner_policy.issued_date', 'DESC'); + $query->orderBy('partner_policy.id', 'DESC'); + $data = $query->findAll(); - // Calculate total commission $total_commission = 0; foreach ($data as $policy) { - $total_commission += $policy['commission_amount']; + $total_commission += (float) ($policy['commission_amount'] ?? 0); } return $this->respond([ - 'status' => 'success', - 'code' => 200, - 'data' => $data, + 'status' => 'success', + 'code' => 200, + 'data' => $data, 'total_commission' => $total_commission, - 'total_policies' => count($data) + 'total_policies' => count($data), ], 200); - } catch (\Exception $e) { return $this->respond([ 'status' => 'failed', 'code' => 500, - 'message'=> $e->getMessage() + 'message'=> $e->getMessage(), ], 500); } } + /** + * Optional filters for getCommissionRateList JSON: agent_id, from_date, to_date, pos_id. + */ + private function applyCommissionRateListPayloadFilters($query, array $input): void + { + if (! empty($input['agent_id'])) { + if (is_array($input['agent_id'])) { + $query->whereIn('pe.agent_id', $input['agent_id']); + } else { + $query->where('pe.agent_id', $input['agent_id']); + } + } + if (! empty($input['from_date'])) { + $query->where('partner_policy.issued_date >=', date('Y-m-d', strtotime((string) $input['from_date']))); + } + if (! empty($input['to_date'])) { + $query->where('partner_policy.issued_date <=', date('Y-m-d', strtotime((string) $input['to_date']))); + } + if (! empty($input['pos_id'])) { + $query->where('partner_policy.pos_id', $input['pos_id']); + } + } + public function getAgentUnusedCommissionList() { try { $manager_id = $this->request->getGet('manager_id'); - // $broker_id = $this->request->getGet('broker_id'); - - // if (empty($broker_id)) { - // return $this->respond([ - // 'status' => 'failed', - // 'code' => 400, - // 'message'=> 'broker_id is required' - // ], 400); - // } + if ($manager_id === null || $manager_id === '') { + return $this->respond([ + 'status' => 'failed', + 'code' => 400, + 'message'=> 'manager_id is required', + ], 400); + } + // Unused = policy has no active invoice line on an active invoice (same idea as commissionPayoutReport pending). + // NOT EXISTS avoids false "invoiced" when only inactive items/invoices exist, and avoids join fan-out. $query = $this->PolicyModel ->select([ 'pe.agent_id', - 'pa.name as agent_name', + 'MAX(pa.name) as agent_name', + 'MAX(pa.agent_code) as agent_code', 'SUM(partner_policy.commission_amount) as unused_commission_amount', - 'COUNT(partner_policy.id) as total_policies' + 'COUNT(partner_policy.id) as total_policies', ]) - // ->join( - // 'partner_enquiry pe', - // 'pe.id = partner_policy.enquiry_id AND pe.broker_id = ' . (int)$broker_id, - // 'left' - // ) - ->join('partner_enquiry pe','pe.id = partner_policy.enquiry_id','left') + ->join('partner_enquiry pe', 'pe.id = partner_policy.enquiry_id', 'left') ->join('partner_agent pa', 'pa.id = pe.agent_id', 'left') - ->join('partner_invoice_items pii', 'pii.policy_id = partner_policy.id', 'left') - ->join('partner_invoice pi', 'pi.id = pii.invoice_id AND pi.is_active = 1', 'left') ->where('partner_policy.is_active', 1) - ->where('partner_policy.commission_amount >=', 0) + // ->where('partner_policy.commission_amount >', 0) // ->where('partner_policy.is_data_accuracy_checked', 1) - ->where('partner_policy.manager_id', $manager_id) - ->where('pii.policy_id IS NULL') // ❗ unused commission + ->where('partner_policy.manager_id', (int) $manager_id) + ->where('partner_policy.policy_number IS NOT NULL', null, false) + ->where('pe.agent_id IS NOT NULL', null, false) + ->where( + 'NOT EXISTS (SELECT 1 FROM partner_invoice_items pii2 ' + . 'INNER JOIN partner_invoice pi2 ON pi2.id = pii2.invoice_id AND pi2.is_active = 1 ' + . 'WHERE pii2.policy_id = partner_policy.id AND pii2.is_active = 1)', + null, + false + ) ->groupBy('pe.agent_id'); - - $data = $query->findAll(); // Grand total (optional but useful for FE)