feat(apps): add Top Nav Apps Launcher Icon & 30-Day Device PIN Authentication Memory

This commit is contained in:
DevSoftTech Server 2026-08-28 19:27:39 +05:30
parent 7b0b1c3415
commit 6a65fc507d
2 changed files with 157 additions and 29 deletions

View File

@ -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 &amp; 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>

View File

@ -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>