414 lines
15 KiB
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);
|
|
}
|
|
}
|
|
}
|
|
|