feat(auth/settings): add hospital login wallpaper upload and redesign right-docked login page

- Add login wallpaper upload, preview, and deletion in Hospital Profile settings

- Redesign login page with right-docked form and left hospital branding hero

- Implement clean blank neutral fallback when no wallpaper is configured

- Add automated unit tests for wallpaper rendering and fallback behavior
This commit is contained in:
DevSoftTech Server 2026-10-27 12:19:07 +05:30
parent cb5c1e3230
commit 7bb751cc41
5 changed files with 630 additions and 179 deletions

View File

@ -15,6 +15,7 @@ class HospitalProfile extends BaseController
$logo = $this->readSettingValue('H_logo');
$pharmacyLogo = $this->readSettingValue('H_Med_logo');
$wallpaper = $this->readSettingValue('H_login_wallpaper');
helper('common');
return view('Setting/Admin/hospital_profile', [
@ -29,6 +30,8 @@ class HospitalProfile extends BaseController
: hms_default_version_id(),
'hospital_logo' => $logo,
'hospital_logo_url' => $logo !== '' ? base_url('assets/images/' . rawurlencode($logo)) : '',
'hospital_wallpaper' => $wallpaper,
'hospital_wallpaper_url' => $wallpaper !== '' ? base_url('assets/images/' . rawurlencode($wallpaper)) : '',
'pharmacy_name' => $this->readSettingValue('H_Med_Name'),
'pharmacy_address' => $this->readSettingValue('H_Med_address_1'),
'pharmacy_phone' => $this->readSettingValue('H_Med_phone_No'),
@ -158,6 +161,24 @@ class HospitalProfile extends BaseController
$savedCount++;
}
$wallpaperResult = $this->processLogoUpload(
$this->request->getFile('hospital_wallpaper'),
'hospital_wallpaper',
'H_login_wallpaper',
'Login wallpaper'
);
if (! $wallpaperResult['ok']) {
return $this->response->setJSON([
'update' => 0,
'error_text' => $wallpaperResult['error'],
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if ($wallpaperResult['changed']) {
$savedCount++;
}
$pharmacyLogoResult = $this->processLogoUpload(
$this->request->getFile('pharmacy_logo'),
'pharmacy_logo',
@ -181,6 +202,8 @@ class HospitalProfile extends BaseController
'error_text' => $savedCount > 0 ? 'Hospital profile saved successfully.' : 'No changes detected.',
'logo_url' => ($logo = $this->readSettingValue('H_logo')) !== '' ? base_url('assets/images/' . rawurlencode($logo)) : '',
'logo_name' => $this->readSettingValue('H_logo'),
'wallpaper_url' => ($wp = $this->readSettingValue('H_login_wallpaper')) !== '' ? base_url('assets/images/' . rawurlencode($wp)) : '',
'wallpaper_name' => $this->readSettingValue('H_login_wallpaper'),
'pharmacy_logo_url' => ($logo = $this->readSettingValue('H_Med_logo')) !== '' ? base_url('assets/images/' . rawurlencode($logo)) : '',
'pharmacy_logo_name' => $this->readSettingValue('H_Med_logo'),
'csrfName' => csrf_token(),
@ -199,24 +222,30 @@ class HospitalProfile extends BaseController
}
$type = strtolower(trim((string) $this->request->getPost('type')));
if ($type !== 'pharmacy') {
if ($type === 'pharmacy') {
$settingKey = 'H_Med_logo';
} elseif ($type === 'wallpaper') {
$settingKey = 'H_login_wallpaper';
} else {
$type = 'hospital';
$settingKey = 'H_logo';
}
$settingKey = $type === 'pharmacy' ? 'H_Med_logo' : 'H_logo';
$oldLogo = $this->readSettingValue($settingKey);
$oldFile = $this->readSettingValue($settingKey);
$ok = $this->upsertSettingValue($settingKey, '');
if ($oldLogo !== '') {
$oldPath = FCPATH . 'assets/images/' . basename($oldLogo);
if ($oldFile !== '') {
$oldPath = FCPATH . 'assets/images/' . basename($oldFile);
if (is_file($oldPath)) {
@unlink($oldPath);
}
}
$label = $type === 'wallpaper' ? 'Login wallpaper' : ucfirst($type) . ' logo';
return $this->response->setJSON([
'update' => $ok ? 1 : 0,
'error_text' => $ok ? ucfirst($type) . ' logo deleted.' : 'Unable to delete logo.',
'error_text' => $ok ? $label . ' deleted.' : 'Unable to delete ' . strtolower($label) . '.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
@ -234,6 +263,7 @@ class HospitalProfile extends BaseController
$logo = $this->readSettingValue('H_logo');
$pharmacyLogo = $this->readSettingValue('H_Med_logo');
$wallpaper = $this->readSettingValue('H_login_wallpaper');
$this->db->table('hospital_setting')->whereIn('s_name', [
'H_Name',
'H_address_1',
@ -243,6 +273,7 @@ class HospitalProfile extends BaseController
'H_HFR_ID',
'HMS_UPDATE_ID',
'H_logo',
'H_login_wallpaper',
'H_Med_Name',
'H_Med_address_1',
'H_Med_phone_No',
@ -266,6 +297,13 @@ class HospitalProfile extends BaseController
}
}
if ($wallpaper !== '') {
$wpPath = FCPATH . 'assets/images/' . basename($wallpaper);
if (is_file($wpPath)) {
@unlink($wpPath);
}
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Hospital profile settings reset.',

View File

@ -67,6 +67,23 @@
</div>
</div>
<div class="col-md-7">
<label class="form-label fw-semibold">Login Screen Wallpaper / Branding Image</label>
<input type="file" class="form-control" id="hospital_wallpaper" accept=".png,.jpg,.jpeg,.webp">
<div class="form-text">Displayed on the software login page. Recommended: 1920x1080 or 1280x720 (PNG, JPG, WEBP).</div>
</div>
<div class="col-md-5">
<label class="form-label fw-semibold">Current Wallpaper</label>
<div id="hospital_wallpaper_preview" class="border rounded p-2 text-center bg-light">
<?php if (!empty($hospital_wallpaper_url ?? '')): ?>
<img src="<?= esc((string) $hospital_wallpaper_url) ?>" alt="Hospital Wallpaper" style="max-height:80px;max-width:100%;object-fit:cover;border-radius:4px;">
<div class="small text-muted mt-1"><?= esc((string) ($hospital_wallpaper ?? '')) ?></div>
<?php else: ?>
<span class="text-muted small">No wallpaper uploaded (Login page uses blank/neutral)</span>
<?php endif; ?>
</div>
</div>
<div class="col-12 mt-3"><hr><h6 class="mb-2">Pharmacy Information</h6></div>
<div class="col-md-6">
<label class="form-label">Pharmacy Name</label>
@ -105,6 +122,7 @@
<div class="d-flex gap-2 flex-wrap mt-3">
<button type="button" class="btn btn-primary" id="btn_hospital_profile_save">Save Profile</button>
<button type="button" class="btn btn-outline-danger" id="btn_hospital_logo_delete">Delete Logo</button>
<button type="button" class="btn btn-outline-danger" id="btn_hospital_wallpaper_delete">Delete Wallpaper</button>
<button type="button" class="btn btn-outline-danger" id="btn_pharmacy_logo_delete">Delete Pharmacy Logo</button>
<button type="button" class="btn btn-outline-dark" id="btn_hospital_profile_reset">Reset All</button>
</div>
@ -149,6 +167,17 @@
wrap.html(html);
}
function renderWallpaper(targetId, url, name) {
var wrap = $('#' + targetId);
if (!url) {
wrap.html('<span class="text-muted small">No wallpaper uploaded (Login page uses blank/neutral)</span>');
return;
}
var html = '<img src="' + url + '" alt="Wallpaper" style="max-height:80px;max-width:100%;object-fit:cover;border-radius:4px;">'
+ '<div class="small text-muted mt-1">' + $('<div>').text(name || '').html() + '</div>';
wrap.html(html);
}
$('#btn_hospital_profile_save').on('click', function() {
var name = ($('#hospital_name').val() || '').trim();
var address = ($('#hospital_address_1').val() || '').trim();
@ -178,6 +207,10 @@
if (fileInput && fileInput.files && fileInput.files.length > 0) {
fd.append('hospital_logo', fileInput.files[0]);
}
var wallpaperInput = document.getElementById('hospital_wallpaper');
if (wallpaperInput && wallpaperInput.files && wallpaperInput.files.length > 0) {
fd.append('hospital_wallpaper', wallpaperInput.files[0]);
}
var pharmacyLogoInput = document.getElementById('pharmacy_logo');
if (pharmacyLogoInput && pharmacyLogoInput.files && pharmacyLogoInput.files.length > 0) {
fd.append('pharmacy_logo', pharmacyLogoInput.files[0]);
@ -200,10 +233,14 @@
if (fileInput) {
fileInput.value = '';
}
if (wallpaperInput) {
wallpaperInput.value = '';
}
if (pharmacyLogoInput) {
pharmacyLogoInput.value = '';
}
renderLogo('hospital_logo_preview', data.logo_url || '', data.logo_name || '');
renderWallpaper('hospital_wallpaper_preview', data.wallpaper_url || '', data.wallpaper_name || '');
renderLogo('pharmacy_logo_preview', data.pharmacy_logo_url || '', data.pharmacy_logo_name || '');
}).fail(function(xhr) {
var data = (xhr && xhr.responseJSON) ? xhr.responseJSON : {};
@ -237,6 +274,30 @@
});
});
$('#btn_hospital_wallpaper_delete').on('click', function() {
if (!confirm('Delete hospital login wallpaper?')) {
return;
}
var csrf = getCsrfPair();
var payload = {};
payload[csrf.name] = csrf.value;
payload.type = 'wallpaper';
$.post('<?= base_url('setting/admin/hospital-profile/delete-logo') ?>', payload, function(data) {
updateCsrf(data || {});
if ((data.update || 0) !== 1) {
showMessage('error', data.error_text || 'Unable to delete wallpaper.');
return;
}
showMessage('success', data.error_text || 'Wallpaper deleted.');
renderWallpaper('hospital_wallpaper_preview', '', '');
}, 'json').fail(function(xhr) {
var data = (xhr && xhr.responseJSON) ? xhr.responseJSON : {};
updateCsrf(data || {});
showMessage('error', data.error_text || 'Server error while deleting wallpaper.');
});
});
$('#btn_pharmacy_logo_delete').on('click', function() {
if (!confirm('Delete pharmacy logo?')) {
return;
@ -293,11 +354,16 @@
if (fileInput) {
fileInput.value = '';
}
var wallpaperInput = document.getElementById('hospital_wallpaper');
if (wallpaperInput) {
wallpaperInput.value = '';
}
var pharmacyLogoInput = document.getElementById('pharmacy_logo');
if (pharmacyLogoInput) {
pharmacyLogoInput.value = '';
}
renderLogo('hospital_logo_preview', '', '');
renderWallpaper('hospital_wallpaper_preview', '', '');
renderLogo('pharmacy_logo_preview', '', '');
showMessage('success', data.error_text || 'Profile reset completed.');
}, 'json').fail(function(xhr) {

View File

@ -1,4 +1,31 @@
<?php helper('common'); ?>
<?php
helper('common');
$hospitalName = hospital_setting_value('H_Name', '');
$hospitalAddress1 = hospital_setting_value('H_address_1', '');
$hospitalAddress2 = hospital_setting_value('H_address_2', '');
$hospitalPhone = hospital_setting_value('H_phone_No', '');
$hospitalEmail = hospital_setting_value('H_Email', '');
$hospitalHfrId = hospital_setting_value('H_HFR_ID', '');
$hospitalLogo = hospital_setting_value('H_logo', '');
$hospitalWallpaper = hospital_setting_value('H_login_wallpaper', '');
$hasLogo = false;
$logoUrl = '';
if ($hospitalLogo !== '' && is_file(FCPATH . 'assets/images/' . $hospitalLogo)) {
$hasLogo = true;
$logoUrl = base_url('assets/images/' . rawurlencode($hospitalLogo));
}
$hasWallpaper = false;
$wallpaperUrl = '';
if ($hospitalWallpaper !== '' && is_file(FCPATH . 'assets/images/' . $hospitalWallpaper)) {
$hasWallpaper = true;
$wallpaperUrl = base_url('assets/images/' . rawurlencode($hospitalWallpaper));
}
$footerVersion = hms_footer_version();
?>
<!DOCTYPE html>
<html lang="en">
@ -6,7 +33,7 @@
<meta charset="utf-8">
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<title><?= lang('Auth.login') ?></title>
<title><?= lang('Auth.login') ?> - <?= esc($hospitalName !== '' ? $hospitalName : 'E-Atria HMS') ?></title>
<meta content="" name="description">
<meta content="" name="keywords">
@ -18,186 +45,441 @@
<meta name="csrf-token" content="<?= csrf_hash() ?>">
<meta name="csrf-header" content="<?= config('Security')->headerName ?>">
<link href="assets/vendor/bootstrap/css/bootstrap.min.css" rel="stylesheet">
<link href="assets/vendor/bootstrap-icons/bootstrap-icons.css" rel="stylesheet">
<link href="assets/css/style.css" rel="stylesheet">
<link href="assets/css/custom-theme-dark-borders.css" rel="stylesheet">
<style>
html, body {
height: 100%;
margin: 0;
padding: 0;
background-color: #f8fafc;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
.login-page-wrapper {
min-height: 100vh;
display: flex;
width: 100%;
position: relative;
background-color: #f1f5f9;
}
/* Left Hero/Branding section */
.login-hero-section {
flex: 1;
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 3.5rem 4rem;
min-height: 100vh;
background-color: #f1f5f9;
overflow: hidden;
}
.login-hero-section.has-wallpaper {
background-size: cover;
background-position: center center;
background-repeat: no-repeat;
color: #ffffff;
}
.login-hero-section.has-wallpaper::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(135deg, rgba(15, 23, 42, 0.82) 0%, rgba(15, 23, 42, 0.45) 50%, rgba(15, 23, 42, 0.88) 100%);
z-index: 1;
}
.login-hero-inner {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.login-hero-badge {
background: rgba(255, 255, 255, 0.18);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
padding: 0.5rem 1.25rem;
display: inline-flex;
align-items: center;
gap: 0.75rem;
color: #ffffff;
width: fit-content;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.15);
}
.login-hero-title {
font-size: clamp(2rem, 3.5vw, 3.25rem);
font-weight: 800;
line-height: 1.15;
letter-spacing: -0.02em;
color: #ffffff;
text-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.login-hero-subtitle {
font-size: 1.15rem;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
max-width: 620px;
}
.login-hero-pill {
background: rgba(255, 255, 255, 0.14);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.25);
border-radius: 9999px;
padding: 0.4rem 1rem;
font-size: 0.85rem;
font-weight: 500;
color: #ffffff;
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
/* Hero Blank / Neutral State (First time / No Wallpaper) */
.login-hero-section.no-wallpaper {
background: linear-gradient(135deg, #f8fafc 0%, #eef2f6 100%);
border-right: 1px solid #e2e8f0;
color: #475569;
}
.login-hero-section.no-wallpaper .login-hero-title {
color: #0f172a;
text-shadow: none;
}
.login-hero-section.no-wallpaper .login-hero-subtitle {
color: #64748b;
text-shadow: none;
}
.login-hero-section.no-wallpaper .login-hero-badge {
background: #ffffff;
border: 1px solid #e2e8f0;
color: #1e293b;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
}
.login-hero-section.no-wallpaper .login-hero-pill {
background: #ffffff;
border: 1px solid #e2e8f0;
color: #475569;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.03);
}
/* Right Side Form Panel */
.login-form-section {
width: 460px;
max-width: 100%;
min-height: 100vh;
background: #ffffff;
border-left: 1px solid #e2e8f0;
box-shadow: -10px 0 40px rgba(0, 0, 0, 0.05);
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 1.5rem 1.75rem 1rem;
z-index: 10;
overflow-y: auto;
}
.login-form-container {
width: 100%;
max-width: 390px;
margin: auto 0;
}
@media (max-width: 991.98px) {
.login-page-wrapper {
flex-direction: column;
}
.login-hero-section {
min-height: auto;
padding: 2rem 1.5rem;
flex: none;
}
.login-hero-section.no-wallpaper {
display: none;
}
.login-form-section {
width: 100%;
min-height: auto;
border-left: none;
box-shadow: none;
padding: 1.5rem 1rem;
}
}
</style>
</head>
<body>
<?php $footerVersion = hms_footer_version(); ?>
<main>
<div class="container">
<section class="section register min-vh-100 d-flex flex-column align-items-center justify-content-center py-4">
<div class="container">
<div class="row justify-content-center">
<div class="col-lg-4 col-md-6 d-flex flex-column align-items-center justify-content-center">
<div class="d-flex justify-content-center py-4">
<a href="/" class="logo d-flex align-items-center w-auto">
<img src="assets/img/logo.png" alt="">
<span class="d-none d-lg-block">E-Atria</span>
</a>
</div>
<div class="card mb-3">
<div class="card-body">
<div class="pt-4 pb-2">
<h5 class="card-title text-center pb-0 fs-4"><?= lang('Auth.loginTitle') ?></h5>
<p class="text-center small"><?= lang('Auth.loginSubtitle') ?></p>
</div>
<?php if (session('error') !== null) : ?>
<div class="alert alert-danger" role="alert"><?= esc(session('error')) ?></div>
<?php elseif (session('errors') !== null) : ?>
<div class="alert alert-danger" role="alert">
<?php if (is_array(session('errors'))) : ?>
<?php foreach (session('errors') as $error) : ?>
<?= esc($error) ?>
<br>
<?php endforeach ?>
<?php else : ?>
<?= esc(session('errors')) ?>
<?php endif ?>
</div>
<?php endif ?>
<?php if (session('message') !== null) : ?>
<div class="alert alert-success" role="alert"><?= esc(session('message')) ?></div>
<?php endif ?>
<form action="<?= url_to('login') ?>" method="post" class="row g-3 needs-validation" novalidate>
<?= csrf_field() ?>
<div class="col-12">
<label for="floatingUsernameInput"><?= lang('Auth.username') ?></label>
<div class="input-group has-validation">
<span class="input-group-text" id="inputGroupPrepend">@</span>
<input type="text" class="form-control" id="floatingUsernameInput" name="username" inputmode="text" autocomplete="username" placeholder="<?= lang('Auth.username') ?>" value="<?= old('username') ?>" required>
<div class="invalid-feedback">Please enter your username.</div>
</div>
</div>
<div class="col-12">
<label for="floatingPasswordInput"><?= lang('Auth.password') ?></label>
<input type="password" name="password" class="form-control" id="floatingPasswordInput" autocomplete="current-password" required>
<div class="invalid-feedback">Please enter your password!</div>
</div>
<!-- Security Verification CAPTCHA (WASA Brute-Force & Bot Defense) -->
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="captchaInput" class="form-label mb-0 small fw-semibold text-secondary">
<i class="bi bi-shield-check text-primary me-1"></i> Security Code
</label>
<button type="button" class="btn btn-sm btn-link p-0 text-decoration-none" id="btnRefreshCaptcha" style="font-size: 11px;">
<i class="bi bi-arrow-clockwise"></i> Change Code
</button>
</div>
<div class="input-group has-validation">
<span class="input-group-text p-1 bg-white border-end-0" style="cursor: pointer;" title="Click to change image code" onclick="refreshLoginCaptcha();">
<img src="<?= base_url('captcha') ?>" id="captchaImage" alt="Security CAPTCHA" style="height: 36px; border-radius: 4px;">
</span>
<input type="text" name="captcha" id="captchaInput" class="form-control form-control-lg text-uppercase fw-bold text-center border-start-0 fs-5" placeholder="Enter Code" maxlength="6" autocomplete="off" required style="letter-spacing: 2px;">
<div class="invalid-feedback">Please enter the security verification characters.</div>
</div>
</div>
<script>
function refreshLoginCaptcha() {
var img = document.getElementById('captchaImage');
if (img) {
img.src = '<?= base_url('captcha') ?>?' + Date.now();
}
var inp = document.getElementById('captchaInput');
if (inp) {
inp.value = '';
inp.focus();
}
}
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('btnRefreshCaptcha');
if (btn) {
btn.addEventListener('click', refreshLoginCaptcha);
}
});
</script>
<?php if (setting('Auth.sessionConfig')['allowRemembering']) : ?>
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="remember" class="form-check-input" <?php if (old('remember')) : ?> checked<?php endif ?>>
<label class="form-check-label" for="rememberMe"><?= lang('Auth.rememberMe') ?></label>
</div>
</div>
<?php endif; ?>
<div class="col-12">
<button type="submit" class="btn btn-primary w-100"><?= lang('Auth.login') ?></button>
</div>
<?php if (setting('Auth.allowMagicLinkLogins')) : ?>
<div class="col-12">
<p class="text-center"><?= lang('Auth.forgotPassword') ?> <a href="<?= url_to('magic-link') ?>"><?= lang('Auth.useMagicLink') ?></a></p>
</div>
<?php endif ?>
<!-- Mobile PWA Quick Scan Button inside Login Card -->
<hr class="my-2 opacity-25">
<div class="col-12 text-center">
<button type="button" class="btn btn-outline-primary w-100 py-2 d-flex align-items-center justify-content-center gap-2 fw-semibold shadow-sm" id="btn_open_mobile_qr_modal" data-bs-toggle="modal" data-bs-target="#loginMobileAppsModal" style="border-radius: 8px;">
<i class="bi bi-qr-code-scan fs-5"></i>
<span>Scan QR for Mobile Apps (PWA)</span>
</button>
<div class="small text-muted mt-1" style="font-size: 11px;">
<i class="bi bi-phone me-1"></i> Scan with phone camera &bull; Install to home screen
</div>
</div>
</form>
</div>
</div>
<!-- Mobile Application QR & PWA Installation Card -->
<div class="card border-0 shadow-sm mt-1 mb-2 w-100 text-center" style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); border-radius: 12px; color: white;">
<div class="card-body p-3">
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="badge bg-primary px-2 py-1" style="font-size: 10px;"><i class="bi bi-phone me-1"></i> Mobile PWA Suite</span>
<span class="badge bg-success-subtle text-success border border-success-subtle" style="font-size: 10px;"><i class="bi bi-download me-1"></i> Installable Apps</span>
</div>
<h6 class="fw-bold text-white mb-1" style="font-size: 14px;"><i class="bi bi-qr-code-scan me-1 text-info"></i> Hospital Mobile Apps &amp; QR Codes</h6>
<p class="text-light small mb-2 opacity-75" style="font-size: 11px; line-height: 1.4;">
Instant home-screen installation on Android &amp; iPhone. Scan QR code to launch Nursing, Doctor, Pharmacy POS &amp; Token Queue apps.
</p>
<button type="button" class="btn btn-primary btn-sm w-100 fw-bold d-flex align-items-center justify-content-center gap-2 shadow-sm py-2" data-bs-toggle="modal" data-bs-target="#loginMobileAppsModal" style="border-radius: 8px;">
<i class="bi bi-qr-code"></i> View All Mobile Apps &amp; QR Codes
</button>
</div>
</div>
<div class="text-center mt-1 mb-3">
<a href="<?= base_url('HMS_FEATURES_PRESENTATION.html') ?>" target="_blank" rel="noopener"
class="text-muted small d-inline-flex align-items-center gap-1" style="text-decoration:none;">
<i class="bi bi-file-earmark-text"></i> View HMS Features &amp; Capabilities
</a>
</div>
<main class="login-page-wrapper">
<?php if ($hasWallpaper): ?>
<!-- Left Branding / Wallpaper Hero Section -->
<section class="login-hero-section has-wallpaper" style="background-image: url('<?= esc($wallpaperUrl) ?>');">
<div class="login-hero-inner">
<!-- Top Header / Hospital Tag -->
<div>
<?php if ($hasLogo): ?>
<div class="login-hero-badge">
<img src="<?= esc($logoUrl) ?>" alt="Hospital Logo" style="max-height: 38px; max-width: 140px; object-fit: contain;">
<?php if (!empty($hospitalName)): ?>
<span class="fw-bold small"><?= esc($hospitalName) ?></span>
<?php endif; ?>
</div>
<?php elseif (!empty($hospitalName)): ?>
<div class="login-hero-badge">
<i class="bi bi-hospital fs-5 text-warning"></i>
<span class="fw-bold small"><?= esc($hospitalName) ?></span>
</div>
<?php endif; ?>
</div>
<!-- Center / Bottom Hospital Branding Showcase -->
<div class="my-auto py-4">
<?php if (!empty($hospitalName)): ?>
<h1 class="login-hero-title mb-2"><?= esc($hospitalName) ?></h1>
<?php else: ?>
<h1 class="login-hero-title mb-2">Hospital Information Management System</h1>
<?php endif; ?>
<p class="login-hero-subtitle mb-4">
Secure Healthcare Delivery, Digital EMR &amp; Clinical Management Portal
</p>
<?php if (!empty($hospitalAddress1) || !empty($hospitalPhone) || !empty($hospitalHfrId)): ?>
<div class="d-flex flex-wrap gap-3 align-items-center mb-4 text-white-50 small">
<?php if (!empty($hospitalAddress1)): ?>
<span class="d-inline-flex align-items-center gap-1">
<i class="bi bi-geo-alt-fill text-warning"></i> <?= esc($hospitalAddress1) ?><?= !empty($hospitalAddress2) ? ', ' . esc($hospitalAddress2) : '' ?>
</span>
<?php endif; ?>
<?php if (!empty($hospitalPhone)): ?>
<span class="d-inline-flex align-items-center gap-1">
<i class="bi bi-telephone-fill text-success"></i> <?= esc($hospitalPhone) ?>
</span>
<?php endif; ?>
<?php if (!empty($hospitalHfrId)): ?>
<span class="badge bg-primary bg-opacity-75 border border-primary px-2 py-1">
<i class="bi bi-shield-check me-1"></i> HFR ID: <?= esc($hospitalHfrId) ?>
</span>
<?php endif; ?>
</div>
<?php endif; ?>
<div class="d-flex flex-wrap gap-2">
<span class="login-hero-pill">
<i class="bi bi-shield-check text-success"></i> ABDM M1, M2 &amp; M3 Integrated
</span>
<span class="login-hero-pill">
<i class="bi bi-hospital text-info"></i> OPD, IPD &amp; E-Prescription
</span>
<span class="login-hero-pill">
<i class="bi bi-phone text-warning"></i> Mobile PWA &amp; Bedside Care
</span>
</div>
</div>
</section>
<!-- Hero Footer Note -->
<div class="small text-white-50">
<i class="bi bi-shield-lock me-1"></i> Government of India ABDM Compliant &bull; Digital Health Mission
</div>
</div>
</section>
<?php else: ?>
<!-- No Wallpaper: Simple Blank / Plain Color as requested by user -->
<section class="login-hero-section no-wallpaper">
<div class="login-hero-inner justify-content-center align-items-center text-center">
<!-- Clean Blank Neutral Surface -->
</div>
</section>
<?php endif; ?>
<!-- Right Side Login Form Section -->
<section class="login-form-section">
<div class="login-form-container">
<!-- System / E-Atria Branding Header -->
<div class="text-center mb-2">
<a href="/" class="logo d-inline-flex align-items-center justify-content-center text-decoration-none">
<img src="assets/img/logo.png" alt="E-Atria" style="height: 34px;">
<span class="fs-4 fw-bold text-primary ms-2">E-Atria</span>
</a>
</div>
<!-- Login Card -->
<div class="card mb-2 border-0 shadow-sm" style="border-radius: 12px; background: #ffffff;">
<div class="card-body p-3">
<div class="pb-1 text-center">
<h5 class="card-title text-center pb-0 fs-5 fw-bold text-dark mb-1"><?= lang('Auth.loginTitle') ?></h5>
<p class="text-center small text-muted mb-2" style="font-size: 12px;"><?= lang('Auth.loginSubtitle') ?></p>
</div>
<?php if (session('error') !== null) : ?>
<div class="alert alert-danger" role="alert"><?= esc(session('error')) ?></div>
<?php elseif (session('errors') !== null) : ?>
<div class="alert alert-danger" role="alert">
<?php if (is_array(session('errors'))) : ?>
<?php foreach (session('errors') as $error) : ?>
<?= esc($error) ?>
<br>
<?php endforeach ?>
<?php else : ?>
<?= esc(session('errors')) ?>
<?php endif ?>
</div>
<?php endif ?>
<?php if (session('message') !== null) : ?>
<div class="alert alert-success" role="alert"><?= esc(session('message')) ?></div>
<?php endif ?>
<form action="<?= url_to('login') ?>" method="post" class="row g-3 needs-validation" novalidate>
<?= csrf_field() ?>
<div class="col-12">
<label for="floatingUsernameInput"><?= lang('Auth.username') ?></label>
<div class="input-group has-validation">
<span class="input-group-text" id="inputGroupPrepend">@</span>
<input type="text" class="form-control" id="floatingUsernameInput" name="username" inputmode="text" autocomplete="username" placeholder="<?= lang('Auth.username') ?>" value="<?= old('username') ?>" required>
<div class="invalid-feedback">Please enter your username.</div>
</div>
</div>
<div class="col-12">
<label for="floatingPasswordInput"><?= lang('Auth.password') ?></label>
<input type="password" name="password" class="form-control" id="floatingPasswordInput" autocomplete="current-password" required>
<div class="invalid-feedback">Please enter your password!</div>
</div>
<!-- Security Verification CAPTCHA (WASA Brute-Force & Bot Defense) -->
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="captchaInput" class="form-label mb-0 small fw-semibold text-secondary">
<i class="bi bi-shield-check text-primary me-1"></i> Security Code
</label>
<button type="button" class="btn btn-sm btn-link p-0 text-decoration-none" id="btnRefreshCaptcha" style="font-size: 11px;">
<i class="bi bi-arrow-clockwise"></i> Change Code
</button>
</div>
<div class="input-group has-validation">
<span class="input-group-text p-1 bg-white border-end-0" style="cursor: pointer;" title="Click to change image code" onclick="refreshLoginCaptcha();">
<img src="<?= base_url('captcha') ?>" id="captchaImage" alt="Security CAPTCHA" style="height: 36px; border-radius: 4px;">
</span>
<input type="text" name="captcha" id="captchaInput" class="form-control form-control-lg text-uppercase fw-bold text-center border-start-0 fs-5" placeholder="Enter Code" maxlength="6" autocomplete="off" required style="letter-spacing: 2px;">
<div class="invalid-feedback">Please enter the security verification characters.</div>
</div>
</div>
<script>
function refreshLoginCaptcha() {
var img = document.getElementById('captchaImage');
if (img) {
img.src = '<?= base_url('captcha') ?>?' + Date.now();
}
var inp = document.getElementById('captchaInput');
if (inp) {
inp.value = '';
inp.focus();
}
}
document.addEventListener('DOMContentLoaded', function() {
var btn = document.getElementById('btnRefreshCaptcha');
if (btn) {
btn.addEventListener('click', refreshLoginCaptcha);
}
});
</script>
<?php if (setting('Auth.sessionConfig')['allowRemembering']) : ?>
<div class="col-12">
<div class="form-check">
<input type="checkbox" name="remember" class="form-check-input" <?php if (old('remember')) : ?> checked<?php endif ?>>
<label class="form-check-label" for="rememberMe"><?= lang('Auth.rememberMe') ?></label>
</div>
</div>
<?php endif; ?>
<div class="col-12">
<button type="submit" class="btn btn-primary w-100 py-2 fw-semibold" style="border-radius: 8px;"><?= lang('Auth.login') ?></button>
</div>
<?php if (setting('Auth.allowMagicLinkLogins')) : ?>
<div class="col-12">
<p class="text-center small"><?= lang('Auth.forgotPassword') ?> <a href="<?= url_to('magic-link') ?>"><?= lang('Auth.useMagicLink') ?></a></p>
</div>
<?php endif ?>
<!-- Mobile PWA Quick Scan Button inside Login Card -->
<hr class="my-2 opacity-25">
<div class="col-12 text-center">
<button type="button" class="btn btn-outline-primary w-100 py-2 d-flex align-items-center justify-content-center gap-2 fw-semibold shadow-sm" id="btn_open_mobile_qr_modal" data-bs-toggle="modal" data-bs-target="#loginMobileAppsModal" style="border-radius: 8px;">
<i class="bi bi-qr-code-scan fs-5"></i>
<span>Scan QR for Mobile Apps (PWA)</span>
</button>
<div class="small text-muted mt-1" style="font-size: 11px;">
<i class="bi bi-phone me-1"></i> Scan with phone camera &bull; Install to home screen
</div>
</div>
</form>
</div>
</div>
<!-- Mobile Application QR & PWA Installation Card -->
<div class="card border-0 shadow-sm mt-1 mb-2 w-100 text-center" style="background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); border-radius: 12px; color: white;">
<div class="card-body p-3">
<div class="d-flex align-items-center justify-content-between mb-2">
<span class="badge bg-primary px-2 py-1" style="font-size: 10px;"><i class="bi bi-phone me-1"></i> Mobile PWA Suite</span>
<span class="badge bg-success-subtle text-success border border-success-subtle" style="font-size: 10px;"><i class="bi bi-download me-1"></i> Installable Apps</span>
</div>
<h6 class="fw-bold text-white mb-1" style="font-size: 14px;"><i class="bi bi-qr-code-scan me-1 text-info"></i> Hospital Mobile Apps &amp; QR Codes</h6>
<p class="text-light small mb-2 opacity-75" style="font-size: 11px; line-height: 1.4;">
Instant home-screen installation on Android &amp; iPhone. Scan QR code to launch Nursing, Doctor, Pharmacy POS &amp; Token Queue apps.
</p>
<button type="button" class="btn btn-primary btn-sm w-100 fw-bold d-flex align-items-center justify-content-center gap-2 shadow-sm py-2" data-bs-toggle="modal" data-bs-target="#loginMobileAppsModal" style="border-radius: 8px;">
<i class="bi bi-qr-code"></i> View All Mobile Apps &amp; QR Codes
</button>
</div>
</div>
<div class="text-center mt-1 mb-2">
<a href="<?= base_url('HMS_FEATURES_PRESENTATION.html') ?>" target="_blank" rel="noopener"
class="text-muted small d-inline-flex align-items-center gap-1" style="text-decoration:none;">
<i class="bi bi-file-earmark-text"></i> View HMS Features &amp; Capabilities
</a>
</div>
</div>
<!-- Panel Footer -->
<footer class="pt-3 text-center text-muted small border-top mt-auto" style="font-size: 11px;">
<div>&copy; 2017 - 2026 <strong><a href="https://www.e-atria.in" target="_blank" rel="noopener" class="text-decoration-none">E-Atria</a></strong>. All Rights Reserved.</div>
<div class="mt-1">
<a href="<?= base_url('software-use-license.html') ?>" target="_blank" rel="noopener" class="text-muted text-decoration-none">Software Use License</a>
&bull; Version: <span class="fw-semibold"><?= esc($footerVersion) ?></span>
</div>
</footer>
</section>
</div>
</main>
<?php
@ -919,12 +1201,6 @@
})();
</script>
<footer class="py-3 text-center text-muted small">
&copy; 2017 - 2026 <strong><a href="https://www.e-atria.in" target="_blank" rel="noopener">E-Atria</a></strong>. All Rights Reserved |
<a href="<?= base_url('software-use-license.html') ?>" target="_blank" rel="noopener">Software Use License</a> |
Version: <?= esc($footerVersion) ?>
</footer>
<a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
<script src="assets/vendor/bootstrap/js/bootstrap.bundle.min.js"></script>

Binary file not shown.

After

Width:  |  Height:  |  Size: 995 KiB

View File

@ -0,0 +1,71 @@
<?php
namespace Tests\Unit;
use CodeIgniter\Test\CIUnitTestCase;
/**
* @internal
*/
final class HospitalProfileWallpaperTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
helper('common');
helper('setting');
$db = \Config\Database::connect();
$db->query("CREATE TABLE IF NOT EXISTS db_hospital_setting (id INTEGER PRIMARY KEY AUTOINCREMENT, s_name TEXT, s_value TEXT)");
$db->query("CREATE TABLE IF NOT EXISTS db_settings (id INTEGER PRIMARY KEY AUTOINCREMENT, class TEXT, key TEXT, value TEXT, type TEXT, context TEXT, created_at TEXT, updated_at TEXT)");
}
public function testHospitalProfileViewContainsWallpaperControls(): void
{
$viewData = [
'hospital_name' => 'City Hospital',
'hospital_address_1' => '123 Medical Road',
'hospital_address_2' => 'Civil Lines',
'hospital_phone' => '9876543210',
'hospital_email' => 'admin@cityhospital.in',
'hospital_hfr_id' => 'IN0510000871',
'footer_version' => '2026.01',
'hospital_logo' => 'test_logo.png',
'hospital_logo_url' => 'http://localhost:8080/assets/images/test_logo.png',
'hospital_wallpaper' => 'test_wallpaper.jpg',
'hospital_wallpaper_url' => 'http://localhost:8080/assets/images/test_wallpaper.jpg',
'pharmacy_name' => 'City Hospital Pharmacy',
'pharmacy_address' => 'Ground Floor',
'pharmacy_phone' => '9876543211',
'pharmacy_gst' => '05ABIPR7922F1ZJ',
'pharmacy_logo' => '',
'pharmacy_logo_url' => '',
'sidebar_auto_hide_seconds' => '7',
'allow_image_preupload_edit' => '0',
];
$html = view('Setting/Admin/hospital_profile', $viewData);
$this->assertStringContainsString('id="hospital_wallpaper"', $html);
$this->assertStringContainsString('id="hospital_wallpaper_preview"', $html);
$this->assertStringContainsString('id="btn_hospital_wallpaper_delete"', $html);
$this->assertStringContainsString('test_wallpaper.jpg', $html);
$this->assertStringContainsString('renderWallpaper', $html);
}
public function testLoginViewRendersWithRightSideLayoutAndKeyElements(): void
{
$html = view('auth/login');
// Check right side layout container
$this->assertStringContainsString('login-page-wrapper', $html);
$this->assertStringContainsString('login-form-section', $html);
$this->assertStringContainsString('login-hero-section', $html);
// Verify key login elements exist in the right panel
$this->assertStringContainsString('floatingUsernameInput', $html);
$this->assertStringContainsString('floatingPasswordInput', $html);
$this->assertStringContainsString('captchaInput', $html);
$this->assertStringContainsString('loginMobileAppsModal', $html);
}
}