myLogger = \Config\Services::mylogger(); $this->expenseModel = new ExpenseModel(); $this->clientModel = new ClientModel(); $this->clientPolicyModel = new ClientPolicyModel(); } /** * Web list + form view */ public function index() { try { $data['tab_name'] = 'Expense'; $data['page_name'] = 'Expense'; $descriptionPattern = '/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+\'"]+$/u'; // Raw GET filters $rawFilters = $this->request->getGet() ?? []; $rawFilters = is_array($rawFilters) ? $rawFilters : []; // Sanitize input array using existing helper $sanitized = sanitizeInputArrayAdvanced($rawFilters); $filters = [ 'client_id' => trim($sanitized['client_id'] ?? ''), 'client_policy_id' => trim($sanitized['client_policy_id'] ?? ''), 'approved_by' => trim($sanitized['approved_by'] ?? ''), 'description' => trim($sanitized['description'] ?? ''), 'amount' => trim($sanitized['amount'] ?? ''), 'expense_date' => trim($sanitized['expense_date'] ?? ''), ]; $validationErrors = []; // Basic type / format validation for filters if ($filters['client_id'] !== '' && ! ctype_digit($filters['client_id'])) { $validationErrors[] = 'Invalid client selected for search.'; $filters['client_id'] = ''; } if ($filters['client_policy_id'] !== '' && ! ctype_digit($filters['client_policy_id'])) { $validationErrors[] = 'Invalid policy selected for search.'; $filters['client_policy_id'] = ''; } if ($filters['approved_by'] !== '' && ! ctype_digit($filters['approved_by'])) { $validationErrors[] = 'Invalid approver selected for search.'; $filters['approved_by'] = ''; } if ($filters['description'] !== '' && ! preg_match($descriptionPattern, $filters['description'])) { $validationErrors[] = 'Description filter contains invalid characters.'; $filters['description'] = ''; } if ($filters['amount'] !== '') { if (! is_numeric($filters['amount']) || (float) $filters['amount'] < 0) { $validationErrors[] = 'Amount filter must be a non-negative number.'; $filters['amount'] = ''; } } if ($filters['expense_date'] !== '') { $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']); $errors = $dt ? \DateTime::getLastErrors() : ['warning_count' => 1, 'error_count' => 1]; if (! $dt || ! empty($errors['warning_count']) || ! empty($errors['error_count'])) { $validationErrors[] = 'Expense Date filter must be in DD-MM-YYYY format.'; $filters['expense_date'] = ''; } } $data['filters'] = $filters; $data['validation_errors'] = $validationErrors; // Clients for dropdown $data['clients'] = $this->clientModel ->select('id, client_name, short_name') ->where('client_type', 1) ->where('is_active', 1) ->orderBy('client_name', 'ASC') ->findAll(); // Approved by (users) dropdown $db = db_connect(); $data['approved_users'] = $db->table('user_profiles') ->select('id, first_name') ->where('is_active', 1) ->whereIn('id', [7, 8]) ->orderBy('first_name', 'ASC') ->get() ->getResultArray(); // Policies for filter dropdown (when client filter is selected) $data['policies_for_filter'] = []; if ($filters['client_id'] !== '') { $data['policies_for_filter'] = $this->clientPolicyModel ->select('id, policy_no') ->where('client_id', (int) $filters['client_id']) ->where('is_active', 1) ->orderBy('id', 'ASC') ->findAll(); } // Existing expenses with optional filters $builder = $this->expenseModel ->select(' expenses.*, clients.client_name, clients.short_name, client_policy.policy_no, user_profiles.first_name AS approved_by_name ') ->join('clients', 'clients.id = expenses.client_id') ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left') ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left') ->where('expenses.is_active', 1); if ($filters['client_id'] !== '') { $builder->where('expenses.client_id', (int) $filters['client_id']); } if ($filters['client_policy_id'] !== '') { $builder->where('expenses.client_policy_id', (int) $filters['client_policy_id']); } if ($filters['approved_by'] !== '') { $builder->where('expenses.approved_by', (int) $filters['approved_by']); } if ($filters['description'] !== '') { $builder->like('expenses.description', (string) $filters['description']); } if ($filters['amount'] !== '') { $builder->where('expenses.amount', (float) $filters['amount']); } if ($filters['expense_date'] !== '') { $dt = \DateTime::createFromFormat('d-m-Y', $filters['expense_date']); if ($dt) { $builder->where('expenses.expense_date', $dt->format('Y-m-d')); } } $data['expenses'] = $builder ->orderBy('expenses.id', 'DESC') ->findAll(); return $this->loadLayout('expense_list', $data); } catch (\Throwable $e) { return handle_exception($e, $this->myLogger, $this->response); } } /** * Create / update expense (AJAX) */ public function save() { try { if ($this->request->getMethod() !== 'post') { return $this->response ->setStatusCode(405) ->setJSON([ 'status' => false, 'message' => 'Invalid request method', ]); } $rawData = $this->request->getPost(); $data = sanitizeInputArrayAdvanced($rawData); $id = isset($data['id']) && $data['id'] !== '' ? (int) $data['id'] : null; $rules = [ 'client_id' => [ 'rules' => 'required|is_natural_no_zero', 'errors' => [ 'required' => 'Client is required', ], ], 'client_policy_id' => [ 'rules' => 'required|is_natural_no_zero', 'errors' => [ 'required' => 'Policy is required', ], ], 'description' => [ 'rules' => 'required|string|min_length[1]|max_length[2000]|regex_match[/^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/]', 'errors' => [ 'required' => 'Description is required', 'min_length' => 'Description cannot be empty', 'regex_match' => 'Description contains invalid characters.', ], ], 'expense_date' => [ 'rules' => 'required|valid_date[d-m-Y]', 'errors' => [ 'required' => 'Expense Date is required', 'valid_date' => 'Expense Date must be in DD-MM-YYYY format', ], ], 'approved_by' => [ 'rules' => 'required|is_natural_no_zero', 'errors' => [ 'required' => 'Approved By is required', ], ], 'amount' => [ 'rules' => 'required|numeric|greater_than_equal_to[0]|less_than_equal_to[100000000]', 'errors' => [ 'required' => 'Amount is required', 'numeric' => 'Amount must be numeric', 'greater_than_equal_to' => 'Amount cannot be negative', 'less_than_equal_to' => 'Amount cannot be greater than 100 Cr.', ], ], ]; if (! $this->validate($rules)) { return $this->response->setStatusCode(400)->setJSON([ 'status' => false, 'message' => 'Input validation failed', 'errors' => $this->validator->getErrors(), ]); } $payload = [ 'client_id' => (int) ($data['client_id'] ?? 0), 'client_policy_id' => (int) ($data['client_policy_id'] ?? 0), 'description' => $data['description'] ?? null, 'approved_by' => (int) ($data['approved_by'] ?? 0), 'amount' => $data['amount'] ?? null, 'is_active' => 1, ]; $expenseDate = $data['expense_date'] ?? null; if (! empty($expenseDate)) { $dt = \DateTime::createFromFormat('d-m-Y', $expenseDate); $payload['expense_date'] = $dt ? $dt->format('Y-m-d') : null; } else { $payload['expense_date'] = null; } if ($id === null) { $insertId = $this->expenseModel->insert($payload, true); $success = ! empty($insertId); $id = $insertId; $message = $success ? 'Expense created successfully' : 'Unable to create expense. Please try again.'; } else { $success = $this->expenseModel->update($id, $payload); $message = $success ? 'Expense updated successfully' : 'Unable to update expense. Please try again.'; } return $this->response ->setStatusCode($success ? 200 : 400) ->setJSON([ 'status' => (bool) $success, 'message' => $message, 'id' => $id, ]); } catch (\Throwable $e) { return handle_exception($e, $this->myLogger, $this->response); } } /** * Get single expense (AJAX) */ public function getExpense($id = null) { try { $id = (int) $id; if (empty($id)) { return $this->response->setStatusCode(400)->setJSON([ 'status' => false, 'message' => 'Invalid expense id', ]); } $expense = $this->expenseModel ->select(' expenses.*, clients.client_name, clients.short_name, client_policy.policy_no, user_profiles.first_name AS approved_by_name ') ->join('clients', 'clients.id = expenses.client_id', 'left') ->join('client_policy', 'client_policy.id = expenses.client_policy_id', 'left') ->join('user_profiles', 'user_profiles.id = expenses.approved_by', 'left') ->where('expenses.id', $id) ->where('expenses.is_active', 1) ->first(); if (empty($expense)) { return $this->response->setStatusCode(404)->setJSON([ 'status' => false, 'message' => 'Expense not found', ]); } return $this->response->setStatusCode(200)->setJSON([ 'status' => true, 'data' => $expense, ]); } catch (\Throwable $e) { return handle_exception($e, $this->myLogger, $this->response); } } /** * Soft delete expense (AJAX) */ public function delete($id = null) { try { if ($this->request->getMethod() !== 'post') { return $this->response ->setStatusCode(405) ->setJSON([ 'status' => false, 'message' => 'Invalid request method', ]); } $id = (int) $id; if (empty($id)) { return $this->response->setStatusCode(400)->setJSON([ 'status' => false, 'message' => 'Invalid expense id', ]); } $payload = [ 'is_active' => 0, ]; $success = $this->expenseModel->update($id, $payload); return $this->response ->setStatusCode($success ? 200 : 400) ->setJSON([ 'status' => (bool) $success, 'message' => $success ? 'Expense deleted successfully' : 'Unable to delete expense. Please try again.', ]); } catch (\Throwable $e) { return handle_exception($e, $this->myLogger, $this->response); } } /** * Get policies by client for dropdown (AJAX) */ public function clientPolicies() { try { $clientId = (int) ($this->request->getGet('client_id') ?? 0); if (empty($clientId)) { return $this->response->setStatusCode(400)->setJSON([ 'status' => false, 'message' => 'Client is required', ]); } $policies = $this->clientPolicyModel ->select('client_policy.id, client_policy.policy_no, policy_type.policy_type') ->join('policy_type', 'policy_type.id = client_policy.policy_type_id AND policy_type.is_active = 1') ->where('client_policy.client_id', $clientId) ->where('client_policy.is_active', 1) ->orderBy('client_policy.id', 'ASC') ->findAll(); return $this->response->setStatusCode(200)->setJSON([ 'status' => true, 'data' => $policies, ]); } catch (\Throwable $e) { return handle_exception($e, $this->myLogger, $this->response); } } }