finance/storestock: implement cash submission audits, bank settlements, and opd payout tracking

This commit is contained in:
DevSoftTech Server 2026-04-22 10:53:20 +05:30
parent 7e4903bd68
commit d02244444e
48 changed files with 5533 additions and 335 deletions

View File

@ -2,6 +2,38 @@
Date: 2026-02-08
---
## Storestock Module — 2026-04-19
Ported Hospital Stock Management (Storestock) module from CI3 to CI4.
### Files created
- `app/Controllers/Storestock.php` — full port: index, indent_list, getIndentTable, new_indent, indent_create, indent_show, indent_items_list, add_item (type 1/2/3), get_drug, final_invoice, print_single_indent, store_stock, store_stock_result, get_product_stock, report_2, main_store
- `app/Views/storestock/dashboard.php`
- `app/Views/storestock/indent_list.php` — jQuery DataTable server-side, CI4 CSRF
- `app/Views/storestock/new_indent.php` — location + employee indent creation
- `app/Views/storestock/indent_edit.php` — add/update/delete items via AJAX, jQuery autocomplete for drug search
- `app/Views/storestock/indent_item_list.php` — AJAX partial; qty edit, remove buttons
- `app/Views/storestock/final_invoice.php` — read-only summary + print link
- `app/Views/storestock/store_stock_search.php` — search form with schedule filter
- `app/Views/storestock/stock_search_result.php` — result table + modal for batch detail
- `app/Views/storestock/stock_item_history.php` — batch-wise purchase detail table
- `app/Views/storestock/day_report.php` — date range stub
- `app/Views/storestock/main_store_dashboard.php` — main store nav panel
- `app/Views/storestock/store_print.php` — mPDF print template
### Routes modified
- `app/Config/Routes.php` — 17 Storestock routes added after `service('auth')->routes()`
### Key patterns
- Auth via `ensureStoreAccess()` — checks `hospital_stock.access` permission + group membership
- `getIndentTable()` uses parameterised queries (no raw user-input interpolation)
- `add_item()` calls stored procs `p_stock_update_purchase_id_store` + `p_update_med_GST_store`
- indent_code generated as `S{ym}{padded_id}` after insert
- Delete archives row to `inv_stock_item_delete` before deleting from `inv_stock_item`
- `print_single_indent` uses mPDF (Mpdf\Mpdf), no debug toolbar
## Summary
Ported OPD Charges, IPD Charges, and Package modules from CI3 to CI4 with consistent NiceAdmin UI, AJAX modal flows, Simple-DataTables, insurance rate handling, and print/Excel exports.

View File

@ -73,6 +73,14 @@ class AuthGroups extends ShieldAuthGroups
'title' => 'Storekeeper',
'description' => 'Handles item issuance and stock receiving operations.',
],
'billing_cashier' => [
'title' => 'Billing Cashier',
'description' => 'Submits billing cash statements to accounts.',
],
'accounts_officer' => [
'title' => 'Accounts Officer',
'description' => 'Accepts/verifies billing statements and audits bank records.',
],
'user' => [
'title' => 'User',
'description' => 'General users of the site. Often customers.',
@ -124,15 +132,13 @@ class AuthGroups extends ShieldAuthGroups
'reports.collection.view' => 'Can view collection reports',
'reports.insurance_credit.view' => 'Can view insurance credit reports',
'reports.nabh_audit.view' => 'Can view NABH audit report',
'finance.access' => 'Can access finance module',
'finance.vendor.manage' => 'Can manage finance vendors',
'finance.po.manage' => 'Can manage purchase orders',
'finance.grn.manage' => 'Can manage GRN entries',
'finance.invoice.manage' => 'Can manage vendor invoices',
'finance.cash.manage' => 'Can manage cash collection and disbursement',
'finance.doctor_payout.manage' => 'Can manage doctor payout workflow',
'finance.bank_deposit.manage' => 'Can manage bank deposit register',
'finance.compliance.view' => 'Can view finance compliance report',
'finance.workflow.view' => 'Can access Accounts and Finance workflow dashboard',
'finance.cash.billing.submit' => 'Can create billing cash entries and submit cash statements',
'finance.cash.accounts.accept' => 'Can accept submitted billing cash statements',
'finance.cash.accounts.verify' => 'Can verify accepted billing cash statements',
'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',
'diagnosis.access' => 'Can access diagnosis module',
'diagnosis.report.view' => 'Can view diagnosis reports',
'doctor_work.access' => 'Can access doctor work module',
@ -269,6 +275,18 @@ class AuthGroups extends ShieldAuthGroups
'hospital_stock.purchase.manage',
'hospital_stock.report.view',
],
'billing_cashier' => [
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.bank.deposit.create',
],
'accounts_officer' => [
'finance.workflow.view',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.audit',
'finance.bank.statement.update',
],
'user' => [],
'beta' => [
'beta.access',

View File

@ -262,6 +262,12 @@ $routes->get('Finance/scroll_items_table', 'Finance::scrollItemsTable');
$routes->get('Finance/bank_deposits', 'Finance::bankDeposits');
$routes->get('Finance/bank_deposits_table', 'Finance::bankDepositsTable');
$routes->get('Finance/bank_audit', 'Finance::bankAudit');
$routes->get('Finance/payout/opd-consult', 'Finance::payoutOpdConsult');
$routes->get('Finance/payout/opd-consult-summary', 'Finance::payoutOpdConsultSummary');
$routes->get('Finance/payout/opd-consult-drafts-table', 'Finance::payoutOpdConsultDraftsTable');
$routes->post('Finance/payout/opd-consult-draft-create', 'Finance::payoutOpdConsultDraftCreate');
$routes->post('Finance/payout/opd-consult-draft-update', 'Finance::payoutOpdConsultDraftUpdate');
$routes->post('Finance/payout/opd-consult-draft-delete', 'Finance::payoutOpdConsultDraftDelete');
$routes->get('Finance/bank_audit_direct_payments_table', 'Finance::bankAuditDirectPaymentsTable');
$routes->get('Finance/bank_statement_entries_table', 'Finance::bankStatementEntriesTable');
$routes->get('Finance/bank_settlement_entries_table', 'Finance::bankSettlementEntriesTable');

View File

@ -2,6 +2,7 @@
namespace App\Controllers;
use App\Models\DoctorModel;
use App\Models\FinanceGrnModel;
use App\Models\FinanceCashTransactionModel;
use App\Models\FinanceBankDepositModel;
@ -1800,6 +1801,384 @@ class Finance extends BaseController
return view('finance/index');
}
public function payoutOpdConsult()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
])) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
$doctorModel = new DoctorModel();
$doctorRows = $doctorModel->getDoctors();
usort($doctorRows, static function ($left, $right): int {
$leftName = trim((string) (($left->p_title ?? '') . ' ' . ($left->p_fname ?? '')));
$rightName = trim((string) (($right->p_title ?? '') . ' ' . ($right->p_fname ?? '')));
return strcasecmp($leftName, $rightName);
});
$doctorOptions = [];
foreach ($doctorRows as $row) {
$isActive = property_exists($row, 'active') ? (int) ($row->active ?? 0) : 1;
if ($isActive !== 1) {
continue;
}
$doctorOptions[] = [
'id' => (int) ($row->id ?? 0),
'name' => trim((string) (($row->p_title ?? '') . ' ' . ($row->p_fname ?? ''))),
];
}
return view('finance/payout_opd_consult', [
'doctor_options' => $doctorOptions,
'state_unit_options' => $this->fetchOpdStateUnitOptions(),
]);
}
public function payoutOpdConsultSummary()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
])) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
$fromDate = $this->normalizeDateInput((string) ($this->request->getGet('from_date') ?? ''));
$toDate = $this->normalizeDateInput((string) ($this->request->getGet('to_date') ?? ''));
$doctorId = (int) ($this->request->getGet('doctor_id') ?? 0);
$stateUnit = trim((string) ($this->request->getGet('state_unit') ?? ''));
if ($fromDate === '') {
$fromDate = date('Y-m-01');
}
if ($toDate === '') {
$toDate = date('Y-m-d');
}
if ($fromDate > $toDate) {
[$fromDate, $toDate] = [$toDate, $fromDate];
}
return view('finance/partials/payout_opd_consult_summary', $this->buildOpdConsultPayoutSummaryData($fromDate, $toDate, $doctorId, $stateUnit));
}
public function payoutOpdConsultDraftsTable()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
'finance.doctor_payout.manage',
])) {
return $this->response->setStatusCode(403)->setBody('Access denied');
}
$fromDate = $this->normalizeDateInput((string) ($this->request->getGet('from_date') ?? ''));
$toDate = $this->normalizeDateInput((string) ($this->request->getGet('to_date') ?? ''));
$doctorId = (int) ($this->request->getGet('doctor_id') ?? 0);
$stateUnit = trim((string) ($this->request->getGet('state_unit') ?? ''));
if ($fromDate === '') {
$fromDate = date('Y-m-01');
}
if ($toDate === '') {
$toDate = date('Y-m-d');
}
if ($fromDate > $toDate) {
[$fromDate, $toDate] = [$toDate, $fromDate];
}
$builder = $this->doctorPayoutModel
->select('finance_doctor_payouts.*, finance_doctor_agreements.doctor_code, finance_doctor_agreements.doctor_name')
->join('finance_doctor_agreements', 'finance_doctor_agreements.id = finance_doctor_payouts.doctor_id', 'left')
->where('finance_doctor_payouts.payout_type', 'consultation')
->where('finance_doctor_payouts.payout_date >=', $fromDate)
->where('finance_doctor_payouts.payout_date <=', $toDate)
->orderBy('finance_doctor_payouts.id', 'DESC');
if ($doctorId > 0) {
$agreementIds = $this->findDoctorAgreementIdsFromMaster($doctorId);
if (! empty($agreementIds)) {
$builder->whereIn('finance_doctor_payouts.doctor_id', $agreementIds);
} else {
$builder->where('1 = 0', null, false);
}
}
if ($stateUnit !== '') {
$builder->like('finance_doctor_payouts.case_reference', '-U' . $this->normalizeStateUnitToken($stateUnit), 'both');
}
return view('finance/partials/payout_opd_consult_drafts_table', [
'rows' => $builder->findAll(30),
]);
}
public function payoutOpdConsultDraftCreate()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
'finance.doctor_payout.manage',
])) {
return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']);
}
if (! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('finance_doctor_payouts')) {
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Payout table not available.']);
}
$doctorId = (int) ($this->request->getPost('doctor_id') ?? 0);
$fromDate = $this->normalizeDateInput((string) ($this->request->getPost('from_date') ?? ''));
$toDate = $this->normalizeDateInput((string) ($this->request->getPost('to_date') ?? ''));
$stateUnit = trim((string) ($this->request->getPost('state_unit') ?? ''));
$baseAmount = (float) ($this->request->getPost('base_amount') ?? 0);
$doctorShare = (float) ($this->request->getPost('doctor_share') ?? 75);
$hospitalShare = (float) ($this->request->getPost('hospital_share') ?? 25);
$deductions = (float) ($this->request->getPost('deductions') ?? 0);
$adjustments = (float) ($this->request->getPost('adjustments') ?? 0);
if ($doctorId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Please select a doctor before creating payout draft.']);
}
if ($fromDate === '') {
$fromDate = date('Y-m-01');
}
if ($toDate === '') {
$toDate = date('Y-m-d');
}
if ($fromDate > $toDate) {
[$fromDate, $toDate] = [$toDate, $fromDate];
}
$summaryData = $this->buildOpdConsultPayoutSummaryData($fromDate, $toDate, $doctorId, $stateUnit);
$summary = (array) ($summaryData['summary'] ?? []);
$eligibleOpdIds = $this->fetchEligibleOpdIdsForPayout($fromDate, $toDate, $doctorId, $stateUnit);
$completedOpd = count($eligibleOpdIds);
if ($completedOpd <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'No completed OPD found for selected filters.']);
}
if ($baseAmount <= 0) {
$baseAmount = (float) ($summary['total_received'] ?? 0);
}
$doctorGross = round(($baseAmount * $doctorShare) / 100, 2);
$hospitalGross = round(($baseAmount * $hospitalShare) / 100, 2);
$netPayable = round($doctorGross - $deductions + $adjustments, 2);
if ($netPayable < 0) {
$netPayable = 0.0;
}
$agreementId = $this->resolveOrCreateDoctorAgreementIdFromMaster($doctorId);
if ($agreementId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Unable to map selected doctor to payout agreement.']);
}
$rate = round($netPayable / max(1, $completedOpd), 2);
$caseReference = 'OPD-' . str_replace('-', '', $fromDate) . '-' . str_replace('-', '', $toDate) . '-D' . $doctorId;
if ($stateUnit !== '') {
$caseReference .= '-U' . $this->normalizeStateUnitToken($stateUnit);
}
$remarks = 'OPD payout draft from OPD Consult Payout | Completed=' . $completedOpd
. ', Base=' . number_format($baseAmount, 2, '.', '')
. ', Doc%=' . number_format($doctorShare, 2, '.', '')
. ', Hosp%=' . number_format($hospitalShare, 2, '.', '')
. ', Deductions=' . number_format($deductions, 2, '.', '')
. ', Adjustments=' . number_format($adjustments, 2, '.', '')
. ', DoctorGross=' . number_format($doctorGross, 2, '.', '')
. ', HospitalShare=' . number_format($hospitalGross, 2, '.', '')
. ', Net=' . number_format($netPayable, 2, '.', '');
$this->db->transStart();
$this->doctorPayoutModel->insert([
'payout_date' => $toDate,
'doctor_id' => $agreementId,
'case_reference' => $caseReference,
'payout_type' => 'consultation',
'units' => $completedOpd,
'rate' => $rate,
'calculated_amount' => $netPayable,
'approved_amount' => $netPayable,
'status' => 'draft',
'remarks' => $remarks,
'hr_submitted_by' => $this->currentUserName(),
]);
$insertId = (int) ($this->doctorPayoutModel->getInsertID() ?? 0);
if ($insertId > 0 && ! empty($eligibleOpdIds) && method_exists($this->db, 'tableExists') && $this->db->tableExists('opd_master')) {
$opdUpdate = [
'payout_draft_id' => $insertId,
'payout_calculated_at' => date('Y-m-d H:i:s'),
'payout_calculated_by' => $this->currentUserName(),
];
if ($this->tableHasField('opd_master', 'updated_at')) {
$opdUpdate['updated_at'] = date('Y-m-d H:i:s');
}
$this->db->table('opd_master')
->whereIn('opd_id', $eligibleOpdIds)
->update($opdUpdate);
}
$this->db->transComplete();
if (! $this->db->transStatus() || $insertId <= 0) {
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to create payout draft.']);
}
return $this->response->setJSON([
'status' => 1,
'message' => 'Payout draft created successfully.',
'payout_id' => $insertId,
'case_reference' => $caseReference,
'net_payable' => $netPayable,
]);
}
public function payoutOpdConsultDraftUpdate()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
'finance.doctor_payout.manage',
])) {
return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']);
}
$payoutId = (int) ($this->request->getPost('payout_id') ?? 0);
$payoutDate = $this->normalizeDateInput((string) ($this->request->getPost('payout_date') ?? ''));
$approvedAmount = (float) ($this->request->getPost('approved_amount') ?? 0);
$remarks = trim((string) ($this->request->getPost('remarks') ?? ''));
if ($payoutId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid payout id.']);
}
$row = $this->doctorPayoutModel->find($payoutId);
if (! is_array($row)) {
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Payout draft not found.']);
}
if ((string) ($row['status'] ?? 'draft') !== 'draft') {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Only draft entries can be edited.']);
}
if ($payoutDate === '') {
$payoutDate = (string) ($row['payout_date'] ?? date('Y-m-d'));
}
if ($approvedAmount < 0) {
$approvedAmount = 0;
}
$units = max(1, (int) ($row['units'] ?? 1));
$rate = round($approvedAmount / $units, 2);
$this->doctorPayoutModel->update($payoutId, [
'payout_date' => $payoutDate,
'approved_amount' => round($approvedAmount, 2),
'rate' => $rate,
'remarks' => $remarks !== '' ? $remarks : null,
]);
return $this->response->setJSON([
'status' => 1,
'message' => 'Payout draft updated successfully.',
]);
}
public function payoutOpdConsultDraftDelete()
{
if (! $this->canFinanceAny([
'finance.workflow.view',
'finance.cash.billing.submit',
'finance.cash.accounts.accept',
'finance.cash.accounts.verify',
'finance.bank.deposit.create',
'finance.bank.audit',
'finance.bank.statement.update',
'finance.doctor_payout.manage',
])) {
return $this->response->setStatusCode(403)->setJSON(['status' => 0, 'message' => 'Access denied']);
}
$payoutId = (int) ($this->request->getPost('payout_id') ?? 0);
if ($payoutId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Invalid payout id.']);
}
$row = $this->doctorPayoutModel->find($payoutId);
if (! is_array($row)) {
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Payout draft not found.']);
}
if ((string) ($row['status'] ?? 'draft') !== 'draft') {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Only draft entries can be deleted.']);
}
$this->db->transStart();
if (method_exists($this->db, 'tableExists') && $this->db->tableExists('opd_master') && $this->tableHasField('opd_master', 'payout_draft_id')) {
$clearData = [
'payout_draft_id' => null,
];
if ($this->tableHasField('opd_master', 'payout_calculated_at')) {
$clearData['payout_calculated_at'] = null;
}
if ($this->tableHasField('opd_master', 'payout_calculated_by')) {
$clearData['payout_calculated_by'] = null;
}
$this->db->table('opd_master')
->where('payout_draft_id', $payoutId)
->update($clearData);
}
$this->doctorPayoutModel->delete($payoutId);
$this->db->transComplete();
if (! $this->db->transStatus()) {
return $this->response->setStatusCode(500)->setJSON(['status' => 0, 'message' => 'Failed to delete payout draft.']);
}
return $this->response->setJSON([
'status' => 1,
'message' => 'Payout draft deleted and linked OPD records unlocked for recalculation.',
]);
}
public function phase2()
{
if (! $this->canFinance('finance.access')) {
@ -3307,6 +3686,369 @@ class Finance extends BaseController
return $value > 0 ? $value : $default;
}
private function buildOpdConsultPayoutSummaryData(string $fromDate, string $toDate, int $doctorId = 0, string $stateUnit = ''): array
{
$db = \Config\Database::connect();
$stateUnitColumn = $this->resolveOpdStateUnitColumn();
$runningExpr = "(COALESCE(o.running_opd,0)=1 OR o.opd_fee_type=3 OR UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%RUNNING%')";
$newExpr = "(o.opd_fee_type=1 OR UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%NEW%')";
$emergencyExpr = "(UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%EMERG%')";
$routineExpr = "(NOT {$emergencyExpr})";
$orgCreditExpr = "(COALESCE(o.insurance_case_id,0) > 0 OR COALESCE(o.insurance_id,0) > 0 OR COALESCE(o.payment_status,0) = 3)";
$doctorName = 'All Doctors';
if ($doctorId > 0) {
$doctorRow = $db->table('doctor_master')->select('p_title, p_fname')->where('id', $doctorId)->get()->getRow();
if ($doctorRow) {
$doctorName = trim((string) (($doctorRow->p_title ?? '') . ' ' . ($doctorRow->p_fname ?? '')));
}
}
$opdBuilder = $db->table('opd_master o')
->select('o.doc_id')
->select("COALESCE(NULLIF(TRIM(o.doc_name), ''), CONCAT('Doctor #', o.doc_id)) AS doctor_name", false)
->select('SUM(CASE WHEN o.opd_status IN (1,2) THEN 1 ELSE 0 END) AS completed_opd', false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$routineExpr} THEN 1 ELSE 0 END) AS routine_opd", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$emergencyExpr} THEN 1 ELSE 0 END) AS emergency_opd", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$runningExpr} THEN 1 ELSE 0 END) AS running_opd", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$newExpr} THEN 1 ELSE 0 END) AS new_opd", false)
->select('SUM(CASE WHEN o.opd_status IN (1,2) THEN COALESCE(o.opd_fee_amount,0) ELSE 0 END) AS gross_amount', false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$orgCreditExpr} THEN COALESCE(o.opd_fee_amount,0) ELSE 0 END) AS org_credit_amount", false)
->where('o.apointment_date >=', $fromDate)
->where('o.apointment_date <=', $toDate)
->whereIn('o.opd_status', [1, 2]);
if ($this->tableHasField('opd_master', 'payout_draft_id')) {
$opdBuilder->groupStart()
->where('o.payout_draft_id IS NULL', null, false)
->orWhere('o.payout_draft_id', 0)
->groupEnd();
}
if ($doctorId > 0) {
$opdBuilder->where('o.doc_id', $doctorId);
}
if ($stateUnit !== '' && $stateUnitColumn !== null) {
$opdBuilder->where('o.' . $stateUnitColumn, $stateUnit);
}
$doctorBreakdown = $opdBuilder
->groupBy('o.doc_id, o.doc_name')
->orderBy('doctor_name', 'ASC')
->get()
->getResultArray();
$summary = [
'completed_opd' => 0,
'routine_opd' => 0,
'emergency_opd' => 0,
'running_opd' => 0,
'new_opd' => 0,
'calculated_opd' => 0,
'gross_amount' => 0.0,
'org_credit_amount' => 0.0,
'doctor_count' => 0,
'approved_consents' => 0,
'payout_count' => 0,
'payout_amount' => 0.0,
'cash_received' => 0.0,
'bank_received' => 0.0,
'total_received' => 0.0,
'credit_amount' => 0.0,
];
foreach ($doctorBreakdown as &$row) {
$row['completed_opd'] = (int) ($row['completed_opd'] ?? 0);
$row['routine_opd'] = (int) ($row['routine_opd'] ?? 0);
$row['emergency_opd'] = (int) ($row['emergency_opd'] ?? 0);
$row['running_opd'] = (int) ($row['running_opd'] ?? 0);
$row['new_opd'] = (int) ($row['new_opd'] ?? 0);
$row['gross_amount'] = (float) ($row['gross_amount'] ?? 0);
$row['org_credit_amount'] = (float) ($row['org_credit_amount'] ?? 0);
$row['cash_received'] = 0.0;
$row['bank_received'] = 0.0;
$row['credit_amount'] = 0.0;
$row['payout_amount'] = 0.0;
$summary['completed_opd'] += $row['completed_opd'];
$summary['routine_opd'] += $row['routine_opd'];
$summary['emergency_opd'] += $row['emergency_opd'];
$summary['running_opd'] += $row['running_opd'];
$summary['new_opd'] += $row['new_opd'];
$summary['gross_amount'] += $row['gross_amount'];
$summary['org_credit_amount'] += $row['org_credit_amount'];
}
unset($row);
$summary['doctor_count'] = count($doctorBreakdown);
if ($this->tableHasField('opd_master', 'payout_draft_id')) {
$calcBuilder = $db->table('opd_master o')
->select('COUNT(*) AS calculated_opd', false)
->where('o.apointment_date >=', $fromDate)
->where('o.apointment_date <=', $toDate)
->whereIn('o.opd_status', [1, 2])
->where('o.payout_draft_id >', 0);
if ($doctorId > 0) {
$calcBuilder->where('o.doc_id', $doctorId);
}
if ($stateUnit !== '' && $stateUnitColumn !== null) {
$calcBuilder->where('o.' . $stateUnitColumn, $stateUnit);
}
$calcRow = $calcBuilder->get()->getRowArray();
$summary['calculated_opd'] = (int) ($calcRow['calculated_opd'] ?? 0);
}
$collectionBuilder = $db->table('payment_history p')
->select('o.doc_id')
->select('SUM(CASE WHEN p.credit_debit = 0 THEN COALESCE(p.amount,0) ELSE 0 END) AS total_received', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payment_mode = 1 THEN COALESCE(p.amount,0) ELSE 0 END) AS cash_received', false)
->select('SUM(CASE WHEN p.credit_debit = 0 AND p.payment_mode = 2 THEN COALESCE(p.amount,0) ELSE 0 END) AS bank_received', false)
->join('opd_master o', 'p.payof_type = 1 AND o.opd_id = p.payof_id', 'inner')
->where('DATE(p.payment_date) >=', $fromDate)
->where('DATE(p.payment_date) <=', $toDate);
if ($doctorId > 0) {
$collectionBuilder->where('o.doc_id', $doctorId);
}
if ($stateUnit !== '' && $stateUnitColumn !== null) {
$collectionBuilder->where('o.' . $stateUnitColumn, $stateUnit);
}
$collectionRows = $collectionBuilder->groupBy('o.doc_id')->get()->getResultArray();
$collectionMap = [];
foreach ($collectionRows as $row) {
$docKey = (int) ($row['doc_id'] ?? 0);
$collectionMap[$docKey] = [
'total_received' => (float) ($row['total_received'] ?? 0),
'cash_received' => (float) ($row['cash_received'] ?? 0),
'bank_received' => (float) ($row['bank_received'] ?? 0),
];
}
foreach ($doctorBreakdown as &$row) {
$docKey = (int) ($row['doc_id'] ?? 0);
$received = $collectionMap[$docKey] ?? ['total_received' => 0.0, 'cash_received' => 0.0, 'bank_received' => 0.0];
$row['cash_received'] = $received['cash_received'];
$row['bank_received'] = $received['bank_received'];
$row['total_received'] = $received['total_received'];
$row['credit_amount'] = max(0, round($row['gross_amount'] - $row['total_received'], 2));
$summary['cash_received'] += $row['cash_received'];
$summary['bank_received'] += $row['bank_received'];
}
unset($row);
$summary['total_received'] = $summary['cash_received'] + $summary['bank_received'];
$summary['credit_amount'] = max(0, round($summary['gross_amount'] - $summary['total_received'], 2));
if ($db->tableExists('abdm_consent_records')) {
$consentBuilder = $db->table('abdm_consent_records acr')
->select('COUNT(DISTINCT acr.id) AS approved_consents', false)
->join('opd_master o', 'o.p_id = acr.patient_id', 'inner')
->where('o.apointment_date >=', $fromDate)
->where('o.apointment_date <=', $toDate)
->whereIn('o.opd_status', [1, 2])
->where('acr.consent_status', 'approved');
if ($doctorId > 0) {
$consentBuilder->where('o.doc_id', $doctorId);
}
if ($stateUnit !== '' && $stateUnitColumn !== null) {
$consentBuilder->where('o.' . $stateUnitColumn, $stateUnit);
}
$consentRow = $consentBuilder->get()->getRowArray();
$summary['approved_consents'] = (int) ($consentRow['approved_consents'] ?? 0);
}
if ($db->tableExists('finance_doctor_payouts')) {
$payoutBuilder = $db->table('finance_doctor_payouts p')
->select('COUNT(*) AS payout_count, SUM(COALESCE(p.approved_amount, p.calculated_amount, 0)) AS payout_amount', false)
->where('p.payout_date >=', $fromDate)
->where('p.payout_date <=', $toDate);
if ($doctorId > 0) {
$agreementIds = $this->findDoctorAgreementIdsFromMaster($doctorId);
if (! empty($agreementIds)) {
$payoutBuilder->whereIn('p.doctor_id', $agreementIds);
} else {
$payoutBuilder->where('1 = 0', null, false);
}
}
$payoutRow = $payoutBuilder->get()->getRowArray();
$summary['payout_count'] = (int) ($payoutRow['payout_count'] ?? 0);
$summary['payout_amount'] = (float) ($payoutRow['payout_amount'] ?? 0);
}
foreach ($doctorBreakdown as &$row) {
$row['payout_amount'] = $summary['doctor_count'] === 1 ? $summary['payout_amount'] : 0.0;
}
unset($row);
return [
'summary' => $summary,
'doctor_breakdown' => $doctorBreakdown,
'from_date' => $fromDate,
'to_date' => $toDate,
'doctor_name' => $doctorName,
'state_unit' => $stateUnit,
'state_unit_label' => $stateUnitColumn !== null ? ucwords(str_replace('_', ' ', $stateUnitColumn)) : 'State/Unit',
];
}
private function fetchOpdStateUnitOptions(): array
{
$column = $this->resolveOpdStateUnitColumn();
if ($column === null || ! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('opd_master')) {
return [];
}
try {
$rows = $this->db->table('opd_master o')
->select('TRIM(o.' . $column . ') AS value', false)
->where('o.' . $column . ' IS NOT NULL', null, false)
->where("TRIM(o." . $column . ") != ''", null, false)
->groupBy('o.' . $column)
->orderBy('value', 'ASC')
->get()
->getResultArray();
return array_values(array_filter(array_map(static function (array $row): string {
return trim((string) ($row['value'] ?? ''));
}, $rows), static fn(string $value): bool => $value !== ''));
} catch (\Throwable $e) {
return [];
}
}
private function resolveOpdStateUnitColumn(): ?string
{
$candidates = ['unit_name', 'unit', 'state_name', 'state', 'branch_name', 'branch', 'center_name', 'location', 'city'];
foreach ($candidates as $column) {
if ($this->tableHasField('opd_master', $column)) {
return $column;
}
}
return null;
}
private function normalizeStateUnitToken(string $value): string
{
return preg_replace('/[^A-Za-z0-9]/', '', strtoupper(trim($value)));
}
private function findDoctorAgreementIdsFromMaster(int $doctorId): array
{
if ($doctorId <= 0) {
return [];
}
$ids = [];
$code = 'DM-' . $doctorId;
$row = $this->doctorAgreementModel->select('id')->where('doctor_code', $code)->first();
if (is_array($row) && (int) ($row['id'] ?? 0) > 0) {
$ids[] = (int) ($row['id'] ?? 0);
}
if (empty($ids) && method_exists($this->db, 'tableExists') && $this->db->tableExists('doctor_master')) {
$doctor = $this->db->table('doctor_master')->select('p_title, p_fname')->where('id', $doctorId)->get(1)->getRowArray();
if (is_array($doctor)) {
$doctorName = trim((string) (($doctor['p_title'] ?? '') . ' ' . ($doctor['p_fname'] ?? '')));
if ($doctorName !== '') {
$nameRows = $this->doctorAgreementModel->select('id')->like('doctor_name', $doctorName, 'both')->findAll();
foreach ($nameRows as $item) {
$id = (int) ($item['id'] ?? 0);
if ($id > 0) {
$ids[] = $id;
}
}
}
}
}
return array_values(array_unique(array_filter($ids, static fn(int $id): bool => $id > 0)));
}
private function fetchEligibleOpdIdsForPayout(string $fromDate, string $toDate, int $doctorId, string $stateUnit = ''): array
{
if (! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('opd_master')) {
return [];
}
$builder = $this->db->table('opd_master o')
->select('o.opd_id')
->where('o.apointment_date >=', $fromDate)
->where('o.apointment_date <=', $toDate)
->whereIn('o.opd_status', [1, 2]);
if ($doctorId > 0) {
$builder->where('o.doc_id', $doctorId);
}
$stateUnitColumn = $this->resolveOpdStateUnitColumn();
if ($stateUnit !== '' && $stateUnitColumn !== null) {
$builder->where('o.' . $stateUnitColumn, $stateUnit);
}
if ($this->tableHasField('opd_master', 'payout_draft_id')) {
$builder->groupStart()
->where('o.payout_draft_id IS NULL', null, false)
->orWhere('o.payout_draft_id', 0)
->groupEnd();
}
$rows = $builder->get()->getResultArray();
return array_values(array_filter(array_map(static fn(array $row): int => (int) ($row['opd_id'] ?? 0), $rows), static fn(int $id): bool => $id > 0));
}
private function resolveOrCreateDoctorAgreementIdFromMaster(int $doctorId): int
{
if ($doctorId <= 0 || ! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('doctor_master')) {
return 0;
}
$doctorRow = $this->db->table('doctor_master')
->select('id, p_title, p_fname')
->where('id', $doctorId)
->get(1)
->getRowArray();
if (! is_array($doctorRow)) {
return 0;
}
$doctorName = trim((string) (($doctorRow['p_title'] ?? '') . ' ' . ($doctorRow['p_fname'] ?? '')));
if ($doctorName === '') {
$doctorName = 'Doctor #' . $doctorId;
}
$doctorCode = 'DM-' . $doctorId;
$existing = $this->doctorAgreementModel->where('doctor_code', $doctorCode)->first();
if (is_array($existing) && (int) ($existing['id'] ?? 0) > 0) {
return (int) $existing['id'];
}
$this->doctorAgreementModel->insert([
'doctor_code' => $doctorCode,
'doctor_name' => $doctorName,
'specialization' => null,
'consultation_rate' => 0,
'surgery_rate' => 0,
'agreement_start_date' => null,
'agreement_end_date' => null,
'status' => 1,
'created_by' => $this->currentUserName(),
]);
return (int) ($this->doctorAgreementModel->getInsertID() ?? 0);
}
private function normalizeDateInput(string $date): string
{
$date = trim($date);

View File

@ -731,8 +731,14 @@ class Patient extends BaseController
return $this->response->setStatusCode(404)->setBody('Patient not found');
}
$opdFields = $this->db->getFieldNames('opd_master') ?? [];
$opdSelect = ['opd_id', 'opd_code', 'doc_name', 'apointment_date'];
if (in_array('queue_no', $opdFields, true)) {
$opdSelect[] = 'queue_no';
}
$opdList = $this->db->table('opd_master')
->select('opd_id, opd_code, doc_name, apointment_date, queue_no')
->select(implode(', ', $opdSelect))
->where('p_id', $pno)
->orderBy('opd_id', 'DESC')
->get()
@ -741,6 +747,7 @@ class Patient extends BaseController
$opdIds = array_column($opdList, 'opd_id');
$filesByOpd = [];
$rxByOpd = [];
$medicinesBySession = [];
if ($opdIds && $this->db->tableExists('file_upload_data')) {
$fields = $this->db->getFieldNames('file_upload_data') ?? [];
@ -790,7 +797,7 @@ class Patient extends BaseController
if (in_array('opd_id', $rxFields, true)) {
$selectParts[] = 'opd_id';
}
foreach (['date_opd_visit', 'queue_no', 'bp', 'diastolic', 'pulse', 'temp', 'spo2', 'complaints', 'diagnosis', 'investigation', 'advice'] as $col) {
foreach (['date_opd_visit', 'queue_no', 'bp', 'diastolic', 'pulse', 'temp', 'spo2', 'complaints', 'diagnosis', 'investigation', 'advice', 'next_visit', 'refer_to'] as $col) {
if (in_array($col, $rxFields, true)) {
$selectParts[] = $col;
}
@ -814,6 +821,55 @@ class Patient extends BaseController
}
}
$medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
$rxSessionIds = array_values(array_filter(array_map(static fn(array $row): int => (int) ($row['id'] ?? 0), $rxByOpd), static fn(int $id): bool => $id > 0));
if ($medicineTable !== null && $this->db->tableExists($medicineTable) && $rxSessionIds !== []) {
$medicineBuilder = $this->db->table($medicineTable . ' pt')
->select('pt.id, pt.opd_pre_id, pt.med_type, pt.med_name, pt.dosage, pt.dosage_when, pt.dosage_freq, pt.dosage_where, pt.no_of_days, pt.qty, pt.remark');
if ($this->db->tableExists('opd_dose_shed')) {
$medicineBuilder->select('d.dose_show_sign AS dose_shed', false)
->join('opd_dose_shed d', 'pt.dosage = d.dose_shed_id', 'left');
}
if ($this->db->tableExists('opd_dose_when')) {
$medicineBuilder->select('dw.dose_sign_desc AS dose_when_label', false)
->join('opd_dose_when dw', 'pt.dosage_when = dw.dose_when_id', 'left');
}
if ($this->db->tableExists('opd_dose_frequency')) {
$medicineBuilder->select('df.dose_sign_desc AS dose_frequency_label', false)
->join('opd_dose_frequency df', 'pt.dosage_freq = df.dose_freq_id', 'left');
}
if ($this->db->tableExists('opd_dose_where')) {
$medicineBuilder->select('d_on.dose_sign_desc AS dose_where_label', false)
->join('opd_dose_where d_on', 'pt.dosage_where = d_on.dose_where_id', 'left');
}
$medicineRows = $medicineBuilder
->whereIn('pt.opd_pre_id', $rxSessionIds)
->orderBy('pt.id', 'ASC')
->get()
->getResultArray();
foreach ($medicineRows as $medicineRow) {
$sessionId = (int) ($medicineRow['opd_pre_id'] ?? 0);
if ($sessionId <= 0) {
continue;
}
$medicinesBySession[$sessionId][] = [
'med_type' => trim((string) ($medicineRow['med_type'] ?? '')),
'med_name' => trim((string) ($medicineRow['med_name'] ?? '')),
'dose' => trim((string) ($medicineRow['dose_shed'] ?? '')),
'timing' => trim((string) ($medicineRow['dose_when_label'] ?? '')),
'frequency' => trim((string) ($medicineRow['dose_frequency_label'] ?? '')),
'where' => trim((string) ($medicineRow['dose_where_label'] ?? '')),
'days' => trim((string) ($medicineRow['no_of_days'] ?? '')),
'qty' => trim((string) ($medicineRow['qty'] ?? '')),
'remark' => trim((string) ($medicineRow['remark'] ?? '')),
];
}
}
$opdGroups = [];
foreach ($opdList as $row) {
$opdId = (int) $row['opd_id'];
@ -838,6 +894,9 @@ class Patient extends BaseController
'diagnosis' => (string) ($rx['diagnosis'] ?? ''),
'investigation' => (string) ($rx['investigation'] ?? ''),
'advice' => (string) ($rx['advice'] ?? ''),
'next_visit' => (string) ($rx['next_visit'] ?? ''),
'refer_to' => (string) ($rx['refer_to'] ?? ''),
'medicines' => $medicinesBySession[$rxSessionId] ?? [],
'files' => $filesByOpd[$opdId] ?? [],
];
}
@ -848,6 +907,17 @@ class Patient extends BaseController
]);
}
private function findExistingTable(array $candidates): ?string
{
foreach ($candidates as $table) {
if ($this->db->tableExists($table)) {
return $table;
}
}
return null;
}
public function save_profile_image(int $pid)
{
$patient = $this->db->table('patient_master')->where('id', $pid)->get()->getRow();

View File

@ -0,0 +1,950 @@
<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use Mpdf\Mpdf;
class Storestock extends BaseController
{
protected $db;
public function __construct()
{
$this->db = \Config\Database::connect();
}
// -------------------------------------------------------------------------
// Auth helper
// -------------------------------------------------------------------------
private function ensureStoreAccess()
{
$user = service('auth')->user();
$allowed = false;
if ($user && method_exists($user, 'can')) {
$allowed = $user->can('hospital_stock.access') || $user->can('pharmacy.access');
}
if (! $allowed && $user && method_exists($user, 'inGroup')) {
$allowed = $user->inGroup(
'superadmin', 'admin', 'developer',
'stock_manager', 'stock_requester', 'stock_issuer', 'storekeeper'
);
}
if ($allowed) {
return null;
}
return $this->response
->setStatusCode(403)
->setBody('<div class="alert alert-danger m-3">Access denied for Hospital Stock module.</div>');
}
private function actorLabel(): string
{
$user = service('auth')->user();
if (! $user) {
return 'System[' . date('Y-m-d H:i:s') . ']';
}
$name = trim(($user->first_name ?? '') . ' ' . ($user->last_name ?? ''));
if ($name === '' || $name === ' ') {
$name = (string) ($user->username ?? ($user->email ?? 'User'));
}
$userId = (int) ($user->id ?? 0);
return $name . '[' . $userId . '][' . date('d-m-Y H:i:s') . ']';
}
private function safeScalar(string $sql, array $bindings = [], $default = 0)
{
try {
$row = $this->db->query($sql, $bindings)->getRowArray();
if (! is_array($row) || empty($row)) {
return $default;
}
$val = reset($row);
return $val !== null ? $val : $default;
} catch (\Throwable $e) {
return $default;
}
}
private function safeRows(string $sql, array $bindings = []): array
{
try {
return $this->db->query($sql, $bindings)->getResultArray();
} catch (\Throwable $e) {
return [];
}
}
private function buildDashboardInsights(): array
{
$todayIndents = (int) $this->safeScalar(
"SELECT COUNT(*) AS c FROM invoice_stock_master WHERE DATE(indent_date) = CURDATE()",
[],
0
);
$monthIndents = (int) $this->safeScalar(
"SELECT COUNT(*) AS c FROM invoice_stock_master WHERE DATE_FORMAT(indent_date, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')",
[],
0
);
$pendingPurchase = (int) $this->safeScalar(
"SELECT COUNT(*) AS c FROM purchase_invoice_stock WHERE IFNULL(inv_status, 0) = 0",
[],
0
);
$lowStockCount = (int) $this->safeScalar(
"SELECT COUNT(*) AS c FROM (
SELECT p.id,
COALESCE(SUM(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit), 0) AS cur_units,
COALESCE(MAX(p.re_order_qty), 0) AS re_order_qty,
COALESCE(MAX(NULLIF(s.packing, 0)), 1) AS packing
FROM med_store_product_master p
LEFT JOIN purchase_invoice_item_stock s
ON p.id = s.item_code
AND s.remove_item = 0
AND s.item_return = 0
WHERE IFNULL(p.is_continue, 1) = 1
GROUP BY p.id
HAVING cur_units <= (re_order_qty * packing)
) x",
[],
0
);
$nearExpiryCount = (int) $this->safeScalar(
"SELECT COUNT(*) AS c
FROM purchase_invoice_item_stock s
WHERE s.remove_item = 0
AND s.item_return = 0
AND (s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) > 0
AND s.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 60 DAY)",
[],
0
);
$stockValue = (float) $this->safeScalar(
"SELECT ROUND(SUM(
(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit)
* IFNULL(s.purchase_unit_rate, s.purchase_price)
), 2) AS v
FROM purchase_invoice_item_stock s
WHERE s.remove_item = 0
AND s.item_return = 0
AND (s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) > 0",
[],
0
);
$lowStockItems = $this->safeRows(
"SELECT p.item_name,
COALESCE(MAX(p.re_order_qty), 0) AS re_order_qty,
COALESCE(MAX(NULLIF(s.packing, 0)), 1) AS packing,
ROUND(COALESCE(SUM(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit), 0), 2) AS cur_units
FROM med_store_product_master p
LEFT JOIN purchase_invoice_item_stock s
ON p.id = s.item_code
AND s.remove_item = 0
AND s.item_return = 0
WHERE IFNULL(p.is_continue, 1) = 1
GROUP BY p.id, p.item_name
HAVING cur_units <= (re_order_qty * packing)
ORDER BY cur_units ASC
LIMIT 8"
);
$nearExpiryItems = $this->safeRows(
"SELECT s.item_name,
s.batch_no,
DATE_FORMAT(s.expiry_date, '%d-%m-%Y') AS expiry_str,
DATEDIFF(s.expiry_date, CURDATE()) AS days_left,
ROUND((s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) / NULLIF(s.packing, 0), 2) AS cur_packs
FROM purchase_invoice_item_stock s
WHERE s.remove_item = 0
AND s.item_return = 0
AND (s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) > 0
AND s.expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 60 DAY)
ORDER BY s.expiry_date ASC
LIMIT 8"
);
return [
'stats' => [
'today_indents' => $todayIndents,
'month_indents' => $monthIndents,
'pending_purchase' => $pendingPurchase,
'low_stock_count' => $lowStockCount,
'near_expiry_count' => $nearExpiryCount,
'stock_value' => $stockValue,
],
'lists' => [
'low_stock_items' => $lowStockItems,
'near_expiry_items' => $nearExpiryItems,
],
];
}
// -------------------------------------------------------------------------
// Dashboard
// -------------------------------------------------------------------------
public function index()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$data = $this->buildDashboardInsights();
return view('storestock/dashboard', $data);
}
// -------------------------------------------------------------------------
// Indent List
// -------------------------------------------------------------------------
public function indent_list()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
return view('storestock/indent_list');
}
// -------------------------------------------------------------------------
// Indent DataTable (server-side)
// -------------------------------------------------------------------------
public function getIndentTable()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$columns = [
0 => 'indent_code',
1 => 'issued_name',
2 => 'indent_date_str',
3 => 'id',
];
$draw = (int) ($this->request->getPost('draw') ?? $this->request->getGet('draw') ?? 1);
$start = (int) ($this->request->getPost('start') ?? $this->request->getGet('start') ?? 0);
$length = (int) ($this->request->getPost('length') ?? $this->request->getGet('length') ?? 10);
$orderColIdx = (int) ($this->request->getPost('order[0][column]') ?? 0);
$orderDir = $this->request->getPost('order[0][dir]') === 'asc' ? 'asc' : 'desc';
$orderCol = $columns[$orderColIdx] ?? 'id';
// Column search values
$searchIndentCode = (string) ($this->request->getPost('columns[0][search][value]') ?? '');
$searchIssuedName = (string) ($this->request->getPost('columns[1][search][value]') ?? '');
$searchIndentDate = (string) ($this->request->getPost('columns[2][search][value]') ?? '');
// Total records
$totalData = (int) $this->db->query("SELECT COUNT(*) AS no_rec FROM invoice_stock_master")->getRow()->no_rec;
$totalFiltered = $totalData;
// Build parameterised WHERE
$whereSQL = ' WHERE 1=1';
$whereParams = [];
if ($searchIndentCode !== '') {
$esc = $this->db->escapeLikeString($searchIndentCode);
$whereSQL .= " AND indent_code LIKE '%{$esc}%'";
}
if ($searchIssuedName !== '') {
$esc = $this->db->escapeLikeString($searchIssuedName);
$whereSQL .= " AND issued_name LIKE '%{$esc}%'";
}
if ($searchIndentDate !== '') {
// Date column — use parameterised bind
$whereSQL .= ' AND DATE(indent_date) = ?';
$whereParams[] = $searchIndentDate;
}
if ($searchIndentCode !== '' || $searchIssuedName !== '' || $searchIndentDate !== '') {
$countSql = "SELECT COUNT(*) AS no_rec FROM invoice_stock_master{$whereSQL}";
$totalFiltered = (int) $this->db->query($countSql, $whereParams)->getRow()->no_rec;
}
$orderClause = " ORDER BY {$orderCol} {$orderDir}, id desc";
$limitClause = ' LIMIT ' . $start . ', ' . $length;
$selectSQL = "SELECT m.id, m.indent_code, m.issued_name, m.indent_date,
DATE_FORMAT(m.indent_date,'%d-%m-%Y') AS indent_date_str
FROM invoice_stock_master m{$whereSQL}{$orderClause}{$limitClause}";
$rdata = $this->db->query($selectSQL, $whereParams)->getResultArray();
$rows = [];
foreach ($rdata as $aRow) {
$rows[] = [
$aRow['indent_code'],
$aRow['issued_name'],
$aRow['indent_date_str'],
$aRow['id'],
];
}
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => $totalData,
'recordsFiltered' => $totalFiltered,
'data' => $rows,
]);
}
// -------------------------------------------------------------------------
// New Indent form
// -------------------------------------------------------------------------
public function new_indent()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$data['location_master'] = $this->db->query("SELECT * FROM location_master ORDER BY loc_name")->getResult();
$data['employee_master'] = $this->db->query("SELECT * FROM employee_master ORDER BY emp_name")->getResult();
return view('storestock/new_indent', $data);
}
// -------------------------------------------------------------------------
// Create Indent
// -------------------------------------------------------------------------
public function indent_create(int $location_type)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$date_indent = $this->request->getPost('date_indent');
$loc_id = (int) $this->request->getPost('loc_id');
// Convert dd/mm/yyyy → yyyy-mm-dd
$mysql_date = date('Y-m-d');
if ($date_indent) {
$parts = explode('/', trim($date_indent));
if (count($parts) === 3) {
$mysql_date = $parts[2] . '-' . $parts[1] . '-' . $parts[0];
} else {
$mysql_date = date('Y-m-d', strtotime($date_indent));
}
}
$issued_name = '';
if ($location_type == 1) {
$row = $this->db->query("SELECT * FROM location_master WHERE l_id = ?", [$loc_id])->getRow();
if ($row) {
$issued_name = $row->loc_name;
}
} elseif ($location_type == 2) {
$row = $this->db->query("SELECT * FROM employee_master WHERE emp_id = ?", [$loc_id])->getRow();
if ($row) {
$issued_name = $row->emp_name . ' [' . $row->emp_code . ']';
}
}
$this->db->table('invoice_stock_master')->insert([
'indent_date' => $mysql_date,
'location_type' => $location_type,
'location_id' => $loc_id,
'issued_name' => $issued_name,
]);
$insert_id = $this->db->insertID();
// Generate indent_code like S{ym}{padded_id}
$ym = date('ym');
$indent_code = 'S' . $ym . str_pad($insert_id, 5, '0', STR_PAD_LEFT);
$this->db->table('invoice_stock_master')->where('id', $insert_id)->update(['indent_code' => $indent_code]);
return redirect()->to(site_url('Storestock/Indent_show/' . $insert_id));
}
// -------------------------------------------------------------------------
// Show / Edit Indent
// -------------------------------------------------------------------------
public function indent_show(int $indent_id)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$data['invoice_stock_master'] = $this->db->query(
"SELECT * FROM invoice_stock_master WHERE id = ?",
[$indent_id]
)->getResult();
$data['inv_stock_item'] = $this->db->query(
"SELECT i.*, IFNULL(DATEDIFF(expiry, CURDATE()), 1000) AS no_day,
SUM(p.total_unit - p.total_sale_unit - p.total_return_unit - p.total_lost_unit) / p.packing AS cur_qty
FROM inv_stock_item i
JOIN purchase_invoice_item_stock p ON i.item_code = p.item_code AND p.remove_item = 0
WHERE i.indent_id = ?
GROUP BY i.id",
[$indent_id]
)->getResult();
$data['invoiceGtotal'] = $this->db->query(
"SELECT SUM(amount) AS Gtotal, SUM(tamount) AS tamt, SUM(CGST) AS TCGST, SUM(SGST) AS TSGST
FROM inv_stock_item WHERE indent_id = ?",
[$indent_id]
)->getResult();
$data['content'] = view('storestock/indent_item_list', $data);
return view('storestock/indent_edit', $data);
}
// -------------------------------------------------------------------------
// Indent Items List (AJAX partial refresh)
// -------------------------------------------------------------------------
public function indent_items_list(int $indent_id)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$data['inv_stock_item'] = $this->db->query(
"SELECT * FROM inv_stock_item WHERE indent_id = ? ORDER BY id",
[$indent_id]
)->getResult();
$data['invoiceGtotal'] = $this->db->query(
"SELECT SUM(amount) AS Gtotal, SUM(tamount) AS tamt, SUM(CGST) AS TCGST, SUM(SGST) AS TSGST
FROM inv_stock_item WHERE indent_id = ?",
[$indent_id]
)->getResult();
$html = view('storestock/indent_item_list', $data);
return $this->response->setBody($html);
}
// -------------------------------------------------------------------------
// Add / Update / Delete item from indent
// -------------------------------------------------------------------------
public function add_item(int $type)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$insert_id = 1;
$error_show = '';
$exist = 0;
$input_id = 0;
$inv_id = 0;
$user = service('auth')->user();
$user_id = $user ? (int) ($user->id ?? 0) : 0;
$user_name_info = $this->actorLabel();
$store_stock_id = (int) $this->request->getPost('l_ssno');
if ($type == 1) {
// ---- Add new item ----
$inv_id = (int) $this->request->getPost('inv_id');
$qty = (float) $this->request->getPost('qty');
$disc = (float) $this->request->getPost('disc');
$product_unit_rate = (float) $this->request->getPost('product_unit_rate');
$item = $this->db->query(
"SELECT p.item_name, t.batch_no, t.expiry_date,
(t.total_unit - t.total_sale_unit) AS S_qty,
t.mrp, t.selling_unit_rate, p.id AS master_product_id,
t.id AS purchase_item_id, p.formulation,
t.HSNCODE, t.CGST_per, t.SGST_per, t.packing
FROM med_store_product_master p
JOIN purchase_invoice_item_stock t ON p.id = t.item_code
WHERE t.id = ?",
[$store_stock_id]
)->getRow();
if (! $item) {
$insert_id = 0;
$error_show = 'Item not found in stock';
} else {
$item_exists = $this->db->query(
"SELECT * FROM inv_stock_item WHERE indent_id = ? AND store_stock_id = ?",
[$inv_id, $store_stock_id]
)->getResult();
$stock_qty = (float) $item->S_qty;
$amount_value = $qty * $product_unit_rate;
$disc_amount = $amount_value * $disc / 100;
$tamount_value = $amount_value - $disc_amount;
if ($qty > $stock_qty) {
$insert_id = 0;
$error_show = 'Stock Qty. is less than Required Qty : Current Qty :' . $stock_qty;
} elseif ($stock_qty == 0) {
$insert_id = 0;
$error_show = 'Stock is Empty';
}
if ($insert_id > 0) {
if (count($item_exists) > 0) {
$exist = count($item_exists);
$input_id = 0;
$error_show = 'Please Check Also Same Item List';
}
$this->db->table('inv_stock_item')->insert([
'indent_id' => $inv_id,
'item_code' => $item->master_product_id,
'item_Name' => $item->item_name,
'formulation' => $item->formulation,
'qty' => $qty,
'batch_no' => $item->batch_no,
'expiry' => $item->expiry_date,
'price' => $product_unit_rate,
'price2' => $product_unit_rate,
'mrp' => $item->mrp,
'disc_per' => $disc,
'disc_amount' => $disc_amount,
'amount' => $amount_value,
'tamount' => $tamount_value,
'CGST_per' => $item->CGST_per,
'SGST_per' => $item->SGST_per,
'HSNCODE' => $item->HSNCODE,
'store_stock_id' => $item->purchase_item_id,
'update_by_id' => $user_id,
'update_by_remark' => $user_name_info,
'packing' => $item->packing,
]);
$insert_id = $this->db->insertID();
}
}
} elseif ($type == 2) {
// ---- Update qty ----
$update_qty = (float) $this->request->getPost('u_qty');
$item_id = (int) $this->request->getPost('itemid');
$inv_item = $this->db->query(
"SELECT * FROM inv_stock_item WHERE id = ?",
[$item_id]
)->getRow();
if (! $inv_item) {
$insert_id = 0;
$error_show = 'Item not found';
} else {
$inv_id = (int) $inv_item->indent_id;
$store_stock_id = (int) $inv_item->store_stock_id;
$disc = (float) $inv_item->disc_per;
$old_qty = (float) $inv_item->qty;
$diff_qty = $update_qty - $old_qty;
if ($diff_qty > 0) {
$stock_item = $this->db->query(
"SELECT (t.total_unit - t.total_sale_unit) AS S_qty
FROM purchase_invoice_item_stock t WHERE t.id = ?",
[$store_stock_id]
)->getRow();
$stock_qty = $stock_item ? (float) $stock_item->S_qty : 0;
if ($diff_qty > $stock_qty) {
$insert_id = 0;
$error_show = 'Stock Qty. is less than Required Qty : Current Qty :' . $stock_qty;
}
}
if ($insert_id > 0) {
$item_rate = (float) $inv_item->price;
$amount_value = $update_qty * $item_rate;
$disc_amount = $amount_value * $disc / 100;
$tamount = $amount_value - $disc_amount;
$this->db->table('inv_stock_item')->where('id', $item_id)->update([
'qty' => $update_qty,
'disc_amount' => $disc_amount,
'amount' => $amount_value,
'tamount' => $tamount,
'store_stock_id'=> $store_stock_id,
'packing' => $inv_item->packing,
'indent_id' => $inv_id,
]);
$insert_id = $item_id;
}
}
} else {
// ---- Delete item ----
$item_id = (int) $this->request->getPost('itemid');
$inv_item = $this->db->query(
"SELECT * FROM inv_stock_item WHERE id = ?",
[$item_id]
)->getRowArray();
if ($inv_item) {
$inv_id = (int) $inv_item['indent_id'];
// Archive to delete table
$del_data = $inv_item;
$del_data['del_by'] = $user_name_info;
unset($del_data['id']);
$this->db->table('inv_stock_item_delete')->insert($del_data);
$this->db->table('inv_stock_item')->where('id', $item_id)->delete();
$insert_id = 1;
} else {
$insert_id = 0;
$error_show = 'Item not found';
}
}
$content = '';
if ($insert_id > 0) {
// Call stored procs
$this->db->query("CALL p_stock_update_purchase_id_store({$inv_id})");
$this->db->query("CALL p_update_med_GST_store({$inv_id})");
$data['inv_stock_item'] = $this->db->query(
"SELECT i.*, IFNULL(DATEDIFF(expiry, CURDATE()), 1000) AS no_day,
SUM(p.total_unit - p.total_sale_unit - p.total_return_unit - p.total_lost_unit) / p.packing AS cur_qty
FROM inv_stock_item i
JOIN purchase_invoice_item_stock p ON i.item_code = p.item_code AND p.remove_item = 0
WHERE i.indent_id = ?
GROUP BY i.id",
[$inv_id]
)->getResult();
$data['invoiceGtotal'] = $this->db->query(
"SELECT SUM(amount) AS Gtotal, SUM(tamount) AS tamt,
SUM(CGST) AS TCGST, SUM(SGST) AS TSGST, SUM(disc_amount) AS t_dis_amt
FROM inv_stock_item WHERE indent_id = ?",
[$inv_id]
)->getResult();
$content = view('storestock/indent_item_list', $data);
}
return $this->response->setJSON([
'exist' => $exist,
'insertid' => $insert_id,
'content' => $content,
'error' => $error_show,
'input_id' => $input_id,
'csrf_token_value' => csrf_hash(),
]);
}
// -------------------------------------------------------------------------
// Drug autocomplete
// -------------------------------------------------------------------------
public function get_drug()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$term = trim((string) $this->request->getGet('term'));
if ($term === '') {
return $this->response->setJSON([]);
}
$q = strtolower($term);
$esc = $this->db->escapeLikeString($q);
$sql = "SELECT p.item_name, p.formulation, t.batch_no,
t.expiry_date AS expiry_date_str, t.expiry_date,
(t.total_unit - t.total_sale_unit - t.total_lost_unit - t.total_return_unit) AS c_qty,
t.id, t.mrp, t.selling_unit_rate, p.id AS item_code, t.packing
FROM med_store_product_master p
JOIN purchase_invoice_item_stock t ON p.id = t.item_code
AND (t.total_unit - t.total_sale_unit - t.total_lost_unit - t.total_return_unit) > 0
AND (expiry_date > DATE_ADD(SYSDATE(), INTERVAL 1 DAY) OR p.exp_date_applicable = 0)
AND t.remove_item = 0 AND t.item_return = 0
WHERE p.item_name LIKE '{$esc}%'
ORDER BY p.item_name, t.id
LIMIT 100";
$rows = $this->db->query($sql)->getResultArray();
$row_set = [];
foreach ($rows as $row) {
$row_set[] = [
'label' => htmlentities($row['item_name'] . ' ' . $row['formulation'])
. ' |B:' . htmlentities($row['batch_no'])
. ' |Pak:' . htmlentities($row['packing'])
. ' |Rs.' . htmlentities($row['mrp'])
. ' |Qty:' . htmlentities($row['c_qty']),
'value' => htmlentities($row['item_name']),
'l_item_code' => htmlentities($row['item_code']),
'l_ss_no' => htmlentities($row['id']),
'l_Batch' => htmlentities($row['batch_no']),
'l_Expiry' => htmlentities($row['expiry_date_str']),
'l_mrp' => htmlentities($row['mrp']),
'l_unit_rate' => htmlentities($row['selling_unit_rate']),
'l_c_qty' => htmlentities($row['c_qty']),
'l_packing' => htmlentities($row['packing']),
];
}
return $this->response->setJSON($row_set);
}
// -------------------------------------------------------------------------
// Final Invoice
// -------------------------------------------------------------------------
public function final_invoice(int $inv_id)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
// Call stored procedure to recalculate GST
$this->db->query("CALL p_update_med_GST_store({$inv_id})");
$data['invoice_stock_master'] = $this->db->query(
"SELECT *, (discount_amount + item_discount_amount) AS inv_disc_total,
(CGST_Tamount + SGST_Tamount) AS TGST
FROM invoice_stock_master WHERE id = ?",
[$inv_id]
)->getResult();
$data['inv_items'] = $this->db->query(
"SELECT * FROM inv_stock_item WHERE indent_id = ?",
[$inv_id]
)->getResult();
$data['invoiceGtotal'] = $this->db->query(
"SELECT SUM(amount) AS Gtotal, SUM(tamount) AS tamt,
SUM(CGST) AS TCGST, SUM(SGST) AS TSGST, SUM(disc_amount) AS t_dis_amt
FROM inv_stock_item WHERE indent_id = ?",
[$inv_id]
)->getResult();
return view('storestock/final_invoice', $data);
}
// -------------------------------------------------------------------------
// Print single indent (mPDF)
// -------------------------------------------------------------------------
public function print_single_indent(int $inv_id)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$this->db->query("CALL p_update_med_GST_store({$inv_id})");
$data['inv_items'] = $this->db->query(
"SELECT i.indent_id, i.id, i.item_Name, i.formulation,
i.batch_no, DATE_FORMAT(i.expiry, '%m-%y') AS expiry,
i.price, i.qty, (i.amount) AS amount,
i.HSNCODE, m.indent_code, m.id AS m_id,
DATE_FORMAT(m.indent_date, '%d-%m-%Y') AS str_inv_date,
(i.disc_amount) AS d_amt,
(i.CGST + i.SGST) AS gst,
(i.CGST_per + i.SGST_per) AS gst_per, i.sale_return
FROM inv_stock_item i
JOIN invoice_stock_master m ON i.indent_id = m.id
WHERE m.id = ?
ORDER BY i.sale_return, i.id",
[$inv_id]
)->getResult();
$data['invoice_stock_master'] = $this->db->query(
"SELECT *, DATE_FORMAT(indent_date,'%d-%m-%Y') AS str_inv_date,
(discount_amount + item_discount_amount) AS inv_disc_total,
(CGST_Tamount + SGST_Tamount) AS TGST
FROM invoice_stock_master WHERE id = ?",
[$inv_id]
)->getResult();
$html = view('storestock/store_print', $data);
$mpdf = new Mpdf([
'margin_top' => 40,
'margin_bottom' => 12,
'margin_left' => 10,
'margin_right' => 5,
]);
$mpdf->showWatermarkText = false;
$mpdf->WriteHTML($html);
$filename = 'Indent-' . $inv_id . '-' . date('Ymdhis') . '.pdf';
$mpdf->Output($filename, 'I');
}
// -------------------------------------------------------------------------
// Store Stock Search
// -------------------------------------------------------------------------
public function store_stock()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$data['supplier_data'] = $this->db->query("SELECT * FROM stock_supplier ORDER BY name_supplier")->getResult();
$data['med_formulation'] = $this->db->query("SELECT * FROM med_formulation ORDER BY formulation_length")->getResult();
return view('storestock/store_stock_search', $data);
}
// -------------------------------------------------------------------------
// Store Stock Result (POST)
// -------------------------------------------------------------------------
public function store_stock_result()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$supplier_id = (int) $this->request->getPost('input_supplier');
$item_name = (string) $this->request->getPost('txtsearch');
$chk_reorder = (string) $this->request->getPost('chk_reorder');
$having = '';
if ($chk_reorder === 'on') {
$having = ' HAVING SUM(s.total_unit - s.total_lost_unit - s.total_sale_unit - s.total_return_unit) <= p.re_order_qty';
}
$where = ' s.remove_item = 0';
$whereParams = [];
if ($supplier_id > 0) {
$where .= ' AND m.sid = ?';
$whereParams[] = $supplier_id;
}
if ($item_name !== '') {
$esc = $this->db->escapeLikeString($item_name);
$where .= " AND p.item_name LIKE '%{$esc}%'";
}
$sql = "SELECT p.id, p.item_name,
IFNULL(s.item_code, 0) AS item_found,
IFNULL(s.packing, p.packing) AS packing,
SUM(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) AS C_Unit_Stock_Qty,
CONCAT(
TRUNCATE(SUM(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit) / s.packing, 0),
':', MOD(SUM(s.total_unit - s.total_sale_unit - s.total_lost_unit - s.total_return_unit), s.packing)
) AS C_Pak_Qty,
SUM(s.total_unit) AS P_Unit_Qty,
SUM(s.total_unit) / s.packing AS Pak_qty,
SUM(s.total_sale_unit) AS sale_unit,
SUM(s.total_sale_unit) / s.packing AS C_Pak_Sale_Qty,
p.re_order_qty,
s.total_lost_unit
FROM med_store_product_master p
JOIN purchase_invoice_item_stock s ON p.id = s.item_code AND s.remove_item = 0 AND s.item_return = 0
JOIN purchase_invoice_stock m ON s.purchase_id = m.id AND p.is_continue = 1
WHERE {$where}
GROUP BY p.id, s.packing{$having}
ORDER BY p.item_name";
$data['stock_list'] = $this->db->query($sql, $whereParams)->getResult();
$html = view('storestock/stock_search_result', $data);
return $this->response->setBody($html);
}
// -------------------------------------------------------------------------
// Get product stock detail (batch-wise)
// -------------------------------------------------------------------------
public function get_product_stock(int $product_id)
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
$supplier_id = (int) $this->request->getPost('input_supplier');
$date_range = (string) $this->request->getPost('date_range');
$product_pak = (float) $this->request->getPost('product_pak');
$where = '1 = 1';
$whereParams = [];
if ($date_range !== '' && $date_range !== '0') {
$rangeArray = explode('S', $date_range);
if (count($rangeArray) === 2) {
$where .= ' AND p.date_of_invoice BETWEEN ? AND ?';
$whereParams[] = $rangeArray[0];
$whereParams[] = $rangeArray[1];
}
}
if ($supplier_id > 0) {
$where .= ' AND m.sid = ?';
$whereParams[] = $supplier_id;
}
$where .= ' AND i.item_code = ?';
$whereParams[] = $product_id;
if ($product_pak > 0) {
$where .= ' AND i.packing = ?';
$whereParams[] = $product_pak;
}
$sql = "SELECT i.id, s.name_supplier, s.short_name, i.Item_name,
p.Invoice_no, p.date_of_invoice, i.mrp, i.packing,
i.batch_no, i.expiry_date, i.purchase_price, i.purchase_unit_rate,
i.qty, i.qty_free, i.tqty, i.total_unit, i.total_sale_unit,
i.total_return_unit, i.total_lost_unit,
(i.total_unit - i.total_sale_unit - i.total_lost_unit - i.total_return_unit) AS cur_unit
FROM purchase_invoice_stock p
JOIN purchase_invoice_item_stock i ON p.id = i.purchase_id AND i.item_return = 0
JOIN stock_supplier s ON p.sid = s.sid
WHERE {$where} AND i.remove_item = 0
ORDER BY p.id";
$data['product_purchase_detail'] = $this->db->query($sql, $whereParams)->getResult();
$html = view('storestock/stock_item_history', $data);
return $this->response->setBody($html);
}
// -------------------------------------------------------------------------
// Day Report (stub)
// -------------------------------------------------------------------------
public function report_2()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
return view('storestock/day_report');
}
// -------------------------------------------------------------------------
// Main Store Dashboard
// -------------------------------------------------------------------------
public function main_store()
{
if ($err = $this->ensureStoreAccess()) {
return $err;
}
return view('storestock/main_store_dashboard');
}
}

View File

@ -0,0 +1,90 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCashSubmissionPeriodFields extends Migration
{
private function tableExists(string $table): bool
{
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getNumRows() > 0;
}
private function fieldExists(string $table, string $field): bool
{
return $this->db->query('SHOW COLUMNS FROM `' . $table . '` LIKE ' . $this->db->escape($field))->getNumRows() > 0;
}
public function up()
{
$table = 'finance_scroll_submissions';
if (! $this->tableExists($table)) {
return;
}
if (! $this->fieldExists($table, 'start_datetime')) {
$this->forge->addColumn($table, [
'start_datetime' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'scroll_date',
],
]);
}
if (! $this->fieldExists($table, 'end_datetime')) {
$this->forge->addColumn($table, [
'end_datetime' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'start_datetime',
],
]);
}
if (! $this->fieldExists($table, 'collected_by')) {
$this->forge->addColumn($table, [
'collected_by' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'department',
],
]);
}
if (! $this->fieldExists($table, 'payment_count')) {
$this->forge->addColumn($table, [
'payment_count' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
'null' => false,
'after' => 'submitted_amount',
],
]);
}
}
public function down()
{
$table = 'finance_scroll_submissions';
if (! $this->tableExists($table)) {
return;
}
if ($this->fieldExists($table, 'payment_count')) {
$this->forge->dropColumn($table, 'payment_count');
}
if ($this->fieldExists($table, 'collected_by')) {
$this->forge->dropColumn($table, 'collected_by');
}
if ($this->fieldExists($table, 'end_datetime')) {
$this->forge->dropColumn($table, 'end_datetime');
}
if ($this->fieldExists($table, 'start_datetime')) {
$this->forge->dropColumn($table, 'start_datetime');
}
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateCashSubmissionItemsTable extends Migration
{
private function tableExists(string $table): bool
{
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getNumRows() > 0;
}
private function fieldExists(string $table, string $field): bool
{
return $this->db->query('SHOW COLUMNS FROM `' . $table . '` LIKE ' . $this->db->escape($field))->getNumRows() > 0;
}
public function up()
{
if ($this->tableExists('finance_scroll_submission_items')) {
return;
}
$this->forge->addField([
'id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'auto_increment' => true,
],
'scroll_submission_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'payment_history_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => false,
],
'payment_date' => [
'type' => 'DATETIME',
'null' => true,
],
'amount' => [
'type' => 'DECIMAL',
'constraint' => '12,2',
'default' => 0,
],
'payment_mode' => [
'type' => 'TINYINT',
'constraint' => 2,
'default' => 0,
],
'payof_type' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'payof_id' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'payof_code' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
'update_by_id' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'update_by' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
],
'snapshot_json' => [
'type' => 'LONGTEXT',
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('scroll_submission_id');
$this->forge->addKey('payment_history_id');
$this->forge->createTable('finance_scroll_submission_items', true);
}
public function down()
{
if ($this->tableExists('finance_scroll_submission_items')) {
$this->forge->dropTable('finance_scroll_submission_items', true);
}
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddCashSubmissionFieldsToPaymentHistory extends Migration
{
private function tableExists(string $table): bool
{
return $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getNumRows() > 0;
}
private function fieldExists(string $table, string $field): bool
{
return $this->db->query('SHOW COLUMNS FROM `' . $table . '` LIKE ' . $this->db->escape($field))->getNumRows() > 0;
}
public function up()
{
if (! $this->tableExists('payment_history')) {
return;
}
if (! $this->fieldExists('payment_history', 'cash_submission_status')) {
$this->forge->addColumn('payment_history', [
'cash_submission_status' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => false,
'default' => 'open',
'after' => 'payment_mode',
],
]);
}
if (! $this->fieldExists('payment_history', 'cash_submission_scroll_id')) {
$this->forge->addColumn('payment_history', [
'cash_submission_scroll_id' => [
'type' => 'INT',
'constraint' => 11,
'null' => true,
'after' => 'cash_submission_status',
],
]);
}
if (! $this->fieldExists('payment_history', 'cash_submission_updated_at')) {
$this->forge->addColumn('payment_history', [
'cash_submission_updated_at' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'cash_submission_scroll_id',
],
]);
}
$this->db->query('UPDATE payment_history SET cash_submission_status = "open" WHERE cash_submission_status IS NULL OR cash_submission_status = ""');
}
public function down()
{
if (! $this->tableExists('payment_history')) {
return;
}
if ($this->fieldExists('payment_history', 'cash_submission_updated_at')) {
$this->forge->dropColumn('payment_history', 'cash_submission_updated_at');
}
if ($this->fieldExists('payment_history', 'cash_submission_scroll_id')) {
$this->forge->dropColumn('payment_history', 'cash_submission_scroll_id');
}
if ($this->fieldExists('payment_history', 'cash_submission_status')) {
$this->forge->dropColumn('payment_history', 'cash_submission_status');
}
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBankAuditTables extends Migration
{
public function up(): void
{
// Table 1: Bank statement entries for direct transaction reconciliation
// (UPI direct, NEFT, RTGS – each line visible individually in the bank statement)
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'entry_date' => ['type' => 'DATE', 'null' => false],
'reference_no' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
'narration' => ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true],
'amount' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'],
'transaction_type' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => false, 'default' => 'credit'],
'matched_payment_id' => ['type' => 'BIGINT', 'unsigned' => true, 'null' => true],
'reconciliation_status'=> ['type' => 'VARCHAR', 'constraint' => 20, 'null' => false, 'default' => 'unmatched'],
'matched_by' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
'matched_at' => ['type' => 'DATETIME', 'null' => true],
'remarks' => ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true],
'created_by' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('entry_date');
$this->forge->addKey('reconciliation_status');
$this->forge->createTable('finance_bank_statement_entries', true);
// Table 2: POS terminal daily settlement records
// (POS transactions don't appear individually in bank; the end-of-day settlement total does)
$this->forge->addField([
'id' => ['type' => 'INT', 'unsigned' => true, 'auto_increment' => true],
'settlement_date' => ['type' => 'DATE', 'null' => false],
'terminal_id' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => false],
'terminal_name' => ['type' => 'VARCHAR', 'constraint' => 200, 'null' => true],
'settlement_amount'=> ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'],
'system_total' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'],
'variance' => ['type' => 'DECIMAL', 'constraint' => '12,2', 'null' => false, 'default' => '0.00'],
'payment_count' => ['type' => 'INT', 'null' => false, 'default' => 0],
'status' => ['type' => 'VARCHAR', 'constraint' => 20, 'null' => false, 'default' => 'pending'],
'reconciled_by' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
'reconciled_at' => ['type' => 'DATETIME', 'null' => true],
'remarks' => ['type' => 'VARCHAR', 'constraint' => 500, 'null' => true],
'created_by' => ['type' => 'VARCHAR', 'constraint' => 100, 'null' => true],
'created_at' => ['type' => 'DATETIME', 'null' => true],
'updated_at' => ['type' => 'DATETIME', 'null' => true],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('settlement_date');
$this->forge->addKey(['settlement_date', 'terminal_id']);
$this->forge->createTable('finance_bank_pos_settlements', true);
// Add reconciliation tracking fields to payment_history
$this->forge->addColumn('payment_history', [
'bank_reconcile_status' => [
'type' => 'VARCHAR',
'constraint' => 20,
'null' => true,
'default' => null,
'after' => 'cash_submission_updated_at',
],
'bank_statement_entry_id' => [
'type' => 'INT',
'unsigned' => true,
'null' => true,
'default' => null,
'after' => 'bank_reconcile_status',
],
]);
}
public function down(): void
{
$this->forge->dropTable('finance_bank_statement_entries', true);
$this->forge->dropTable('finance_bank_pos_settlements', true);
$this->forge->dropColumn('payment_history', 'bank_reconcile_status');
$this->forge->dropColumn('payment_history', 'bank_statement_entry_id');
}
}

View File

@ -0,0 +1,79 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBankReconciliationAuditTable extends Migration
{
public function up(): void
{
$this->forge->addField([
'id' => [
'type' => 'BIGINT',
'unsigned' => true,
'auto_increment' => true,
],
'payment_history_id' => [
'type' => 'BIGINT',
'unsigned' => true,
'null' => false,
],
'action_type' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => false,
'default' => 'matched',
],
'old_status' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
],
'new_status' => [
'type' => 'VARCHAR',
'constraint' => 30,
'null' => true,
],
'batch_ref' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
],
'remarks' => [
'type' => 'VARCHAR',
'constraint' => 500,
'null' => true,
],
'action_by' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
],
'action_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addKey('payment_history_id');
$this->forge->addKey('action_type');
$this->forge->addKey('batch_ref');
$this->forge->createTable('finance_bank_reconciliation_audit', true);
$this->forge->addColumn('payment_history', [
'bank_reconcile_batch_ref' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
'after' => 'bank_statement_entry_id',
],
]);
}
public function down(): void
{
$this->forge->dropTable('finance_bank_reconciliation_audit', true);
$this->forge->dropColumn('payment_history', 'bank_reconcile_batch_ref');
}
}

View File

@ -0,0 +1,104 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateBankSettlementEntriesTable extends Migration
{
public function up()
{
if (! $this->hasTable('finance_bank_settlement_entries')) {
$this->forge->addField([
'id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'auto_increment' => true,
],
'settlement_ref' => [
'type' => 'VARCHAR',
'constraint' => 100,
],
'settlement_date' => [
'type' => 'DATE',
],
'payment_count' => [
'type' => 'INT',
'constraint' => 11,
'default' => 0,
],
'total_amount' => [
'type' => 'DECIMAL',
'constraint' => '12,2',
'default' => '0.00',
],
'remarks' => [
'type' => 'TEXT',
'null' => true,
],
'created_by' => [
'type' => 'VARCHAR',
'constraint' => 100,
'null' => true,
],
'created_at' => [
'type' => 'DATETIME',
'null' => true,
],
]);
$this->forge->addKey('id', true);
$this->forge->addUniqueKey('settlement_ref');
$this->forge->addKey('settlement_date');
$this->forge->createTable('finance_bank_settlement_entries', true);
}
if ($this->hasTable('payment_history') && ! $this->hasField('payment_history', 'bank_settlement_entry_id')) {
$this->forge->addColumn('payment_history', [
'bank_settlement_entry_id' => [
'type' => 'BIGINT',
'constraint' => 20,
'unsigned' => true,
'null' => true,
'after' => 'bank_reconcile_batch_ref',
],
]);
}
}
public function down()
{
if ($this->hasTable('payment_history') && $this->hasField('payment_history', 'bank_settlement_entry_id')) {
$this->forge->dropColumn('payment_history', 'bank_settlement_entry_id');
}
if ($this->hasTable('finance_bank_settlement_entries')) {
$this->forge->dropTable('finance_bank_settlement_entries', true);
}
}
private function hasTable(string $table): bool
{
try {
$result = $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getResultArray();
return ! empty($result);
} catch (\Throwable $e) {
return false;
}
}
private function hasField(string $table, string $field): bool
{
if (! $this->hasTable($table)) {
return false;
}
try {
$sql = 'SHOW COLUMNS FROM `' . str_replace('`', '``', $table) . '` LIKE ' . $this->db->escape($field);
$result = $this->db->query($sql)->getResultArray();
return ! empty($result);
} catch (\Throwable $e) {
return false;
}
}
}

View File

@ -0,0 +1,92 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddOpdPayoutTrackingFields extends Migration
{
private function hasTable(string $table): bool
{
$row = $this->db->query('SHOW TABLES LIKE ' . $this->db->escape($table))->getRowArray();
return ! empty($row);
}
private function getTableFields(string $table): array
{
if (! $this->hasTable($table)) {
return [];
}
try {
$rows = $this->db->query('SHOW COLUMNS FROM `' . $table . '`')->getResultArray();
return array_values(array_filter(array_map(static function (array $row): string {
return (string) ($row['Field'] ?? '');
}, $rows), static fn(string $field): bool => $field !== ''));
} catch (\Throwable $e) {
return [];
}
}
public function up()
{
if (! $this->hasTable('opd_master')) {
return;
}
$fields = $this->getTableFields('opd_master');
if (! in_array('payout_draft_id', $fields, true)) {
$this->forge->addColumn('opd_master', [
'payout_draft_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'doc_id',
],
]);
}
if (! in_array('payout_calculated_at', $fields, true)) {
$this->forge->addColumn('opd_master', [
'payout_calculated_at' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'payout_draft_id',
],
]);
}
if (! in_array('payout_calculated_by', $fields, true)) {
$this->forge->addColumn('opd_master', [
'payout_calculated_by' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'payout_calculated_at',
],
]);
}
}
public function down()
{
if (! $this->hasTable('opd_master')) {
return;
}
$fields = $this->getTableFields('opd_master');
if (in_array('payout_calculated_by', $fields, true)) {
$this->forge->dropColumn('opd_master', 'payout_calculated_by');
}
if (in_array('payout_calculated_at', $fields, true)) {
$this->forge->dropColumn('opd_master', 'payout_calculated_at');
}
if (in_array('payout_draft_id', $fields, true)) {
$this->forge->dropColumn('opd_master', 'payout_draft_id');
}
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class FinanceBankPosSettlementModel extends Model
{
protected $table = 'finance_bank_pos_settlements';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $allowedFields = [
'settlement_date',
'terminal_id',
'terminal_name',
'settlement_amount',
'system_total',
'variance',
'payment_count',
'status',
'reconciled_by',
'reconciled_at',
'remarks',
'created_by',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
}

View File

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class FinanceBankReconciliationAuditModel extends Model
{
protected $table = 'finance_bank_reconciliation_audit';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $allowedFields = [
'payment_history_id',
'action_type',
'old_status',
'new_status',
'batch_ref',
'remarks',
'action_by',
'action_at',
];
protected $useTimestamps = false;
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class FinanceBankStatementEntryModel extends Model
{
protected $table = 'finance_bank_statement_entries';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $allowedFields = [
'entry_date',
'reference_no',
'narration',
'amount',
'transaction_type',
'matched_payment_id',
'reconciliation_status',
'matched_by',
'matched_at',
'remarks',
'created_by',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
}

View File

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class FinanceScrollSubmissionItemModel extends Model
{
protected $table = 'finance_scroll_submission_items';
protected $primaryKey = 'id';
protected $returnType = 'array';
protected $useAutoIncrement = true;
protected $allowedFields = [
'scroll_submission_id',
'payment_history_id',
'payment_date',
'amount',
'payment_mode',
'payof_type',
'payof_id',
'payof_code',
'update_by_id',
'update_by',
'snapshot_json',
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = '';
}

View File

@ -12,7 +12,11 @@ class FinanceScrollSubmissionModel extends Model
protected $useAutoIncrement = true;
protected $allowedFields = [
'scroll_date',
'start_datetime',
'end_datetime',
'department',
'collected_by',
'payment_count',
'total_receipts',
'submitted_amount',
'variance_amount',

View File

@ -105,7 +105,7 @@
</a>
</div>
<div class="col-6 col-md-2 col-lg-2">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('setting/admin/hospital-stock') ?>','maindiv','Hospital Stock Management');">
<a class="admin-tile" href="javascript:load_form_div('<?= base_url('Storestock') ?>','maindiv','Hospital Stock');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>

View File

@ -10,105 +10,123 @@
<section class="section profile">
<div class="card">
<div class="card-header">
<h3 class="card-title mb-0">OPD History</h3>
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h3 class="card-title mb-0">Old Prescription With Scanned Records</h3>
<div class="small text-muted mt-1">Patient: <?= esc($patient->p_fname ?? '') ?></div>
</div>
<span class="badge bg-secondary"><?= count($opdGroups ?? []) ?> OPD Record(s)</span>
</div>
<div class="card-body">
<p class="mb-3">Patient: <strong><?= esc($patient->p_fname ?? '') ?></strong></p>
<?php if (empty($opdGroups)) { ?>
<div class="alert alert-info mb-0">No OPD scans found.</div>
<div class="alert alert-info mb-0">No OPD history found.</div>
<?php } else { ?>
<?php foreach ($opdGroups as $group) { ?>
<div class="border rounded p-3 mb-3">
<div class="d-flex flex-wrap justify-content-between">
<article class="border rounded p-3 mb-4 bg-white shadow-sm">
<div class="d-flex justify-content-between align-items-start flex-wrap gap-2 mb-2">
<div>
<strong><?= esc($group['opd_code']) ?></strong>
<span class="text-muted ms-2">Dr. <?= esc($group['doc_name']) ?></span>
</div>
<div class="text-muted">
<?= esc($group['opd_date']) ?> <?= esc($group['queue_no']) ?>
</div>
</div>
<div class="mt-2">
<?php if ((int) ($group['rx_session_id'] ?? 0) > 0) { ?>
<a class="btn btn-primary btn-sm" target="_blank"
href="<?= base_url('Opd_prescription/opd_prescription_print/' . (int) $group['opd_id'] . '/' . (int) $group['rx_session_id']) ?>">
Prescription Print
</a>
<?php } ?>
<?php if (!empty($group['bp']) || !empty($group['diastolic']) || !empty($group['pulse']) || !empty($group['temp']) || !empty($group['spo2'])) { ?>
<div class="small text-muted mt-2">
<?php if (!empty($group['bp'])) { ?><span class="me-3"><strong>BP:</strong> <?= esc($group['bp']) ?></span><?php } ?>
<?php if (!empty($group['diastolic'])) { ?><span class="me-3"><strong>Diastolic:</strong> <?= esc($group['diastolic']) ?></span><?php } ?>
<?php if (!empty($group['pulse'])) { ?><span class="me-3"><strong>Pulse:</strong> <?= esc($group['pulse']) ?></span><?php } ?>
<?php if (!empty($group['temp'])) { ?><span class="me-3"><strong>Temp:</strong> <?= esc($group['temp']) ?></span><?php } ?>
<?php if (!empty($group['spo2'])) { ?><span class="me-3"><strong>SPO2:</strong> <?= esc($group['spo2']) ?></span><?php } ?>
<div class="fw-semibold">Dr. <?= esc($group['doc_name'] ?? '') ?></div>
<div class="small text-muted">
<strong>OPD ID:</strong> <?= esc($group['opd_code'] ?? '') ?>
<span class="ms-2"><strong>Date:</strong> <?= esc($group['opd_date'] ?? '') ?></span>
<?php if (!empty($group['queue_no'])) { ?><span class="ms-2"><strong><?= esc($group['queue_no']) ?></strong></span><?php } ?>
</div>
<?php } ?>
<?php if (!empty($group['complaints']) || !empty($group['diagnosis']) || !empty($group['investigation']) || !empty($group['advice'])) { ?>
<div class="row g-2 mt-2">
<?php if (!empty($group['complaints'])) { ?>
<div class="col-12 col-lg-6">
<div class="border rounded p-2 bg-light small">
<strong>Complaints:</strong> <?= esc($group['complaints']) ?>
</div>
</div>
<?php } ?>
<?php if (!empty($group['diagnosis'])) { ?>
<div class="col-12 col-lg-6">
<div class="border rounded p-2 bg-light small">
<strong>Diagnosis:</strong> <?= esc($group['diagnosis']) ?>
</div>
</div>
<?php } ?>
<?php if (!empty($group['investigation'])) { ?>
<div class="col-12 col-lg-6">
<div class="border rounded p-2 bg-light small">
<strong>Investigation:</strong> <?= esc($group['investigation']) ?>
</div>
</div>
<?php } ?>
<?php if (!empty($group['advice'])) { ?>
<div class="col-12 col-lg-6">
<div class="border rounded p-2 bg-light small">
<strong>Advice:</strong> <?= esc($group['advice']) ?>
</div>
</div>
<?php } ?>
</div>
<?php } ?>
</div>
<?php if (empty($group['files'])) { ?>
<div class="text-muted mt-2">No scanned files for this OPD.</div>
<?php } else { ?>
<div class="small fw-semibold mt-3 mb-1">Scanned Files</div>
<div class="row g-2 mt-2">
<?php foreach ($group['files'] as $index => $file) { ?>
<div class="col-6 col-md-3">
<?php if ($file['isPdf']) { ?>
<a class="btn btn-outline-secondary btn-sm w-100" href="<?= esc($file['path']) ?>" target="_blank">Open PDF</a>
<?php } else { ?>
<img src="<?= esc($file['path']) ?>" class="img-fluid rounded border opd-thumb"
data-bs-toggle="modal" data-bs-target="#opdScanModal"
data-src="<?= esc($file['path']) ?>" alt="OPD Scan">
<?php } ?>
</div>
</div>
<div class="d-flex gap-2 flex-wrap">
<?php if ((int) ($group['rx_session_id'] ?? 0) > 0) { ?>
<a class="btn btn-outline-primary btn-sm" target="_blank"
href="<?= base_url('Opd_prescription/opd_prescription_print/' . (int) $group['opd_id'] . '/' . (int) $group['rx_session_id']) ?>">
Prescription Print
</a>
<?php } ?>
</div>
</div>
<?php if (!empty($group['bp']) || !empty($group['diastolic']) || !empty($group['pulse']) || !empty($group['temp']) || !empty($group['spo2'])) { ?>
<div class="small text-dark mb-2">
<?php if (!empty($group['bp'])) { ?><span class="me-3"><strong>BP:</strong> <?= esc($group['bp']) ?></span><?php } ?>
<?php if (!empty($group['diastolic'])) { ?><span class="me-3"><strong>Diastolic:</strong> <?= esc($group['diastolic']) ?></span><?php } ?>
<?php if (!empty($group['pulse'])) { ?><span class="me-3"><strong>Pulse:</strong> <?= esc($group['pulse']) ?></span><?php } ?>
<?php if (!empty($group['temp'])) { ?><span class="me-3"><strong>Temp:</strong> <?= esc($group['temp']) ?></span><?php } ?>
<?php if (!empty($group['spo2'])) { ?><span class="me-3"><strong>SPO2:</strong> <?= esc($group['spo2']) ?></span><?php } ?>
</div>
<?php } ?>
</div>
<?php if (!empty($group['complaints']) || !empty($group['diagnosis']) || !empty($group['investigation']) || !empty($group['advice']) || !empty($group['next_visit']) || !empty($group['refer_to'])) { ?>
<div class="small text-dark mb-3">
<?php if (!empty($group['complaints'])) { ?><div><strong>Complaint:</strong> <?= esc($group['complaints']) ?></div><?php } ?>
<?php if (!empty($group['diagnosis'])) { ?><div><strong>Diagnosis:</strong> <?= esc($group['diagnosis']) ?></div><?php } ?>
<?php if (!empty($group['investigation'])) { ?><div><strong>Investigation Advised:</strong> <?= esc($group['investigation']) ?></div><?php } ?>
<?php if (!empty($group['advice'])) { ?><div><strong>Advice:</strong> <?= esc($group['advice']) ?></div><?php } ?>
<?php if (!empty($group['next_visit'])) { ?><div><strong>Next Visit:</strong> <?= esc($group['next_visit']) ?></div><?php } ?>
<?php if (!empty($group['refer_to'])) { ?><div><strong>Refer To:</strong> <?= esc($group['refer_to']) ?></div><?php } ?>
</div>
<?php } ?>
<?php if (!empty($group['medicines'])) { ?>
<div class="table-responsive mb-3">
<table class="table table-sm table-bordered align-middle mb-0">
<thead class="table-light">
<tr>
<th>Prescribed</th>
<th>Dose</th>
<th>Timing - Freq. - Route - Duration</th>
<th>Qty</th>
</tr>
</thead>
<tbody>
<?php foreach ($group['medicines'] as $medicine) { ?>
<tr>
<td>
<?= esc(trim(($medicine['med_type'] ?? '') . ' ' . ($medicine['med_name'] ?? ''))) ?>
<?php if (!empty($medicine['remark'])) { ?>
<div class="small text-muted mt-1"><?= esc($medicine['remark']) ?></div>
<?php } ?>
</td>
<td><?= esc($medicine['dose'] ?? '') ?></td>
<td>
<?= esc(implode(' - ', array_values(array_filter([
$medicine['timing'] ?? '',
$medicine['frequency'] ?? '',
$medicine['where'] ?? '',
$medicine['days'] ?? '',
], static fn($value): bool => trim((string) $value) !== '')))) ?>
</td>
<td><?= esc($medicine['qty'] ?? '') ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
<?php } ?>
<?php if (empty($group['files'])) { ?>
<div class="text-muted">No scanned files for this OPD.</div>
<?php } else { ?>
<div class="fw-semibold mb-2">Scanned Files</div>
<?php foreach ($group['files'] as $file) { ?>
<div class="mb-3">
<?php if ($file['isPdf']) { ?>
<div class="d-flex gap-2 flex-wrap mb-2">
<a class="btn btn-outline-secondary btn-sm" href="<?= esc($file['path']) ?>" target="_blank">Open PDF</a>
</div>
<embed src="<?= esc($file['path']) ?>" type="application/pdf" width="100%" height="900px" class="border rounded">
<?php } else { ?>
<img src="<?= esc($file['path']) ?>" class="img-fluid rounded border opd-thumb"
data-bs-toggle="modal" data-bs-target="#opdScanModal"
data-src="<?= esc($file['path']) ?>" alt="OPD Scan">
<?php } ?>
</div>
<?php } ?>
<?php } ?>
</article>
<?php } ?>
<?php } ?>
</div>
</div>
<div class="modal fade" id="opdScanModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">OPD Scan</h5>

View File

@ -1,7 +1,7 @@
<section class="content finance-bank-deposit">
<div class="mb-3">
<h2 class="mb-1">Bank Deposit Register</h2>
<p class="text-muted mb-0">Track cash to bank deposits and reconcile with submitted scrolls.</p>
<h2 class="mb-1">Bank Transaction Audit And Statement Update</h2>
<p class="text-muted mb-0">Track deposits, audit bank transactions, and mark entries updated in bank statement.</p>
</div>
<div id="deposit_alert"></div>
@ -20,7 +20,7 @@
<div class="col-md-3"><input type="text" class="form-control" name="department" placeholder="Department" required></div>
<div class="col-md-3"><input type="text" class="form-control" name="bank_name" placeholder="Bank Name" required></div>
<div class="col-md-3"><input type="text" class="form-control" name="slip_no" placeholder="Slip No"></div>
<div class="col-md-3"><input type="number" $11" class="form-control" name="deposited_amount" placeholder="Deposited Amount" required></div>
<div class="col-md-3"><input type="number" min="0" step="0.01" class="form-control" name="deposited_amount" placeholder="Deposited Amount" required></div>
<div class="col-md-3">
<select class="form-select" name="related_scroll_id">
<option value="">Related Scroll (optional)</option>
@ -51,6 +51,28 @@
load_form_div('<?= base_url('Finance/bank_deposits_table') ?>', 'bank_deposit_table_wrap');
}
window.financeDepositStatusAction = function(depositId, action) {
var fd = new window.FormData();
fd.append('deposit_id', String(depositId));
fd.append('action', action);
fetch('<?= base_url('Finance/bank_deposit_status_update') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
})
.then(function(res) { return res.json().then(function(data) { return { ok: res.ok, data: data }; }); })
.then(function(result) {
if (!result.ok || !result.data || result.data.status !== 1) {
showAlert((result.data && result.data.message) ? result.data.message : 'Request failed', false);
return;
}
showAlert(result.data.message || 'Updated successfully', true);
refreshTable();
})
.catch(function() { showAlert('Network or server error.', false); });
};
var form = document.getElementById('bank_deposit_form');
if (form) {
form.addEventListener('submit', function(e) {

View File

@ -1,88 +1,127 @@
<section class="content finance-cashbook">
<?php
$queue = $queue_summary ?? ['all' => 0, 'pending' => 0, 'received' => 0, 'deposited' => 0];
$collectorOptions = $collector_options ?? [];
$showBillingPanel = isset($show_billing_panel) ? (bool) $show_billing_panel : true;
$showAccountsPanel = isset($show_accounts_panel) ? (bool) $show_accounts_panel : true;
if (! $showBillingPanel && ! $showAccountsPanel) {
$showAccountsPanel = true;
}
$pageTitle = 'Cash Submission';
$pageSubtitle = 'Select patient payment rows from Payment History, submit exact totals to Accounts, and verify with full payment-level audit.';
if ($showBillingPanel && ! $showAccountsPanel) {
$pageTitle = 'Billing Cash Submission';
$pageSubtitle = 'Select cash payments and submit to Accounts Department.';
} elseif (! $showBillingPanel && $showAccountsPanel) {
$pageTitle = 'Accounts Cash Submission Verification';
$pageSubtitle = 'Accept submitted cash statements and mark them deposited after verification.';
}
?>
<div class="mb-3">
<h2 class="mb-1">Cash Collection & Disbursement SOP</h2>
<p class="text-muted mb-0">Capture receipts/disbursements, scroll submission, and compliance checks for Section 269ST and 40A(3).</p>
<h2 class="mb-1"><?= esc($pageTitle) ?></h2>
<p class="text-muted mb-0"><?= esc($pageSubtitle) ?></p>
</div>
<div id="cash_alert"></div>
<div class="row g-2 mb-2">
<div class="col-md-3 col-6">
<div class="card border-primary"><div class="card-body py-2"><div class="small text-muted">Today Receipts</div><div class="h5 mb-0 text-primary"><?= number_format((float) ($summary['today_receipts'] ?? 0), 2) ?></div></div></div>
</div>
<div class="col-md-3 col-6">
<div class="card border-info"><div class="card-body py-2"><div class="small text-muted">Today Disbursements</div><div class="h5 mb-0 text-info"><?= number_format((float) ($summary['today_disbursements'] ?? 0), 2) ?></div></div></div>
</div>
<div class="col-md-3 col-6">
<div class="card border-danger"><div class="card-body py-2"><div class="small text-muted">Compliance Hold</div><div class="h5 mb-0 text-danger"><?= (int) ($summary['hold_count'] ?? 0) ?></div></div></div>
</div>
<div class="col-md-3 col-6">
<div class="card border-dark"><div class="card-body py-2"><div class="small text-muted">Pending Scrolls</div><div class="h5 mb-0"><?= (int) ($summary['pending_scroll'] ?? 0) ?></div></div></div>
</div>
<div class="col-md-3 col-6"><div class="card border-secondary"><div class="card-body py-2"><div class="small text-muted">All</div><div class="h5 mb-0"><?= (int) ($queue['all'] ?? 0) ?></div></div></div></div>
<div class="col-md-3 col-6"><div class="card border-warning"><div class="card-body py-2"><div class="small text-muted">Pending</div><div class="h5 mb-0 text-warning"><?= (int) ($queue['pending'] ?? 0) ?></div></div></div></div>
<div class="col-md-3 col-6"><div class="card border-primary"><div class="card-body py-2"><div class="small text-muted">Received</div><div class="h5 mb-0 text-primary"><?= (int) ($queue['received'] ?? 0) ?></div></div></div></div>
<div class="col-md-3 col-6"><div class="card border-success"><div class="card-body py-2"><div class="small text-muted">Deposited</div><div class="h5 mb-0 text-success"><?= (int) ($queue['deposited'] ?? 0) ?></div></div></div></div>
</div>
<div class="row g-3">
<div class="col-xl-6">
<?php if ($showBillingPanel): ?>
<div class="<?= $showAccountsPanel ? 'col-xl-7' : 'col-12' ?>">
<div class="card h-100">
<div class="card-header"><strong>1) Cash Entry</strong></div>
<div class="card-header"><strong>1) Select Payments For Submission</strong></div>
<div class="card-body">
<form id="cash_txn_form" class="row g-2">
<div class="col-md-4"><input type="date" name="txn_date" class="form-control" value="<?= date('Y-m-d') ?>" required></div>
<form id="scroll_filter_form" class="row g-2">
<div class="col-md-6">
<label class="form-label mb-1">Start Date &amp; Time</label>
<input type="datetime-local" name="start_datetime" class="form-control" value="<?= date('Y-m-d\\T00:00') ?>" required>
</div>
<div class="col-md-6">
<label class="form-label mb-1">End Date &amp; Time</label>
<input type="datetime-local" name="end_datetime" class="form-control" value="<?= date('Y-m-d\\TH:i') ?>" required>
</div>
<div class="col-md-4">
<select class="form-select" name="txn_type" required>
<option value="receipt">Receipt</option>
<option value="disbursement">Disbursement</option>
<label class="form-label mb-1">Collected By</label>
<select class="form-select" name="collected_by">
<option value="">All Users</option>
<?php foreach ($collectorOptions as $opt): ?>
<?php $oid = (int) ($opt['user_id'] ?? 0); ?>
<?php $oname = trim((string) ($opt['user_name'] ?? '')); ?>
<?php if ($oname !== ''): ?>
<option value="<?= $oid > 0 ? $oid : esc($oname) ?>"><?= esc($oname) ?></option>
<?php endif; ?>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-4">
<select class="form-select" name="flow_type" required>
<option value="patient">Patient</option>
<option value="pharmacy">Pharmacy</option>
<option value="vendor">Vendor</option>
<option value="salary">Salary</option>
<option value="reimbursement">Reimbursement</option>
<option value="doctor_payout">Doctor Payout</option>
<option value="other">Other</option>
</select>
<label class="form-label mb-1">Payment Mode</label>
<input type="text" class="form-control" value="Cash" readonly>
<input type="hidden" name="payment_mode" value="cash">
</div>
<div class="col-md-4"><input type="text" class="form-control" name="department" placeholder="Department (OPD/IPD/Pharmacy)"></div>
<div class="col-md-4"><input type="text" class="form-control" name="reference_no" placeholder="Reference No"></div>
<div class="col-md-4"><input type="number" $11" class="form-control" name="amount" placeholder="Amount" required></div>
<div class="col-md-4">
<select class="form-select" name="mode" required>
<option value="cash">Cash</option>
<option value="bank">Bank</option>
<option value="upi">UPI</option>
<option value="card">Card</option>
</select>
<label class="form-label mb-1">Department</label>
<input type="text" class="form-control" name="department" value="Billing" required>
</div>
<div class="col-12">
<button type="button" id="load_payments_btn" class="btn btn-outline-secondary btn-sm">Load Payments</button>
</div>
<div class="col-md-8"><input type="text" class="form-control" name="party_name" placeholder="Party / Patient / Vendor"></div>
<div class="col-12"><textarea class="form-control" name="narration" rows="2" placeholder="Narration"></textarea></div>
<div class="col-12"><button type="submit" class="btn btn-primary btn-sm">Save Cash Entry</button></div>
</form>
<hr>
<div id="cash_txn_table_wrap"><?= view('finance/partials/cash_transactions_table', ['transactions' => $transactions ?? []]) ?></div>
<form id="scroll_form" class="row g-2">
<input type="hidden" name="start_datetime" value="">
<input type="hidden" name="end_datetime" value="">
<input type="hidden" name="collected_by" value="">
<input type="hidden" name="payment_mode" value="cash">
<input type="hidden" name="department" value="Billing">
<div id="payment_selection_wrap"><?= view('finance/partials/payment_selection_table', ['rows' => [], 'totals' => ['payment_count' => 0, 'total_receipts' => 0]]) ?></div>
<div class="col-md-6"><input type="text" name="submitted_by" class="form-control" placeholder="Submitted By"></div>
<div class="col-md-6"><input type="text" name="remarks" class="form-control" placeholder="Remarks"></div>
<div class="col-md-6"><input type="number" min="0" step="0.01" name="submitted_amount" class="form-control" placeholder="Calculated Total" readonly></div>
<div class="col-md-6"><input type="text" id="selection_info" class="form-control" value="Selected payments: 0" readonly></div>
<div class="col-12"><button type="submit" class="btn btn-primary btn-sm">Submit Selected Payments To Accounts</button></div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<div class="col-xl-6">
<?php if ($showAccountsPanel): ?>
<div class="<?= $showBillingPanel ? 'col-xl-5' : 'col-12' ?>">
<div class="card h-100">
<div class="card-header"><strong>2) Scroll Submission & Reconciliation</strong></div>
<div class="card-header"><strong>2) Accounts Acceptance And Verification Queue</strong></div>
<div class="card-body">
<form id="scroll_form" class="row g-2">
<div class="col-md-4"><input type="date" name="scroll_date" class="form-control" value="<?= date('Y-m-d') ?>" required></div>
<div class="col-md-4"><input type="text" name="department" class="form-control" placeholder="Department" required></div>
<div class="col-md-4"><input type="number" $11" name="submitted_amount" class="form-control" placeholder="Submitted Amount" required></div>
<div class="col-md-6"><input type="text" name="submitted_by" class="form-control" placeholder="Submitted By"></div>
<div class="col-md-6"><input type="text" name="remarks" class="form-control" placeholder="Remarks"></div>
<div class="col-12"><button type="submit" class="btn btn-primary btn-sm">Submit Scroll</button></div>
</form>
<hr>
<div id="scroll_table_wrap"><?= view('finance/partials/scroll_table', ['scrolls' => $scrolls ?? []]) ?></div>
</div>
</div>
</div>
<?php endif; ?>
</div>
<div class="modal fade" id="scrollItemsModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Submitted Payment Details</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="scroll_items_body">
<div class="text-muted">Loading...</div>
</div>
</div>
</div>
</div>
</section>
@ -100,27 +139,214 @@
}
}
function refreshTables() {
load_form_div('<?= base_url('Finance/cash_transactions_table') ?>', 'cash_txn_table_wrap');
load_form_div('<?= base_url('Finance/scroll_table') ?>', 'scroll_table_wrap');
function refreshScrollTable() {
if (document.getElementById('scroll_table_wrap')) {
load_form_div('<?= base_url('Finance/scroll_table') ?>', 'scroll_table_wrap');
}
}
function wireForm(formId, endpoint) {
var form = document.getElementById(formId);
function copyFiltersToSubmitForm() {
var filterForm = document.getElementById('scroll_filter_form');
var submitForm = document.getElementById('scroll_form');
if (!filterForm || !submitForm) {
return;
}
['start_datetime', 'end_datetime', 'collected_by', 'payment_mode', 'department'].forEach(function(name) {
var src = filterForm.querySelector('[name="' + name + '"]');
var dst = submitForm.querySelector('[name="' + name + '"]');
if (src && dst) {
dst.value = src.value;
}
});
}
function updateSelectionSummary() {
var wrap = document.getElementById('payment_selection_wrap');
var selected = 0;
var total = 0;
if (wrap) {
wrap.querySelectorAll('.payment-checkbox:checked').forEach(function(chk) {
selected++;
var row = chk.closest('tr');
if (row) {
var amountCell = row.querySelector('.payment-amount');
if (amountCell) {
total += Number(amountCell.getAttribute('data-amount') || '0');
}
}
});
}
var amountInput = document.querySelector('#scroll_form [name="submitted_amount"]');
if (amountInput) {
amountInput.value = total.toFixed(2);
}
var info = document.getElementById('selection_info');
if (info) {
info.value = 'Selected payments: ' + String(selected);
}
}
function wirePaymentSelectionEvents() {
var wrap = document.getElementById('payment_selection_wrap');
if (!wrap) {
return;
}
var selectAll = wrap.querySelector('#payment_select_all');
if (selectAll) {
selectAll.addEventListener('change', function() {
var checked = !!selectAll.checked;
wrap.querySelectorAll('.payment-checkbox').forEach(function(chk) {
chk.checked = checked;
});
updateSelectionSummary();
});
}
wrap.querySelectorAll('.payment-checkbox').forEach(function(chk) {
chk.addEventListener('change', updateSelectionSummary);
});
updateSelectionSummary();
}
function loadPayments() {
var form = document.getElementById('scroll_filter_form');
if (!form) {
return;
}
form.addEventListener('submit', function(e) {
e.preventDefault();
var formData = new window.FormData(form);
copyFiltersToSubmitForm();
fetch(endpoint, {
var params = new window.URLSearchParams(new window.FormData(form));
fetch('<?= base_url('Finance/payment_selection_table') ?>?' + params.toString(), {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(res) {
return res.text().then(function(html) {
return { ok: res.ok, html: html };
});
})
.then(function(result) {
if (!result.ok) {
showAlert('Unable to load payment list.', false);
return;
}
var wrap = document.getElementById('payment_selection_wrap');
if (wrap) {
wrap.innerHTML = result.html;
}
wirePaymentSelectionEvents();
showAlert('Payments loaded. Select rows to submit.', true);
})
.catch(function() {
showAlert('Network or server error while loading payments.', false);
});
}
window.financeScrollAction = function(scrollId, action) {
var endpoint = action === 'accept'
? '<?= base_url('Finance/scroll_accept') ?>'
: '<?= base_url('Finance/scroll_verify') ?>';
var fd = new window.FormData();
fd.append('scroll_id', String(scrollId));
fetch(endpoint, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: fd
})
.then(function(res) {
return res.json().then(function(data) {
return { ok: res.ok, data: data };
});
})
.then(function(result) {
if (!result.ok || !result.data || result.data.status !== 1) {
showAlert((result.data && result.data.message) ? result.data.message : 'Action failed', false);
return;
}
showAlert(result.data.message || 'Updated successfully', true);
refreshScrollTable();
})
.catch(function() {
showAlert('Network or server error.', false);
});
};
window.financeViewScrollItems = function(scrollId) {
var target = document.getElementById('scroll_items_body');
if (target) {
target.innerHTML = '<div class="text-muted">Loading...</div>';
}
fetch('<?= base_url('Finance/scroll_items_table') ?>?scroll_id=' + encodeURIComponent(String(scrollId)), {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(function(res) {
return res.text().then(function(html) {
return { ok: res.ok, html: html };
});
})
.then(function(result) {
if (!target) {
return;
}
target.innerHTML = result.ok ? result.html : '<div class="text-danger">Unable to load submission items.</div>';
})
.catch(function() {
if (target) {
target.innerHTML = '<div class="text-danger">Network or server error.</div>';
}
});
var modalEl = document.getElementById('scrollItemsModal');
if (modalEl && window.bootstrap && window.bootstrap.Modal) {
var modal = window.bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
}
};
var loadBtn = document.getElementById('load_payments_btn');
if (loadBtn) {
loadBtn.addEventListener('click', loadPayments);
}
var submitForm = document.getElementById('scroll_form');
if (submitForm) {
submitForm.addEventListener('submit', function(e) {
e.preventDefault();
copyFiltersToSubmitForm();
var payload = new window.FormData(submitForm);
var wrap = document.getElementById('payment_selection_wrap');
if (wrap) {
wrap.querySelectorAll('.payment-checkbox:checked').forEach(function(chk) {
payload.append('payment_ids[]', chk.value);
});
}
fetch('<?= base_url('Finance/scroll_create') ?>', {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
body: formData
body: payload
})
.then(function(res) {
return res.json().then(function(data) {
@ -133,9 +359,9 @@
return;
}
showAlert(result.data.message || 'Saved successfully', true);
form.reset();
refreshTables();
showAlert(result.data.message || 'Submission sent.', true);
refreshScrollTable();
loadPayments();
})
.catch(function() {
showAlert('Network or server error.', false);
@ -143,7 +369,6 @@
});
}
wireForm('cash_txn_form', '<?= base_url('Finance/cash_transaction_create') ?>');
wireForm('scroll_form', '<?= base_url('Finance/scroll_create') ?>');
wirePaymentSelectionEvents();
})();
</script>

View File

@ -1,123 +1,53 @@
<section class="content finance-sop">
<div class="finance-head mb-3">
<h2 class="mb-1">Finance & Accounting - Phase 1</h2>
<p class="mb-0 text-muted">Vendor onboarding, PO booking, GRN capture, and invoice registration.</p>
<div class="mt-2 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary btn-sm js-finance-launch" data-url="<?= base_url('Finance/section/vendor_master') ?>" data-title="Vendor Master">
<i class="bi bi-building"></i> Vendor Master
</button>
<button type="button" class="btn btn-outline-primary btn-sm js-finance-launch" data-url="<?= base_url('Finance/section/purchase_order') ?>" data-title="Purchase Order">
<i class="bi bi-card-checklist"></i> Purchase Order
</button>
<button type="button" class="btn btn-outline-primary btn-sm js-finance-launch" data-url="<?= base_url('Finance/section/grn_entry') ?>" data-title="GRN Entry">
<i class="bi bi-box-seam"></i> GRN Entry
</button>
<button type="button" class="btn btn-outline-primary btn-sm js-finance-launch" data-url="<?= base_url('Finance/section/vendor_invoice') ?>" data-title="Vendor Invoice">
<i class="bi bi-receipt"></i> Vendor Invoice
</button>
<a class="btn btn-primary btn-sm" href="javascript:load_form('<?= base_url('Finance/phase2') ?>','Finance & Accounting - Phase 2');">
Open Finance & Accounting - Phase 2
</a>
<a class="btn btn-outline-primary btn-sm" href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Cash Collection & Disbursement SOP');">
Open Cash Collection & Disbursement SOP
</a>
<a class="btn btn-outline-secondary btn-sm" href="javascript:load_form('<?= base_url('Finance/doctor_payout') ?>','Doctor Payout Workflow');">
Open Doctor Payout Workflow
</a>
<a class="btn btn-outline-success btn-sm" href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Deposit Register');">
Open Bank Deposit Register
</a>
<a class="btn btn-outline-dark btn-sm" href="javascript:load_form('<?= base_url('Finance/compliance_report') ?>','Finance Compliance Report');">
Open Finance Compliance Report
</a>
</div>
<?php $user = function_exists('auth') ? auth()->user() : null; ?>
<?php $canBillingSubmit = $user && method_exists($user, 'can') ? ($user->can('finance.cash.billing.submit') || $user->can('finance.*')) : false; ?>
<?php $canAccountsVerify = $user && method_exists($user, 'can') ? ($user->can('finance.cash.accounts.accept') || $user->can('finance.cash.accounts.verify') || $user->can('finance.*')) : false; ?>
<?php $canBankAudit = $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.*')) : false; ?>
<div class="mb-3">
<h2 class="mb-1">Accounts And Finance</h2>
<p class="text-muted mb-0">Simple operational workflow for Billing cash statement submission, Accounts verification, and bank transaction audit.</p>
</div>
<div class="row g-2 mb-2">
<div class="col-md-3 col-6">
<div class="card border-success">
<div class="card-body py-2">
<div class="small text-muted">Matched</div>
<div class="h5 mb-0 text-success"><?= (int) (($match_summary['matched'] ?? 0)) ?></div>
<div class="row g-3">
<?php if ($canBillingSubmit): ?>
<div class="col-md-4">
<div class="card h-100 border-primary">
<div class="card-body">
<h5 class="card-title">1) Cash Collection Statement</h5>
<p class="small text-muted">Billing Department creates and submits daily cash statement to Accounts.</p>
<a class="btn btn-primary btn-sm" href="javascript:load_form('<?= base_url('billing/cash-submission/create') ?>','Billing Cash Statement Submission');">
Open Billing Statement
</a>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="card border-warning">
<div class="card-body py-2">
<div class="small text-muted">Minor Variance</div>
<div class="h5 mb-0 text-warning"><?= (int) (($match_summary['minor_variance'] ?? 0)) ?></div>
<?php endif; ?>
<?php if ($canAccountsVerify): ?>
<div class="col-md-4">
<div class="card h-100 border-info">
<div class="card-body">
<h5 class="card-title">2) Accounts Accept & Verify</h5>
<p class="small text-muted">Accounts Department accepts submitted statements and verifies payment records.</p>
<a class="btn btn-info btn-sm text-white" href="javascript:load_form('<?= base_url('Finance/cashbook/accounts') ?>','Accounts Accept and Verify');">
Open Verification Queue
</a>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="card border-danger">
<div class="card-body py-2">
<div class="small text-muted">Mismatch / Not Checked</div>
<div class="h5 mb-0 text-danger"><?= (int) (($match_summary['mismatch'] ?? 0)) ?></div>
<?php endif; ?>
<?php if ($canBankAudit): ?>
<div class="col-md-4">
<div class="card h-100 border-success">
<div class="card-body">
<h5 class="card-title">3) Bank Trans Audit</h5>
<p class="small text-muted">Audit bank transactions and mark updates posted in the bank statement register.</p>
<a class="btn btn-success btn-sm" href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Transaction Audit');">
Open Bank Audit Register
</a>
</div>
</div>
</div>
</div>
<div class="col-md-3 col-6">
<div class="card border-dark">
<div class="card-body py-2">
<div class="small text-muted">Compliance Hold</div>
<div class="h5 mb-0"><?= (int) (($match_summary['hold'] ?? 0)) ?></div>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-header">
<strong id="finance_sub_title">Vendor Master</strong>
</div>
<div class="card-body">
<div id="finance-main"></div>
</div>
<?php endif; ?>
</div>
</section>
<script>
(function() {
var launchers = Array.prototype.slice.call(document.querySelectorAll('.js-finance-launch'));
var titleBox = document.getElementById('finance_sub_title');
function activateButton(activeBtn) {
launchers.forEach(function(btn) {
btn.classList.remove('btn-primary');
btn.classList.add('btn-outline-primary');
});
if (activeBtn) {
activeBtn.classList.remove('btn-outline-primary');
activeBtn.classList.add('btn-primary');
}
}
function loadSection(btn) {
if (!btn) {
return;
}
var url = btn.getAttribute('data-url');
var title = btn.getAttribute('data-title') || 'Finance Section';
activateButton(btn);
if (titleBox) {
titleBox.textContent = title;
}
load_form_div(url, 'finance-main', title + ' :Finance');
}
launchers.forEach(function(btn) {
btn.addEventListener('click', function() {
loadSection(btn);
});
});
if (launchers.length > 0) {
loadSection(launchers[0]);
}
})();
</script>

View File

@ -0,0 +1,94 @@
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th style="width:36px;"><input type="checkbox" id="select_all_bank_payments" onchange="toggleSelectAllBankPayments(this)"></th>
<th>#</th>
<th>HMS Ref</th>
<th>Payment Date</th>
<th>Amount</th>
<th>Txn Ref / UTR</th>
<th>Channel</th>
<th>Accepted By / Matched By</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="10" class="text-center text-muted py-3">No bank payments found for the selected criteria.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach (($rows ?? []) as $row): ?>
<?php
$status = (string) ($row['bank_reconcile_status'] ?? '');
$isMatched = $status === 'matched';
$sourceLabel = trim((string) ($row['bank_source_label'] ?? ''));
if ($sourceLabel === '[]') {
$sourceLabel = '';
}
$isPos = stripos($sourceLabel, 'machine') !== false || stripos((string) ($row['bankcard_machine'] ?? ''), 'machine') !== false;
$channel = $isPos ? 'POS' : 'UPI/Direct';
?>
<tr>
<td>
<?php if (! $isMatched): ?>
<input type="checkbox" class="bank-payment-check" value="<?= (int) ($row['id'] ?? 0) ?>">
<?php endif; ?>
</td>
<td><?= $sr++ ?></td>
<td class="font-monospace small">PH-<?= (int) ($row['id'] ?? 0) ?></td>
<td class="text-nowrap"><?= esc((string) ($row['payment_date'] ?? '')) ?></td>
<td class="text-end fw-semibold">₹<?= number_format((float) ($row['amount'] ?? 0), 2) ?></td>
<td class="font-monospace small"><?= esc((string) ($row['card_tran_id'] ?? '—')) ?></td>
<td>
<span class="badge <?= $isPos ? 'bg-info text-dark' : 'bg-secondary' ?>"><?= esc($channel) ?></span>
<?php if ($sourceLabel !== ''): ?>
<div class="small text-muted"><?= esc($sourceLabel) ?></div>
<?php elseif ((string) ($row['bankcard_machine'] ?? '') !== ''): ?>
<div class="small text-muted"><?= esc((string) ($row['bankcard_machine'] ?? '')) ?></div>
<?php endif; ?>
</td>
<td class="small">
<div><?= esc((string) ($row['accepted_by_name'] ?? '—')) ?></div>
<?php if ($isMatched && (string)($row['matched_by'] ?? '') !== ''): ?>
<div class="text-muted" style="font-size:0.78em;">
<i class="bi bi-check2-circle me-1 text-success"></i>Matched: <?= esc((string)($row['matched_by'] ?? '')) ?>
<?php if ((string)($row['matched_at'] ?? '') !== ''): ?>
<br><span><?= esc(date('d-M-y H:i', strtotime((string)$row['matched_at']))) ?></span>
<?php endif; ?>
</div>
<?php endif; ?>
</td>
<td>
<?php if ($isMatched): ?>
<span class="badge bg-success">Matched</span>
<?php if ((string) ($row['bank_reconcile_batch_ref'] ?? '') !== ''): ?>
<div class="small text-muted"><?= esc((string) ($row['bank_reconcile_batch_ref'] ?? '')) ?></div>
<?php endif; ?>
<?php if ((int) ($row['bank_settlement_entry_id'] ?? 0) > 0): ?>
<div class="small mt-1"><span class="badge bg-warning text-dark">Settlement</span></div>
<?php if ((string) ($row['settlement_ref'] ?? '') !== ''): ?>
<div class="small text-muted"><?= esc((string) ($row['settlement_ref'] ?? '')) ?></div>
<?php endif; ?>
<?php endif; ?>
<?php else: ?>
<span class="badge bg-warning text-dark">Unmatched</span>
<?php endif; ?>
</td>
<td class="text-nowrap">
<?php if ($isMatched): ?>
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="unmatchPayment(<?= (int)($row['id']??0) ?>)">
<i class="bi bi-x-circle me-1"></i>Unmatch
</button>
<?php else: ?>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="matchSinglePayment(<?= (int)($row['id']??0) ?>)">
<i class="bi bi-link-45deg me-1"></i>Match
</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -9,11 +9,12 @@
<th>Slip</th>
<th>Amount</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($deposits ?? [])): ?>
<tr><td colspan="7" class="text-center text-muted">No bank deposits found.</td></tr>
<tr><td colspan="8" class="text-center text-muted">No bank deposits found.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach (($deposits ?? []) as $row): ?>
<?php $status = (string) ($row['reconciliation_status'] ?? 'pending'); ?>
@ -25,12 +26,25 @@
<td><?= esc((string) ($row['slip_no'] ?? '')) ?></td>
<td><?= number_format((float) ($row['deposited_amount'] ?? 0), 2) ?></td>
<td>
<?php if ($status === 'matched'): ?>
<?php if ($status === 'statement_updated'): ?>
<span class="badge bg-success">Statement Updated</span>
<?php elseif ($status === 'audited'): ?>
<span class="badge bg-info text-dark">Audited</span>
<?php elseif ($status === 'matched'): ?>
<span class="badge bg-success">Matched</span>
<?php else: ?>
<span class="badge bg-warning text-dark">Pending</span>
<?php endif; ?>
</td>
<td>
<?php if ($status === 'statement_updated'): ?>
<span class="text-muted small">Completed</span>
<?php elseif ($status === 'audited'): ?>
<button type="button" class="btn btn-outline-success btn-sm" onclick="financeDepositStatusAction(<?= (int) ($row['id'] ?? 0) ?>, 'statement_update')">Update Statement</button>
<?php else: ?>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="financeDepositStatusAction(<?= (int) ($row['id'] ?? 0) ?>, 'audit')">Mark Audited</button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>

View File

@ -0,0 +1,77 @@
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Date</th>
<th>Terminal</th>
<th>Settlement Amt</th>
<th>System Total</th>
<th>Variance</th>
<th>Transactions</th>
<th>Status</th>
<th>By</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="10" class="text-center text-muted py-3">No POS settlements recorded yet.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach (($rows ?? []) as $row): ?>
<?php
$status = (string) ($row['status'] ?? 'pending');
$variance = (float) ($row['variance'] ?? 0);
$badgeMap = [
'matched' => '<span class="badge bg-success">Matched</span>',
'variance' => '<span class="badge bg-danger">Variance</span>',
'accepted' => '<span class="badge bg-primary">Accepted</span>',
'pending' => '<span class="badge bg-warning text-dark">Pending</span>',
];
$badge = $badgeMap[$status] ?? '<span class="badge bg-secondary">' . esc($status) . '</span>';
?>
<tr>
<td><?= $sr++ ?></td>
<td class="text-nowrap"><?= esc((string)($row['settlement_date']??'')) ?></td>
<td>
<span class="fw-semibold"><?= esc((string)($row['terminal_id']??'')) ?></span>
<?php if ((string)($row['terminal_name']??'') !== ''): ?>
<div class="small text-muted"><?= esc((string)($row['terminal_name'])) ?></div>
<?php endif; ?>
</td>
<td class="text-end">₹<?= number_format((float)($row['settlement_amount']??0),2) ?></td>
<td class="text-end">₹<?= number_format((float)($row['system_total']??0),2) ?></td>
<td class="text-end <?= abs($variance) <= 0.01 ? 'text-success' : 'text-danger fw-bold' ?>">
<?= ($variance >= 0 ? '+' : '') . number_format($variance, 2) ?>
</td>
<td class="text-center"><?= (int)($row['payment_count']??0) ?></td>
<td><?= $badge ?></td>
<td class="small">
<?= esc((string)($row['reconciled_by']??'')) ?>
<?php if ((string)($row['reconciled_at']??'') !== ''): ?>
<div class="text-muted"><?= esc(substr((string)$row['reconciled_at'],0,16)) ?></div>
<?php endif; ?>
</td>
<td class="text-nowrap">
<?php if ($status === 'accepted'): ?>
<span class="text-muted small">Closed</span>
<?php elseif ($status === 'matched'): ?>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="openAcceptModal(<?= (int)($row['id']??0) ?>)">
<i class="bi bi-lock me-1"></i>Accept
</button>
<?php elseif ($status === 'variance'): ?>
<button type="button" class="btn btn-outline-danger btn-sm"
onclick="openAcceptModal(<?= (int)($row['id']??0) ?>)">
<i class="bi bi-exclamation-triangle me-1"></i>Accept Variance
</button>
<?php else: ?>
<span class="text-muted small">No txns found</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,80 @@
<?php
$rows = $rows ?? [];
$entry = $entry ?? [];
$settlementId = (int) ($settlement_id ?? 0);
$total = 0.0;
foreach ($rows as $r) {
$total += (float) ($r['amount'] ?? 0);
}
?>
<div class="p-2 border-bottom bg-light small d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong>Settlement Ref:</strong> <?= esc((string) ($entry['settlement_ref'] ?? '')) ?>
<span class="ms-3"><strong>Date:</strong> <?= esc((string) ($entry['settlement_date'] ?? '')) ?></span>
<span class="ms-3"><strong>Rows:</strong> <?= count($rows) ?></span>
<span class="ms-3"><strong>Total:</strong> ₹<?= number_format($total, 2) ?></span>
</div>
<?php if ($settlementId > 0): ?>
<button type="button" class="btn btn-outline-success btn-sm"
onclick="window.open('<?= base_url('Finance/bank_settlement_linked_payments_export?settlement_id=' . $settlementId) ?>', '_blank')">
<i class="bi bi-file-earmark-spreadsheet me-1"></i>Export CSV
</button>
<?php endif; ?>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>HMS Ref</th>
<th>Payment Date</th>
<th class="text-end">Amount</th>
<th>Txn Ref / UTR</th>
<th>Channel</th>
<th>Accepted By</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td colspan="8" class="text-center text-muted py-3">No linked payments found.</td>
</tr>
<?php else: ?>
<?php $sr = 1; foreach ($rows as $row): ?>
<?php
$sourceLabel = trim((string) ($row['bank_source_label'] ?? ''));
if ($sourceLabel === '[]') {
$sourceLabel = '';
}
$isPos = stripos($sourceLabel, 'machine') !== false || stripos((string) ($row['bankcard_machine'] ?? ''), 'machine') !== false;
?>
<tr>
<td><?= $sr++ ?></td>
<td class="font-monospace small">PH-<?= (int) ($row['id'] ?? 0) ?></td>
<td class="text-nowrap"><?= esc((string) ($row['payment_date'] ?? '')) ?></td>
<td class="text-end fw-semibold">₹<?= number_format((float) ($row['amount'] ?? 0), 2) ?></td>
<td class="font-monospace small"><?= esc((string) ($row['card_tran_id'] ?? '—')) ?></td>
<td>
<span class="badge <?= $isPos ? 'bg-info text-dark' : 'bg-secondary' ?>"><?= $isPos ? 'POS' : 'UPI/Direct' ?></span>
<?php if ($sourceLabel !== ''): ?>
<div class="small text-muted"><?= esc($sourceLabel) ?></div>
<?php endif; ?>
</td>
<td><?= esc((string) ($row['accepted_by_name'] ?? '—')) ?></td>
<td>
<?php if ((string) ($row['bank_reconcile_status'] ?? '') === 'matched'): ?>
<span class="badge bg-success">Matched</span>
<?php else: ?>
<span class="badge bg-warning text-dark">Unmatched</span>
<?php endif; ?>
<?php if ((string) ($row['bank_reconcile_batch_ref'] ?? '') !== ''): ?>
<div class="small text-muted"><?= esc((string) ($row['bank_reconcile_batch_ref'] ?? '')) ?></div>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,61 @@
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Date</th>
<th>Ref / UTR</th>
<th>Narration</th>
<th>Amount</th>
<th>Type</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($entries ?? [])): ?>
<tr><td colspan="8" class="text-center text-muted py-3">No statement entries found.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach (($entries ?? []) as $entry): ?>
<?php $status = (string) ($entry['reconciliation_status'] ?? 'unmatched'); ?>
<tr>
<td><?= $sr++ ?></td>
<td class="text-nowrap"><?= esc((string)($entry['entry_date'] ?? '')) ?></td>
<td class="font-monospace small"><?= esc((string)($entry['reference_no'] ?? '—')) ?></td>
<td class="small"><?= esc((string)($entry['narration'] ?? '—')) ?></td>
<td class="text-end fw-semibold">₹<?= number_format((float)($entry['amount'] ?? 0), 2) ?></td>
<td>
<?php if ((string)($entry['transaction_type']??'') === 'debit'): ?>
<span class="badge bg-secondary">Debit</span>
<?php else: ?>
<span class="badge bg-info text-dark">Credit</span>
<?php endif; ?>
</td>
<td>
<?php if ($status === 'matched'): ?>
<span class="badge bg-success">Matched</span>
<?php if ($entry['matched_payment_id']): ?>
<div class="small text-muted">Pmt #<?= (int)($entry['matched_payment_id']) ?></div>
<?php endif; ?>
<?php else: ?>
<span class="badge bg-warning text-dark">Unmatched</span>
<?php endif; ?>
</td>
<td>
<?php if ($status === 'unmatched'): ?>
<!-- Match button used when loaded inside the match modal -->
<button type="button" class="btn btn-success btn-sm" style="display:none"
data-entry-id="<?= (int)($entry['id']??0) ?>"
onclick="confirmMatch(<?= (int)($entry['id']??0) ?>)">
<i class="bi bi-check2 me-1"></i>Select
</button>
<?php else: ?>
<span class="text-muted small"><?= esc((string)($entry['matched_by']??'')) ?></span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,52 @@
<?php
$rows = $rows ?? [];
$totals = $totals ?? ['payment_count' => 0, 'total_receipts' => 0];
?>
<div class="d-flex justify-content-between align-items-center mb-2">
<div class="small text-muted">Matched Payments: <strong><?= (int) ($totals['payment_count'] ?? 0) ?></strong></div>
<div class="small text-muted">Total: <strong><?= number_format((float) ($totals['total_receipts'] ?? 0), 2) ?></strong></div>
</div>
<div class="table-responsive border rounded">
<table class="table table-sm table-striped align-middle mb-0">
<thead>
<tr>
<th style="width:36px;"><input type="checkbox" id="payment_select_all"></th>
<th>#</th>
<th>Payment Date</th>
<th>Invoice Ref</th>
<th>Collected By</th>
<th>Mode</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" class="text-center text-muted">No payments found for selected filter.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach ($rows as $row): ?>
<?php $pid = (int) ($row['id'] ?? 0); ?>
<tr>
<td><input type="checkbox" class="payment-checkbox" name="payment_ids[]" value="<?= $pid ?>"></td>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row['payment_date'] ?? '')) ?></td>
<td><?= esc((string) ($row['payof_code'] ?? '')) ?></td>
<td>
<?php
$collector = trim((string) ($row['update_by'] ?? ''));
$collectorId = (int) ($row['update_by_id'] ?? 0);
echo esc($collector !== '' ? $collector : ('User #' . $collectorId));
?>
</td>
<td>
<?php
$mode = (int) ($row['payment_mode'] ?? 0);
echo esc($mode === 1 ? 'Cash' : ($mode === 2 ? 'Bank' : 'Other'));
?>
</td>
<td class="text-end payment-amount" data-amount="<?= (float) ($row['amount'] ?? 0) ?>"><?= number_format((float) ($row['amount'] ?? 0), 2) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,76 @@
<?php
$rows = $rows ?? [];
?>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<strong>Recent OPD Payout Drafts</strong>
<span class="small text-muted">Filtered by current date/doctor/state selection</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Payout Date</th>
<th>Case Ref</th>
<th>Doctor</th>
<th class="text-end">Units</th>
<th class="text-end">Rate</th>
<th class="text-end">Calculated</th>
<th class="text-end">Approved</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr>
<td colspan="10" class="text-center text-muted py-3">No payout drafts found for selected filters.</td>
</tr>
<?php else: ?>
<?php $sr = 1; foreach ($rows as $row): ?>
<?php $status = strtolower(trim((string) ($row['status'] ?? 'draft'))); ?>
<tr>
<td><?= $sr++ ?></td>
<td class="text-nowrap"><?= esc((string) ($row['payout_date'] ?? '')) ?></td>
<td class="font-monospace"><?= esc((string) ($row['case_reference'] ?? '')) ?></td>
<td><?= esc((string) ($row['doctor_name'] ?? '')) ?></td>
<td class="text-end"><?= (int) ($row['units'] ?? 0) ?></td>
<td class="text-end">Rs <?= number_format((float) ($row['rate'] ?? 0), 2) ?></td>
<td class="text-end">Rs <?= number_format((float) ($row['calculated_amount'] ?? 0), 2) ?></td>
<td class="text-end">Rs <?= number_format((float) ($row['approved_amount'] ?? 0), 2) ?></td>
<td>
<?php if ($status === 'paid'): ?>
<span class="badge bg-success">Paid</span>
<?php elseif ($status === 'ceo_approved'): ?>
<span class="badge bg-primary">CEO Approved</span>
<?php elseif ($status === 'finance_approved'): ?>
<span class="badge bg-info text-dark">Finance Approved</span>
<?php else: ?>
<span class="badge bg-secondary">Draft</span>
<?php endif; ?>
</td>
<td>
<?php if ($status === 'draft'): ?>
<button type="button" class="btn btn-outline-primary btn-sm"
onclick="editOpdPayoutDraft(<?= (int) ($row['id'] ?? 0) ?>, '<?= esc((string) ($row['payout_date'] ?? ''), 'js') ?>', '<?= esc((string) ($row['approved_amount'] ?? '0'), 'js') ?>', '<?= esc((string) ($row['remarks'] ?? ''), 'js') ?>')">
<i class="bi bi-pencil-square me-1"></i>Edit
</button>
<button type="button" class="btn btn-outline-danger btn-sm mt-1"
onclick="deleteOpdPayoutDraft(<?= (int) ($row['id'] ?? 0) ?>)">
<i class="bi bi-trash me-1"></i>Delete
</button>
<?php else: ?>
<span class="text-muted small">Locked</span>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>

View File

@ -0,0 +1,256 @@
<?php
$summary = $summary ?? [];
$doctorBreakdown = $doctor_breakdown ?? [];
$fromDate = (string) ($from_date ?? '');
$toDate = (string) ($to_date ?? '');
$doctorName = trim((string) ($doctor_name ?? 'All Doctors'));
$stateUnit = trim((string) ($state_unit ?? ''));
$stateUnitLabel = trim((string) ($state_unit_label ?? 'State/Unit'));
$formatDate = static function (string $value): string {
$value = trim($value);
if ($value === '') {
return '';
}
$timestamp = strtotime($value);
return $timestamp === false ? $value : date('d-m-Y', $timestamp);
};
$money = static function ($value): string {
return 'Rs ' . number_format((float) $value, 2);
};
?>
<div class="card border-0 bg-light mb-3">
<div class="card-body py-2 px-3 d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<strong>Summary Range:</strong> <?= esc($formatDate($fromDate)) ?> to <?= esc($formatDate($toDate)) ?>
</div>
<div>
<strong>Doctor:</strong> <?= esc($doctorName !== '' ? $doctorName : 'All Doctors') ?>
</div>
<div>
<strong><?= esc($stateUnitLabel) ?>:</strong> <?= esc($stateUnit !== '' ? $stateUnit : 'All State/Unit') ?>
</div>
</div>
</div>
<div class="row g-2 mb-3">
<div class="col-xl-3 col-md-4 col-6"><div class="card border-primary"><div class="card-body py-2"><div class="small text-muted">Completed OPDs</div><div class="h5 mb-0 text-primary"><?= (int) ($summary['completed_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-secondary"><div class="card-body py-2"><div class="small text-muted">Routine OPDs</div><div class="h5 mb-0"><?= (int) ($summary['routine_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-danger"><div class="card-body py-2"><div class="small text-muted">Emergency OPDs</div><div class="h5 mb-0 text-danger"><?= (int) ($summary['emergency_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-info"><div class="card-body py-2"><div class="small text-muted">Doctors Involved</div><div class="h5 mb-0 text-info"><?= (int) ($summary['doctor_count'] ?? 0) ?></div></div></div></div>
</div>
<div class="row g-2 mb-3">
<div class="col-xl-3 col-md-4 col-6"><div class="card border-dark"><div class="card-body py-2"><div class="small text-muted">Already Calculated OPDs</div><div class="h5 mb-0\"><?= (int) ($summary['calculated_opd'] ?? 0) ?></div></div></div></div>
</div>
<div class="row g-2 mb-3">
<div class="col-xl-3 col-md-4 col-6"><div class="card border-success"><div class="card-body py-2"><div class="small text-muted">Gross OPD Revenue</div><div class="h5 mb-0 text-success"><?= esc($money($summary['gross_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-success"><div class="card-body py-2"><div class="small text-muted">Total Received</div><div class="h5 mb-0 text-success"><?= esc($money($summary['total_received'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-warning"><div class="card-body py-2"><div class="small text-muted">Cash Received</div><div class="h5 mb-0 text-warning"><?= esc($money($summary['cash_received'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-primary"><div class="card-body py-2"><div class="small text-muted">Bank Received</div><div class="h5 mb-0 text-primary"><?= esc($money($summary['bank_received'] ?? 0)) ?></div></div></div></div>
</div>
<div class="row g-2 mb-3">
<div class="col-xl-3 col-md-4 col-6"><div class="card border-dark"><div class="card-body py-2"><div class="small text-muted">Total Credit Amount</div><div class="h5 mb-0"><?= esc($money($summary['credit_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-dark"><div class="card-body py-2"><div class="small text-muted">Organizational Credit</div><div class="h5 mb-0"><?= esc($money($summary['org_credit_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-info"><div class="card-body py-2"><div class="small text-muted">Approved Consents</div><div class="h5 mb-0 text-info"><?= (int) ($summary['approved_consents'] ?? 0) ?></div></div></div></div>
<div class="col-xl-3 col-md-4 col-6"><div class="card border-secondary"><div class="card-body py-2"><div class="small text-muted">Payout Records / Amount</div><div class="h6 mb-0"><?= (int) ($summary['payout_count'] ?? 0) ?> / <?= esc($money($summary['payout_amount'] ?? 0)) ?></div></div></div></div>
</div>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center flex-wrap gap-2">
<strong>Doctor-wise OPD and Collection Summary</strong>
<span class="small text-muted">Running and New are shown for internal review; routine figure is non-emergency total.</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead class="table-light">
<tr>
<th>#</th>
<th>Doctor</th>
<th class="text-end">Completed</th>
<th class="text-end">Routine</th>
<th class="text-end">Emergency</th>
<th class="text-end">Running</th>
<th class="text-end">New</th>
<th class="text-end">Gross Amount</th>
<th class="text-end">Cash</th>
<th class="text-end">Bank</th>
<th class="text-end">Credit</th>
</tr>
</thead>
<tbody>
<?php if (empty($doctorBreakdown)): ?>
<tr>
<td colspan="11" class="text-center text-muted py-3">No OPD summary records found for the selected filters.</td>
</tr>
<?php else: ?>
<?php $sr = 1; foreach ($doctorBreakdown as $row): ?>
<tr>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row['doctor_name'] ?? '')) ?></td>
<td class="text-end"><?= (int) ($row['completed_opd'] ?? 0) ?></td>
<td class="text-end"><?= (int) ($row['routine_opd'] ?? 0) ?></td>
<td class="text-end"><?= (int) ($row['emergency_opd'] ?? 0) ?></td>
<td class="text-end"><?= (int) ($row['running_opd'] ?? 0) ?></td>
<td class="text-end"><?= (int) ($row['new_opd'] ?? 0) ?></td>
<td class="text-end"><?= esc($money($row['gross_amount'] ?? 0)) ?></td>
<td class="text-end"><?= esc($money($row['cash_received'] ?? 0)) ?></td>
<td class="text-end"><?= esc($money($row['bank_received'] ?? 0)) ?></td>
<td class="text-end"><?= esc($money($row['credit_amount'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
</table>
</div>
</div>
</div>
<?php
$baseForPayout = (float) ($summary['total_received'] ?? 0);
?>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white">
<strong>Payout Calculation (Preview)</strong>
</div>
<div class="card-body">
<div class="row g-2 align-items-end mb-2">
<div class="col-lg-3 col-md-6">
<label class="form-label form-label-sm">Base Amount</label>
<input type="number" class="form-control form-control-sm" id="opd_payout_base_amount" value="<?= number_format($baseForPayout, 2, '.', '') ?>" step="0.01" min="0">
</div>
<div class="col-lg-2 col-md-6">
<label class="form-label form-label-sm">Doctor Share %</label>
<input type="number" class="form-control form-control-sm" id="opd_payout_doctor_share" value="75" step="0.01" min="0" max="100">
</div>
<div class="col-lg-2 col-md-6">
<label class="form-label form-label-sm">Hospital Share %</label>
<input type="number" class="form-control form-control-sm" id="opd_payout_hospital_share" value="25" step="0.01" min="0" max="100">
</div>
<div class="col-lg-2 col-md-6">
<label class="form-label form-label-sm">Deductions</label>
<input type="number" class="form-control form-control-sm" id="opd_payout_deductions" value="0" step="0.01" min="0">
</div>
<div class="col-lg-3 col-md-6">
<label class="form-label form-label-sm">Adjustments (+/-)</label>
<input type="number" class="form-control form-control-sm" id="opd_payout_adjustments" value="0" step="0.01">
</div>
<div class="col-12 d-flex justify-content-end">
<button type="button" class="btn btn-success btn-sm" id="opd_payout_create_draft_btn">
<i class="bi bi-file-earmark-plus me-1"></i>Create Payout Draft
</button>
</div>
</div>
<div class="row g-2">
<div class="col-md-4 col-12"><div class="card border-info"><div class="card-body py-2"><div class="small text-muted">Doctor Gross Share</div><div class="h6 mb-0 text-info" id="opd_payout_doctor_gross">Rs 0.00</div></div></div></div>
<div class="col-md-4 col-12"><div class="card border-secondary"><div class="card-body py-2"><div class="small text-muted">Hospital Share</div><div class="h6 mb-0" id="opd_payout_hospital_gross">Rs 0.00</div></div></div></div>
<div class="col-md-4 col-12"><div class="card border-success"><div class="card-body py-2"><div class="small text-muted">Net Payable To Doctor</div><div class="h6 mb-0 text-success" id="opd_payout_net_payable">Rs 0.00</div></div></div></div>
</div>
</div>
</div>
<script>
(function () {
function toNumber(id) {
var el = document.getElementById(id);
if (!el) return 0;
var value = parseFloat(el.value || '0');
return isNaN(value) ? 0 : value;
}
function formatMoney(value) {
var num = isNaN(value) ? 0 : value;
return 'Rs ' + num.toLocaleString('en-IN', {minimumFractionDigits: 2, maximumFractionDigits: 2});
}
function recalc() {
var baseAmount = toNumber('opd_payout_base_amount');
var doctorShare = toNumber('opd_payout_doctor_share');
var hospitalShare = toNumber('opd_payout_hospital_share');
var deductions = toNumber('opd_payout_deductions');
var adjustments = toNumber('opd_payout_adjustments');
var doctorGross = (baseAmount * doctorShare) / 100;
var hospitalGross = (baseAmount * hospitalShare) / 100;
var netPayable = doctorGross - deductions + adjustments;
var doctorEl = document.getElementById('opd_payout_doctor_gross');
var hospitalEl = document.getElementById('opd_payout_hospital_gross');
var netEl = document.getElementById('opd_payout_net_payable');
if (doctorEl) doctorEl.textContent = formatMoney(doctorGross);
if (hospitalEl) hospitalEl.textContent = formatMoney(hospitalGross);
if (netEl) netEl.textContent = formatMoney(netPayable);
}
['opd_payout_base_amount', 'opd_payout_doctor_share', 'opd_payout_hospital_share', 'opd_payout_deductions', 'opd_payout_adjustments']
.forEach(function (id) {
var el = document.getElementById(id);
if (el) {
el.addEventListener('input', recalc);
}
});
var draftBtn = document.getElementById('opd_payout_create_draft_btn');
if (draftBtn) {
draftBtn.addEventListener('click', function () {
var fromDate = document.getElementById('opd_payout_from_date')?.value || '';
var toDate = document.getElementById('opd_payout_to_date')?.value || '';
var doctorId = document.getElementById('opd_payout_doctor_id')?.value || '';
var stateUnit = document.getElementById('opd_payout_state_unit')?.value || '';
if (!doctorId) {
if (typeof window.showOpdPayoutAlert === 'function') {
window.showOpdPayoutAlert('Please select a doctor before creating payout draft.', false);
}
return;
}
var fd = new window.FormData();
fd.append('from_date', fromDate);
fd.append('to_date', toDate);
fd.append('doctor_id', doctorId);
fd.append('state_unit', stateUnit);
fd.append('base_amount', String(toNumber('opd_payout_base_amount')));
fd.append('doctor_share', String(toNumber('opd_payout_doctor_share')));
fd.append('hospital_share', String(toNumber('opd_payout_hospital_share')));
fd.append('deductions', String(toNumber('opd_payout_deductions')));
fd.append('adjustments', String(toNumber('opd_payout_adjustments')));
fetch('<?= base_url('Finance/payout/opd-consult-draft-create') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd,
})
.then(function (res) { return res.json().then(function (data) { return {ok: res.ok, data: data}; }); })
.then(function (result) {
var ok = result.ok && result.data && result.data.status === 1;
var msg = (result.data && result.data.message) ? result.data.message : (ok ? 'Payout draft created.' : 'Unable to create payout draft.');
if (ok && result.data.case_reference) {
msg += ' Ref: ' + result.data.case_reference;
}
if (typeof window.showOpdPayoutAlert === 'function') {
window.showOpdPayoutAlert(msg, ok);
}
if (ok && typeof window.refreshOpdConsultPayoutSummary === 'function') {
window.refreshOpdConsultPayoutSummary();
}
})
.catch(function () {
if (typeof window.showOpdPayoutAlert === 'function') {
window.showOpdPayoutAlert('Network or server error while creating payout draft.', false);
}
});
});
}
recalc();
})();
</script>

View File

@ -0,0 +1,43 @@
<?php $rows = $rows ?? []; ?>
<div class="table-responsive">
<table class="table table-sm table-striped align-middle mb-0">
<thead>
<tr>
<th>#</th>
<th>Payment ID</th>
<th>Payment Date</th>
<th>Invoice Ref</th>
<th>Collector</th>
<th>Mode</th>
<th class="text-end">Amount</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows)): ?>
<tr><td colspan="7" class="text-center text-muted">No payment rows were saved for this submission.</td></tr>
<?php else: ?>
<?php $sr = 1; $total = 0.0; foreach ($rows as $row): ?>
<?php $amt = (float) ($row['amount'] ?? 0); $total += $amt; ?>
<tr>
<td><?= $sr++ ?></td>
<td><?= (int) ($row['payment_history_id'] ?? 0) ?></td>
<td><?= esc((string) ($row['payment_date'] ?? '')) ?></td>
<td><?= esc((string) ($row['payof_code'] ?? '')) ?></td>
<td><?= esc((string) ($row['update_by'] ?? '')) ?></td>
<td>
<?php
$mode = (int) ($row['payment_mode'] ?? 0);
echo esc($mode === 1 ? 'Cash' : ($mode === 2 ? 'Bank' : 'Other'));
?>
</td>
<td class="text-end"><?= number_format($amt, 2) ?></td>
</tr>
<?php endforeach; ?>
<tr>
<td colspan="6" class="text-end fw-semibold">Total</td>
<td class="text-end fw-semibold"><?= number_format($total, 2) ?></td>
</tr>
<?php endif; ?>
</tbody>
</table>
</div>

View File

@ -3,32 +3,61 @@
<thead>
<tr>
<th>#</th>
<th>Date</th>
<th>Collection Period</th>
<th>Department</th>
<th>Collected By</th>
<th>Payments</th>
<th>Total Receipts</th>
<th>Submitted</th>
<th>Variance</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<?php if (empty($scrolls ?? [])): ?>
<tr><td colspan="7" class="text-center text-muted">No scroll submissions yet.</td></tr>
<tr><td colspan="10" class="text-center text-muted">No cash statements submitted yet.</td></tr>
<?php else: ?>
<?php $sr = 1; foreach (($scrolls ?? []) as $row): ?>
<?php $st = (string) ($row['reconciliation_status'] ?? 'pending'); ?>
<tr>
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row['scroll_date'] ?? '')) ?></td>
<td>
<?php
$start = trim((string) ($row['start_datetime'] ?? ''));
$end = trim((string) ($row['end_datetime'] ?? ''));
if ($start !== '' && $end !== '') {
echo esc($start) . ' to ' . esc($end);
} else {
echo esc((string) ($row['scroll_date'] ?? ''));
}
?>
</td>
<td><?= esc((string) ($row['department'] ?? '')) ?></td>
<td><?= esc((string) ($row['collected_by'] ?? ($row['submitted_by'] ?? ''))) ?></td>
<td><?= (int) ($row['payment_count'] ?? 0) ?></td>
<td><?= number_format((float) ($row['total_receipts'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($row['submitted_amount'] ?? 0), 2) ?></td>
<td><?= number_format((float) ($row['variance_amount'] ?? 0), 2) ?></td>
<td>
<?php $st = (string) ($row['reconciliation_status'] ?? 'pending'); ?>
<?php if ($st === 'matched'): ?>
<span class="badge bg-success">Matched</span>
<?php else: ?>
<?php if ($st === 'deposited' || $st === 'verified' || $st === 'matched'): ?>
<span class="badge bg-success">Deposited</span>
<?php elseif ($st === 'received' || $st === 'accepted'): ?>
<span class="badge bg-primary">Received</span>
<?php elseif ($st === 'submitted' || $st === 'pending'): ?>
<span class="badge bg-warning text-dark">Pending</span>
<?php else: ?>
<span class="badge bg-secondary"><?= esc(ucfirst($st)) ?></span>
<?php endif; ?>
</td>
<td>
<button type="button" class="btn btn-outline-secondary btn-sm me-1" onclick="financeViewScrollItems(<?= (int) ($row['id'] ?? 0) ?>)">View Payments</button>
<?php if ($st === 'submitted' || $st === 'pending'): ?>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="financeScrollAction(<?= (int) ($row['id'] ?? 0) ?>, 'accept')">Mark Received</button>
<?php elseif ($st === 'accepted' || $st === 'received'): ?>
<button type="button" class="btn btn-outline-success btn-sm" onclick="financeScrollAction(<?= (int) ($row['id'] ?? 0) ?>, 'verify')">Mark Deposited</button>
<?php else: ?>
<span class="text-muted small">No action</span>
<?php endif; ?>
</td>
</tr>

View File

@ -74,20 +74,23 @@ if (! $canHospitalStock && $user && method_exists($user, 'inGroup')) {
$canFinance = false;
if ($user && method_exists($user, 'can')) {
$canFinance = $user->can('finance.access')
|| $user->can('finance.vendor.manage')
|| $user->can('finance.po.manage')
|| $user->can('finance.grn.manage')
|| $user->can('finance.invoice.manage')
|| $user->can('finance.cash.manage')
|| $user->can('finance.doctor_payout.manage')
|| $user->can('finance.bank_deposit.manage')
|| $user->can('finance.compliance.view')
$canFinance = $user->can('finance.workflow.view')
|| $user->can('finance.cash.billing.submit')
|| $user->can('finance.cash.accounts.accept')
|| $user->can('finance.cash.accounts.verify')
|| $user->can('finance.bank.deposit.create')
|| $user->can('finance.bank.audit')
|| $user->can('finance.bank.statement.update')
|| $user->can('finance.*');
}
if (! $canFinance && $user && method_exists($user, 'inGroup')) {
$canFinance = $user->inGroup('superadmin', 'admin', 'developer');
$canFinanceBilling = false;
$canFinanceAccounts = false;
$canFinanceBank = 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.*');
}
$canDoctorWork = false;
@ -246,44 +249,61 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
<span>Org Packing </span>
</a>
</li>
<?php if ($canFinanceBilling) { ?>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('billing/cash-submission/create') ?>','Cash Submission')">
<i class="bi bi-cash-stack"></i>
<span>Cash Submission</span>
</a>
</li>
<?php } ?>
<?php } ?>
<?php if ($canFinance) { ?>
<li class="nav-heading">Accounts And Finance</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/index') ?>','Finance & Accounting')">
<i class="bi bi-bank"></i>
<span>Finance & Accounting</span>
<span>Accounts And Finance</span>
</a>
</li>
<?php if ($canFinanceBilling) { ?>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('billing/cash-submission/create') ?>','Billing Cash Statement Submission')">
<i class="bi bi-journal-arrow-up"></i>
<span>Billing Cash Statement</span>
</a>
</li>
<?php } ?>
<?php if ($canFinanceAccounts) { ?>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/cashbook/accounts') ?>','Accounts Accept and Verify')">
<i class="bi bi-patch-check"></i>
<span>Accounts Verify Payment</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')">
<i class="bi bi-building"></i>
<span>Bank Audit And Statement</span>
</a>
</li>
<?php } ?>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/phase2') ?>','Finance & Accounting - Phase 2')">
<i class="bi bi-diagram-3"></i>
<span>Finance Phase 2</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Cash Collection & Disbursement SOP')">
<i class="bi bi-cash-coin"></i>
<span>Cash & Disbursement SOP</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/doctor_payout') ?>','Doctor Payout Workflow')">
<i class="bi bi-person-badge"></i>
<span>Doctor Payout Workflow</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Deposit Register')">
<i class="bi bi-building"></i>
<span>Bank Deposit Register</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Finance/compliance_report') ?>','Finance Compliance Report')">
<i class="bi bi-shield-exclamation"></i>
<span>Finance Compliance Report</span>
<a class="nav-link collapsed" data-bs-target="#finance-payout-nav" data-bs-toggle="collapse" href="#">
<i class="bi bi-cash-stack"></i>
<span>Payout</span>
<i class="bi bi-chevron-down ms-auto"></i>
</a>
<ul id="finance-payout-nav" class="nav-content collapse" data-bs-parent="#sidebar-nav">
<li>
<a href="javascript:load_form('<?= base_url('Finance/payout/opd-consult') ?>','OPD Consult Payout')">
<i class="bi bi-circle"></i>
<span>OPD Consult Payout</span>
</a>
</li>
</ul>
</li>
<?php } ?>
<?php if ($canIpdBilling) { ?>
@ -360,7 +380,7 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
<?php if ($canHospitalStock) { ?>
<li class="nav-heading">Hospital Stock</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('setting/admin/hospital-stock') ?>','Hospital Stock Management');">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Storestock') ?>','Hospital Stock');">
<i class="bi bi-box-seam"></i>
<span>Hospital Stock</span>
</a>

View File

@ -16,7 +16,7 @@
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Deposit Register');"><i class="bi bi-arrow-right-short"></i> Bank Deposit Register</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Transaction Audit');"><i class="bi bi-arrow-right-short"></i> Bank Transaction Audit</a></li>
</ul>
</div>
</div>
@ -32,12 +32,10 @@
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/index') ?>','Finance & Accounting');"><i class="bi bi-arrow-right-short"></i> Finance & Accounting (Phase 1)</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/phase2') ?>','Finance & Accounting - Phase 2');"><i class="bi bi-arrow-right-short"></i> Finance & Accounting (Phase 2)</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Cash Collection & Disbursement SOP');"><i class="bi bi-arrow-right-short"></i> Cash Collection & Disbursement SOP</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/doctor_payout') ?>','Doctor Payout Workflow');"><i class="bi bi-arrow-right-short"></i> Doctor Payout Workflow</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Deposit Register');"><i class="bi bi-arrow-right-short"></i> Bank Deposit Register</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/compliance_report') ?>','Finance Compliance Report');"><i class="bi bi-arrow-right-short"></i> Finance Compliance Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/index') ?>','Accounts And Finance');"><i class="bi bi-arrow-right-short"></i> Accounts And Finance Dashboard</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Billing Cash Statement Submission');"><i class="bi bi-arrow-right-short"></i> Billing Cash Statement Submission</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/cashbook') ?>','Accounts Accept and Verify');"><i class="bi bi-arrow-right-short"></i> Accounts Accept and Verify</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Transaction Audit');"><i class="bi bi-arrow-right-short"></i> Bank Transaction Audit</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/nabh_audit_report') ?>','NABH Audit Report');"><i class="bi bi-arrow-right-short"></i> NABH Audit Report</a></li>
</ul>
</div>

View File

@ -0,0 +1,392 @@
<style>
.storestock-ui .content-header h1 {
font-size: 1.35rem;
font-weight: 700;
color: #012970;
margin-bottom: 1rem;
}
.storestock-ui .content-header h1 small {
color: #6c757d;
font-weight: 500;
margin-left: .45rem;
}
.storestock-ui .box {
background: #fff;
border: 1px solid #ebeef4;
border-radius: 12px;
box-shadow: 0 0 24px rgba(1, 41, 112, 0.06);
margin-bottom: 1rem;
}
.storestock-ui .box-header,
.storestock-ui .box-footer {
padding: .85rem 1rem;
border-color: #ebeef4;
}
.storestock-ui .box-body {
padding: 1rem;
}
.storestock-ui .box-title {
color: #012970;
font-size: 1rem;
font-weight: 700;
margin: 0;
}
.storestock-ui .btn-app {
display: inline-flex;
align-items: center;
justify-content: center;
flex-direction: column;
min-width: 128px;
min-height: 94px;
margin: .35rem;
padding: .75rem;
border: 1px solid #ebeef4;
border-radius: 10px;
background: #fff;
color: #012970;
text-decoration: none;
font-weight: 600;
transition: all .18s ease;
}
.storestock-ui .btn-app i {
font-size: 1.25rem;
margin-bottom: .35rem;
color: #4154f1;
}
.storestock-ui .btn-app:hover {
transform: translateY(-1px);
border-color: #dbe3ff;
box-shadow: 0 6px 20px rgba(65, 84, 241, 0.12);
}
.storestock-ui .table {
margin-bottom: 0;
}
.storestock-ui .table > thead > tr > th {
background: #f6f9ff;
color: #012970;
border-bottom-width: 1px;
font-weight: 700;
font-size: .86rem;
}
.storestock-ui .table > tbody > tr > td,
.storestock-ui .table > tfoot > tr > th,
.storestock-ui .table > tfoot > tr > td {
vertical-align: middle;
}
.storestock-ui .form-control,
.storestock-ui .input-sm {
border-radius: 8px;
border-color: #dfe3ea;
}
.storestock-ui .input-group-addon {
background: #f6f9ff;
border: 1px solid #dfe3ea;
border-right: 0;
border-radius: 8px 0 0 8px;
color: #4154f1;
padding: .45rem .7rem;
}
.storestock-ui .btn-xs,
.storestock-ui .btn-sm {
border-radius: 8px;
}
.storestock-ui .nav-stacked > li > a {
display: block;
padding: .5rem .2rem;
color: #4154f1;
text-decoration: none;
font-weight: 600;
}
.storestock-ui .nav-stacked > li > a:hover {
color: #012970;
}
.storestock-ui .bg-gray {
background: #f6f9ff;
color: #012970;
border-bottom: 1px solid #ebeef4;
}
.storestock-ui .modal-content {
border-radius: 12px;
border: 1px solid #ebeef4;
box-shadow: 0 0 30px rgba(1, 41, 112, 0.15);
}
.storestock-ui .modal-header {
background: #f6f9ff;
border-bottom: 1px solid #ebeef4;
}
.storestock-ui .modal-title {
color: #012970;
font-weight: 700;
}
.storestock-ui .module-hero {
border: 1px solid #e9edf5;
background: linear-gradient(135deg, #f6f9ff 0%, #ffffff 85%);
border-radius: 14px;
padding: 1rem 1.1rem;
margin-bottom: 1rem;
}
.storestock-ui .module-hero h4 {
margin: 0;
color: #012970;
font-weight: 700;
}
.storestock-ui .module-hero p {
margin: .35rem 0 0 0;
color: #6c757d;
}
.storestock-ui .stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(120px, 1fr));
gap: .75rem;
margin-bottom: 1rem;
}
.storestock-ui .stat-card {
border: 1px solid #ebeef4;
border-radius: 12px;
background: #fff;
padding: .75rem .9rem;
}
.storestock-ui .stat-card .label {
display: block;
color: #6c757d;
font-size: .76rem;
font-weight: 600;
margin-bottom: .35rem;
}
.storestock-ui .stat-card .value {
color: #012970;
font-size: 1.15rem;
font-weight: 700;
line-height: 1;
}
.storestock-ui .action-grid {
display: grid;
grid-template-columns: repeat(4, minmax(180px, 1fr));
gap: .75rem;
}
.storestock-ui .action-card {
display: block;
border: 1px solid #e6eaf2;
border-radius: 12px;
background: #fff;
padding: .95rem;
text-decoration: none;
color: #212529;
transition: all .18s ease;
}
.storestock-ui .action-card:hover {
transform: translateY(-1px);
border-color: #dbe3ff;
box-shadow: 0 8px 22px rgba(65, 84, 241, 0.14);
text-decoration: none;
}
.storestock-ui .action-card .icon {
width: 42px;
height: 42px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
background: #eef2ff;
color: #4154f1;
font-size: 1.1rem;
margin-bottom: .55rem;
}
.storestock-ui .action-card h5 {
margin: 0;
font-size: .98rem;
color: #012970;
font-weight: 700;
}
.storestock-ui .action-card p {
margin: .35rem 0 0 0;
color: #6c757d;
font-size: .82rem;
}
.storestock-ui .toolbar-row {
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-items: center;
justify-content: space-between;
margin-bottom: .8rem;
}
.storestock-ui .toolbar-row .left,
.storestock-ui .toolbar-row .right {
display: flex;
flex-wrap: wrap;
gap: .5rem;
align-items: center;
}
.storestock-ui .pill-btn {
border: 1px solid #dfe3ea;
border-radius: 999px;
background: #fff;
color: #012970;
padding: .4rem .8rem;
font-size: .82rem;
font-weight: 600;
cursor: pointer;
}
.storestock-ui .pill-btn.active,
.storestock-ui .pill-btn:hover {
border-color: #4154f1;
color: #4154f1;
}
.storestock-ui .panel-grid {
display: grid;
grid-template-columns: repeat(2, minmax(260px, 1fr));
gap: .9rem;
}
.storestock-ui .control-card {
border: 1px solid #ebeef4;
border-radius: 12px;
background: #fff;
padding: .95rem;
}
.storestock-ui .control-card h5 {
margin: 0 0 .65rem 0;
color: #012970;
font-size: .95rem;
font-weight: 700;
}
.storestock-ui .control-card .help {
margin-top: .55rem;
color: #6c757d;
font-size: .8rem;
}
.storestock-ui .menu-grid {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: .9rem;
}
.storestock-ui .menu-section {
border: 1px solid #e9edf5;
border-radius: 12px;
background: #fff;
overflow: hidden;
}
.storestock-ui .menu-section .title {
background: #f6f9ff;
color: #012970;
font-weight: 700;
padding: .75rem .9rem;
border-bottom: 1px solid #e9edf5;
}
.storestock-ui .menu-list {
padding: .5rem .9rem .75rem;
}
.storestock-ui .menu-list a {
display: flex;
align-items: center;
gap: .45rem;
color: #4154f1;
padding: .38rem 0;
font-weight: 600;
text-decoration: none;
}
.storestock-ui .menu-list a:hover {
color: #012970;
}
.storestock-ui .search-bar {
display: grid;
grid-template-columns: 170px 1fr 300px;
gap: .7rem;
align-items: center;
}
.storestock-ui .table-wrap {
border: 1px solid #ebeef4;
border-radius: 12px;
overflow: hidden;
}
.storestock-ui .insight-grid {
display: grid;
grid-template-columns: repeat(2, minmax(280px, 1fr));
gap: .9rem;
margin-top: 1rem;
}
.storestock-ui .mini-badge {
display: inline-block;
min-width: 46px;
text-align: center;
border-radius: 999px;
padding: .2rem .45rem;
font-size: .75rem;
font-weight: 700;
}
.storestock-ui .mini-badge.warn {
background: #fff6e5;
color: #ad6800;
}
.storestock-ui .mini-badge.danger {
background: #ffe8e8;
color: #b42318;
}
@media (max-width: 991px) {
.storestock-ui .stats-grid,
.storestock-ui .action-grid,
.storestock-ui .panel-grid,
.storestock-ui .menu-grid,
.storestock-ui .search-bar {
grid-template-columns: 1fr;
}
.storestock-ui .insight-grid {
grid-template-columns: 1fr;
}
}
</style>

View File

@ -0,0 +1,151 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<?php
$stats = $stats ?? [];
$lists = $lists ?? [];
$stockValue = (float) ($stats['stock_value'] ?? 0);
?>
<section class="content-header">
<h1>Store Stock <small>Dashboard</small></h1>
</section>
<section class="content">
<div class="module-hero">
<h4>Hospital Stock Control Center</h4>
<p>Track indents, monitor stock movement, and jump to master controls from one screen.</p>
</div>
<div class="stats-grid">
<div class="stat-card">
<span class="label">Indents Today</span>
<span class="value"><?= (int) ($stats['today_indents'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Indents This Month</span>
<span class="value"><?= (int) ($stats['month_indents'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Low Stock Items</span>
<span class="value"><?= (int) ($stats['low_stock_count'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Near Expiry (60 Days)</span>
<span class="value"><?= (int) ($stats['near_expiry_count'] ?? 0) ?></span>
</div>
</div>
<div class="stats-grid" style="grid-template-columns: repeat(2, minmax(180px, 1fr));">
<div class="stat-card">
<span class="label">Pending Purchase Invoices</span>
<span class="value"><?= (int) ($stats['pending_purchase'] ?? 0) ?></span>
</div>
<div class="stat-card">
<span class="label">Approx. Current Stock Value</span>
<span class="value">Rs. <?= number_format($stockValue, 2) ?></span>
</div>
</div>
<div class="action-grid">
<a class="action-card" href="javascript:load_form_div('/Storestock/Indent_List','maindiv','Store : Indent');">
<span class="icon"><i class="fa fa-shopping-cart"></i></span>
<h5>Indent Desk</h5>
<p>Create and process departmental item requests.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/Report_2','maindiv','Day Report :Store');">
<span class="icon"><i class="fa fa-line-chart"></i></span>
<h5>Day Report</h5>
<p>Review daily issue trend and transaction summary.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/store_stock','maindiv','Store Stock : Store');">
<span class="icon"><i class="fa fa-barcode"></i></span>
<h5>Stock Search</h5>
<p>Find real-time availability with reorder controls.</p>
</a>
<a class="action-card" href="javascript:load_form_div('/Storestock/main_store','maindiv','Store Main : Store');">
<span class="icon"><i class="fa fa-cogs"></i></span>
<h5>Store Controls</h5>
<p>Open masters, inventory, and procurement tools.</p>
</a>
</div>
<div class="box" style="margin-top:1rem;">
<div class="box-header">
<h3 class="box-title">Workspace</h3>
</div>
<div class="box-body">
<div id="maindiv" class="row"></div>
</div>
</div>
<div class="insight-grid">
<div class="box">
<div class="box-header">
<h3 class="box-title">Low Stock Alerts</h3>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Item</th>
<th style="width:120px;">Current Unit</th>
<th style="width:140px;">Reorder Threshold</th>
</tr>
</thead>
<tbody>
<?php $lowStockItems = $lists['low_stock_items'] ?? []; ?>
<?php if (empty($lowStockItems)) { ?>
<tr><td colspan="3" class="text-center text-muted">No low-stock items right now.</td></tr>
<?php } ?>
<?php foreach ($lowStockItems as $row) { ?>
<tr>
<td><?= esc($row['item_name'] ?? '') ?></td>
<td><?= esc($row['cur_units'] ?? 0) ?></td>
<td><?= esc($row['re_order_qty'] ?? 0) ?> x <?= esc($row['packing'] ?? 1) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
<h3 class="box-title">Near Expiry Alerts</h3>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Item / Batch</th>
<th style="width:100px;">Expiry</th>
<th style="width:90px;">Days Left</th>
<th style="width:95px;">Cur Packs</th>
</tr>
</thead>
<tbody>
<?php $nearExpiryItems = $lists['near_expiry_items'] ?? []; ?>
<?php if (empty($nearExpiryItems)) { ?>
<tr><td colspan="4" class="text-center text-muted">No near-expiry batches in next 60 days.</td></tr>
<?php } ?>
<?php foreach ($nearExpiryItems as $row) { ?>
<tr>
<td><?= esc($row['item_name'] ?? '') ?> <span class="text-muted">(<?= esc($row['batch_no'] ?? '-') ?>)</span></td>
<td><?= esc($row['expiry_str'] ?? '-') ?></td>
<td>
<span class="mini-badge <?= ((int) ($row['days_left'] ?? 0)) <= 15 ? 'danger' : 'warn' ?>">
<?= (int) ($row['days_left'] ?? 0) ?>
</span>
</td>
<td><?= esc($row['cur_packs'] ?? 0) ?></td>
</tr>
<?php } ?>
</tbody>
</table>
</div>
</div>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,47 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">Day Report - Store</h3>
</div>
<div class="box-body">
<form class="form-day-report" method="post">
<?= csrf_field() ?>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>From Date</label>
<input type="date" class="form-control input-sm" id="date_from" name="date_from"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>To Date</label>
<input type="date" class="form-control input-sm" id="date_to" name="date_to"
value="<?= date('Y-m-d') ?>">
</div>
</div>
<div class="col-md-3">
<div class="form-group" style="margin-top:24px;">
<button type="submit" class="btn btn-info btn-sm">Generate Report</button>
</div>
</div>
</div>
</form>
<div id="day_report_result" class="row"></div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('form.form-day-report').on('submit', function (e) {
e.preventDefault();
$('#day_report_result').html('<p class="text-muted"><i class="fa fa-spinner fa-spin"></i> Loading...</p>');
// Day report endpoint to be implemented
});
});
</script>
</div>

View File

@ -0,0 +1,103 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div id="Medical_invoice_final">
<section class="content-header">
<h1>Indent <small>No. : <?= esc($invoice_stock_master[0]->indent_code ?? '') ?></small></h1>
</section>
<section class="content">
<?= csrf_field() ?>
<div class="box box-danger">
<div class="box-header">
<div class="row">
<input type="hidden" id="location_type" name="location_type" value="<?= $invoice_stock_master[0]->location_type ?? 0 ?>" />
<input type="hidden" id="location_id" name="location_id" value="<?= $invoice_stock_master[0]->location_id ?? 0 ?>" />
<input type="hidden" id="indent_id" name="indent_id" value="<?= $invoice_stock_master[0]->id ?? 0 ?>" />
<?php if (($invoice_stock_master[0]->location_id ?? 0) > 0) { ?>
<div class="col-md-12">
<p>
<strong>Location Name :</strong> <?= esc($invoice_stock_master[0]->issued_name) ?>
<strong>/ Indent No. :</strong> <?= esc($invoice_stock_master[0]->indent_code) ?>
<strong>/ Date :</strong> <?= date('d-m-Y', strtotime($invoice_stock_master[0]->indent_date)) ?>
</p>
</div>
<?php } ?>
</div>
</div>
<div class="box-body">
<div class="row" id="show_item_list">
<div class="col-md-12">
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>Item Code</th>
<th>Item Name</th>
<th>Formulation</th>
<th>Batch No</th>
<th>Exp.</th>
<th>Rate</th>
<th>Qty.</th>
<th>Price</th>
<th>Disc.</th>
<th>HSNCODE</th>
<th>CGST</th>
<th>SGST</th>
<th>Amount</th>
</tr>
</thead>
<tbody>
<?php
$srno = 0;
foreach ($inv_items as $row) {
$srno++;
?>
<tr>
<td><?= $srno ?></td>
<td><?= esc($row->item_code) ?></td>
<td><?= esc($row->item_Name) ?></td>
<td><?= esc($row->formulation) ?></td>
<td><?= esc($row->batch_no) ?></td>
<td><?= esc($row->expiry) ?></td>
<td><?= $row->price ?></td>
<td><?= $row->qty ?></td>
<td><?= $row->amount ?></td>
<td><?= $row->disc_amount ?></td>
<td><?= esc($row->HSNCODE) ?></td>
<td><?= $row->CGST ?? 0 ?></td>
<td><?= $row->SGST ?? 0 ?></td>
<td><?= $row->tamount ?></td>
</tr>
<?php
}
?>
</tbody>
<tfoot>
<tr>
<th colspan="11">Gross Total</th>
<th><?= $invoiceGtotal[0]->Gtotal ?? 0 ?></th>
<th><?= $invoiceGtotal[0]->tamt ?? 0 ?></th>
<th></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="row no-print">
<div class="col-xs-6">
<a href="/Storestock/print_single_indent/<?= $invoice_stock_master[0]->id ?>"
target="_blank" class="btn btn-default">
<i class="fa fa-print"></i> Print
</a>
</div>
</div>
</div>
</div>
</section>
<script>
$(document).ready(function () {
document.title = 'Indent : <?= esc($invoice_stock_master[0]->issued_name ?? '') ?> / <?= esc($invoice_stock_master[0]->indent_code ?? '') ?>';
});
</script>
</div>
</div>

View File

@ -0,0 +1,261 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<section class="content-header">
<h1>
Indent No. <?= esc($invoice_stock_master[0]->indent_code ?? '') ?>
<small>Edit</small>
</h1>
</section>
<section class="content">
<?= csrf_field() ?>
<div class="col-md-8" style="padding: 5px;">
<div class="box box-danger">
<div class="box-header">
<div class="row">
<input type="hidden" id="location_type" name="location_type" value="<?= $invoice_stock_master[0]->location_type ?? 0 ?>" />
<input type="hidden" id="location_id" name="location_id" value="<?= $invoice_stock_master[0]->location_id ?? 0 ?>" />
<input type="hidden" id="indent_id" name="indent_id" value="<?= $invoice_stock_master[0]->id ?? 0 ?>" />
<?php if (($invoice_stock_master[0]->location_id ?? 0) > 0) { ?>
<div class="col-md-12">
<p>
<strong>Location Name :</strong> <?= esc($invoice_stock_master[0]->issued_name) ?>
<strong>/ Indent No. :</strong> <?= esc($invoice_stock_master[0]->indent_code) ?>
<strong>/ Date :</strong> <?= date('d-m-Y', strtotime($invoice_stock_master[0]->indent_date)) ?>
</p>
</div>
<?php } ?>
</div>
</div>
<div class="box-body">
<div class="col-md-12">
<!-- Item List (AJAX-refreshable) -->
<div class="row">
<div id="show_item_list">
<?= $content ?>
</div>
</div>
<hr />
<div class="row">
<div id="show_add_item_list"></div>
</div>
<!-- Add Item Form -->
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="input_drug">Product Search:</label>
<input class="form-control input-sm" name="input_drug" id="input_drug"
placeholder="Item Code / Item Name" type="text" autocomplete="off">
</div>
</div>
<div class="col-md-4">
<span class="text-muted" id="input_product_code"></span>
</div>
</div>
<div class="row">
<div class="col-md-12">
<input type="hidden" id="l_ssno" name="l_ssno" value="0" />
<input type="hidden" id="item_code" name="item_code" value="0" />
<p class="text-red">
<span class="text-green" id="input_product_name"></span>
<span class="text-light-blue" id="input_batch"></span>
<span class="text-lead" id="input_product_mrp"></span>
<span class="text-yellow" id="stock_product_qty"></span>
</p>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label>Unit Rate</label>
<input class="form-control input-sm" id="input_product_unit_rate"
name="input_product_unit_rate" placeholder="Unit Rate" autocomplete="off" />
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Qty</label>
<input class="form-control number input-sm" id="input_product_qty"
name="input_product_qty" placeholder="Qty (Tabs/Units)" type="text"
value="0" autocomplete="off" />
<input type="hidden" id="hid_c_qty" name="hid_c_qty" value="0" />
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label>Disc %</label>
<input class="form-control number input-sm" id="input_disc"
name="input_disc" placeholder="Discount %" type="text"
value="0" autocomplete="off" />
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<button type="button" class="btn btn-primary" onclick="add_item_invoice()">Add</button>
</div>
</div>
</div>
<!-- /Add Item Form -->
</div>
</div>
<div class="box-footer">
<div class="row">
<div class="col-md-2">
<button type="button" class="btn btn-success" id="finalinvoice">Final Invoice</button>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-4" style="padding: 5px;">
<div class="box box-info">
<div class="box-header with-border">
<h4 class="box-title">Search Medicine</h4>
</div>
<div class="box-body" style="overflow:scroll;" id="search_body_part"></div>
<div class="box-footer" id="search_footer_part"></div>
</div>
</div>
</section>
<script>
$(document).ready(function () {
document.title = 'Indent : <?= esc($invoice_stock_master[0]->issued_name ?? '') ?> / <?= esc($invoice_stock_master[0]->indent_code ?? '') ?>';
// Final invoice button
$('#finalinvoice').click(function () {
var inv_id = $('#indent_id').val();
load_form_div('/Storestock/final_invoice/' + inv_id, 'maindiv');
});
// Autocomplete
$("#input_drug").autocomplete({
source: function (request, response) {
$.getJSON('/Storestock/get_drug', { term: request.term }, response);
},
minLength: 2,
select: function (event, ui) {
$('#input_product_code').text(ui.item.l_item_code);
$('#input_product_name').text(ui.item.value);
$('#input_batch').text(' | Batch: ' + ui.item.l_Batch);
$('#input_product_mrp').text(' | MRP: ' + ui.item.l_mrp);
$('#input_product_unit_rate').val(ui.item.l_unit_rate);
$('#stock_product_qty').text(' | Qty: ' + ui.item.l_c_qty);
$('#hid_c_qty').val(ui.item.l_c_qty);
$('#l_ssno').val(ui.item.l_ss_no);
$('#item_code').val(ui.item.l_item_code);
}
});
});
function add_item_invoice() {
var inv_id = $('#indent_id').val();
var l_ssno = $('#l_ssno').val();
var input_qty = $('#input_product_qty').val();
var hid_c_qty = $('#hid_c_qty').val();
var product_unit_rate = $('#input_product_unit_rate').val();
var csrf_value = $('input[name="<?= csrf_token() ?>"]').val();
var wait = parseInt($('#wait_for_next').val() || '0');
if (wait > 0) {
notify('error', 'Please Attention', 'Wait for last process');
return false;
}
if (parseInt(l_ssno) < 1) {
notify('error', 'Please Attention', 'Select Product First');
$('#input_drug').focus();
return false;
}
if (parseInt(input_qty) < 1) {
notify('error', 'Please Attention', 'Qty. is 0');
$('#input_product_qty').focus();
return false;
}
if (parseInt(input_qty) > parseInt(hid_c_qty)) {
notify('error', 'Please Attention', 'Product Qty. is less than available: ' + hid_c_qty);
$('#input_product_qty').focus();
return false;
}
$('#wait_for_next').val('1');
setTimeout(function () { $('#wait_for_next').val('0'); }, 1000);
$.post('/Storestock/add_item/1', {
"l_ssno" : l_ssno,
"qty" : input_qty,
"product_unit_rate" : product_unit_rate,
"disc" : $('#input_disc').val(),
"inv_id" : inv_id,
'<?= csrf_token() ?>' : csrf_value
}, function (data) {
// Reset form
$('#input_product_code').text('');
$('#input_product_name').text('');
$('#input_batch').text('');
$('#input_product_mrp').text('');
$('#input_product_unit_rate').val('');
$('#l_ssno').val(0);
$('#input_drug').val('');
$('#input_product_qty').val('0');
$('#input_disc').val('0');
$('#stock_product_qty').text('');
$('#input_drug').focus();
if (data.exist > 0) {
notify('warning', 'Please Attention', 'This Item Already in List');
}
if (data.insertid > 0) {
$('#show_item_list').html(data.content);
} else {
notify('error', 'Please Attention', data.error);
$('#wait_for_next').val('0');
}
// Refresh CSRF
$('input[name="<?= csrf_token() ?>"]').val(data.csrf_token_value);
}, 'json');
}
function update_qty(item_id) {
var new_qty = $('#input_qty_' + item_id).val();
var csrf_value = $('input[name="<?= csrf_token() ?>"]').val();
$.post('/Storestock/add_item/2', {
"itemid" : item_id,
"u_qty" : new_qty,
'<?= csrf_token() ?>' : csrf_value
}, function (data) {
if (data.insertid > 0) {
$('#show_item_list').html(data.content);
} else {
notify('error', 'Please Attention', data.error);
}
$('input[name="<?= csrf_token() ?>"]').val(data.csrf_token_value);
}, 'json');
}
function remove_item_invoice(item_id) {
if (!confirm('Are you sure you want to remove this item?')) return false;
var csrf_value = $('input[name="<?= csrf_token() ?>"]').val();
$.post('/Storestock/add_item/3', {
"itemid" : item_id,
'<?= csrf_token() ?>' : csrf_value
}, function (data) {
if (data.insertid > 0) {
$('#show_item_list').html(data.content);
} else {
notify('error', 'Please Attention', data.error || 'Delete failed');
}
$('input[name="<?= csrf_token() ?>"]').val(data.csrf_token_value);
}, 'json');
}
</script>
</div>

View File

@ -0,0 +1,98 @@
<?php
$item_codes = [];
foreach ($inv_stock_item as $row) {
$item_codes[] = $row->item_code;
}
$duplicate_codes = array_filter(
array_count_values($item_codes),
fn($c) => $c > 1
);
?>
<table class="table table-striped table-condensed table-hover">
<tr>
<th style="width:10px;">#</th>
<th>Item Name</th>
<th>Batch No</th>
<th>Exp.</th>
<th style="text-align:right;">Rate</th>
<th style="text-align:right;">S.Qty.</th>
<th>Qty.</th>
<th style="text-align:right;">Price</th>
<th style="text-align:right;">Amount</th>
<th></th>
</tr>
<?php
$srno = 0;
foreach ($inv_stock_item as $row) {
$srno++;
$style = '';
if (isset($row->sale_return) && $row->sale_return == 1) {
$style = 'style="background:orange;"';
}
if (isset($row->no_day) && $row->no_day < 90) {
$style = 'style="color:red;"';
}
if (isset($row->cur_qty) && $row->cur_qty < 6) {
$style = 'style="color:red;"';
}
$style_repeat = isset($duplicate_codes[$row->item_code]) ? 'style="color:green;"' : '';
?>
<tr <?= $style ?>>
<td><?= $srno ?></td>
<td <?= $style_repeat ?>><?= esc($row->item_Name) ?></td>
<td><?= esc($row->batch_no) ?></td>
<td><?= esc($row->expiry) ?></td>
<td style="text-align:right;"><?= $row->price ?></td>
<td style="text-align:right;"><?= $row->qty ?>
<input type="hidden" id="hid_oldqty_<?= $row->id ?>" value="<?= $row->qty ?>" />
</td>
<td>
<?php if (!isset($row->sale_return) || $row->sale_return == 0) { ?>
<div class="input-group input-group-sm">
<input type="text" class="form-control input-sm number"
style="width:50px;height:20px;padding:1px;"
id="input_qty_<?= $row->id ?>"
value="<?= $row->qty ?>" min="1">
<span class="input-group-btn">
<button type="button" class="btn btn-info btn-flat btn-xs"
style="width:20px;height:20px;padding:1px;"
onclick="update_qty(<?= $row->id ?>)">
<i class="fa fa-edit"></i>
</button>
</span>
</div>
<?php } ?>
</td>
<td style="text-align:right;"><?= $row->amount ?></td>
<td style="text-align:right;"><?= $row->tamount ?></td>
<td>
<button type="button" class="btn btn-danger btn-xs"
onclick="remove_item_invoice(<?= $row->id ?>)">
<i class="fa fa-remove"></i>
</button>
</td>
</tr>
<?php
}
?>
<input type="hidden" id="srno" name="srno" value="<?= $srno ?>" />
<input type="hidden" id="wait_for_next" name="wait_for_next" value="0" />
<!-- Totals -->
<tr>
<th>#</th><th></th><th></th><th></th><th></th><th></th>
<th>Gross Total</th>
<th style="text-align:right;"><?= $invoiceGtotal[0]->Gtotal ?? 0 ?></th>
<th style="text-align:right;"><?= $invoiceGtotal[0]->tamt ?? 0 ?></th>
<th></th>
</tr>
<tr>
<th>#</th><th></th><th></th><th></th><th></th>
<th>Tax Total</th>
<th colspan="2">TCGST : <?= $invoiceGtotal[0]->TCGST ?? 0 ?> / TSGST : <?= $invoiceGtotal[0]->TSGST ?? 0 ?></th>
<th></th><th></th>
</tr>
</table>

View File

@ -0,0 +1,99 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<div class="toolbar-row">
<div class="left">
<h3 class="box-title" style="margin-right:.65rem;">Indent List</h3>
<button type="button" class="pill-btn active">Live Queue</button>
<button type="button" class="pill-btn" onclick="$('#indent-grid').DataTable().ajax.reload();">Refresh</button>
</div>
<div class="right">
<a class="btn btn-primary btn-sm" href="javascript:load_form_div('/Storestock/new_indent','maindiv');">
<i class="fa fa-plus"></i> New Indent
</a>
</div>
</div>
</div>
<div class="box-body">
<div class="table-wrap">
<table class="table table-bordered table-striped" id="indent-grid" width="100%">
<thead>
<tr>
<th>Indent No.</th>
<th>Department</th>
<th>Date</th>
<th style="display:none;">ID</th>
</tr>
</thead>
<thead>
<tr>
<td><input type="text" data-column="0" class="form-control input-sm" placeholder="Indent No."></td>
<td><input type="text" data-column="1" class="form-control input-sm" placeholder="Department"></td>
<td><input type="date" data-column="2" class="form-control input-sm"></td>
<td style="display:none;"></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<script type="text/javascript">
$(document).ready(function () {
var dataTable = $('#indent-grid').DataTable({
"order": [[0, "desc"]],
"processing": true,
"serverSide": true,
"ajax": {
url: "Storestock/getIndentTable",
dataType: "json",
type: "post",
data: function (d) {
d['<?= csrf_token() ?>'] = '<?= csrf_hash() ?>';
},
error: function () {
$("#indent-grid_processing").css("display", "none");
$("#indent-grid tbody").html('<tr><td colspan="4">No data found in the server</td></tr>');
}
},
"columnDefs": [
{
targets: 3,
visible: false
},
{
targets: 0,
render: function (data, type, row) {
if (type === 'display') {
var url = "javascript:load_form_div('/Storestock/Indent_show/" +
encodeURIComponent(row[3]) + "','maindiv','Inv.:" +
encodeURIComponent(row[1]) + "/" + encodeURIComponent(row[3]) + " :Store');";
return '<a href="' + url + '">' + data + '</a>';
}
return data;
}
}
]
});
$("#indent-grid_filter").css("display", "none");
$('input[type=date]').on('input', function () {
var i = $(this).attr('data-column');
var v = $(this).val();
dataTable.columns(i).search(v).draw();
});
$('input[type=text]').on('input', function () {
var i = $(this).attr('data-column');
var v = $(this).val();
dataTable.columns(i).search(v).draw();
});
});
</script>
</div>

View File

@ -0,0 +1,47 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<section class="content-header">
<h1>Store Main <small>Panel</small></h1>
</section>
<section class="content">
<div class="module-hero">
<h4>Store Operations</h4>
<p>Use grouped controls to manage masters, inventory, and procurement operations.</p>
</div>
<div class="menu-grid">
<div class="menu-section">
<div class="title">Master Controls</div>
<div class="menu-list">
<a href="javascript:load_form_div('/Storestock/SupplierList','maindiv','Supplier : Store');">
<i class="fa fa-hand-pointer-o"></i> Supplier
</a>
<a href="javascript:load_form_div('/product_stock_master/drug_master_list','maindiv','Drug Master : Store');">
<i class="fa fa-hand-pointer-o"></i> Item Master
</a>
<a href="javascript:load_form_div('/product_master/company_master_list','maindiv','Drug Company : Store');">
<i class="fa fa-hand-pointer-o"></i> Company
</a>
<a href="javascript:load_form_div('/Storestock/location_master_list','maindiv','Location : Store');">
<i class="fa fa-hand-pointer-o"></i> Location Master
</a>
<a href="javascript:load_form_div('/Master_data/Employee_master_list','maindiv','Employee : Store');">
<i class="fa fa-hand-pointer-o"></i> Employee Master
</a>
</div>
</div>
<div class="menu-section">
<div class="title">Inventory Controls</div>
<div class="menu-list">
<a href="javascript:load_form_div('/Storestock/Purchase','maindiv','Purchase : Store');">
<i class="fa fa-hand-pointer-o"></i> Purchase
</a>
<a href="javascript:load_form_div('/Storestock/Purchase_return','maindiv','Purchase Return : Store');">
<i class="fa fa-hand-pointer-o"></i> Purchase Return
</a>
</div>
</div>
</div>
</section>
</div>

View File

@ -0,0 +1,92 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="col-md-12">
<div class="box">
<div class="box-header">
<h3 class="box-title">New Indent</h3>
</div>
<div class="box-body">
<?= csrf_field() ?>
<div class="module-hero" style="margin-bottom:.9rem;">
<h4>Create Indent Request</h4>
<p>Select date, then choose request origin: location or employee.</p>
</div>
<div class="row" style="margin-bottom:.9rem;">
<div class="col-md-4">
<label>Date</label>
<div class="input-group input-group-sm date">
<div class="input-group-addon"><i class="fa fa-calendar"></i></div>
<input class="form-control pull-right datepicker"
id="date_indent" name="date_indent" type="text"
data-inputmask="'alias': 'dd/mm/yyyy'" data-mask=""
value="<?= date('d/m/Y') ?>" />
</div>
</div>
</div>
<div class="panel-grid">
<div class="control-card">
<h5><i class="fa fa-map-marker"></i> By Location</h5>
<div class="form-group" style="margin-bottom:.5rem;">
<label>Select Location</label>
<select class="form-control select2" id="location_id" name="location_id">
<?php foreach ($location_master as $row) { ?>
<option value="<?= $row->l_id ?>"><?= esc($row->loc_name) ?></option>
<?php } ?>
</select>
</div>
<button type="button" class="btn btn-primary btn-sm" onclick="add_indent(1)">
<i class="fa fa-plus-circle"></i> Create from Location
</button>
<div class="help">Use when indent is raised for a ward/department location.</div>
</div>
<div class="control-card">
<h5><i class="fa fa-user"></i> By Employee</h5>
<div class="form-group" style="margin-bottom:.5rem;">
<label>Select Employee</label>
<select class="form-control select2" id="emp_id" name="emp_id">
<?php foreach ($employee_master as $row) { ?>
<option value="<?= $row->emp_id ?>"><?= esc($row->emp_code) ?> [<?= esc($row->emp_name) ?>]</option>
<?php } ?>
</select>
</div>
<button type="button" class="btn btn-danger btn-sm" onclick="add_indent(2)">
<i class="fa fa-plus-circle"></i> Create from Employee
</button>
<div class="help">Use when indent is raised in personal accountability mode.</div>
</div>
</div>
</div>
<div class="box-footer"></div>
</div>
</div>
<script>
$('.select2').select2();
function add_indent(location_type) {
var csrf_value = $('input[name="<?= csrf_token() ?>"]').val();
var loc_id = 0;
if (location_type == 1) {
loc_id = $('#location_id').val();
} else if (location_type == 2) {
loc_id = $('#emp_id').val();
}
if (loc_id > 0) {
$.post('/Storestock/Indent_create/' + location_type, {
"loc_id": loc_id,
"date_indent": $('#date_indent').val(),
'<?= csrf_token() ?>': csrf_value
}, function (data) {
$('#maindiv').html(data);
});
} else {
notify('error', 'Please Attention', 'Select a location or employee first');
}
}
</script>
</div>

View File

@ -0,0 +1,42 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<table class="table table-condensed table-hover">
<thead>
<tr>
<th>Pur. ID</th>
<th>Supplier</th>
<th>Inv.No / Date</th>
<th>Bat.No.</th>
<th>Exp.Dt.</th>
<th>MRP</th>
<th>Purchase</th>
<th>Qty / Packing</th>
<th>Unit Qty</th>
<th>Sale U.Qty</th>
<th>Lost Unit</th>
<th>Curr.U.Qty</th>
</tr>
</thead>
<tbody>
<?php foreach ($product_purchase_detail as $row) { ?>
<tr>
<td><?= esc($row->id) ?></td>
<td><?= esc($row->name_supplier) ?></td>
<td><?= esc($row->Invoice_no) ?><br /><?= esc($row->date_of_invoice) ?></td>
<td><?= esc($row->batch_no) ?></td>
<td><?= esc($row->expiry_date) ?></td>
<td><?= esc($row->mrp) ?></td>
<td><?= esc($row->purchase_price) ?></td>
<td><?= esc($row->tqty) ?> / <?= esc($row->packing) ?></td>
<td><?= esc($row->total_unit) ?></td>
<td><?= esc($row->total_sale_unit) ?></td>
<td><?= esc($row->total_lost_unit) ?></td>
<td><?= esc($row->cur_unit) ?></td>
</tr>
<?php } ?>
<?php if (empty($product_purchase_detail)) { ?>
<tr><td colspan="12" class="text-center text-muted">No stock records found.</td></tr>
<?php } ?>
</tbody>
</table>
</div>

View File

@ -0,0 +1,99 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<div class="row">
<div class="col-md-12">
<div id="stock_result_body" class="box-body" style="height:500px;overflow-y:auto;">
<table id="stock_result_table" class="table table-bordered table-striped TableData">
<thead>
<tr>
<th style="width:300px;">Item Name</th>
<th>Current Pak.</th>
<th>Current Unit Qty</th>
<th>Total Sale Pak.</th>
<th>Total Sale Unit Qty</th>
<th>Lost Unit</th>
<th>Package / Re-Order Qty</th>
<th>Detail</th>
</tr>
</thead>
<tbody>
<?php foreach ($stock_list as $row) { ?>
<tr>
<td>
<p class="text-danger">
<a href="javascript:void(0)"
onclick="loadProductStock(<?= (int)$row->id ?>, <?= (float)($row->packing ?? 1) ?>, '<?= esc($row->item_name) ?>')">
<?= esc($row->item_name) ?>
</a>
</p>
</td>
<td><p class="text-warning"><?= esc($row->C_Pak_Qty ?? '') ?></p></td>
<td><p class="text-warning"><?= round($row->C_Unit_Stock_Qty ?? 0, 2) ?></p></td>
<td><p class="text-warning"><?= round($row->C_Pak_Sale_Qty ?? 0, 2) ?></p></td>
<td><p class="text-warning"><?= round($row->sale_unit ?? 0, 2) ?></p></td>
<td><p class="text-danger"><?= round($row->total_lost_unit ?? 0, 2) ?></p></td>
<td><p class="text-warning"><?= esc($row->packing ?? '') ?> / <?= esc($row->re_order_qty ?? '') ?></p></td>
<td>
<button type="button" class="btn btn-primary btn-xs"
onclick="loadProductStock(<?= (int)$row->id ?>, <?= (float)($row->packing ?? 1) ?>, '<?= esc($row->item_name) ?>')">
Show
</button>
</td>
</tr>
<?php } ?>
</tbody>
<tfoot>
<tr>
<th style="width:300px;">Item Name</th>
<th>Current Pak.</th>
<th>Current Unit Qty</th>
<th>Total Sale Pak.</th>
<th>Total Sale Unit Qty</th>
<th>Lost Unit</th>
<th>Package / Re-Order Qty</th>
<th>Detail</th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<!-- Product Stock History Modal -->
<div class="modal fade" id="productStockModal" tabindex="-1" role="dialog" aria-labelledby="productStockModalLabel">
<div class="modal-dialog modal-lg" role="document" style="width:95%;">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="productStockModalLabel">Product Stock Detail</h4>
</div>
<div class="modal-body" style="overflow:auto;" id="productStockModalBody">
<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
if ($.fn.DataTable) {
$('#stock_result_table').DataTable({ "order": [[0, "asc"]] });
}
});
function loadProductStock(product_id, product_pak, product_name) {
$('#productStockModalLabel').text('Stock Detail : ' + product_name);
$('#productStockModalBody').html('<div class="text-center"><i class="fa fa-spinner fa-spin"></i> Loading...</div>');
$('#productStockModal').modal('show');
$.post('/Storestock/get_product_stock/' + product_id, {
product_pak: product_pak,
'<?= csrf_token() ?>': '<?= csrf_hash() ?>'
}, function (data) {
$('#productStockModalBody').html(data);
});
}
</script>
</div>

View File

@ -0,0 +1,104 @@
<style>
@page {
margin-top: 4.2cm;
margin-bottom: 1.2cm;
margin-left: 1cm;
margin-right: 0.5cm;
margin-header: 0.5cm;
margin-footer: 0.5cm;
header: html_myHeader;
footer: html_myFooter;
}
</style>
<htmlpageheader name="myHeader">
<table style="font-size:12px;" cellpadding="5">
<tr>
<td style="width:60%;vertical-align:top;">
<p align="center" style="font-size:25px;">
<?php echo defined('H_Name') ? constant('H_Name') : ''; ?>
</p>
<p align="center" style="font-size:12px;">
<?php echo defined('M_address') ? constant('M_address') : ''; ?>,
<?php echo defined('H_address_2') ? constant('H_address_2') : ''; ?>
</p>
</td>
<td style="width:40%;">
<p>
<strong>Location Name :</strong> <?= esc($invoice_stock_master[0]->issued_name ?? '') ?><br/>
<strong>Indent No. :</strong> <?= esc($invoice_stock_master[0]->indent_code ?? '') ?><br/>
<strong>Date :</strong> <?= esc($invoice_stock_master[0]->str_inv_date ?? '') ?>
</p>
</td>
</tr>
</table>
<h3 align="center">Indent</h3>
</htmlpageheader>
<htmlpagefooter name="myFooter">
<table width="100%" style="font-size:10px;">
<tr>
<td width="33%">Page : {PAGENO}/{nbpg}</td>
<td width="67%" style="text-align:right;">
Invoice No.: <?= esc($invoice_stock_master[0]->indent_code ?? '') ?>
/ Location : <?= esc($invoice_stock_master[0]->issued_name ?? '') ?>
</td>
</tr>
</table>
</htmlpagefooter>
<table style="font-size:10px;width:100%;border-collapse:collapse;border-style:solid;border-width:0.1mm;padding:3mm;" autosize="1">
<?php
$srno = 0;
$head_start = 0;
foreach ($inv_items as $row) {
if ($head_start == 0) {
echo '<tr style="border-style:solid;border-width:0.1mm;"><td colspan="7" style="font-size:18px;"><b>Invoice ID : </b>'
. esc($row->indent_code) . ' [<b>Dated : </b><i>' . esc($row->str_inv_date) . '</i>]</td></tr>';
echo '<tr>';
echo '<th style="width:20px;">#</th>';
echo '<th align="left" style="width:200px;">Item Name</th>';
echo '<th align="left">Batch No</th>';
echo '<th>Exp.</th>';
echo '<th align="right">Rate</th>';
echo '<th align="right">Qty.</th>';
echo '<th align="right">Gross Amt</th>';
echo '</tr>';
}
$srno++;
$head_start++;
if (isset($row->sale_return) && $row->sale_return == 1) {
echo '<tr><td colspan="7">Sale Return</td></tr>';
}
echo '<tr>';
echo '<td style="width:20px;">' . $srno . '</td>';
echo '<td style="width:200px;">' . esc($row->item_Name) . ' ' . esc($row->formulation) . '</td>';
echo '<td>' . esc($row->batch_no) . '</td>';
echo '<td>' . esc($row->expiry) . '</td>';
echo '<td align="right">' . $row->price . '</td>';
echo '<td align="right">' . $row->qty . '</td>';
echo '<td align="right">' . $row->amount . '</td>';
echo '</tr>';
}
if (count($invoice_stock_master) > 0) {
echo '<tr style="font-size:12px;border-style:solid;border-width:0.1mm;">';
echo '<th style="width:20px;">#</th>';
echo '<th colspan="5" align="right">Total</th>';
echo '<th align="right">' . ($invoice_stock_master[0]->gross_amount ?? '') . '</th>';
echo '</tr>';
}
?>
</table>
<br/><br/>
<table width="100%" style="font-size:12px;">
<tr>
<td style="width:60%;"></td>
<td style="width:40%;text-align:center;">
<b>For <?php echo defined('H_Name') ? constant('H_Name') : ''; ?></b>
</td>
</tr>
</table>

View File

@ -0,0 +1,101 @@
<?= $this->include('storestock/_niceadmin_theme') ?>
<div class="storestock-ui">
<section class="content-header">
<h1>Store Stock <small>Search</small></h1>
</section>
<section class="content">
<div class="module-hero">
<h4>Stock Visibility & Reorder Controls</h4>
<p>Filter by schedule, quickly export, and inspect product movement batch-wise.</p>
</div>
<div class="box">
<div class="box-body">
<form role="form" class="form-stock-search" method="post" action="/Storestock/store_Stock_result">
<?= csrf_field() ?>
<div class="search-bar">
<div>
<label style="display:block; margin-bottom:.3rem;">Stock Mode</label>
<label style="margin:0; font-weight:600; color:#012970;">
<input id="chk_reorder" name="chk_reorder" type="checkbox"> ReOrder List
</label>
</div>
<div>
<label style="display:block; margin-bottom:.3rem;">Drug Schedule</label>
<select class="form-control select2" id="schedule_id" name="schedule_id[]"
multiple="multiple" data-placeholder="Select a Schedule, Blank for All">
<option value="1">Schedule H</option>
<option value="2">Schedule H1</option>
<option value="3">Schedule X</option>
<option value="4">Schedule G</option>
<option value="5">Narcotic</option>
<option value="6">High Risk</option>
</select>
</div>
<div>
<label style="display:block; margin-bottom:.3rem;">Search Keyword</label>
<div class="input-group input-group-sm">
<input class="form-control" type="text" id="txtsearch" name="txtsearch"
placeholder="Item Name / Supplier Name">
<span class="input-group-btn">
<button type="submit" class="btn btn-primary btn-flat">Search</button>
</span>
</div>
</div>
</div>
<div class="toolbar-row" style="margin-top:.8rem; margin-bottom:0;">
<div class="left"></div>
<div class="right">
<button type="button" id="btn_excel" class="btn btn-info btn-sm">
<i class="fa fa-file-excel-o"></i> Excel Export
</button>
<button type="button" id="btn_excel_3" class="btn btn-default btn-sm">
<i class="fa fa-table"></i> Batch-wise Excel
</button>
</div>
</div>
</form>
</div>
</div>
<div class="box">
<div class="box-header">
<h3 class="box-title">Search Result</h3>
</div>
<div class="box-body">
<div class="searchresult" id="searchresult"></div>
</div>
</div>
</section>
<script>
$(document).ready(function () {
$('.select2').select2();
$('form.form-stock-search').on('submit', function (e) {
e.preventDefault();
$.post('/Storestock/store_Stock_result', $(this).serialize(), function (data) {
$('#searchresult').html(data);
});
});
$('#btn_excel').click(function () {
var reorder = $('#chk_reorder').is(':checked') ? '1' : '0';
var itemName = $('#txtsearch').val() || '-';
var schIds = $('#schedule_id').val();
var schStr = (schIds && schIds.length > 0) ? schIds.join('S') : '0';
window.open('/Storestock/Stock_result_excel/' + reorder + '/' + encodeURIComponent(itemName) + '/' + schStr, '_blank');
});
$('#btn_excel_3').click(function () {
var reorder = $('#chk_reorder').is(':checked') ? '1' : '0';
var itemName = $('#txtsearch').val() || '-';
window.open('/Storestock/Stock_result_excel_3/0/0/' + encodeURIComponent(itemName) + '/' + reorder, '_blank');
});
});
</script>
</div>