feat: add payment editing and fix discharge templates
This commit is contained in:
parent
681ad44a69
commit
4ca463cc1e
|
|
@ -157,6 +157,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'finance.bank.deposit.create' => 'Can create bank deposit register entries',
|
||||
'finance.bank.audit' => 'Can mark bank transaction audit status',
|
||||
'finance.bank.statement.update' => 'Can mark updated in bank statement status',
|
||||
'finance.payment.edit' => 'Can correct payment mode, amount, or recorded user',
|
||||
'finance.compliance.view' => 'Can view finance compliance reports',
|
||||
'finance.doctor_payout.manage' => 'Can manage doctor payouts and agreements',
|
||||
'finance.vendor.manage' => 'Can manage finance vendors',
|
||||
|
|
@ -378,6 +379,7 @@ class AuthGroups extends ShieldAuthGroups
|
|||
'finance.cash.accounts.verify',
|
||||
'finance.bank.audit',
|
||||
'finance.bank.statement.update',
|
||||
'finance.payment.edit',
|
||||
],
|
||||
'user' => [],
|
||||
'beta' => [
|
||||
|
|
|
|||
|
|
@ -292,6 +292,16 @@ $routes->post('Payment_Medical/change_to_cash', 'Payment_Medical::change_to_cash
|
|||
$routes->post('Payment_Medical/update_amount', 'Payment_Medical::update_amount');
|
||||
$routes->post('Payment_Medical/change_user', 'Payment_Medical::change_user');
|
||||
|
||||
$paymentEditFilter = ['filter' => 'permission:finance.payment.edit'];
|
||||
$routes->get('Payment', 'Payment::index', $paymentEditFilter);
|
||||
$routes->post('Payment/payment_record', 'Payment::paymentRecord', $paymentEditFilter);
|
||||
$routes->post('Payment/change_to_bank', 'Payment::changeToBank', $paymentEditFilter);
|
||||
$routes->post('Payment/change_to_cash', 'Payment::changeToCash', $paymentEditFilter);
|
||||
$routes->post('Payment/change_user', 'Payment::changeUser', $paymentEditFilter);
|
||||
$routes->post('Payment/change_amount', 'Payment::changeAmount', $paymentEditFilter);
|
||||
$routes->post('Payment/update_amount', 'Payment::changeAmount', $paymentEditFilter);
|
||||
$routes->get('finance/payment-edit', 'Payment::index', $paymentEditFilter);
|
||||
|
||||
// Finance & Accounting SOP - Phase 1
|
||||
$routes->get('Finance', 'Finance::index');
|
||||
$routes->get('Finance/index', 'Finance::index');
|
||||
|
|
|
|||
|
|
@ -498,9 +498,6 @@ class Ipd_discharge extends BaseController
|
|||
private function buildDischargeSectionTokenVars(string $content, array $panelData): array
|
||||
{
|
||||
$full = trim($content);
|
||||
if ($full === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$ipd = $panelData['ipd_info'] ?? null;
|
||||
$dischargeStatus = $this->getDischargeStatusText($ipd);
|
||||
|
|
@ -1114,7 +1111,7 @@ class Ipd_discharge extends BaseController
|
|||
$patientCode = html_entity_decode((string) ($tokenVars['UHID'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
$ipdCode = html_entity_decode((string) ($tokenVars['IPD_CODE'] ?? ''), ENT_QUOTES, 'UTF-8');
|
||||
|
||||
if ($this->shouldUseContentOnlyTemplate($content, $templateHtml)) {
|
||||
if ($requestedTemplateId === null && $this->shouldUseContentOnlyTemplate($content, $templateHtml)) {
|
||||
$templateHtml = '{{CONTENT}}';
|
||||
}
|
||||
|
||||
|
|
|
|||
264
app/Controllers/Payment.php
Normal file
264
app/Controllers/Payment.php
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Models\PaymentModel;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class Payment extends BaseController
|
||||
{
|
||||
private PaymentModel $paymentModel;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->paymentModel = new PaymentModel();
|
||||
}
|
||||
|
||||
public function index(): string
|
||||
{
|
||||
return view('payment/payment_search');
|
||||
}
|
||||
|
||||
public function paymentRecord()
|
||||
{
|
||||
$paymentId = (int) preg_replace('/\D+/', '', (string) $this->request->getPost('txtsearch'));
|
||||
if ($paymentId <= 0) {
|
||||
return $this->searchResponse('<div class="alert alert-warning">Enter a valid Payment ID.</div>', 422);
|
||||
}
|
||||
|
||||
$payment = $this->paymentModel->findPayment($paymentId);
|
||||
if ($payment === null) {
|
||||
return $this->searchResponse('<div class="alert alert-warning">Payment record not found.</div>', 404);
|
||||
}
|
||||
|
||||
return $this->searchResponse($this->renderPayment($payment));
|
||||
}
|
||||
|
||||
public function changeToBank()
|
||||
{
|
||||
$paymentId = (int) $this->request->getPost('pay_id');
|
||||
$sourceId = (int) $this->request->getPost('cbo_pay_type');
|
||||
$transactionId = trim((string) $this->request->getPost('input_card_tran'));
|
||||
$source = $this->findBankSource($sourceId);
|
||||
|
||||
if ($source === null || $transactionId === '') {
|
||||
return $this->error('Bank/online source and transaction reference are required.', 422);
|
||||
}
|
||||
|
||||
$label = (string) $source['label'];
|
||||
return $this->applyCorrection($paymentId, [
|
||||
'payment_mode' => 2,
|
||||
'pay_bank_id' => $sourceId,
|
||||
'insert_code' => (string) $sourceId,
|
||||
'card_bank' => $label,
|
||||
'card_tran_id' => $transactionId,
|
||||
'update_remark' => 'Changed to Bank/Online',
|
||||
], 2, 'Payment mode changed to BANK [' . $label . '], reference ' . $transactionId);
|
||||
}
|
||||
|
||||
public function changeToCash()
|
||||
{
|
||||
$paymentId = (int) $this->request->getPost('pay_id');
|
||||
|
||||
return $this->applyCorrection($paymentId, [
|
||||
'payment_mode' => 1,
|
||||
'pay_bank_id' => 0,
|
||||
'card_bank' => '',
|
||||
'card_remark' => '',
|
||||
'cust_card' => '',
|
||||
'card_tran_id' => '',
|
||||
'bankcard_machine' => '',
|
||||
'insert_code' => '',
|
||||
'update_remark' => 'Changed to Cash',
|
||||
], 1, 'Payment mode changed to CASH');
|
||||
}
|
||||
|
||||
public function changeUser()
|
||||
{
|
||||
$paymentId = (int) $this->request->getPost('pay_id');
|
||||
$userId = (int) $this->request->getPost('user_list');
|
||||
$user = $this->db->table('users')->select('id,username')->where('id', $userId)->where('active', 1)->get(1)->getRowArray();
|
||||
if ($user === null) {
|
||||
return $this->error('Select a valid active user.', 422);
|
||||
}
|
||||
|
||||
$payment = $this->paymentModel->findPayment($paymentId);
|
||||
$oldUser = trim((string) ($payment['update_by'] ?? '-'));
|
||||
$newUser = (string) $user['username'] . '[' . $userId . ']';
|
||||
|
||||
return $this->applyCorrection($paymentId, [
|
||||
'update_by_id' => $userId,
|
||||
'update_by' => $newUser,
|
||||
'update_remark' => 'Payment user corrected',
|
||||
], 3, 'Payment user changed from ' . $oldUser . ' to ' . $newUser);
|
||||
}
|
||||
|
||||
public function changeAmount()
|
||||
{
|
||||
$paymentId = (int) $this->request->getPost('pay_id');
|
||||
$rawAmount = trim((string) $this->request->getPost('change_value'));
|
||||
if (! is_numeric($rawAmount) || (float) $rawAmount <= 0) {
|
||||
return $this->error('Enter a valid amount greater than zero.', 422);
|
||||
}
|
||||
|
||||
$payment = $this->paymentModel->findPayment($paymentId);
|
||||
$oldAmount = number_format((float) ($payment['amount'] ?? 0), 2, '.', '');
|
||||
$newAmount = number_format((float) $rawAmount, 2, '.', '');
|
||||
|
||||
return $this->applyCorrection($paymentId, [
|
||||
'amount' => $newAmount,
|
||||
'update_remark' => 'Payment amount corrected',
|
||||
], 4, 'Payment amount changed from ' . $oldAmount . ' to ' . $newAmount);
|
||||
}
|
||||
|
||||
private function applyCorrection(int $paymentId, array $changes, int $updateType, string $remark)
|
||||
{
|
||||
if ($paymentId <= 0) {
|
||||
return $this->error('Payment record not found.', 404);
|
||||
}
|
||||
|
||||
$reason = trim((string) $this->request->getPost('correction_reason'));
|
||||
if ($reason === '') {
|
||||
return $this->error('Correction reason is required.', 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$payment = $this->paymentModel->correctPayment($paymentId, $changes, $updateType, $this->actorName(), $remark . '. Reason: ' . $reason);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => 'Payment updated successfully.',
|
||||
'html' => $this->renderPayment($payment),
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
} catch (RuntimeException $e) {
|
||||
$status = str_contains(strtolower($e->getMessage()), 'not found') ? 404 : 409;
|
||||
return $this->error($e->getMessage(), $status);
|
||||
} catch (Throwable $e) {
|
||||
log_message('error', 'Payment correction failed: {message}', ['message' => $e->getMessage()]);
|
||||
return $this->error('Unable to update the payment record.', 500);
|
||||
}
|
||||
}
|
||||
|
||||
private function renderPayment(array $payment): string
|
||||
{
|
||||
$context = $this->paymentContext($payment);
|
||||
|
||||
return view('payment/payment_edit', [
|
||||
'payment' => $payment,
|
||||
'context' => $context,
|
||||
'users' => $this->db->table('users')->select('id,username')->where('active', 1)->orderBy('username')->get()->getResultArray(),
|
||||
'bank_sources' => $this->bankSources(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function paymentContext(array $payment): array
|
||||
{
|
||||
$type = (int) ($payment['payof_type'] ?? 0);
|
||||
$referenceId = (int) ($payment['payof_id'] ?? 0);
|
||||
$context = [
|
||||
'type' => [1 => 'OPD', 2 => 'OPD Charge', 3 => 'Organization Charge', 4 => 'IPD Payment'][$type] ?? 'Payment',
|
||||
'code' => (string) ($payment['payof_code'] ?? ''),
|
||||
'patient' => '',
|
||||
];
|
||||
|
||||
$tableMap = [
|
||||
1 => ['opd_master', 'opd_id', ['opd_code'], ['p_id'], ['p_name', 'P_name']],
|
||||
2 => ['invoice_master', 'id', ['invoice_code'], ['attach_id'], ['inv_name']],
|
||||
3 => ['organization_case_master', 'id', ['case_id_code'], ['p_id'], ['p_name', 'insurance_company_name']],
|
||||
4 => ['ipd_master', 'id', ['ipd_code'], ['p_id'], ['P_name', 'p_name']],
|
||||
];
|
||||
|
||||
if (! isset($tableMap[$type])) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
[$table, $key, $codeColumns, $patientColumns, $nameColumns] = $tableMap[$type];
|
||||
if (! $this->db->tableExists($table)) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$row = $this->db->table($table)->where($key, $referenceId)->get(1)->getRowArray();
|
||||
if ($row === null) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$context['code'] = $this->firstValue($row, $codeColumns, $context['code']);
|
||||
$context['patient'] = $this->firstValue($row, $nameColumns);
|
||||
$patientId = (int) $this->firstValue($row, $patientColumns, '0');
|
||||
if ($context['patient'] === '' && $patientId > 0) {
|
||||
foreach (['patient_master', 'patient_master_exten'] as $patientTable) {
|
||||
if (! $this->db->tableExists($patientTable)) {
|
||||
continue;
|
||||
}
|
||||
$patient = $this->db->table($patientTable)->where('id', $patientId)->get(1)->getRowArray();
|
||||
if ($patient !== null) {
|
||||
$context['patient'] = trim((string) ($patient['p_fname'] ?? '') . ' ' . (string) ($patient['p_lname'] ?? ''));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
private function bankSources(): array
|
||||
{
|
||||
if (! $this->db->tableExists('hospital_bank_payment_source') || ! $this->db->tableExists('hospital_bank')) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->db->table('hospital_bank_payment_source s')
|
||||
->select("s.id, CONCAT(COALESCE(s.pay_type, ''), ' [', COALESCE(b.bank_name, ''), ']') AS label", false)
|
||||
->join('hospital_bank b', 'b.id = s.bank_id', 'left')
|
||||
->orderBy('s.pay_type')
|
||||
->get()
|
||||
->getResultArray();
|
||||
}
|
||||
|
||||
private function findBankSource(int $sourceId): ?array
|
||||
{
|
||||
foreach ($this->bankSources() as $source) {
|
||||
if ((int) $source['id'] === $sourceId) {
|
||||
return $source;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function firstValue(array $row, array $columns, string $default = ''): string
|
||||
{
|
||||
foreach ($columns as $column) {
|
||||
if (isset($row[$column]) && trim((string) $row[$column]) !== '') {
|
||||
return trim((string) $row[$column]);
|
||||
}
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
private function actorName(): string
|
||||
{
|
||||
$user = auth()->user();
|
||||
return $user ? ((string) ($user->username ?? 'user') . '[' . (int) $user->id . ']') : 'system';
|
||||
}
|
||||
|
||||
private function searchResponse(string $body, int $status = 200)
|
||||
{
|
||||
return $this->response
|
||||
->setStatusCode($status)
|
||||
->setHeader('X-CSRF-TOKEN', csrf_hash())
|
||||
->setBody($body);
|
||||
}
|
||||
|
||||
private function error(string $message, int $status)
|
||||
{
|
||||
return $this->response->setStatusCode($status)->setJSON([
|
||||
'status' => 0,
|
||||
'message' => $message,
|
||||
'csrf_hash' => csrf_hash(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use App\Libraries\RoleRegistry;
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class AddFinancePaymentEditPermission extends Migration
|
||||
{
|
||||
private const PERMISSION = 'finance.payment.edit';
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (! $this->tableExists('auth_roles') || ! $this->tableExists('auth_role_permissions')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$now = date('Y-m-d H:i:s');
|
||||
foreach (['superadmin', 'admin', 'developer', 'accounts_officer'] as $alias) {
|
||||
$role = $this->db->table('auth_roles')->select('id')->where('alias', $alias)->get(1)->getRowArray();
|
||||
if ($role === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$exists = $this->db->table('auth_role_permissions')
|
||||
->where('role_id', (int) $role['id'])
|
||||
->where('permission', self::PERMISSION)
|
||||
->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_role_permissions')->insert([
|
||||
'role_id' => (int) $role['id'],
|
||||
'permission' => self::PERMISSION,
|
||||
'created_at' => $now,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->tableExists('auth_role_permissions')) {
|
||||
$this->db->table('auth_role_permissions')->where('permission', self::PERMISSION)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$this->db->table('auth_permissions_users')->where('permission', self::PERMISSION)->delete();
|
||||
}
|
||||
if ($this->tableExists('auth_roles')) {
|
||||
(new RoleRegistry($this->db))->publish();
|
||||
}
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Database\Migrations;
|
||||
|
||||
use CodeIgniter\Database\Migration;
|
||||
|
||||
class BackfillFinancePaymentEditUserPermission extends Migration
|
||||
{
|
||||
private const PERMISSION = 'finance.payment.edit';
|
||||
|
||||
public function up()
|
||||
{
|
||||
if (! $this->tableExists('auth_permissions_users')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$eligiblePermissions = [
|
||||
'finance.cash.accounts.accept',
|
||||
'finance.cash.accounts.verify',
|
||||
'finance.bank.audit',
|
||||
'users.manage-admins',
|
||||
];
|
||||
$userIds = array_unique(array_column(
|
||||
$this->db->table('auth_permissions_users')
|
||||
->select('user_id')
|
||||
->whereIn('permission', $eligiblePermissions)
|
||||
->get()
|
||||
->getResultArray(),
|
||||
'user_id'
|
||||
));
|
||||
|
||||
foreach ($userIds as $userId) {
|
||||
$exists = $this->db->table('auth_permissions_users')
|
||||
->where('user_id', (int) $userId)
|
||||
->where('permission', self::PERMISSION)
|
||||
->countAllResults();
|
||||
if ($exists === 0) {
|
||||
$this->db->table('auth_permissions_users')->insert([
|
||||
'user_id' => (int) $userId,
|
||||
'permission' => self::PERMISSION,
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function down()
|
||||
{
|
||||
if ($this->tableExists('auth_permissions_users')) {
|
||||
$this->db->table('auth_permissions_users')->where('permission', self::PERMISSION)->delete();
|
||||
}
|
||||
}
|
||||
|
||||
private function tableExists(string $table): bool
|
||||
{
|
||||
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray() !== null;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,14 +3,15 @@
|
|||
namespace App\Models;
|
||||
|
||||
use CodeIgniter\Database\BaseConnection;
|
||||
use RuntimeException;
|
||||
|
||||
class PaymentModel
|
||||
{
|
||||
private BaseConnection $db;
|
||||
|
||||
public function __construct()
|
||||
public function __construct(?BaseConnection $db = null)
|
||||
{
|
||||
$this->db = db_connect();
|
||||
$this->db = $db ?? db_connect();
|
||||
}
|
||||
|
||||
public function insertPayment(array $data): int
|
||||
|
|
@ -24,4 +25,59 @@ class PaymentModel
|
|||
{
|
||||
return (bool) $this->db->table('payment_history')->where('id', $id)->update($data);
|
||||
}
|
||||
|
||||
public function findPayment(int $id): ?array
|
||||
{
|
||||
return $this->db->table('payment_history')->where('id', $id)->get(1)->getRowArray();
|
||||
}
|
||||
|
||||
public function assertEditable(array $payment): void
|
||||
{
|
||||
$cashStatus = strtolower(trim((string) ($payment['cash_submission_status'] ?? 'open')));
|
||||
if ($cashStatus !== '' && $cashStatus !== 'open') {
|
||||
throw new RuntimeException('This payment is already included in a cash submission and cannot be edited.');
|
||||
}
|
||||
if ((int) ($payment['cash_submission_scroll_id'] ?? 0) > 0) {
|
||||
throw new RuntimeException('This payment is linked to a cash submission and cannot be edited.');
|
||||
}
|
||||
|
||||
$bankStatus = strtolower(trim((string) ($payment['bank_reconcile_status'] ?? '')));
|
||||
if ($bankStatus !== '' || (int) ($payment['bank_statement_entry_id'] ?? 0) > 0 || (int) ($payment['bank_settlement_entry_id'] ?? 0) > 0) {
|
||||
throw new RuntimeException('This payment is already reconciled with the bank and cannot be edited.');
|
||||
}
|
||||
}
|
||||
|
||||
public function correctPayment(int $id, array $changes, int $updateType, string $actor, string $remark): array
|
||||
{
|
||||
$payment = $this->findPayment($id);
|
||||
if ($payment === null) {
|
||||
throw new RuntimeException('Payment record not found.');
|
||||
}
|
||||
|
||||
$this->assertEditable($payment);
|
||||
$fields = $this->db->getFieldNames('payment_history') ?? [];
|
||||
$changes = array_intersect_key($changes, array_flip($fields));
|
||||
if ($changes === []) {
|
||||
throw new RuntimeException('No supported payment fields were provided.');
|
||||
}
|
||||
if (! $this->db->tableExists('payment_history_log')) {
|
||||
throw new RuntimeException('Payment audit log table is unavailable.');
|
||||
}
|
||||
|
||||
$this->db->transStart();
|
||||
$this->db->table('payment_history')->where('id', $id)->update($changes);
|
||||
$this->db->table('payment_history_log')->insert([
|
||||
'pay_id' => $id,
|
||||
'update_type' => $updateType,
|
||||
'update_by' => mb_substr($actor, 0, 50),
|
||||
'update_remark' => mb_substr($remark, 0, 200),
|
||||
]);
|
||||
$this->db->transComplete();
|
||||
|
||||
if (! $this->db->transStatus()) {
|
||||
throw new RuntimeException('Unable to update the payment record.');
|
||||
}
|
||||
|
||||
return $this->findPayment($id) ?? $payment;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
<div id="maindiv">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex align-items-center justify-content-between flex-wrap gap-2">
|
||||
<h3 class="card-title mb-0">Users</h3>
|
||||
|
|
@ -167,4 +168,5 @@
|
|||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</script>
|
||||
</div>
|
||||
|
|
@ -92,16 +92,19 @@ if ($user && method_exists($user, 'can')) {
|
|||
|| $user->can('finance.bank.deposit.create')
|
||||
|| $user->can('finance.bank.audit')
|
||||
|| $user->can('finance.bank.statement.update')
|
||||
|| $user->can('finance.payment.edit')
|
||||
|| $user->can('finance.*');
|
||||
}
|
||||
|
||||
$canFinanceBilling = false;
|
||||
$canFinanceAccounts = false;
|
||||
$canFinanceBank = false;
|
||||
$canFinancePaymentEdit = false;
|
||||
if ($user && method_exists($user, 'can')) {
|
||||
$canFinanceBilling = $user->can('finance.cash.billing.submit') || $user->can('finance.*');
|
||||
$canFinanceAccounts = $user->can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.*');
|
||||
$canFinanceBank = $user->can('finance.bank.deposit.create') || $user->can('finance.bank.audit') || $user->can('finance.bank.statement.update') || $user->can('finance.*');
|
||||
$canFinancePaymentEdit = $user->can('finance.payment.edit') || $user->can('finance.*');
|
||||
}
|
||||
|
||||
$canDoctorWork = false;
|
||||
|
|
@ -294,6 +297,14 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
|
|||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($canFinancePaymentEdit) { ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Payment') ?>','Payment Edit')">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
<span>Payment Edit</span>
|
||||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<?php if ($canFinanceBank) { ?>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/bank_audit') ?>','Bank Payment Audit')">
|
||||
|
|
|
|||
101
app/Views/payment/payment_edit.php
Normal file
101
app/Views/payment/payment_edit.php
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
$payment = isset($payment) && is_array($payment) ? $payment : [];
|
||||
$context = isset($context) && is_array($context) ? $context : [];
|
||||
$users = isset($users) && is_array($users) ? $users : [];
|
||||
$bank_sources = isset($bank_sources) && is_array($bank_sources) ? $bank_sources : [];
|
||||
$paymentId = (int) ($payment['id'] ?? 0);
|
||||
$paymentMode = (int) ($payment['payment_mode'] ?? 0);
|
||||
$updateById = (int) ($payment['update_by_id'] ?? 0);
|
||||
$bankSourceId = (int) ($payment['pay_bank_id'] ?? 0);
|
||||
$cashStatus = strtolower(trim((string) ($payment['cash_submission_status'] ?? 'open')));
|
||||
$bankStatus = strtolower(trim((string) ($payment['bank_reconcile_status'] ?? '')));
|
||||
$locked = ($cashStatus !== '' && $cashStatus !== 'open')
|
||||
|| (int) ($payment['cash_submission_scroll_id'] ?? 0) > 0
|
||||
|| $bankStatus !== ''
|
||||
|| (int) ($payment['bank_statement_entry_id'] ?? 0) > 0
|
||||
|| (int) ($payment['bank_settlement_entry_id'] ?? 0) > 0;
|
||||
?>
|
||||
|
||||
<div class="card border-primary">
|
||||
<div class="card-body pt-3">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-3">
|
||||
<div>
|
||||
<h5 class="mb-1">Payment ID: <?= esc((string) $paymentId) ?></h5>
|
||||
<div class="text-muted small"><?= esc((string) ($payment['payment_date'] ?? $payment['insert_time'] ?? '')) ?></div>
|
||||
</div>
|
||||
<span class="badge <?= $paymentMode === 1 ? 'bg-success' : 'bg-primary' ?>"><?= $paymentMode === 1 ? 'CASH' : 'BANK / ONLINE' ?></span>
|
||||
</div>
|
||||
|
||||
<div class="row g-3 mb-3 small">
|
||||
<div class="col-md-3"><strong>Invoice Type</strong><br><?= esc((string) ($context['type'] ?? '-')) ?></div>
|
||||
<div class="col-md-3"><strong>Invoice / Visit No.</strong><br><?= esc((string) ($context['code'] ?? '-')) ?></div>
|
||||
<div class="col-md-3"><strong>Patient</strong><br><?= esc((string) ($context['patient'] ?? '-')) ?></div>
|
||||
<div class="col-md-3"><strong>Amount</strong><br>Rs. <?= esc(number_format((float) ($payment['amount'] ?? 0), 2)) ?></div>
|
||||
</div>
|
||||
|
||||
<?php if ($locked): ?>
|
||||
<div class="alert alert-warning">This payment is already submitted or bank-reconciled. Corrections are locked to preserve the finance audit trail.</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<input type="hidden" id="payment-edit-id" value="<?= esc((string) $paymentId) ?>">
|
||||
<div id="payment-edit-action-message"></div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="payment-edit-reason" class="form-label">Correction Reason <span class="text-danger">*</span></label>
|
||||
<input class="form-control" id="payment-edit-reason" maxlength="120" placeholder="Why is this payment being corrected?" <?= $locked ? 'disabled' : '' ?> required>
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-3 mb-3">
|
||||
<h6>Payment Mode</h6>
|
||||
<?php if ($paymentMode === 1): ?>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-5">
|
||||
<label for="payment-edit-bank-source" class="form-label">Bank / Online Source</label>
|
||||
<select class="form-select" id="payment-edit-bank-source" <?= $locked ? 'disabled' : '' ?>>
|
||||
<option value="">Select source</option>
|
||||
<?php foreach ($bank_sources as $source): ?>
|
||||
<option value="<?= esc((string) $source['id']) ?>"><?= esc((string) $source['label']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="payment-edit-transaction" class="form-label">Transaction ID / Reference</label>
|
||||
<input class="form-control" id="payment-edit-transaction" maxlength="100" <?= $locked ? 'disabled' : '' ?>>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" id="payment-edit-to-bank" <?= $locked ? 'disabled' : '' ?>>Change to Bank/Online</button></div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-6"><strong>Source:</strong> <?= esc((string) ($payment['card_bank'] ?? 'Bank/Online')) ?><br><strong>Reference:</strong> <?= esc((string) ($payment['card_tran_id'] ?? '-')) ?></div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" id="payment-edit-to-cash" <?= $locked ? 'disabled' : '' ?>>Change to Cash</button></div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-3 mb-3">
|
||||
<h6>Recorded User</h6>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-6">
|
||||
<label for="payment-edit-user" class="form-label">User</label>
|
||||
<select class="form-select" id="payment-edit-user" <?= $locked ? 'disabled' : '' ?>>
|
||||
<?php foreach ($users as $user): ?>
|
||||
<option value="<?= esc((string) $user['id']) ?>" <?= (int) $user['id'] === $updateById ? 'selected' : '' ?>><?= esc((string) $user['username']) ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" id="payment-edit-change-user" <?= $locked ? 'disabled' : '' ?>>Change User</button></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border rounded p-3">
|
||||
<h6>Payment Amount</h6>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-4">
|
||||
<label for="payment-edit-amount" class="form-label">Correct Amount</label>
|
||||
<input class="form-control" type="number" min="0.01" step="0.01" id="payment-edit-amount" value="<?= esc(number_format((float) ($payment['amount'] ?? 0), 2, '.', '')) ?>" <?= $locked ? 'disabled' : '' ?>>
|
||||
</div>
|
||||
<div class="col-md-3 d-grid"><button class="btn btn-primary" id="payment-edit-change-amount" <?= $locked ? 'disabled' : '' ?>>Change Amount</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
124
app/Views/payment/payment_search.php
Normal file
124
app/Views/payment/payment_search.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<div class="card mb-3">
|
||||
<div class="card-body py-3">
|
||||
<h5 class="card-title mb-3">Payment Edit</h5>
|
||||
<form id="payment-edit-search-form" method="post">
|
||||
<?= csrf_field() ?>
|
||||
<div class="row g-2 align-items-end">
|
||||
<div class="col-md-10">
|
||||
<label for="payment-edit-search-id" class="form-label">Payment ID</label>
|
||||
<input class="form-control" type="number" min="1" step="1" id="payment-edit-search-id" name="txtsearch" placeholder="Enter Payment ID" required>
|
||||
</div>
|
||||
<div class="col-md-2 d-grid">
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-search me-1"></i>Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="payment-edit-result"></div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
if (!window.jQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const $doc = window.jQuery(document);
|
||||
let actionInFlight = false;
|
||||
let csrfHash = '<?= esc(csrf_hash()) ?>';
|
||||
const csrfToken = '<?= esc(csrf_token()) ?>';
|
||||
|
||||
function showResult(html) {
|
||||
window.jQuery('#payment-edit-result').html(html);
|
||||
}
|
||||
|
||||
function showError(xhr) {
|
||||
const message = xhr.responseJSON && xhr.responseJSON.message
|
||||
? xhr.responseJSON.message
|
||||
: 'Unable to update payment record.';
|
||||
window.jQuery('#payment-edit-action-message').html('<div class="alert alert-danger mb-3">' + window.jQuery('<div>').text(message).html() + '</div>');
|
||||
if (xhr.responseJSON && xhr.responseJSON.csrf_hash) {
|
||||
csrfHash = xhr.responseJSON.csrf_hash;
|
||||
}
|
||||
}
|
||||
|
||||
function captureSearchToken(xhr) {
|
||||
const nextToken = xhr.getResponseHeader('X-CSRF-TOKEN');
|
||||
if (nextToken) {
|
||||
csrfHash = nextToken;
|
||||
window.jQuery('#payment-edit-search-form input[name="' + csrfToken + '"]').val(nextToken);
|
||||
}
|
||||
}
|
||||
|
||||
function correctionPayload(extra) {
|
||||
return Object.assign({
|
||||
pay_id: window.jQuery('#payment-edit-id').val(),
|
||||
correction_reason: window.jQuery('#payment-edit-reason').val(),
|
||||
[csrfToken]: csrfHash
|
||||
}, extra || {});
|
||||
}
|
||||
|
||||
function updatePayment(url, payload, confirmation) {
|
||||
if (actionInFlight || !confirm(confirmation)) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionInFlight = true;
|
||||
window.jQuery('#payment-edit-action-message').html('<div class="alert alert-info mb-3">Updating payment...</div>');
|
||||
window.jQuery.post(url, payload)
|
||||
.done(function (response) {
|
||||
if (response.csrf_hash) {
|
||||
csrfHash = response.csrf_hash;
|
||||
}
|
||||
showResult('<div class="alert alert-success">' + window.jQuery('<div>').text(response.message).html() + '</div>' + response.html);
|
||||
})
|
||||
.fail(showError)
|
||||
.always(function () {
|
||||
actionInFlight = false;
|
||||
});
|
||||
}
|
||||
|
||||
$doc.off('submit.paymentGeneralEdit', '#payment-edit-search-form')
|
||||
.on('submit.paymentGeneralEdit', '#payment-edit-search-form', function (event) {
|
||||
event.preventDefault();
|
||||
showResult('<div class="alert alert-info">Loading payment...</div>');
|
||||
window.jQuery.post('<?= base_url('Payment/payment_record') ?>', window.jQuery(this).serialize())
|
||||
.done(function (html, textStatus, xhr) {
|
||||
captureSearchToken(xhr);
|
||||
showResult(html);
|
||||
})
|
||||
.fail(function (xhr) {
|
||||
captureSearchToken(xhr);
|
||||
showResult(xhr.responseText || '<div class="alert alert-danger">Unable to find payment.</div>');
|
||||
});
|
||||
});
|
||||
|
||||
$doc.off('click.paymentGeneralEdit', '#payment-edit-to-bank')
|
||||
.on('click.paymentGeneralEdit', '#payment-edit-to-bank', function () {
|
||||
updatePayment('<?= base_url('Payment/change_to_bank') ?>', correctionPayload({
|
||||
cbo_pay_type: window.jQuery('#payment-edit-bank-source').val(),
|
||||
input_card_tran: window.jQuery('#payment-edit-transaction').val()
|
||||
}), 'Change this payment to Bank/Online?');
|
||||
});
|
||||
|
||||
$doc.off('click.paymentGeneralEdit', '#payment-edit-to-cash')
|
||||
.on('click.paymentGeneralEdit', '#payment-edit-to-cash', function () {
|
||||
updatePayment('<?= base_url('Payment/change_to_cash') ?>', correctionPayload(), 'Change this payment to Cash?');
|
||||
});
|
||||
|
||||
$doc.off('click.paymentGeneralEdit', '#payment-edit-change-user')
|
||||
.on('click.paymentGeneralEdit', '#payment-edit-change-user', function () {
|
||||
updatePayment('<?= base_url('Payment/change_user') ?>', correctionPayload({
|
||||
user_list: window.jQuery('#payment-edit-user').val()
|
||||
}), 'Change the user recorded for this payment?');
|
||||
});
|
||||
|
||||
$doc.off('click.paymentGeneralEdit', '#payment-edit-change-amount')
|
||||
.on('click.paymentGeneralEdit', '#payment-edit-change-amount', function () {
|
||||
updatePayment('<?= base_url('Payment/change_amount') ?>', correctionPayload({
|
||||
change_value: window.jQuery('#payment-edit-amount').val()
|
||||
}), 'Change the amount recorded for this payment?');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user