Add OPD Total Report with doctor/type/fee filters

This commit is contained in:
DevSoftTech Server 2026-04-09 18:44:35 +05:30
parent ccbc5d7b72
commit aa2e092758
6 changed files with 381 additions and 0 deletions

View File

@ -717,6 +717,9 @@ $routes->get('report/index', 'Report::index');
$reportsCollectionFilter = 'permission:reports.collection.view,reports.access';
$routes->get('Report/collection_report', 'Report::collection_report', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/report_opd_total', 'Report::report_opd_total', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/opd_total_data/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::opd_total_data/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/opd_total_data/(:segment)/(:segment)/(:segment)/(:segment)/(:num)', 'Report::opd_total_data/$1/$2/$3/$4/$5', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:num)', 'Report::report_total_payment_app_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]);
$routes->get('Report/report_total_payment_app_show/(:segment)/(:segment)/(:segment)/(:segment)', 'Report::report_total_payment_app_show/$1/$2/$3/$4', ['filter' => $reportsCollectionFilter]);

View File

@ -31,6 +31,162 @@ class Report extends BaseController
]);
}
public function report_opd_total()
{
$doctors = $this->db->table('doctor_master')
->select('id, p_fname')
->where('active', 1)
->orderBy('p_fname', 'ASC')
->get()
->getResult();
$feeTypes = $this->db->table('opd_master')
->select('opd_fee_id, opd_fee_desc, opd_fee_amount')
->where('opd_fee_id IS NOT NULL', null, false)
->where('opd_fee_id >', 0)
->where('opd_fee_desc IS NOT NULL', null, false)
->where("TRIM(opd_fee_desc) != ''", null, false)
->groupBy('opd_fee_id, opd_fee_desc, opd_fee_amount')
->orderBy('opd_fee_desc', 'ASC')
->orderBy('opd_fee_amount', 'ASC')
->get()
->getResult();
return view('report/opd_total_report', [
'doctors' => $doctors,
'fee_types' => $feeTypes,
]);
}
public function opd_total_data(
string $dateRange,
string $doctorId = '0',
string $opdType = '0',
string $feeId = '0',
int $output = 0
) {
[$minRange, $maxRange] = $this->parseDateRangeDateOnly($dateRange);
$runningExpr = "(COALESCE(o.running_opd,0)=1 OR o.opd_fee_type=3 OR UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%RUNNING%')";
$newExpr = "(o.opd_fee_type=1 OR UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%NEW%')";
$emergencyExpr = "(UPPER(COALESCE(o.opd_fee_desc,'')) LIKE '%EMERG%')";
$regularExpr = "(NOT {$runningExpr} AND NOT {$newExpr} AND NOT {$emergencyExpr})";
$builder = $this->db->table('opd_master o')
->select("COALESCE(NULLIF(TRIM(o.doc_name),''), CONCAT('Doctor #', o.doc_id)) as doctor_name", false)
->select('o.doc_id, o.opd_fee_id, o.opd_fee_desc, o.opd_fee_amount')
->select('SUM(CASE WHEN o.opd_status IN (1,2) THEN 1 ELSE 0 END) as no_of_opd', false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$runningExpr} THEN 1 ELSE 0 END) as running_count", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$regularExpr} THEN 1 ELSE 0 END) as regular_count", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$newExpr} THEN 1 ELSE 0 END) as new_count", false)
->select("SUM(CASE WHEN o.opd_status IN (1,2) AND {$emergencyExpr} THEN 1 ELSE 0 END) as emergency_count", false)
->select('SUM(CASE WHEN o.opd_status IN (1,2) THEN o.opd_fee_amount ELSE 0 END) as total_amount', false)
->where('o.apointment_date >=', $minRange)
->where('o.apointment_date <=', $maxRange)
->whereIn('o.opd_status', [1, 2])
->where('o.payment_status', 1)
->where('(o.payment_mode > 0 OR COALESCE(o.running_opd,0)=1)', null, false);
$doctorIdValue = (int) $doctorId;
if ($doctorIdValue > 0) {
$builder->where('o.doc_id', $doctorIdValue);
}
$feeIdValue = (int) $feeId;
if ($feeIdValue > 0) {
$builder->where('o.opd_fee_id', $feeIdValue);
}
$opdTypeKey = strtolower(trim((string) urldecode($opdType)));
if ($opdTypeKey !== '' && $opdTypeKey !== '0' && $opdTypeKey !== 'all') {
if ($opdTypeKey === 'running') {
$builder->where($runningExpr, null, false);
} elseif ($opdTypeKey === 'regular') {
$builder->where($regularExpr, null, false);
} elseif ($opdTypeKey === 'new') {
$builder->where($newExpr, null, false);
} elseif ($opdTypeKey === 'emergency') {
$builder->where($emergencyExpr, null, false);
}
}
$rows = $builder
->groupBy('o.doc_id, o.doc_name, o.opd_fee_id, o.opd_fee_desc, o.opd_fee_amount')
->orderBy('o.doc_name', 'ASC')
->orderBy('o.opd_fee_desc', 'ASC')
->orderBy('o.opd_fee_amount', 'ASC')
->get()
->getResult();
$summary = [
'no_of_opd' => 0,
'running_count' => 0,
'regular_count' => 0,
'new_count' => 0,
'emergency_count' => 0,
'total_amount' => 0.0,
];
foreach ($rows as $row) {
$summary['no_of_opd'] += (int) ($row->no_of_opd ?? 0);
$summary['running_count'] += (int) ($row->running_count ?? 0);
$summary['regular_count'] += (int) ($row->regular_count ?? 0);
$summary['new_count'] += (int) ($row->new_count ?? 0);
$summary['emergency_count'] += (int) ($row->emergency_count ?? 0);
$summary['total_amount'] += (float) ($row->total_amount ?? 0);
}
$content = view('report/opd_total_report_table', [
'rows' => $rows,
'summary' => $summary,
'min_range' => $minRange,
'max_range' => $maxRange,
]);
if ($output === 1) {
ExportExcel($content, 'OPD_Total_Report_' . date('YmdHis'));
return $this->response->setBody('');
}
if ($output === 2) {
$mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf';
if (! is_dir($mpdfTempDir)) {
mkdir($mpdfTempDir, 0755, true);
}
$mpdf = new Mpdf([
'tempDir' => $mpdfTempDir,
'format' => 'A4-L',
'margin_top' => 8,
'margin_bottom' => 8,
'margin_left' => 8,
'margin_right' => 8,
]);
$pdfHtml = '<style>'
. 'body{font-family:Arial,sans-serif;color:#111;font-size:12px;}'
. 'table{width:100%;border-collapse:collapse;}'
. 'th,td{border:1px solid #9aa4ad;padding:6px 8px;}'
. 'th{text-align:left;background:#f5f6f7;}'
. '.text-end{text-align:right;}'
. '.table-warning td,.table-warning th{background:#fff6cc !important;}'
. '</style>'
. '<h3 style="margin:0 0 10px 0;">OPD Total Report</h3>'
. $content;
$mpdf->WriteHTML($pdfHtml);
$fileName = 'OPD_Total_Report_' . date('Ymd_His') . '.pdf';
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
->setBody($mpdf->Output($fileName, 'S'));
}
return $this->response->setBody($content);
}
public function report_total_payment_app_show(
string $dateRange,
string $employeeIds,
@ -274,6 +430,26 @@ class Report extends BaseController
return [$start, $end];
}
private function parseDateRangeDateOnly(string $dateRange): array
{
$rangeParts = explode('S', $dateRange);
$startRaw = (string) ($rangeParts[0] ?? '');
$endRaw = (string) ($rangeParts[1] ?? '');
$startRaw = str_replace('T', ' ', $startRaw);
$endRaw = str_replace('T', ' ', $endRaw);
$start = substr(trim($startRaw), 0, 10);
$end = substr(trim($endRaw), 0, 10);
if ($start === '' || $end === '') {
$today = date('Y-m-d');
return [$today, $today];
}
return [$start, $end];
}
private function normalizeIdList(string $idList): array
{
$idList = trim($idList);

View File

@ -386,6 +386,12 @@ if (! $canChargesSettings && $user && method_exists($user, 'inGroup')) {
<span>Collection Report</span>
</a>
</li>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report')">
<i class="bi bi-bar-chart"></i>
<span>OPD Total Report</span>
</a>
</li>
<?php if ($canDiagnosis) { ?>
<li class="nav-item">
<a class="nav-link collapsed" href="javascript:load_form('<?= base_url('Report/diagnosis_report') ?>','Diagnosis Report')">

View File

@ -14,6 +14,7 @@
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/bank_deposits') ?>','Bank Deposit Register');"><i class="bi bi-arrow-right-short"></i> Bank Deposit Register</a></li>
</ul>
@ -29,6 +30,7 @@
</div>
<ul class="report-links">
<li><a href="javascript:load_form('<?= base_url('Report/collection_report') ?>','Collection Report');"><i class="bi bi-arrow-right-short"></i> Total Payment (Cash and Bank)</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/report_opd_total') ?>','OPD Total Report');"><i class="bi bi-arrow-right-short"></i> OPD Total Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Report/billing_operations_report') ?>','Billing Operations Report');"><i class="bi bi-arrow-right-short"></i> Billing Operations Report</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/index') ?>','Finance & Accounting');"><i class="bi bi-arrow-right-short"></i> Finance & Accounting (Phase 1)</a></li>
<li><a href="javascript:load_form('<?= base_url('Finance/phase2') ?>','Finance & Accounting - Phase 2');"><i class="bi bi-arrow-right-short"></i> Finance & Accounting (Phase 2)</a></li>

View File

@ -0,0 +1,116 @@
<?php
$doctors = $doctors ?? [];
$feeTypes = $fee_types ?? [];
?>
<section class="content">
<div class="card">
<div class="card-header">
<h5 class="mb-0">OPD Total Report</h5>
</div>
<div class="card-body">
<div class="row g-3 align-items-end">
<div class="col-md-3">
<label class="form-label">OPD Date Range</label>
<div class="d-flex gap-2">
<input type="date" class="form-control" id="opd_report_start">
<input type="date" class="form-control" id="opd_report_end">
</div>
</div>
<div class="col-md-3">
<label class="form-label">Doctor Name</label>
<select class="form-control select2" id="doc_name_id" name="doc_name_id">
<option value="0">All Doctors</option>
<?php foreach ($doctors as $row) : ?>
<option value="<?= esc($row->id ?? '') ?>"><?= esc($row->p_fname ?? '') ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3">
<label class="form-label">OPD Type</label>
<select class="form-control select2" id="opd_type_id" name="opd_type_id">
<option value="0">All Types</option>
<option value="running">Running OPD</option>
<option value="regular">Regular OPD</option>
<option value="new">New Patient OPD</option>
<option value="emergency">Emergency OPD</option>
</select>
</div>
<div class="col-md-3">
<label class="form-label">Specific Fee</label>
<select class="form-control select2" id="fee_id" name="fee_id">
<option value="0">All Fee Types</option>
<?php foreach ($feeTypes as $row) : ?>
<option value="<?= esc($row->opd_fee_id ?? '') ?>">
<?= esc(($row->opd_fee_desc ?? '') . ' (Fee: ' . number_format((float) ($row->opd_fee_amount ?? 0), 2) . ')') ?>
</option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="mt-3 d-flex flex-wrap gap-2">
<button type="button" class="btn btn-primary" id="show_opd_total_report">Show</button>
<button type="button" class="btn btn-outline-primary" id="export_opd_total_report">Export</button>
<button type="button" class="btn btn-outline-danger" id="pdf_opd_total_report">PDF</button>
</div>
</div>
</div>
<div class="card mt-3">
<div class="card-body">
<div id="opd_total_report_result" class="table-responsive">Select filters and click Show.</div>
</div>
</div>
</section>
<script>
(function() {
function toDateValue(date) {
var y = date.getFullYear();
var m = String(date.getMonth() + 1).padStart(2, '0');
var d = String(date.getDate()).padStart(2, '0');
return y + '-' + m + '-' + d;
}
var now = new Date();
var firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
var lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
document.getElementById('opd_report_start').value = toDateValue(firstDay);
document.getElementById('opd_report_end').value = toDateValue(lastDay);
function buildQuery() {
var startVal = document.getElementById('opd_report_start').value;
var endVal = document.getElementById('opd_report_end').value;
if (!startVal || !endVal) {
startVal = toDateValue(firstDay);
endVal = toDateValue(lastDay);
}
var dateRange = encodeURIComponent(startVal + 'S' + endVal);
var doctorId = document.getElementById('doc_name_id').value || '0';
var opdType = encodeURIComponent(document.getElementById('opd_type_id').value || '0');
var feeId = document.getElementById('fee_id').value || '0';
return '<?= base_url('Report/opd_total_data') ?>/' + dateRange + '/' + doctorId + '/' + opdType + '/' + feeId;
}
document.getElementById('show_opd_total_report').addEventListener('click', function() {
load_form_div(buildQuery(), 'opd_total_report_result');
});
document.getElementById('export_opd_total_report').addEventListener('click', function() {
window.open(buildQuery() + '/1', '_blank');
});
document.getElementById('pdf_opd_total_report').addEventListener('click', function() {
window.open(buildQuery() + '/2', '_blank');
});
$('.select2').select2({
theme: 'bootstrap4',
width: '100%'
});
})();
</script>

View File

@ -0,0 +1,78 @@
<?php
$rows = $rows ?? [];
$summary = $summary ?? [];
$minRange = $min_range ?? '';
$maxRange = $max_range ?? '';
$totalOpd = (int) ($summary['no_of_opd'] ?? 0);
$totalRunning = (int) ($summary['running_count'] ?? 0);
$totalRegular = (int) ($summary['regular_count'] ?? 0);
$totalNew = (int) ($summary['new_count'] ?? 0);
$totalEmergency = (int) ($summary['emergency_count'] ?? 0);
$totalAmount = (float) ($summary['total_amount'] ?? 0);
$formatDate = static function ($value): string {
$value = trim((string) $value);
if ($value === '') {
return '';
}
$timestamp = strtotime($value);
if ($timestamp === false) {
return $value;
}
return date('d-m-Y', $timestamp);
};
?>
<p class="mb-2"><strong>OPD Date Range:</strong> <?= esc($formatDate($minRange)) ?> to <?= esc($formatDate($maxRange)) ?></p>
<?php if (empty($rows)) : ?>
<div class="text-muted">No OPD records found for the selected filters.</div>
<?php else : ?>
<table class="table table-bordered table-sm align-middle">
<thead class="table-warning">
<tr>
<th style="width: 60px;">#</th>
<th>Doctor Name</th>
<th>OPD Type / Fee Type</th>
<th class="text-end" style="width: 95px;">OPD Fee</th>
<th class="text-end" style="width: 95px;">No. of OPD</th>
<th class="text-end" style="width: 95px;">Running</th>
<th class="text-end" style="width: 95px;">Regular</th>
<th class="text-end" style="width: 95px;">New</th>
<th class="text-end" style="width: 95px;">Emergency</th>
<th class="text-end" style="width: 110px;">Total Amt</th>
</tr>
</thead>
<tbody>
<?php $srNo = 1; ?>
<?php foreach ($rows as $row) : ?>
<tr>
<td><?= esc($srNo++) ?></td>
<td><?= esc('Dr. ' . ($row->doctor_name ?? '')) ?></td>
<td><?= esc($row->opd_fee_desc ?? '') ?></td>
<td class="text-end"><?= esc(number_format((float) ($row->opd_fee_amount ?? 0), 2)) ?></td>
<td class="text-end"><?= esc((int) ($row->no_of_opd ?? 0)) ?></td>
<td class="text-end"><?= esc((int) ($row->running_count ?? 0)) ?></td>
<td class="text-end"><?= esc((int) ($row->regular_count ?? 0)) ?></td>
<td class="text-end"><?= esc((int) ($row->new_count ?? 0)) ?></td>
<td class="text-end"><?= esc((int) ($row->emergency_count ?? 0)) ?></td>
<td class="text-end"><?= esc(number_format((float) ($row->total_amount ?? 0), 2)) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
<tfoot>
<tr class="table-warning">
<th colspan="4">Total</th>
<th class="text-end"><?= esc($totalOpd) ?></th>
<th class="text-end"><?= esc($totalRunning) ?></th>
<th class="text-end"><?= esc($totalRegular) ?></th>
<th class="text-end"><?= esc($totalNew) ?></th>
<th class="text-end"><?= esc($totalEmergency) ?></th>
<th class="text-end"><?= esc(number_format($totalAmount, 2)) ?></th>
</tr>
</tfoot>
</table>
<?php endif; ?>