697 lines
30 KiB
PHP
697 lines
30 KiB
PHP
<style>
|
|
|
|
.truncate {
|
|
max-width: 80px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.desc-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
#expense-desc-popup {
|
|
position: absolute;
|
|
z-index: 9999;
|
|
background: #fff;
|
|
border: 1px solid #ddd;
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
padding: 8px 10px;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
max-width: 320px;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
display: none;
|
|
}
|
|
|
|
</style>
|
|
|
|
<div class="row" id="expense_module">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<div class="row mb-3">
|
|
<div class="col-6 d-flex align-items-center">
|
|
<!-- <h4 class="mb-0">Expense</h4> -->
|
|
</div>
|
|
<div class="col-6 text-right">
|
|
<!-- <button type="button" class="btn app-btn-secondary" id="btnResetExpense">
|
|
<i class="mdi mdi-refresh mr-1"></i>Reset
|
|
</button> -->
|
|
</div>
|
|
</div>
|
|
|
|
<div class="custom-form mb-4">
|
|
<form role="form" class="parsley-examples" method="get" id="expense_form">
|
|
<input type="hidden" value="<?= csrf_hash() ?>" name="<?= csrf_token() ?>" />
|
|
<input type="hidden" name="id" id="expense_id" />
|
|
|
|
<div class="form-row">
|
|
<div class="form-group col-md-4">
|
|
<label for="client_id">Client Name <span class="text-danger">*</span></label>
|
|
<select class="form-control" id="client_id" name="client_id" required>
|
|
<option value="">Select Client</option>
|
|
<?php if (! empty($clients)) : ?>
|
|
<?php foreach ($clients as $client) : ?>
|
|
<option value="<?= $client['id']; ?>" <?= ! empty($filters['client_id']) && (int) $filters['client_id'] === (int) $client['id'] ? 'selected' : ''; ?>>
|
|
<?= esc($client['client_name']); ?>
|
|
<?= ! empty($client['short_name']) ? ' (' . esc($client['short_name']) . ')' : ''; ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group col-md-4">
|
|
<label for="client_policy_id">Policy No <span class="text-danger">*</span></label>
|
|
<select class="form-control" id="client_policy_id" name="client_policy_id" required>
|
|
<option value="">Select Policy</option>
|
|
<?php if (! empty($policies_for_filter)) : ?>
|
|
<?php foreach ($policies_for_filter as $policy) : ?>
|
|
<option value="<?= $policy['id']; ?>" <?= ! empty($filters['client_policy_id']) && (int) $filters['client_policy_id'] === (int) $policy['id'] ? 'selected' : ''; ?>>
|
|
<?= ! empty($policy['policy_type']) ? esc($policy['policy_type']) . ' - ' : ''; ?> <?= esc($policy['policy_no']); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group col-md-4">
|
|
<label for="approved_by">Approved By <span class="text-danger">*</span></label>
|
|
<select class="form-control" id="approved_by" name="approved_by" required>
|
|
<option value="">Select User</option>
|
|
<?php if (! empty($approved_users)) : ?>
|
|
<?php foreach ($approved_users as $user) : ?>
|
|
<option value="<?= $user['id']; ?>" <?= ! empty($filters['approved_by']) && (int) $filters['approved_by'] === (int) $user['id'] ? 'selected' : ''; ?>>
|
|
<?= esc($user['first_name']); ?>
|
|
</option>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
|
|
<div class="form-group col-md-4">
|
|
<label for="expense_date">Expense Date <span class="text-danger">*</span></label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="expense_date"
|
|
name="expense_date"
|
|
placeholder="DD-MM-YYYY"
|
|
value="<?= isset($filters['expense_date']) ? esc($filters['expense_date']) : ''; ?>"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group col-md-4">
|
|
<label for="amount">Amount <span class="text-danger">*</span></label>
|
|
<input
|
|
type="number"
|
|
step="0.01"
|
|
min="0"
|
|
max="100000000"
|
|
class="form-control"
|
|
id="amount"
|
|
name="amount"
|
|
placeholder="Enter amount"
|
|
value="<?= isset($filters['amount']) ? esc($filters['amount']) : ''; ?>"
|
|
required
|
|
>
|
|
</div>
|
|
|
|
<div class="form-group col-md-4">
|
|
<label for="description">Description <span class="text-danger">*</span></label>
|
|
<textarea class="form-control" id="description" name="description" rows="3" placeholder="Enter description" required><?= isset($filters['description']) ? esc($filters['description']) : ''; ?></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group text-right m-b-0">
|
|
<!-- <button type="button" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSearchExpense">
|
|
Search
|
|
</button> -->
|
|
<button type="submit" class="btn app-btn-secondary waves-effect waves-light mr-1" id="btnSubmitExpense">
|
|
Save
|
|
</button>
|
|
<button type="button" class="btn btn-secondary waves-effect" id="btnCancelExpense">
|
|
Reset
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<table data-custom-table-css="table" class="table mb-0 nowrap w-100 table-centered" cellspacing="0" id="expense-table">
|
|
<thead class="bg-light">
|
|
<tr>
|
|
<th class="font-weight-medium">S.No</th>
|
|
<th class="font-weight-medium">Client</th>
|
|
<th class="font-weight-medium">Policy No</th>
|
|
<th class="font-weight-medium">Description</th>
|
|
<th class="font-weight-medium">Approved By</th>
|
|
<th class="font-weight-medium">Amount</th>
|
|
<th class="font-weight-medium">Expense Date</th>
|
|
<th class="font-weight-medium">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php if (! empty($expenses)) : ?>
|
|
<?php foreach ($expenses as $index => $row) : ?>
|
|
<tr>
|
|
<td class="text-center"><?= $index + 1; ?></td>
|
|
<td>
|
|
<?= esc($row['client_name'] ?? ''); ?>
|
|
<?= ! empty($row['short_name']) ? ' (' . esc($row['short_name']) . ')' : ''; ?>
|
|
</td>
|
|
<td><?= esc($row['policy_no'] ?? ''); ?></td>
|
|
<td class="desc-cell">
|
|
<i
|
|
class="mdi mdi-information-outline text-muted expense-desc-icon"
|
|
style="cursor: pointer;"
|
|
data-description="<?= esc($row['description'] ?? ''); ?>"
|
|
></i>
|
|
<span class="truncate">
|
|
<?= esc($row['description'] ?? ''); ?>
|
|
</span>
|
|
</td>
|
|
<td><?= esc($row['approved_by_name'] ?? ''); ?></td>
|
|
<td><?= number_format((float) ($row['amount'] ?? 0), 2); ?></td>
|
|
<td>
|
|
<?php if (! empty($row['expense_date'])): ?>
|
|
<?= date('d-m-Y', strtotime($row['expense_date'])); ?>
|
|
<?php endif; ?>
|
|
</td>
|
|
<td>
|
|
<div class="btn-group dropdown">
|
|
<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" href="javascript:void(0);" onclick="editExpense(<?= (int) $row['id']; ?>)">
|
|
<i class="mdi mdi-pencil mr-2 text-muted font-18 vertical-middle"></i>Edit
|
|
</a>
|
|
<a class="dropdown-item" href="javascript:void(0);" onclick="deleteExpense(<?= (int) $row['id']; ?>)">
|
|
<i class="mdi mdi-delete mr-2 text-muted font-18 vertical-middle"></i>Delete
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
<?php endif; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const expenseValidationErrors = <?= ! empty($validation_errors) ? json_encode(array_values($validation_errors)) : '[]'; ?>;
|
|
let expensePoliciesCache = {};
|
|
|
|
function resetExpenseForm() {
|
|
$('#client_id').val('').trigger('change');
|
|
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
|
|
$('#approved_by').val('').trigger('change');
|
|
$('#description').val('');
|
|
$('#amount').val('');
|
|
$('#expense_date').val('');
|
|
$('#btnSubmitExpense').text('Save');
|
|
window.location.href = '<?= base_url("expense"); ?>';
|
|
}
|
|
|
|
function validateExpenseSave() {
|
|
const errors = [];
|
|
const clientId = $('#client_id').val();
|
|
const policyId = $('#client_policy_id').val();
|
|
const approvedBy = $('#approved_by').val();
|
|
const desc = ($('#description').val() || '').trim();
|
|
const amount = ($('#amount').val() || '').trim();
|
|
const expenseDate= ($('#expense_date').val() || '').trim();
|
|
|
|
const idPattern = /^[0-9]+$/;
|
|
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
|
|
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
|
|
|
|
if (!clientId) {
|
|
errors.push('Client is required.');
|
|
} else if (!idPattern.test(clientId)) {
|
|
errors.push('Invalid client selected.');
|
|
}
|
|
|
|
if (!policyId) {
|
|
errors.push('Policy is required.');
|
|
} else if (!idPattern.test(policyId)) {
|
|
errors.push('Invalid policy selected.');
|
|
}
|
|
|
|
if (!approvedBy) {
|
|
errors.push('Approved By is required.');
|
|
} else if (!idPattern.test(approvedBy)) {
|
|
errors.push('Invalid approver selected.');
|
|
}
|
|
|
|
if (!desc) {
|
|
errors.push('Description is required.');
|
|
} else if (!descPattern.test(desc)) {
|
|
errors.push('Description contains invalid characters.');
|
|
}
|
|
|
|
if (!amount) {
|
|
errors.push('Amount is required.');
|
|
} else {
|
|
const amountNum = Number(amount);
|
|
if (isNaN(amountNum) || amountNum < 0) {
|
|
errors.push('Amount must be a non-negative number.');
|
|
} else if (amountNum > 100000000) {
|
|
errors.push('Amount cannot be greater than 100 Cr.');
|
|
}
|
|
}
|
|
|
|
if (!expenseDate) {
|
|
errors.push('Expense Date is required.');
|
|
} else if (!datePattern.test(expenseDate)) {
|
|
errors.push('Expense Date must be in DD-MM-YYYY format.');
|
|
}
|
|
|
|
if (errors.length) {
|
|
toastr.warning(errors.join('<br>'), 'Validation');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function validateExpenseSearchFilters() {
|
|
const errors = [];
|
|
const clientId = $('#client_id').val();
|
|
const policyId = $('#client_policy_id').val();
|
|
const approvedBy = $('#approved_by').val();
|
|
const desc = ($('#description').val() || '').trim();
|
|
const amount = ($('#amount').val() || '').trim();
|
|
const expenseDate= ($('#expense_date').val() || '').trim();
|
|
|
|
const idPattern = /^[0-9]+$/;
|
|
const descPattern = /^[a-zA-Z0-9\s\.\,\-\(\)\/\&\+]+$/;
|
|
const datePattern = /^[0-9]{2}-[0-9]{2}-[0-9]{4}$/;
|
|
|
|
if (clientId && !idPattern.test(clientId)) {
|
|
errors.push('Invalid client selected for search.');
|
|
}
|
|
|
|
if (policyId && !idPattern.test(policyId)) {
|
|
errors.push('Invalid policy selected for search.');
|
|
}
|
|
|
|
if (approvedBy && !idPattern.test(approvedBy)) {
|
|
errors.push('Invalid approver selected for search.');
|
|
}
|
|
|
|
if (desc && !descPattern.test(desc)) {
|
|
errors.push('Description filter contains invalid characters.');
|
|
}
|
|
|
|
if (amount) {
|
|
const amountNum = Number(amount);
|
|
if (isNaN(amountNum) || amountNum < 0) {
|
|
errors.push('Amount filter must be a non-negative number.');
|
|
} else if (amountNum > 100000000) {
|
|
errors.push('Amount filter cannot be greater than 100 Cr.');
|
|
}
|
|
}
|
|
|
|
if (expenseDate && !datePattern.test(expenseDate)) {
|
|
errors.push('Expense Date filter must be in DD-MM-YYYY format.');
|
|
}
|
|
|
|
if (errors.length) {
|
|
toastr.warning(errors.join('<br>'), 'Validation');
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
function populatePolicies(clientId, selectedPolicyId = null) {
|
|
if (!clientId) {
|
|
$('#client_policy_id').empty().append('<option value="">Select Policy</option>').trigger('change');
|
|
return;
|
|
}
|
|
|
|
if (expensePoliciesCache[clientId]) {
|
|
const policies = expensePoliciesCache[clientId];
|
|
let optionsHtml = '<option value=\"\">Select Policy</option>';
|
|
|
|
policies.forEach(function (p) {
|
|
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
|
|
});
|
|
|
|
$('#client_policy_id').html(optionsHtml);
|
|
|
|
if (selectedPolicyId) {
|
|
$('#client_policy_id').val(selectedPolicyId).trigger('change');
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: '<?= base_url("expense/client-policies"); ?>',
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
data: {client_id: clientId},
|
|
success: function (res) {
|
|
if (res.status) {
|
|
expensePoliciesCache[clientId] = res.data || [];
|
|
|
|
let optionsHtml = '<option value=\"\">Select Policy</option>';
|
|
(res.data || []).forEach(function (p) {
|
|
optionsHtml += `<option value=\"${p.id}\">${p.policy_no}</option>`;
|
|
});
|
|
|
|
$('#client_policy_id').html(optionsHtml);
|
|
|
|
if (selectedPolicyId) {
|
|
$('#client_policy_id').val(selectedPolicyId).trigger('change');
|
|
}
|
|
} else {
|
|
toastr.warning(res.message || 'Unable to load policies', 'WARNING');
|
|
}
|
|
},
|
|
error: function () {
|
|
toastr.error('Unexpected error while loading policies', 'ERROR');
|
|
}
|
|
});
|
|
}
|
|
|
|
function editExpense(id) {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
$.ajax({
|
|
url: '<?= base_url("expense/get"); ?>/' + id,
|
|
type: 'GET',
|
|
dataType: 'json',
|
|
success: function (res) {
|
|
if (res.status && res.data) {
|
|
const data = res.data;
|
|
|
|
$('#expense_id').val(data.id);
|
|
$('#description').val(data.description || '');
|
|
$('#amount').val(data.amount || '');
|
|
if (data.expense_date) {
|
|
const parts = (data.expense_date || '').split('-'); // Y-m-d
|
|
if (parts.length === 3) {
|
|
const formatted = parts[2] + '-' + parts[1] + '-' + parts[0];
|
|
$('#expense_date').val(formatted);
|
|
if (window.expenseDatePicker) {
|
|
window.expenseDatePicker.setDate(formatted, true, 'd-m-Y');
|
|
}
|
|
}
|
|
}
|
|
|
|
if (data.approved_by) {
|
|
$('#approved_by').val(data.approved_by).trigger('change');
|
|
}
|
|
|
|
if (data.client_id) {
|
|
$('#client_id').val(data.client_id).trigger('change');
|
|
populatePolicies(data.client_id, data.client_policy_id || null);
|
|
}
|
|
|
|
$('#btnSubmitExpense').text('Update');
|
|
$('html, body').animate({
|
|
scrollTop: $('#expense_form').offset().top - 100
|
|
}, 400);
|
|
} else {
|
|
toastr.warning(res.message || 'Expense not found', 'WARNING');
|
|
}
|
|
},
|
|
error: function () {
|
|
toastr.error('Unable to fetch expense details', 'ERROR');
|
|
}
|
|
});
|
|
}
|
|
|
|
function deleteExpense(id) {
|
|
if (!id) {
|
|
return;
|
|
}
|
|
|
|
Swal.fire({
|
|
title: "Are you sure?",
|
|
text: "You want to delete this expense",
|
|
icon: "warning",
|
|
showCancelButton: true,
|
|
confirmButtonColor: "#3085d6",
|
|
cancelButtonColor: "#d33",
|
|
confirmButtonText: "Yes",
|
|
}).then((result) => {
|
|
if (!result.isConfirmed) {
|
|
return;
|
|
}
|
|
|
|
const postData = {};
|
|
postData['<?= csrf_token() ?>'] = '<?= csrf_hash() ?>';
|
|
|
|
$.ajax({
|
|
url: '<?= base_url("expense/delete"); ?>/' + id,
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: postData,
|
|
success: function (res) {
|
|
if (res.status) {
|
|
toastr.success(res.message || 'Expense deleted successfully', 'SUCCESS');
|
|
setTimeout(function () {
|
|
location.reload();
|
|
}, 800);
|
|
} else {
|
|
toastr.warning(res.message || 'Unable to delete expense', 'WARNING');
|
|
}
|
|
},
|
|
error: function () {
|
|
toastr.error('Unable to delete expense', 'ERROR');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
$('#client_id').select2();
|
|
$('#client_policy_id').select2();
|
|
$('#approved_by').select2();
|
|
|
|
window.expenseDatePicker = flatpickr('#expense_date', {
|
|
dateFormat: 'd-m-Y',
|
|
allowInput: false,
|
|
maxDate: new Date(),
|
|
});
|
|
|
|
$('#expense-table').DataTable({
|
|
scrollX: true,
|
|
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 align-items-center'<'col-5 text-start'i><'col-7 d-flex justify-content-end align-items-center'<'me-2'l>p>>",
|
|
lengthMenu: [[10, 20, 50, 100], [10, 20, 50, 100]],
|
|
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>',
|
|
title: 'Expense List',
|
|
className: 'app-btn-primary ',
|
|
exportOptions: {
|
|
columns: ':not(:last-child)',
|
|
format: {
|
|
body: function (data, row, column, node) {
|
|
// Column index 2 is "Policy No" (S.No=0, Client=1, Policy No=2)
|
|
if (column === 2) {
|
|
// Prefix with apostrophe to force Excel treat as text and preserve long numbers
|
|
var text = $(node).text ? $(node).text() : data;
|
|
return "'" + text;
|
|
}
|
|
return data;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
extend: 'excel',
|
|
title: 'Expense-List',
|
|
sheetName: 'Expense-List',
|
|
text: '<i class="mdi mdi-file-excel"></i><span class="btn-custom"> EXCEL </span>',
|
|
className: 'app-btn-primary ',
|
|
exportOptions: {
|
|
orthogonal: 'sort',
|
|
columns: ':not(:last-child)',
|
|
format: {
|
|
body: function (data, row, column, node) {
|
|
if (column === 2) {
|
|
var text = $(node).text ? $(node).text() : data;
|
|
return "'" + text;
|
|
}
|
|
return data;
|
|
}
|
|
}
|
|
}
|
|
},
|
|
{
|
|
extend: 'pdf',
|
|
text: '<i class="mdi mdi-file-pdf"></i><span class="btn-custom"> PDF </span>',
|
|
title: 'Expense List',
|
|
className: 'app-btn-primary ',
|
|
exportOptions: {
|
|
columns: ':not(:last-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:11px; top:50%; transform:translateY(-53%); color:#666;"></i>
|
|
<i class="mdi mdi-close-circle datatable-clear-icon"
|
|
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>'
|
|
}
|
|
});
|
|
|
|
$('.dataTables_length label').css('height', '21px');
|
|
|
|
// Show any backend validation errors from search
|
|
if (Array.isArray(expenseValidationErrors) && expenseValidationErrors.length) {
|
|
expenseValidationErrors.forEach(function (msg) {
|
|
toastr.warning(msg, 'Validation');
|
|
});
|
|
}
|
|
|
|
$('#client_id').on('change', function () {
|
|
const clientId = $(this).val();
|
|
populatePolicies(clientId);
|
|
});
|
|
|
|
$('#btnSearchExpense').on('click', function () {
|
|
if (!validateExpenseSearchFilters()) {
|
|
return;
|
|
}
|
|
|
|
const params = {};
|
|
const clientId = $('#client_id').val();
|
|
const policyId = $('#client_policy_id').val();
|
|
const approvedBy = $('#approved_by').val();
|
|
const desc = $('#description').val();
|
|
const amount = $('#amount').val();
|
|
const expenseDate = $('#expense_date').val();
|
|
|
|
if (clientId) {
|
|
params.client_id = clientId;
|
|
}
|
|
if (policyId) {
|
|
params.client_policy_id = policyId;
|
|
}
|
|
if (approvedBy) {
|
|
params.approved_by = approvedBy;
|
|
}
|
|
if (desc) {
|
|
params.description = desc;
|
|
}
|
|
if (amount) {
|
|
params.amount = amount;
|
|
}
|
|
if (expenseDate) {
|
|
params.expense_date = expenseDate;
|
|
}
|
|
|
|
const query = $.param(params);
|
|
const baseUrl = '<?= base_url("expense"); ?>';
|
|
window.location.href = query ? baseUrl + '?' + query : baseUrl;
|
|
});
|
|
|
|
$('#btnSubmitExpense').on('click', function (e) {
|
|
e.preventDefault();
|
|
|
|
if (!validateExpenseSave()) {
|
|
return;
|
|
}
|
|
|
|
const formData = $('#expense_form').serialize();
|
|
|
|
$.ajax({
|
|
url: '<?= base_url("expense/save"); ?>',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: formData,
|
|
success: function (res) {
|
|
if (res.status) {
|
|
toastr.success(res.message || 'Expense saved successfully', 'SUCCESS');
|
|
setTimeout(function () {
|
|
location.reload();
|
|
}, 800);
|
|
} else {
|
|
const errors = res.errors || {};
|
|
let errorMsg = res.message || 'Unable to save expense';
|
|
|
|
if (Object.keys(errors).length) {
|
|
errorMsg = Object.values(errors).join('<br>');
|
|
}
|
|
|
|
toastr.warning(errorMsg, 'WARNING');
|
|
}
|
|
},
|
|
error: function () {
|
|
toastr.error('Unexpected error while saving expense', 'ERROR');
|
|
}
|
|
});
|
|
});
|
|
|
|
$('#btnResetExpense, #btnCancelExpense').on('click', function () {
|
|
resetExpenseForm();
|
|
});
|
|
|
|
// Show small popup near the info icon on hover (no tooltip)
|
|
let $popup = $('#expense-desc-popup');
|
|
if (!$popup.length) {
|
|
$popup = $('<div id="expense-desc-popup"></div>').appendTo('body');
|
|
}
|
|
|
|
$(document).on('mouseenter', '.expense-desc-icon', function () {
|
|
const desc = $(this).data('description') || '';
|
|
if (!desc) {
|
|
return;
|
|
}
|
|
|
|
$popup.text(desc);
|
|
|
|
const offset = $(this).offset();
|
|
const iconHeight = $(this).outerHeight() || 16;
|
|
|
|
$popup.css({
|
|
top: offset.top + iconHeight + 4,
|
|
left: offset.left,
|
|
display: 'block'
|
|
});
|
|
});
|
|
|
|
$(document).on('mouseleave', '.expense-desc-icon', function () {
|
|
$popup.hide();
|
|
});
|
|
});
|
|
</script>
|
|
|