feat: add scan doc list modal and print medicine formulation prefix

This commit is contained in:
DevSoftTech Server 2026-04-06 12:11:56 +05:30
parent 2154af394c
commit 30643c87f8
5 changed files with 274 additions and 7 deletions

View File

@ -477,12 +477,14 @@ $routes->post('Opd/confirm_payment', 'Opd::confirm_payment');
$routes->post('Opd/opd_discount_update/(:num)', 'Opd::opd_discount_update/$1');
$routes->post('Opd/opd_cancel/(:num)', 'Opd::opd_cancel/$1');
$routes->match(['get', 'post'], 'Opd/opd_load_doc/(:num)', 'Opd::opd_load_doc/$1');
$routes->match(['get', 'post'], 'Opd/opd_file_list/(:num)', 'Opd::opd_file_list/$1');
$routes->match(['get', 'post'], 'Opd/opd_file_last_list', 'Opd::opd_file_last_list');
$routes->match(['get', 'post'], 'Opd/opd_file_last_list/(:num)', 'Opd::opd_file_last_list/$1');
$routes->match(['get', 'post'], 'Opd/save_image/(:num)', 'Opd::save_image/$1');
$routes->post('Opd/scan_ai_process_file', 'Opd::scan_ai_process_file');
$routes->post('Opd/scan_ai_assist', 'Opd::scan_ai_assist');
$routes->match(['get', 'post'], 'Opd/opd_file_hide/(:num)', 'Opd::opd_file_hide/$1');
$routes->match(['get', 'post'], 'Opd/opd_file_delete/(:num)', 'Opd::opd_file_delete/$1');
$routes->get('Opd/invoice/(:num)', 'Opd::invoice/$1');
$routes->get('Opd/invoice_print/(:num)', 'Opd::invoice_print/$1');
$routes->get('Opd/opd_lettre_print/(:num)', 'Opd::opd_lettre_print/$1');

View File

@ -1924,6 +1924,30 @@ class Opd extends BaseController
foreach ($rxMeds as $med) {
$i++;
$name = trim((string) ($med['med_name'] ?? $med['drug_name'] ?? $med['medicine_name'] ?? $med['item_name'] ?? ''));
$formulationRaw = trim((string) ($med['formulation'] ?? $med['dosage_form'] ?? $med['form'] ?? $med['medicine_form'] ?? ''));
$formulationPrefixMap = [
'tablet' => 'Tab',
'tab' => 'Tab',
'capsule' => 'Cap',
'cap' => 'Cap',
'syrup' => 'Syp',
'syp' => 'Syp',
'injection' => 'Inj',
'inj' => 'Inj',
'drop' => 'Drop',
'drops' => 'Drops',
'cream' => 'Cream',
'ointment' => 'Oint',
'gel' => 'Gel',
'lotion' => 'Lotion',
'powder' => 'Powder',
'sachet' => 'Sachet',
];
$formulationKey = strtolower($formulationRaw);
$formulationPrefix = $formulationPrefixMap[$formulationKey] ?? $formulationRaw;
if ($formulationPrefix !== '' && $name !== '' && preg_match('/^' . preg_quote($formulationPrefix, '/') . '\b/i', $name) !== 1) {
$name = trim($formulationPrefix . ' ' . $name);
}
$generic = trim((string) ($med['genericname'] ?? $med['generic_name'] ?? $med['salt_name'] ?? ''));
$dose = trim((string) ($med['dosage_label'] ?? $med['dosage'] ?? $med['drug_dose'] ?? $med['dose'] ?? ''));
$freq = trim((string) ($med['dosage_freq_label'] ?? $med['dosage_freq'] ?? $med['drug_freq'] ?? $med['frequency'] ?? ''));
@ -3257,7 +3281,9 @@ class Opd extends BaseController
$mapFreqLocal = [];
$mapWhereLocal = [];
$medGenericMap = [];
$medFormulationMap = [];
$missingGenericMedIds = [];
$missingFormulationMedIds = [];
if ($this->db->tableExists('opd_dose_shed')) {
$f = $this->db->getFieldNames('opd_dose_shed');
@ -3380,20 +3406,40 @@ class Opd extends BaseController
$missingGenericMedIds[$medId] = true;
}
}
$formulationInline = trim((string) ($medRow['formulation'] ?? ($medRow['dosage_form'] ?? '')));
if ($formulationInline === '') {
$medId = (int) ($medRow['med_id'] ?? 0);
if ($medId > 0) {
$missingFormulationMedIds[$medId] = true;
}
}
}
unset($medRow);
if (!empty($missingGenericMedIds) && $this->db->tableExists('opd_med_master')) {
if ((!empty($missingGenericMedIds) || !empty($missingFormulationMedIds)) && $this->db->tableExists('opd_med_master')) {
$masterFields = $this->db->getFieldNames('opd_med_master');
$masterIdField = $this->resolveFirstField($masterFields, ['id']);
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$masterFormulationField = $this->resolveFirstField($masterFields, ['formulation', 'dosage_form', 'form']);
$labelField = $masterSaltField ?? $masterGenericField;
$select = [];
if ($masterIdField !== null) {
$select[] = $masterIdField . ' as med_id';
}
if ($labelField !== null) {
$select[] = $labelField . ' as generic_fallback';
}
if ($masterFormulationField !== null) {
$select[] = $masterFormulationField . ' as formulation_fallback';
}
if ($masterIdField !== null && $labelField !== null) {
if ($masterIdField !== null && !empty($select)) {
$neededIds = array_unique(array_map('intval', array_merge(array_keys($missingGenericMedIds), array_keys($missingFormulationMedIds))));
$rows = $this->db->table('opd_med_master')
->select($masterIdField . ' as med_id,' . $labelField . ' as generic_fallback')
->whereIn($masterIdField, array_map('intval', array_keys($missingGenericMedIds)))
->select(implode(',', $select))
->whereIn($masterIdField, $neededIds)
->get()
->getResultArray();
@ -3403,6 +3449,10 @@ class Opd extends BaseController
if ($id > 0 && $label !== '') {
$medGenericMap[$id] = $label;
}
$formulation = trim((string) ($r['formulation_fallback'] ?? ''));
if ($id > 0 && $formulation !== '') {
$medFormulationMap[$id] = $formulation;
}
}
}
}
@ -3415,6 +3465,14 @@ class Opd extends BaseController
$medRow['genericname'] = $fallback;
}
}
$formulationInline = trim((string) ($medRow['formulation'] ?? ($medRow['dosage_form'] ?? '')));
if ($formulationInline === '') {
$formulationFallback = $medFormulationMap[(int) ($medRow['med_id'] ?? 0)] ?? '';
if ($formulationFallback !== '') {
$medRow['formulation'] = $formulationFallback;
}
}
}
unset($medRow);
}
@ -4193,6 +4251,61 @@ class Opd extends BaseController
return view('billing/opd_scan_last_list', ['opd_file_list' => $list]);
}
public function opd_file_list(int $opdid)
{
if (! $this->db->tableExists('file_upload_data')) {
return $this->response->setBody('<div class="text-muted">No scan table found.</div>');
}
$fields = $this->db->getFieldNames('file_upload_data');
$safeSelect = ['id', 'full_path', 'file_ext', 'insert_date'];
if (in_array('show_type', $fields, true)) {
$safeSelect[] = 'show_type';
}
$rows = $this->db->table('file_upload_data')
->select(implode(',', $safeSelect))
->where('opd_id', (int) $opdid)
->where('show_type', 0)
->orderBy('id', 'ASC')
->get()
->getResultArray();
$today = date('Y-m-d');
$slides = [];
foreach ($rows as $row) {
$rawPath = (string) ($row['full_path'] ?? '');
$normalizedPath = str_replace('\\', '/', $rawPath);
$publicPath = $normalizedPath;
$uploadPos = strpos($normalizedPath, '/uploads/');
if ($uploadPos !== false) {
$publicPath = substr($normalizedPath, $uploadPos);
}
$ext = strtolower((string) ($row['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION)));
if ($ext !== '' && $ext[0] === '.') {
$ext = substr($ext, 1);
}
$insertDate = (string) ($row['insert_date'] ?? '');
$insertYmd = $insertDate !== '' ? date('Y-m-d', strtotime($insertDate)) : '';
$canDeleteToday = $insertYmd === $today;
$slides[] = [
'id' => (int) ($row['id'] ?? 0),
'path' => $publicPath,
'is_pdf' => $ext === 'pdf',
'insert_date' => $insertDate !== '' ? date('d-m-Y H:i', strtotime($insertDate)) : '',
'can_delete_today' => $canDeleteToday ? 1 : 0,
];
}
return view('billing/opd_scan_carousel_list', [
'opdid' => (int) $opdid,
'slides' => $slides,
]);
}
public function scan_ai_assist()
{
if (! $this->request->isAJAX()) {
@ -4319,6 +4432,59 @@ class Opd extends BaseController
return $this->opd_file_last_list((int) ($row['opd_id'] ?? 0));
}
public function opd_file_delete(int $fileId)
{
if (! $this->db->tableExists('file_upload_data')) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'File table not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$row = $this->db->table('file_upload_data')
->select('id,opd_id,full_path,insert_date')
->where('id', $fileId)
->get(1)
->getRowArray();
if (empty($row)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'File not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$insertDate = (string) ($row['insert_date'] ?? '');
$isToday = $insertDate !== '' && date('Y-m-d', strtotime($insertDate)) === date('Y-m-d');
if (! $isToday) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Delete allowed only for current date uploads',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$fullPath = (string) ($row['full_path'] ?? '');
if ($fullPath !== '' && is_file($fullPath)) {
@unlink($fullPath);
}
$this->db->table('file_upload_data')->where('id', (int) $fileId)->delete();
return $this->response->setJSON([
'update' => 1,
'opd_id' => (int) ($row['opd_id'] ?? 0),
'error_text' => 'Scan deleted',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function insertOpdFileUploadRecord(string $fullPath, string $publicPath, int $pid, int $opdid, int $caseId, string $uploadBy, int $uploadById, string $mimeType = 'image/jpeg', int $isImageFile = 1, string $fileExt = '.jpg'): int
{
if (!$this->db->tableExists('file_upload_data')) {

View File

@ -197,6 +197,58 @@
});
});
$(document).off('click.opdscanlistopen', '.btn-opd-scan-list').on('click.opdscanlistopen', '.btn-opd-scan-list', function() {
// Open modal with slide-by-slide scan document list for this OPD.
var opdid = parseInt($(this).data('opdid') || '0', 10);
if (!opdid || !modalObj) {
return;
}
$('#testentryLabel').text('Scan Document List');
$('#testentry-bodyc').html('<div class="text-muted">Loading...</div>');
modalObj.show();
$.post('/Opd/opd_file_list/' + opdid, {}, function(html) {
$('#testentry-bodyc').html(html || '<div class="text-danger">No scan documents found.</div>');
}).fail(function() {
$('#testentry-bodyc').html('<div class="text-danger">Unable to load scan document list.</div>');
});
});
window.deleteOpdScanFromList = function(fileId, opdid) {
// Delete only allowed for current-date uploads; server enforces this rule.
if (!fileId || !opdid) {
return;
}
if (!window.confirm('Delete this scan document?')) {
return;
}
var csrf = getCsrfPair();
var payload = {};
payload[csrf.name] = csrf.value;
$.post('/Opd/opd_file_delete/' + fileId, payload, function(resp) {
if (resp && resp.csrfName && resp.csrfHash) {
updateCsrf(resp);
}
if (!resp || parseInt(resp.update || 0, 10) !== 1) {
var msg = (resp && resp.error_text) ? resp.error_text : 'Unable to delete document.';
$('#testentry-bodyc').prepend('<div class="alert alert-danger py-1 mb-2">' + $('<div>').text(msg).html() + '</div>');
return;
}
$.post('/Opd/opd_file_list/' + opdid, {}, function(html) {
$('#testentry-bodyc').html(html || '<div class="text-muted">No scan documents found.</div>');
}).fail(function() {
$('#testentry-bodyc').prepend('<div class="alert alert-danger py-1 mb-2">Unable to refresh scan list.</div>');
});
}, 'json').fail(function() {
$('#testentry-bodyc').prepend('<div class="alert alert-danger py-1 mb-2">Unable to delete document.</div>');
});
};
$(document).off('click.opdstatus', '.btn-opd-status').on('click.opdstatus', '.btn-opd-status', function() {
var opdId = $(this).data('opd-id');
var opdStatus = $(this).data('opd-status');

View File

@ -44,9 +44,9 @@ if ($authUser) {
<button type="button" class="btn btn-outline-secondary btn-sm btn-opd-scan" title="Upload Scan" data-opdid="<?= esc((int) ($row->opd_id ?? 0)) ?>">
Upload
</button>
<a class="btn btn-outline-dark btn-sm" title="Profile" href="javascript:load_form('/billing/patient/person_record/<?= esc((int) ($row->id ?? 0)) ?>/0','Patient Profile');">
Profile
</a>
<button type="button" class="btn btn-outline-dark btn-sm btn-opd-scan-list" title="Scan Document List" data-opdid="<?= esc((int) ($row->opd_id ?? 0)) ?>">
Scan Doc List
</button>
<?php if (($tabType ?? '') === 'waiting') : ?>
<button type="button" class="btn btn-outline-success btn-sm btn-opd-status" data-opd-id="<?= esc((int) ($row->opd_id ?? 0)) ?>" data-opd-status="2" title="Visit Done">
Visit Done

View File

@ -0,0 +1,47 @@
<?php $items = $slides ?? []; ?>
<?php if (empty($items)) : ?>
<div class="text-muted">No scan documents found.</div>
<?php else : ?>
<div id="opdScanCarousel" class="carousel slide" data-bs-ride="false">
<div class="carousel-indicators">
<?php foreach ($items as $index => $item) : ?>
<button type="button" data-bs-target="#opdScanCarousel" data-bs-slide-to="<?= (int) $index ?>" class="<?= $index === 0 ? 'active' : '' ?>" <?= $index === 0 ? 'aria-current="true"' : '' ?> aria-label="Slide <?= (int) ($index + 1) ?>"></button>
<?php endforeach; ?>
</div>
<div class="carousel-inner bg-light rounded border" style="min-height:420px;">
<?php foreach ($items as $index => $item) : ?>
<div class="carousel-item <?= $index === 0 ? 'active' : '' ?>" style="padding:14px;">
<div class="text-center">
<?php if (!empty($item['is_pdf'])) : ?>
<iframe src="<?= esc($item['path']) ?>" style="width:100%;height:460px;border:1px solid #dee2e6;"></iframe>
<div class="mt-2"><a class="btn btn-outline-secondary btn-sm" target="_blank" href="<?= esc($item['path']) ?>">Open PDF in new tab</a></div>
<?php else : ?>
<img src="<?= esc($item['path']) ?>" alt="Scan Document" class="img-fluid rounded" style="max-height:520px;">
<?php endif; ?>
</div>
<div class="d-flex justify-content-between align-items-center mt-2">
<div class="small text-muted">
Uploaded: <?= esc($item['insert_date'] ?? '') ?>
</div>
<?php if ((int) ($item['can_delete_today'] ?? 0) === 1) : ?>
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deleteOpdScanFromList(<?= (int) ($item['id'] ?? 0) ?>, <?= (int) ($opdid ?? 0) ?>)">Delete</button>
<?php else : ?>
<span class="badge bg-secondary">Delete allowed only for today upload</span>
<?php endif; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<button class="carousel-control-prev" type="button" data-bs-target="#opdScanCarousel" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">Previous</span>
</button>
<button class="carousel-control-next" type="button" data-bs-target="#opdScanCarousel" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">Next</span>
</button>
</div>
<?php endif; ?>