nhance/app/Controllers/ExpenseController.php

414 lines
15 KiB
PHP

<?php
namespace App\Controllers;
use App\Models\ExpenseModel;
use App\Models\ClientModel;
use App\Models\ClientPolicyModel;
use CodeIgniter\API\ResponseTrait;
class ExpenseController extends AdminController
{
use ResponseTrait;
protected $myLogger;
protected $expenseModel;
protected $clientModel;
protected $clientPolicyModel;
public function __construct()
{
set_session_context('Expense');
$this->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);
}
}
}