Finance: separate OPD Consult and Doctor Charges payout screens; compact dashboard cards; add IPD payout tracking migration

This commit is contained in:
DevSoftTech Server 2026-05-01 21:02:57 +05:30
parent 54c8543902
commit 7ab37ad80d
14 changed files with 2117 additions and 203 deletions

View File

@ -242,6 +242,7 @@ $routes->match(['get', 'post'], 'Medical/credit_payout_pool_datatable', 'Medical
$routes->get('Medical/credit_payout_request', 'Medical::credit_payout_request');
$routes->post('Medical/credit_payout_request_create', 'Medical::credit_payout_request_create');
$routes->get('Medical/credit_payout_requests', 'Medical::credit_payout_requests');
$routes->match(['get', 'post'], 'Medical/credit_payout_requests_datatable', 'Medical::credit_payout_requests_datatable');
$routes->get('Medical/credit_payout_request_detail/(:num)', 'Medical::credit_payout_request_detail/$1');
$routes->get('Payment_Medical', 'Payment_Medical::index');
@ -276,6 +277,12 @@ $routes->get('Finance/payout/opd-consult-drafts-table', 'Finance::payoutOpdConsu
$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/payout/doctor-charges', 'Finance::doctorChargesPayout');
$routes->get('Finance/payout/doctor-charges-summary', 'Finance::doctorChargesPayoutSummary');
$routes->get('Finance/payout/doctor-charges-drafts-table', 'Finance::doctorChargesPayoutDraftsTable');
$routes->post('Finance/payout/doctor-charges-draft-create', 'Finance::doctorChargesPayoutDraftCreate');
$routes->post('Finance/payout/doctor-charges-draft-update', 'Finance::doctorChargesPayoutDraftUpdate');
$routes->post('Finance/payout/doctor-charges-draft-delete', 'Finance::doctorChargesPayoutDraftDelete');
$routes->get('Finance/bank_audit_direct_payments_table', 'Finance::bankAuditDirectPaymentsTable');
$routes->get('Finance/bank_audit_outgoing_payments_table', 'Finance::bankAuditOutgoingPaymentsTable');
$routes->get('Finance/bank_statement_entries_table', 'Finance::bankStatementEntriesTable');

View File

@ -1858,6 +1858,48 @@ class Finance extends BaseController
]);
}
public function doctorChargesPayout()
{
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/doctor_charges_payout', [
'doctor_options' => $doctorOptions,
'state_unit_options' => $this->fetchOpdStateUnitOptions(),
]);
}
public function payoutOpdConsultSummary()
{
if (! $this->canFinanceAny([
@ -1876,6 +1918,7 @@ class Finance extends BaseController
$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') ?? ''));
$payoutSource = $this->normalizePayoutSource((string) ($this->request->getGet('payout_source') ?? 'consultation'));
if ($fromDate === '') {
$fromDate = date('Y-m-01');
@ -1887,7 +1930,40 @@ class Finance extends BaseController
[$fromDate, $toDate] = [$toDate, $fromDate];
}
return view('finance/partials/payout_opd_consult_summary', $this->buildOpdConsultPayoutSummaryData($fromDate, $toDate, $doctorId, $stateUnit));
return view('finance/partials/payout_opd_consult_summary', $this->buildPayoutSummaryDataForSource($payoutSource, $fromDate, $toDate, $doctorId, $stateUnit));
}
public function doctorChargesPayoutSummary()
{
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') ?? ''));
$payoutSource = $this->normalizePayoutSource((string) ($this->request->getGet('payout_source') ?? 'pathology'));
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/doctor_charges_payout_summary', $this->buildPayoutSummaryDataForSource($payoutSource, $fromDate, $toDate, $doctorId, $stateUnit));
}
public function payoutOpdConsultDraftsTable()
@ -1909,6 +1985,8 @@ class Finance extends BaseController
$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') ?? ''));
$payoutSource = $this->normalizePayoutSource((string) ($this->request->getGet('payout_source') ?? 'consultation'));
$payoutType = $this->payoutTypeFromSource($payoutSource);
if ($fromDate === '') {
$fromDate = date('Y-m-01');
@ -1923,7 +2001,7 @@ class Finance extends BaseController
$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_type', $payoutType)
->where('finance_doctor_payouts.payout_date >=', $fromDate)
->where('finance_doctor_payouts.payout_date <=', $toDate)
->orderBy('finance_doctor_payouts.id', 'DESC');
@ -1943,6 +2021,68 @@ class Finance extends BaseController
return view('finance/partials/payout_opd_consult_drafts_table', [
'rows' => $builder->findAll(30),
'source_type' => $payoutSource,
'source_label' => $this->payoutSourceLabel($payoutSource),
]);
}
public function doctorChargesPayoutDraftsTable()
{
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') ?? ''));
$payoutSource = $this->normalizePayoutSource((string) ($this->request->getGet('payout_source') ?? 'pathology'));
$payoutType = $this->payoutTypeFromSource($payoutSource);
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', $payoutType)
->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/doctor_charges_payout_drafts_table', [
'rows' => $builder->findAll(30),
'source_type' => $payoutSource,
'source_label' => $this->payoutSourceLabel($payoutSource),
]);
}
@ -1969,12 +2109,28 @@ class Finance extends BaseController
$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') ?? ''));
$payoutSource = $this->normalizePayoutSource((string) ($this->request->getPost('payout_source') ?? 'consultation'));
$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 ($payoutSource !== 'consultation') {
return $this->createChargeInvoicePayoutDraft(
$payoutSource,
$doctorId,
$fromDate,
$toDate,
$stateUnit,
$baseAmount,
$doctorShare,
$hospitalShare,
$deductions,
$adjustments
);
}
if ($doctorId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Please select a doctor before creating payout draft.']);
}
@ -2019,7 +2175,7 @@ class Finance extends BaseController
$caseReference .= '-U' . $this->normalizeStateUnitToken($stateUnit);
}
$remarks = 'OPD payout draft from OPD Consult Payout | Completed=' . $completedOpd
$remarks = 'OPD payout draft from Hospital Doctor Payout | Completed=' . $completedOpd
. ', Base=' . number_format($baseAmount, 2, '.', '')
. ', Doc%=' . number_format($doctorShare, 2, '.', '')
. ', Hosp%=' . number_format($hospitalShare, 2, '.', '')
@ -2077,6 +2233,11 @@ class Finance extends BaseController
]);
}
public function doctorChargesPayoutDraftCreate()
{
return $this->payoutOpdConsultDraftCreate();
}
public function payoutOpdConsultDraftUpdate()
{
if (! $this->canFinanceAny([
@ -2133,6 +2294,11 @@ class Finance extends BaseController
]);
}
public function doctorChargesPayoutDraftUpdate()
{
return $this->payoutOpdConsultDraftUpdate();
}
public function payoutOpdConsultDraftDelete()
{
if (! $this->canFinanceAny([
@ -2164,20 +2330,39 @@ class Finance extends BaseController
$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;
}
$payoutType = strtolower(trim((string) ($row['payout_type'] ?? 'consultation')));
if ($payoutType === 'consultation') {
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->db->table('opd_master')
->where('payout_draft_id', $payoutId)
->update($clearData);
}
} else {
if (method_exists($this->db, 'tableExists') && $this->db->tableExists('invoice_item') && $this->tableHasField('invoice_item', 'payout_draft_id')) {
$clearData = [
'payout_draft_id' => null,
];
if ($this->tableHasField('invoice_item', 'payout_calculated_at')) {
$clearData['payout_calculated_at'] = null;
}
if ($this->tableHasField('invoice_item', 'payout_calculated_by')) {
$clearData['payout_calculated_by'] = null;
}
$this->db->table('invoice_item')
->where('payout_draft_id', $payoutId)
->update($clearData);
}
}
$this->doctorPayoutModel->delete($payoutId);
@ -2190,10 +2375,15 @@ class Finance extends BaseController
return $this->response->setJSON([
'status' => 1,
'message' => 'Payout draft deleted and linked OPD records unlocked for recalculation.',
'message' => 'Payout draft deleted and linked records unlocked for recalculation.',
]);
}
public function doctorChargesPayoutDraftDelete()
{
return $this->payoutOpdConsultDraftDelete();
}
public function phase2()
{
if (! $this->canFinance('finance.access')) {
@ -4517,6 +4707,567 @@ class Finance extends BaseController
];
}
private function normalizePayoutSource(string $source): string
{
$source = strtolower(trim($source));
$allowed = ['consultation', 'pathology', 'radiology', 'other_items'];
return in_array($source, $allowed, true) ? $source : 'consultation';
}
private function payoutTypeFromSource(string $source): string
{
return match ($this->normalizePayoutSource($source)) {
'pathology' => 'pathology',
'radiology' => 'radiology',
'other_items' => 'other_items',
default => 'consultation',
};
}
private function payoutSourceLabel(string $source): string
{
return match ($this->normalizePayoutSource($source)) {
'pathology' => 'Pathology Charges',
'radiology' => 'Radiology Charges',
'other_items' => 'Other Charge Items',
default => 'OPD Consultation',
};
}
private function buildPayoutSummaryDataForSource(string $source, string $fromDate, string $toDate, int $doctorId = 0, string $stateUnit = ''): array
{
$source = $this->normalizePayoutSource($source);
if ($source === 'consultation') {
$payload = $this->buildOpdConsultPayoutSummaryData($fromDate, $toDate, $doctorId, $stateUnit);
$payload['source_type'] = $source;
$payload['source_label'] = $this->payoutSourceLabel($source);
$payload['scope_label'] = 'Completed OPDs';
$payload['unit_label'] = 'OPDs';
return $payload;
}
$payload = $this->buildChargeInvoicePayoutSummaryData($source, $fromDate, $toDate, $doctorId, $stateUnit);
$payload['source_type'] = $source;
$payload['source_label'] = $this->payoutSourceLabel($source);
$payload['scope_label'] = 'Eligible Charge Invoices';
$payload['unit_label'] = 'Invoices';
return $payload;
}
private function resolveInvoiceDoctorColumn(): ?string
{
$candidates = ['doctor_id', 'doc_id', 'dr_id'];
foreach ($candidates as $column) {
if ($this->tableHasField('invoice_master', $column)) {
return $column;
}
}
return null;
}
private function resolveInvoiceStateUnitColumn(): ?string
{
$candidates = ['unit_name', 'unit', 'state_name', 'state', 'branch_name', 'branch', 'center_name', 'location', 'city'];
foreach ($candidates as $column) {
if ($this->tableHasField('invoice_master', $column)) {
return $column;
}
}
return null;
}
private function applyChargeCategoryFilter($builder, string $source, string $itemTypeColumn): void
{
$source = $this->normalizePayoutSource($source);
if (! $this->tableHasField('invoice_item', 'item_type')) {
if ($source !== 'other_items') {
$builder->where('1 = 0', null, false);
}
return;
}
if ($source === 'pathology') {
$builder->whereIn($itemTypeColumn, [5, 30]);
return;
}
if ($source === 'radiology') {
$builder->whereIn($itemTypeColumn, [1, 2, 3, 4, 6]);
return;
}
if ($source === 'other_items') {
$builder->groupStart()
->where($itemTypeColumn . ' IS NULL', null, false)
->orWhereNotIn($itemTypeColumn, [1, 2, 3, 4, 5, 6, 30])
->groupEnd();
}
}
private function fetchEligibleChargeItemsForPayout(string $source, string $fromDate, string $toDate, int $doctorId, string $stateUnit = ''): array
{
if (! method_exists($this->db, 'tableExists') || ! $this->db->tableExists('invoice_master') || ! $this->db->tableExists('invoice_item')) {
return ['item_ids' => [], 'invoice_ids' => []];
}
$doctorColumn = $this->resolveInvoiceDoctorColumn();
if ($doctorColumn === null) {
return ['item_ids' => [], 'invoice_ids' => []];
}
$stateColumn = $this->resolveInvoiceStateUnitColumn();
$itemAmountExpr = $this->tableHasField('invoice_item', 'item_amount')
? 'COALESCE(ii.item_amount, 0)'
: 'COALESCE(ii.item_rate,0) * COALESCE(ii.item_qty,0)';
$builder = $this->db->table('invoice_item ii')
->select('ii.id AS item_id, ii.inv_master_id AS invoice_id')
->join('invoice_master inv', 'inv.id = ii.inv_master_id', 'inner')
->where('DATE(inv.inv_date) >=', $fromDate)
->where('DATE(inv.inv_date) <=', $toDate)
->where($itemAmountExpr . ' > 0', null, false);
if ($doctorId > 0) {
$builder->where('inv.' . $doctorColumn, $doctorId);
}
if ($stateUnit !== '' && $stateColumn !== null) {
$builder->where('inv.' . $stateColumn, $stateUnit);
}
$this->applyChargeCategoryFilter($builder, $source, 'ii.item_type');
if ($this->tableHasField('invoice_item', 'payout_draft_id')) {
$builder->groupStart()
->where('ii.payout_draft_id IS NULL', null, false)
->orWhere('ii.payout_draft_id', 0)
->groupEnd();
}
$rows = $builder->get()->getResultArray();
$itemIds = [];
$invoiceIds = [];
foreach ($rows as $row) {
$itemId = (int) ($row['item_id'] ?? 0);
$invoiceId = (int) ($row['invoice_id'] ?? 0);
if ($itemId > 0) {
$itemIds[] = $itemId;
}
if ($invoiceId > 0) {
$invoiceIds[] = $invoiceId;
}
}
return [
'item_ids' => array_values(array_unique($itemIds)),
'invoice_ids' => array_values(array_unique($invoiceIds)),
];
}
private function buildChargeInvoicePayoutSummaryData(string $source, string $fromDate, string $toDate, int $doctorId = 0, string $stateUnit = ''): array
{
$db = \Config\Database::connect();
$source = $this->normalizePayoutSource($source);
$doctorColumn = $this->resolveInvoiceDoctorColumn();
$stateColumn = $this->resolveInvoiceStateUnitColumn();
$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,
];
$doctorBreakdown = [];
$doctorName = 'All Doctors';
if ($doctorId > 0 && $db->tableExists('doctor_master')) {
$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 ?? '')));
}
}
if ($doctorColumn === null || ! $db->tableExists('invoice_master') || ! $db->tableExists('invoice_item')) {
return [
'summary' => $summary,
'doctor_breakdown' => [],
'from_date' => $fromDate,
'to_date' => $toDate,
'doctor_name' => $doctorName,
'state_unit' => $stateUnit,
'state_unit_label' => $stateColumn !== null ? ucwords(str_replace('_', ' ', $stateColumn)) : 'State/Unit',
];
}
$itemAmountExpr = $this->tableHasField('invoice_item', 'item_amount')
? 'COALESCE(ii.item_amount, 0)'
: 'COALESCE(ii.item_rate,0) * COALESCE(ii.item_qty,0)';
$breakdownBuilder = $db->table('invoice_item ii')
->select('inv.' . $doctorColumn . ' AS doc_id', false)
->select("COALESCE(NULLIF(TRIM(CONCAT(COALESCE(dm.p_title,''), ' ', COALESCE(dm.p_fname,''))), ''), CONCAT('Doctor #', inv." . $doctorColumn . ")) AS doctor_name", false)
->select('COUNT(DISTINCT inv.id) AS completed_opd', false)
->select('SUM(' . $itemAmountExpr . ') AS gross_amount', false)
->join('invoice_master inv', 'inv.id = ii.inv_master_id', 'inner')
->join('doctor_master dm', 'dm.id = inv.' . $doctorColumn, 'left')
->where('DATE(inv.inv_date) >=', $fromDate)
->where('DATE(inv.inv_date) <=', $toDate)
->where($itemAmountExpr . ' > 0', null, false);
if ($doctorId > 0) {
$breakdownBuilder->where('inv.' . $doctorColumn, $doctorId);
}
if ($stateUnit !== '' && $stateColumn !== null) {
$breakdownBuilder->where('inv.' . $stateColumn, $stateUnit);
}
$this->applyChargeCategoryFilter($breakdownBuilder, $source, 'ii.item_type');
if ($this->tableHasField('invoice_item', 'payout_draft_id')) {
$breakdownBuilder->groupStart()
->where('ii.payout_draft_id IS NULL', null, false)
->orWhere('ii.payout_draft_id', 0)
->groupEnd();
}
$doctorBreakdown = $breakdownBuilder
->groupBy('inv.' . $doctorColumn)
->orderBy('doctor_name', 'ASC')
->get()
->getResultArray();
foreach ($doctorBreakdown as &$row) {
$row['completed_opd'] = (int) ($row['completed_opd'] ?? 0);
$row['routine_opd'] = 0;
$row['emergency_opd'] = 0;
$row['running_opd'] = 0;
$row['new_opd'] = 0;
$row['gross_amount'] = (float) ($row['gross_amount'] ?? 0);
$row['org_credit_amount'] = 0.0;
$row['cash_received'] = 0.0;
$row['bank_received'] = 0.0;
$row['total_received'] = 0.0;
$row['credit_amount'] = 0.0;
$row['payout_amount'] = 0.0;
$summary['completed_opd'] += $row['completed_opd'];
$summary['gross_amount'] += $row['gross_amount'];
}
unset($row);
$summary['doctor_count'] = count($doctorBreakdown);
if ($this->tableHasField('invoice_item', 'payout_draft_id')) {
$calcBuilder = $db->table('invoice_item ii')
->select('COUNT(DISTINCT inv.id) AS calculated_opd', false)
->join('invoice_master inv', 'inv.id = ii.inv_master_id', 'inner')
->where('DATE(inv.inv_date) >=', $fromDate)
->where('DATE(inv.inv_date) <=', $toDate)
->where($itemAmountExpr . ' > 0', null, false)
->where('ii.payout_draft_id >', 0);
if ($doctorId > 0) {
$calcBuilder->where('inv.' . $doctorColumn, $doctorId);
}
if ($stateUnit !== '' && $stateColumn !== null) {
$calcBuilder->where('inv.' . $stateColumn, $stateUnit);
}
$this->applyChargeCategoryFilter($calcBuilder, $source, 'ii.item_type');
$calcRow = $calcBuilder->get()->getRowArray();
$summary['calculated_opd'] = (int) ($calcRow['calculated_opd'] ?? 0);
}
$eligible = $this->fetchEligibleChargeItemsForPayout($source, $fromDate, $toDate, $doctorId, $stateUnit);
if (! empty($eligible['invoice_ids'])) {
$invoiceIds = $eligible['invoice_ids'];
$categoryRows = $db->table('invoice_item ii')
->select('ii.inv_master_id AS invoice_id, SUM(' . $itemAmountExpr . ') AS category_total', false)
->whereIn('ii.inv_master_id', $invoiceIds)
->where($itemAmountExpr . ' > 0', null, false);
$this->applyChargeCategoryFilter($categoryRows, $source, 'ii.item_type');
$categoryRows = $categoryRows->groupBy('ii.inv_master_id')->get()->getResultArray();
$invoiceTotals = $db->table('invoice_item ii')
->select('ii.inv_master_id AS invoice_id, SUM(' . $itemAmountExpr . ') AS invoice_total', false)
->whereIn('ii.inv_master_id', $invoiceIds)
->where($itemAmountExpr . ' > 0', null, false)
->groupBy('ii.inv_master_id')
->get()->getResultArray();
$payments = $db->table('payment_history p')
->select('p.payof_id AS invoice_id', false)
->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)
->where('p.payof_type', 2)
->whereIn('p.payof_id', $invoiceIds)
->where('DATE(p.payment_date) >=', $fromDate)
->where('DATE(p.payment_date) <=', $toDate)
->groupBy('p.payof_id')
->get()->getResultArray();
$invoiceToDocRows = $db->table('invoice_master inv')
->select('inv.id AS invoice_id, inv.' . $doctorColumn . ' AS doc_id', false)
->whereIn('inv.id', $invoiceIds)
->get()->getResultArray();
$categoryMap = [];
foreach ($categoryRows as $row) {
$categoryMap[(int) ($row['invoice_id'] ?? 0)] = (float) ($row['category_total'] ?? 0);
}
$invoiceTotalMap = [];
foreach ($invoiceTotals as $row) {
$invoiceTotalMap[(int) ($row['invoice_id'] ?? 0)] = (float) ($row['invoice_total'] ?? 0);
}
$paymentMap = [];
foreach ($payments as $row) {
$paymentMap[(int) ($row['invoice_id'] ?? 0)] = [
'total_received' => (float) ($row['total_received'] ?? 0),
'cash_received' => (float) ($row['cash_received'] ?? 0),
'bank_received' => (float) ($row['bank_received'] ?? 0),
];
}
$invoiceDocMap = [];
foreach ($invoiceToDocRows as $row) {
$invoiceDocMap[(int) ($row['invoice_id'] ?? 0)] = (int) ($row['doc_id'] ?? 0);
}
$perDoctorReceived = [];
foreach ($invoiceIds as $invoiceId) {
$invoiceId = (int) $invoiceId;
$docId = (int) ($invoiceDocMap[$invoiceId] ?? 0);
if ($docId <= 0) {
continue;
}
$categoryTotal = (float) ($categoryMap[$invoiceId] ?? 0);
$invoiceTotal = (float) ($invoiceTotalMap[$invoiceId] ?? 0);
if ($categoryTotal <= 0 || $invoiceTotal <= 0) {
continue;
}
$ratio = $categoryTotal / $invoiceTotal;
$recv = $paymentMap[$invoiceId] ?? ['total_received' => 0.0, 'cash_received' => 0.0, 'bank_received' => 0.0];
$perDoctorReceived[$docId]['total_received'] = (float) ($perDoctorReceived[$docId]['total_received'] ?? 0) + ($recv['total_received'] * $ratio);
$perDoctorReceived[$docId]['cash_received'] = (float) ($perDoctorReceived[$docId]['cash_received'] ?? 0) + ($recv['cash_received'] * $ratio);
$perDoctorReceived[$docId]['bank_received'] = (float) ($perDoctorReceived[$docId]['bank_received'] ?? 0) + ($recv['bank_received'] * $ratio);
}
foreach ($doctorBreakdown as &$row) {
$docId = (int) ($row['doc_id'] ?? 0);
$recv = $perDoctorReceived[$docId] ?? ['total_received' => 0.0, 'cash_received' => 0.0, 'bank_received' => 0.0];
$row['total_received'] = round((float) ($recv['total_received'] ?? 0), 2);
$row['cash_received'] = round((float) ($recv['cash_received'] ?? 0), 2);
$row['bank_received'] = round((float) ($recv['bank_received'] ?? 0), 2);
$row['credit_amount'] = max(0, round((float) $row['gross_amount'] - (float) $row['total_received'], 2));
$summary['cash_received'] += (float) $row['cash_received'];
$summary['bank_received'] += (float) $row['bank_received'];
}
unset($row);
}
$summary['total_received'] = round((float) ($summary['cash_received'] + $summary['bank_received']), 2);
$summary['credit_amount'] = max(0, round((float) $summary['gross_amount'] - (float) $summary['total_received'], 2));
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_type', $this->payoutTypeFromSource($source))
->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' => $stateColumn !== null ? ucwords(str_replace('_', ' ', $stateColumn)) : 'State/Unit',
];
}
private function createChargeInvoicePayoutDraft(
string $source,
int $doctorId,
string $fromDate,
string $toDate,
string $stateUnit,
float $baseAmount,
float $doctorShare,
float $hospitalShare,
float $deductions,
float $adjustments
) {
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];
}
$source = $this->normalizePayoutSource($source);
$summaryData = $this->buildChargeInvoicePayoutSummaryData($source, $fromDate, $toDate, $doctorId, $stateUnit);
$summary = (array) ($summaryData['summary'] ?? []);
$eligible = $this->fetchEligibleChargeItemsForPayout($source, $fromDate, $toDate, $doctorId, $stateUnit);
$eligibleItemIds = $eligible['item_ids'] ?? [];
$eligibleInvoiceIds = $eligible['invoice_ids'] ?? [];
$eligibleUnits = count($eligibleInvoiceIds);
if ($eligibleUnits <= 0) {
return $this->response->setStatusCode(422)->setJSON([
'status' => 0,
'message' => 'No eligible ' . strtolower($this->payoutSourceLabel($source)) . ' found for selected filters.',
]);
}
if ($baseAmount <= 0) {
$baseAmount = (float) ($summary['total_received'] ?? 0);
if ($baseAmount <= 0) {
$baseAmount = (float) ($summary['gross_amount'] ?? 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, $eligibleUnits), 2);
$token = match ($source) {
'pathology' => 'PATH',
'radiology' => 'RAD',
'other_items' => 'OTH',
default => 'CHG',
};
$caseReference = 'CHG-' . $token . '-' . str_replace('-', '', $fromDate) . '-' . str_replace('-', '', $toDate) . '-D' . $doctorId;
if ($stateUnit !== '') {
$caseReference .= '-U' . $this->normalizeStateUnitToken($stateUnit);
}
$remarks = $this->payoutSourceLabel($source) . ' payout draft | Invoices=' . $eligibleUnits
. ', Items=' . count($eligibleItemIds)
. ', 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' => $this->payoutTypeFromSource($source),
'units' => $eligibleUnits,
'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($eligibleItemIds) && $this->tableHasField('invoice_item', 'payout_draft_id')) {
$itemUpdate = [
'payout_draft_id' => $insertId,
];
if ($this->tableHasField('invoice_item', 'payout_calculated_at')) {
$itemUpdate['payout_calculated_at'] = date('Y-m-d H:i:s');
}
if ($this->tableHasField('invoice_item', 'payout_calculated_by')) {
$itemUpdate['payout_calculated_by'] = $this->currentUserName();
}
if ($this->tableHasField('invoice_item', 'updated_at')) {
$itemUpdate['updated_at'] = date('Y-m-d H:i:s');
}
$this->db->table('invoice_item')
->whereIn('id', $eligibleItemIds)
->update($itemUpdate);
}
$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,
]);
}
private function fetchOpdStateUnitOptions(): array
{
$column = $this->resolveOpdStateUnitColumn();

View File

@ -11477,6 +11477,165 @@ class Medical extends BaseController
]);
}
public function credit_payout_requests_datatable()
{
if ($deny = $this->ensurePharmacyAccess()) {
return $deny;
}
$draw = (int) ($this->request->getPost('draw') ?? $this->request->getGet('draw') ?? 1);
$start = max(0, (int) ($this->request->getPost('start') ?? $this->request->getGet('start') ?? 0));
$length = (int) ($this->request->getPost('length') ?? $this->request->getGet('length') ?? 25);
if ($length <= 0) {
$length = 25;
}
if ($length > 200) {
$length = 200;
}
if (! $this->db->tableExists('finance_payout_requests')) {
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => 0,
'recordsFiltered' => 0,
'data' => [],
'status_summary' => [
'total' => 0,
'submitted' => 0,
'finance_review' => 0,
'approved' => 0,
'partially_paid' => 0,
'paid' => 0,
'rejected' => 0,
],
]);
}
$searchRaw = $this->request->getPost('search');
if ($searchRaw === null) {
$searchRaw = $this->request->getGet('search');
}
$searchValue = '';
if (is_array($searchRaw)) {
$searchValue = trim((string) ($searchRaw['value'] ?? ''));
} elseif (is_string($searchRaw)) {
$searchValue = trim($searchRaw);
}
$orderRaw = $this->request->getPost('order');
if ($orderRaw === null) {
$orderRaw = $this->request->getGet('order');
}
$order = is_array($orderRaw) ? $orderRaw : [];
$columnsRaw = $this->request->getPost('columns');
if ($columnsRaw === null) {
$columnsRaw = $this->request->getGet('columns');
}
$columns = is_array($columnsRaw) ? $columnsRaw : [];
$builder = $this->db->table('finance_payout_requests pr')
->where('pr.request_type', 'medical_store_credit');
$recordsTotal = (int) $builder->countAllResults(false);
if ($searchValue !== '') {
$builder
->groupStart()
->like('pr.request_no', $searchValue)
->orLike('pr.status', $searchValue)
->orLike('pr.request_date', $searchValue)
->orLike('pr.created_by', $searchValue)
->groupEnd();
}
$recordsFiltered = (int) $builder->countAllResults(false);
$orderableMap = [
'request_no' => 'pr.request_no',
'request_date' => 'pr.request_date',
'status' => 'pr.status',
'requested_amount' => 'pr.requested_amount',
'paid_amount' => 'pr.paid_amount',
'pending_amount' => 'pr.pending_amount',
'created_by' => 'pr.created_by',
];
$appliedOrder = false;
if (! empty($order)) {
foreach ($order as $ord) {
$colIndex = (int) ($ord['column'] ?? -1);
$dir = strtolower((string) ($ord['dir'] ?? 'desc')) === 'asc' ? 'ASC' : 'DESC';
$colData = '';
if ($colIndex >= 0 && isset($columns[$colIndex]) && is_array($columns[$colIndex])) {
$colData = (string) ($columns[$colIndex]['data'] ?? '');
}
if ($colData !== '' && isset($orderableMap[$colData])) {
$builder->orderBy($orderableMap[$colData], $dir);
$appliedOrder = true;
}
}
}
if (! $appliedOrder) {
$builder->orderBy('pr.id', 'DESC');
}
$rows = $builder
->select('pr.id, pr.request_no, pr.request_date, pr.status, pr.requested_amount, pr.paid_amount, pr.pending_amount, pr.created_by')
->limit($length, $start)
->get()
->getResultArray();
$data = [];
foreach ($rows as $i => $row) {
$data[] = [
'row_no' => $start + $i + 1,
'id' => (int) ($row['id'] ?? 0),
'request_no' => (string) ($row['request_no'] ?? ''),
'request_date' => (string) ($row['request_date'] ?? ''),
'status' => (string) ($row['status'] ?? ''),
'requested_amount' => round((float) ($row['requested_amount'] ?? 0), 2),
'paid_amount' => round((float) ($row['paid_amount'] ?? 0), 2),
'pending_amount' => round((float) ($row['pending_amount'] ?? 0), 2),
'created_by' => (string) ($row['created_by'] ?? ''),
];
}
$summaryRows = $this->db->table('finance_payout_requests')
->select('LOWER(status) AS status_key, COUNT(1) AS cnt', false)
->where('request_type', 'medical_store_credit')
->groupBy('LOWER(status)')
->get()
->getResultArray();
$statusSummary = [
'total' => 0,
'submitted' => 0,
'finance_review' => 0,
'approved' => 0,
'partially_paid' => 0,
'paid' => 0,
'rejected' => 0,
];
foreach ($summaryRows as $sr) {
$key = strtolower(trim((string) ($sr['status_key'] ?? '')));
$cnt = (int) ($sr['cnt'] ?? 0);
$statusSummary['total'] += $cnt;
if (array_key_exists($key, $statusSummary)) {
$statusSummary[$key] += $cnt;
}
}
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => $recordsTotal,
'recordsFiltered' => $recordsFiltered,
'data' => $data,
'status_summary' => $statusSummary,
]);
}
public function credit_payout_request_detail($requestId = 0)
{
if ($deny = $this->ensurePharmacyAccess()) {

View File

@ -0,0 +1,92 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddInvoiceItemPayoutTrackingFields 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('invoice_item')) {
return;
}
$fields = $this->getTableFields('invoice_item');
if (! in_array('payout_draft_id', $fields, true)) {
$this->forge->addColumn('invoice_item', [
'payout_draft_id' => [
'type' => 'INT',
'constraint' => 11,
'unsigned' => true,
'null' => true,
'after' => 'inv_master_id',
],
]);
}
if (! in_array('payout_calculated_at', $fields, true)) {
$this->forge->addColumn('invoice_item', [
'payout_calculated_at' => [
'type' => 'DATETIME',
'null' => true,
'after' => 'payout_draft_id',
],
]);
}
if (! in_array('payout_calculated_by', $fields, true)) {
$this->forge->addColumn('invoice_item', [
'payout_calculated_by' => [
'type' => 'VARCHAR',
'constraint' => 120,
'null' => true,
'after' => 'payout_calculated_at',
],
]);
}
}
public function down()
{
if (! $this->hasTable('invoice_item')) {
return;
}
$fields = $this->getTableFields('invoice_item');
if (in_array('payout_calculated_by', $fields, true)) {
$this->forge->dropColumn('invoice_item', 'payout_calculated_by');
}
if (in_array('payout_calculated_at', $fields, true)) {
$this->forge->dropColumn('invoice_item', 'payout_calculated_at');
}
if (in_array('payout_draft_id', $fields, true)) {
$this->forge->dropColumn('invoice_item', 'payout_draft_id');
}
}
}

View File

@ -0,0 +1,208 @@
<section class="content finance-doctor-charges-payout">
<div class="mb-3">
<h2 class="mb-1">Doctor Charges Payout</h2>
<p class="text-muted mb-0">Calculate and manage doctor payout from Pathology, Radiology, and Other charge invoices.</p>
</div>
<div id="dc_payout_alert"></div>
<div class="card border-0 shadow-sm">
<div class="card-body">
<div class="row g-3">
<div class="col-md-3 col-6">
<label class="form-label form-label-sm">From Date</label>
<input type="date" class="form-control form-control-sm" id="dc_payout_from_date" value="<?= date('Y-m-01') ?>">
</div>
<div class="col-md-3 col-6">
<label class="form-label form-label-sm">To Date</label>
<input type="date" class="form-control form-control-sm" id="dc_payout_to_date" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-3 col-6">
<label class="form-label form-label-sm">Doctor</label>
<select class="form-select form-select-sm" id="dc_payout_doctor_id" name="doctor_id">
<option value="">Select Doctor</option>
<?php foreach (($doctor_options ?? []) as $doctor): ?>
<option value="<?= (int) ($doctor['id'] ?? 0) ?>"><?= esc((string) ($doctor['name'] ?? '')) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 col-6">
<label class="form-label form-label-sm">State/Unit</label>
<select class="form-select form-select-sm" id="dc_payout_state_unit" name="state_unit">
<option value="">All State/Unit</option>
<?php foreach (($state_unit_options ?? []) as $opt): ?>
<option value="<?= esc((string) $opt) ?>"><?= esc((string) $opt) ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 col-6">
<label class="form-label form-label-sm">Charge Source</label>
<select class="form-select form-select-sm" id="dc_payout_source" name="payout_source">
<option value="pathology">Pathology Charges</option>
<option value="radiology">Radiology Charges</option>
<option value="other_items">Other Charge Items</option>
</select>
</div>
<div class="col-12 d-flex justify-content-end">
<button type="button" class="btn btn-primary btn-sm" onclick="refreshDcPayoutSummary()">
<i class="bi bi-arrow-repeat me-1"></i>Load Summary
</button>
</div>
</div>
<div class="mt-3" id="dc_payout_summary_wrap">
<div class="text-muted">Select filters and click Load Summary.</div>
</div>
<div class="mt-3" id="dc_payout_drafts_wrap">
<div class="text-muted">Loading payout drafts...</div>
</div>
</div>
</div>
</section>
<div class="modal fade" id="dcPayoutEditModal" tabindex="-1" aria-labelledby="dcPayoutEditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="dcPayoutEditModalLabel">Edit Payout Draft</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<input type="hidden" id="dc_edit_payout_id" value="0">
<div class="mb-2">
<label class="form-label form-label-sm">Payout Date</label>
<input type="date" class="form-control form-control-sm" id="dc_edit_payout_date">
</div>
<div class="mb-2">
<label class="form-label form-label-sm">Approved Amount</label>
<input type="number" class="form-control form-control-sm" id="dc_edit_approved_amount" min="0" step="0.01">
</div>
<div class="mb-0">
<label class="form-label form-label-sm">Remarks</label>
<textarea class="form-control form-control-sm" id="dc_edit_remarks" rows="2"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary btn-sm" onclick="submitDcPayoutDraftEdit()">Save Changes</button>
</div>
</div>
</div>
</div>
<script>
(function () {
window.showDcPayoutAlert = function (message, ok) {
var box = document.getElementById('dc_payout_alert');
if (!box) return;
box.innerHTML = '<div class="alert ' + (ok ? 'alert-success' : 'alert-danger') + ' alert-dismissible fade show" role="alert">'
+ message
+ '<button type="button" class="btn-close" data-bs-dismiss="alert"></button>'
+ '</div>';
};
window.refreshDcPayoutSummary = function () {
var fromDate = document.getElementById('dc_payout_from_date')?.value || '';
var toDate = document.getElementById('dc_payout_to_date')?.value || '';
var doctorId = document.getElementById('dc_payout_doctor_id')?.value || '';
var stateUnit = document.getElementById('dc_payout_state_unit')?.value || '';
var payoutSource = document.getElementById('dc_payout_source')?.value || 'pathology';
load_form_div(
'<?= base_url('Finance/payout/doctor-charges-summary') ?>?from_date=' + encodeURIComponent(fromDate)
+ '&to_date=' + encodeURIComponent(toDate)
+ '&doctor_id=' + encodeURIComponent(doctorId)
+ '&state_unit=' + encodeURIComponent(stateUnit)
+ '&payout_source=' + encodeURIComponent(payoutSource),
'dc_payout_summary_wrap'
);
load_form_div(
'<?= base_url('Finance/payout/doctor-charges-drafts-table') ?>?from_date=' + encodeURIComponent(fromDate)
+ '&to_date=' + encodeURIComponent(toDate)
+ '&doctor_id=' + encodeURIComponent(doctorId)
+ '&state_unit=' + encodeURIComponent(stateUnit)
+ '&payout_source=' + encodeURIComponent(payoutSource),
'dc_payout_drafts_wrap'
);
};
window.editDcPayoutDraft = function (payoutId, payoutDate, approvedAmount, remarks) {
document.getElementById('dc_edit_payout_id').value = String(payoutId || 0);
document.getElementById('dc_edit_payout_date').value = String(payoutDate || '');
document.getElementById('dc_edit_approved_amount').value = String(approvedAmount || '0');
document.getElementById('dc_edit_remarks').value = String(remarks || '');
var modalEl = document.getElementById('dcPayoutEditModal');
if (modalEl && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
}
};
window.submitDcPayoutDraftEdit = function () {
var payoutId = document.getElementById('dc_edit_payout_id')?.value || '0';
var newDate = document.getElementById('dc_edit_payout_date')?.value || '';
var newAmount = document.getElementById('dc_edit_approved_amount')?.value || '0';
var newRemarks = document.getElementById('dc_edit_remarks')?.value || '';
if (!payoutId || payoutId === '0') {
window.showDcPayoutAlert('Invalid payout draft selected.', false);
return;
}
var fd = new window.FormData();
fd.append('payout_id', String(payoutId || '0'));
fd.append('payout_date', String(newDate || ''));
fd.append('approved_amount', String(newAmount || '0'));
fd.append('remarks', String(newRemarks || ''));
fetch('<?= base_url('Finance/payout/doctor-charges-draft-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) {
var ok = result.ok && result.data && result.data.status === 1;
window.showDcPayoutAlert((result.data && result.data.message) ? result.data.message : (ok ? 'Updated.' : 'Failed.'), ok);
if (ok) {
var modalEl = document.getElementById('dcPayoutEditModal');
if (modalEl && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).hide();
}
window.refreshDcPayoutSummary();
}
}).catch(function () {
window.showDcPayoutAlert('Network or server error while updating draft.', false);
});
};
window.deleteDcPayoutDraft = function (payoutId) {
if (!window.confirm('Delete this payout draft? Linked charge records will be unlocked for recalculation.')) {
return;
}
var fd = new window.FormData();
fd.append('payout_id', String(payoutId));
fetch('<?= base_url('Finance/payout/doctor-charges-draft-delete') ?>', {
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;
window.showDcPayoutAlert((result.data && result.data.message) ? result.data.message : (ok ? 'Deleted.' : 'Failed.'), ok);
if (ok) {
window.refreshDcPayoutSummary();
}
}).catch(function () {
window.showDcPayoutAlert('Network or server error while deleting draft.', false);
});
};
document.getElementById('dc_payout_source')?.addEventListener('change', refreshDcPayoutSummary);
refreshDcPayoutSummary();
})();
</script>

View File

@ -14,64 +14,8 @@
</div>
<div class="row g-3">
<div class="col-xl-4">
<div class="card h-100">
<div class="card-header"><strong>Request Actions</strong></div>
<div class="card-body">
<form id="ms_request_action_form" class="row g-2">
<div class="col-12">
<label class="form-label mb-1">Request ID</label>
<input type="number" class="form-control" name="request_id" id="ms_request_id" min="1" required>
</div>
<div class="col-12">
<label class="form-label mb-1">Remark</label>
<textarea class="form-control" name="remark" id="ms_action_remark" rows="2" placeholder="Optional review/approval remark"></textarea>
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_ms_review">Mark Finance Review</button>
<button type="button" class="btn btn-success btn-sm" id="btn_ms_approve">Approve</button>
<button type="button" class="btn btn-danger btn-sm" id="btn_ms_reject">Reject</button>
</div>
</form>
<hr>
<form id="ms_payment_form" class="row g-2">
<div class="col-12"><strong>Settlement Payment</strong></div>
<div class="col-12">
<label class="form-label mb-1">Request ID</label>
<input type="number" class="form-control" name="request_id" id="ms_payment_request_id" min="1" required>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Amount</label>
<input type="number" class="form-control" name="amount" step="0.01" min="0.01" required>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Mode</label>
<select class="form-select" name="payment_mode">
<option value="1">Cash</option>
<option value="2">Bank</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Bank Source ID</label>
<input type="number" class="form-control" name="insert_code" placeholder="Optional">
</div>
<div class="col-md-6">
<label class="form-label mb-1">Bank ID</label>
<input type="number" class="form-control" name="pay_bank_id" placeholder="Optional">
</div>
<div class="col-md-6"><input type="text" class="form-control" name="card_tran_id" placeholder="Transaction ID"></div>
<div class="col-md-6"><input type="text" class="form-control" name="card_bank" placeholder="Bank/Card Name"></div>
<div class="col-12"><textarea class="form-control" name="remark" rows="2" placeholder="Payment remark"></textarea></div>
<div class="col-12"><button type="submit" class="btn btn-primary btn-sm">Post Payment</button></div>
</form>
</div>
</div>
</div>
<div class="col-xl-8">
<div class="card h-100">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Request List</strong>
<div class="d-flex gap-2">
@ -95,15 +39,119 @@
</div>
</section>
<div class="modal fade" id="msRequestDetailModal" tabindex="-1" aria-labelledby="msRequestDetailModalLabel">
<div class="modal fade" id="msRequestActionModal" tabindex="-1" aria-labelledby="msRequestActionModalLabel">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="msRequestDetailModalLabel">Request Details</h5>
<div>
<h5 class="modal-title" id="msRequestActionModalLabel">Request Action</h5>
<div class="small text-muted" id="ms_request_action_modal_subtitle">Review request details and update its workflow status.</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body p-0" id="ms_request_detail_modal_body">
<div class="p-3 text-muted">Loading...</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-lg-4">
<form id="ms_request_action_form" class="row g-2">
<div class="col-12">
<label class="form-label mb-1">Request ID</label>
<input type="number" class="form-control" name="request_id" id="ms_request_id" min="1" required readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Request No</label>
<input type="text" class="form-control" id="ms_request_no" readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Status</label>
<input type="text" class="form-control" id="ms_request_status" readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Requested</label>
<input type="text" class="form-control" id="ms_request_requested_amount" readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Pending</label>
<input type="text" class="form-control" id="ms_request_pending_amount" readonly>
</div>
<div class="col-12">
<label class="form-label mb-1">Remark</label>
<textarea class="form-control" name="remark" id="ms_action_remark" rows="3" placeholder="Optional review/approval remark"></textarea>
</div>
<div class="col-12 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-outline-primary btn-sm" id="btn_ms_review">Mark Finance Review</button>
<button type="button" class="btn btn-success btn-sm" id="btn_ms_approve">Approve</button>
<button type="button" class="btn btn-danger btn-sm" id="btn_ms_reject">Reject</button>
<button type="button" class="btn btn-primary btn-sm d-none" id="btn_ms_to_settlement">Settlement Payment</button>
</div>
</form>
</div>
<div class="col-lg-8">
<div id="ms_request_action_detail_wrap" class="border rounded overflow-hidden">
<div class="p-3 text-muted">Loading...</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="msSettlementModal" tabindex="-1" aria-labelledby="msSettlementModalLabel">
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title" id="msSettlementModalLabel">Settlement Payment</h5>
<div class="small text-muted" id="ms_settlement_modal_subtitle">Post settlement for the approved medical store credit request.</div>
</div>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form id="ms_payment_form" class="row g-2">
<div class="col-md-6">
<label class="form-label mb-1">Request ID</label>
<input type="number" class="form-control" name="request_id" id="ms_payment_request_id" min="1" required readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Request No</label>
<input type="text" class="form-control" id="ms_payment_request_no" readonly>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Amount</label>
<input type="number" class="form-control" name="amount" id="ms_payment_amount" step="0.01" min="0.01" required>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Mode</label>
<select class="form-select" name="payment_mode" id="ms_payment_mode">
<option value="1">Cash</option>
<option value="2">Bank</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label mb-1">Bank Source ID</label>
<input type="number" class="form-control" name="insert_code" placeholder="Optional">
</div>
<div class="col-md-6">
<label class="form-label mb-1">Bank ID</label>
<input type="number" class="form-control" name="pay_bank_id" placeholder="Optional">
</div>
<div class="col-md-6">
<label class="form-label mb-1">Transaction ID</label>
<input type="text" class="form-control" name="card_tran_id" placeholder="Transaction ID">
</div>
<div class="col-md-6">
<label class="form-label mb-1">Bank/Card Name</label>
<input type="text" class="form-control" name="card_bank" placeholder="Bank/Card Name">
</div>
<div class="col-12">
<label class="form-label mb-1">Payment remark</label>
<textarea class="form-control" name="remark" rows="3" placeholder="Payment remark"></textarea>
</div>
<div class="col-12 d-flex justify-content-between align-items-center pt-2">
<div class="small text-muted" id="ms_payment_pending_hint">Pending amount will be loaded from the selected request.</div>
<button type="submit" class="btn btn-primary btn-sm">Post Payment</button>
</div>
</form>
</div>
</div>
</div>
@ -112,6 +160,29 @@
<script>
(function () {
var BASE = '<?= base_url() ?>';
var currentRequest = null;
function formatAmount(value) {
return Number(value || 0).toFixed(2);
}
function getActionModal() {
var modalEl = document.getElementById('msRequestActionModal');
if (!modalEl || !window.bootstrap || !window.bootstrap.Modal) {
return null;
}
return window.bootstrap.Modal.getOrCreateInstance(modalEl);
}
function getSettlementModal() {
var modalEl = document.getElementById('msSettlementModal');
if (!modalEl || !window.bootstrap || !window.bootstrap.Modal) {
return null;
}
return window.bootstrap.Modal.getOrCreateInstance(modalEl);
}
function showAlert(msg, ok) {
var cls = ok ? 'alert-success' : 'alert-danger';
@ -160,6 +231,98 @@
});
}
function updateActionButtons(request) {
var status = request && request.status ? String(request.status) : '';
var pending = Number(request && request.pending_amount ? request.pending_amount : 0);
var btnReview = document.getElementById('btn_ms_review');
var btnApprove = document.getElementById('btn_ms_approve');
var btnReject = document.getElementById('btn_ms_reject');
var btnSettlement = document.getElementById('btn_ms_to_settlement');
if (btnReview) btnReview.disabled = status !== 'submitted';
if (btnApprove) btnApprove.disabled = status !== 'finance_review';
if (btnReject) btnReject.disabled = !(status === 'submitted' || status === 'finance_review');
if (btnSettlement) {
var canSettle = (status === 'approved' || status === 'partially_paid') && pending > 0;
btnSettlement.classList.toggle('d-none', !canSettle);
btnSettlement.disabled = !canSettle;
}
}
function fillActionModal(request) {
currentRequest = request || null;
var requestId = document.getElementById('ms_request_id');
var requestNo = document.getElementById('ms_request_no');
var requestStatus = document.getElementById('ms_request_status');
var requestedAmount = document.getElementById('ms_request_requested_amount');
var pendingAmount = document.getElementById('ms_request_pending_amount');
var subtitle = document.getElementById('ms_request_action_modal_subtitle');
if (requestId) requestId.value = request && request.id ? String(request.id) : '';
if (requestNo) requestNo.value = request && request.request_no ? String(request.request_no) : '';
if (requestStatus) requestStatus.value = request && request.status ? String(request.status) : '';
if (requestedAmount) requestedAmount.value = formatAmount(request && request.requested_amount ? request.requested_amount : 0);
if (pendingAmount) pendingAmount.value = formatAmount(request && request.pending_amount ? request.pending_amount : 0);
if (subtitle) {
subtitle.textContent = request
? 'Request #' + String(request.id || '') + ' is currently in ' + String(request.status || 'draft') + ' status.'
: 'Review request details and update its workflow status.';
}
updateActionButtons(request || {});
}
function fillSettlementForm(request) {
var requestId = document.getElementById('ms_payment_request_id');
var requestNo = document.getElementById('ms_payment_request_no');
var amount = document.getElementById('ms_payment_amount');
var hint = document.getElementById('ms_payment_pending_hint');
if (requestId) requestId.value = request && request.id ? String(request.id) : '';
if (requestNo) requestNo.value = request && request.request_no ? String(request.request_no) : '';
if (amount) amount.value = formatAmount(request && request.pending_amount ? request.pending_amount : 0);
if (hint) hint.textContent = 'Pending amount: ' + formatAmount(request && request.pending_amount ? request.pending_amount : 0);
}
function loadRequestActionModal(requestId) {
var detailWrap = document.getElementById('ms_request_action_detail_wrap');
if (detailWrap) {
detailWrap.innerHTML = '<div class="p-3 text-muted">Loading...</div>';
}
fetch(BASE + 'Finance/medical_store_request_detail/' + encodeURIComponent(String(requestId)), {
method: 'GET',
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).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 || !result.data.request) {
showAlert((result.data && result.data.message) ? result.data.message : 'Unable to load request.', false);
return;
}
fillActionModal(result.data.request);
fetch(BASE + 'Finance/medical_store_request_lines_table?request_id=' + encodeURIComponent(String(requestId)), {
method: 'GET',
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function (r) {
return r.text().then(function (html) { return { ok: r.ok, html: html }; });
}).then(function (htmlResult) {
if (!detailWrap) return;
detailWrap.innerHTML = htmlResult.ok ? htmlResult.html : '<div class="p-3 text-danger">Unable to load request details.</div>';
}).catch(function () {
if (detailWrap) detailWrap.innerHTML = '<div class="p-3 text-danger">Network error while loading details.</div>';
});
var actionModal = getActionModal();
if (actionModal) {
actionModal.show();
}
}).catch(function () {
showAlert('Network or server error.', false);
});
}
function doStatusAction(endpoint) {
var requestId = document.getElementById('ms_request_id') ? document.getElementById('ms_request_id').value : '';
var remark = document.getElementById('ms_action_remark') ? document.getElementById('ms_action_remark').value : '';
@ -179,6 +342,19 @@
showAlert(result.data.message || 'Updated successfully.', true);
refreshRequestsTable();
refreshSummary();
if (requestId) {
loadRequestActionModal(requestId);
}
if (endpoint === 'Finance/medical_store_request_approve' && currentRequest) {
currentRequest.status = 'approved';
fillSettlementForm(currentRequest);
var actionModal = getActionModal();
var settlementModal = getSettlementModal();
if (actionModal) actionModal.hide();
if (settlementModal) settlementModal.show();
}
});
}
@ -196,31 +372,23 @@
paymentForm.reset();
refreshRequestsTable();
refreshSummary();
var settlementModal = getSettlementModal();
if (settlementModal) settlementModal.hide();
if (currentRequest && currentRequest.id) {
loadRequestActionModal(currentRequest.id);
}
});
});
}
window.openMedicalStoreRequestDetail = function (requestId) {
if (!requestId) return;
var body = document.getElementById('ms_request_detail_modal_body');
if (body) body.innerHTML = '<div class="p-3 text-muted">Loading...</div>';
fetch(BASE + 'Finance/medical_store_request_lines_table?request_id=' + encodeURIComponent(String(requestId)), {
method: 'GET',
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function (r) {
return r.text().then(function (html) { return { ok: r.ok, html: html }; });
}).then(function (result) {
if (!body) return;
body.innerHTML = result.ok ? result.html : '<div class="p-3 text-danger">Unable to load request details.</div>';
}).catch(function () {
if (body) body.innerHTML = '<div class="p-3 text-danger">Network error while loading details.</div>';
});
var modalEl = document.getElementById('msRequestDetailModal');
if (modalEl && window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
window.openMedicalStoreRequestAction = function (requestId) {
if (!requestId) {
return;
}
loadRequestActionModal(requestId);
};
var btnRefresh = document.getElementById('btn_ms_refresh');
@ -237,5 +405,21 @@
var btnReject = document.getElementById('btn_ms_reject');
if (btnReject) btnReject.addEventListener('click', function () { doStatusAction('Finance/medical_store_request_reject'); });
var btnSettlement = document.getElementById('btn_ms_to_settlement');
if (btnSettlement) {
btnSettlement.addEventListener('click', function () {
if (!currentRequest) {
showAlert('Select a request first.', false);
return;
}
fillSettlementForm(currentRequest);
var settlementModal = getSettlementModal();
if (settlementModal) {
settlementModal.show();
}
});
}
})();
</script>

View File

@ -0,0 +1,78 @@
<?php
$rows = $rows ?? [];
$sourceLabel = trim((string) ($source_label ?? 'Doctor Charges'));
$sourceType = trim((string) ($source_type ?? 'pathology'));
?>
<div class="card border-0 shadow-sm">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<strong>Recent <?= esc($sourceLabel) ?> 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">Invoices</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="editDcPayoutDraft(<?= (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="deleteDcPayoutDraft(<?= (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,244 @@
<?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'));
$sourceType = trim((string) ($source_type ?? 'pathology'));
$sourceLabel = trim((string) ($source_label ?? 'Pathology Charges'));
$scopeLabel = trim((string) ($scope_label ?? 'Eligible Invoices'));
$unitLabel = trim((string) ($unit_label ?? 'Invoices'));
$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>Charge Source:</strong> <?= esc($sourceLabel) ?>
</div>
<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"><?= esc($scopeLabel) ?></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">Already Calculated</div><div class="h5 mb-0"><?= (int) ($summary['calculated_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 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="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 Charges Amount</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="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 <?= esc($sourceLabel) ?> Collection Summary</strong>
<span class="small text-muted">Collection is allocated by invoice share for selected charge category.</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"><?= esc($unitLabel) ?></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="7" class="text-center text-muted py-3">No 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"><?= 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) > 0 ? ($summary['total_received'] ?? 0) : ($summary['gross_amount'] ?? 0)); ?>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white">
<strong>Payout Calculation (Preview: <?= esc($sourceLabel) ?>)</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="dc_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="dc_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="dc_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="dc_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="dc_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="dc_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="dc_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="dc_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="dc_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('dc_payout_base_amount');
var doctorShare = toNumber('dc_payout_doctor_share');
var hospitalShare = toNumber('dc_payout_hospital_share');
var deductions = toNumber('dc_payout_deductions');
var adjustments = toNumber('dc_payout_adjustments');
var doctorGross = (baseAmount * doctorShare) / 100;
var hospitalGross = (baseAmount * hospitalShare) / 100;
var netPayable = doctorGross - deductions + adjustments;
var doctorEl = document.getElementById('dc_payout_doctor_gross');
var hospitalEl = document.getElementById('dc_payout_hospital_gross');
var netEl = document.getElementById('dc_payout_net_payable');
if (doctorEl) doctorEl.textContent = formatMoney(doctorGross);
if (hospitalEl) hospitalEl.textContent = formatMoney(hospitalGross);
if (netEl) netEl.textContent = formatMoney(netPayable);
}
['dc_payout_base_amount', 'dc_payout_doctor_share', 'dc_payout_hospital_share', 'dc_payout_deductions', 'dc_payout_adjustments']
.forEach(function (id) {
var el = document.getElementById(id);
if (el) {
el.addEventListener('input', recalc);
}
});
var draftBtn = document.getElementById('dc_payout_create_draft_btn');
if (draftBtn) {
draftBtn.addEventListener('click', function () {
var fromDate = document.getElementById('dc_payout_from_date')?.value || '';
var toDate = document.getElementById('dc_payout_to_date')?.value || '';
var doctorId = document.getElementById('dc_payout_doctor_id')?.value || '';
var stateUnit = document.getElementById('dc_payout_state_unit')?.value || '';
var payoutSource = document.getElementById('dc_payout_source')?.value || 'pathology';
if (!doctorId) {
if (typeof window.showDcPayoutAlert === 'function') {
window.showDcPayoutAlert('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('payout_source', payoutSource);
fd.append('base_amount', String(toNumber('dc_payout_base_amount')));
fd.append('doctor_share', String(toNumber('dc_payout_doctor_share')));
fd.append('hospital_share', String(toNumber('dc_payout_hospital_share')));
fd.append('deductions', String(toNumber('dc_payout_deductions')));
fd.append('adjustments', String(toNumber('dc_payout_adjustments')));
fetch('<?= base_url('Finance/payout/doctor-charges-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.showDcPayoutAlert === 'function') {
window.showDcPayoutAlert(msg, ok);
}
if (ok && typeof window.refreshDcPayoutSummary === 'function') {
window.refreshDcPayoutSummary();
}
})
.catch(function () {
if (typeof window.showDcPayoutAlert === 'function') {
window.showDcPayoutAlert('Network or server error while creating payout draft.', false);
}
});
});
}
recalc();
})();
</script>

View File

@ -18,17 +18,34 @@
<tr><td colspan="9" class="text-center text-muted py-3">No requests found.</td></tr>
<?php else: ?>
<?php foreach (($rows ?? []) as $i => $row): ?>
<?php
$status = (string) ($row['status'] ?? '');
$badgeClass = 'bg-secondary';
if ($status === 'submitted') {
$badgeClass = 'bg-primary';
} elseif ($status === 'finance_review') {
$badgeClass = 'bg-warning text-dark';
} elseif ($status === 'approved') {
$badgeClass = 'bg-success';
} elseif ($status === 'partially_paid') {
$badgeClass = 'bg-info text-dark';
} elseif ($status === 'paid') {
$badgeClass = 'bg-dark';
} elseif ($status === 'rejected') {
$badgeClass = 'bg-danger';
}
?>
<tr>
<td><?= (int) $i + 1 ?></td>
<td><strong><?= esc((string) ($row['request_no'] ?? '')) ?></strong><br><small class="text-muted">ID: <?= (int) ($row['id'] ?? 0) ?></small></td>
<td><?= esc((string) ($row['request_date'] ?? '')) ?></td>
<td><span class="badge bg-secondary"><?= esc((string) ($row['status'] ?? '')) ?></span></td>
<td><span class="badge <?= esc($badgeClass) ?>"><?= esc($status) ?></span></td>
<td class="text-end"><?= esc(number_format((float) ($row['requested_amount'] ?? 0), 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['paid_amount'] ?? 0), 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['pending_amount'] ?? 0), 2)) ?></td>
<td><?= esc((string) ($row['created_by'] ?? '')) ?></td>
<td class="text-center">
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openMedicalStoreRequestDetail(<?= (int) ($row['id'] ?? 0) ?>)">View</button>
<button type="button" class="btn btn-outline-primary btn-sm" onclick="openMedicalStoreRequestAction(<?= (int) ($row['id'] ?? 0) ?>)">Next</button>
</td>
</tr>
<?php endforeach; ?>

View File

@ -1,10 +1,13 @@
<?php
$rows = $rows ?? [];
$sourceLabel = trim((string) ($source_label ?? 'OPD Consultation'));
$sourceType = trim((string) ($source_type ?? 'consultation'));
$isConsultation = $sourceType === 'consultation';
?>
<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>
<strong>Recent <?= esc($sourceLabel) ?> Payout Drafts</strong>
<span class="small text-muted">Filtered by current date/doctor/state selection</span>
</div>
<div class="card-body p-0">
@ -16,7 +19,7 @@ $rows = $rows ?? [];
<th>Payout Date</th>
<th>Case Ref</th>
<th>Doctor</th>
<th class="text-end">Units</th>
<th class="text-end"><?= $isConsultation ? 'OPDs' : 'Invoices' ?></th>
<th class="text-end">Rate</th>
<th class="text-end">Calculated</th>
<th class="text-end">Approved</th>

View File

@ -6,6 +6,11 @@ $toDate = (string) ($to_date ?? '');
$doctorName = trim((string) ($doctor_name ?? 'All Doctors'));
$stateUnit = trim((string) ($state_unit ?? ''));
$stateUnitLabel = trim((string) ($state_unit_label ?? 'State/Unit'));
$sourceType = trim((string) ($source_type ?? 'consultation'));
$sourceLabel = trim((string) ($source_label ?? 'OPD Consultation'));
$scopeLabel = trim((string) ($scope_label ?? 'Completed OPDs'));
$unitLabel = trim((string) ($unit_label ?? 'OPDs'));
$isConsultation = $sourceType === 'consultation';
$formatDate = static function (string $value): string {
$value = trim($value);
@ -24,6 +29,9 @@ $money = static function ($value): string {
<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>Source:</strong> <?= esc($sourceLabel) ?>
</div>
<div>
<strong>Summary Range:</strong> <?= esc($formatDate($fromDate)) ?> to <?= esc($formatDate($toDate)) ?>
</div>
@ -37,18 +45,25 @@ $money = static function ($value): string {
</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-primary"><div class="card-body py-2"><div class="small text-muted"><?= esc($scopeLabel) ?></div><div class="h5 mb-0 text-primary"><?= (int) ($summary['completed_opd'] ?? 0) ?></div></div></div></div>
<?php if ($isConsultation): ?>
<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>
<?php else: ?>
<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">Calculated Invoices</div><div class="h5 mb-0"><?= (int) ($summary['calculated_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">Locked Credit</div><div class="h5 mb-0 text-danger"><?= esc($money($summary['credit_amount'] ?? 0)) ?></div></div></div></div>
<?php endif; ?>
<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>
<?php if ($isConsultation): ?>
<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>
<?php endif; ?>
<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"><?= $isConsultation ? 'Gross OPD Revenue' : 'Gross Charges Amount' ?></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>
@ -57,14 +72,14 @@ $money = static function ($value): string {
<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-info"><div class="card-body py-2"><div class="small text-muted"><?= $isConsultation ? 'Approved Consents' : 'Charge Group' ?></div><div class="h5 mb-0 text-info"><?= $isConsultation ? (int) ($summary['approved_consents'] ?? 0) : esc($sourceLabel) ?></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>
<strong>Doctor-wise <?= esc($sourceLabel) ?> and Collection Summary</strong>
<span class="small text-muted"><?= $isConsultation ? 'Running and New are shown for internal review; routine figure is non-emergency total.' : 'Collection is allocated by invoice share for selected charge category.' ?></span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
@ -73,11 +88,13 @@ $money = static function ($value): string {
<tr>
<th>#</th>
<th>Doctor</th>
<th class="text-end">Completed</th>
<th class="text-end"><?= esc($unitLabel) ?></th>
<?php if ($isConsultation): ?>
<th class="text-end">Routine</th>
<th class="text-end">Emergency</th>
<th class="text-end">Running</th>
<th class="text-end">New</th>
<?php endif; ?>
<th class="text-end">Gross Amount</th>
<th class="text-end">Cash</th>
<th class="text-end">Bank</th>
@ -87,7 +104,7 @@ $money = static function ($value): string {
<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>
<td colspan="<?= $isConsultation ? '11' : '7' ?>" class="text-center text-muted py-3">No summary records found for the selected filters.</td>
</tr>
<?php else: ?>
<?php $sr = 1; foreach ($doctorBreakdown as $row): ?>
@ -95,10 +112,12 @@ $money = static function ($value): string {
<td><?= $sr++ ?></td>
<td><?= esc((string) ($row['doctor_name'] ?? '')) ?></td>
<td class="text-end"><?= (int) ($row['completed_opd'] ?? 0) ?></td>
<?php if ($isConsultation): ?>
<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>
<?php endif; ?>
<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>
@ -112,12 +131,10 @@ $money = static function ($value): string {
</div>
</div>
<?php
$baseForPayout = (float) ($summary['total_received'] ?? 0);
?>
<?php $baseForPayout = (float) (($summary['total_received'] ?? 0) > 0 ? ($summary['total_received'] ?? 0) : ($summary['gross_amount'] ?? 0)); ?>
<div class="card border-0 shadow-sm mt-3">
<div class="card-header bg-white">
<strong>Payout Calculation (Preview)</strong>
<strong>Payout Calculation (Preview: <?= esc($sourceLabel) ?>)</strong>
</div>
<div class="card-body">
<div class="row g-2 align-items-end mb-2">
@ -125,37 +142,26 @@ $baseForPayout = (float) ($summary['total_received'] ?? 0);
<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 class="row g-1 mb-2">
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-primary border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem"><?= esc($scopeLabel) ?></div><div class="fw-bold text-primary"><?= (int) ($summary['completed_opd'] ?? 0) ?></div></div></div></div>
<?php if ($isConsultation): ?>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-secondary border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Routine OPDs</div><div class="fw-bold"><?= (int) ($summary['routine_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-danger border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Emergency OPDs</div><div class="fw-bold text-danger"><?= (int) ($summary['emergency_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-dark border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Already Calculated</div><div class="fw-bold"><?= (int) ($summary['calculated_opd'] ?? 0) ?></div></div></div></div>
<?php else: ?>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-secondary border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Already Calculated</div><div class="fw-bold"><?= (int) ($summary['calculated_opd'] ?? 0) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-danger border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Locked Credit</div><div class="fw-bold text-danger" style="font-size:.85rem"><?= esc($money($summary['credit_amount'] ?? 0)) ?></div></div></div></div>
<?php endif; ?>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-info border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Doctors Involved</div><div class="fw-bold text-info"><?= (int) ($summary['doctor_count'] ?? 0) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-success border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem"><?= $isConsultation ? 'Gross OPD Revenue' : 'Gross Charges' ?></div><div class="fw-bold text-success" style="font-size:.85rem"><?= esc($money($summary['gross_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-success border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Total Received</div><div class="fw-bold text-success" style="font-size:.85rem"><?= esc($money($summary['total_received'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-warning border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Cash Received</div><div class="fw-bold text-warning" style="font-size:.85rem"><?= esc($money($summary['cash_received'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-primary border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Bank Received</div><div class="fw-bold text-primary" style="font-size:.85rem"><?= esc($money($summary['bank_received'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-dark border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Total Credit</div><div class="fw-bold" style="font-size:.85rem"><?= esc($money($summary['credit_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-dark border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Org. Credit</div><div class="fw-bold" style="font-size:.85rem"><?= esc($money($summary['org_credit_amount'] ?? 0)) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-info border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem"><?= $isConsultation ? 'Approved Consents' : 'Charge Group' ?></div><div class="fw-bold text-info"><?= $isConsultation ? (int) ($summary['approved_consents'] ?? 0) : esc($sourceLabel) ?></div></div></div></div>
<div class="col-xl-2 col-md-3 col-6"><div class="card border-start border-secondary border-2 border-top-0 border-end-0 border-bottom-0 rounded-0"><div class="card-body py-1 px-2"><div class="text-muted" style="font-size:.7rem">Payout Drafts / Amt</div><div class="fw-bold" style="font-size:.8rem"><?= (int) ($summary['payout_count'] ?? 0) ?> / <?= esc($money($summary['payout_amount'] ?? 0)) ?></div></div></div></div>
</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) {

View File

@ -1,7 +1,7 @@
<section class="content finance-opd-payout">
<div class="mb-3">
<h2 class="mb-1">OPD Consult Payout</h2>
<p class="text-muted mb-0">Start payout calculation from OPD consultation and collection data. This screen is enabled for testing and will be extended in the next phase.</p>
<h2 class="mb-1">OPD Consultation Payout</h2>
<p class="text-muted mb-0">Calculate and manage doctor payout from OPD consultation fees.</p>
</div>
<div id="opd_payout_alert"></div>
@ -42,13 +42,6 @@
</div>
</div>
<hr>
<div class="alert alert-info mb-0" role="alert">
<strong>Phase note:</strong> Menu and page are now available under Accounts And Finance -> Payout -> OPD Consult Payout.
Calculation grid, policy split engine, and payout approval workflow will be implemented next.
</div>
<div class="mt-3" id="opd_consult_payout_summary_wrap">
<div class="text-muted">Loading summary...</div>
</div>
@ -106,7 +99,6 @@
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 || '';
load_form_div(
'<?= base_url('Finance/payout/opd-consult-summary') ?>?from_date=' + encodeURIComponent(fromDate)
+ '&to_date=' + encodeURIComponent(toDate)
@ -174,7 +166,7 @@
};
window.deleteOpdPayoutDraft = function (payoutId) {
if (!window.confirm('Delete this payout draft? Linked OPDs will be unlocked for recalculation.')) {
if (!window.confirm('Delete this payout draft? Linked source records will be unlocked for recalculation.')) {
return;
}
@ -198,5 +190,7 @@
};
refreshOpdConsultPayoutSummary();
})();
</script>

View File

@ -10,6 +10,88 @@
<section class="section">
<div id="med_credit_req_alert"></div>
<style>
#med_req_status_dashboard_strip {
display: grid;
grid-template-columns: repeat(7, minmax(110px, 1fr));
gap: 8px;
}
#med_req_status_dashboard_strip .med-stat {
border: 1px solid #d9dee6;
border-radius: 6px;
padding: 8px 10px;
background: #fff;
min-height: 58px;
}
#med_req_status_dashboard_strip .med-stat-label {
color: #6c757d;
font-size: 12px;
line-height: 1.1;
}
#med_req_status_dashboard_strip .med-stat-value {
font-size: 28px;
font-weight: 600;
line-height: 1;
margin-top: 4px;
}
#med_request_history_table {
width: 100% !important;
}
@media (max-width: 1200px) {
#med_req_status_dashboard_strip {
grid-template-columns: repeat(4, minmax(110px, 1fr));
}
}
@media (max-width: 768px) {
#med_req_status_dashboard_strip {
grid-template-columns: repeat(2, minmax(110px, 1fr));
}
}
</style>
<?php
$historyRows = $rows ?? [];
$statusCounts = [
'submitted' => 0,
'finance_review' => 0,
'approved' => 0,
'partially_paid' => 0,
'paid' => 0,
'rejected' => 0,
];
foreach ($historyRows as $historyRow) {
$st = strtolower(trim((string) ($historyRow['status'] ?? '')));
if (array_key_exists($st, $statusCounts)) {
$statusCounts[$st]++;
}
}
?>
<div class="card mb-3" id="med_req_status_dashboard">
<div class="card-body p-2">
<div id="med_req_status_dashboard_strip">
<div class="med-stat"><div class="med-stat-label">Total</div><div class="med-stat-value text-dark" id="med_dash_total"><?= (int) count($historyRows) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Submitted</div><div class="med-stat-value text-primary" id="med_dash_submitted"><?= (int) ($statusCounts['submitted'] ?? 0) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Finance Review</div><div class="med-stat-value text-warning" id="med_dash_finance_review"><?= (int) ($statusCounts['finance_review'] ?? 0) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Approved</div><div class="med-stat-value text-success" id="med_dash_approved"><?= (int) ($statusCounts['approved'] ?? 0) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Partial</div><div class="med-stat-value text-info" id="med_dash_partially_paid"><?= (int) ($statusCounts['partially_paid'] ?? 0) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Paid</div><div class="med-stat-value text-secondary" id="med_dash_paid"><?= (int) ($statusCounts['paid'] ?? 0) ?></div></div>
<div class="med-stat"><div class="med-stat-label">Rejected</div><div class="med-stat-value text-danger" id="med_dash_rejected"><?= (int) ($statusCounts['rejected'] ?? 0) ?></div></div>
</div>
</div>
</div>
<ul class="nav nav-tabs" id="medCreditTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="med-credit-select-tab" data-bs-toggle="tab" data-bs-target="#med-credit-select-pane" type="button" role="tab" aria-controls="med-credit-select-pane" aria-selected="true">1) Select Credit Entries</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="med-credit-history-tab" data-bs-toggle="tab" data-bs-target="#med-credit-history-pane" type="button" role="tab" aria-controls="med-credit-history-pane" aria-selected="false">Request History</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 bg-white" id="medCreditTabsContent">
<div class="tab-pane fade show active" id="med-credit-select-pane" role="tabpanel" aria-labelledby="med-credit-select-tab" tabindex="0">
<div class="row g-3">
<div class="col-xl-7">
@ -117,13 +199,16 @@
</div>
</form>
<hr>
<div class="small text-muted">Request history is shown below. Use Finance panel for approval and payment settlement.</div>
<div class="small text-muted">Use Request History tab to track status updates. Use Finance panel for approval and payment settlement.</div>
</div>
</div>
</div>
</div>
<div class="card mt-3">
</div>
<div class="tab-pane fade" id="med-credit-history-pane" role="tabpanel" aria-labelledby="med-credit-history-tab" tabindex="0">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<strong>Request History</strong>
<button type="button" class="btn btn-outline-secondary btn-sm" id="btn_med_history_refresh">Refresh</button>
@ -142,33 +227,20 @@
<th class="text-end">Pending</th>
</tr>
</thead>
<tbody>
<?php if (empty($rows ?? [])): ?>
<tr><td colspan="7" class="text-center text-muted py-3">No payout requests yet.</td></tr>
<?php else: ?>
<?php foreach (($rows ?? []) as $i => $row): ?>
<tr>
<td><?= (int) $i + 1 ?></td>
<td><strong><?= esc((string) ($row['request_no'] ?? '')) ?></strong><br><small class="text-muted">ID: <?= (int) ($row['id'] ?? 0) ?></small></td>
<td><?= esc((string) ($row['request_date'] ?? '')) ?></td>
<td><span class="badge bg-secondary"><?= esc((string) ($row['status'] ?? '')) ?></span></td>
<td class="text-end"><?= esc(number_format((float) ($row['requested_amount'] ?? 0), 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['paid_amount'] ?? 0), 2)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row['pending_amount'] ?? 0), 2)) ?></td>
</tr>
<?php endforeach; ?>
<?php endif; ?>
</tbody>
<tbody><tr><td colspan="7" class="text-center text-muted py-3">Loading...</td></tr></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
<script>
(function () {
var BASE = '<?= base_url() ?>';
var poolTable = null;
var historyTable = null;
var selectedMap = {};
var poolRowCache = {};
@ -179,6 +251,46 @@
+ msg + '<button type="button" class="btn-close" data-bs-dismiss="alert"></button></div>';
}
function getStatusBadgeClass(status) {
var s = String(status || '').toLowerCase();
if (s === 'submitted') return 'bg-primary';
if (s === 'finance_review') return 'bg-warning text-dark';
if (s === 'approved') return 'bg-success';
if (s === 'partially_paid') return 'bg-info text-dark';
if (s === 'paid') return 'bg-dark';
if (s === 'rejected') return 'bg-danger';
return 'bg-secondary';
}
function refreshStatusDashboard(summary) {
var s = summary || {};
var counts = {
total: Number(s.total || 0),
submitted: Number(s.submitted || 0),
finance_review: Number(s.finance_review || 0),
approved: Number(s.approved || 0),
partially_paid: Number(s.partially_paid || 0),
paid: Number(s.paid || 0),
rejected: Number(s.rejected || 0)
};
var totalEl = document.getElementById('med_dash_total');
var submittedEl = document.getElementById('med_dash_submitted');
var reviewEl = document.getElementById('med_dash_finance_review');
var approvedEl = document.getElementById('med_dash_approved');
var partialEl = document.getElementById('med_dash_partially_paid');
var paidEl = document.getElementById('med_dash_paid');
var rejectedEl = document.getElementById('med_dash_rejected');
if (totalEl) totalEl.textContent = String(counts.total);
if (submittedEl) submittedEl.textContent = String(counts.submitted);
if (reviewEl) reviewEl.textContent = String(counts.finance_review);
if (approvedEl) approvedEl.textContent = String(counts.approved);
if (partialEl) partialEl.textContent = String(counts.partially_paid);
if (paidEl) paidEl.textContent = String(counts.paid);
if (rejectedEl) rejectedEl.textContent = String(counts.rejected);
}
function getRowKey(row) {
return String(row.source_type || '') + ':' + String(row.source_ref_id || 0);
}
@ -436,37 +548,89 @@
}
function loadHistory() {
fetch(BASE + 'Medical/credit_payout_requests', {
method: 'GET',
headers: {'X-Requested-With': 'XMLHttpRequest'}
}).then(function (res) {
return res.json().then(function (data) { return { ok: res.ok, data: data }; });
}).then(function (result) {
var table = document.querySelector('#med_request_history_table tbody');
if (!table) return;
if (!result.ok || !result.data || result.data.status !== 1) {
table.innerHTML = '<tr><td colspan="7" class="text-center text-danger py-3">Unable to load request history.</td></tr>';
return;
if (!historyTable) {
return;
}
historyTable.ajax.reload(null, false);
}
function initHistoryTable() {
if (typeof window.jQuery === 'undefined' || typeof window.jQuery.fn.DataTable === 'undefined') {
showAlert('DataTable dependency missing. Unable to load request history.', false);
return;
}
historyTable = window.jQuery('#med_request_history_table').DataTable({
processing: true,
serverSide: true,
searching: true,
autoWidth: false,
lengthMenu: [[10, 25, 50, 100], [10, 25, 50, 100]],
pageLength: 25,
order: [[0, 'desc']],
ajax: {
url: BASE + 'Medical/credit_payout_requests_datatable',
type: 'POST',
dataSrc: function (json) {
refreshStatusDashboard(json && json.status_summary ? json.status_summary : {});
return (json && Array.isArray(json.data)) ? json.data : [];
},
error: function () {
refreshStatusDashboard({});
showAlert('Unable to load request history.', false);
}
},
columns: [
{ data: 'row_no', orderable: false, searchable: false },
{
data: null,
render: function (data, type, row) {
return '<strong>' + String(row.request_no || '') + '</strong><br><small class="text-muted">ID: ' + Number(row.id || 0) + '</small>';
}
},
{ data: 'request_date', defaultContent: '' },
{
data: 'status',
render: function (v) {
var status = String(v || '');
return '<span class="badge ' + getStatusBadgeClass(status) + '">' + status + '</span>';
}
},
{
data: 'requested_amount',
className: 'text-end',
render: function (v) {
return Number(v || 0).toFixed(2);
}
},
{
data: 'paid_amount',
className: 'text-end',
render: function (v) {
return Number(v || 0).toFixed(2);
}
},
{
data: 'pending_amount',
className: 'text-end',
render: function (v) {
return Number(v || 0).toFixed(2);
}
}
],
language: {
emptyTable: 'No payout requests yet.'
}
var rows = result.data.rows || [];
if (!rows.length) {
table.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-3">No payout requests yet.</td></tr>';
return;
}
var html = '';
rows.forEach(function (row, i) {
html += '<tr>'
+ '<td>' + (i + 1) + '</td>'
+ '<td><strong>' + (row.request_no || '') + '</strong><br><small class="text-muted">ID: ' + Number(row.id || 0) + '</small></td>'
+ '<td>' + (row.request_date || '') + '</td>'
+ '<td><span class="badge bg-secondary">' + (row.status || '') + '</span></td>'
+ '<td class="text-end">' + Number(row.requested_amount || 0).toFixed(2) + '</td>'
+ '<td class="text-end">' + Number(row.paid_amount || 0).toFixed(2) + '</td>'
+ '<td class="text-end">' + Number(row.pending_amount || 0).toFixed(2) + '</td>'
+ '</tr>';
});
table.innerHTML = html;
});
var historyTab = document.getElementById('med-credit-history-tab');
if (historyTab) {
historyTab.addEventListener('shown.bs.tab', function () {
if (historyTable && historyTable.columns) {
historyTable.columns.adjust().draw(false);
}
});
}
}
var form = document.getElementById('med_credit_request_form');
@ -613,6 +777,7 @@
syncScopeWithGroupMode();
initPoolTable();
initHistoryTable();
loadHistory();
})();
</script>

View File

@ -303,6 +303,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
<span>OPD Consult Payout</span>
</a>
</li>
<li>
<a href="javascript:load_form('<?= base_url('Finance/payout/doctor-charges') ?>','Doctor Charges Payout')">
<i class="bi bi-circle"></i>
<span>Doctor Charges Payout</span>
</a>
</li>
<li>
<a href="javascript:load_form('<?= base_url('Finance/medical_store_credit_account') ?>','Medical Store Credit Account')">
<i class="bi bi-circle"></i>