1225 lines
31 KiB
Plaintext
1225 lines
31 KiB
Plaintext
{% extends 'base.html' %}
|
|
{% load static custom_filters %}
|
|
|
|
{% block title %}Bed Management - Hospital HMS{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
:root {
|
|
--primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--success-gradient: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
|
|
--warning-gradient: linear-gradient(135deg, #fcb045 0%, #fd1d1d 100%);
|
|
--info-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
--danger-gradient: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
|
|
--maintenance-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
|
--cleaning-gradient: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
--shadow-light: 0 2px 10px rgba(0,0,0,0.1);
|
|
--shadow-medium: 0 4px 20px rgba(0,0,0,0.15);
|
|
--shadow-heavy: 0 8px 30px rgba(0,0,0,0.2);
|
|
--border-radius: 12px;
|
|
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.management-header {
|
|
background: var(--primary-gradient);
|
|
color: white;
|
|
padding: 2rem;
|
|
border-radius: var(--border-radius);
|
|
margin-bottom: 2rem;
|
|
box-shadow: var(--shadow-medium);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.management-header::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -50%;
|
|
right: -50%;
|
|
width: 200%;
|
|
height: 200%;
|
|
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 70%);
|
|
animation: rotate 20s linear infinite;
|
|
}
|
|
|
|
@keyframes rotate {
|
|
from { transform: rotate(0deg); }
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
padding: 1.5rem;
|
|
box-shadow: var(--shadow-light);
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
transition: var(--transition);
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.stat-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 4px;
|
|
height: 100%;
|
|
background: var(--primary-gradient);
|
|
}
|
|
|
|
.stat-card:hover {
|
|
transform: translateY(-5px);
|
|
box-shadow: var(--shadow-heavy);
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 60px;
|
|
height: 60px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.5rem;
|
|
margin-bottom: 1rem;
|
|
position: relative;
|
|
}
|
|
|
|
.stat-icon.total { background: var(--primary-gradient); color: white; }
|
|
.stat-icon.available { background: var(--success-gradient); color: white; }
|
|
.stat-icon.occupied { background: var(--danger-gradient); color: white; }
|
|
.stat-icon.maintenance { background: var(--warning-gradient); color: white; }
|
|
|
|
.stat-number {
|
|
font-size: 2.5rem;
|
|
font-weight: 700;
|
|
margin-bottom: 0.5rem;
|
|
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 0.9rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.stat-trend {
|
|
font-size: 0.8rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.trend-up { color: #28a745; }
|
|
.trend-down { color: #dc3545; }
|
|
|
|
.main-content {
|
|
display: grid;
|
|
grid-template-columns: 1fr 320px;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.filters-section {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
padding: 1.5rem;
|
|
box-shadow: var(--shadow-light);
|
|
margin-bottom: 2rem;
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
}
|
|
|
|
.filters-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.filter-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.filter-label {
|
|
font-size: 0.85rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.filter-input {
|
|
padding: 0.5rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.filter-input:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
|
}
|
|
|
|
.search-input {
|
|
position: relative;
|
|
}
|
|
|
|
.search-input i {
|
|
position: absolute;
|
|
right: 0.75rem;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
color: #6c757d;
|
|
}
|
|
|
|
.ward-section {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
padding: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
box-shadow: var(--shadow-light);
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.ward-section:hover {
|
|
box-shadow: var(--shadow-medium);
|
|
}
|
|
|
|
.ward-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.ward-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin: 0;
|
|
}
|
|
|
|
.ward-stats {
|
|
display: flex;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
}
|
|
|
|
.ward-stat {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
font-size: 0.85rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.beds-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.bed-card {
|
|
background: white;
|
|
border-radius: 10px;
|
|
padding: 1rem;
|
|
box-shadow: var(--shadow-light);
|
|
border: 2px solid transparent;
|
|
transition: var(--transition);
|
|
cursor: pointer;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.bed-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 4px;
|
|
background: var(--primary-gradient);
|
|
}
|
|
|
|
.bed-card:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: var(--shadow-medium);
|
|
}
|
|
|
|
.bed-card.selected {
|
|
border-color: #667eea;
|
|
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
|
|
}
|
|
|
|
.bed-card.available {
|
|
background: linear-gradient(135deg, #f8fff8 0%, #e8f5e8 100%);
|
|
border-left: 4px solid #28a745;
|
|
}
|
|
|
|
.bed-card.available::before {
|
|
background: var(--success-gradient);
|
|
}
|
|
|
|
.bed-card.occupied {
|
|
background: linear-gradient(135deg, #fff8f8 0%, #feeaea 100%);
|
|
border-left: 4px solid #dc3545;
|
|
}
|
|
|
|
.bed-card.occupied::before {
|
|
background: var(--danger-gradient);
|
|
}
|
|
|
|
.bed-card.maintenance {
|
|
background: linear-gradient(135deg, #fffbf0 0%, #fef3c7 100%);
|
|
border-left: 4px solid #f59e0b;
|
|
}
|
|
|
|
.bed-card.maintenance::before {
|
|
background: var(--warning-gradient);
|
|
}
|
|
|
|
.bed-card.cleaning {
|
|
background: linear-gradient(135deg, #f0fdff 0%, #cffafe 100%);
|
|
border-left: 4px solid #06b6d4;
|
|
}
|
|
|
|
.bed-card.cleaning::before {
|
|
background: var(--cleaning-gradient);
|
|
}
|
|
|
|
.bed-card.blocked {
|
|
background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
|
|
border-left: 4px solid #ef4444;
|
|
}
|
|
|
|
.bed-card.out_of_order {
|
|
background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%);
|
|
border-left: 4px solid #6b7280;
|
|
}
|
|
|
|
.bed-icon-container {
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.bed-icon {
|
|
width: 50px;
|
|
height: 50px;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1.25rem;
|
|
margin-bottom: 0.5rem;
|
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.bed-card.available .bed-icon { background: var(--success-gradient); color: white; }
|
|
.bed-card.occupied .bed-icon { background: var(--danger-gradient); color: white; }
|
|
.bed-card.maintenance .bed-icon { background: var(--warning-gradient); color: white; }
|
|
.bed-card.cleaning .bed-icon { background: var(--cleaning-gradient); color: white; }
|
|
.bed-card.blocked .bed-icon { background: var(--danger-gradient); color: white; }
|
|
.bed-card.out_of_order .bed-icon { background: #f3f4f6; color: #6b7280; }
|
|
|
|
.bed-number {
|
|
font-size: 1.1rem;
|
|
font-weight: 700;
|
|
color: #1f2937;
|
|
text-align: center;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.bed-room {
|
|
font-size: 0.8rem;
|
|
color: #6b7280;
|
|
text-align: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.bed-patient {
|
|
font-size: 0.85rem;
|
|
color: #374151;
|
|
text-align: center;
|
|
margin-bottom: 0.25rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.bed-time {
|
|
font-size: 0.75rem;
|
|
color: #9ca3af;
|
|
text-align: center;
|
|
}
|
|
|
|
.bed-status {
|
|
position: absolute;
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 12px;
|
|
font-size: 0.7rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.bed-card.available .bed-status { background: #dcfce7; color: #166534; }
|
|
.bed-card.occupied .bed-status { background: #fee2e2; color: #991b1b; }
|
|
.bed-card.maintenance .bed-status { background: #fef3c7; color: #92400e; }
|
|
.bed-card.cleaning .bed-status { background: #cffafe; color: #0e7490; }
|
|
.bed-card.blocked .bed-status { background: #fee2e2; color: #991b1b; }
|
|
.bed-card.out_of_order .bed-status { background: #f3f4f6; color: #374151; }
|
|
|
|
.bed-actions {
|
|
position: absolute;
|
|
bottom: 0.5rem;
|
|
left: 0.5rem;
|
|
right: 0.5rem;
|
|
opacity: 0;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.bed-card:hover .bed-actions {
|
|
opacity: 1;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
justify-content: center;
|
|
}
|
|
|
|
.action-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.action-btn:hover {
|
|
transform: scale(1.1);
|
|
}
|
|
|
|
.action-btn.view { background: #e3f2fd; color: #1976d2; }
|
|
.action-btn.edit { background: #f3e5f5; color: #7b1fa2; }
|
|
.action-btn.assign { background: #e8f5e8; color: #388e3c; }
|
|
.action-btn.discharge { background: #fff3e0; color: #f57c00; }
|
|
.action-btn.more { background: #f5f5f5; color: #616161; }
|
|
|
|
.sidebar {
|
|
position: sticky;
|
|
top: 2rem;
|
|
}
|
|
|
|
.sidebar-panel {
|
|
background: white;
|
|
border-radius: var(--border-radius);
|
|
padding: 1.5rem;
|
|
box-shadow: var(--shadow-light);
|
|
border: 1px solid rgba(0,0,0,0.05);
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 1rem;
|
|
padding-bottom: 0.75rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #1f2937;
|
|
margin: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.panel-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.panel-icon.primary { background: var(--primary-gradient); color: white; }
|
|
.panel-icon.success { background: var(--success-gradient); color: white; }
|
|
.panel-icon.warning { background: var(--warning-gradient); color: white; }
|
|
|
|
.occupancy-display {
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.occupancy-percentage {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
background: var(--primary-gradient);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-clip: text;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.occupancy-label {
|
|
font-size: 0.9rem;
|
|
color: #6b7280;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.occupancy-bar {
|
|
height: 12px;
|
|
background: #e5e7eb;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.occupancy-fill {
|
|
height: 100%;
|
|
background: var(--primary-gradient);
|
|
border-radius: 6px;
|
|
transition: width 0.5s ease;
|
|
}
|
|
|
|
.occupancy-stats {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 0.5rem;
|
|
font-size: 0.8rem;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.ward-breakdown {
|
|
space-y: 0.75rem;
|
|
}
|
|
|
|
.ward-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding: 0.5rem;
|
|
border-radius: 6px;
|
|
background: #f9fafb;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.ward-name {
|
|
font-weight: 500;
|
|
color: #374151;
|
|
}
|
|
|
|
.ward-occupancy {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.ward-bar {
|
|
width: 60px;
|
|
height: 6px;
|
|
background: #e5e7eb;
|
|
border-radius: 3px;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ward-fill {
|
|
height: 100%;
|
|
border-radius: 3px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.ward-percentage {
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
color: #6b7280;
|
|
min-width: 35px;
|
|
text-align: right;
|
|
}
|
|
|
|
.quick-actions {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.quick-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border: 1px solid #e5e7eb;
|
|
border-radius: 8px;
|
|
background: white;
|
|
color: #374151;
|
|
text-decoration: none;
|
|
transition: var(--transition);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.quick-btn:hover {
|
|
border-color: #667eea;
|
|
background: #f8faff;
|
|
color: #667eea;
|
|
transform: translateX(2px);
|
|
}
|
|
|
|
.quick-btn i {
|
|
font-size: 1.1rem;
|
|
width: 20px;
|
|
}
|
|
|
|
.loading-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(255,255,255,0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.loading-spinner {
|
|
width: 40px;
|
|
height: 40px;
|
|
border: 4px solid #f3f3f3;
|
|
border-top: 4px solid #667eea;
|
|
border-radius: 50%;
|
|
animation: spin 1s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
0% { transform: rotate(0deg); }
|
|
100% { transform: rotate(360deg); }
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.main-content {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.sidebar {
|
|
position: static;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.stats-grid {
|
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
|
}
|
|
|
|
.beds-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
}
|
|
|
|
.filters-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.ward-stats {
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
align-items: flex-start;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 576px) {
|
|
.beds-grid {
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
}
|
|
|
|
.management-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.stat-card {
|
|
padding: 1rem;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
|
<div>
|
|
<h1 class="h2">
|
|
<i class="fas fa-bed me-2"></i>Bed<span class="fw-light">Management</span>
|
|
</h1>
|
|
<p class="text-muted">Real-time bed occupancy and management dashboard</p>
|
|
</div>
|
|
<div class="btn-toolbar mb-2 mb-md-0">
|
|
<div class="btn-group me-2">
|
|
<a href="{% url 'inpatients:bed_create' %}" class="btn btn-primary btn-action">
|
|
<i class="fas fa-plus me-1"></i>Add Bed
|
|
</a>
|
|
<button class="btn btn-outline-info btn-action" onclick="viewFloorPlan()">
|
|
<i class="fas fa-map me-1"></i>Floor Plan
|
|
</button>
|
|
<button class="btn btn-outline-success btn-action" onclick="generateReport()">
|
|
<i class="fas fa-chart-bar me-1"></i>Reports
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Management Header -->
|
|
<div class="management-header">
|
|
<div class="row">
|
|
<div class="col-lg-8">
|
|
<h3 class="mb-2">Hospital Bed Management System</h3>
|
|
<p class="mb-0 opacity-75">Monitor and manage bed occupancy across all wards in real-time</p>
|
|
</div>
|
|
<div class="col-lg-4 text-end">
|
|
<div class="d-flex align-items-center justify-content-end gap-3">
|
|
<div>
|
|
<div class="fw-bold fs-4">{{ total_beds }}</div>
|
|
<small class="text-white-50">Total Beds</small>
|
|
</div>
|
|
<div class="vr bg-white opacity-25"></div>
|
|
<div>
|
|
<div class="fw-bold fs-4 text-success">{{ available_beds }}</div>
|
|
<small class="text-white-50">Available</small>
|
|
</div>
|
|
<div class="vr bg-white opacity-25"></div>
|
|
<div>
|
|
<div class="fw-bold fs-4 text-danger">{{ occupied_beds }}</div>
|
|
<small class="text-white-50">Occupied</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon total">
|
|
<i class="fas fa-bed"></i>
|
|
</div>
|
|
<div class="stat-number">{{ total_beds }}</div>
|
|
<div class="stat-label">Total Beds</div>
|
|
<div class="stat-trend">
|
|
<i class="fas fa-arrow-up trend-up"></i>
|
|
<span>100% Capacity</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon available">
|
|
<i class="fas fa-check-circle"></i>
|
|
</div>
|
|
<div class="stat-number">{{ available_beds }}</div>
|
|
<div class="stat-label">Available Beds</div>
|
|
<div class="stat-trend">
|
|
<i class="fas fa-arrow-{% if available_beds > total_beds|div:2 %}up trend-up{% else %}down trend-down{% endif %}"></i>
|
|
<span>{{ available_beds|div:total_beds|mul:100|floatformat:0 }}% Free</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon occupied">
|
|
<i class="fas fa-users"></i>
|
|
</div>
|
|
<div class="stat-number">{{ occupied_beds }}</div>
|
|
<div class="stat-label">Occupied Beds</div>
|
|
<div class="stat-trend">
|
|
<i class="fas fa-arrow-{% if occupied_beds > total_beds|div:2 %}up trend-up{% else %}down trend-down{% endif %}"></i>
|
|
<span>{{ occupied_beds|div:total_beds|mul:100|floatformat:0 }}% Utilized</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon maintenance">
|
|
<i class="fas fa-tools"></i>
|
|
</div>
|
|
<div class="stat-number">{{ maintenance_beds }}</div>
|
|
<div class="stat-label">Maintenance</div>
|
|
<div class="stat-trend">
|
|
<i class="fas fa-arrow-{% if maintenance_beds > 0 %}up trend-down{% else %}down trend-up{% endif %}"></i>
|
|
<span>{{ maintenance_beds|div:total_beds|mul:100|floatformat:0 }}% Down</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Content -->
|
|
<div class="main-content">
|
|
<!-- Beds Section -->
|
|
<div class="beds-section">
|
|
<!-- Filters -->
|
|
<div class="filters-section">
|
|
<div class="filters-grid">
|
|
<div class="filter-group">
|
|
<label class="filter-label">Ward</label>
|
|
<select class="filter-input" id="wardFilter">
|
|
<option value="">All Wards</option>
|
|
{% for ward in wards %}
|
|
<option value="{{ ward.id }}">{{ ward.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label class="filter-label">Status</label>
|
|
<select class="filter-input" id="statusFilter">
|
|
<option value="">All Statuses</option>
|
|
<option value="AVAILABLE">Available</option>
|
|
<option value="OCCUPIED">Occupied</option>
|
|
<option value="MAINTENANCE">Maintenance</option>
|
|
<option value="BLOCKED">Blocked</option>
|
|
<option value="CLEANING">Cleaning</option>
|
|
<option value="OUT_OF_ORDER">Out of Order</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label class="filter-label">Bed Type</label>
|
|
<select class="filter-input" id="bedTypeFilter">
|
|
<option value="">All Types</option>
|
|
<option value="STANDARD">Standard</option>
|
|
<option value="ICU">ICU</option>
|
|
<option value="ISOLATION">Isolation</option>
|
|
<option value="PEDIATRIC">Pediatric</option>
|
|
<option value="MATERNITY">Maternity</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="filter-group">
|
|
<label class="filter-label">Search</label>
|
|
<div class="search-input">
|
|
<input type="text" class="filter-input" placeholder="Search beds..." id="bedSearch" />
|
|
<i class="fas fa-search"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ward Sections -->
|
|
{% for ward in wards %}
|
|
<div class="ward-section" data-ward="{{ ward.id }}">
|
|
<div class="ward-header">
|
|
<h5 class="ward-title">
|
|
<i class="fas fa-hospital me-2 text-primary"></i>{{ ward.name }}
|
|
</h5>
|
|
<div class="ward-stats">
|
|
<div class="ward-stat">
|
|
<i class="fas fa-bed me-1"></i>
|
|
<span>{{ ward.beds.count }} beds</span>
|
|
</div>
|
|
<div class="ward-stat">
|
|
<i class="fas fa-chart-pie me-1"></i>
|
|
<span>{{ ward.occupancy_rate|floatformat:0 }}% occupied</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="beds-grid">
|
|
{% for bed in ward.beds.all %}
|
|
<div class="bed-card {{ bed.status|lower }}"
|
|
data-bed-id="{{ bed.id }}"
|
|
data-status="{{ bed.status|lower }}"
|
|
data-type="{{ bed.bed_type }}"
|
|
onclick="selectBed('{{ bed.id }}')">
|
|
<div class="bed-icon-container">
|
|
<div class="bed-icon">
|
|
<i class="fas fa-bed"></i>
|
|
</div>
|
|
<div class="bed-number">{{ bed.bed_number }}</div>
|
|
<div class="bed-room">Room {{ bed.room_number }}</div>
|
|
</div>
|
|
|
|
{% if bed.current_admission %}
|
|
<div class="bed-patient">{{ bed.current_admission.patient.get_full_name }}</div>
|
|
<div class="bed-time">{{ bed.occupied_since|timesince }} ago</div>
|
|
{% endif %}
|
|
|
|
<div class="bed-status">
|
|
{{ bed.get_status_display }}
|
|
</div>
|
|
|
|
<div class="bed-actions">
|
|
<div class="action-buttons">
|
|
<a href="{% url 'inpatients:bed_detail' bed.id %}" class="action-btn view" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
<a href="{% url 'inpatients:bed_update' bed.id %}" class="action-btn edit" title="Edit Bed">
|
|
<i class="fas fa-edit"></i>
|
|
</a>
|
|
{% if bed.status == 'AVAILABLE' %}
|
|
<a href="{% url 'inpatients:admission_create' %}" class="action-btn assign" title="Assign Patient">
|
|
<i class="fas fa-user-plus"></i>
|
|
</a>
|
|
{% elif bed.current_admission %}
|
|
<a href="{% url 'inpatients:discharge_patient' bed.current_admission.id %}" class="action-btn discharge" title="Discharge">
|
|
<i class="fas fa-sign-out-alt"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="sidebar">
|
|
<!-- Occupancy Overview -->
|
|
<div class="sidebar-panel">
|
|
<div class="panel-header">
|
|
<h6 class="panel-title">
|
|
<div class="panel-icon primary">
|
|
<i class="fas fa-chart-pie"></i>
|
|
</div>
|
|
Occupancy Overview
|
|
</h6>
|
|
</div>
|
|
|
|
<div class="occupancy-display">
|
|
<div class="occupancy-percentage">{{ occupancy_rate|floatformat:0 }}%</div>
|
|
<div class="occupancy-label">Bed Utilization Rate</div>
|
|
<div class="occupancy-bar">
|
|
<div class="occupancy-fill" style="width: {{ occupancy_rate }}%"></div>
|
|
</div>
|
|
<div class="occupancy-stats">
|
|
<div>Occupied: {{ occupied_beds }}</div>
|
|
<div>Available: {{ available_beds }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Ward Breakdown -->
|
|
<div class="sidebar-panel">
|
|
<div class="panel-header">
|
|
<h6 class="panel-title">
|
|
<div class="panel-icon success">
|
|
<i class="fas fa-building"></i>
|
|
</div>
|
|
Ward Breakdown
|
|
</h6>
|
|
</div>
|
|
|
|
<div class="ward-breakdown">
|
|
{% for ward in wards %}
|
|
<div class="ward-item">
|
|
<div class="ward-name">{{ ward.name }}</div>
|
|
<div class="ward-occupancy">
|
|
<div class="ward-bar">
|
|
<div class="ward-fill" style="width: {{ ward.occupancy_rate }}%; background: {% if ward.occupancy_rate >= 90 %}#dc3545{% elif ward.occupancy_rate >= 75 %}#fd7e14{% elif ward.occupancy_rate >= 50 %}#0dcaf0{% else %}#198754{% endif %}"></div>
|
|
</div>
|
|
<div class="ward-percentage">{{ ward.occupancy_rate|floatformat:0 }}%</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="sidebar-panel">
|
|
<div class="panel-header">
|
|
<h6 class="panel-title">
|
|
<span class="panel-icon warning">
|
|
<i class="fas fa-bolt"></i>
|
|
</span>
|
|
Quick Actions
|
|
</h6>
|
|
</div>
|
|
|
|
<div class="quick-actions">
|
|
<a href="#" class="quick-btn" onclick="bulkUpdate()">
|
|
<i class="fas fa-edit"></i>
|
|
<span>Bulk Update</span>
|
|
</a>
|
|
<a href="#" class="quick-btn" onclick="exportData()">
|
|
<i class="fas fa-download"></i>
|
|
<span>Export Data</span>
|
|
</a>
|
|
<a href="#" class="quick-btn" onclick="scheduleMaintenance()">
|
|
<i class="fas fa-tools"></i>
|
|
<span>Schedule Maintenance</span>
|
|
</a>
|
|
<a href="#" class="quick-btn" onclick="viewAlerts()">
|
|
<i class="fas fa-bell"></i>
|
|
<span>View Alerts</span>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading Overlay -->
|
|
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
|
|
<div class="loading-spinner"></div>
|
|
</div>
|
|
|
|
<!-- Bed Details Modal -->
|
|
<div class="modal fade" id="bedDetailsModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Bed Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="bedDetailsContent">
|
|
<!-- Content loaded dynamically -->
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
<button type="button" class="btn btn-primary" onclick="editCurrentBed()">Edit Bed</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
$(document).ready(function() {
|
|
setupEventHandlers();
|
|
setupFilters();
|
|
updateBedStatuses();
|
|
|
|
// Auto-refresh every 30 seconds
|
|
setInterval(updateBedStatuses, 30000);
|
|
});
|
|
|
|
function setupEventHandlers() {
|
|
// Filter handlers
|
|
$('#wardFilter, #statusFilter, #bedTypeFilter').on('change', function() {
|
|
filterBeds();
|
|
});
|
|
|
|
$('#bedSearch').on('input', function() {
|
|
filterBeds();
|
|
});
|
|
}
|
|
|
|
function setupFilters() {
|
|
$('.bed-card').each(function() {
|
|
$(this).data('original-display', $(this).css('display'));
|
|
});
|
|
}
|
|
|
|
function selectBed(bedId) {
|
|
// Remove previous selection
|
|
$('.bed-card').removeClass('selected');
|
|
|
|
// Add selection
|
|
$(`.bed-card[data-bed-id="${bedId}"]`).addClass('selected');
|
|
|
|
// Store selected bed
|
|
window.selectedBedId = bedId;
|
|
}
|
|
|
|
function editBed(bedId) {
|
|
window.location.href = `{% url 'inpatients:bed_update' 0 %}`.replace('0', bedId);
|
|
}
|
|
|
|
function editCurrentBed() {
|
|
if (window.selectedBedId) {
|
|
editBed(window.selectedBedId);
|
|
}
|
|
}
|
|
|
|
function viewBedDetails(bedId) {
|
|
$('#loadingOverlay').show();
|
|
$.get('', {bed_id: bedId}, function(data) {
|
|
$('#loadingOverlay').hide();
|
|
if (data.success) {
|
|
$('#bedDetailsContent').html(data.html);
|
|
$('#bedDetailsModal').modal('show');
|
|
window.selectedBedId = bedId;
|
|
} else {
|
|
showNotification('Failed to load bed details', 'error');
|
|
}
|
|
}).fail(function() {
|
|
$('#loadingOverlay').hide();
|
|
showNotification('Network error occurred', 'error');
|
|
});
|
|
}
|
|
|
|
function filterBeds() {
|
|
const wardFilter = $('#wardFilter').val();
|
|
const statusFilter = $('#statusFilter').val();
|
|
const typeFilter = $('#bedTypeFilter').val();
|
|
const searchTerm = $('#bedSearch').val().toLowerCase();
|
|
|
|
$('.bed-card').each(function() {
|
|
const card = $(this);
|
|
const bedId = card.data('bed-id');
|
|
const status = card.data('status');
|
|
const type = card.data('type');
|
|
const text = card.text().toLowerCase();
|
|
const wardId = card.closest('.ward-section').data('ward');
|
|
|
|
const matchesWard = !wardFilter || wardId == wardFilter;
|
|
const matchesStatus = !statusFilter || status === statusFilter.toLowerCase();
|
|
const matchesType = !typeFilter || type === typeFilter;
|
|
const matchesSearch = !searchTerm || text.includes(searchTerm);
|
|
|
|
if (matchesWard && matchesStatus && matchesType && matchesSearch) {
|
|
card.show();
|
|
} else {
|
|
card.hide();
|
|
}
|
|
});
|
|
|
|
// Hide empty ward sections
|
|
$('.ward-section').each(function() {
|
|
const section = $(this);
|
|
const visibleBeds = section.find('.bed-card:visible').length;
|
|
if (visibleBeds === 0) {
|
|
section.hide();
|
|
} else {
|
|
section.show();
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateBedStatuses() {
|
|
$.get('', {action: 'update_status'}, function(data) {
|
|
if (data.success && data.beds) {
|
|
data.beds.forEach(function(bed) {
|
|
const bedCard = $(`.bed-card[data-bed-id="${bed.id}"]`);
|
|
if (bedCard.length) {
|
|
// Update status classes
|
|
bedCard.removeClass('available occupied maintenance cleaning blocked out_of_order')
|
|
.addClass(bed.status.toLowerCase());
|
|
|
|
// Update status badge
|
|
bedCard.find('.bed-status').text(bed.status_display);
|
|
|
|
// Update patient info if changed
|
|
if (bed.patient_name) {
|
|
bedCard.find('.bed-patient').text(bed.patient_name);
|
|
bedCard.find('.bed-time').text(bed.occupied_duration);
|
|
} else {
|
|
bedCard.find('.bed-patient').text('');
|
|
bedCard.find('.bed-time').text('');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Update statistics
|
|
if (data.stats) {
|
|
$('.stat-number').eq(0).text(data.stats.total_beds);
|
|
$('.stat-number').eq(1).text(data.stats.available_beds);
|
|
$('.stat-number').eq(2).text(data.stats.occupied_beds);
|
|
$('.stat-number').eq(3).text(data.stats.maintenance_beds);
|
|
|
|
// Update occupancy rate
|
|
const occupancyRate = data.stats.occupancy_rate;
|
|
$('.occupancy-percentage').text(occupancyRate + '%');
|
|
$('.occupancy-fill').css('width', occupancyRate + '%');
|
|
$('.occupancy-stats').html(`
|
|
<div>Occupied: ${data.stats.occupied_beds}</div>
|
|
<div>Available: ${data.stats.available_beds}</div>
|
|
`);
|
|
}
|
|
}
|
|
}).fail(function() {
|
|
console.log('Failed to update bed statuses');
|
|
});
|
|
}
|
|
|
|
function showNotification(message, type = 'info') {
|
|
// Simple notification - you can replace with a proper notification library
|
|
const colors = {
|
|
success: '#28a745',
|
|
error: '#dc3545',
|
|
warning: '#ffc107',
|
|
info: '#17a2b8'
|
|
};
|
|
|
|
const notification = $(`
|
|
<div style="
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: ${colors[type]};
|
|
color: white;
|
|
padding: 15px 20px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
z-index: 10000;
|
|
font-weight: 500;
|
|
">
|
|
${message}
|
|
</div>
|
|
`);
|
|
|
|
$('body').append(notification);
|
|
|
|
setTimeout(() => {
|
|
notification.fadeOut(() => notification.remove());
|
|
}, 3000);
|
|
}
|
|
|
|
// Placeholder functions for buttons
|
|
function viewFloorPlan() {
|
|
showNotification('Floor plan view coming soon!', 'info');
|
|
}
|
|
|
|
function generateReport() {
|
|
showNotification('Report generation started...', 'success');
|
|
// Simulate report generation
|
|
setTimeout(() => {
|
|
showNotification('Report generated successfully!', 'success');
|
|
}, 2000);
|
|
}
|
|
|
|
function bulkUpdate() {
|
|
showNotification('Bulk update feature coming soon!', 'info');
|
|
}
|
|
|
|
function exportData() {
|
|
showNotification('Data export started...', 'success');
|
|
// Simulate export
|
|
setTimeout(() => {
|
|
showNotification('Data exported successfully!', 'success');
|
|
}, 1500);
|
|
}
|
|
|
|
function scheduleMaintenance() {
|
|
showNotification('Maintenance scheduling opened', 'info');
|
|
}
|
|
|
|
function viewAlerts() {
|
|
showNotification('No active alerts at this time', 'success');
|
|
}
|
|
</script>
|
|
{% endblock %}
|