feat(doctor): add Doctor App PIN auth, QR generator, DoctorCare EMR PWA (/app/doctor), and bedside clinical tools

This commit is contained in:
DevSoftTech Server 2026-08-28 19:44:54 +05:30
parent 405226d66c
commit a6beb124cd
9 changed files with 1262 additions and 26 deletions

View File

@ -1562,9 +1562,11 @@ $routes->get('Document_Patient/health_document_fhir_preview/(:num)', 'DoctorDocu
* --------------------------------------------------------------------
*/
// Dedicated PWA Web App Endpoint (Serves React PWA without touching standard CI4 web views)
// Dedicated PWA Web App Endpoints (Serves React PWAs without touching standard CI4 web views)
$routes->get('app/nursing', 'Api\v1\NursingApi::pwaIndex');
$routes->get('app/nursing/(:any)', 'Api\v1\NursingApi::pwaIndex');
$routes->get('app/doctor', 'Api\v1\DoctorApi::pwaIndex');
$routes->get('app/doctor/(:any)', 'Api\v1\DoctorApi::pwaIndex');
// Dedicated RESTful JSON API v1 Group for React PWAs & Mobile Apps
$routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static function ($routes) {
@ -1575,5 +1577,14 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
$routes->post('nursing/auth/set-pin', 'NursingApi::setPin');
$routes->get('nursing/workspace/(:num)', 'NursingApi::workspace/$1');
$routes->post('nursing/entry/save/(:num)', 'NursingApi::saveEntry/$1');
// Doctor PWA APIs
$routes->get('doctor/list', 'DoctorApi::doctors');
$routes->post('doctor/auth/verify-pin', 'DoctorApi::verifyPin');
$routes->post('doctor/auth/set-pin', 'DoctorApi::setPin');
$routes->get('doctor/opd/list/(:num)', 'DoctorApi::opdList/$1');
$routes->get('doctor/ipd/list/(:num)', 'DoctorApi::ipdList/$1');
$routes->get('doctor/ipd/workspace/(:num)', 'DoctorApi::patientWorkspace/$1');
$routes->post('doctor/ipd/treatment/save/(:num)', 'DoctorApi::saveTreatmentNote/$1');
});

View File

@ -0,0 +1,262 @@
<?php
namespace App\Controllers\Api\v1;
use App\Controllers\BaseController;
use App\Models\DoctorModel;
use App\Models\IpdNursingEntryModel;
use App\Models\OpdModel;
class DoctorApi extends BaseController
{
protected DoctorModel $doctorModel;
protected IpdNursingEntryModel $ipdNursingEntryModel;
protected OpdModel $opdModel;
public function __construct()
{
$this->doctorModel = new DoctorModel();
$this->ipdNursingEntryModel = new IpdNursingEntryModel();
$this->opdModel = new OpdModel();
}
/**
* Serves the React PWA app entry point at /app/doctor
*/
public function pwaIndex()
{
$pwaPath = FCPATH . 'App/Doctor/index.html';
if (file_exists($pwaPath)) {
return $this->response->setBody(file_get_contents($pwaPath));
}
return $this->response->setJSON([
'app_name' => 'Doctor EMR & Bedside Care PWA Gateway',
'status' => 'ready',
'api_base_url' => base_url('api/v1/doctor/'),
'message' => 'React PWA build target directory /App/Doctor is active. API endpoints are ready.',
'endpoints' => [
'doctors' => base_url('api/v1/doctor/list'),
'verify_pin' => base_url('api/v1/doctor/auth/verify-pin'),
'set_pin' => base_url('api/v1/doctor/auth/set-pin'),
'opd_list' => base_url('api/v1/doctor/opd/list/{docId}'),
'ipd_list' => base_url('api/v1/doctor/ipd/list/{docId}'),
'patient_workspace' => base_url('api/v1/doctor/ipd/workspace/{ipdId}'),
'save_treatment' => base_url('api/v1/doctor/ipd/treatment/save/{ipdId}'),
]
]);
}
/**
* GET api/v1/doctor/list
*/
public function doctors()
{
$doctors = $this->doctorModel->getDoctors();
$list = [];
foreach ($doctors as $d) {
$list[] = [
'id' => (int) $d->id,
'name' => trim(($d->p_title ?? '') . ' ' . ($d->p_fname ?? '')),
'email' => $d->email1 ?? '',
'phone' => $d->mphone1 ?? '',
'doc_sign' => $d->doc_sign ?? '',
'has_pin' => ! empty($d->app_pin),
];
}
return $this->response->setJSON([
'status' => 1,
'data' => $list,
]);
}
/**
* POST api/v1/doctor/auth/verify-pin
*/
public function verifyPin()
{
$post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []);
$docId = (int) ($post['doctor_id'] ?? 0);
$pin = trim((string) ($post['pin'] ?? ''));
if ($docId <= 0) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Doctor ID is required']);
}
$doctorRows = $this->doctorModel->getDoctorById($docId);
$doctorRow = $doctorRows[0] ?? null;
if (! $doctorRow) {
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Doctor profile not found']);
}
$hashedPin = (string) ($doctorRow->app_pin ?? '');
if ($hashedPin === '') {
return $this->response->setJSON([
'status' => 2,
'message' => 'PIN not set yet. Please set a 4-6 digit PIN.',
'doctor' => [
'id' => (int) $doctorRow->id,
'name' => trim(($doctorRow->p_title ?? '') . ' ' . ($doctorRow->p_fname ?? '')),
],
]);
}
$isValid = password_verify($pin, $hashedPin) || ($pin === $hashedPin);
if (! $isValid) {
return $this->response->setStatusCode(401)->setJSON(['status' => 0, 'message' => 'Incorrect Security PIN']);
}
return $this->response->setJSON([
'status' => 1,
'message' => 'Authentication successful',
'doctor' => [
'id' => (int) $doctorRow->id,
'name' => trim(($doctorRow->p_title ?? '') . ' ' . ($doctorRow->p_fname ?? '')),
],
]);
}
/**
* POST api/v1/doctor/auth/set-pin
*/
public function setPin()
{
$post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []);
$docId = (int) ($post['doctor_id'] ?? 0);
$newPin = trim((string) ($post['new_pin'] ?? ''));
$oldPin = trim((string) ($post['old_pin'] ?? ''));
if ($docId <= 0 || strlen($newPin) < 4) {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Valid Doctor ID and 4-6 digit PIN are required']);
}
$doctorRows = $this->doctorModel->getDoctorById($docId);
$doctorRow = $doctorRows[0] ?? null;
if (! $doctorRow) {
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Doctor profile not found']);
}
$hashedPin = (string) ($doctorRow->app_pin ?? '');
if ($hashedPin !== '' && $oldPin !== '') {
$isValid = password_verify($oldPin, $hashedPin) || ($oldPin === $hashedPin);
if (! $isValid) {
return $this->response->setStatusCode(401)->setJSON(['status' => 0, 'message' => 'Current PIN is incorrect']);
}
}
$this->doctorModel->updateDoctor([
'app_pin' => password_hash($newPin, PASSWORD_DEFAULT),
], $docId);
return $this->response->setJSON([
'status' => 1,
'message' => 'Security PIN updated successfully',
]);
}
/**
* GET api/v1/doctor/opd/list/(:num)
*/
public function opdList(int $docId)
{
$db = db_connect();
$builder = $db->table('opd_master opd')
->select('opd.*, p.fname, p.mname, p.lname, p.uhid, p.mphone1, p.gender, p.age')
->join('patient_master p', 'p.id = opd.patient_id', 'left')
->orderBy('opd.id', 'DESC')
->limit(50);
if ($docId > 0 && $db->fieldExists('doctor_id', 'opd_master')) {
$builder->where('opd.doctor_id', $docId);
}
$records = $builder->get()->getResultArray();
return $this->response->setJSON([
'status' => 1,
'doctor_id' => $docId,
'appointments' => $records,
]);
}
/**
* GET api/v1/doctor/ipd/list/(:num)
*/
public function ipdList(int $docId)
{
$db = db_connect();
$builder = $db->table('ipd_master ipd')
->select('ipd.*, p.fname, p.mname, p.lname, p.uhid, p.mphone1, p.gender, p.age, b.bed_code, b.bed_number, w.ward_name')
->join('patient_master p', 'p.id = ipd.patient_id', 'left')
->join('bed_master b', 'b.id = ipd.bed_id', 'left')
->join('ward_master w', 'w.id = b.ward_id', 'left')
->where('ipd.status', 'admitted')
->orderBy('ipd.id', 'DESC');
if ($docId > 0 && $db->fieldExists('doctor_id', 'ipd_master')) {
$builder->where('ipd.doctor_id', $docId);
}
$patients = $builder->get()->getResultArray();
return $this->response->setJSON([
'status' => 1,
'doctor_id' => $docId,
'patients' => $patients,
]);
}
/**
* GET api/v1/doctor/ipd/workspace/(:num)
*/
public function patientWorkspace(int $ipdId)
{
$entries = $this->ipdNursingEntryModel->getByIpd($ipdId);
return $this->response->setJSON([
'status' => 1,
'ipd_id' => $ipdId,
'entries' => $entries,
]);
}
/**
* POST api/v1/doctor/ipd/treatment/save/(:num)
*/
public function saveTreatmentNote(int $ipdId)
{
$post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []);
$treatmentText = trim((string) ($post['treatment_text'] ?? ''));
if ($treatmentText === '') {
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Doctor treatment note / order is required']);
}
$docId = (int) ($post['doctor_id'] ?? 0);
$doctorName = trim((string) ($post['doctor_name'] ?? 'Doctor'));
$data = [
'ipd_id' => $ipdId,
'entry_type' => 'treatment',
'recorded_at' => date('Y-m-d H:i:s'),
'treatment_text' => '[Doctor Note] ' . $treatmentText,
'general_note' => (string) ($post['general_note'] ?? ''),
'recorded_by' => 'Dr. ' . $doctorName,
'recorded_by_id' => $docId,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
];
$this->ipdNursingEntryModel->insert($data);
return $this->response->setJSON([
'status' => 1,
'message' => 'Doctor clinical note / treatment saved successfully',
]);
}
}

View File

@ -128,6 +128,11 @@ class Doctor extends BaseController
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
}
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
if ($appPin !== '') {
$data['app_pin'] = password_hash($appPin, PASSWORD_DEFAULT);
}
$insertId = $doctorModel->insert($data);
if ($insertId <= 0) {
@ -246,6 +251,11 @@ class Doctor extends BaseController
$data['healthplix_doctor_identifier'] = $healthplixDoctorIdentifier;
}
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
if ($appPin !== '') {
$data['app_pin'] = password_hash($appPin, PASSWORD_DEFAULT);
}
$updated = $doctorModel->updateDoctor($data, $docId);
return $this->response->setJSON([

View File

@ -11,6 +11,18 @@ class DoctorModel
public function __construct()
{
$this->db = db_connect();
$this->ensureDoctorMasterTable();
}
public function ensureDoctorMasterTable(): bool
{
if ($this->db->tableExists('doctor_master')) {
$fields = $this->db->getFieldNames('doctor_master') ?? [];
if (! in_array('app_pin', $fields, true)) {
$this->db->query("ALTER TABLE doctor_master ADD COLUMN app_pin VARCHAR(255) NULL");
}
}
return true;
}
public function getDoctors(): array

View File

@ -35,6 +35,9 @@
<td><?= esc($row->mphone1 ?? '') ?></td>
<td><?= esc($row->email1 ?? '') ?></td>
<td>
<button type="button" class="btn btn-outline-dark btn-sm me-1 btn-qr-doctor" data-id="<?= (int)$row->id ?>" data-name="<?= esc(($row->p_title ?? '') . ' ' . ($row->p_fname ?? '')) ?>">
<i class="bi bi-qr-code"></i> QR
</button>
<button onclick="load_form_div('<?= base_url('setting/admin/doctor/' . $row->id) ?>','maindiv','<?= esc(($row->p_title ?? '') . ' ' . ($row->p_fname ?? '')) ?>');" type="button" class="btn btn-success btn-sm">Edit</button>
</td>
</tr>
@ -49,3 +52,57 @@
</div>
</div>
</div>
<!-- Modal for Doctor App QR Code -->
<div class="modal fade" id="doctorQrModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-sm">
<div class="modal-content text-center shadow-lg border-0">
<div class="modal-header bg-primary text-white py-2">
<h5 class="modal-title fs-6 mb-0" id="docQrModalTitle"><i class="bi bi-qr-code-scan me-1"></i> Doctor App QR Code</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body py-3">
<h6 class="fw-bold mb-1 text-dark" id="qr_doc_name">Doctor Name</h6>
<span class="badge bg-primary mb-3">Doctor PWA Access</span>
<div class="p-2 border rounded d-inline-block bg-white shadow-sm mb-3">
<img id="qr_doc_code_img" src="" alt="Doctor App QR Code" style="width: 180px; height: 180px;" />
</div>
<p class="small text-muted mb-0" style="font-size: 11px;">Scan with mobile phone to launch <strong>DoctorCare PWA</strong> for this doctor profile.</p>
</div>
<div class="modal-footer bg-light py-2 justify-content-center">
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
<script>
(function() {
var doctorAppBaseUrl = '<?= base_url('app/doctor') ?>';
document.addEventListener('click', function(e) {
var qrBtn = e.target.closest('.btn-qr-doctor');
if (qrBtn) {
var docId = qrBtn.getAttribute('data-id') || '0';
var name = qrBtn.getAttribute('data-name') || '';
var appUrl = doctorAppBaseUrl + '?doctor_id=' + encodeURIComponent(docId);
var qrApiUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=' + encodeURIComponent(appUrl);
document.getElementById('qr_doc_name').textContent = name;
document.getElementById('qr_doc_code_img').src = qrApiUrl;
var modalEl = document.getElementById('doctorQrModal');
if (modalEl) {
if (modalEl.parentElement !== document.body) {
document.body.appendChild(modalEl);
}
if (window.bootstrap && window.bootstrap.Modal) {
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
} else if (typeof $ !== 'undefined' && $.fn && $.fn.modal) {
$(modalEl).modal('show');
}
}
}
});
})();
</script>

View File

@ -302,17 +302,17 @@
<label class="form-label fw-bold small text-secondary mb-2">1. SELECT APPLICATION</label>
<div class="row g-2 mb-3">
<div class="col-4">
<div class="card p-2 text-center border-primary bg-primary-subtle text-primary h-100" style="cursor: pointer;" id="app_card_nursing">
<div class="card p-2 text-center border-primary bg-primary-subtle text-primary h-100 app-card-item active" style="cursor: pointer;" id="app_card_nursing" data-app="nursing">
<i class="bi bi-heart-pulse-fill fs-3 mb-1"></i>
<div class="fw-bold small" style="font-size:12px;">Nursing Care</div>
<span class="badge bg-primary mt-1" style="font-size:9px;">Active PWA</span>
</div>
</div>
<div class="col-4">
<div class="card p-2 text-center border-secondary bg-light text-muted opacity-75 h-100">
<i class="bi bi-person-badge-fill fs-3 mb-1"></i>
<div class="card p-2 text-center border-secondary bg-light text-dark h-100 app-card-item" style="cursor: pointer;" id="app_card_doctor" data-app="doctor">
<i class="bi bi-person-badge-fill fs-3 mb-1 text-primary"></i>
<div class="fw-bold small" style="font-size:12px;">Doctor App</div>
<span class="badge bg-secondary mt-1" style="font-size:9px;">Coming Soon</span>
<span class="badge bg-success mt-1" style="font-size:9px;">Active PWA</span>
</div>
</div>
<div class="col-4">
@ -326,7 +326,7 @@
<!-- Step 2: Staff User Selection -->
<div class="mb-3">
<label class="form-label fw-bold small text-secondary mb-1">2. SELECT NURSING STAFF PROFILE</label>
<label class="form-label fw-bold small text-secondary mb-1" id="hdr_apps_staff_label">2. SELECT NURSING STAFF PROFILE</label>
<select class="form-select form-select-sm" id="hdr_apps_nurse_select">
<option value="">-- General / All Staff Access --</option>
</select>
@ -360,15 +360,18 @@
<script>
(function() {
var selectedApp = 'nursing';
function getAppBaseUrl() {
return '<?= base_url('app/nursing') ?>';
return '<?= base_url('app') ?>/' + selectedApp;
}
function updateHdrAppDetails() {
var sel = document.getElementById('hdr_apps_nurse_select');
var code = sel ? sel.value : '';
var appBaseUrl = getAppBaseUrl();
var appUrl = appBaseUrl + (code ? '?nurse_code=' + encodeURIComponent(code) : '');
var paramKey = (selectedApp === 'doctor') ? 'doctor_id=' : 'nurse_code=';
var appUrl = appBaseUrl + (code ? '?' + paramKey + encodeURIComponent(code) : '');
var qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=' + encodeURIComponent(appUrl);
var qrImg = document.getElementById('hdr_apps_qr_img');
@ -381,13 +384,15 @@
if (urlInput) urlInput.value = appUrl;
if (launchLink) launchLink.href = appUrl;
var title = (selectedApp === 'doctor') ? 'DoctorCare EMR App' : 'Nursing Care App';
if (code && sel && sel.options[sel.selectedIndex]) {
var text = sel.options[sel.selectedIndex].text;
if (targetName) targetName.textContent = 'Nursing Care App: ' + text;
if (targetName) targetName.textContent = title + ': ' + text;
if (targetCode) targetCode.textContent = code;
} else {
if (targetName) targetName.textContent = 'Nursing Care App';
if (targetCode) targetCode.textContent = '/app/nursing';
if (targetName) targetName.textContent = title;
if (targetCode) targetCode.textContent = '/app/' + selectedApp;
}
}
@ -405,13 +410,32 @@
}
}
function openAppsModal() {
var apiUrl = '<?= base_url('api/v1/nursing/nurses') ?>';
fetch(apiUrl)
.then(function(res) { return res.json(); })
.then(function(resp) {
var sel = document.getElementById('hdr_apps_nurse_select');
if (sel) {
function loadAppStaffList() {
var sel = document.getElementById('hdr_apps_nurse_select');
var label = document.getElementById('hdr_apps_staff_label');
if (!sel) return;
if (selectedApp === 'doctor') {
if (label) label.textContent = '2. SELECT DOCTOR PROFILE';
fetch('<?= base_url('api/v1/doctor/list') ?>')
.then(function(r) { return r.json(); })
.then(function(resp) {
sel.innerHTML = '<option value="">-- General / All Doctor Access --</option>';
if (resp && resp.status === 1 && Array.isArray(resp.data)) {
resp.data.forEach(function(d) {
var opt = document.createElement('option');
opt.value = d.id;
opt.textContent = d.name;
sel.appendChild(opt);
});
}
updateHdrAppDetails();
});
} else {
if (label) label.textContent = '2. SELECT NURSING STAFF PROFILE';
fetch('<?= base_url('api/v1/nursing/nurses') ?>')
.then(function(r) { return r.json(); })
.then(function(resp) {
sel.innerHTML = '<option value="">-- General / All Staff Access --</option>';
if (resp && resp.status === 1 && Array.isArray(resp.data)) {
resp.data.forEach(function(n) {
@ -421,14 +445,14 @@
sel.appendChild(opt);
});
}
}
updateHdrAppDetails();
showAppsModal();
})
.catch(function(err) {
updateHdrAppDetails();
showAppsModal();
});
updateHdrAppDetails();
});
}
}
function openAppsModal() {
loadAppStaffList();
showAppsModal();
}
document.addEventListener('click', function(e) {
@ -439,6 +463,22 @@
return;
}
var appCard = e.target.closest('.app-card-item');
if (appCard) {
var appType = appCard.getAttribute('data-app');
if (appType) {
selectedApp = appType;
document.querySelectorAll('.app-card-item').forEach(function(c) {
c.classList.remove('border-primary', 'bg-primary-subtle', 'text-primary');
c.classList.add('border-secondary', 'bg-light', 'text-dark');
});
appCard.classList.remove('border-secondary', 'bg-light', 'text-dark');
appCard.classList.add('border-primary', 'bg-primary-subtle', 'text-primary');
loadAppStaffList();
}
return;
}
var copyBtn = e.target.closest('#btn_hdr_apps_copy_url');
if (copyBtn) {
e.preventDefault();

View File

@ -0,0 +1,807 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Doctor EMR & Bedside Care PWA App</title>
<link rel="manifest" href="/App/Doctor/manifest.json">
<meta name="theme-color" content="#0d6efd">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="apple-touch-icon" href="https://cdn-icons-png.flaticon.com/512/3774/3774299.png">
<!-- Bootstrap 5 CSS & Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css">
<!-- React 18 & ReactDOM UMD -->
<script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js" crossorigin></script>
<script src="https://cdn.jsdelivr.net/npm/@babel/standalone/babel.min.js"></script>
<style>
:root {
--primary-bg: #0f172a;
--card-bg: #1e293b;
--accent-blue: #0d6efd;
--accent-indigo: #6610f2;
--accent-green: #198754;
--accent-amber: #ffc107;
--accent-red: #dc3545;
}
body {
background-color: #f8fafc;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
margin: 0;
padding: 0;
user-select: none;
-webkit-user-select: none;
}
.doctor-header {
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
color: #ffffff;
padding: 12px 16px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
position: sticky;
top: 0;
z-index: 1000;
}
.patient-card {
border: 1px solid #e2e8f0;
border-radius: 12px;
background: #ffffff;
transition: all 0.2s ease-in-out;
cursor: pointer;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
border-left: 5px solid var(--accent-blue);
}
.patient-card:hover, .patient-card:active {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(13,110,253,0.12);
border-color: #0d6efd;
}
.nav-pills .nav-link {
border-radius: 8px;
font-weight: 500;
color: #64748b;
}
.nav-pills .nav-link.active {
background-color: var(--accent-blue);
color: #ffffff;
}
/* Lock Screen Styling */
.lock-screen-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.lock-card {
background: #ffffff;
border-radius: 16px;
width: 100%;
max-width: 400px;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3);
overflow: hidden;
}
.vitals-pill {
font-size: 12px;
padding: 4px 10px;
border-radius: 20px;
background: #f1f5f9;
color: #334155;
font-weight: 600;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const DOC_DEVICE_AUTH_KEY = 'doctor_pwa_device_auth_v1';
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
function DoctorApp() {
const [doctors, setDoctors] = useState([]);
const [ipdPatients, setIpdPatients] = useState([]);
const [opdAppointments, setOpdAppointments] = useState([]);
const [loading, setLoading] = useState(true);
const [searchQuery, setSearchQuery] = useState('');
const [mainTab, setMainTab] = useState('ipd'); // 'ipd', 'opd', 'discharge'
const [activePatient, setActivePatient] = useState(null);
const [workspaceData, setWorkspaceData] = useState([]);
const [emrTab, setEmrTab] = useState('vitals'); // 'vitals', 'progress', 'rx', 'lab'
const [installPrompt, setInstallPrompt] = useState(null);
// Auth state
const [authenticatedDoctor, setAuthenticatedDoctor] = useState(null);
const [authDocId, setAuthDocId] = useState(0);
const [pinInput, setPinInput] = useState('');
const [pinError, setPinError] = useState('');
const [isSettingNewPin, setIsSettingNewPin] = useState(false);
const [newPin, setNewPin] = useState('');
const [confirmPin, setConfirmPin] = useState('');
// Clinical Progress Note Form
const [doctorNote, setDoctorNote] = useState('');
const [doctorAdvice, setDoctorAdvice] = useState('');
// Quick E-Rx Form
const [rxForm, setRxForm] = useState({ drug_name: '', dosage: '', frequency: '1-0-1', duration: '5 days', instructions: '' });
// Lab Order Form
const [labOrderText, setLabOrderText] = useState('');
// Fetch doctors list
const fetchDoctors = async () => {
try {
const res = await fetch('/api/v1/doctor/list');
const json = await res.json();
if (json.status === 1 && json.data) {
setDoctors(json.data);
// Check URL query parameters for doctor_id (QR scan or link)
const params = new URLSearchParams(window.location.search);
const docIdParam = params.get('doctor_id');
if (docIdParam) {
setAuthDocId(docIdParam);
}
}
} catch (err) {
console.error('Failed to load doctors', err);
}
};
// Fetch IPD Patients for current doctor
const fetchIpdPatients = async (docId) => {
try {
setLoading(true);
const res = await fetch(`/api/v1/doctor/ipd/list/${docId || 0}`);
const json = await res.json();
if (json.status === 1) {
setIpdPatients(json.patients || []);
}
} catch (err) {
console.error('Failed to load IPD patients', err);
} finally {
setLoading(false);
}
};
// Fetch OPD Appointments for current doctor
const fetchOpdAppointments = async (docId) => {
try {
const res = await fetch(`/api/v1/doctor/opd/list/${docId || 0}`);
const json = await res.json();
if (json.status === 1) {
setOpdAppointments(json.appointments || []);
}
} catch (err) {
console.error('Failed to load OPD appointments', err);
}
};
// Fetch patient bedside workspace
const fetchWorkspace = async (ipdId) => {
try {
const res = await fetch(`/api/v1/doctor/ipd/workspace/${ipdId}`);
const json = await res.json();
if (json.status === 1) {
setWorkspaceData(json.entries || []);
}
} catch (err) {
console.error('Failed to load patient workspace', err);
}
};
// Restore 30-Day Device Authentication
useEffect(() => {
const savedAuth = localStorage.getItem(DOC_DEVICE_AUTH_KEY);
if (savedAuth) {
try {
const parsed = JSON.parse(savedAuth);
if (parsed && parsed.doctor && parsed.expires_at > Date.now()) {
setAuthenticatedDoctor(parsed.doctor);
fetchIpdPatients(parsed.doctor.id);
fetchOpdAppointments(parsed.doctor.id);
} else {
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
}
} catch (e) {}
}
fetchDoctors();
window.addEventListener('beforeinstallprompt', (e) => {
e.preventDefault();
setInstallPrompt(e);
});
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/App/Doctor/sw.js')
.then(() => console.log('Doctor PWA Service Worker Registered'))
.catch(err => console.error('SW failed', err));
}
}, []);
// Verify Doctor PIN
const handleVerifyPin = async () => {
if (!authDocId || authDocId === '0') {
setPinError('Please select your Doctor Profile.');
return;
}
if (!pinInput || pinInput.length < 4) {
setPinError('Enter your 4 to 6 digit Security PIN.');
return;
}
setPinError('');
try {
const body = new URLSearchParams();
body.append('doctor_id', authDocId);
body.append('pin', pinInput);
const res = await fetch('/api/v1/doctor/auth/verify-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1 && json.doctor) {
setAuthenticatedDoctor(json.doctor);
// Save 30-day device authentication
const deviceAuthObj = {
doctor: json.doctor,
expires_at: Date.now() + THIRTY_DAYS_MS
};
localStorage.setItem(DOC_DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
setPinInput('');
fetchIpdPatients(json.doctor.id);
fetchOpdAppointments(json.doctor.id);
} else if (json.status === 2) {
setIsSettingNewPin(true);
} else {
setPinError(json.message || 'Invalid Security PIN');
}
} catch (err) {
setPinError('Connection error verifying PIN.');
}
};
// Set Doctor PIN
const handleSaveNewPin = async () => {
if (!newPin || newPin.length < 4) {
setPinError('PIN must be 4 to 6 numeric digits.');
return;
}
if (newPin !== confirmPin) {
setPinError('New PIN and Confirm PIN do not match.');
return;
}
try {
const body = new URLSearchParams();
body.append('doctor_id', authDocId || (authenticatedDoctor ? authenticatedDoctor.id : 0));
body.append('new_pin', newPin);
const res = await fetch('/api/v1/doctor/auth/set-pin', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1) {
alert('Security PIN set successfully! You can now login with your PIN.');
setIsSettingNewPin(false);
setNewPin('');
setConfirmPin('');
setPinInput('');
} else {
setPinError(json.message || 'Failed to update PIN');
}
} catch (err) {
setPinError('Error setting PIN.');
}
};
const handleLogout = () => {
setAuthenticatedDoctor(null);
localStorage.removeItem(DOC_DEVICE_AUTH_KEY);
setPinInput('');
setActivePatient(null);
};
const openPatientWorkspace = (patient) => {
setActivePatient(patient);
fetchWorkspace(patient.id);
};
// Save Doctor Daily Rounds / Treatment Note
const handleSaveDoctorTreatmentNote = async () => {
if (!activePatient) return;
if (!doctorNote.trim()) {
alert('Please enter doctor treatment / daily progress note.');
return;
}
try {
const body = new URLSearchParams();
body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0);
body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor');
body.append('treatment_text', doctorNote);
body.append('general_note', doctorAdvice);
const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1) {
alert(json.message || 'Doctor note saved');
setDoctorNote('');
setDoctorAdvice('');
fetchWorkspace(activePatient.id);
} else {
alert(json.message || 'Failed to save note');
}
} catch (err) {
alert('Error submitting note');
}
};
// Save Quick E-Prescription Order
const handleSaveQuickRx = async () => {
if (!activePatient) return;
if (!rxForm.drug_name.trim()) {
alert('Please enter Drug / Medicine Name.');
return;
}
const rxText = `Rx: ${rxForm.drug_name} | Dosage: ${rxForm.dosage} | Freq: ${rxForm.frequency} | Duration: ${rxForm.duration} ${rxForm.instructions ? '(' + rxForm.instructions + ')' : ''}`;
try {
const body = new URLSearchParams();
body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0);
body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor');
body.append('treatment_text', rxText);
const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1) {
alert('E-Prescription order added to patient record!');
setRxForm({ drug_name: '', dosage: '', frequency: '1-0-1', duration: '5 days', instructions: '' });
fetchWorkspace(activePatient.id);
}
} catch (err) {
alert('Error submitting prescription');
}
};
// Save Lab Test Order
const handleSaveLabOrder = async () => {
if (!activePatient || !labOrderText.trim()) {
alert('Please enter Lab / Pathology / Radiology test orders.');
return;
}
const labText = `Lab & Diagnostics Order: ${labOrderText}`;
try {
const body = new URLSearchParams();
body.append('doctor_id', authenticatedDoctor ? authenticatedDoctor.id : 0);
body.append('doctor_name', authenticatedDoctor ? authenticatedDoctor.name : 'Doctor');
body.append('treatment_text', labText);
const res = await fetch(`/api/v1/doctor/ipd/treatment/save/${activePatient.id}`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString()
});
const json = await res.json();
if (json.status === 1) {
alert('Lab & Diagnostic order submitted!');
setLabOrderText('');
fetchWorkspace(activePatient.id);
}
} catch (err) {
alert('Error submitting lab order');
}
};
const filteredIpdPatients = ipdPatients.filter(p => {
const name = `${p.fname || ''} ${p.lname || ''}`.toLowerCase();
const q = searchQuery.toLowerCase();
return name.includes(q) || (p.uhid && p.uhid.toLowerCase().includes(q)) || (p.bed_code && p.bed_code.toLowerCase().includes(q));
});
// -------------------------------------------------------------
// RENDER: LOCK SCREEN (DOCTOR PIN AUTHENTICATION)
// -------------------------------------------------------------
if (!authenticatedDoctor) {
return (
<div className="lock-screen-overlay">
<div className="lock-card">
<div className="p-4 text-center bg-primary text-white">
<i className="bi bi-person-badge-fill display-4 d-block mb-2"></i>
<h5 className="fw-bold mb-0">DoctorCare EMR App</h5>
<small className="opacity-75">30-Day Device Authentication Security</small>
</div>
<div className="p-4">
{pinError && <div className="alert alert-danger py-2 px-3 small mb-3">{pinError}</div>}
{!isSettingNewPin ? (
<div>
<div className="mb-3">
<label className="form-label fw-bold small">Select Doctor Profile</label>
<select
className="form-select"
value={authDocId}
onChange={(e) => setAuthDocId(e.target.value)}
>
<option value="0">-- Choose Doctor Profile --</option>
{doctors.map(d => (
<option key={d.id} value={d.id}>{d.name}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="form-label fw-bold small">Enter App Security PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-4"
placeholder="••••"
value={pinInput}
onChange={(e) => setPinInput(e.target.value)}
onKeyUp={(e) => e.key === 'Enter' && handleVerifyPin()}
/>
<small className="text-muted text-center d-block mt-1" style={{fontSize: '11px'}}>
Unlocks device access for 30 days
</small>
</div>
<button className="btn btn-primary w-100 py-2 fw-bold" onClick={handleVerifyPin}>
<i className="bi bi-unlock-fill me-1"></i> Unlock Doctor App
</button>
</div>
) : (
<div>
<div className="alert alert-info py-2 small mb-3">
Initial PIN Setup for Doctor Profile. Set a 4-6 digit Security PIN.
</div>
<div className="mb-3">
<label className="form-label fw-bold small">New Security PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-4"
placeholder="••••"
value={newPin}
onChange={(e) => setNewPin(e.target.value)}
/>
</div>
<div className="mb-3">
<label className="form-label fw-bold small">Confirm New PIN</label>
<input
type="password"
maxLength="6"
className="form-control text-center fs-4"
placeholder="••••"
value={confirmPin}
onChange={(e) => setConfirmPin(e.target.value)}
/>
</div>
<button className="btn btn-success w-100 py-2 fw-bold" onClick={handleSaveNewPin}>
<i className="bi bi-check-circle-fill me-1"></i> Save PIN &amp; Unlock
</button>
</div>
)}
</div>
</div>
</div>
);
}
// -------------------------------------------------------------
// RENDER: UNLOCKED DOCTOR PWA APP
// -------------------------------------------------------------
return (
<div>
{/* PWA Install Banner */}
{installPrompt && (
<div className="install-pwa-banner d-flex justify-content-between align-items-center">
<span><i className="bi bi-download me-2"></i> Install Doctor EMR App on your home screen</span>
<button className="btn btn-sm btn-light text-primary fw-bold" onClick={() => installPrompt.prompt()}>Install App</button>
</div>
)}
{/* Top Doctor Header */}
<header className="doctor-header d-flex justify-content-between align-items-center">
<div className="d-flex align-items-center">
<i className="bi bi-person-badge-fill text-primary fs-3 me-2"></i>
<div>
<h6 className="mb-0 fw-bold">{authenticatedDoctor.name}</h6>
<small className="text-light opacity-75">Doctor EMR <span className="badge bg-success ms-1" style={{fontSize: '9px'}}>30-Day Device Auth</span></small>
</div>
</div>
<div className="d-flex align-items-center gap-2">
<button className="btn btn-sm btn-outline-light" onClick={() => fetchIpdPatients(authenticatedDoctor.id)} title="Refresh">
<i className="bi bi-arrow-clockwise"></i>
</button>
<button className="btn btn-sm btn-danger" onClick={handleLogout} title="Lock Device Access">
<i className="bi bi-lock-fill me-1"></i> Lock
</button>
</div>
</header>
{/* Main Container */}
<main className="container-fluid py-3">
{!activePatient ? (
<div>
{/* Top Main Tabs */}
<ul className="nav nav-pills mb-3 gap-2">
<li className="nav-item">
<button className={`nav-link ${mainTab === 'ipd' ? 'active' : ''}`} onClick={() => setMainTab('ipd')}>
<i className="bi bi-hospital me-1"></i> Admitted IPD Patients ({ipdPatients.length})
</button>
</li>
<li className="nav-item">
<button className={`nav-link ${mainTab === 'opd' ? 'active' : ''}`} onClick={() => setMainTab('opd')}>
<i className="bi bi-calendar-event me-1"></i> OPD Appointments ({opdAppointments.length})
</button>
</li>
</ul>
{/* Search Bar */}
<div className="input-group mb-3">
<span className="input-group-text bg-white"><i className="bi bi-search"></i></span>
<input
type="text"
className="form-control"
placeholder="Search patient by name, UHID, or Bed Code..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
{mainTab === 'ipd' && (
loading ? (
<div className="text-center py-5">
<div className="spinner-border text-primary" role="status"></div>
<p className="mt-2 text-muted">Loading Admitted IPD Patients...</p>
</div>
) : (
<div className="row g-3">
{filteredIpdPatients.length === 0 ? (
<div className="col-12 text-center py-4 text-muted">No admitted IPD patients found.</div>
) : (
filteredIpdPatients.map(patient => (
<div key={patient.id} className="col-lg-4 col-md-6 col-12">
<div className="card p-3 patient-card" onClick={() => openPatientWorkspace(patient)}>
<div className="d-flex justify-content-between align-items-start mb-2">
<div>
<h5 className="fw-bold mb-0 text-primary">{patient.fname} {patient.lname}</h5>
<small className="text-muted">UHID: {patient.uhid || 'N/A'} | {patient.gender}, {patient.age || 'N/A'}</small>
</div>
<span className="badge bg-primary fs-6">
{patient.bed_code || patient.bed_number || 'Bed N/A'}
</span>
</div>
<div className="d-flex justify-content-between align-items-center mt-2 pt-2 border-top">
<span className="badge bg-secondary-subtle text-dark">Ward: {patient.ward_name || 'General Ward'}</span>
<small className="text-muted">IPD #{patient.id}</small>
</div>
</div>
</div>
))
)}
</div>
)
)}
{mainTab === 'opd' && (
<div className="card shadow-sm border-0">
<div className="card-body p-0 table-responsive">
<table className="table table-hover align-middle mb-0">
<thead className="table-light">
<tr>
<th>UHID</th>
<th>Patient Name</th>
<th>Gender / Age</th>
<th>Mobile</th>
</tr>
</thead>
<tbody>
{opdAppointments.length === 0 ? (
<tr><td colSpan="4" className="text-center text-muted py-4">No OPD appointments found for today.</td></tr>
) : (
opdAppointments.map(app => (
<tr key={app.id}>
<td><code>{app.uhid || '-'}</code></td>
<td className="fw-bold text-primary">{app.fname} {app.lname}</td>
<td>{app.gender}, {app.age}</td>
<td>{app.mphone1 || '-'}</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
</div>
) : (
/* Doctor Bedside EMR Workspace */
<div>
<button className="btn btn-sm btn-outline-dark mb-3" onClick={() => setActivePatient(null)}>
<i className="bi bi-arrow-left me-1"></i> Back to Patient List
</button>
<div className="card shadow-sm mb-3 border-0">
<div className="card-body bg-light rounded d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<h4 className="fw-bold mb-1 text-primary">{activePatient.fname} {activePatient.lname}</h4>
<span className="badge bg-secondary me-2">UHID: {activePatient.uhid || 'N/A'}</span>
<span className="badge bg-info text-dark me-2">Gender/Age: {activePatient.gender}, {activePatient.age || 'N/A'}</span>
<span className="badge bg-primary">Bed: {activePatient.bed_code || 'N/A'} ({activePatient.ward_name || 'General Ward'})</span>
</div>
</div>
</div>
{/* EMR Tabs */}
<ul className="nav nav-pills mb-3 gap-2">
<li className="nav-item">
<button className={`nav-link ${emrTab === 'vitals' ? 'active' : ''}`} onClick={() => setEmrTab('vitals')}>
<i className="bi bi-activity me-1"></i> Vitals &amp; Nursing Records
</button>
</li>
<li className="nav-item">
<button className={`nav-link ${emrTab === 'progress' ? 'active' : ''}`} onClick={() => setEmrTab('progress')}>
<i className="bi bi-journal-medical me-1"></i> Daily Progress Note / Orders
</button>
</li>
<li className="nav-item">
<button className={`nav-link ${emrTab === 'rx' ? 'active' : ''}`} onClick={() => setEmrTab('rx')}>
<i className="bi bi-capsule me-1"></i> E-Prescription Order
</button>
</li>
<li className="nav-item">
<button className={`nav-link ${emrTab === 'lab' ? 'active' : ''}`} onClick={() => setEmrTab('lab')}>
<i className="bi bi-file-earmark-medical me-1"></i> Lab &amp; Diagnostic Order
</button>
</li>
</ul>
{/* EMR Tab Content */}
<div className="card p-3 shadow-sm border-0">
{emrTab === 'vitals' && (
<div>
<h6 className="fw-bold mb-3"><i className="bi bi-activity text-danger me-1"></i> Bedside Vitals &amp; Fluid History</h6>
<div className="table-responsive">
<table className="table table-sm table-striped">
<thead>
<tr>
<th>Recorded At</th>
<th>Type</th>
<th>Staff</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
{workspaceData.length === 0 ? (
<tr><td colSpan="4" className="text-center text-muted py-3">No nursing entries logged for this admission yet.</td></tr>
) : (
workspaceData.map(entry => (
<tr key={entry.id}>
<td><small>{entry.recorded_at}</small></td>
<td><span className="badge bg-secondary text-uppercase">{entry.entry_type}</span></td>
<td><small>{entry.recorded_by || 'Staff'}</small></td>
<td>
<small>
{entry.entry_type === 'vitals' && `T: ${entry.temperature_c ? ((entry.temperature_c * 9/5) + 32).toFixed(1) + '°F' : '-'}, P: ${entry.pulse_rate || '-'}, BP: ${entry.bp_systolic || '-'}/${entry.bp_diastolic || '-'}, SpO2: ${entry.spo2 || '-'}`}
{entry.entry_type === 'fluid' && `${entry.fluid_direction?.toUpperCase()} - ${entry.fluid_route}: ${entry.fluid_amount_ml}ml`}
{entry.entry_type === 'treatment' && entry.treatment_text}
{entry.general_note && ` (${entry.general_note})`}
</small>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
</div>
)}
{emrTab === 'progress' && (
<div>
<h6 className="fw-bold mb-3"><i className="bi bi-journal-medical text-primary me-1"></i> Doctor Daily Rounds Note &amp; Clinical Orders</h6>
<div className="row g-2">
<div className="col-12">
<label className="form-label small fw-bold">Daily Progress / Treatment Orders <span className="text-danger">*</span></label>
<textarea className="form-control" rows="3" placeholder="Enter clinical progress note, diagnosis adjustments, or bedside instructions..." value={doctorNote} onChange={e => setDoctorNote(e.target.value)}></textarea>
</div>
<div className="col-12">
<label className="form-label small fw-bold">Additional Advice / Instructions to Nursing Staff</label>
<textarea className="form-control" rows="2" placeholder="e.g. Keep head elevated 30 deg, monitor BP q4h..." value={doctorAdvice} onChange={e => setDoctorAdvice(e.target.value)}></textarea>
</div>
<div className="col-12 mt-3">
<button className="btn btn-primary" onClick={handleSaveDoctorTreatmentNote}>
<i className="bi bi-check-circle-fill me-1"></i> Save Doctor Progress Note
</button>
</div>
</div>
</div>
)}
{emrTab === 'rx' && (
<div>
<h6 className="fw-bold mb-3"><i className="bi bi-capsule text-success me-1"></i> Quick E-Prescription Bedside Order</h6>
<div className="row g-2">
<div className="col-md-5 col-12"><label className="form-label small">Medicine / Drug Name <span className="text-danger">*</span></label><input type="text" className="form-control" placeholder="e.g. Paracetamol 500mg" value={rxForm.drug_name} onChange={e => setRxForm({...rxForm, drug_name: e.target.value})} /></div>
<div className="col-md-3 col-6"><label className="form-label small">Dosage</label><input type="text" className="form-control" placeholder="e.g. 1 Tablet" value={rxForm.dosage} onChange={e => setRxForm({...rxForm, dosage: e.target.value})} /></div>
<div className="col-md-2 col-6"><label className="form-label small">Frequency</label><input type="text" className="form-control" value={rxForm.frequency} onChange={e => setRxForm({...rxForm, frequency: e.target.value})} /></div>
<div className="col-md-2 col-6"><label className="form-label small">Duration</label><input type="text" className="form-control" value={rxForm.duration} onChange={e => setRxForm({...rxForm, duration: e.target.value})} /></div>
<div className="col-12"><label className="form-label small">Instructions</label><input type="text" className="form-control" placeholder="e.g. After meals" value={rxForm.instructions} onChange={e => setRxForm({...rxForm, instructions: e.target.value})} /></div>
<div className="col-12 mt-3">
<button className="btn btn-success" onClick={handleSaveQuickRx}>
<i className="bi bi-plus-circle me-1"></i> Submit E-Prescription Order
</button>
</div>
</div>
</div>
)}
{emrTab === 'lab' && (
<div>
<h6 className="fw-bold mb-3"><i className="bi bi-file-earmark-medical text-warning me-1"></i> Lab &amp; Diagnostic Test Orders</h6>
<div className="row g-2">
<div className="col-12">
<label className="form-label small fw-bold">Required Tests <span className="text-danger">*</span></label>
<textarea className="form-control" rows="3" placeholder="e.g. CBC, Serum Creatinine, Chest X-Ray PA view..." value={labOrderText} onChange={e => setLabOrderText(e.target.value)}></textarea>
</div>
<div className="col-12 mt-3">
<button className="btn btn-warning text-dark fw-bold" onClick={handleSaveLabOrder}>
<i className="bi bi-send me-1"></i> Submit Diagnostic Orders
</button>
</div>
</div>
</div>
)}
</div>
</div>
)}
</main>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<DoctorApp />);
</script>
</body>
</html>

View File

@ -0,0 +1,16 @@
{
"short_name": "Doctor EMR",
"name": "E-Atria Doctor EMR & Bedside Care PWA",
"icons": [
{
"src": "https://cdn-icons-png.flaticon.com/512/3774/3774299.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": "/app/doctor",
"background_color": "#0f172a",
"theme_color": "#0d6efd",
"display": "standalone",
"orientation": "portrait"
}

21
public/App/Doctor/sw.js Normal file
View File

@ -0,0 +1,21 @@
const CACHE_NAME = 'doctor-pwa-v1';
const ASSETS_TO_CACHE = [
'/app/doctor',
'https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css',
'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css'
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(ASSETS_TO_CACHE);
})
);
});
self.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET') return;
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
});