feat(auth): add Nurse PIN Security Authentication, App QR Codes, and Lock Screen for NursingCare PWA
This commit is contained in:
parent
6f199d9743
commit
7b0b1c3415
|
|
@ -1571,6 +1571,8 @@ $routes->group('api/v1', ['namespace' => 'App\Controllers\Api\v1'], static funct
|
|||
// Nursing PWA APIs
|
||||
$routes->get('nursing/beds', 'NursingApi::beds');
|
||||
$routes->get('nursing/nurses', 'NursingApi::nurses');
|
||||
$routes->post('nursing/auth/verify-pin', 'NursingApi::verifyPin');
|
||||
$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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -78,6 +78,96 @@ class NursingApi extends BaseController
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST api/v1/nursing/auth/verify-pin
|
||||
*/
|
||||
public function verifyPin()
|
||||
{
|
||||
$post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []);
|
||||
$nurseId = (int) ($post['nurse_id'] ?? 0);
|
||||
$nurseCode = trim((string) ($post['nurse_code'] ?? ''));
|
||||
$pin = trim((string) ($post['pin'] ?? ''));
|
||||
|
||||
if ($nurseId <= 0 && $nurseCode !== '') {
|
||||
$nurseRow = db_connect()->table('nurse_master')->where('nurse_code', $nurseCode)->get()->getRowArray();
|
||||
} else {
|
||||
$nurseRow = $this->nurseModel->getNurseById($nurseId);
|
||||
}
|
||||
|
||||
if (! $nurseRow) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Nurse profile not found']);
|
||||
}
|
||||
|
||||
$hashedPin = (string) ($nurseRow['app_pin'] ?? '');
|
||||
|
||||
if ($hashedPin === '') {
|
||||
return $this->response->setJSON([
|
||||
'status' => 2,
|
||||
'message' => 'PIN not set yet. Please set a 4-6 digit PIN.',
|
||||
'nurse' => [
|
||||
'id' => (int) $nurseRow['id'],
|
||||
'nurse_code' => $nurseRow['nurse_code'],
|
||||
'name' => $nurseRow['name'],
|
||||
'designation' => $nurseRow['designation'] ?? 'Staff Nurse',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$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',
|
||||
'nurse' => [
|
||||
'id' => (int) $nurseRow['id'],
|
||||
'nurse_code' => $nurseRow['nurse_code'],
|
||||
'name' => $nurseRow['name'],
|
||||
'designation' => $nurseRow['designation'] ?? 'Staff Nurse',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST api/v1/nursing/auth/set-pin
|
||||
*/
|
||||
public function setPin()
|
||||
{
|
||||
$post = $this->request->getPost() ?: ($this->request->getJSON(true) ?? []);
|
||||
$nurseId = (int) ($post['nurse_id'] ?? 0);
|
||||
$newPin = trim((string) ($post['new_pin'] ?? ''));
|
||||
$oldPin = trim((string) ($post['old_pin'] ?? ''));
|
||||
|
||||
if ($nurseId <= 0 || strlen($newPin) < 4) {
|
||||
return $this->response->setStatusCode(422)->setJSON(['status' => 0, 'message' => 'Valid Nurse ID and 4-6 digit PIN are required']);
|
||||
}
|
||||
|
||||
$nurseRow = $this->nurseModel->getNurseById($nurseId);
|
||||
if (! $nurseRow) {
|
||||
return $this->response->setStatusCode(404)->setJSON(['status' => 0, 'message' => 'Nurse profile not found']);
|
||||
}
|
||||
|
||||
$hashedPin = (string) ($nurseRow['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->nurseModel->updateNurse($nurseId, [
|
||||
'app_pin' => password_hash($newPin, PASSWORD_DEFAULT),
|
||||
]);
|
||||
|
||||
return $this->response->setJSON([
|
||||
'status' => 1,
|
||||
'message' => 'Security PIN updated successfully',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* GET api/v1/nursing/workspace/(:num)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -53,6 +53,8 @@ class Nurse extends BaseController
|
|||
$nurseCode = 'NUR-' . str_pad((string) (rand(100, 999)), 3, '0', STR_PAD_LEFT);
|
||||
}
|
||||
|
||||
$appPin = trim((string) ($this->request->getPost('app_pin') ?? ''));
|
||||
|
||||
$payload = [
|
||||
'nurse_code' => $nurseCode,
|
||||
'name' => $name,
|
||||
|
|
@ -67,6 +69,10 @@ class Nurse extends BaseController
|
|||
'is_active' => $isActive,
|
||||
];
|
||||
|
||||
if ($appPin !== '') {
|
||||
$payload['app_pin'] = password_hash($appPin, PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
if ($id > 0) {
|
||||
$this->nurseModel->updateNurse($id, $payload);
|
||||
$savedId = $id;
|
||||
|
|
|
|||
|
|
@ -52,6 +52,9 @@ class NurseModel
|
|||
if (! in_array('designation', $fields, true)) {
|
||||
$this->db->query("ALTER TABLE nurse_master ADD COLUMN designation VARCHAR(100) NULL AFTER gender");
|
||||
}
|
||||
if (! in_array('app_pin', $fields, true)) {
|
||||
$this->db->query("ALTER TABLE nurse_master ADD COLUMN app_pin VARCHAR(255) NULL AFTER email");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,9 @@
|
|||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="text-end pe-3">
|
||||
<button type="button" class="btn btn-outline-dark btn-sm py-0 px-2 me-1 btn-qr-nurse" data-code="<?= esc($nurse['nurse_code']) ?>" data-name="<?= esc($nurse['name']) ?>">
|
||||
<i class="bi bi-qr-code"></i> QR
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm py-0 px-2 btn-edit-nurse" data-id="<?= (int)$nurse['id'] ?>">
|
||||
<i class="bi bi-pencil-square"></i> Edit
|
||||
</button>
|
||||
|
|
@ -185,6 +188,15 @@
|
|||
<label class="form-label fw-bold small mb-1">Email Address</label>
|
||||
<input type="email" class="form-control form-control-sm" id="nurse_email" name="email" placeholder="Email address">
|
||||
</div>
|
||||
|
||||
<!-- App Security PIN Section -->
|
||||
<div class="col-md-6 mt-2">
|
||||
<div class="p-2 bg-warning-subtle border border-warning rounded">
|
||||
<label class="form-label fw-bold small text-dark mb-1"><i class="bi bi-key me-1"></i> Mobile App Security PIN</label>
|
||||
<input type="password" maxlength="6" class="form-control form-control-sm" id="nurse_app_pin" name="app_pin" placeholder="Set or update 4-6 digit PIN">
|
||||
<small class="text-muted" style="font-size: 10px;">Used by nurse to login to NursingCare PWA App</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer bg-light py-2">
|
||||
|
|
@ -198,11 +210,34 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal for Nurse App QR Code -->
|
||||
<div class="modal fade" id="nurseQrModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered modal-sm">
|
||||
<div class="modal-content text-center">
|
||||
<div class="modal-header bg-light py-2">
|
||||
<h5 class="modal-title text-primary fs-6 mb-0" id="qrModalTitle"><i class="bi bi-qr-code-scan me-1"></i> Nurse App QR Code</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body py-3">
|
||||
<h6 class="fw-bold mb-0 text-dark" id="qr_nurse_name">Staff Nurse</h6>
|
||||
<span class="badge bg-secondary mb-3" id="qr_nurse_code">NUR-001</span>
|
||||
<div class="p-2 border rounded d-inline-block bg-white shadow-sm mb-3">
|
||||
<img id="qr_code_img" src="" alt="Nurse 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 open <strong>NursingCare PWA</strong> for this nurse 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 saveUrl = '<?= base_url('setting/admin/nurse/save') ?>';
|
||||
var deleteUrlBase = '<?= base_url('setting/admin/nurse/delete') ?>';
|
||||
var csrfTokenName = '<?= csrf_token() ?>';
|
||||
var appBaseUrl = '<?= base_url('app/nursing') ?>';
|
||||
|
||||
function resetNurseForm() {
|
||||
$('#nurse_id').val('0');
|
||||
|
|
@ -215,12 +250,31 @@
|
|||
$('#nurse_qualification').val('');
|
||||
$('#nurse_contact_no').val('');
|
||||
$('#nurse_email').val('');
|
||||
$('#nurse_app_pin').val('');
|
||||
$('#nurse_department').val('Nursing');
|
||||
$('#nurse_is_active').val('1');
|
||||
$('#nurse_form_status').addClass('d-none').text('');
|
||||
$('#nurseModalTitle').html('<i class="bi bi-person-plus me-1"></i> Add Nursing Staff');
|
||||
}
|
||||
|
||||
$(document).on('click', '.btn-qr-nurse', function() {
|
||||
var code = $(this).data('code') || '';
|
||||
var name = $(this).data('name') || '';
|
||||
var appUrl = appBaseUrl + '?nurse_code=' + encodeURIComponent(code);
|
||||
var qrApiUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=' + encodeURIComponent(appUrl);
|
||||
|
||||
$('#qr_nurse_name').text(name);
|
||||
$('#qr_nurse_code').text(code);
|
||||
$('#qr_code_img').attr('src', qrApiUrl);
|
||||
|
||||
var modalEl = document.getElementById('nurseQrModal');
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||
} else {
|
||||
$(modalEl).modal('show');
|
||||
}
|
||||
});
|
||||
|
||||
$('#btn_add_nurse').on('click', function() {
|
||||
resetNurseForm();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -98,6 +98,36 @@
|
|||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
.pin-pad-btn {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -117,6 +147,16 @@
|
|||
const [installPrompt, setInstallPrompt] = useState(null);
|
||||
const [nurses, setNurses] = useState([]);
|
||||
|
||||
// Authentication & Security State
|
||||
const [authenticatedNurse, setAuthenticatedNurse] = useState(null);
|
||||
const [authNurseId, setAuthNurseId] = useState(0);
|
||||
const [pinInput, setPinInput] = useState('');
|
||||
const [pinError, setPinError] = useState('');
|
||||
const [isSettingNewPin, setIsSettingNewPin] = useState(false);
|
||||
const [newPin, setNewPin] = useState('');
|
||||
const [confirmPin, setConfirmPin] = useState('');
|
||||
const [showChangePinModal, setShowChangePinModal] = useState(false);
|
||||
|
||||
// Form state for workspace
|
||||
const [vitalsForm, setVitalsForm] = useState({
|
||||
temperature_f: '', pulse_rate: '', resp_rate: '',
|
||||
|
|
@ -132,8 +172,6 @@
|
|||
treatment_text: '', general_note: ''
|
||||
});
|
||||
|
||||
const [selectedNurseId, setSelectedNurseId] = useState(0);
|
||||
|
||||
// Fetch live bed list from CI4 API
|
||||
const fetchBeds = async () => {
|
||||
try {
|
||||
|
|
@ -157,26 +195,33 @@
|
|||
const json = await res.json();
|
||||
if (json.status === 1 && json.data) {
|
||||
setNurses(json.data);
|
||||
|
||||
// Check URL query parameters for nurse_code (QR Code scan)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const codeParam = params.get('nurse_code');
|
||||
if (codeParam) {
|
||||
const matched = json.data.find(n => n.nurse_code === codeParam);
|
||||
if (matched) {
|
||||
setAuthNurseId(matched.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load nurses', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch patient workspace entries
|
||||
const fetchWorkspace = async (ipdId) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/nursing/workspace/${ipdId}`);
|
||||
const json = await res.json();
|
||||
if (json.status === 1) {
|
||||
setWorkspaceData(json.entries || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load workspace data', err);
|
||||
}
|
||||
};
|
||||
|
||||
// Restore saved authentication from sessionStorage
|
||||
useEffect(() => {
|
||||
const saved = sessionStorage.getItem('nursing_auth_nurse');
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed && parsed.id) {
|
||||
setAuthenticatedNurse(parsed);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
fetchBeds();
|
||||
fetchNurses();
|
||||
|
||||
|
|
@ -194,6 +239,103 @@
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Authenticate Nurse with Security PIN
|
||||
const handleVerifyPin = async () => {
|
||||
if (!authNurseId || authNurseId === '0') {
|
||||
setPinError('Please select your Nursing Staff ID / 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('nurse_id', authNurseId);
|
||||
body.append('pin', pinInput);
|
||||
|
||||
const res = await fetch('/api/v1/nursing/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.nurse) {
|
||||
setAuthenticatedNurse(json.nurse);
|
||||
sessionStorage.setItem('nursing_auth_nurse', JSON.stringify(json.nurse));
|
||||
setPinInput('');
|
||||
} else if (json.status === 2) {
|
||||
// PIN not set yet for this nurse
|
||||
setIsSettingNewPin(true);
|
||||
} else {
|
||||
setPinError(json.message || 'Invalid Security PIN');
|
||||
}
|
||||
} catch (err) {
|
||||
setPinError('Connection error verifying PIN.');
|
||||
}
|
||||
};
|
||||
|
||||
// Set Initial PIN or Change 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('nurse_id', authNurseId || (authenticatedNurse ? authenticatedNurse.id : 0));
|
||||
body.append('new_pin', newPin);
|
||||
|
||||
const res = await fetch('/api/v1/nursing/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 updated! You can now login with your PIN.');
|
||||
setIsSettingNewPin(false);
|
||||
setShowChangePinModal(false);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setPinInput('');
|
||||
} else {
|
||||
setPinError(json.message || 'Failed to update PIN');
|
||||
}
|
||||
} catch (err) {
|
||||
setPinError('Error setting PIN.');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
setAuthenticatedNurse(null);
|
||||
sessionStorage.removeItem('nursing_auth_nurse');
|
||||
setPinInput('');
|
||||
setActiveBed(null);
|
||||
};
|
||||
|
||||
// Fetch patient workspace entries
|
||||
const fetchWorkspace = async (ipdId) => {
|
||||
try {
|
||||
const res = await fetch(`/api/v1/nursing/workspace/${ipdId}`);
|
||||
const json = await res.json();
|
||||
if (json.status === 1) {
|
||||
setWorkspaceData(json.entries || []);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load workspace data', err);
|
||||
}
|
||||
};
|
||||
|
||||
const openBedWorkspace = (bed) => {
|
||||
setActiveBed(bed);
|
||||
if (bed.current_ipd_id) {
|
||||
|
|
@ -211,7 +353,8 @@
|
|||
|
||||
const body = new URLSearchParams();
|
||||
body.append('entry_type', entryType);
|
||||
body.append('recorded_by_id', selectedNurseId);
|
||||
body.append('recorded_by_id', authenticatedNurse ? authenticatedNurse.id : 0);
|
||||
body.append('recorded_by', authenticatedNurse ? `[${authenticatedNurse.nurse_code}] ${authenticatedNurse.name}` : 'Staff');
|
||||
Object.keys(formData).forEach(key => body.append(key, formData[key]));
|
||||
|
||||
try {
|
||||
|
|
@ -242,6 +385,96 @@
|
|||
return matchesWard && matchesSearch;
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RENDER: LOCK SCREEN (PIN AUTHENTICATION)
|
||||
// -------------------------------------------------------------
|
||||
if (!authenticatedNurse) {
|
||||
return (
|
||||
<div className="lock-screen-overlay">
|
||||
<div className="lock-card">
|
||||
<div className="p-4 text-center bg-primary text-white">
|
||||
<i className="bi bi-shield-lock-fill display-4 d-block mb-2"></i>
|
||||
<h5 className="fw-bold mb-0">Nursing Care PWA App</h5>
|
||||
<small className="opacity-75">Staff PIN 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 Nursing Staff</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={authNurseId}
|
||||
onChange={(e) => setAuthNurseId(e.target.value)}
|
||||
>
|
||||
<option value="0">-- Choose Nursing Staff --</option>
|
||||
{nurses.map(n => (
|
||||
<option key={n.id} value={n.id}>[{n.nurse_code}] {n.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 tracking-widest"
|
||||
placeholder="••••"
|
||||
value={pinInput}
|
||||
onChange={(e) => setPinInput(e.target.value)}
|
||||
onKeyUp={(e) => e.key === 'Enter' && handleVerifyPin()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button className="btn btn-primary w-100 py-2 fw-bold" onClick={handleVerifyPin}>
|
||||
<i className="bi bi-unlock-fill me-1"></i> Unlock App Access
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="alert alert-info py-2 small mb-3">
|
||||
Initial PIN Setup for selected nurse. 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 & Unlock
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------
|
||||
// RENDER: UNLOCKED PWA APP DASHBOARD
|
||||
// -------------------------------------------------------------
|
||||
return (
|
||||
<div>
|
||||
{/* PWA Install Banner */}
|
||||
|
|
@ -258,23 +491,16 @@
|
|||
<i className="bi bi-heart-pulse-fill text-danger fs-4 me-2"></i>
|
||||
<div>
|
||||
<h6 className="mb-0 fw-bold">NursingCare PWA</h6>
|
||||
<small className="text-light opacity-75">Bedside Care & Vitals Logger</small>
|
||||
<small className="text-light opacity-75">Staff: <strong>[{authenticatedNurse.nurse_code}] {authenticatedNurse.name}</strong></small>
|
||||
</div>
|
||||
</div>
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<select
|
||||
className="form-select form-select-sm bg-dark text-white border-secondary"
|
||||
value={selectedNurseId}
|
||||
onChange={(e) => setSelectedNurseId(e.target.value)}
|
||||
>
|
||||
<option value="0">-- Staff Nurse --</option>
|
||||
{nurses.map(n => (
|
||||
<option key={n.id} value={n.id}>[{n.nurse_code}] {n.name}</option>
|
||||
))}
|
||||
</select>
|
||||
<button className="btn btn-sm btn-outline-light" onClick={fetchBeds}>
|
||||
<button className="btn btn-sm btn-outline-light" onClick={fetchBeds} title="Refresh Beds">
|
||||
<i className="bi bi-arrow-clockwise"></i>
|
||||
</button>
|
||||
<button className="btn btn-sm btn-danger" onClick={handleLogout} title="Lock App Device Access">
|
||||
<i className="bi bi-lock-fill me-1"></i> Lock
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
@ -403,12 +629,12 @@
|
|||
<div>
|
||||
<h6 className="fw-bold mb-3"><i className="bi bi-heart-pulse text-danger me-1"></i> Log Patient Vitals</h6>
|
||||
<div className="row g-2">
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Temp °F</label><input type="number" step="0.1" class="form-control" value={vitalsForm.temperature_f} onChange={e => setVitalsForm({...vitalsForm, temperature_f: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Pulse</label><input type="number" class="form-control" value={vitalsForm.pulse_rate} onChange={e => setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Resp</label><input type="number" class="form-control" value={vitalsForm.resp_rate} onChange={e => setVitalsForm({...vitalsForm, resp_rate: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">SBP</label><input type="number" class="form-control" value={vitalsForm.bp_systolic} onChange={e => setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">DBP</label><input type="number" class="form-control" value={vitalsForm.bp_diastolic} onChange={e => setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">SpO2 %</label><input type="number" class="form-control" value={vitalsForm.spo2} onChange={e => setVitalsForm({...vitalsForm, spo2: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Temp °F</label><input type="number" step="0.1" className="form-control" value={vitalsForm.temperature_f} onChange={e => setVitalsForm({...vitalsForm, temperature_f: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Pulse</label><input type="number" className="form-control" value={vitalsForm.pulse_rate} onChange={e => setVitalsForm({...vitalsForm, pulse_rate: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">Resp</label><input type="number" className="form-control" value={vitalsForm.resp_rate} onChange={e => setVitalsForm({...vitalsForm, resp_rate: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">SBP</label><input type="number" className="form-control" value={vitalsForm.bp_systolic} onChange={e => setVitalsForm({...vitalsForm, bp_systolic: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">DBP</label><input type="number" className="form-control" value={vitalsForm.bp_diastolic} onChange={e => setVitalsForm({...vitalsForm, bp_diastolic: e.target.value})} /></div>
|
||||
<div className="col-md-2 col-6"><label className="form-label small">SpO2 %</label><input type="number" className="form-control" value={vitalsForm.spo2} onChange={e => setVitalsForm({...vitalsForm, spo2: e.target.value})} /></div>
|
||||
<div className="col-12"><label className="form-label small">Nursing Observation</label><textarea className="form-control" rows="2" value={vitalsForm.general_note} onChange={e => setVitalsForm({...vitalsForm, general_note: e.target.value})}></textarea></div>
|
||||
<div className="col-12 mt-3">
|
||||
<button className="btn btn-primary" onClick={() => handleSaveEntry('vitals', vitalsForm)}>Save Vitals Entry</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user