feat(apps): add Top Nav Apps Launcher Icon & 30-Day Device PIN Authentication Memory
This commit is contained in:
parent
7b0b1c3415
commit
6a65fc507d
|
|
@ -136,6 +136,11 @@
|
|||
</a>
|
||||
</li>
|
||||
<?php } ?>
|
||||
<li class="nav-item d-flex align-items-center" style="margin-right:20px;">
|
||||
<a class="nav-shortcut-icon text-decoration-none text-primary" href="javascript:void(0)" id="btn_hdr_apps_launcher" data-bs-toggle="tooltip" data-bs-placement="bottom" title="HMS Mobile & PWA Apps">
|
||||
<i class="bi bi-phone-vibrate fs-4"></i>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item pe-3 d-flex align-items-center text-nowrap">
|
||||
<a class="text-decoration-none" href="<?= base_url('help.html') ?>" target="_blank" rel="noopener">
|
||||
<i class="bi bi-question-circle me-1"></i>
|
||||
|
|
@ -282,4 +287,125 @@
|
|||
refreshClock();
|
||||
setInterval(refreshClock, 1000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
<!-- Modal for HMS Mobile & PWA Apps Launcher -->
|
||||
<div class="modal fade" id="hmsAppsLauncherModal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content shadow-lg border-0">
|
||||
<div class="modal-header bg-primary text-white py-2">
|
||||
<h5 class="modal-title fs-6 fw-bold mb-0"><i class="bi bi-grid-3x3-gap-fill me-2"></i> HMS Mobile & PWA Apps Launcher</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-3">
|
||||
<!-- Step 1: App Selection Cards -->
|
||||
<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">
|
||||
<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="fw-bold small" style="font-size:12px;">Doctor App</div>
|
||||
<span class="badge bg-secondary mt-1" style="font-size:9px;">Coming Soon</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-capsule fs-3 mb-1"></i>
|
||||
<div class="fw-bold small" style="font-size:12px;">Pharmacy App</div>
|
||||
<span class="badge bg-secondary mt-1" style="font-size:9px;">Coming Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<select class="form-select form-select-sm" id="hdr_apps_nurse_select">
|
||||
<option value="">-- General / All Staff Access --</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Step 3: QR Code & Access Link -->
|
||||
<div class="p-3 bg-light rounded border text-center">
|
||||
<div class="fw-bold small text-dark mb-1" id="hdr_apps_target_name">Nursing Care App</div>
|
||||
<span class="badge bg-secondary mb-2" id="hdr_apps_target_code">/app/nursing</span>
|
||||
|
||||
<div class="my-2">
|
||||
<img id="hdr_apps_qr_img" src="" alt="App QR Code" style="width: 160px; height: 160px; border-radius: 8px;" class="border p-1 bg-white shadow-sm" />
|
||||
</div>
|
||||
<p class="small text-muted mb-2" style="font-size: 11px;">
|
||||
<i class="bi bi-qr-code-scan me-1"></i> Scan with Phone Camera to launch app on Mobile, or click link below to open on Computer.
|
||||
</p>
|
||||
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<input type="text" class="form-control form-control-sm text-center" id="hdr_apps_url_input" readonly />
|
||||
<button class="btn btn-outline-primary" type="button" id="btn_hdr_apps_copy_url"><i class="bi bi-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
|
||||
<a href="#" target="_blank" class="btn btn-primary btn-sm w-100 fw-bold" id="hdr_apps_launch_link">
|
||||
<i class="bi bi-box-arrow-up-right me-1"></i> Launch App on Computer / Browser
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function() {
|
||||
function updateHdrAppDetails() {
|
||||
var code = $('#hdr_apps_nurse_select').val();
|
||||
var appBaseUrl = '<?= base_url('app/nursing') ?>';
|
||||
var appUrl = appBaseUrl + (code ? '?nurse_code=' + encodeURIComponent(code) : '');
|
||||
var qrUrl = 'https://api.qrserver.com/v1/create-qr-code/?size=250x250&data=' + encodeURIComponent(appUrl);
|
||||
|
||||
$('#hdr_apps_qr_img').attr('src', qrUrl);
|
||||
$('#hdr_apps_url_input').val(appUrl);
|
||||
$('#hdr_apps_launch_link').attr('href', appUrl);
|
||||
|
||||
if (code) {
|
||||
var text = $('#hdr_apps_nurse_select option:selected').text();
|
||||
$('#hdr_apps_target_name').text('Nursing Care App: ' + text);
|
||||
$('#hdr_apps_target_code').text(code);
|
||||
} else {
|
||||
$('#hdr_apps_target_name').text('Nursing Care App');
|
||||
$('#hdr_apps_target_code').text('/app/nursing');
|
||||
}
|
||||
}
|
||||
|
||||
$(document).on('click', '#btn_hdr_apps_launcher', function() {
|
||||
$.getJSON('<?= base_url('api/v1/nursing/nurses') ?>', function(resp) {
|
||||
var $sel = $('#hdr_apps_nurse_select');
|
||||
$sel.html('<option value="">-- General / All Staff Access --</option>');
|
||||
if (resp && resp.status === 1 && resp.data) {
|
||||
resp.data.forEach(function(n) {
|
||||
$sel.append('<option value="' + n.nurse_code + '">[' + n.nurse_code + '] ' + n.name + '</option>');
|
||||
});
|
||||
}
|
||||
updateHdrAppDetails();
|
||||
var modalEl = document.getElementById('hmsAppsLauncherModal');
|
||||
if (window.bootstrap && window.bootstrap.Modal) {
|
||||
window.bootstrap.Modal.getOrCreateInstance(modalEl).show();
|
||||
} else {
|
||||
$(modalEl).modal('show');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$(document).on('change', '#hdr_apps_nurse_select', updateHdrAppDetails);
|
||||
|
||||
$(document).on('click', '#btn_hdr_apps_copy_url', function() {
|
||||
var input = document.getElementById('hdr_apps_url_input');
|
||||
input.select();
|
||||
document.execCommand('copy');
|
||||
alert('App URL copied to clipboard!');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
|
|
@ -119,15 +119,6 @@
|
|||
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>
|
||||
|
|
@ -136,6 +127,9 @@
|
|||
<script type="text/babel">
|
||||
const { useState, useEffect } = React;
|
||||
|
||||
const DEVICE_AUTH_KEY = 'nursing_pwa_device_auth_v1';
|
||||
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
|
||||
function NursingApp() {
|
||||
const [beds, setBeds] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
|
@ -155,7 +149,6 @@
|
|||
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({
|
||||
|
|
@ -196,7 +189,7 @@
|
|||
if (json.status === 1 && json.data) {
|
||||
setNurses(json.data);
|
||||
|
||||
// Check URL query parameters for nurse_code (QR Code scan)
|
||||
// Check URL query parameters for nurse_code (QR Code scan or Direct URL)
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const codeParam = params.get('nurse_code');
|
||||
if (codeParam) {
|
||||
|
|
@ -211,14 +204,16 @@
|
|||
}
|
||||
};
|
||||
|
||||
// Restore saved authentication from sessionStorage
|
||||
// Restore 30-day device authentication from localStorage
|
||||
useEffect(() => {
|
||||
const saved = sessionStorage.getItem('nursing_auth_nurse');
|
||||
if (saved) {
|
||||
const savedAuth = localStorage.getItem(DEVICE_AUTH_KEY);
|
||||
if (savedAuth) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed && parsed.id) {
|
||||
setAuthenticatedNurse(parsed);
|
||||
const parsed = JSON.parse(savedAuth);
|
||||
if (parsed && parsed.nurse && parsed.expires_at > Date.now()) {
|
||||
setAuthenticatedNurse(parsed.nurse);
|
||||
} else {
|
||||
localStorage.removeItem(DEVICE_AUTH_KEY);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
|
@ -239,7 +234,7 @@
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Authenticate Nurse with Security PIN
|
||||
// Authenticate Nurse with Security PIN & Save 30-Day Device Auth
|
||||
const handleVerifyPin = async () => {
|
||||
if (!authNurseId || authNurseId === '0') {
|
||||
setPinError('Please select your Nursing Staff ID / Profile.');
|
||||
|
|
@ -265,10 +260,15 @@
|
|||
|
||||
if (json.status === 1 && json.nurse) {
|
||||
setAuthenticatedNurse(json.nurse);
|
||||
sessionStorage.setItem('nursing_auth_nurse', JSON.stringify(json.nurse));
|
||||
|
||||
// Save 30-day persistent device authentication in localStorage
|
||||
const deviceAuthObj = {
|
||||
nurse: json.nurse,
|
||||
expires_at: Date.now() + THIRTY_DAYS_MS
|
||||
};
|
||||
localStorage.setItem(DEVICE_AUTH_KEY, JSON.stringify(deviceAuthObj));
|
||||
setPinInput('');
|
||||
} else if (json.status === 2) {
|
||||
// PIN not set yet for this nurse
|
||||
setIsSettingNewPin(true);
|
||||
} else {
|
||||
setPinError(json.message || 'Invalid Security PIN');
|
||||
|
|
@ -302,9 +302,8 @@
|
|||
const json = await res.json();
|
||||
|
||||
if (json.status === 1) {
|
||||
alert('Security PIN updated! You can now login with your PIN.');
|
||||
alert('Security PIN set successfully! Enter your PIN to unlock.');
|
||||
setIsSettingNewPin(false);
|
||||
setShowChangePinModal(false);
|
||||
setNewPin('');
|
||||
setConfirmPin('');
|
||||
setPinInput('');
|
||||
|
|
@ -318,7 +317,7 @@
|
|||
|
||||
const handleLogout = () => {
|
||||
setAuthenticatedNurse(null);
|
||||
sessionStorage.removeItem('nursing_auth_nurse');
|
||||
localStorage.removeItem(DEVICE_AUTH_KEY);
|
||||
setPinInput('');
|
||||
setActiveBed(null);
|
||||
};
|
||||
|
|
@ -395,7 +394,7 @@
|
|||
<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>
|
||||
<small className="opacity-75">30-Day Device Authentication Security</small>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
|
|
@ -404,7 +403,7 @@
|
|||
{!isSettingNewPin ? (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
<label className="form-label fw-bold small">Select Nursing Staff</label>
|
||||
<label className="form-label fw-bold small">Select Nursing Staff Profile</label>
|
||||
<select
|
||||
className="form-select"
|
||||
value={authNurseId}
|
||||
|
|
@ -422,16 +421,19 @@
|
|||
<input
|
||||
type="password"
|
||||
maxLength="6"
|
||||
className="form-control text-center fs-4 tracking-widest"
|
||||
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 App Access
|
||||
<i className="bi bi-unlock-fill me-1"></i> Unlock Device Access
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -491,14 +493,14 @@
|
|||
<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">Staff: <strong>[{authenticatedNurse.nurse_code}] {authenticatedNurse.name}</strong></small>
|
||||
<small className="text-light opacity-75">Staff: <strong>[{authenticatedNurse.nurse_code}] {authenticatedNurse.name}</strong> <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={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">
|
||||
<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>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user