update
This commit is contained in:
parent
4d06ca4b5e
commit
94e180f776
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
@ -67,10 +67,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Problems Table -->
|
<!-- Problems Table -->
|
||||||
<div class="card">
|
<div class="card border border-primary-subtle">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-sm table-hover mb-0 align-middle">
|
<table class="table table-sm table-hover mb-0 align-middle">
|
||||||
<thead class="table-primary">
|
<thead class="table-primary ">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Patient</th>
|
<th>Patient</th>
|
||||||
<th>Problem</th>
|
<th>Problem</th>
|
||||||
|
|||||||
BIN
facility_management/__pycache__/forms.cpython-312.pyc
Normal file
BIN
facility_management/__pycache__/forms.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
facility_management/__pycache__/urls.cpython-312.pyc
Normal file
BIN
facility_management/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
facility_management/__pycache__/views.cpython-312.pyc
Normal file
BIN
facility_management/__pycache__/views.cpython-312.pyc
Normal file
Binary file not shown.
@ -69,10 +69,8 @@ class RoomForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Room
|
model = Room
|
||||||
fields = [
|
fields = [
|
||||||
'floor', 'room_number', 'name', 'room_type', 'area_sqm',
|
'floor', 'room_number', 'name', 'area_sqm',
|
||||||
'capacity', 'occupancy_status', 'is_accessible', 'current_tenant',
|
'capacity', 'occupancy_status', 'is_accessible', 'notes'
|
||||||
'lease_start_date', 'lease_end_date', 'monthly_rent',
|
|
||||||
'has_hvac', 'has_electrical', 'has_plumbing', 'has_internet', 'notes'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'floor': forms.Select(attrs={'class': 'form-select'}),
|
'floor': forms.Select(attrs={'class': 'form-select'}),
|
||||||
@ -81,26 +79,13 @@ class RoomForm(forms.ModelForm):
|
|||||||
'room_type': forms.Select(attrs={'class': 'form-select'}),
|
'room_type': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
'area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||||
'capacity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
'capacity': forms.NumberInput(attrs={'class': 'form-control', 'min': '1'}),
|
||||||
'occupancy_status': forms.Select(attrs={'class': 'form-select'}),
|
|
||||||
'is_accessible': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_accessible': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
'current_tenant': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'lease_start_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
||||||
'lease_end_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
||||||
'monthly_rent': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
|
||||||
'has_hvac': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
'has_electrical': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
'has_plumbing': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
'has_internet': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
||||||
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
'notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
}
|
}
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
lease_start = cleaned_data.get('lease_start_date')
|
|
||||||
lease_end = cleaned_data.get('lease_end_date')
|
|
||||||
|
|
||||||
if lease_start and lease_end and lease_start >= lease_end:
|
|
||||||
raise ValidationError('Lease end date must be after lease start date.')
|
|
||||||
|
|
||||||
return cleaned_data
|
return cleaned_data
|
||||||
|
|
||||||
@ -132,7 +117,7 @@ class AssetForm(forms.ModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Asset
|
model = Asset
|
||||||
fields = [
|
fields = [
|
||||||
'asset_id', 'name', 'category', 'building', 'floor', 'room',
|
'name', 'category', 'building', 'floor', 'room',
|
||||||
'location_description', 'manufacturer', 'model', 'serial_number',
|
'location_description', 'manufacturer', 'model', 'serial_number',
|
||||||
'purchase_date', 'purchase_cost', 'current_value', 'depreciation_rate',
|
'purchase_date', 'purchase_cost', 'current_value', 'depreciation_rate',
|
||||||
'warranty_start_date', 'warranty_end_date', 'service_provider',
|
'warranty_start_date', 'warranty_end_date', 'service_provider',
|
||||||
@ -140,7 +125,6 @@ class AssetForm(forms.ModelForm):
|
|||||||
'last_inspection_date', 'next_maintenance_date', 'assigned_to', 'notes'
|
'last_inspection_date', 'next_maintenance_date', 'assigned_to', 'notes'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'asset_id': forms.TextInput(attrs={'class': 'form-control'}),
|
|
||||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'category': forms.Select(attrs={'class': 'form-select'}),
|
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||||
'building': forms.Select(attrs={'class': 'form-select'}),
|
'building': forms.Select(attrs={'class': 'form-select'}),
|
||||||
@ -271,7 +255,7 @@ class MaintenanceScheduleForm(forms.ModelForm):
|
|||||||
model = MaintenanceSchedule
|
model = MaintenanceSchedule
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'description', 'maintenance_type', 'asset', 'building',
|
'name', 'description', 'maintenance_type', 'asset', 'building',
|
||||||
'room', 'frequency', 'frequency_interval', 'start_date',
|
'room', 'frequency_interval', 'start_date',
|
||||||
'end_date', 'assigned_to', 'estimated_duration_hours', 'is_active'
|
'end_date', 'assigned_to', 'estimated_duration_hours', 'is_active'
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
@ -322,8 +306,7 @@ class VendorForm(forms.ModelForm):
|
|||||||
model = Vendor
|
model = Vendor
|
||||||
fields = [
|
fields = [
|
||||||
'name', 'vendor_type', 'contact_person', 'email', 'phone',
|
'name', 'vendor_type', 'contact_person', 'email', 'phone',
|
||||||
'address', 'license_number', 'insurance_policy',
|
'address', 'crn', 'vrn', 'rating', 'is_active'
|
||||||
'insurance_expiry', 'rating', 'is_active'
|
|
||||||
]
|
]
|
||||||
widgets = {
|
widgets = {
|
||||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
@ -332,9 +315,8 @@ class VendorForm(forms.ModelForm):
|
|||||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||||
'license_number': forms.TextInput(attrs={'class': 'form-control'}),
|
'crn': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'insurance_policy': forms.TextInput(attrs={'class': 'form-control'}),
|
'vrn': forms.TextInput(attrs={'class': 'form-control'}),
|
||||||
'insurance_expiry': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
||||||
'rating': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0', 'max': '5'}),
|
'rating': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0', 'max': '5'}),
|
||||||
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
||||||
}
|
}
|
||||||
|
|||||||
0
facility_management/management/__init__.py
Normal file
0
facility_management/management/__init__.py
Normal file
Binary file not shown.
0
facility_management/management/commands/__init__.py
Normal file
0
facility_management/management/commands/__init__.py
Normal file
Binary file not shown.
Binary file not shown.
633
facility_management/management/commands/seed_facility.py
Normal file
633
facility_management/management/commands/seed_facility.py
Normal file
@ -0,0 +1,633 @@
|
|||||||
|
# facility/management/commands/seed_facility_saudi.py
|
||||||
|
import random
|
||||||
|
import uuid
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
from django.contrib.auth import get_user_model
|
||||||
|
from django.db import transaction
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
# TODO: Change this import path to your app's models module
|
||||||
|
from facility_management.models import (
|
||||||
|
Building, Floor, Room,
|
||||||
|
AssetCategory, Asset,
|
||||||
|
MaintenanceType, MaintenanceRequest, MaintenanceSchedule,
|
||||||
|
Vendor, ServiceContract,
|
||||||
|
Inspection, EnergyMeter, EnergyReading,
|
||||||
|
SpaceReservation,
|
||||||
|
)
|
||||||
|
from core.models import Tenant # Adjust if your Tenant lives elsewhere
|
||||||
|
|
||||||
|
|
||||||
|
SAUDI_CITIES = [
|
||||||
|
("Riyadh", 24.7136, 46.6753, [
|
||||||
|
"Al Olaya", "Al Malqa", "Al Nakheel", "Al Sulaymaniyah", "Hittin", "Al Yasmin", "Al Wurud"
|
||||||
|
]),
|
||||||
|
("Jeddah", 21.4858, 39.1925, [
|
||||||
|
"Al Rawdah", "Al Nahda", "Al Andalus", "Al Zahra", "Al Basateen", "Al Salama"
|
||||||
|
]),
|
||||||
|
("Dammam", 26.3927, 49.9777, [
|
||||||
|
"Al Faisaliyah", "Al Shati", "Al Badiyah", "Al Mazruiyah", "An Nakheel"
|
||||||
|
]),
|
||||||
|
("Makkah", 21.3891, 39.8579, [
|
||||||
|
"Al Awali", "Aziziyah", "Al Naseem", "Al Shara'i", "Al Kaakiya"
|
||||||
|
]),
|
||||||
|
("Madinah", 24.5247, 39.5692, [
|
||||||
|
"Quba", "Qurban", "Al Khalidiyyah", "Al Azhari", "Al Rabwa"
|
||||||
|
]),
|
||||||
|
("Khobar", 26.2794, 50.2083, [
|
||||||
|
"Al Aqrabiyah", "Al Khobar Al Shamaliyah", "Al Yarmouk", "Al Rakah"
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Lightweight Saudi-flavored suppliers
|
||||||
|
VENDOR_NAMES = [
|
||||||
|
"Saudi HVAC Co.", "Riyadh Fire Systems", "Gulf Electrical Services",
|
||||||
|
"Najd Facilities Cleaning", "Red Sea Security", "Eastern Landscaping",
|
||||||
|
"Hijaz Plumbing & Drainage", "Nusuk IT Services", "Al Haramain Maintenance"
|
||||||
|
]
|
||||||
|
|
||||||
|
SERVICE_AREAS_SNIPPETS = [
|
||||||
|
"Riyadh HQ campus, central plant and clinics",
|
||||||
|
"Jeddah airport satellite buildings and hangars",
|
||||||
|
"Dammam logistics & cold-storage facilities",
|
||||||
|
"Makkah accommodation & hospitality zones",
|
||||||
|
"Madinah clinical towers and outpatient blocks",
|
||||||
|
]
|
||||||
|
|
||||||
|
ASSET_CATEGORIES = [
|
||||||
|
("HVAC Systems", "HVAC", "Chillers, AHUs, FCUs, pumps"),
|
||||||
|
("Electrical", "ELEC", "Transformers, switchgear, UPS"),
|
||||||
|
("Medical Equipment", "MED", "Imaging, patient monitors, ventilators"),
|
||||||
|
("IT Equipment", "IT", "Servers, switches, wireless APs"),
|
||||||
|
("Fire & Safety", "FIRE", "Fire pump, alarm panels, detectors"),
|
||||||
|
("Plumbing", "PLB", "Pumps, tanks, RO/softeners"),
|
||||||
|
]
|
||||||
|
|
||||||
|
MAINT_TYPES = [
|
||||||
|
("Preventive - HVAC", "PM_HVAC", "Quarterly PM for AHUs, filters, belts"),
|
||||||
|
("Fire Alarm Test (SASO)", "FIRE_TEST", "Monthly fire alarm & pump tests"),
|
||||||
|
("Elevator Inspection", "LIFT_INSP", "Quarterly inspection per standard"),
|
||||||
|
("Water Quality Check", "WATER_QA", "Legionella sampling & flushing"),
|
||||||
|
("Electrical IR Scan", "ELEC_IR", "Annual IR thermography of panels"),
|
||||||
|
("Generator Load Test", "GEN_TEST", "Monthly DG set test under load"),
|
||||||
|
]
|
||||||
|
|
||||||
|
ASSET_MODELS = [
|
||||||
|
("Trane", "RTAC-300", "CHILLER"),
|
||||||
|
("Siemens", "SIVACON-S8", "SWITCHGEAR"),
|
||||||
|
("Philips", "IntelliVue MX700", "PATIENT_MONITOR"),
|
||||||
|
("Cisco", "Catalyst 9300", "SWITCH"),
|
||||||
|
("Honeywell", "Notifier NFS2-3030", "FIRE_ALARM"),
|
||||||
|
("Grundfos", "CR-32-10", "PUMP"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def rand_decimal(a, b, precision="0.01"):
|
||||||
|
q = Decimal(precision)
|
||||||
|
return (Decimal(a) + (Decimal(random.random()) * (Decimal(b) - Decimal(a)))).quantize(q)
|
||||||
|
|
||||||
|
def rand_phone():
|
||||||
|
# Saudi mobile format: 05x-xxxxxxx
|
||||||
|
return f"05{random.randint(0,9)}-{random.randint(1000000,9999999)}"
|
||||||
|
|
||||||
|
def rand_email(name):
|
||||||
|
base = name.lower().replace(" ", ".")
|
||||||
|
return f"{base}@example.sa"
|
||||||
|
|
||||||
|
def rand_crn():
|
||||||
|
# KSA CRN often represented with 10 digits
|
||||||
|
return f"{random.randint(10**9, 10**10 - 1)}"
|
||||||
|
|
||||||
|
def rand_vrn():
|
||||||
|
# KSA VAT/TRN is 15 digits
|
||||||
|
return f"{random.randint(10**14, 10**15 - 1)}"
|
||||||
|
|
||||||
|
def safe_choice(seq):
|
||||||
|
return random.choice(seq) if seq else None
|
||||||
|
|
||||||
|
def ensure_users_for_tenant(tenant):
|
||||||
|
"""
|
||||||
|
Try to fetch existing users for the tenant; if not found, create a small set.
|
||||||
|
Works for custom AUTH_USER_MODEL that likely has a 'tenant' FK/field.
|
||||||
|
"""
|
||||||
|
User = get_user_model()
|
||||||
|
qs = User.objects.all()
|
||||||
|
# If your user model has 'tenant', filter by it. Otherwise, just use any user.
|
||||||
|
if hasattr(User, "tenant"):
|
||||||
|
qs = qs.filter(tenant=tenant)
|
||||||
|
|
||||||
|
users = list(qs[:10])
|
||||||
|
if users:
|
||||||
|
return users
|
||||||
|
|
||||||
|
# Create a small staff set (adjust fields if your User requires more)
|
||||||
|
seed_names = [
|
||||||
|
("mohammed.fm", "Mohammed", "Al-Qahtani"),
|
||||||
|
("ibrahim.bme", "Ibrahim", "Al-Otaibi"),
|
||||||
|
("abdullah.main", "Abdullah", "Al-Zahrani"),
|
||||||
|
("nora.admin", "Nora", "Al-Anazi"),
|
||||||
|
("aisha.ops", "Aisha", "Al-Qurashi"),
|
||||||
|
]
|
||||||
|
created = []
|
||||||
|
for uname, first, last in seed_names:
|
||||||
|
kwargs = dict(
|
||||||
|
username=uname,
|
||||||
|
first_name=first,
|
||||||
|
last_name=last,
|
||||||
|
email=f"{uname}@tenhal.sa",
|
||||||
|
)
|
||||||
|
if hasattr(User, "tenant"):
|
||||||
|
kwargs["tenant"] = tenant
|
||||||
|
try:
|
||||||
|
user = User.objects.create_user(**kwargs, password="Pass1234!")
|
||||||
|
except TypeError:
|
||||||
|
# Fallback if create_user signature differs
|
||||||
|
user = User.objects.create(**kwargs)
|
||||||
|
user.set_password("Pass1234!")
|
||||||
|
user.save()
|
||||||
|
created.append(user)
|
||||||
|
return created
|
||||||
|
|
||||||
|
def unique_building_code(base, tenant_id):
|
||||||
|
# Make globally-unique code (your model has unique=True)
|
||||||
|
suffix = 1
|
||||||
|
while True:
|
||||||
|
code = f"T{tenant_id}-{base}-{suffix:02d}"
|
||||||
|
if not Building.objects.filter(code=code).exists():
|
||||||
|
return code
|
||||||
|
suffix += 1
|
||||||
|
|
||||||
|
def make_building(tenant, city_tuple, facility_manager):
|
||||||
|
city, lat, lng, districts = city_tuple
|
||||||
|
district = safe_choice(districts)
|
||||||
|
code_base = city.upper()[:3]
|
||||||
|
code = unique_building_code(code_base, tenant.id)
|
||||||
|
b = Building.objects.create(
|
||||||
|
tenant=tenant,
|
||||||
|
name=f"{city} {district or 'Campus'}",
|
||||||
|
code=code,
|
||||||
|
building_type=safe_choice([ct[0] for ct in Building.BuildingType.choices]),
|
||||||
|
address=f"{district or 'District'}, {city}, Saudi Arabia",
|
||||||
|
latitude=Decimal(f"{lat + random.uniform(-0.05, 0.05):.6f}"),
|
||||||
|
longitude=Decimal(f"{lng + random.uniform(-0.05, 0.05):.6f}"),
|
||||||
|
floor_count=random.randint(2, 10),
|
||||||
|
total_area_sqm=rand_decimal(5000, 80000),
|
||||||
|
construction_year=random.choice(range(1990, date.today().year + 1)),
|
||||||
|
architect="Dar Al Riyadh",
|
||||||
|
contractor="Saudi Binladin Group",
|
||||||
|
facility_manager=facility_manager,
|
||||||
|
)
|
||||||
|
return b
|
||||||
|
|
||||||
|
def make_floors_and_rooms(building, floors=5, rooms_per_floor=20):
|
||||||
|
floor_objs = []
|
||||||
|
for n in range(floors):
|
||||||
|
f = Floor.objects.create(
|
||||||
|
building=building,
|
||||||
|
floor_number=n,
|
||||||
|
name=f"Level {n}",
|
||||||
|
area_sqm=rand_decimal(1000, 20000),
|
||||||
|
ceiling_height_m=rand_decimal(2.7, 4.2, "0.10"),
|
||||||
|
is_public_access=(n == 0),
|
||||||
|
)
|
||||||
|
floor_objs.append(f)
|
||||||
|
room_objs = []
|
||||||
|
for r in range(rooms_per_floor):
|
||||||
|
rn = f"{n:02d}{r+1:03d}"
|
||||||
|
room = Room.objects.create(
|
||||||
|
floor=f,
|
||||||
|
room_number=rn,
|
||||||
|
name=random.choice(["Clinic", "Office", "Storage", "Server", "Lab", "Ward", "Meeting"]),
|
||||||
|
area_sqm=rand_decimal(12, 120),
|
||||||
|
capacity=random.choice([None, 2, 4, 6, 10, 20]),
|
||||||
|
occupancy_status=safe_choice([s[0] for s in Room.OccupancyStatus.choices]),
|
||||||
|
is_accessible=True,
|
||||||
|
notes=""
|
||||||
|
)
|
||||||
|
room_objs.append(room)
|
||||||
|
return floor_objs
|
||||||
|
|
||||||
|
def seed_asset_categories():
|
||||||
|
results = []
|
||||||
|
for name, code, desc in ASSET_CATEGORIES:
|
||||||
|
obj, _ = AssetCategory.objects.get_or_create(code=code, defaults=dict(name=name, description=desc))
|
||||||
|
if obj.name != name or obj.description != desc:
|
||||||
|
obj.name = name
|
||||||
|
obj.description = desc
|
||||||
|
obj.save()
|
||||||
|
results.append(obj)
|
||||||
|
# set some parents (simple tree)
|
||||||
|
hvac = next((c for c in results if c.code == "HVAC"), None)
|
||||||
|
fire = next((c for c in results if c.code == "FIRE"), None)
|
||||||
|
if fire and hvac and fire.parent_category_id is None:
|
||||||
|
fire.parent_category = hvac
|
||||||
|
fire.save()
|
||||||
|
return results
|
||||||
|
|
||||||
|
def seed_maintenance_types():
|
||||||
|
results = []
|
||||||
|
for name, code, desc in MAINT_TYPES:
|
||||||
|
obj, _ = MaintenanceType.objects.get_or_create(code=code, defaults=dict(name=name, description=desc))
|
||||||
|
if obj.name != name or obj.description != desc:
|
||||||
|
obj.name = name
|
||||||
|
obj.description = desc
|
||||||
|
obj.save()
|
||||||
|
results.append(obj)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_assets(buildings, categories, users, count=80):
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
building = safe_choice(buildings)
|
||||||
|
floor = safe_choice(list(Floor.objects.filter(building=building)))
|
||||||
|
room = safe_choice(list(Room.objects.filter(floor=floor))) if floor else None
|
||||||
|
manufacturer, model, _family = safe_choice(ASSET_MODELS)
|
||||||
|
|
||||||
|
purchase_years_ago = random.randint(0, 12)
|
||||||
|
purchase_date = timezone.now().date() - timedelta(days=365 * purchase_years_ago + random.randint(0, 364))
|
||||||
|
warranty_years = random.choice([1, 2, 3, 5])
|
||||||
|
warranty_start = purchase_date
|
||||||
|
warranty_end = warranty_start + timedelta(days=365 * warranty_years)
|
||||||
|
|
||||||
|
a = Asset.objects.create(
|
||||||
|
name=f"{manufacturer} {model}",
|
||||||
|
category=safe_choice(categories),
|
||||||
|
building=building,
|
||||||
|
floor=floor,
|
||||||
|
room=room,
|
||||||
|
location_description=f"Near {room.name if room else 'corridor'}",
|
||||||
|
manufacturer=manufacturer,
|
||||||
|
model=model,
|
||||||
|
serial_number=f"{manufacturer[:3].upper()}-{uuid.uuid4().hex[:10].upper()}",
|
||||||
|
purchase_date=purchase_date,
|
||||||
|
purchase_cost=rand_decimal(15000, 1500000, "1.00"),
|
||||||
|
current_value=rand_decimal(5000, 1200000, "1.00"),
|
||||||
|
depreciation_rate=Decimal("10.00"),
|
||||||
|
warranty_start_date=warranty_start,
|
||||||
|
warranty_end_date=warranty_end,
|
||||||
|
service_provider=safe_choice([v for v in VENDOR_NAMES]),
|
||||||
|
service_contract_number=f"SC-{timezone.now().year}-{random.randint(1000,9999)}",
|
||||||
|
status=safe_choice([s[0] for s in Asset.AssetStatus.choices]),
|
||||||
|
condition=safe_choice([c[0] for c in Asset.AssetCondition.choices]),
|
||||||
|
last_inspection_date=timezone.now().date() - timedelta(days=random.randint(5, 180)),
|
||||||
|
next_maintenance_date=timezone.now().date() + timedelta(days=random.randint(10, 120)),
|
||||||
|
assigned_to=safe_choice(users),
|
||||||
|
notes=""
|
||||||
|
)
|
||||||
|
results.append(a)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_vendors(tenant, count=8):
|
||||||
|
results = []
|
||||||
|
for i in range(count):
|
||||||
|
vname = VENDOR_NAMES[i % len(VENDOR_NAMES)]
|
||||||
|
contact = f"{vname.split()[0]} Rep"
|
||||||
|
obj, _ = Vendor.objects.get_or_create(
|
||||||
|
tenant=tenant, name=vname,
|
||||||
|
defaults=dict(
|
||||||
|
vendor_type=safe_choice([t[0] for t in Vendor.VendorType.choices]),
|
||||||
|
contact_person=contact,
|
||||||
|
email=rand_email(vname.replace(" ", "")),
|
||||||
|
phone=rand_phone(),
|
||||||
|
address=f"{safe_choice(SAUDI_CITIES)[0]} – {safe_choice(['Business Park','Industrial Area','Tech Valley'])}",
|
||||||
|
crn=rand_crn(),
|
||||||
|
vrn=rand_vrn(),
|
||||||
|
rating=rand_decimal("3.20", "4.90", "0.10"),
|
||||||
|
total_contracts=random.randint(1, 30),
|
||||||
|
is_active=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
results.append(obj)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_contracts(vendors, buildings, users, count=6):
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
ven = safe_choice(vendors)
|
||||||
|
start = timezone.now().date() - timedelta(days=random.randint(0, 400))
|
||||||
|
end = start + timedelta(days=random.randint(180, 1095))
|
||||||
|
cn = f"CTR-{ven.id}-{random.randint(1000,9999)}"
|
||||||
|
sc = ServiceContract.objects.create(
|
||||||
|
contract_number=cn,
|
||||||
|
vendor=ven,
|
||||||
|
title=f"{ven.name} – Comprehensive Service",
|
||||||
|
description="Preventive & corrective maintenance aligned with Vision 2030 reliability goals.",
|
||||||
|
start_date=start,
|
||||||
|
end_date=end,
|
||||||
|
contract_value=rand_decimal(100000, 5000000, "1.00"),
|
||||||
|
payment_terms="Net 30 days",
|
||||||
|
service_areas=safe_choice(SERVICE_AREAS_SNIPPETS),
|
||||||
|
status=safe_choice([c[0] for c in ServiceContract.ContractStatus.choices]),
|
||||||
|
auto_renewal=random.choice([True, False]),
|
||||||
|
renewal_notice_days=random.choice([30, 45, 60]),
|
||||||
|
contract_manager=safe_choice(users),
|
||||||
|
notes=""
|
||||||
|
)
|
||||||
|
# Link to 1–3 buildings
|
||||||
|
sc.buildings.add(*random.sample(buildings, k=min(len(buildings), random.randint(1, 3))))
|
||||||
|
results.append(sc)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_maintenance_requests(assets, mtypes, users, per_asset=1):
|
||||||
|
results = []
|
||||||
|
for asset in assets:
|
||||||
|
for _ in range(per_asset):
|
||||||
|
req_by = safe_choice(users)
|
||||||
|
assigned = safe_choice(users)
|
||||||
|
mr = MaintenanceRequest.objects.create(
|
||||||
|
title=f"{asset.name} – {safe_choice(['Noise','Leak','Alarm','Performance drop','Vibration'])}",
|
||||||
|
description="Auto-generated test request reflecting typical O&M issues.",
|
||||||
|
maintenance_type=safe_choice(mtypes),
|
||||||
|
building=asset.building,
|
||||||
|
floor=asset.floor,
|
||||||
|
room=asset.room,
|
||||||
|
asset=asset,
|
||||||
|
priority=safe_choice([p[0] for p in MaintenanceRequest.Priority.choices]),
|
||||||
|
status=safe_choice([s[0] for s in MaintenanceRequest.MaintenanceStatus.choices]),
|
||||||
|
requested_by=req_by,
|
||||||
|
assigned_to=assigned if random.random() > 0.3 else None,
|
||||||
|
estimated_hours=rand_decimal("1.00", "16.00", "0.25"),
|
||||||
|
estimated_cost=rand_decimal(250, 10000, "1.00"),
|
||||||
|
actual_cost=None,
|
||||||
|
notes="",
|
||||||
|
completion_notes=""
|
||||||
|
)
|
||||||
|
results.append(mr)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_schedules(assets, mtypes, users, count=60):
|
||||||
|
results = []
|
||||||
|
combos = set()
|
||||||
|
for _ in range(count):
|
||||||
|
asset = safe_choice(assets)
|
||||||
|
building = asset.building
|
||||||
|
room = asset.room
|
||||||
|
key = (asset.id, building.id, room.id if room else None)
|
||||||
|
if key in combos:
|
||||||
|
continue
|
||||||
|
combos.add(key)
|
||||||
|
|
||||||
|
freq = safe_choice([f[0] for f in MaintenanceSchedule.FrequencyInterval.choices])
|
||||||
|
start = timezone.now().date() - timedelta(days=random.randint(0, 365))
|
||||||
|
est = rand_decimal("1.00", "8.00", "0.25")
|
||||||
|
sched = MaintenanceSchedule.objects.create(
|
||||||
|
name=f"{asset.name} {safe_choice(['PM','Inspection','Service'])}",
|
||||||
|
description="Auto-generated PM program",
|
||||||
|
maintenance_type=safe_choice(mtypes),
|
||||||
|
asset=asset,
|
||||||
|
building=building,
|
||||||
|
room=room,
|
||||||
|
frequency_interval=freq,
|
||||||
|
start_date=start,
|
||||||
|
end_date=None,
|
||||||
|
assigned_to=safe_choice(users),
|
||||||
|
estimated_duration_hours=est,
|
||||||
|
is_active=True,
|
||||||
|
last_generated_date=None,
|
||||||
|
next_due_date=start + timedelta(days=freq),
|
||||||
|
)
|
||||||
|
results.append(sched)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_inspections(buildings, users, count=20):
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
b = safe_choice(buildings)
|
||||||
|
scheduled = timezone.now() + timedelta(days=random.randint(-60, 60))
|
||||||
|
insp = Inspection.objects.create(
|
||||||
|
inspection_id="", # let .save generate
|
||||||
|
inspection_type=safe_choice([t[0] for t in Inspection.InspectionType.choices]),
|
||||||
|
title=f"{b.code} – {safe_choice(['Quarterly','Annual','Random'])} Inspection",
|
||||||
|
description="Safety & compliance per local authority guidance.",
|
||||||
|
building=b,
|
||||||
|
scheduled_date=scheduled,
|
||||||
|
estimated_duration_hours=rand_decimal("1.00", "6.00", "0.25"),
|
||||||
|
inspector=safe_choice(users),
|
||||||
|
inspector_external=random.choice(["", "TUV Middle East", "Saudi Safety Org", "SASO Certifier"]),
|
||||||
|
inspector_organization=random.choice(["", "SASO", "Civil Defense", "Third-Party"]),
|
||||||
|
status=safe_choice([s[0] for s in Inspection.Status.choices]),
|
||||||
|
started_date=None,
|
||||||
|
completed_date=None,
|
||||||
|
overall_rating=random.choice(["", "Pass", "Fail", "Conditional"]),
|
||||||
|
findings="",
|
||||||
|
recommendations="",
|
||||||
|
requires_followup=random.choice([True, False]),
|
||||||
|
followup_date=None,
|
||||||
|
)
|
||||||
|
# relate a few floors/rooms/assets
|
||||||
|
floors = list(Floor.objects.filter(building=b))
|
||||||
|
if floors:
|
||||||
|
insp.floors.add(*random.sample(floors, k=min(len(floors), random.randint(1, 3))))
|
||||||
|
rooms = list(Room.objects.filter(floor__in=floors))
|
||||||
|
if rooms:
|
||||||
|
insp.rooms.add(*random.sample(rooms, k=min(len(rooms), random.randint(1, 5))))
|
||||||
|
assets = list(Asset.objects.filter(building=b))
|
||||||
|
if assets:
|
||||||
|
insp.assets.add(*random.sample(assets, k=min(len(assets), random.randint(1, 5))))
|
||||||
|
results.append(insp)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_energy_meters(buildings):
|
||||||
|
"""
|
||||||
|
Create 2–4 meters per building (electricity + water + optional others)
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
meter_types = [EnergyMeter.MeterType.ELECTRICITY, EnergyMeter.MeterType.WATER,
|
||||||
|
EnergyMeter.MeterType.GAS, EnergyMeter.MeterType.CHILLED_WATER]
|
||||||
|
for b in buildings:
|
||||||
|
for mt in random.sample(meter_types, k=random.randint(2, 4)):
|
||||||
|
meter_id = f"MTR-{b.code}-{mt[:2]}-{random.randint(1000,9999)}"
|
||||||
|
em = EnergyMeter.objects.create(
|
||||||
|
meter_id=meter_id,
|
||||||
|
meter_type=mt,
|
||||||
|
building=b,
|
||||||
|
location_description=f"Utility Room – {safe_choice(['A','B','C'])}",
|
||||||
|
manufacturer=safe_choice(["Schneider", "Siemens", "ABB", "Yokogawa"]),
|
||||||
|
model=f"{safe_choice(['Q1','E45','HX','Prime'])}-{random.randint(100,999)}",
|
||||||
|
serial_number=uuid.uuid4().hex[:12].upper(),
|
||||||
|
installation_date=timezone.now().date() - timedelta(days=random.randint(100, 2000)),
|
||||||
|
current_reading=Decimal("0.00"),
|
||||||
|
last_reading_date=None,
|
||||||
|
is_active=True,
|
||||||
|
calibration_date=timezone.now().date() - timedelta(days=random.randint(100, 800)),
|
||||||
|
next_calibration_date=timezone.now().date() + timedelta(days=random.randint(100, 800)),
|
||||||
|
)
|
||||||
|
results.append(em)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_energy_readings(meters, days=60, users=None):
|
||||||
|
"""
|
||||||
|
Generate monotonic readings over past N days. Electricity in kWh, water in m3, etc.
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
users = users or []
|
||||||
|
reader = safe_choice(users)
|
||||||
|
for m in meters:
|
||||||
|
base = rand_decimal(1000, 50000, "1.00")
|
||||||
|
daily_incr = rand_decimal(50, 1500, "1.00")
|
||||||
|
ts = timezone.now() - timedelta(days=days)
|
||||||
|
for d in range(days):
|
||||||
|
ts += timedelta(days=1)
|
||||||
|
base += daily_incr + rand_decimal(-0.20, 0.20, "0.01") * daily_incr # mild variation
|
||||||
|
er = EnergyReading.objects.create(
|
||||||
|
meter=m,
|
||||||
|
reading_date=ts,
|
||||||
|
reading_value=base.quantize(Decimal("1.00")),
|
||||||
|
consumption=None, # model .save() derives consumption
|
||||||
|
cost=None,
|
||||||
|
read_by=reader,
|
||||||
|
is_estimated=random.random() < 0.1,
|
||||||
|
notes=""
|
||||||
|
)
|
||||||
|
results.append(er)
|
||||||
|
return results
|
||||||
|
|
||||||
|
def make_reservations(rooms, users, count=50):
|
||||||
|
results = []
|
||||||
|
for _ in range(count):
|
||||||
|
room = safe_choice(rooms)
|
||||||
|
reserved_by = safe_choice(users)
|
||||||
|
start = timezone.now() + timedelta(days=random.randint(-15, 30), hours=random.randint(8, 17))
|
||||||
|
duration_h = random.choice([1, 2, 3, 4, 6, 8])
|
||||||
|
end = start + timedelta(hours=duration_h)
|
||||||
|
hourly_rate = random.choice([None, Decimal("150.00"), Decimal("300.00")])
|
||||||
|
|
||||||
|
sr = SpaceReservation.objects.create(
|
||||||
|
reservation_id="", # let .save generate
|
||||||
|
room=room,
|
||||||
|
title=f"{safe_choice(['Training', 'Meeting', 'Workshop', 'Orientation'])} – {room.name}",
|
||||||
|
description=f"Auto-generated booking for {room.floor.building.code}",
|
||||||
|
start_datetime=start,
|
||||||
|
end_datetime=end,
|
||||||
|
reserved_by=reserved_by,
|
||||||
|
contact_person=f"{reserved_by.first_name or 'Contact'} {reserved_by.last_name or ''}".strip(),
|
||||||
|
contact_email=reserved_by.email or rand_email("contact"),
|
||||||
|
contact_phone=rand_phone(),
|
||||||
|
expected_attendees=random.randint(3, 30),
|
||||||
|
setup_requirements=random.choice(["Theater", "U-Shape", "Classroom", "Boardroom"]),
|
||||||
|
catering_required=random.random() < 0.3,
|
||||||
|
av_equipment_required=random.random() < 0.6,
|
||||||
|
status=safe_choice([s[0] for s in SpaceReservation.ReservationStatus.choices]),
|
||||||
|
approved_by=safe_choice(users) if random.random() < 0.7 else None,
|
||||||
|
approved_at=timezone.now() if random.random() < 0.7 else None,
|
||||||
|
hourly_rate=hourly_rate,
|
||||||
|
total_cost=None, # model .save() computes if hourly_rate is provided
|
||||||
|
notes=""
|
||||||
|
)
|
||||||
|
results.append(sr)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = (
|
||||||
|
"Seed Saudi-influenced, multi-tenant facility data (buildings, floors/rooms, assets, "
|
||||||
|
"maintenance, vendors/contracts, inspections, meters/readings, reservations).\n\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" python manage.py seed_facility_saudi --tenant 1\n"
|
||||||
|
" python manage.py seed_facility_saudi --tenant 1 --buildings 3 --floors 6 --rooms-per-floor 25 --assets 120 --seed 42 --purge\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument("--tenant", required=True, help="Tenant ID or slug.")
|
||||||
|
parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility.")
|
||||||
|
parser.add_argument("--buildings", type=int, default=4)
|
||||||
|
parser.add_argument("--floors", type=int, default=5)
|
||||||
|
parser.add_argument("--rooms-per-floor", type=int, default=20)
|
||||||
|
parser.add_argument("--assets", type=int, default=80)
|
||||||
|
parser.add_argument("--purge", action="store_true", help="Delete existing tenant data first.")
|
||||||
|
|
||||||
|
def handle(self, *args, **opts):
|
||||||
|
if opts["seed"] is not None:
|
||||||
|
random.seed(opts["seed"])
|
||||||
|
|
||||||
|
tenant = self._get_tenant(opts["tenant"])
|
||||||
|
if not tenant:
|
||||||
|
raise CommandError("Tenant not found.")
|
||||||
|
|
||||||
|
User = get_user_model()
|
||||||
|
users = ensure_users_for_tenant(tenant)
|
||||||
|
|
||||||
|
building_count = max(1, opts["buildings"])
|
||||||
|
floors = max(1, opts["floors"])
|
||||||
|
rpf = max(5, opts["rooms_per_floor"])
|
||||||
|
asset_target = max(10, opts["assets"])
|
||||||
|
|
||||||
|
with transaction.atomic():
|
||||||
|
if opts["purge"]:
|
||||||
|
self._purge_tenant_data(tenant)
|
||||||
|
self.stdout.write(self.style.WARNING(f"Purged existing data for tenant {tenant}."))
|
||||||
|
|
||||||
|
# Seed lookup tables
|
||||||
|
categories = seed_asset_categories()
|
||||||
|
mtypes = seed_maintenance_types()
|
||||||
|
|
||||||
|
# Buildings, floors, rooms
|
||||||
|
buildings = []
|
||||||
|
for i in range(building_count):
|
||||||
|
city_tuple = safe_choice(SAUDI_CITIES)
|
||||||
|
fm = safe_choice(users)
|
||||||
|
b = make_building(tenant, city_tuple, fm)
|
||||||
|
buildings.append(b)
|
||||||
|
make_floors_and_rooms(b, floors=floors, rooms_per_floor=rpf)
|
||||||
|
|
||||||
|
# Vendors & contracts
|
||||||
|
vendors = make_vendors(tenant, count=min(10, len(VENDOR_NAMES)))
|
||||||
|
contracts = make_contracts(vendors, buildings, users, count=6)
|
||||||
|
|
||||||
|
# Assets
|
||||||
|
assets = make_assets(buildings, categories, users, count=asset_target)
|
||||||
|
|
||||||
|
# Maintenance
|
||||||
|
mreqs = make_maintenance_requests(assets, mtypes, users, per_asset=1)
|
||||||
|
msched = make_schedules(assets, mtypes, users, count=min(60, len(assets)))
|
||||||
|
|
||||||
|
# Inspections
|
||||||
|
insps = make_inspections(buildings, users, count=20)
|
||||||
|
|
||||||
|
# Energy meters and readings
|
||||||
|
meters = make_energy_meters(buildings)
|
||||||
|
mreads = make_energy_readings(meters, days=60, users=users)
|
||||||
|
|
||||||
|
# Reservations
|
||||||
|
rooms = list(Room.objects.filter(floor__building__in=buildings))
|
||||||
|
resvs = make_reservations(rooms, users, count=50)
|
||||||
|
|
||||||
|
self.stdout.write(self.style.SUCCESS(
|
||||||
|
f"Seed complete for tenant {tenant}:\n"
|
||||||
|
f" Buildings: {len(buildings)} (Floors x Rooms: {floors} x {rpf})\n"
|
||||||
|
f" Vendors: {len(vendors)} | Contracts: {len(contracts)}\n"
|
||||||
|
f" Asset Categories: {len(categories)} | Assets: {len(assets)}\n"
|
||||||
|
f" Maint. Types: {len(mtypes)} | Requests: {len(mreqs)} | Schedules: {len(msched)}\n"
|
||||||
|
f" Inspections: {len(insps)}\n"
|
||||||
|
f" Energy Meters: {len(meters)} | Readings: {len(mreads)}\n"
|
||||||
|
f" Reservations: {len(resvs)}"
|
||||||
|
))
|
||||||
|
|
||||||
|
def _get_tenant(self, ident_or_slug):
|
||||||
|
# Try pk then slug
|
||||||
|
try:
|
||||||
|
return Tenant.objects.get(pk=int(ident_or_slug))
|
||||||
|
except (Tenant.DoesNotExist, ValueError):
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
return Tenant.objects.get(slug=ident_or_slug)
|
||||||
|
except Tenant.DoesNotExist:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _purge_tenant_data(self, tenant):
|
||||||
|
# Order matters due to FK relations
|
||||||
|
SpaceReservation.objects.filter(room__floor__building__tenant=tenant).delete()
|
||||||
|
EnergyReading.objects.filter(meter__building__tenant=tenant).delete()
|
||||||
|
EnergyMeter.objects.filter(building__tenant=tenant).delete()
|
||||||
|
Inspection.objects.filter(building__tenant=tenant).delete()
|
||||||
|
MaintenanceSchedule.objects.filter(building__tenant=tenant).delete()
|
||||||
|
MaintenanceRequest.objects.filter(building__tenant=tenant).delete()
|
||||||
|
Asset.objects.filter(building__tenant=tenant).delete()
|
||||||
|
ServiceContract.objects.filter(buildings__tenant=tenant).delete()
|
||||||
|
Vendor.objects.filter(tenant=tenant).delete()
|
||||||
|
Room.objects.filter(floor__building__tenant=tenant).delete()
|
||||||
|
Floor.objects.filter(building__tenant=tenant).delete()
|
||||||
|
Building.objects.filter(tenant=tenant).delete()
|
||||||
@ -61,7 +61,7 @@ class Building(models.Model):
|
|||||||
return 0
|
return 0
|
||||||
occupied_rooms = Room.objects.filter(
|
occupied_rooms = Room.objects.filter(
|
||||||
floor__building=self,
|
floor__building=self,
|
||||||
occupancy_status='occupied'
|
occupancy_status=Room.OccupancyStatus.OCCUPIED
|
||||||
).count()
|
).count()
|
||||||
return (occupied_rooms / total_rooms) * 100
|
return (occupied_rooms / total_rooms) * 100
|
||||||
|
|
||||||
|
|||||||
@ -268,7 +268,7 @@
|
|||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
{% include 'pagination.html' %}
|
{% include 'partial/pagination.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-5">
|
||||||
|
|||||||
@ -11,7 +11,7 @@ urlpatterns = [
|
|||||||
path('buildings/', views.BuildingListView.as_view(), name='building_list'),
|
path('buildings/', views.BuildingListView.as_view(), name='building_list'),
|
||||||
path('buildings/create/', views.BuildingCreateView.as_view(), name='building_create'),
|
path('buildings/create/', views.BuildingCreateView.as_view(), name='building_create'),
|
||||||
path('buildings/<int:pk>/', views.BuildingDetailView.as_view(), name='building_detail'),
|
path('buildings/<int:pk>/', views.BuildingDetailView.as_view(), name='building_detail'),
|
||||||
path('buildings/<int:pk>/edit/', views.BuildingUpdateView.as_view(), name='building_edit'),
|
path('buildings/<int:pk>/edit/', views.BuildingUpdateView.as_view(), name='building_update'),
|
||||||
|
|
||||||
# Asset Management
|
# Asset Management
|
||||||
path('assets/', views.AssetListView.as_view(), name='asset_list'),
|
path('assets/', views.AssetListView.as_view(), name='asset_list'),
|
||||||
|
|||||||
@ -29,7 +29,7 @@ class FacilityDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
context['stats'] = {
|
context['stats'] = {
|
||||||
'total_buildings': Building.objects.filter(is_active=True).count(),
|
'total_buildings': Building.objects.filter(is_active=True).count(),
|
||||||
'total_assets': Asset.objects.count(),
|
'total_assets': Asset.objects.count(),
|
||||||
'operational_assets': Asset.objects.filter(status='operational').count(),
|
'operational_assets': Asset.objects.filter(status=Asset.AssetStatus.OPERATIONAL).count(),
|
||||||
'assets_needing_maintenance': Asset.objects.filter(next_maintenance_date__lte=today).count(),
|
'assets_needing_maintenance': Asset.objects.filter(next_maintenance_date__lte=today).count(),
|
||||||
'open_work_orders': MaintenanceRequest.objects.filter(status__in=['submitted', 'assigned', 'in_progress']).count(),
|
'open_work_orders': MaintenanceRequest.objects.filter(status__in=['submitted', 'assigned', 'in_progress']).count(),
|
||||||
'completed_today': MaintenanceRequest.objects.filter(completed_date__date=today).count(),
|
'completed_today': MaintenanceRequest.objects.filter(completed_date__date=today).count(),
|
||||||
@ -90,7 +90,7 @@ class BuildingListView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
tenant = self.request.user.tenant
|
tenant = self.request.user.tenant
|
||||||
queryset = Building.objects.filter(tenant=tenant).order_by('airport_code', 'name')
|
queryset = Building.objects.filter(tenant=tenant).order_by( 'name')
|
||||||
|
|
||||||
# Search functionality
|
# Search functionality
|
||||||
search = self.request.GET.get('search')
|
search = self.request.GET.get('search')
|
||||||
@ -107,16 +107,16 @@ class BuildingListView(LoginRequiredMixin, ListView):
|
|||||||
queryset = queryset.filter(building_type=building_type)
|
queryset = queryset.filter(building_type=building_type)
|
||||||
|
|
||||||
# Filter by airport
|
# Filter by airport
|
||||||
airport = self.request.GET.get('airport')
|
# airport = self.request.GET.get('airport')
|
||||||
if airport:
|
# if airport:
|
||||||
queryset = queryset.filter(airport_code=airport)
|
# queryset = queryset.filter(airport_code=airport)
|
||||||
|
|
||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
context = super().get_context_data(**kwargs)
|
context = super().get_context_data(**kwargs)
|
||||||
context['building_types'] = Building.BuildingType.choices
|
context['building_types'] = Building.BuildingType.choices
|
||||||
context['airports'] = Building.objects.values_list('airport_code', flat=True).distinct()
|
# context['airports'] = Building.objects.values_list('airport_code', flat=True).distinct()
|
||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
@ -40,6 +40,7 @@ urlpatterns += i18n_patterns(
|
|||||||
path('blood-bank/', include('blood_bank.urls')),
|
path('blood-bank/', include('blood_bank.urls')),
|
||||||
path('patients/', include('patients.urls')),
|
path('patients/', include('patients.urls')),
|
||||||
path('appointments/', include('appointments.urls')),
|
path('appointments/', include('appointments.urls')),
|
||||||
|
path('facility-management/', include('facility_management.urls')),
|
||||||
path('inpatients/', include('inpatients.urls')),
|
path('inpatients/', include('inpatients.urls')),
|
||||||
path('emr/', include('emr.urls')),
|
path('emr/', include('emr.urls')),
|
||||||
path('pharmacy/', include('pharmacy.urls')),
|
path('pharmacy/', include('pharmacy.urls')),
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -90,72 +90,120 @@
|
|||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<!-- Page Header -->
|
<div>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<h1 class="h2">
|
||||||
<div>
|
<i class="fas fa-users me-2"></i>Human<span class="fw-light">Resources</span>
|
||||||
<h1 class="h3 mb-1">
|
</h1>
|
||||||
<i class="fas fa-users me-2"></i>Human Resources Dashboard
|
<p class="text-muted">Manage all HR from one place.</p>
|
||||||
</h1>
|
</div>
|
||||||
<nav aria-label="breadcrumb">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<ol class="breadcrumb mb-0">
|
<div class="btn-group me-2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshDashboard()">
|
||||||
<li class="breadcrumb-item active">Human Resources</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshDashboard()">
|
|
||||||
<i class="fas fa-sync-alt me-2"></i>Refresh
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
||||||
</button>
|
</button>
|
||||||
<div class="btn-group">
|
<a class="btn btn-sm btn-outline-primary" href="{% url 'hr:employee_create' %}">
|
||||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
<i class="fas fa-user-plus me-2"></i>Add Employee
|
||||||
<i class="fas fa-cog me-2"></i>Quick Actions
|
</a>
|
||||||
</button>
|
<a class="btn btn-sm btn-outline-secondary" href="{% url 'hr:department_create' %}">
|
||||||
<ul class="dropdown-menu">
|
<i class="fas fa-building me-2"></i>Add Department
|
||||||
<li><a class="dropdown-item" href="{% url 'hr:employee_create' %}">
|
</a>
|
||||||
<i class="fas fa-user-plus me-2"></i>Add Employee
|
<a class="btn btn-sm btn-outline-theme" href="{% url 'hr:schedule_create' %}">
|
||||||
</a></li>
|
<i class="fas fa-calendar-plus me-2"></i>Create Schedule
|
||||||
<li><a class="dropdown-item" href="{% url 'hr:department_create' %}">
|
</a>
|
||||||
<i class="fas fa-building me-2"></i>Add Department
|
<a class="btn btn-sm btn-outline-warning" href="#">
|
||||||
</a></li>
|
<i class="fas fa-chart-bar me-2"></i>Generate Report
|
||||||
<li><a class="dropdown-item" href="{% url 'hr:schedule_create' %}">
|
</a>
|
||||||
<i class="fas fa-calendar-plus me-2"></i>Create Schedule
|
|
||||||
</a></li>
|
|
||||||
<li><hr class="dropdown-divider"></li>
|
|
||||||
<li><a class="dropdown-item" href="#">
|
|
||||||
<i class="fas fa-chart-bar me-2"></i>Generate Report
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<!-- Statistics Cards -->
|
<!-- Statistics Cards -->
|
||||||
<div class="row mb-4" id="hr-stats">
|
<div class="row mb-2">
|
||||||
{% include "hr/partials/hr_stats.html" %}
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-blue">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-users fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Total Employees</div>
|
||||||
|
<div class="stats-number">{{ total_employees }}</div>
|
||||||
|
<div class="stats-desc">All staff members</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-success">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-user-check fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Active Employees</div>
|
||||||
|
<div class="stats-number">{{ active_employees }}</div>
|
||||||
|
<div class="stats-desc">Currently employed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-info">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-clock fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Present Today</div>
|
||||||
|
<div class="stats-number">{{ employees_clocked_in }}</div>
|
||||||
|
<div class="stats-desc">Currently employed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-warning">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-building fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Departments</div>
|
||||||
|
<div class="stats-number">{{ departments.count }}</div>
|
||||||
|
<div class="stats-desc">Active departments</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-red">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-star fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Pending Reviews</div>
|
||||||
|
<div class="stats-number">{{ pending_reviews }}</div>
|
||||||
|
<div class="stats-desc">Awaiting completion</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-2 col-md-4">
|
||||||
|
<div class="widget widget-stats bg-black">
|
||||||
|
<div class="stats-icon stats-icon-lg"><i class="fas fa-stopwatch fa-fw"></i></div>
|
||||||
|
<div class="stats-content">
|
||||||
|
<div class="stats-title">Total Hours Today</div>
|
||||||
|
<div class="stats-number">{{ total_hours_today|floatformat:1 }}h</div>
|
||||||
|
<div class="stats-desc">Logged work hours</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Recent Activity -->
|
<!-- Recent Activity -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<h4 class="panel-title">
|
||||||
<h5 class="mb-0">
|
<i class="fas fa-clock me-2"></i>Recent Activity
|
||||||
<i class="fas fa-clock me-2"></i>Recent Activity
|
</h4>
|
||||||
</h5>
|
<div class="panel-heading-btn">
|
||||||
<div class="btn-group btn-group-sm">
|
<button type="button" class="btn btn-xs btn-outline-secondary me-2" onclick="refreshActivity()">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshActivity()">
|
<i class="fas fa-sync-alt"></i>
|
||||||
<i class="fas fa-sync-alt"></i>
|
</button>
|
||||||
</button>
|
<a href="#" class="btn btn-xs btn-outline-primary me-2">
|
||||||
<a href="#" class="btn btn-outline-primary">
|
<i class="fas fa-list me-1"></i>View All
|
||||||
<i class="fas fa-list me-1"></i>View All
|
</a>
|
||||||
</a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
</div>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="timeline" id="activity-timeline">
|
<div class="timeline" id="activity-timeline">
|
||||||
<!-- Recent Hires -->
|
<!-- Recent Hires -->
|
||||||
{% for employee in recent_hires %}
|
{% for employee in recent_hires %}
|
||||||
@ -217,13 +265,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Attendance Summary -->
|
<!-- Attendance Summary -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-clock me-2"></i>Today's Attendance
|
<i class="fas fa-clock me-2"></i>Today's Attendance
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
{% include "hr/partials/attendance_summary.html" %}
|
{% include "hr/partials/attendance_summary.html" %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -232,13 +286,19 @@
|
|||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
||||||
<i class="fas fa-user-plus me-2"></i>Add Employee
|
<i class="fas fa-user-plus me-2"></i>Add Employee
|
||||||
@ -266,13 +326,22 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Department Overview -->
|
<!-- Department Overview -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-building me-2"></i>Department Overview
|
<i class="fas fa-building me-2"></i>Department Overview
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="{% url 'hr:department_create' %}" class="btn btn-xs btn-outline-theme me-2">
|
||||||
|
<i class="fas fa-plus me-1"></i>Add Department
|
||||||
|
</a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% for dept in departments %}
|
{% for dept in departments %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
@ -286,9 +355,7 @@
|
|||||||
<div class="text-center py-3">
|
<div class="text-center py-3">
|
||||||
<i class="fas fa-building fa-2x text-muted mb-2"></i>
|
<i class="fas fa-building fa-2x text-muted mb-2"></i>
|
||||||
<p class="text-muted mb-0">No departments found</p>
|
<p class="text-muted mb-0">No departments found</p>
|
||||||
<a href="{% url 'hr:department_create' %}" class="btn btn-sm btn-outline-primary mt-2">
|
|
||||||
<i class="fas fa-plus me-1"></i>Add Department
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@ -296,13 +363,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pending Tasks -->
|
<!-- Pending Tasks -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-tasks me-2"></i>Pending Tasks
|
<i class="fas fa-tasks me-2"></i>Pending Tasks
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% if pending_reviews > 0 %}
|
{% if pending_reviews > 0 %}
|
||||||
<a href="{% url 'hr:performance_review_list' %}?status=PENDING" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0">
|
<a href="{% url 'hr:performance_review_list' %}?status=PENDING" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0">
|
||||||
@ -383,51 +456,51 @@ function refreshActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Clock in/out functionality
|
// Clock in/out functionality
|
||||||
function clockIn() {
|
{#function clockIn() {#}
|
||||||
fetch('{% url "hr:clock_in" %}', {
|
{# fetch('{% url "hr:clock_in" employee_id%}', {#}
|
||||||
method: 'POST',
|
{# method: 'POST',#}
|
||||||
headers: {
|
{# headers: {#}
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
{# 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,#}
|
||||||
'Content-Type': 'application/json',
|
{# 'Content-Type': 'application/json',#}
|
||||||
},
|
{# },#}
|
||||||
})
|
{# })#}
|
||||||
.then(response => response.json())
|
{# .then(response => response.json())#}
|
||||||
.then(data => {
|
{# .then(data => {#}
|
||||||
if (data.success) {
|
{# if (data.success) {#}
|
||||||
alert('Clocked in successfully!');
|
{# alert('Clocked in successfully!');#}
|
||||||
refreshDashboard();
|
{# refreshDashboard();#}
|
||||||
} else {
|
{# } else {#}
|
||||||
alert('Error clocking in: ' + data.message);
|
{# alert('Error clocking in: ' + data.message);#}
|
||||||
}
|
{# }#}
|
||||||
})
|
{# })#}
|
||||||
.catch(error => {
|
{# .catch(error => {#}
|
||||||
console.error('Error:', error);
|
{# console.error('Error:', error);#}
|
||||||
alert('Error clocking in');
|
{# alert('Error clocking in');#}
|
||||||
});
|
{# });#}
|
||||||
}
|
{# }#}
|
||||||
|
{##}
|
||||||
function clockOut() {
|
{#function clockOut() {#}
|
||||||
fetch('{% url "hr:clock_out" %}', {
|
{# fetch('{% url "hr:clock_out" %}', {#}
|
||||||
method: 'POST',
|
{# method: 'POST',#}
|
||||||
headers: {
|
{# headers: {#}
|
||||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
{# 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,#}
|
||||||
'Content-Type': 'application/json',
|
{# 'Content-Type': 'application/json',#}
|
||||||
},
|
{# },#}
|
||||||
})
|
{# })#}
|
||||||
.then(response => response.json())
|
{# .then(response => response.json())#}
|
||||||
.then(data => {
|
{# .then(data => {#}
|
||||||
if (data.success) {
|
{# if (data.success) {#}
|
||||||
alert('Clocked out successfully!');
|
{# alert('Clocked out successfully!');#}
|
||||||
refreshDashboard();
|
{# refreshDashboard();#}
|
||||||
} else {
|
{# } else {#}
|
||||||
alert('Error clocking out: ' + data.message);
|
{# alert('Error clocking out: ' + data.message);#}
|
||||||
}
|
{# }#}
|
||||||
})
|
{# })#}
|
||||||
.catch(error => {
|
{# .catch(error => {#}
|
||||||
console.error('Error:', error);
|
{# console.error('Error:', error);#}
|
||||||
alert('Error clocking out');
|
{# alert('Error clocking out');#}
|
||||||
});
|
{# });#}
|
||||||
}
|
{# }#}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@ -28,235 +28,240 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- begin breadcrumb -->
|
|
||||||
<ol class="breadcrumb float-xl-end">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Home</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
|
||||||
<li class="breadcrumb-item active">Departments</li>
|
|
||||||
</ol>
|
|
||||||
<!-- end breadcrumb -->
|
|
||||||
|
|
||||||
<!-- begin page-header -->
|
<!-- begin page-header -->
|
||||||
<h1 class="page-header">Department Management <small>view and manage hospital departments</small></h1>
|
|
||||||
|
<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-users me-2"></i>Department<span class="fw-light">Management</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">view and manage hospital departments.</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- end page-header -->
|
<!-- end page-header -->
|
||||||
|
<div class="container-fluid">
|
||||||
<!-- begin row -->
|
<!-- begin row -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- begin col-12 -->
|
<!-- begin col-12 -->
|
||||||
<div class="col-xl-12">
|
<div class="col-xl-12">
|
||||||
<!-- begin panel -->
|
<!-- begin panel -->
|
||||||
<div class="panel panel-inverse">
|
<div class="panel panel-inverse">
|
||||||
<!-- begin panel-heading -->
|
<!-- begin panel-heading -->
|
||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title">Departments</h4>
|
<h4 class="panel-title">Departments</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
<a href="{% url 'hr:department_create' %}" class="btn btn-xs btn-outline-theme me-2">
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<i class="fas fa-plus"></i> Add Department
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- end panel-heading -->
|
|
||||||
|
|
||||||
<!-- begin panel-body -->
|
|
||||||
<div class="panel-body">
|
|
||||||
<div class="d-flex justify-content-between mb-3">
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'hr:department_create' %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus"></i> Add Department
|
|
||||||
</a>
|
</a>
|
||||||
<div class="btn-group ms-2 view-toggle" role="group">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<button type="button" class="btn btn-outline-secondary active" id="table-view-btn">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<i class="fas fa-table"></i> Table
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
</button>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
<button type="button" class="btn btn-outline-secondary" id="card-view-btn">
|
|
||||||
<i class="fas fa-th-large"></i> Cards
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex">
|
</div>
|
||||||
<form method="get" class="me-2">
|
<!-- end panel-heading -->
|
||||||
<div class="input-group">
|
|
||||||
<input type="text" class="form-control" placeholder="Search departments..." name="search" value="{{ search }}">
|
<!-- begin panel-body -->
|
||||||
<button class="btn btn-outline-secondary" type="submit">
|
<div class="panel-body">
|
||||||
<i class="fas fa-search"></i>
|
<div class="d-flex justify-content-between mb-3">
|
||||||
|
<div>
|
||||||
|
<div class="btn-group ms-2 view-toggle" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-secondary active" id="table-view-btn">
|
||||||
|
<i class="fas fa-table"></i> Table
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="card-view-btn">
|
||||||
|
<i class="fas fa-th-large"></i> Cards
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
<div class="dropdown">
|
<div class="d-flex">
|
||||||
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="exportDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
<form method="get" class="me-2">
|
||||||
<i class="fas fa-download"></i> Export
|
<div class="input-group">
|
||||||
</button>
|
<input type="text" class="form-control" placeholder="Search departments..." name="search" value="{{ search }}">
|
||||||
<ul class="dropdown-menu" aria-labelledby="exportDropdown">
|
<button class="btn btn-outline-secondary" type="submit">
|
||||||
<li><a class="dropdown-item" href="#" id="export-csv"><i class="fas fa-file-csv"></i> CSV</a></li>
|
<i class="fas fa-search"></i>
|
||||||
<li><a class="dropdown-item" href="#" id="export-excel"><i class="fas fa-file-excel"></i> Excel</a></li>
|
</button>
|
||||||
<li><a class="dropdown-item" href="#" id="export-pdf"><i class="fas fa-file-pdf"></i> PDF</a></li>
|
</div>
|
||||||
</ul>
|
</form>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-outline-secondary dropdown-toggle" type="button" id="exportDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
<i class="fas fa-download"></i> Export
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="exportDropdown">
|
||||||
|
<li><a class="dropdown-item" href="#" id="export-csv"><i class="fas fa-file-csv"></i> CSV</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" id="export-excel"><i class="fas fa-file-excel"></i> Excel</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#" id="export-pdf"><i class="fas fa-file-pdf"></i> PDF</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Table View -->
|
<!-- Table View -->
|
||||||
<div id="table-view">
|
<div id="table-view">
|
||||||
<table id="departments-table" class="table table-striped table-bordered align-middle">
|
<table id="departments-table" class="table table-striped table-bordered align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Department Name</th>
|
<th>Department Name</th>
|
||||||
<th>Code</th>
|
<th>Code</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
<th>Head</th>
|
<th>Head</th>
|
||||||
<th>Employees</th>
|
<th>Employees</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Actions</th>
|
<th>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for department in departments %}
|
{% for department in departments %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<strong>{{ department.name }}</strong>
|
<strong>{{ department.name }}</strong>
|
||||||
{% if department.parent_department %}
|
{% if department.parent_department %}
|
||||||
<small class="d-block text-muted">
|
<small class="d-block text-muted">
|
||||||
<i class="fas fa-level-up-alt fa-flip-horizontal"></i> {{ department.parent_department.name }}
|
<i class="fas fa-level-up-alt fa-flip-horizontal"></i> {{ department.parent_department.name }}
|
||||||
</small>
|
</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ department.department_code }}</td>
|
<td>{{ department.department_code }}</td>
|
||||||
<td>{{ department.get_department_type_display }}</td>
|
<td>{{ department.get_department_type_display }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if department.department_head %}
|
{% if department.department_head %}
|
||||||
<a href="{% url 'hr:employee_detail' department.department_head.id %}">
|
<a href="{% url 'hr:employee_detail' department.department_head.id %}">
|
||||||
{{ department.department_head.get_full_name }}
|
{{ department.department_head.get_full_name }}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not assigned</span>
|
<span class="text-muted">Not assigned</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-primary">{{ department.employee_count }}</span>
|
<span class="badge bg-primary">{{ department.employee_count }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
{% if department.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-danger">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group">
|
||||||
|
<a href="{% url 'hr:department_detail' department.id %}" class="btn btn-sm btn-info">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'hr:department_update' department.id %}" class="btn btn-sm btn-primary">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'hr:department_delete' department.id %}" class="btn btn-sm btn-danger">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% empty %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="7" class="text-center">No departments found.</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if is_paginated %}
|
||||||
|
{% include 'partial/pagination.html'%}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card View -->
|
||||||
|
<div id="card-view" class="row g-3" style="display: none;">
|
||||||
|
{% for department in departments %}
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="card department-card h-100">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">{{ department.name }}</h5>
|
||||||
{% if department.is_active %}
|
{% if department.is_active %}
|
||||||
<span class="badge bg-success">Active</span>
|
<span class="badge bg-success">Active</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">Inactive</span>
|
<span class="badge bg-danger">Inactive</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</div>
|
||||||
<td>
|
<div class="card-body">
|
||||||
<div class="btn-group">
|
<div class="mb-3">
|
||||||
|
<strong>Code:</strong> {{ department.department_code }}<br>
|
||||||
|
<strong>Type:</strong> {{ department.get_department_type_display }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Department Head:</strong><br>
|
||||||
|
{% if department.department_head %}
|
||||||
|
<a href="{% url 'hr:employee_detail' department.department_head.id %}">
|
||||||
|
{{ department.department_head.get_full_name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not assigned</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Parent Department:</strong><br>
|
||||||
|
{% if department.parent_department %}
|
||||||
|
<a href="{% url 'hr:department_detail' department.parent_department.id %}">
|
||||||
|
{{ department.parent_department.name }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">None (Top Level)</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="department-stats d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-users"></i> {{ department.employee_count }} Employees
|
||||||
|
</div>
|
||||||
|
{% if department.location %}
|
||||||
|
<div>
|
||||||
|
<i class="fas fa-map-marker-alt"></i> {{ department.location }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer">
|
||||||
|
<div class="btn-group w-100">
|
||||||
<a href="{% url 'hr:department_detail' department.id %}" class="btn btn-sm btn-info">
|
<a href="{% url 'hr:department_detail' department.id %}" class="btn btn-sm btn-info">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i> View
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'hr:department_update' department.id %}" class="btn btn-sm btn-primary">
|
<a href="{% url 'hr:department_update' department.id %}" class="btn btn-sm btn-primary">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i> Edit
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'hr:department_delete' department.id %}" class="btn btn-sm btn-danger">
|
<a href="{% url 'hr:department_delete' department.id %}" class="btn btn-sm btn-danger">
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i> Delete
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% empty %}
|
|
||||||
<tr>
|
|
||||||
<td colspan="7" class="text-center">No departments found.</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if is_paginated %}
|
|
||||||
{% include 'partial/pagination.html'%}
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Card View -->
|
|
||||||
<div id="card-view" class="row g-3" style="display: none;">
|
|
||||||
{% for department in departments %}
|
|
||||||
<div class="col-md-6 col-lg-4">
|
|
||||||
<div class="card department-card h-100">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h5 class="card-title mb-0">{{ department.name }}</h5>
|
|
||||||
{% if department.is_active %}
|
|
||||||
<span class="badge bg-success">Active</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">Inactive</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Code:</strong> {{ department.department_code }}<br>
|
|
||||||
<strong>Type:</strong> {{ department.get_department_type_display }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Department Head:</strong><br>
|
|
||||||
{% if department.department_head %}
|
|
||||||
<a href="{% url 'hr:employee_detail' department.department_head.id %}">
|
|
||||||
{{ department.department_head.get_full_name }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Not assigned</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>Parent Department:</strong><br>
|
|
||||||
{% if department.parent_department %}
|
|
||||||
<a href="{% url 'hr:department_detail' department.parent_department.id %}">
|
|
||||||
{{ department.parent_department.name }}
|
|
||||||
</a>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">None (Top Level)</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="department-stats d-flex justify-content-between">
|
|
||||||
<div>
|
|
||||||
<i class="fas fa-users"></i> {{ department.employee_count }} Employees
|
|
||||||
</div>
|
|
||||||
{% if department.location %}
|
|
||||||
<div>
|
|
||||||
<i class="fas fa-map-marker-alt"></i> {{ department.location }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="btn-group w-100">
|
|
||||||
<a href="{% url 'hr:department_detail' department.id %}" class="btn btn-sm btn-info">
|
|
||||||
<i class="fas fa-eye"></i> View
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'hr:department_update' department.id %}" class="btn btn-sm btn-primary">
|
|
||||||
<i class="fas fa-edit"></i> Edit
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'hr:department_delete' department.id %}" class="btn btn-sm btn-danger">
|
|
||||||
<i class="fas fa-trash"></i> Delete
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% empty %}
|
||||||
{% empty %}
|
<div class="col-12">
|
||||||
<div class="col-12">
|
<div class="alert alert-info">
|
||||||
<div class="alert alert-info">
|
No departments found.
|
||||||
No departments found.
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<!-- Pagination for Card View -->
|
<!-- Pagination for Card View -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
{% include 'partial/pagination.html' %}
|
{% include 'partial/pagination.html' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- end panel-body -->
|
||||||
</div>
|
</div>
|
||||||
<!-- end panel-body -->
|
<!-- end panel -->
|
||||||
</div>
|
</div>
|
||||||
<!-- end panel -->
|
<!-- end col-12 -->
|
||||||
</div>
|
</div>
|
||||||
<!-- end col-12 -->
|
|
||||||
</div>
|
|
||||||
<!-- end row -->
|
<!-- end row -->
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
|||||||
@ -11,13 +11,14 @@
|
|||||||
<h1 class="h3 mb-1">
|
<h1 class="h3 mb-1">
|
||||||
<i class="fas fa-user me-2"></i>{{ employee.get_full_name }}
|
<i class="fas fa-user me-2"></i>{{ employee.get_full_name }}
|
||||||
</h1>
|
</h1>
|
||||||
<nav aria-label="breadcrumb">
|
<div
|
||||||
<ol class="breadcrumb mb-0">
|
id="clock-controls-{{ employee.id }}"
|
||||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
hx-get="{% url 'hr:clock_controls' employee.id %}"
|
||||||
<li class="breadcrumb-item"><a href="{% url 'hr:employee_list' %}">Employees</a></li>
|
hx-trigger="load"
|
||||||
<li class="breadcrumb-item active">{{ employee.get_full_name }}</li>
|
hx-swap="outerHTML">
|
||||||
</ol>
|
<!-- Optional skeleton while loading -->
|
||||||
</nav>
|
<button class="btn btn-outline-secondary" disabled>Loading…</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="{% url 'hr:employee_list' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'hr:employee_list' %}" class="btn btn-outline-secondary">
|
||||||
@ -60,15 +61,16 @@
|
|||||||
<div class="panel-heading">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title"><i class="fa fa-user-alt me-2 "></i>Employee Details</h4>
|
<h4 class="panel-title"><i class="fa fa-user-alt me-2 "></i>Employee Details</h4>
|
||||||
<div class="panel-heading-btn">
|
<div class="panel-heading-btn">
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="card mb-4">
|
<div class="card border border-primary mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header bg-gradient bg-primary text-white">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-id-card me-2"></i>Employee Information
|
<i class="fas fa-id-card me-2"></i>Employee Information
|
||||||
</h5>
|
</h5>
|
||||||
@ -135,8 +137,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Information -->
|
<!-- Contact Information -->
|
||||||
<div class="card mb-4">
|
<div class="card border border-success mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header bg-gradient bg-success text-white">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-address-book me-2"></i>Contact Information
|
<i class="fas fa-address-book me-2"></i>Contact Information
|
||||||
</h5>
|
</h5>
|
||||||
@ -195,9 +197,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Emergency Contact -->
|
<!-- Emergency Contact -->
|
||||||
{% if employee.emergency_contact_name %}
|
|
||||||
<div class="card mb-4">
|
<div class="card border border-danger mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header bg-gradient bg-danger text-white">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>Emergency Contact
|
<i class="fas fa-exclamation-triangle me-2"></i>Emergency Contact
|
||||||
</h5>
|
</h5>
|
||||||
@ -228,11 +230,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Recent Activity -->
|
<!-- Recent Activity -->
|
||||||
<div class="card">
|
<div class="card border border-warning mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header bg-gradient bg-warning">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-history me-2"></i>Recent Activity
|
<i class="fas fa-history me-2"></i>Recent Activity
|
||||||
</h5>
|
</h5>
|
||||||
@ -285,13 +287,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Stats -->
|
<!-- Quick Stats -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-chart-bar me-2"></i>Quick Stats
|
<i class="fas fa-chart-bar me-2"></i>Quick Stats
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<span class="text-muted">Years of Service</span>
|
<span class="text-muted">Years of Service</span>
|
||||||
<span class="badge bg-primary">{{ employee.years_of_service|floatformat:1 }}</span>
|
<span class="badge bg-primary">{{ employee.years_of_service|floatformat:1 }}</span>
|
||||||
@ -312,14 +321,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
|
{# <a hx-post="{% url 'hr:clock_in' employee.id %}"#}
|
||||||
|
{# hx-target="#clock-controls-{{ employee.id }}"#}
|
||||||
|
{# hx-swap="outerHTML"#}
|
||||||
|
{# hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'#}
|
||||||
|
{# class="btn btn-outline-green">#}
|
||||||
|
{# <i class="fa fa-check-circle text-green"></i> Clock In#}
|
||||||
|
{# </a>#}
|
||||||
|
|
||||||
|
{# If you don't have open_entry_for_today, pass None and the partial will show "Clock In". #}
|
||||||
|
|
||||||
<a href="#" class="btn btn-outline-primary">
|
<a href="#" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-clock me-2"></i>View Schedule
|
<i class="fas fa-clock me-2"></i>View Schedule
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@ -74,7 +74,6 @@
|
|||||||
{{ form.father_name.errors.0 }}
|
{{ form.father_name.errors.0 }}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="form-text">Name the employee prefers to be called</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
|
|||||||
@ -4,112 +4,54 @@
|
|||||||
{% block title %}Employee Management - {{ block.super }}{% endblock %}
|
{% block title %}Employee Management - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<!-- Page Header -->
|
<div>
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<h1 class="h2">
|
||||||
<div>
|
<i class="fas fa-users me-2"></i>Employee<span class="fw-light">Management</span>
|
||||||
<h1 class="h3 mb-1">
|
</h1>
|
||||||
<i class="fas fa-users me-2"></i>Employee Management
|
<p class="text-muted">Review employees and add new ones.</p>
|
||||||
</h1>
|
</div>
|
||||||
<nav aria-label="breadcrumb">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<ol class="breadcrumb mb-0">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
|
||||||
<li class="breadcrumb-item active">Employees</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
<a href="{% url 'hr:employee_create' %}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="fas fa-download me-2"></i>Export
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportEmployees('csv')">
|
|
||||||
<i class="fas fa-file-csv me-2"></i>Export as CSV
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportEmployees('excel')">
|
|
||||||
<i class="fas fa-file-excel me-2"></i>Export as Excel
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportEmployees('pdf')">
|
|
||||||
<i class="fas fa-file-pdf me-2"></i>Export as PDF
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus me-2"></i>Add Employee
|
<i class="fas fa-plus me-2"></i>Add Employee
|
||||||
</a>
|
</a>
|
||||||
</div>
|
<div class="btn-group">
|
||||||
</div>
|
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-download me-2"></i>Export
|
||||||
<!-- Statistics Cards -->
|
</button>
|
||||||
<div class="row mb-4">
|
<ul class="dropdown-menu">
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('csv')">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<i class="fas fa-file-csv me-2"></i>Export as CSV
|
||||||
<div class="card-body">
|
</a></li>
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('excel')">
|
||||||
<div>
|
<i class="fas fa-file-excel me-2"></i>Export as Excel
|
||||||
<h6 class="card-title text-muted mb-1">Total Employees</h6>
|
</a></li>
|
||||||
<h3 class="mb-0 text-primary">{{ total_employees }}</h3>
|
<li><a class="dropdown-item" href="#" onclick="exportEmployees('pdf')">
|
||||||
<small class="text-muted">All staff members</small>
|
<i class="fas fa-file-pdf me-2"></i>Export as PDF
|
||||||
</div>
|
</a></li>
|
||||||
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
</ul>
|
||||||
<i class="fas fa-users fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Active Employees</h6>
|
|
||||||
<h3 class="mb-0 text-success">{{ active_employees }}</h3>
|
|
||||||
<small class="text-muted">Currently employed</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-success bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-user-check fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">New Hires</h6>
|
|
||||||
<h3 class="mb-0 text-info">{{ new_hires_count }}</h3>
|
|
||||||
<small class="text-muted">This month</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-user-plus fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Departments</h6>
|
|
||||||
<h3 class="mb-0 text-warning">{{ departments_count }}</h3>
|
|
||||||
<small class="text-muted">Active departments</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-warning bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-building fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-users-gear me-2"></i>Employee Management
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<!-- Filters Card -->
|
<!-- Filters Card -->
|
||||||
<div class="card mb-4">
|
<div class="card border border-success mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header bg-success text-white">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-filter me-2"></i>Filters
|
<i class="fas fa-filter me-2"></i>Filters
|
||||||
</h5>
|
</h5>
|
||||||
@ -165,8 +107,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Employees List -->
|
<!-- Employees List -->
|
||||||
<div class="card">
|
<div class="card border border-primary">
|
||||||
<div class="card-header">
|
<div class="card-header bg-primary text-white">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h5 class="mb-0">
|
<h5 class="mb-0">
|
||||||
<i class="fas fa-list me-2"></i>Employees
|
<i class="fas fa-list me-2"></i>Employees
|
||||||
@ -175,10 +117,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshEmployees()">
|
<button type="button" class="btn btn-sm btn-outline-light" onclick="refreshEmployees()">
|
||||||
<i class="fas fa-sync-alt me-1"></i>Refresh
|
<i class="fas fa-sync-alt me-1"></i>Refresh
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="toggleBulkActions()">
|
<button type="button" class="btn btn-sm btn-outline-light" onclick="toggleBulkActions()">
|
||||||
<i class="fas fa-check-square me-1"></i>Bulk Actions
|
<i class="fas fa-check-square me-1"></i>Bulk Actions
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -247,7 +189,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="font-monospace">{{ employee.employee_number }}</span>
|
<span class="fw-bold">{{ employee.employee_id }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if employee.department %}
|
{% if employee.department %}
|
||||||
@ -326,7 +268,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
// Employee list functionality
|
// Employee list functionality
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
|||||||
45
hr/templates/hr/partials/clock_controls.html
Normal file
45
hr/templates/hr/partials/clock_controls.html
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
{% load tz %}
|
||||||
|
<div id="clock-controls-{{ employee.id }}">
|
||||||
|
{% if time_entry %}
|
||||||
|
{% if not time_entry.clock_out_time %}
|
||||||
|
<!-- State: clocked in, waiting to clock out -->
|
||||||
|
<span class="badge bg-success me-2 py-2 px-3">
|
||||||
|
<i class="fa fa-check-circle me-1"></i>
|
||||||
|
Clocked in at {{ time_entry.clock_in_time|localtime|date:"Y-m-d H:i" }}
|
||||||
|
</span>
|
||||||
|
<form
|
||||||
|
hx-post="{% url 'hr:clock_out' employee.id %}"
|
||||||
|
hx-target="#clock-controls-{{ employee.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-danger">
|
||||||
|
<i class="fa fa-sign-out-alt me-1"></i> Clock Out
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% else %}
|
||||||
|
<!-- State: finished -->
|
||||||
|
<div class="note alert-success py-2 px-3 mb-0 d-inline-block">
|
||||||
|
<i class="fa fa-clock me-1"></i>
|
||||||
|
Clocked in: {{ time_entry.clock_in_time|localtime|date:"Y-m-d H:i" }}
|
||||||
|
—
|
||||||
|
Clocked out: {{ time_entry.clock_out_time|localtime|date:"Y-m-d H:i" }}
|
||||||
|
{% if time_entry.total_hours %}
|
||||||
|
(<strong>{{ time_entry.total_hours|floatformat:2 }}</strong> h)
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<!-- State: not clocked in yet -->
|
||||||
|
<form
|
||||||
|
hx-post="{% url 'hr:clock_in' employee.id %}"
|
||||||
|
hx-target="#clock-controls-{{ employee.id }}"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<button type="submit" class="btn btn-sm btn-outline-green">
|
||||||
|
<i class="fa fa-check-circle me-1"></i> Clock In
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
@ -1,102 +1,2 @@
|
|||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Total Employees</h6>
|
|
||||||
<h3 class="mb-0 text-primary">{{ total_employees }}</h3>
|
|
||||||
<small class="text-muted">All staff members</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-primary bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-users fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Active Employees</h6>
|
|
||||||
<h3 class="mb-0 text-success">{{ active_employees }}</h3>
|
|
||||||
<small class="text-muted">Currently employed</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-success bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-user-check fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Present Today</h6>
|
|
||||||
<h3 class="mb-0 text-info">{{ employees_clocked_in }}</h3>
|
|
||||||
<small class="text-muted">Currently working</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-info bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-clock fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Departments</h6>
|
|
||||||
<h3 class="mb-0 text-warning">{{ total_departments }}</h3>
|
|
||||||
<small class="text-muted">Active departments</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-warning bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-building fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Pending Reviews</h6>
|
|
||||||
<h3 class="mb-0 text-danger">{{ pending_reviews }}</h3>
|
|
||||||
<small class="text-muted">Awaiting completion</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-danger bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-star fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-lg-3 col-md-6 mb-3">
|
|
||||||
<div class="card border-0 shadow-sm h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h6 class="card-title text-muted mb-1">Total Hours Today</h6>
|
|
||||||
<h3 class="mb-0 text-secondary">{{ total_hours_today|floatformat:1 }}h</h3>
|
|
||||||
<small class="text-muted">Logged work hours</small>
|
|
||||||
</div>
|
|
||||||
<div class="bg-secondary bg-gradient rounded-circle d-flex align-items-center justify-content-center" style="width: 50px; height: 50px;">
|
|
||||||
<i class="fas fa-stopwatch fa-lg text-white"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|||||||
@ -268,9 +268,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ entry.date|date:"M d, Y" }}</td>
|
<td>{{ entry.work_date|date:"M d, Y" }}</td>
|
||||||
<td>{{ entry.start_time|time:"H:i" }} - {{ entry.end_time|time:"H:i" }}</td>
|
<td>{{ entry.clock_in_time|time:"H:i" }} - {{ entry.clock_out_time|time:"H:i" }}</td>
|
||||||
<td>{{ entry.hours }} hrs</td>
|
<td>{{ entry.regular_hours }} hrs</td>
|
||||||
<td>
|
<td>
|
||||||
{% if entry.is_overtime %}
|
{% if entry.is_overtime %}
|
||||||
<span class="badge bg-danger">Overtime</span>
|
<span class="badge bg-danger">Overtime</span>
|
||||||
|
|||||||
@ -94,8 +94,9 @@ urlpatterns = [
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# ACTION URLS FOR WORKFLOW OPERATIONS
|
# ACTION URLS FOR WORKFLOW OPERATIONS
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
path('clock-in/', views.clock_in, name='clock_in'),
|
path('employees/<int:employee_id>/clock-controls/', views.clock_controls, name='clock_controls'),
|
||||||
path('clock-out/', views.clock_out, name='clock_out'),
|
path('employees/<int:employee_id>/clock-in/', views.clock_in, name='clock_in'),
|
||||||
|
path('employees/<int:employee_id>/clock-out/', views.clock_out, name='clock_out'),
|
||||||
path('time-entries/<int:entry_id>/approve/', views.approve_time_entry, name='approve_time_entry'),
|
path('time-entries/<int:entry_id>/approve/', views.approve_time_entry, name='approve_time_entry'),
|
||||||
path('schedules/<int:schedule_id>/publish/', views.publish_schedule, name='publish_schedule'),
|
path('schedules/<int:schedule_id>/publish/', views.publish_schedule, name='publish_schedule'),
|
||||||
|
|
||||||
|
|||||||
681
hr/views.py
681
hr/views.py
@ -4,6 +4,8 @@ HR app views with comprehensive CRUD operations following healthcare best practi
|
|||||||
|
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
from django.views.decorators.http import require_POST, require_GET
|
||||||
|
from django.template.loader import render_to_string
|
||||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.views.generic import (
|
from django.views.generic import (
|
||||||
@ -29,6 +31,7 @@ from .forms import (
|
|||||||
from core.utils import AuditLogger
|
from core.utils import AuditLogger
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||||
"""
|
"""
|
||||||
Main HR dashboard with comprehensive statistics and recent activity.
|
Main HR dashboard with comprehensive statistics and recent activity.
|
||||||
@ -45,7 +48,7 @@ class HRDashboardView(LoginRequiredMixin, TemplateView):
|
|||||||
tenant=self.request.user.tenant,
|
tenant=self.request.user.tenant,
|
||||||
employment_status='ACTIVE'
|
employment_status='ACTIVE'
|
||||||
).count(),
|
).count(),
|
||||||
'total_departments': Department.objects.filter(tenant=self.request.user.tenant).count(),
|
'departments': Department.objects.filter(tenant=self.request.user.tenant),
|
||||||
'pending_reviews': PerformanceReview.objects.filter(
|
'pending_reviews': PerformanceReview.objects.filter(
|
||||||
employee__tenant=self.request.user.tenant,
|
employee__tenant=self.request.user.tenant,
|
||||||
status='PENDING'
|
status='PENDING'
|
||||||
@ -1098,103 +1101,193 @@ def attendance_summary(request):
|
|||||||
return render(request, 'hr/partials/attendance_summary.html', context)
|
return render(request, 'hr/partials/attendance_summary.html', context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
def clock_in(request):
|
|
||||||
"""
|
|
||||||
Clock in an employee.
|
|
||||||
"""
|
|
||||||
if request.method == 'POST':
|
|
||||||
employee_id = request.POST.get('employee_id')
|
|
||||||
|
|
||||||
try:
|
@require_GET
|
||||||
employee = Employee.objects.get(
|
def clock_controls(request, employee_id):
|
||||||
id=employee_id,
|
"""Return the clock controls partial for today's state (HTMX GET)."""
|
||||||
tenant=request.user.tenant
|
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||||
)
|
today = timezone.localdate()
|
||||||
|
|
||||||
# Check if already clocked in today
|
# Prefer the open entry for today; otherwise the last entry today (finished)
|
||||||
today = timezone.now().date()
|
time_entry = (
|
||||||
existing_entry = TimeEntry.objects.filter(
|
TimeEntry.objects
|
||||||
employee=employee,
|
.filter(employee=employee, work_date=today)
|
||||||
work_date=today,
|
.order_by('clock_out_time', '-clock_in_time') # open first (clock_out_time NULL), else latest finished
|
||||||
clock_out_time__isnull=True
|
.first()
|
||||||
).first()
|
)
|
||||||
|
|
||||||
if existing_entry:
|
return render(request, 'hr/partials/clock_controls.html', {
|
||||||
return JsonResponse({
|
'employee': employee,
|
||||||
'success': False,
|
'time_entry': time_entry,
|
||||||
'message': 'Employee is already clocked in.'
|
})
|
||||||
})
|
|
||||||
|
|
||||||
# Create new time entry
|
|
||||||
time_entry = TimeEntry.objects.create(
|
|
||||||
employee=employee,
|
|
||||||
work_date=today,
|
|
||||||
clock_in_time=timezone.now(),
|
|
||||||
status='DRAFT'
|
|
||||||
)
|
|
||||||
|
|
||||||
return JsonResponse({
|
|
||||||
'success': True,
|
|
||||||
'message': f'{employee.get_full_name()} clocked in successfully.',
|
|
||||||
'time_entry_id': time_entry.id
|
|
||||||
})
|
|
||||||
|
|
||||||
except Employee.DoesNotExist:
|
|
||||||
return JsonResponse({
|
|
||||||
'success': False,
|
|
||||||
'message': 'Employee not found.'
|
|
||||||
})
|
|
||||||
|
|
||||||
return JsonResponse({'success': False, 'message': 'Invalid request.'})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
def _render_controls(request, employee, time_entry):
|
||||||
def clock_out(request):
|
html = render_to_string('hr/partials/clock_controls.html',
|
||||||
"""
|
{'employee': employee, 'time_entry': time_entry},
|
||||||
Clock out an employee.
|
request=request)
|
||||||
"""
|
return HttpResponse(html)
|
||||||
if request.method == 'POST':
|
|
||||||
employee_id = request.POST.get('employee_id')
|
|
||||||
|
|
||||||
try:
|
@require_POST
|
||||||
employee = Employee.objects.get(
|
def clock_in(request, employee_id):
|
||||||
id=employee_id,
|
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||||
tenant=request.user.tenant
|
today = timezone.localdate()
|
||||||
)
|
|
||||||
|
|
||||||
# Find active time entry
|
open_entry = TimeEntry.objects.filter(
|
||||||
today = timezone.now().date()
|
employee=employee, work_date=today, clock_out_time__isnull=True
|
||||||
time_entry = TimeEntry.objects.filter(
|
).first()
|
||||||
employee=employee,
|
if open_entry:
|
||||||
work_date=today,
|
return _render_controls(request, employee, open_entry)
|
||||||
clock_out_time__isnull=True
|
|
||||||
).first()
|
|
||||||
|
|
||||||
if not time_entry:
|
time_entry = TimeEntry.objects.create(
|
||||||
return JsonResponse({
|
employee=employee, work_date=today, clock_in_time=timezone.now(), status='DRAFT'
|
||||||
'success': False,
|
)
|
||||||
'message': 'No active clock-in found for this employee.'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Update time entry
|
if request.headers.get('HX-Request'):
|
||||||
time_entry.clock_out_time = timezone.now()
|
return _render_controls(request, employee, time_entry)
|
||||||
time_entry.save() # This will trigger the save method to calculate hours
|
return JsonResponse({'success': True, 'time_entry_id': time_entry.id})
|
||||||
|
|
||||||
return JsonResponse({
|
@require_POST
|
||||||
'success': True,
|
def clock_out(request, employee_id):
|
||||||
'message': f'{employee.get_full_name()} clocked out successfully.',
|
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||||
'time_entry_id': time_entry.id,
|
today = timezone.localdate()
|
||||||
'hours_worked': float(time_entry.total_hours)
|
|
||||||
})
|
|
||||||
|
|
||||||
except Employee.DoesNotExist:
|
time_entry = TimeEntry.objects.filter(
|
||||||
return JsonResponse({
|
employee=employee, work_date=today, clock_out_time__isnull=True
|
||||||
'success': False,
|
).first()
|
||||||
'message': 'Employee not found.'
|
if not time_entry:
|
||||||
})
|
# Re-render to default state (will show "Clock In")
|
||||||
|
if request.headers.get('HX-Request'):
|
||||||
|
return _render_controls(request, employee, None)
|
||||||
|
return JsonResponse({'success': False, 'message': 'No active clock-in found.'}, status=400)
|
||||||
|
|
||||||
return JsonResponse({'success': False, 'message': 'Invalid request.'})
|
time_entry.clock_out_time = timezone.now()
|
||||||
|
time_entry.save()
|
||||||
|
|
||||||
|
if request.headers.get('HX-Request'):
|
||||||
|
return _render_controls(request, employee, time_entry)
|
||||||
|
return JsonResponse({'success': True, 'time_entry_id': time_entry.id})
|
||||||
|
|
||||||
|
# @login_required
|
||||||
|
# @require_POST
|
||||||
|
# def clock_in(request, employee_id):
|
||||||
|
# """
|
||||||
|
# Clock in an employee. If HTMX, return the updated controls partial (button -> time).
|
||||||
|
# Otherwise return JSON.
|
||||||
|
# """
|
||||||
|
# # Prefer the URL param; fall back to POST field for safety
|
||||||
|
# emp_id = employee_id or request.POST.get('employee_id')
|
||||||
|
#
|
||||||
|
# try:
|
||||||
|
# employee = Employee.objects.get(id=emp_id, tenant=request.user.tenant)
|
||||||
|
# except Employee.DoesNotExist:
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# return HttpResponse("Employee not found.", status=404)
|
||||||
|
# return JsonResponse({'success': False, 'message': 'Employee not found.'}, status=404)
|
||||||
|
#
|
||||||
|
# today = timezone.now().date()
|
||||||
|
# open_entry = TimeEntry.objects.filter(
|
||||||
|
# employee=employee,
|
||||||
|
# work_date=today,
|
||||||
|
# clock_out_time__isnull=True
|
||||||
|
# ).first()
|
||||||
|
#
|
||||||
|
# if open_entry:
|
||||||
|
# # Already clocked in — just re-render the controls so the UI shows the time
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# html = render_to_string(
|
||||||
|
# 'hr/partials/clock_controls.html',
|
||||||
|
# {'employee': employee, 'time_entry': open_entry},
|
||||||
|
# request=request
|
||||||
|
# )
|
||||||
|
# return HttpResponse(html)
|
||||||
|
# return JsonResponse({'success': False, 'message': 'Employee is already clocked in.'}, status=400)
|
||||||
|
#
|
||||||
|
# # Create new time entry
|
||||||
|
# time_entry = TimeEntry.objects.create(
|
||||||
|
# employee=employee,
|
||||||
|
# work_date=today,
|
||||||
|
# clock_in_time=timezone.now(),
|
||||||
|
# status='DRAFT'
|
||||||
|
# )
|
||||||
|
#
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# html = render_to_string(
|
||||||
|
# 'hr/partials/clock_controls.html',
|
||||||
|
# {'employee': employee, 'time_entry': time_entry},
|
||||||
|
# request=request
|
||||||
|
# )
|
||||||
|
# return HttpResponse(html)
|
||||||
|
#
|
||||||
|
# return JsonResponse({
|
||||||
|
# 'success': True,
|
||||||
|
# 'message': f'{employee.get_full_name()} clocked in successfully.',
|
||||||
|
# 'time_entry_id': time_entry.id
|
||||||
|
# })
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# @login_required
|
||||||
|
# @require_POST
|
||||||
|
# def clock_out(request, employee_id):
|
||||||
|
# """
|
||||||
|
# Clock out an employee.
|
||||||
|
# If HTMX, return the updated controls partial (button -> times).
|
||||||
|
# Otherwise, return JSON.
|
||||||
|
# """
|
||||||
|
# emp_id = employee_id or request.POST.get('employee_id')
|
||||||
|
#
|
||||||
|
# # Find employee in same tenant
|
||||||
|
# try:
|
||||||
|
# employee = Employee.objects.get(id=emp_id, tenant=request.user.tenant)
|
||||||
|
# except Employee.DoesNotExist:
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# # Re-render controls without an active entry (will show "Clock In")
|
||||||
|
# html = render_to_string(
|
||||||
|
# 'hr/partials/clock_controls.html',
|
||||||
|
# {'employee': None, 'time_entry': None}, # or pass the real employee if you want the button still visible
|
||||||
|
# request=request
|
||||||
|
# )
|
||||||
|
# return HttpResponse(html, status=404)
|
||||||
|
# return JsonResponse({'success': False, 'message': 'Employee not found.'}, status=404)
|
||||||
|
#
|
||||||
|
# # Locate today's open entry
|
||||||
|
# today = timezone.now().date()
|
||||||
|
# time_entry = TimeEntry.objects.filter(
|
||||||
|
# employee=employee,
|
||||||
|
# work_date=today,
|
||||||
|
# clock_out_time__isnull=True
|
||||||
|
# ).first()
|
||||||
|
#
|
||||||
|
# if not time_entry:
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# # No open entry → re-render to show "Clock In"
|
||||||
|
# html = render_to_string(
|
||||||
|
# 'hr/partials/clock_controls.html',
|
||||||
|
# {'employee': employee, 'time_entry': None},
|
||||||
|
# request=request
|
||||||
|
# )
|
||||||
|
# return HttpResponse(html, status=400)
|
||||||
|
# return JsonResponse({'success': False, 'message': 'No active clock-in found for this employee.'}, status=400)
|
||||||
|
#
|
||||||
|
# # Close the entry
|
||||||
|
# time_entry.clock_out_time = timezone.now()
|
||||||
|
# time_entry.save() # if your model computes total_hours on save
|
||||||
|
#
|
||||||
|
# if request.headers.get('HX-Request'):
|
||||||
|
# html = render_to_string(
|
||||||
|
# 'hr/partials/clock_controls.html',
|
||||||
|
# {'employee': employee, 'time_entry': time_entry},
|
||||||
|
# request=request
|
||||||
|
# )
|
||||||
|
# return HttpResponse(html)
|
||||||
|
#
|
||||||
|
# return JsonResponse({
|
||||||
|
# 'success': True,
|
||||||
|
# 'message': f'{employee.get_full_name()} clocked out successfully.',
|
||||||
|
# 'time_entry_id': time_entry.id,
|
||||||
|
# # If total_hours is Decimal, this makes it JSON-safe:
|
||||||
|
# 'hours_worked': float(getattr(time_entry, 'total_hours', 0) or 0)
|
||||||
|
# })
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2644,111 +2737,111 @@ def assign_department_head(request, pk):
|
|||||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
@login_required
|
||||||
# def mark_time_entry_paid(request, pk):
|
def mark_time_entry_paid(request, pk):
|
||||||
# """
|
"""
|
||||||
# Mark a time entry as paid.
|
Mark a time entry as paid.
|
||||||
# """
|
"""
|
||||||
# time_entry = get_object_or_404(
|
time_entry = get_object_or_404(
|
||||||
# TimeEntry,
|
TimeEntry,
|
||||||
# pk=pk,
|
pk=pk,
|
||||||
# tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# if not request.user.has_perm('hr.change_timeentry'):
|
if not request.user.has_perm('hr.change_timeentry'):
|
||||||
# messages.error(request, "You don't have permission to mark time entries as paid.")
|
messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||||
#
|
|
||||||
# time_entry.is_paid = True
|
time_entry.is_paid = True
|
||||||
# time_entry.payment_date = timezone.now().date()
|
time_entry.payment_date = timezone.now().date()
|
||||||
# time_entry.save()
|
time_entry.save()
|
||||||
#
|
|
||||||
# messages.success(
|
messages.success(
|
||||||
# request,
|
request,
|
||||||
# f"Time entry for {time_entry.employee.get_full_name()} on {time_entry.entry_date} marked as paid."
|
f"Time entry for {time_entry.employee.get_full_name()} on {time_entry.entry_date} marked as paid."
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# redirect_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.pk}))
|
redirect_url = request.POST.get('next', reverse('hr:time_entry_detail', kwargs={'pk': time_entry.pk}))
|
||||||
# return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
#
|
|
||||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def bulk_approve_time_entries(request):
|
def bulk_approve_time_entries(request):
|
||||||
# """
|
"""
|
||||||
# Bulk approve time entries.
|
Bulk approve time entries.
|
||||||
# """
|
"""
|
||||||
# if not request.user.has_perm('hr.approve_timeentry'):
|
if not request.user.has_perm('hr.approve_timeentry'):
|
||||||
# messages.error(request, "You don't have permission to approve time entries.")
|
messages.error(request, "You don't have permission to approve time entries.")
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# entry_ids = request.POST.getlist('entry_ids')
|
entry_ids = request.POST.getlist('entry_ids')
|
||||||
#
|
|
||||||
# if not entry_ids:
|
if not entry_ids:
|
||||||
# messages.warning(request, "No time entries selected for approval.")
|
messages.warning(request, "No time entries selected for approval.")
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
|
||||||
# # Get entries that belong to this tenant
|
# Get entries that belong to this tenant
|
||||||
# entries = TimeEntry.objects.filter(
|
entries = TimeEntry.objects.filter(
|
||||||
# tenant=request.user.tenant,
|
tenant=request.user.tenant,
|
||||||
# pk__in=entry_ids,
|
pk__in=entry_ids,
|
||||||
# is_approved=False
|
is_approved=False
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# # Update all entries
|
# Update all entries
|
||||||
# updated_count = entries.update(
|
updated_count = entries.update(
|
||||||
# is_approved=True,
|
is_approved=True,
|
||||||
# approved_by=request.user,
|
approved_by=request.user,
|
||||||
# approval_date=timezone.now()
|
approval_date=timezone.now()
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# messages.success(request, f"{updated_count} time entries approved successfully.")
|
messages.success(request, f"{updated_count} time entries approved successfully.")
|
||||||
#
|
|
||||||
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||||
# return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
#
|
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def bulk_mark_time_entries_paid(request):
|
def bulk_mark_time_entries_paid(request):
|
||||||
# """
|
"""
|
||||||
# Bulk mark time entries as paid.
|
Bulk mark time entries as paid.
|
||||||
# """
|
"""
|
||||||
# if not request.user.has_perm('hr.change_timeentry'):
|
if not request.user.has_perm('hr.change_timeentry'):
|
||||||
# messages.error(request, "You don't have permission to mark time entries as paid.")
|
messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# entry_ids = request.POST.getlist('entry_ids')
|
entry_ids = request.POST.getlist('entry_ids')
|
||||||
#
|
|
||||||
# if not entry_ids:
|
if not entry_ids:
|
||||||
# messages.warning(request, "No time entries selected for payment.")
|
messages.warning(request, "No time entries selected for payment.")
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
|
||||||
# # Get entries that belong to this tenant
|
# Get entries that belong to this tenant
|
||||||
# entries = TimeEntry.objects.filter(
|
entries = TimeEntry.objects.filter(
|
||||||
# tenant=request.user.tenant,
|
tenant=request.user.tenant,
|
||||||
# pk__in=entry_ids,
|
pk__in=entry_ids,
|
||||||
# is_approved=True,
|
is_approved=True,
|
||||||
# is_paid=False
|
is_paid=False
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# # Update all entries
|
# Update all entries
|
||||||
# updated_count = entries.update(
|
updated_count = entries.update(
|
||||||
# is_paid=True,
|
is_paid=True,
|
||||||
# payment_date=timezone.now().date()
|
payment_date=timezone.now().date()
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# messages.success(request, f"{updated_count} time entries marked as paid successfully.")
|
messages.success(request, f"{updated_count} time entries marked as paid successfully.")
|
||||||
#
|
|
||||||
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||||
# return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
#
|
|
||||||
# return redirect('hr:time_entry_list')
|
return redirect('hr:time_entry_list')
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# class PerformanceReviewListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
# class PerformanceReviewListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||||
@ -2950,76 +3043,76 @@ def assign_department_head(request, pk):
|
|||||||
# return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk})
|
# return reverse('hr:performance_review_detail', kwargs={'pk': self.object.pk})
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
@login_required
|
||||||
# def acknowledge_review(request, pk):
|
def acknowledge_review(request, pk):
|
||||||
# """
|
"""
|
||||||
# Acknowledge a performance review.
|
Acknowledge a performance review.
|
||||||
# """
|
"""
|
||||||
# review = get_object_or_404(
|
review = get_object_or_404(
|
||||||
# PerformanceReview,
|
PerformanceReview,
|
||||||
# pk=pk,
|
pk=pk,
|
||||||
# tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# # Check if the user is the employee being reviewed
|
# Check if the user is the employee being reviewed
|
||||||
# if hasattr(request.user, 'employee_profile') and request.user.employee_profile == review.employee:
|
if hasattr(request.user, 'employee_profile') and request.user.employee_profile == review.employee:
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# review.is_acknowledged = True
|
review.is_acknowledged = True
|
||||||
# review.acknowledgment_date = timezone.now()
|
review.acknowledgment_date = timezone.now()
|
||||||
# review.save()
|
review.save()
|
||||||
#
|
|
||||||
# messages.success(request, "You have acknowledged this performance review.")
|
messages.success(request, "You have acknowledged this performance review.")
|
||||||
#
|
|
||||||
# redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
||||||
# return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
# else:
|
else:
|
||||||
# messages.error(request, "Only the employee being reviewed can acknowledge this review.")
|
messages.error(request, "Only the employee being reviewed can acknowledge this review.")
|
||||||
#
|
|
||||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||||
#
|
|
||||||
#
|
|
||||||
# @login_required
|
@login_required
|
||||||
# def complete_review(request, pk):
|
def complete_review(request, pk):
|
||||||
# """
|
"""
|
||||||
# Mark a performance review as completed.
|
Mark a performance review as completed.
|
||||||
# """
|
"""
|
||||||
# review = get_object_or_404(
|
review = get_object_or_404(
|
||||||
# PerformanceReview,
|
PerformanceReview,
|
||||||
# pk=pk,
|
pk=pk,
|
||||||
# tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# if not request.user.has_perm('hr.change_performancereview'):
|
if not request.user.has_perm('hr.change_performancereview'):
|
||||||
# messages.error(request, "You don't have permission to complete reviews.")
|
messages.error(request, "You don't have permission to complete reviews.")
|
||||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||||
#
|
|
||||||
# # Check if reviewer is the one completing the review
|
# Check if reviewer is the one completing the review
|
||||||
# if review.reviewer != request.user:
|
if review.reviewer != request.user:
|
||||||
# messages.warning(request, "Only the assigned reviewer should complete this review.")
|
messages.warning(request, "Only the assigned reviewer should complete this review.")
|
||||||
#
|
|
||||||
# # Update review status
|
# Update review status
|
||||||
# review.status = 'COMPLETED'
|
review.status = 'COMPLETED'
|
||||||
# review.completion_date = timezone.now().date()
|
review.completion_date = timezone.now().date()
|
||||||
# review.save()
|
review.save()
|
||||||
#
|
|
||||||
# # Update employee's last and next review dates
|
# Update employee's last and next review dates
|
||||||
# employee = review.employee
|
employee = review.employee
|
||||||
# employee.last_review_date = review.completion_date
|
employee.last_review_date = review.completion_date
|
||||||
# employee.next_review_date = review.next_review_date
|
employee.next_review_date = review.next_review_date
|
||||||
# employee.save()
|
employee.save()
|
||||||
#
|
|
||||||
# messages.success(
|
messages.success(
|
||||||
# request,
|
request,
|
||||||
# f"Performance review for {review.employee.get_full_name()} marked as completed."
|
f"Performance review for {review.employee.get_full_name()} marked as completed."
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
||||||
# return redirect(redirect_url)
|
return redirect(redirect_url)
|
||||||
#
|
|
||||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||||
#
|
|
||||||
#
|
|
||||||
# class TrainingRecordListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
# class TrainingRecordListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||||
# """
|
# """
|
||||||
# List view for training records.
|
# List view for training records.
|
||||||
@ -3349,36 +3442,36 @@ def assign_department_head(request, pk):
|
|||||||
# return redirect('hr:dashboard')
|
# return redirect('hr:dashboard')
|
||||||
#
|
#
|
||||||
#
|
#
|
||||||
# @login_required
|
@login_required
|
||||||
# def employee_schedule(request, employee_id):
|
def employee_schedule(request, employee_id):
|
||||||
# """
|
"""
|
||||||
# View an employee's schedule.
|
View an employee's schedule.
|
||||||
# """
|
"""
|
||||||
# employee = get_object_or_404(
|
employee = get_object_or_404(
|
||||||
# Employee,
|
Employee,
|
||||||
# pk=employee_id,
|
pk=employee_id,
|
||||||
# tenant=request.user.tenant
|
tenant=request.user.tenant
|
||||||
# )
|
)
|
||||||
#
|
|
||||||
# # Get current schedule
|
# Get current schedule
|
||||||
# current_schedule = Schedule.objects.filter(
|
current_schedule = Schedule.objects.filter(
|
||||||
# employee=employee,
|
employee=employee,
|
||||||
# is_current=True,
|
is_current=True,
|
||||||
# is_active=True
|
is_active=True
|
||||||
# ).first()
|
).first()
|
||||||
#
|
|
||||||
# # Get upcoming assignments
|
# Get upcoming assignments
|
||||||
# today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
# upcoming_assignments = ScheduleAssignment.objects.filter(
|
upcoming_assignments = ScheduleAssignment.objects.filter(
|
||||||
# schedule__employee=employee,
|
schedule__employee=employee,
|
||||||
# assignment_date__gte=today
|
assignment_date__gte=today
|
||||||
# ).order_by('assignment_date', 'start_time')[:14] # Next 2 weeks
|
).order_by('assignment_date', 'start_time')[:14] # Next 2 weeks
|
||||||
#
|
|
||||||
# context = {
|
context = {
|
||||||
# 'employee': employee,
|
'employee': employee,
|
||||||
# 'current_schedule': current_schedule,
|
'current_schedule': current_schedule,
|
||||||
# 'upcoming_assignments': upcoming_assignments,
|
'upcoming_assignments': upcoming_assignments,
|
||||||
# }
|
}
|
||||||
#
|
|
||||||
# return render(request, 'hr/employee_schedule.html', context)
|
return render(request, 'hr/employee_schedule.html', context)
|
||||||
#
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -114,7 +114,7 @@ class BedAdmin(admin.ModelAdmin):
|
|||||||
'fields': ('ward', 'bed_number', 'room_number', 'bed_type', 'room_type')
|
'fields': ('ward', 'bed_number', 'room_number', 'bed_type', 'room_type')
|
||||||
}),
|
}),
|
||||||
('Current Status', {
|
('Current Status', {
|
||||||
'fields': ('status', 'current_patient', 'current_admission',
|
'fields': ('status', 'current_admission',
|
||||||
'occupied_since', 'reserved_until')
|
'occupied_since', 'reserved_until')
|
||||||
}),
|
}),
|
||||||
('Equipment & Features', {
|
('Equipment & Features', {
|
||||||
@ -451,6 +451,9 @@ class SurgeryScheduleAdmin(admin.ModelAdmin):
|
|||||||
('Basic Information', {
|
('Basic Information', {
|
||||||
'fields': ('tenant', 'surgery_id', 'patient', 'admission')
|
'fields': ('tenant', 'surgery_id', 'patient', 'admission')
|
||||||
}),
|
}),
|
||||||
|
# ('Operating Room',{
|
||||||
|
# 'fields': ('operating_room')
|
||||||
|
# }),
|
||||||
('Surgery Details', {
|
('Surgery Details', {
|
||||||
'fields': ('procedure_name', 'procedure_code', 'surgery_type', 'preop_diagnosis')
|
'fields': ('procedure_name', 'procedure_code', 'surgery_type', 'preop_diagnosis')
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -42,7 +42,7 @@ class WardForm(forms.ModelForm):
|
|||||||
self.fields['nurse_manager'].queryset = User.objects.filter(
|
self.fields['nurse_manager'].queryset = User.objects.filter(
|
||||||
tenant=user.tenant,
|
tenant=user.tenant,
|
||||||
is_active=True,
|
is_active=True,
|
||||||
role__in=['NURSE', 'NURSE_MANAGER']
|
employee_profile__role__in=['NURSE', 'NURSE_MANAGER']
|
||||||
).order_by('last_name', 'first_name')
|
).order_by('last_name', 'first_name')
|
||||||
|
|
||||||
def clean_total_beds(self):
|
def clean_total_beds(self):
|
||||||
|
|||||||
@ -1157,6 +1157,7 @@ class Transfer(models.Model):
|
|||||||
('COMPLETED', 'Completed'),
|
('COMPLETED', 'Completed'),
|
||||||
('CANCELLED', 'Cancelled'),
|
('CANCELLED', 'Cancelled'),
|
||||||
('DELAYED', 'Delayed'),
|
('DELAYED', 'Delayed'),
|
||||||
|
('REJECTED', 'Rejected'),
|
||||||
]
|
]
|
||||||
PRIORITY_CHOICES = [
|
PRIORITY_CHOICES = [
|
||||||
('ROUTINE', 'Routine'),
|
('ROUTINE', 'Routine'),
|
||||||
|
|||||||
BIN
inpatients/templates/inpatients/.DS_Store
vendored
Normal file
BIN
inpatients/templates/inpatients/.DS_Store
vendored
Normal file
Binary file not shown.
@ -53,16 +53,21 @@
|
|||||||
<tr><td>Type</td><td>{{ admission.get_admission_type_display }}</td></tr>
|
<tr><td>Type</td><td>{{ admission.get_admission_type_display }}</td></tr>
|
||||||
<tr><td>Status</td><td>
|
<tr><td>Status</td><td>
|
||||||
{% if admission.status == 'ADMITTED' %}
|
{% if admission.status == 'ADMITTED' %}
|
||||||
<span class="badge bg-success">{{ admission.get_status_display }}</span>
|
<span class="badge bg-success">
|
||||||
{% elif admission.status == 'PENDING' %}
|
{% elif admission.status == 'PENDING' %}
|
||||||
<span class="badge bg-warning">{{ admission.get_status_display }}</span>
|
<span class="badge bg-warning">
|
||||||
{% elif admission.status == 'TRANSFERRED' %}
|
{% elif admission.status == 'TRANSFERRED' %}
|
||||||
<span class="badge bg-info">{{ admission.get_status_display }}</span>
|
<span class="badge bg-info">
|
||||||
{% elif admission.status == 'DISCHARGED' %}
|
{% elif admission.status == 'DISCHARGED' %}
|
||||||
<span class="badge bg-secondary">{{ admission.get_status_display }}</span>
|
<span class="badge bg-secondary">
|
||||||
{% elif admission.status == 'CANCELLED' %}
|
{% elif admission.status == 'CANCELLED' %}
|
||||||
<span class="badge bg-danger">{{ admission.get_status_display }}</span>
|
<span class="badge bg-danger">
|
||||||
|
{% elif admission.status == 'LEFT_AMA' %}
|
||||||
|
<span class="badge bg-purple">
|
||||||
|
{% elif admission.status == 'DECEASED' %}
|
||||||
|
<span class="badge bg-black">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{{ admission.get_status_display }}</span>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr><td>Priority</td><td>{{ admission.get_priority_display }}</td></tr>
|
<tr><td>Priority</td><td>{{ admission.get_priority_display }}</td></tr>
|
||||||
<tr><td>Length of Stay</td><td>{{ admission.length_of_stay }} days</td></tr>
|
<tr><td>Length of Stay</td><td>{{ admission.length_of_stay }} days</td></tr>
|
||||||
|
|||||||
@ -4,414 +4,432 @@
|
|||||||
{% block title %}Bed {{ object.bed_number }} Details{% endblock %}
|
{% block title %}Bed {{ object.bed_number }} Details{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
|
<i class="fas fa-bed me-2"></i>Bed<span class="fw-light">Details</span>
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:bed_management' %}">Bed Management</a></li>
|
</h1>
|
||||||
<li class="breadcrumb-item active">Bed {{ object.bed_number }}</li>
|
<p class="text-muted">{{ object.bed_number }}</p>
|
||||||
</ol>
|
|
||||||
<h1 class="page-header mb-0">Bed {{ object.bed_number }} Details</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
<div class="btn-group">
|
<div class="btn-group me-2">
|
||||||
<a href="{% url 'inpatients:bed_update' object.pk %}" class="btn btn-primary">
|
<a href="{% url 'inpatients:bed_update' object.pk %}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="fas fa-edit me-2"></i>Edit Bed
|
<i class="fas fa-edit me-2"></i>Edit Bed
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||||
<span class="visually-hidden">Toggle Dropdown</span>
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
<i class="fas fa-ellipsis-v"></i>
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('available')">
|
<li>
|
||||||
<i class="fas fa-check text-success me-2"></i>Mark Available
|
<a class="dropdown-item" href="#" onclick="updateBedStatus('available')">
|
||||||
</a></li>
|
<i class="fas fa-check text-success me-2"></i>Mark Available
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('occupied')">
|
</a>
|
||||||
<i class="fas fa-user text-warning me-2"></i>Mark Occupied
|
</li>
|
||||||
</a></li>
|
<li>
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('maintenance')">
|
<a class="dropdown-item" href="#" onclick="updateBedStatus('occupied')">
|
||||||
<i class="fas fa-tools text-info me-2"></i>Mark for Maintenance
|
<i class="fas fa-user text-warning me-2"></i>Mark Occupied
|
||||||
</a></li>
|
</a>
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('cleaning')">
|
</li>
|
||||||
<i class="fas fa-broom text-primary me-2"></i>Mark for Cleaning
|
<li>
|
||||||
</a></li>
|
<a class="dropdown-item" href="#" onclick="updateBedStatus('maintenance')">
|
||||||
<li><hr class="dropdown-divider"></li>
|
<i class="fas fa-tools text-info me-2"></i>Mark for Maintenance
|
||||||
{# <li><a class="dropdown-item text-danger" href="{% url 'inpatients:bed_confirm_delete' object.pk %}">#}
|
</a>
|
||||||
{# <i class="fas fa-trash me-2"></i>Delete Bed#}
|
</li>
|
||||||
{# </a></li>#}
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick="updateBedStatus('cleaning')">
|
||||||
|
<i class="fas fa-broom text-primary me-2"></i>Mark for Cleaning
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xl-8">
|
<div class="col-xl-9">
|
||||||
<!-- Bed Information -->
|
<!-- Bed Information -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title">
|
<h4 class="panel-title"><i class="fa fa-bed me-2"></i>Bed Information</h4>
|
||||||
<i class="fas fa-bed me-2"></i>
|
<div class="panel-heading-btn">
|
||||||
Bed Information
|
<span class="badge bg-{% if object.status == 'AVAILABLE' %}success{% elif object.status == 'OCCUPIED' %}warning{% elif object.status == 'MAINTENANCE' %}danger{% elif object.status == 'RESERVED' %}purple{% else %}info{% endif %} fs-6 me-2">
|
||||||
</h4>
|
{{ object.get_status_display }}
|
||||||
<div class="card-toolbar">
|
</span>
|
||||||
<span class="badge bg-{% if object.status == 'available' %}success{% elif object.status == 'occupied' %}warning{% elif object.status == 'maintenance' %}danger{% else %}info{% endif %} fs-6">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
{{ object.get_status_display }}
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
</span>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
</div>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Bed Number:</td>
|
|
||||||
<td>{{ object.bed_number }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Ward:</td>
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
|
||||||
{{ object.ward.name }}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Room:</td>
|
|
||||||
<td>{{ object.room_number|default:"Not assigned" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Bed Type:</td>
|
|
||||||
<td>{{ object.get_bed_type_display }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Status:</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-{% if object.status == 'available' %}success{% elif object.status == 'occupied' %}warning{% elif object.status == 'maintenance' %}danger{% else %}info{% endif %}">
|
|
||||||
{{ object.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Features:</td>
|
|
||||||
<td>
|
|
||||||
{% if object.has_oxygen %}
|
|
||||||
<span class="badge bg-info me-1">Oxygen</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if object.has_suction %}
|
|
||||||
<span class="badge bg-info me-1">Suction</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if object.has_monitor %}
|
|
||||||
<span class="badge bg-info me-1">Monitor</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if object.is_isolation %}
|
|
||||||
<span class="badge bg-warning me-1">Isolation</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if not object.has_oxygen and not object.has_suction and not object.has_monitor and not object.is_isolation %}
|
|
||||||
<span class="text-muted">Standard bed</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Last Cleaned:</td>
|
|
||||||
<td>{{ object.last_cleaned|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Last Maintenance:</td>
|
|
||||||
<td>{{ object.last_maintenance|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Created:</td>
|
|
||||||
<td>{{ object.created_at|date:"M d, Y g:i A" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td class="fw-bold">Last Updated:</td>
|
|
||||||
<td>{{ object.updated_at|date:"M d, Y g:i A" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
{% if object.notes %}
|
<div class="row">
|
||||||
<div class="mt-3">
|
<div class="col-md-6">
|
||||||
<h6>Notes:</h6>
|
<table class="table table-borderless">
|
||||||
<div class="alert alert-info">
|
|
||||||
{{ object.notes }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Current Patient -->
|
|
||||||
{% if object.current_patient %}
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">
|
|
||||||
<i class="fas fa-user-injured me-2"></i>
|
|
||||||
Current Patient
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<div class="avatar avatar-lg">
|
|
||||||
{% if object.current_patient.profile_picture %}
|
|
||||||
<img src="{{ object.current_patient.profile_picture.url }}" alt="{{ object.current_patient.get_full_name }}" class="rounded-circle">
|
|
||||||
{% else %}
|
|
||||||
<div class="avatar-initial rounded-circle bg-primary">
|
|
||||||
{{ object.current_patient.first_name|first }}{{ object.current_patient.last_name|first }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 ms-3">
|
|
||||||
<h5 class="mb-1">
|
|
||||||
<a href="{% url 'patients:patient_detail' object.current_patient.pk %}">
|
|
||||||
{{ object.current_patient.get_full_name }}
|
|
||||||
</a>
|
|
||||||
</h5>
|
|
||||||
<div class="text-muted mb-1">
|
|
||||||
<strong>MRN:</strong> {{ object.current_patient.medical_record_number }}
|
|
||||||
</div>
|
|
||||||
<div class="text-muted mb-1">
|
|
||||||
<strong>DOB:</strong> {{ object.current_patient.date_of_birth|date:"M d, Y" }}
|
|
||||||
</div>
|
|
||||||
<div class="text-muted">
|
|
||||||
<strong>Admitted:</strong> {{ object.current_admission.admission_date|date:"M d, Y g:i A" }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<a href="{% url 'inpatients:admission_detail' object.current_admission.pk %}" class="btn btn-outline-primary">
|
|
||||||
<i class="fas fa-eye me-2"></i>View Admission
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Bed History -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h4 class="card-title">
|
|
||||||
<i class="fas fa-history me-2"></i>
|
|
||||||
Bed History
|
|
||||||
</h4>
|
|
||||||
<div class="card-toolbar">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-primary" onclick="refreshHistory()">
|
|
||||||
<i class="fas fa-sync me-2"></i>Refresh
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if bed_history %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-hover">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date</th>
|
<td class="fw-bold">Bed Number:</td>
|
||||||
<th>Action</th>
|
<td>{{ object.bed_number }}</td>
|
||||||
<th>Patient</th>
|
|
||||||
<th>Duration</th>
|
|
||||||
<th>Notes</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for history in bed_history %}
|
|
||||||
<tr>
|
<tr>
|
||||||
|
<td class="fw-bold">Ward:</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ history.date|date:"M d, Y" }}</div>
|
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
||||||
<div class="small text-muted">{{ history.date|time:"g:i A" }}</div>
|
{{ object.ward.name }}
|
||||||
|
</a>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Room:</td>
|
||||||
|
<td>{{ object.room_number|default:"Not assigned" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Bed Type:</td>
|
||||||
|
<td>{{ object.get_bed_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Status:</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-{% if history.action == 'admission' %}success{% elif history.action == 'discharge' %}info{% elif history.action == 'transfer' %}warning{% else %}secondary{% endif %}">
|
<span class="badge bg-{% if object.status == 'available' %}success{% elif object.status == 'occupied' %}warning{% elif object.status == 'maintenance' %}danger{% else %}info{% endif %}">
|
||||||
{{ history.get_action_display }}
|
{{ object.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Features:</td>
|
||||||
<td>
|
<td>
|
||||||
{% if history.patient %}
|
{% if object.has_oxygen %}
|
||||||
<a href="{% url 'patients:patient_detail' history.patient.pk %}">
|
<span class="badge bg-info me-1">Oxygen</span>
|
||||||
{{ history.patient.get_full_name }}
|
|
||||||
</a>
|
|
||||||
<div class="small text-muted">{{ history.patient.medical_record_number }}</div>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">System action</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
{% if object.has_suction %}
|
||||||
<td>
|
<span class="badge bg-info me-1">Suction</span>
|
||||||
{% if history.duration %}
|
|
||||||
{{ history.duration }}
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">-</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
{% if object.has_monitor %}
|
||||||
<td>
|
<span class="badge bg-info me-1">Monitor</span>
|
||||||
{% if history.notes %}
|
{% endif %}
|
||||||
<span class="text-truncate" style="max-width: 200px;" title="{{ history.notes }}">
|
{% if object.is_isolation %}
|
||||||
{{ history.notes }}
|
<span class="badge bg-warning me-1">Isolation</span>
|
||||||
</span>
|
{% endif %}
|
||||||
{% else %}
|
{% if not object.has_oxygen and not object.has_suction and not object.has_monitor and not object.is_isolation %}
|
||||||
<span class="text-muted">-</span>
|
<span class="text-muted">Standard bed</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
<tr>
|
||||||
</tbody>
|
<td class="fw-bold">Last Cleaned:</td>
|
||||||
</table>
|
<td>{{ object.last_cleaned|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Last Maintenance:</td>
|
||||||
|
<td>{{ object.last_maintenance|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Created:</td>
|
||||||
|
<td>{{ object.created_at|date:"M d, Y g:i A" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Last Updated:</td>
|
||||||
|
<td>{{ object.updated_at|date:"M d, Y g:i A" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="text-center text-muted py-4">
|
|
||||||
<i class="fas fa-history fa-3x mb-3"></i>
|
|
||||||
<h5>No History Available</h5>
|
|
||||||
<p>No bed history records found for this bed.</p>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-xl-4">
|
{% if object.notes %}
|
||||||
<!-- Quick Actions -->
|
<div class="mt-3">
|
||||||
<div class="card mb-4">
|
<h6>Notes:</h6>
|
||||||
<div class="card-header">
|
<div class="note alert-info">
|
||||||
<h5 class="card-title">
|
{{ object.notes }}
|
||||||
<i class="fas fa-bolt me-2"></i>
|
</div>
|
||||||
Quick Actions
|
</div>
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-grid gap-2">
|
|
||||||
{% if object.status == 'available' %}
|
|
||||||
<button type="button" class="btn btn-success" onclick="assignPatient()">
|
|
||||||
<i class="fas fa-user-plus me-2"></i>Assign Patient
|
|
||||||
</button>
|
|
||||||
{% elif object.status == 'occupied' %}
|
|
||||||
<button type="button" class="btn btn-warning" onclick="dischargePatient()">
|
|
||||||
<i class="fas fa-sign-out-alt me-2"></i>Discharge Patient
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-info" onclick="transferPatient()">
|
|
||||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Patient
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="scheduleCleaning()">
|
|
||||||
<i class="fas fa-broom me-2"></i>Schedule Cleaning
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="scheduleMaintenance()">
|
|
||||||
<i class="fas fa-tools me-2"></i>Schedule Maintenance
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-outline-info" onclick="viewBedChart()">
|
|
||||||
<i class="fas fa-chart-line me-2"></i>View Utilization
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Bed Statistics -->
|
<!-- Current Patient -->
|
||||||
<div class="card mb-4">
|
{% if object.current_admission.patient %}
|
||||||
<div class="card-header">
|
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||||
<h5 class="card-title">
|
<div class="panel-heading">
|
||||||
<i class="fas fa-chart-bar me-2"></i>
|
<h4 class="panel-title">
|
||||||
Statistics
|
<i class="fas fa-user-injured me-2"></i>Current Patient
|
||||||
</h5>
|
</h4>
|
||||||
</div>
|
<div class="panel-heading-btn">
|
||||||
<div class="card-body">
|
<a href="{% url 'inpatients:admission_detail' object.current_admission.pk %}" class="btn btn-xs btn-outline-theme">
|
||||||
<div class="mb-3">
|
<i class="fas fa-eye me-2"></i>View Admission
|
||||||
<div class="d-flex justify-content-between mb-1">
|
</a>
|
||||||
<span>Occupancy Rate (30 days)</span>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<span>{{ bed_stats.occupancy_rate|default:0 }}%</span>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
</div>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<div class="progress">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
<div class="progress-bar bg-primary" style="width: {{ bed_stats.occupancy_rate|default:0 }}%"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="row text-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="col-6">
|
|
||||||
<div class="border-end">
|
|
||||||
<div class="fs-4 fw-bold text-primary">{{ bed_stats.total_admissions|default:0 }}</div>
|
|
||||||
<div class="small text-muted">Total Admissions</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<div class="fs-4 fw-bold text-success">{{ bed_stats.avg_stay_days|default:0 }}</div>
|
|
||||||
<div class="small text-muted">Avg Stay (days)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Maintenance Schedule -->
|
|
||||||
<div class="card mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5 class="card-title">
|
|
||||||
<i class="fas fa-calendar-check me-2"></i>
|
|
||||||
Maintenance Schedule
|
|
||||||
</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if maintenance_schedule %}
|
|
||||||
{% for maintenance in maintenance_schedule %}
|
|
||||||
<div class="d-flex align-items-center mb-3">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-{% if maintenance.type == 'cleaning' %}broom text-primary{% elif maintenance.type == 'inspection' %}search text-info{% else %}tools text-warning{% endif %} fa-lg"></i>
|
|
||||||
</div>
|
|
||||||
<div class="flex-grow-1 ms-3">
|
<div class="flex-grow-1 ms-3">
|
||||||
<div class="fw-bold">{{ maintenance.get_type_display }}</div>
|
<h5 class="mb-1">
|
||||||
<div class="small text-muted">{{ maintenance.scheduled_date|date:"M d, Y g:i A" }}</div>
|
<a href="{% url 'patients:patient_detail' object.current_admission.patient.pk %}">
|
||||||
</div>
|
{{ object.current_admission.patient.get_full_name }}
|
||||||
<div class="flex-shrink-0">
|
</a>
|
||||||
<span class="badge bg-{% if maintenance.status == 'pending' %}warning{% elif maintenance.status == 'completed' %}success{% else %}secondary{% endif %}">
|
</h5>
|
||||||
{{ maintenance.get_status_display }}
|
<div class="text-muted mb-1">
|
||||||
</span>
|
<strong>MRN:</strong> {{ object.current_admission.patient.mrn }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted mb-1">
|
||||||
|
<strong>DOB:</strong> {{ object.current_admission.patient.date_of_birth|date:"M d, Y" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-muted">
|
||||||
|
<strong>Admitted:</strong> {{ object.current_admission.admission_datetime|date:"M d, Y g:i A" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
</div>
|
||||||
{% else %}
|
</div>
|
||||||
<div class="text-center text-muted">
|
{% endif %}
|
||||||
<i class="fas fa-calendar-check fa-2x mb-2"></i>
|
|
||||||
<div>No scheduled maintenance</div>
|
<!-- Bed History -->
|
||||||
|
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-history me-2"></i>Bed History
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<button type="button" class="btn btn-xs btn-outline-primary me-2" onclick="refreshHistory()">
|
||||||
|
<i class="fas fa-sync me-2"></i>Refresh
|
||||||
|
</button>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
{% if admission_history %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Patient</th>
|
||||||
|
<th>Duration</th>
|
||||||
|
<th>Notes</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for history in admission_history %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>{{ history.admission_datetime|date:"M d, Y" }}</div>
|
||||||
|
<div class="small text-muted">{{ history.admission_datetime|time:"g:i A" }}</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if history.action == 'ADMISSION' %}success{% elif history.action == 'DISCHARGE' %}info{% elif history.action == 'TRANSFER' %}warning{% else %}secondary{% endif %}">
|
||||||
|
{{ history.get_action_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if history.patient %}
|
||||||
|
<a href="{% url 'patients:patient_detail' history.patient.pk %}">
|
||||||
|
{{ history.patient.get_full_name }}
|
||||||
|
</a>
|
||||||
|
<div class="small text-muted">{{ history.patient.mrn }}</div>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">System action</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if history.duration %}
|
||||||
|
{{ history.duration }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if history.notes %}
|
||||||
|
<span class="text-truncate" style="max-width: 200px;" title="{{ history.notes }}">
|
||||||
|
{{ history.notes }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted py-4">
|
||||||
|
<i class="fas fa-history fa-3x mb-3"></i>
|
||||||
|
<h5>No History Available</h5>
|
||||||
|
<p>No bed history records found for this bed.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ward Information -->
|
<div class="col-xl-3">
|
||||||
<div class="card">
|
<!-- Quick Actions -->
|
||||||
<div class="card-header">
|
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||||
<h5 class="card-title">
|
<div class="panel-heading">
|
||||||
<i class="fas fa-hospital me-2"></i>
|
<h4 class="panel-title">
|
||||||
Ward Information
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="d-grid gap-2">
|
||||||
|
{% if object.status == 'AVAILABLE' %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-success" onclick="assignPatient()">
|
||||||
|
<i class="fas fa-user-plus me-2"></i>Assign Patient
|
||||||
|
</button>
|
||||||
|
{% elif object.status == 'OCCUPIED' %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-warning" onclick="dischargePatient()">
|
||||||
|
<i class="fas fa-sign-out-alt me-2"></i>Discharge Patient
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info" onclick="transferPatient()">
|
||||||
|
<i class="fas fa-exchange-alt me-2"></i>Transfer Patient
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-primary" onclick="scheduleCleaning()">
|
||||||
|
<i class="fas fa-broom me-2"></i>Schedule Cleaning
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="scheduleMaintenance()">
|
||||||
|
<i class="fas fa-tools me-2"></i>Schedule Maintenance
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info" onclick="viewBedChart()">
|
||||||
|
<i class="fas fa-chart-line me-2"></i>View Utilization
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-2">
|
<!-- Bed Statistics -->
|
||||||
<strong>Ward:</strong>
|
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||||
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
<div class="panel-heading">
|
||||||
{{ object.ward.name }}
|
<h4 class="panel-title">
|
||||||
</a>
|
<i class="fas fa-chart-bar me-2"></i>Statistics
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="panel-body">
|
||||||
<strong>Department:</strong> {{ object.ward.department.name }}
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between mb-1">
|
||||||
|
<span>Occupancy Rate (30 days)</span>
|
||||||
|
<span>{{ bed_stats.occupancy_rate|default:0 }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress">
|
||||||
|
<div class="progress-bar bg-primary" style="width: {{ bed_stats.occupancy_rate|default:0 }}%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="border-end">
|
||||||
|
<div class="fs-4 fw-bold text-primary">{{ bed_stats.total_admissions|default:0 }}</div>
|
||||||
|
<div class="small text-muted">Total Admissions</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<div class="fs-4 fw-bold text-success">{{ bed_stats.avg_stay_days|default:0 }}</div>
|
||||||
|
<div class="small text-muted">Avg Stay (days)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
</div>
|
||||||
<strong>Capacity:</strong> {{ object.ward.total_beds }} beds
|
|
||||||
|
<!-- Maintenance Schedule -->
|
||||||
|
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar-check me-2"></i>Maintenance Schedule
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-2">
|
<div class="panel-body">
|
||||||
<strong>Available:</strong> {{ object.ward.available_beds }} beds
|
{% if maintenance_schedule %}
|
||||||
|
{% for maintenance in maintenance_schedule %}
|
||||||
|
<div class="d-flex align-items-center mb-3">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<i class="fas fa-{% if maintenance.type == 'cleaning' %}broom text-primary{% elif maintenance.type == 'inspection' %}search text-info{% else %}tools text-warning{% endif %} fa-lg"></i>
|
||||||
|
</div>
|
||||||
|
<div class="flex-grow-1 ms-3">
|
||||||
|
<div class="fw-bold">{{ maintenance.get_type_display }}</div>
|
||||||
|
<div class="small text-muted">{{ maintenance.scheduled_date|date:"M d, Y g:i A" }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<span class="badge bg-{% if maintenance.status == 'pending' %}warning{% elif maintenance.status == 'completed' %}success{% else %}secondary{% endif %}">
|
||||||
|
{{ maintenance.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center text-muted">
|
||||||
|
<i class="fas fa-calendar-check fa-2x mb-2"></i>
|
||||||
|
<div>No scheduled maintenance</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</div>
|
||||||
<strong>Occupancy:</strong> {{ object.ward.occupancy_rate }}%
|
|
||||||
|
<!-- Ward Information -->
|
||||||
|
<div class="panel panel-inverse" data-sortable-id="index-7">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-hospital me-2"></i>Ward Information
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Ward:</strong>
|
||||||
|
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
||||||
|
{{ object.ward.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Department:</strong> {{ object.ward.department.name }}
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Capacity:</strong> {{ object.ward.total_beds }} beds
|
||||||
|
</div>
|
||||||
|
<div class="mb-2">
|
||||||
|
<strong>Available:</strong> {{ object.ward.available_beds }} beds
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<strong>Occupancy:</strong> {{ object.ward.occupancy_rate|floatformat:0 }}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Update Status Modal -->
|
<!-- Update Status Modal -->
|
||||||
<div class="modal fade" id="updateStatusModal" tabindex="-1">
|
<div class="modal fade" id="updateStatusModal" tabindex="-1">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
|
|||||||
@ -4,21 +4,22 @@
|
|||||||
{% block title %}Bed Management{% endblock %}
|
{% block title %}Bed Management{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="d-flex align-items-center mb-3">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<ol class="breadcrumb">
|
<h1 class="h2">
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
|
<i class="fas fa-list me-2"></i>Bed<span class="fw-light">Management</span>
|
||||||
<li class="breadcrumb-item active">Bed Management</li>
|
</h1>
|
||||||
</ol>
|
<p class="text-muted">Real-time bed occupancy and availability management.</p>
|
||||||
<h1 class="page-header mb-0">Bed Management</h1>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ms-auto">
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="ms-auto">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="{% url 'inpatients:bed_form' %}" class="btn btn-primary">
|
<a href="{% url 'inpatients:bed_create' %}" class="btn btn-primary">
|
||||||
<i class="fas fa-plus me-2"></i>Add Bed
|
<i class="fas fa-plus me-2"></i>Add Bed
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||||
<span class="visually-hidden">Toggle Dropdown</span>
|
<span class="visually-hidden">Toggle Dropdown</span>
|
||||||
|
<i class="fas fa-ellipsis-v"></i>
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><a class="dropdown-item" href="#" onclick="exportBeds()">
|
<li><a class="dropdown-item" href="#" onclick="exportBeds()">
|
||||||
@ -34,93 +35,26 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Statistics Cards -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card bg-primary text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fs-6 text-white-75">Total Beds</div>
|
|
||||||
<div class="fs-2 fw-bold">{{ total_beds|default:0 }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-bed fa-2x text-white-50"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card bg-success text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fs-6 text-white-75">Available</div>
|
|
||||||
<div class="fs-2 fw-bold">{{ available_beds|default:0 }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-check-circle fa-2x text-white-50"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card bg-warning text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fs-6 text-white-75">Occupied</div>
|
|
||||||
<div class="fs-2 fw-bold">{{ occupied_beds|default:0 }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-user fa-2x text-white-50"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-xl-3 col-md-6">
|
|
||||||
<div class="card bg-info text-white">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="flex-grow-1">
|
|
||||||
<div class="fs-6 text-white-75">Occupancy Rate</div>
|
|
||||||
<div class="fs-2 fw-bold">{{ occupancy_rate|default:0 }}%</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<i class="fas fa-chart-pie fa-2x text-white-50"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-bed me-2"></i>
|
<i class="fa fa-bed me-2"></i>Bed List
|
||||||
Bed List
|
</h4>
|
||||||
</h4>
|
<div class="panel-heading-btn">
|
||||||
<div class="card-toolbar">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<div class="d-flex align-items-center">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
<!-- View Toggle -->
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<div class="btn-group btn-group-sm me-3" role="group">
|
<a href="javascript:;" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
<input type="radio" class="btn-check" name="viewType" id="gridView" autocomplete="off" checked>
|
</div>
|
||||||
<label class="btn btn-outline-secondary" for="gridView">
|
</div>
|
||||||
<i class="fas fa-th"></i>
|
<div class="panel-body ">
|
||||||
</label>
|
<div class="d-flex align-items-center mb-3">
|
||||||
<input type="radio" class="btn-check" name="viewType" id="listView" autocomplete="off">
|
|
||||||
<label class="btn btn-outline-secondary" for="listView">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@ -132,127 +66,24 @@
|
|||||||
</select>
|
</select>
|
||||||
<select class="form-select form-select-sm me-2" id="statusFilter" onchange="filterBeds()">
|
<select class="form-select form-select-sm me-2" id="statusFilter" onchange="filterBeds()">
|
||||||
<option value="">All Status</option>
|
<option value="">All Status</option>
|
||||||
<option value="available">Available</option>
|
<option value="AVAILABLE">Available</option>
|
||||||
<option value="occupied">Occupied</option>
|
<option value="OCCUPIED">Occupied</option>
|
||||||
<option value="maintenance">Maintenance</option>
|
<option value="MAINTENANCE">Maintenance</option>
|
||||||
<option value="cleaning">Cleaning</option>
|
<option value="CLEANING">Cleaning</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearFilters()">
|
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearFilters()">
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Grid View -->
|
|
||||||
<div id="gridViewContent">
|
|
||||||
{% if object_list %}
|
|
||||||
<div class="row" id="bedGrid">
|
|
||||||
{% for bed in object_list %}
|
|
||||||
<div class="col-xl-3 col-lg-4 col-md-6 mb-4 bed-item"
|
|
||||||
data-ward="{{ bed.ward.id }}"
|
|
||||||
data-status="{{ bed.status }}"
|
|
||||||
data-bed-type="{{ bed.bed_type }}">
|
|
||||||
<div class="card h-100 bed-card {% if bed.status == 'occupied' %}border-warning{% elif bed.status == 'available' %}border-success{% elif bed.status == 'maintenance' %}border-danger{% else %}border-info{% endif %}">
|
|
||||||
<div class="card-header d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="card-title mb-0">
|
|
||||||
<i class="fas fa-bed me-2"></i>
|
|
||||||
Bed {{ bed.bed_number }}
|
|
||||||
</h6>
|
|
||||||
<span class="badge bg-{% if bed.status == 'available' %}success{% elif bed.status == 'occupied' %}warning{% elif bed.status == 'maintenance' %}danger{% else %}info{% endif %}">
|
|
||||||
{{ bed.get_status_display }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="mb-2">
|
|
||||||
<small class="text-muted">Ward:</small>
|
|
||||||
<div class="fw-bold">{{ bed.ward.name }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<small class="text-muted">Room:</small>
|
|
||||||
<div>{{ bed.room_number|default:"Not assigned" }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-2">
|
|
||||||
<small class="text-muted">Type:</small>
|
|
||||||
<div>{{ bed.get_bed_type_display }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if bed.current_patient %}
|
|
||||||
<div class="mb-2">
|
|
||||||
<small class="text-muted">Patient:</small>
|
|
||||||
<div class="fw-bold text-primary">
|
|
||||||
<a href="{% url 'patients:patient_detail' bed.current_patient.pk %}">
|
|
||||||
{{ bed.current_patient.get_full_name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<small class="text-muted">{{ bed.current_patient.medical_record_number }}</small>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
|
||||||
{% if bed.has_oxygen %}
|
|
||||||
<span class="badge bg-info me-1">O2</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if bed.has_suction %}
|
|
||||||
<span class="badge bg-info me-1">Suction</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if bed.has_monitor %}
|
|
||||||
<span class="badge bg-info me-1">Monitor</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if bed.is_isolation %}
|
|
||||||
<span class="badge bg-warning me-1">Isolation</span>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="card-footer">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a href="{% url 'inpatients:bed_detail' bed.pk %}" class="btn btn-sm btn-outline-primary">
|
|
||||||
<i class="fas fa-eye me-1"></i>View
|
|
||||||
</a>
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href="{% url 'inpatients:bed_form' bed.pk %}" class="btn btn-sm btn-outline-secondary">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
|
||||||
<span class="visually-hidden">Toggle Dropdown</span>
|
|
||||||
</button>
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('{{ bed.pk }}', 'available')">
|
|
||||||
<i class="fas fa-check text-success me-2"></i>Mark Available
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('{{ bed.pk }}', 'maintenance')">
|
|
||||||
<i class="fas fa-tools text-warning me-2"></i>Maintenance
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('{{ bed.pk }}', 'cleaning')">
|
|
||||||
<i class="fas fa-broom text-info me-2"></i>Cleaning
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center text-muted py-5">
|
|
||||||
<i class="fas fa-bed fa-3x mb-3"></i>
|
|
||||||
<h5>No Beds Found</h5>
|
|
||||||
<p>No beds match your current filters. Try adjusting your search criteria.</p>
|
|
||||||
<a href="{% url 'inpatients:bed_form' %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus me-2"></i>Add First Bed
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- List View -->
|
<!-- List View -->
|
||||||
<div id="listViewContent" style="display: none;">
|
<div id="listViewContent">
|
||||||
{% if object_list %}
|
{% if object_list %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive border border-primary-subtle">
|
||||||
<table class="table table-hover" id="bedTable">
|
<table class="table table-hover" id="bedTable">
|
||||||
<thead>
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th>
|
<th>
|
||||||
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
|
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
|
||||||
@ -290,11 +121,11 @@
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if bed.current_patient %}
|
{% if bed.current_admission.patient %}
|
||||||
<a href="{% url 'patients:patient_detail' bed.current_patient.pk %}">
|
<a href="{% url 'patients:patient_detail' bed.current_admission.patient.pk %}">
|
||||||
{{ bed.current_patient.get_full_name }}
|
{{ bed.current_admission.patient.get_full_name }}
|
||||||
</a>
|
</a>
|
||||||
<div class="small text-muted">{{ bed.current_patient.medical_record_number }}</div>
|
<div class="small text-muted">{{ bed.current_admission.patient.mrn }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">-</span>
|
<span class="text-muted">-</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -318,7 +149,7 @@
|
|||||||
<a href="{% url 'inpatients:bed_detail' bed.pk %}" class="btn btn-outline-primary">
|
<a href="{% url 'inpatients:bed_detail' bed.pk %}" class="btn btn-outline-primary">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'inpatients:bed_form' bed.pk %}" class="btn btn-outline-secondary">
|
<a href="{% url 'inpatients:bed_update' bed.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-edit"></i>
|
<i class="fas fa-edit"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-danger" onclick="deleteBed('{{ bed.pk }}')">
|
<button type="button" class="btn btn-outline-danger" onclick="deleteBed('{{ bed.pk }}')">
|
||||||
@ -342,29 +173,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
// View toggle functionality
|
|
||||||
const gridViewRadio = document.getElementById('gridView');
|
|
||||||
const listViewRadio = document.getElementById('listView');
|
|
||||||
const gridViewContent = document.getElementById('gridViewContent');
|
|
||||||
const listViewContent = document.getElementById('listViewContent');
|
|
||||||
|
|
||||||
gridViewRadio.addEventListener('change', function() {
|
|
||||||
if (this.checked) {
|
|
||||||
gridViewContent.style.display = 'block';
|
|
||||||
listViewContent.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
listViewRadio.addEventListener('change', function() {
|
|
||||||
if (this.checked) {
|
|
||||||
gridViewContent.style.display = 'none';
|
|
||||||
listViewContent.style.display = 'block';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function filterBeds() {
|
function filterBeds() {
|
||||||
const wardFilter = document.getElementById('wardFilter').value;
|
const wardFilter = document.getElementById('wardFilter').value;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -171,7 +171,7 @@
|
|||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="planning-header">
|
<div class="planning-header">
|
||||||
<div class="row">
|
<div class="row mb-2">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<h5><i class="fas fa-user"></i> Patient: {{ admission.patient.get_full_name }}</h5>
|
<h5><i class="fas fa-user"></i> Patient: {{ admission.patient.get_full_name }}</h5>
|
||||||
<p><strong>MRN:</strong> {{ admission.patient.mrn }} | <strong>Age:</strong> {{ admission.patient.age }} | <strong>Gender:</strong>
|
<p><strong>MRN:</strong> {{ admission.patient.mrn }} | <strong>Age:</strong> {{ admission.patient.age }} | <strong>Gender:</strong>
|
||||||
@ -189,33 +189,46 @@
|
|||||||
<p class="mt-2 mb-0">Expected Discharge: {{ admission.anticipated_discharge_date}}</p>
|
<p class="mt-2 mb-0">Expected Discharge: {{ admission.anticipated_discharge_date}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-8"></div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<button type="button" class="btn btn-outline-theme btn-sm me-2">
|
||||||
|
<i class="fas fa-check"></i> Approve Discharge Plan
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-warning btn-sm me-2">
|
||||||
|
<i class="fas fa-edit"></i> Modify Plan
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-info btn-sm me-2">
|
||||||
|
<i class="fas fa-share"></i> Share with Team
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger btn-sm">
|
||||||
|
<i class="fas fa-save"></i> Save Draft
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-inverse">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<h4 class="panel-title">
|
|
||||||
<i class="fas fa-clipboard-list me-2"></i>Discharge Plan
|
|
||||||
</h4>
|
|
||||||
<div class="panel-heading-btn">
|
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
|
||||||
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="panel-body">
|
|
||||||
<!-- Main Planning Content -->
|
<!-- Main Planning Content -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<!-- Discharge Timeline -->
|
<!-- Discharge Timeline -->
|
||||||
<div class="panel panel-inverse border border-primary">
|
<div class="panel panel-inverse">
|
||||||
<div class="panel-heading bg-primary">
|
<div class="panel-heading">
|
||||||
<h4 class="panel-title"><i class="fas fa-timeline"></i> Discharge Timeline</h4>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-timeline"></i> Discharge Timeline
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="timeline">
|
<div class="timeline">
|
||||||
{% %}
|
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-time">
|
<div class="timeline-time">
|
||||||
<span class="date">today</span>
|
<span class="date">today</span>
|
||||||
@ -242,11 +255,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Discharge Checklist -->
|
<!-- Discharge Checklist -->
|
||||||
<div class="card mt-3">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-tasks"></i> Discharge Checklist</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-tasks"></i> Discharge Checklist
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="mb-2">
|
<div class="mb-2">
|
||||||
<h6>Medical Clearance</h6>
|
<h6>Medical Clearance</h6>
|
||||||
<div class="checklist-item">
|
<div class="checklist-item">
|
||||||
@ -332,11 +353,19 @@
|
|||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<!-- Risk Assessment -->
|
<!-- Risk Assessment -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-exclamation-triangle"></i> Risk Assessment</h5>
|
<h4 class="panel-title">
|
||||||
</div>
|
<i class="fas fa-exclamation-triangle"></i> Risk Assessment
|
||||||
<div class="card-body">
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="alert-item alert-medium">
|
<div class="alert-item alert-medium">
|
||||||
<strong>Readmission Risk:</strong> Moderate
|
<strong>Readmission Risk:</strong> Moderate
|
||||||
<br><small>Age >65, cardiac condition, multiple medications</small>
|
<br><small>Age >65, cardiac condition, multiple medications</small>
|
||||||
@ -357,11 +386,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Care Team -->
|
<!-- Care Team -->
|
||||||
<div class="card mt-3">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-users"></i> Discharge Care Team</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-users"></i> Discharge Care Team
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="care-team">
|
<div class="care-team">
|
||||||
<div class="team-member">
|
<div class="team-member">
|
||||||
<i class="fas fa-user-md fa-2x text-primary mb-2"></i>
|
<i class="fas fa-user-md fa-2x text-primary mb-2"></i>
|
||||||
@ -388,11 +425,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card mt-3">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-bolt"></i> Quick Actions</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-bolt"></i> Quick Actions
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<button class="btn btn-outline-primary btn-sm">
|
<button class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-calendar"></i> Schedule Follow-up
|
<i class="fas fa-calendar"></i> Schedule Follow-up
|
||||||
@ -415,11 +460,19 @@
|
|||||||
<!-- Discharge Medications -->
|
<!-- Discharge Medications -->
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-pills"></i> Discharge Medications</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-pills"></i> Discharge Medications
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="medication-item">
|
<div class="medication-item">
|
||||||
<h6>Aspirin 81mg</h6>
|
<h6>Aspirin 81mg</h6>
|
||||||
<p>Take 1 tablet daily by mouth</p>
|
<p>Take 1 tablet daily by mouth</p>
|
||||||
@ -445,11 +498,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5><i class="fas fa-calendar-check"></i> Follow-up Appointments</h5>
|
<h4 class="panel-title">
|
||||||
|
<i class="fas fa-calendar-check"></i> Follow-up Appointments
|
||||||
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="appointment-item">
|
<div class="appointment-item">
|
||||||
<h6>Cardiology Follow-up</h6>
|
<h6>Cardiology Follow-up</h6>
|
||||||
<p><strong>Date:</strong> January 30, 2024 at 2:00 PM</p>
|
<p><strong>Date:</strong> January 30, 2024 at 2:00 PM</p>
|
||||||
@ -481,25 +542,13 @@
|
|||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-body text-center">
|
<div class="card-body text-center">
|
||||||
<button type="button" class="btn btn-success btn-lg me-3">
|
|
||||||
<i class="fas fa-check"></i> Approve Discharge Plan
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-warning btn-lg me-3">
|
|
||||||
<i class="fas fa-edit"></i> Modify Plan
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-info btn-lg me-3">
|
|
||||||
<i class="fas fa-share"></i> Share with Team
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-secondary btn-lg">
|
|
||||||
<i class="fas fa-save"></i> Save Draft
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
84
inpatients/templates/inpatients/partials/bed_details.html
Normal file
84
inpatients/templates/inpatients/partials/bed_details.html
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Bed Number:</td>
|
||||||
|
<td>{{ object.bed_number }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Ward:</td>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
||||||
|
{{ object.ward.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Room:</td>
|
||||||
|
<td>{{ object.room_number|default:"Not assigned" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Bed Type:</td>
|
||||||
|
<td>{{ object.get_bed_type_display }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Status:</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-{% if object.status == 'AVAILABLE' %}success{% elif object.status == 'OCCUPIED' %}primary{% elif object.status == 'MAINTENANCE' %}warning{% elif object.status == 'RESERVED' %}purple{% elif object.status == 'BLOCKED' %}red{% else %}info{% endif %}">
|
||||||
|
{{ object.get_status_display }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<table class="table table-borderless">
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Features:</td>
|
||||||
|
<td>
|
||||||
|
{% if object.has_oxygen %}
|
||||||
|
<span class="badge bg-info me-1">Oxygen</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.has_suction %}
|
||||||
|
<span class="badge bg-info me-1">Suction</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.has_monitor %}
|
||||||
|
<span class="badge bg-info me-1">Monitor</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if object.is_isolation %}
|
||||||
|
<span class="badge bg-warning me-1">Isolation</span>
|
||||||
|
{% endif %}
|
||||||
|
{% if not object.has_oxygen and not object.has_suction and not object.has_monitor and not object.is_isolation %}
|
||||||
|
<span class="text-muted">Standard bed</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Last Cleaned:</td>
|
||||||
|
<td>{{ object.last_cleaned|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Last Maintenance:</td>
|
||||||
|
<td>{{ object.last_maintenance|date:"M d, Y g:i A"|default:"Not recorded" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Created:</td>
|
||||||
|
<td>{{ object.created_at|date:"M d, Y g:i A" }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td class="fw-bold">Last Updated:</td>
|
||||||
|
<td>{{ object.updated_at|date:"M d, Y g:i A" }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if object.notes %}
|
||||||
|
<div class="mt-3">
|
||||||
|
<h6>Notes:</h6>
|
||||||
|
<div class="note alert-info">
|
||||||
|
{{ object.notes }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
@ -5,7 +5,7 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form id="updateBedStatusForm" method="post" action="{% url 'inpatients:update_bed_status' bed_id=bed.id %}">
|
<form id="updateBedStatusForm" method="post" action="{% url 'inpatients:update_bed_status' bed.id %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="bed-info mb-3">
|
<div class="bed-info mb-3">
|
||||||
@ -26,9 +26,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if bed.current_patient %}
|
{% if bed.current_admission.patient %}
|
||||||
<div class="alert alert-warning mt-2 mb-0">
|
<div class="alert alert-warning mt-2 mb-0">
|
||||||
<i class="fa fa-exclamation-triangle"></i> This bed is currently occupied by <strong>{{ bed.current_patient.get_full_name }}</strong>.
|
<i class="fa fa-exclamation-triangle"></i> This bed is currently occupied by <strong>{{ bed.current_admission.patient.get_full_name }}</strong>.
|
||||||
Changing the status will affect the patient's admission.
|
Changing the status will affect the patient's admission.
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -4,17 +4,35 @@
|
|||||||
{% block title %}Surgery Schedule - {{ block.super }}{% endblock %}
|
{% block title %}Surgery Schedule - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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-procedures me-2"></i></i>Surgeries<span class="fw-light">Schedule</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">Comprehensive discharge planning and coordination.</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-toolbar mb-2 mb-md-0">
|
||||||
|
<div class="btn-group me-2">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="panel panel-inverse">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-procedures me-2"></i>Surgery Schedule
|
<i class="fas fa-list-dots"></i> List
|
||||||
</h4>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<div class="card-body">
|
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
@ -52,12 +70,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Surgery Schedule Table -->
|
<!-- Surgery Schedule Table -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive border border-primary-subtle">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Date/Time</th>
|
<th>Date/Time</th>
|
||||||
<th>Patient</th>
|
<th>Patient</th>
|
||||||
|
<th>Room</th>
|
||||||
<th>Procedure</th>
|
<th>Procedure</th>
|
||||||
<th>Surgeon</th>
|
<th>Surgeon</th>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
@ -81,6 +100,12 @@
|
|||||||
</small>
|
</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<strong>{{ surgery.operating_room.room_name }}</strong>
|
||||||
|
{% if surgery.operating_room.room_name %}
|
||||||
|
<br><small class="text-muted">Room No.: {{ surgery.operating_room.room_number }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
<strong>{{ surgery.procedure_name }}</strong>
|
<strong>{{ surgery.procedure_name }}</strong>
|
||||||
{% if surgery.procedure_code %}
|
{% if surgery.procedure_code %}
|
||||||
<br><small class="text-muted">Code: {{ surgery.procedure_code }}</small>
|
<br><small class="text-muted">Code: {{ surgery.procedure_code }}</small>
|
||||||
@ -127,7 +152,7 @@
|
|||||||
{% elif surgery.status == 'CONFIRMED' %}
|
{% elif surgery.status == 'CONFIRMED' %}
|
||||||
<button class="btn btn-outline-primary"
|
<button class="btn btn-outline-primary"
|
||||||
title="Start Prep"
|
title="Start Prep"
|
||||||
hx-post="{% url 'inpatients:start_surgery_prep' surgery.id %}"
|
hx-post="{% url 'inpatients:prep_surgery' surgery.id %}"
|
||||||
hx-confirm="Start surgery prep?"
|
hx-confirm="Start surgery prep?"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
|
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
|
||||||
@ -153,18 +178,18 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button class="btn btn-outline-info" title="View Details">
|
<a href="{% url 'inpatients:surgery_detail' surgery.id %}" class="btn btn-outline-info" title="View Details">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
{% if surgery.status not in 'COMPLETED,CANCELLED' %}
|
{% if surgery.status not in 'COMPLETED,CANCELLED' %}
|
||||||
{# <button class="btn btn-outline-warning" #}
|
<button class="btn btn-outline-warning"
|
||||||
{# title="Postpone"#}
|
title="Postpone"
|
||||||
{# hx-post="{% url 'inpatients:s' surgery.id %}"#}
|
hx-post="{% url 'inpatients:postpone_surgery' surgery.id %}"
|
||||||
{# hx-confirm="Postpone this surgery?"#}
|
hx-confirm="Postpone this surgery?"
|
||||||
{# hx-swap="none">#}
|
hx-swap="none">
|
||||||
{# <i class="fas fa-clock"></i>#}
|
<i class="fas fa-clock"></i>
|
||||||
{# </button>#}
|
</button>
|
||||||
<button class="btn btn-outline-danger"
|
<button class="btn btn-outline-danger"
|
||||||
title="Cancel"
|
title="Cancel"
|
||||||
hx-post="{% url 'inpatients:cancel_surgery' surgery.id %}"
|
hx-post="{% url 'inpatients:cancel_surgery' surgery.id %}"
|
||||||
@ -188,38 +213,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
<nav aria-label="Surgery pagination">
|
{% include 'partial/pagination.html' %}
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1">First</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Previous</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<li class="page-item active">
|
|
||||||
<span class="page-link">
|
|
||||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Next</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Last</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title">Transfer Patient</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<form id="transferForm" method="post" action="{% url 'inpatients:transfer_patient' admission.id %}">
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="transferForm" method="post" action="{% url 'inpatients:request_transfer' admission_id=admission.id %}">
|
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="patient-info mb-3">
|
<div class="patient-info mb-3">
|
||||||
@ -103,7 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="submit" form="transferForm" class="btn btn-primary">
|
<button type="submit" form="transferForm" class="btn btn-primary">
|
||||||
|
|||||||
@ -4,17 +4,35 @@
|
|||||||
{% block title %}Transfer Management - {{ block.super }}{% endblock %}
|
{% block title %}Transfer Management - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% 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-exchange-alt me-2"></i> Transfer<span class="fw-light">Management</span>
|
||||||
|
</h1>
|
||||||
|
<p class="text-muted">Manage all transfers</p>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group">
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h4 class="card-title mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Management
|
<i class="fas fa-exchange me-2"></i>Transfer Information
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
<div class="panel-heading-btn">
|
||||||
|
|
||||||
<div class="card-body">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
<!-- Filters -->
|
<!-- Filters -->
|
||||||
<div class="row mb-3">
|
<div class="row mb-3">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
@ -51,9 +69,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Transfers Table -->
|
<!-- Transfers Table -->
|
||||||
<div class="table-responsive">
|
<div class="table-responsive border border-primary-subtle">
|
||||||
<table class="table table-hover">
|
<table class="table table-hover">
|
||||||
<thead>
|
<thead class="table-primary">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Patient</th>
|
<th>Patient</th>
|
||||||
<th>Requested</th>
|
<th>Requested</th>
|
||||||
@ -121,18 +139,23 @@
|
|||||||
<td>
|
<td>
|
||||||
<div class="btn-group btn-group-sm">
|
<div class="btn-group btn-group-sm">
|
||||||
{% if transfer.status == 'REQUESTED' %}
|
{% if transfer.status == 'REQUESTED' %}
|
||||||
<a class="btn btn-outline-success"
|
<button class="btn btn-outline-success"
|
||||||
title="Approve"
|
title="Approve"
|
||||||
href="{% url 'inpatients:approve_transfer' transfer.id %}">
|
hx-post="{% url 'inpatients:approve_transfer' transfer.id %}"
|
||||||
|
hx-confirm="Approve this transfer?"
|
||||||
|
hx-swap="none"
|
||||||
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
</a>
|
</button>
|
||||||
{# <button class="btn btn-outline-danger" #}
|
|
||||||
{# title="Reject"#}
|
<button class="btn btn-outline-danger"
|
||||||
{# hx-post="{% url 'inpatients:reject_transfer' transfer.id %}"#}
|
title="Reject"
|
||||||
{# hx-confirm="Reject this transfer?"#}
|
hx-post="{% url 'inpatients:reject_transfer' transfer.id %}"
|
||||||
{# hx-swap="none">#}
|
hx-confirm="Reject this transfer?"
|
||||||
{# <i class="fas fa-times"></i>#}
|
hx-swap="none"
|
||||||
{# </button>#}
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
{% elif transfer.status == 'APPROVED' or transfer.status == 'SCHEDULED' %}
|
{% elif transfer.status == 'APPROVED' or transfer.status == 'SCHEDULED' %}
|
||||||
{# <button class="btn btn-outline-primary" #}
|
{# <button class="btn btn-outline-primary" #}
|
||||||
{# title="Execute Transfer"#}
|
{# title="Execute Transfer"#}
|
||||||
@ -143,18 +166,19 @@
|
|||||||
{# </button>#}
|
{# </button>#}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<button class="btn btn-outline-info" title="View Details">
|
<a href="{% url 'inpatients:transfer_patient' transfer.admission.id%}" class="btn btn-outline-primary" title="View Details">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</a>
|
||||||
|
|
||||||
{% if transfer.status not in 'COMPLETED,CANCELLED,REJECTED' %}
|
{% if transfer.status not in 'COMPLETED,CANCELLED,REJECTED' %}
|
||||||
{# <button class="btn btn-outline-secondary" #}
|
<button class="btn btn-outline-warning"
|
||||||
{# title="Cancel"#}
|
title="Cancel"
|
||||||
{# hx-post="{% url 'inpatients:cancel_transfer' transfer.id %}"#}
|
hx-post="{% url 'inpatients:cancel_transfer' transfer.id %}"
|
||||||
{# hx-confirm="Cancel this transfer?"#}
|
hx-confirm="Cancel this transfer?"
|
||||||
{# hx-swap="none">#}
|
hx-swap="none"
|
||||||
{# <i class="fas fa-ban"></i>#}
|
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||||
{# </button>#}
|
<i class="fas fa-ban"></i>
|
||||||
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -170,12 +194,11 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
<!-- Pagination -->
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if is_paginated %}
|
{% if is_paginated %}
|
||||||
{% include 'partial/pagination.html'%}
|
{% include 'partial/pagination.html'%}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,20 +4,14 @@
|
|||||||
{% block title %}{{ ward.name }} - Ward Details - {{ block.super }}{% endblock %}
|
{% block title %}{{ ward.name }} - Ward Details - {{ block.super }}{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid">
|
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h3 mb-1">
|
<h1 class="h2">
|
||||||
<i class="fas fa-hospital me-2"></i>{{ ward.name }}
|
<i class="fas fa-hospital me-2"></i>{{ ward.name }}<span class="fw-light">Details</span>
|
||||||
</h1>
|
</h1>
|
||||||
<nav aria-label="breadcrumb">
|
<p class="text-muted">Manage ward aspects</p>
|
||||||
<ol class="breadcrumb mb-0">
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
|
|
||||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:ward_list' %}">Wards</a></li>
|
|
||||||
<li class="breadcrumb-item active">{{ ward.name }}</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
<a href="{% url 'inpatients:ward_list' %}" class="btn btn-outline-secondary">
|
<a href="{% url 'inpatients:ward_list' %}" class="btn btn-outline-secondary">
|
||||||
@ -48,18 +42,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="col-lg-8">
|
<div class="col-lg-8">
|
||||||
<!-- Ward Information -->
|
<!-- Ward Information -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-info-circle me-2"></i>Ward Information
|
<i class="fas fa-info-circle me-2"></i>Ward Information
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
@ -132,35 +133,37 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bed Layout -->
|
<!-- Bed Layout -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<h4 class="panel-title">
|
||||||
<h5 class="mb-0">
|
<i class="fas fa-bed me-2"></i>Bed Layout
|
||||||
<i class="fas fa-bed me-2"></i>Bed Layout
|
</h4>
|
||||||
</h5>
|
<div class="panel-heading-btn">
|
||||||
<div class="btn-group btn-group-sm">
|
|
||||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshBedLayout()">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
</button>
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
<button type="button" class="btn btn-outline-primary" onclick="toggleBedView()">
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
<i class="fas fa-th me-1"></i>Grid View
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div id="bed-layout" class="bed-grid">
|
<div id="bed-layout" class="bed-grid">
|
||||||
{% for bed in ward.beds.all %}
|
{% for bed in ward.beds.all %}
|
||||||
<div class="bed-item bed-{{ bed.status|lower }}" data-bed-id="{{ bed.id }}" onclick="showBedDetails({{ bed.id }})">
|
<div class="bed-item bed-{{ bed.status|lower }}" data-bed-id="{{ bed.id }}" onclick="showBedDetails({{ bed.id }})">
|
||||||
<div class="bed-number">{{ bed.bed_number }}</div>
|
<div class="bed-number">{{ bed.bed_number }}</div>
|
||||||
<div class="bed-status">
|
<div class="bed-status">
|
||||||
<span class="badge bg-{% if bed.status == 'AVAILABLE' %}success{% elif bed.status == 'OCCUPIED' %}primary{% elif bed.status == 'MAINTENANCE' %}warning{% elif bed.status == 'RESERVED' %}info{% else %}secondary{% endif %}">
|
<span class="badge bg-{% if bed.status == 'AVAILABLE' %}success
|
||||||
|
{% elif bed.status == 'OCCUPIED' %}primary
|
||||||
|
{% elif bed.status == 'MAINTENANCE' %}warning
|
||||||
|
{% elif bed.status == 'RESERVED' %}purple
|
||||||
|
{% elif object.status == 'BLOCKED' %}red
|
||||||
|
{% else %}info{% endif %}">
|
||||||
{{ bed.get_status_display }}
|
{{ bed.get_status_display }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% if bed.current_patient %}
|
{% if bed.current_admission.patient %}
|
||||||
<div class="patient-info">
|
<div class="patient-info">
|
||||||
<small>{{ bed.current_patient.get_full_name }}</small>
|
<small>{{ bed.current_admission.patient.get_full_name }}</small>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -179,16 +182,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Current Patients -->
|
<!-- Current Patients -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<h4 class="panel-title">
|
||||||
<h5 class="mb-0">
|
<i class="fas fa-users me-2"></i>Current Patients
|
||||||
<i class="fas fa-users me-2"></i>Current Patients
|
</h4>
|
||||||
</h5>
|
<div class="panel-heading-btn">
|
||||||
<span class="badge bg-primary">{{ current_patients.count }} patients</span>
|
<span class="badge bg-primary me-2">{{ recent_admissions.count }} patients</span>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body p-0">
|
<div class="panel-body">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-hover mb-0">
|
<table class="table table-hover mb-0">
|
||||||
<thead class="table-light">
|
<thead class="table-light">
|
||||||
@ -202,10 +209,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for admission in current_patients %}
|
{% for admission in recent_admissions %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-info">{{ admission.bed.bed_number }}</span>
|
<p class="fw-bold">{{ admission.current_bed.bed_number }}
|
||||||
|
<span class="badge bg-info">{{ admission.current_bed.bed_position }}</span>
|
||||||
|
</p>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
@ -216,17 +225,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="fw-semibold">{{ admission.patient.get_full_name }}</div>
|
<div class="fw-semibold">{{ admission.patient.get_full_name }}</div>
|
||||||
<small class="text-muted">{{ admission.patient.patient_id }}</small>
|
<small class="text-muted">{{ admission.patient.mrn }}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>{{ admission.admission_date|date:"M d, Y" }}</div>
|
<div>{{ admission.admission_datetime|date:"M d, Y" }}</div>
|
||||||
<small class="text-muted">{{ admission.admission_date|timesince }} ago</small>
|
<small class="text-muted">{{ admission.admission_datetime|timesince }} ago</small>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if admission.primary_diagnosis %}
|
{% if admission.admitting_diagnosis %}
|
||||||
{{ admission.primary_diagnosis|truncatechars:30 }}
|
{{ admission.admitting_diagnosis|truncatechars:30 }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">Not specified</span>
|
<span class="text-muted">Not specified</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -244,21 +253,15 @@
|
|||||||
class="btn btn-outline-primary btn-sm" title="View Details">
|
class="btn btn-outline-primary btn-sm" title="View Details">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</a>
|
</a>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm dropdown-toggle dropdown-toggle-split"
|
<a class="btn btn-outline-warning btn-sm" onclick="showTransfer({{ admission.id }})">
|
||||||
data-bs-toggle="dropdown">
|
<i class="fas fa-exchange-alt"></i>
|
||||||
<span class="visually-hidden">Toggle Dropdown</span>
|
</a>
|
||||||
</button>
|
<a class="btn btn-outline-danger btn-sm" href="{% url 'inpatients:discharge_patient' admission.id %}">
|
||||||
<ul class="dropdown-menu">
|
<i class="fas fa-sign-out-alt"></i>
|
||||||
<li><a class="dropdown-item" href="#">
|
</a>
|
||||||
<i class="fas fa-exchange-alt me-2"></i>Transfer
|
<a class="btn btn-outline-info btn-sm" href="#">
|
||||||
</a></li>
|
<i class="fas fa-notes-medical"></i>
|
||||||
<li><a class="dropdown-item" href="#">
|
</a>
|
||||||
<i class="fas fa-sign-out-alt me-2"></i>Discharge
|
|
||||||
</a></li>
|
|
||||||
<li><a class="dropdown-item" href="#">
|
|
||||||
<i class="fas fa-notes-medical me-2"></i>Add Note
|
|
||||||
</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -281,18 +284,24 @@
|
|||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<div class="col-lg-4">
|
<div class="col-lg-4">
|
||||||
<!-- Ward Statistics -->
|
<!-- Ward Statistics -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-chart-bar me-2"></i>Ward Statistics
|
<i class="fas fa-chart-bar me-2"></i>Ward Statistics
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="row text-center">
|
<div class="row text-center">
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<div class="card bg-success bg-gradient text-white">
|
<div class="card bg-success bg-gradient text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="mb-1">{{ ward.available_beds }}</h4>
|
<h4 class="mb-1">{{ available_beds }}</h4>
|
||||||
<small>Available</small>
|
<small>Available</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -300,7 +309,7 @@
|
|||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<div class="card bg-primary bg-gradient text-white">
|
<div class="card bg-primary bg-gradient text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="mb-1">{{ ward.occupied_beds }}</h4>
|
<h4 class="mb-1">{{ occupied_beds }}</h4>
|
||||||
<small>Occupied</small>
|
<small>Occupied</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -308,15 +317,15 @@
|
|||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<div class="card bg-warning bg-gradient text-white">
|
<div class="card bg-warning bg-gradient text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="mb-1">{{ ward.maintenance_beds }}</h4>
|
<h4 class="mb-1">{{ maintenance_beds }}</h4>
|
||||||
<small>Maintenance</small>
|
<small>Maintenance</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="col-6 mb-3">
|
||||||
<div class="card bg-info bg-gradient text-white">
|
<div class="card bg-purple bg-gradient text-white">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4 class="mb-1">{{ ward.reserved_beds }}</h4>
|
<h4 class="mb-1">{{ reserved_beds }}</h4>
|
||||||
<small>Reserved</small>
|
<small>Reserved</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -328,7 +337,7 @@
|
|||||||
<div class="progress mb-2" style="height: 25px;">
|
<div class="progress mb-2" style="height: 25px;">
|
||||||
<div class="progress-bar bg-{% if ward.occupancy_rate > 90 %}danger{% elif ward.occupancy_rate > 75 %}warning{% else %}success{% endif %}"
|
<div class="progress-bar bg-{% if ward.occupancy_rate > 90 %}danger{% elif ward.occupancy_rate > 75 %}warning{% else %}success{% endif %}"
|
||||||
style="width: {{ ward.occupancy_rate }}%">
|
style="width: {{ ward.occupancy_rate }}%">
|
||||||
{{ ward.occupancy_rate|floatformat:1 }}%
|
{{ ward.occupancy_rate|floatformat:0 }}%
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<small class="text-muted">{{ ward.occupied_beds }} of {{ ward.total_beds }} beds occupied</small>
|
<small class="text-muted">{{ ward.occupied_beds }} of {{ ward.total_beds }} beds occupied</small>
|
||||||
@ -337,13 +346,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card mb-4">
|
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="d-grid gap-2">
|
<div class="d-grid gap-2">
|
||||||
<a href="#" class="btn btn-primary">
|
<a href="#" class="btn btn-primary">
|
||||||
<i class="fas fa-user-plus me-2"></i>Admit Patient
|
<i class="fas fa-user-plus me-2"></i>Admit Patient
|
||||||
@ -351,12 +366,12 @@
|
|||||||
<a href="{% url 'inpatients:bed_management' %}?ward={{ ward.id }}" class="btn btn-outline-secondary">
|
<a href="{% url 'inpatients:bed_management' %}?ward={{ ward.id }}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-bed me-2"></i>Manage Beds
|
<i class="fas fa-bed me-2"></i>Manage Beds
|
||||||
</a>
|
</a>
|
||||||
<a href="#" class="btn btn-outline-info">
|
<a href="{% url 'inpatients:transfer_management' %}" class="btn btn-outline-info">
|
||||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Patient
|
<i class="fas fa-exchange-alt me-2"></i>Transfer Management
|
||||||
</a>
|
|
||||||
<a href="#" class="btn btn-outline-success">
|
|
||||||
<i class="fas fa-sign-out-alt me-2"></i>Discharge Patient
|
|
||||||
</a>
|
</a>
|
||||||
|
{# <a href="{% url 'inpatients:discharge_patient' %}" class="btn btn-outline-success">#}
|
||||||
|
{# <i class="fas fa-sign-out-alt me-2"></i>Discharge Management#}
|
||||||
|
{# </a>#}
|
||||||
<a href="#" class="btn btn-outline-warning">
|
<a href="#" class="btn btn-outline-warning">
|
||||||
<i class="fas fa-chart-bar me-2"></i>Ward Report
|
<i class="fas fa-chart-bar me-2"></i>Ward Report
|
||||||
</a>
|
</a>
|
||||||
@ -368,13 +383,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ward Staff -->
|
<!-- Ward Staff -->
|
||||||
<div class="card">
|
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||||
<div class="card-header">
|
<div class="panel-heading">
|
||||||
<h5 class="mb-0">
|
<h4 class="panel-title">
|
||||||
<i class="fas fa-user-nurse me-2"></i>Ward Staff
|
<i class="fas fa-user-nurse me-2"></i>Ward Staff
|
||||||
</h5>
|
</h4>
|
||||||
|
<div class="panel-heading-btn">
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-default" data-toggle="panel-expand"><i class="fa fa-expand"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-success" data-toggle="panel-reload"><i class="fa fa-redo"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-warning" data-toggle="panel-collapse"><i class="fa fa-minus"></i></a>
|
||||||
|
<a href="javascript:" class="btn btn-xs btn-icon btn-danger" data-toggle="panel-remove"><i class="fa fa-times"></i></a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="panel-body">
|
||||||
<div class="list-group list-group-flush">
|
<div class="list-group list-group-flush">
|
||||||
{% if ward.nurse_manager %}
|
{% if ward.nurse_manager %}
|
||||||
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||||
@ -422,6 +443,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="modal fade" id="transferModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Transfer Patient</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="transferContent">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Ward detail functionality
|
// Ward detail functionality
|
||||||
function refreshBedLayout() {
|
function refreshBedLayout() {
|
||||||
@ -436,7 +470,7 @@ function toggleBedView() {
|
|||||||
|
|
||||||
function showBedDetails(bedId) {
|
function showBedDetails(bedId) {
|
||||||
// Load bed details in modal
|
// Load bed details in modal
|
||||||
fetch(`/inpatients/beds/${bedId}/details/`)
|
fetch(`{% url 'inpatients:bed_details' 0%}`.replace('0', bedId))
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => {
|
.then(html => {
|
||||||
document.getElementById('bedDetailsContent').innerHTML = html;
|
document.getElementById('bedDetailsContent').innerHTML = html;
|
||||||
@ -446,6 +480,18 @@ function showBedDetails(bedId) {
|
|||||||
console.error('Error loading bed details:', error);
|
console.error('Error loading bed details:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
function showTransfer(bedId) {
|
||||||
|
// Load bed details in modal
|
||||||
|
fetch(`{% url 'inpatients:transfer_patient' 0%}`.replace('0', bedId))
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(html => {
|
||||||
|
document.getElementById('transferContent').innerHTML = html;
|
||||||
|
new bootstrap.Modal(document.getElementById('transferModal')).show();
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading transfer details:', error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function printWardCensus() {
|
function printWardCensus() {
|
||||||
window.print();
|
window.print();
|
||||||
|
|||||||
@ -83,12 +83,12 @@
|
|||||||
class="btn btn-outline-primary btn-sm">
|
class="btn btn-outline-primary btn-sm">
|
||||||
<i class="fas fa-bed me-1"></i>Beds
|
<i class="fas fa-bed me-1"></i>Beds
|
||||||
</a>
|
</a>
|
||||||
<button class="btn btn-outline-info btn-sm">
|
<a href="{% url 'inpatients:ward_detail' ward.id %}" class="btn btn-outline-info btn-sm">
|
||||||
<i class="fas fa-eye me-1"></i>View
|
<i class="fas fa-eye me-1"></i>View
|
||||||
</button>
|
</a>
|
||||||
<button class="btn btn-outline-secondary btn-sm">
|
<a href="{% url 'inpatients:ward_update' ward.id %}" class="btn btn-outline-secondary btn-sm">
|
||||||
<i class="fas fa-edit me-1"></i>Edit
|
<i class="fas fa-edit me-1"></i>Edit
|
||||||
</button>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -13,6 +13,8 @@ urlpatterns = [
|
|||||||
# wards
|
# wards
|
||||||
path('wards/', views.WardListView.as_view(), name='ward_list'),
|
path('wards/', views.WardListView.as_view(), name='ward_list'),
|
||||||
path('wards/<int:pk>/', views.WardDetailView.as_view(), name='ward_detail'),
|
path('wards/<int:pk>/', views.WardDetailView.as_view(), name='ward_detail'),
|
||||||
|
path('wards/create/', views.WardCreateView.as_view(), name='ward_create'),
|
||||||
|
path('wards/<int:pk>/edit/', views.WardUpdateView.as_view(), name='ward_update'),
|
||||||
path('wards/stats/', views.ward_stats, name='ward_stats'),
|
path('wards/stats/', views.ward_stats, name='ward_stats'),
|
||||||
|
|
||||||
# Admissions
|
# Admissions
|
||||||
@ -30,11 +32,12 @@ urlpatterns = [
|
|||||||
path('transfer/<int:admission_id>/', views.transfer_patient, name='transfer_patient'),
|
path('transfer/<int:admission_id>/', views.transfer_patient, name='transfer_patient'),
|
||||||
path('transfers/<int:transfer_id>/approve/', views.approve_transfer, name='approve_transfer'),
|
path('transfers/<int:transfer_id>/approve/', views.approve_transfer, name='approve_transfer'),
|
||||||
path('transfers/<int:transfer_id>/complete/', views.complete_transfer, name='complete_transfer'),
|
path('transfers/<int:transfer_id>/complete/', views.complete_transfer, name='complete_transfer'),
|
||||||
|
path('transfers/<int:transfer_id>/cancel/', views.cancel_transfer, name='cancel_transfer'),
|
||||||
|
path('transfers/<int:transfer_id>/reject/', views.reject_transfer, name='reject_transfer'),
|
||||||
|
|
||||||
|
# Beds
|
||||||
# Beds
|
|
||||||
path('beds/', views.BedManagementView.as_view(), name='bed_management'),
|
path('beds/', views.BedManagementView.as_view(), name='bed_management'),
|
||||||
path('beds/', views.BedListView.as_view(), name='bed_list'),
|
path('beds/list/', views.BedListView.as_view(), name='bed_list'),
|
||||||
path('beds/create/', views.BedCreateView.as_view(), name='bed_create'),
|
path('beds/create/', views.BedCreateView.as_view(), name='bed_create'),
|
||||||
path('beds/<int:pk>/', views.BedDetailView.as_view(), name='bed_detail'),
|
path('beds/<int:pk>/', views.BedDetailView.as_view(), name='bed_detail'),
|
||||||
path('beds/<int:pk>/edit/', views.BedUpdateView.as_view(), name='bed_update'),
|
path('beds/<int:pk>/edit/', views.BedUpdateView.as_view(), name='bed_update'),
|
||||||
@ -45,7 +48,8 @@ urlpatterns = [
|
|||||||
path('beds/<int:pk>/unblock/', views.unblock_bed, name='unblock_bed'),
|
path('beds/<int:pk>/unblock/', views.unblock_bed, name='unblock_bed'),
|
||||||
path('beds/<int:pk>/maintenance/', views.maintenance_bed, name='maintenance_bed'),
|
path('beds/<int:pk>/maintenance/', views.maintenance_bed, name='maintenance_bed'),
|
||||||
path('beds/grid/', views.bed_grid, name='bed_grid'),
|
path('beds/grid/', views.bed_grid, name='bed_grid'),
|
||||||
path('beds/<int:bed_id>/status/', views.update_bed_status, name='update_bed_status'),
|
path('beds/status/', views.bed_status_board, name='update_bed_status'),
|
||||||
|
path('beds/<int:bed_id>/details/', views.bed_details, name='bed_details'),
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -56,6 +60,10 @@ urlpatterns = [
|
|||||||
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
|
path('surgery/create/', views.SurgeryScheduleCreateView.as_view(), name='surgery_create'),
|
||||||
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
|
path('surgery/calendar/', views.surgery_calendar, name='surgery_calendar'),
|
||||||
path('surgery/<int:pk>/cancel/', views.cancel_surgery, name='cancel_surgery'),
|
path('surgery/<int:pk>/cancel/', views.cancel_surgery, name='cancel_surgery'),
|
||||||
|
path('surgery/<int:pk>/complete/', views.mark_surgery_completed, name='complete_surgery'),
|
||||||
|
path('surgery/<int:pk>/confirm/', views.confirm_surgery, name='confirm_surgery'),
|
||||||
|
path('surgery/<int:pk>/prep/', views.prep_surgery, name='prep_surgery'),
|
||||||
|
path('surgery/<int:pk>/postpone/', views.postpone_surgery, name='postpone_surgery'),
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
Views for inpatients app.
|
Views for inpatients app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from django.conf.locale import te
|
from django.conf.locale import te
|
||||||
from django.shortcuts import render, get_object_or_404, redirect
|
from django.shortcuts import render, get_object_or_404, redirect
|
||||||
from django.contrib.auth.decorators import login_required, permission_required
|
from django.contrib.auth.decorators import login_required, permission_required
|
||||||
@ -172,13 +173,13 @@ class WardListView(LoginRequiredMixin, ListView):
|
|||||||
|
|
||||||
context['buildings'] = Ward.objects.filter(
|
context['buildings'] = Ward.objects.filter(
|
||||||
tenant=tenant
|
tenant=tenant
|
||||||
).exclude(building__isnull=True).exclude(building='').values_list(
|
).values_list(
|
||||||
'building', flat=True
|
'building', flat=True
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
context['floors'] = Ward.objects.filter(
|
context['floors'] = Ward.objects.filter(
|
||||||
tenant=tenant
|
tenant=tenant
|
||||||
).exclude(floor__isnull=True).exclude(floor='').values_list(
|
).values_list(
|
||||||
'floor', flat=True
|
'floor', flat=True
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
@ -215,7 +216,7 @@ class WardDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context['beds'] = Bed.objects.filter(
|
context['beds'] = Bed.objects.filter(
|
||||||
ward=ward
|
ward=ward
|
||||||
).select_related(
|
).select_related(
|
||||||
'current_patient', 'current_admission'
|
'current_admission'
|
||||||
).order_by('room_number', 'bed_number')
|
).order_by('room_number', 'bed_number')
|
||||||
|
|
||||||
# Group beds by room for display
|
# Group beds by room for display
|
||||||
@ -234,6 +235,7 @@ class WardDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context['maintenance_beds'] = context['beds'].filter(
|
context['maintenance_beds'] = context['beds'].filter(
|
||||||
status__in=['MAINTENANCE', 'OUT_OF_ORDER', 'CLEANING']
|
status__in=['MAINTENANCE', 'OUT_OF_ORDER', 'CLEANING']
|
||||||
).count()
|
).count()
|
||||||
|
context['reserved_beds'] = context['beds'].filter(status='RESERVED').count()
|
||||||
|
|
||||||
if context['total_beds'] > 0:
|
if context['total_beds'] > 0:
|
||||||
context['occupancy_rate'] = (context['occupied_beds'] / context['total_beds']) * 100
|
context['occupancy_rate'] = (context['occupied_beds'] / context['total_beds']) * 100
|
||||||
@ -339,7 +341,7 @@ class BedListView(LoginRequiredMixin, ListView):
|
|||||||
"""Filter beds by tenant and search query."""
|
"""Filter beds by tenant and search query."""
|
||||||
queryset = Bed.objects.filter(
|
queryset = Bed.objects.filter(
|
||||||
ward__tenant=self.request.user.tenant
|
ward__tenant=self.request.user.tenant
|
||||||
).select_related('ward', 'current_patient')
|
).select_related('ward', 'current_admission')
|
||||||
|
|
||||||
# Handle search query
|
# Handle search query
|
||||||
search_query = self.request.GET.get('search', '')
|
search_query = self.request.GET.get('search', '')
|
||||||
@ -394,13 +396,13 @@ class BedListView(LoginRequiredMixin, ListView):
|
|||||||
).order_by('name')
|
).order_by('name')
|
||||||
|
|
||||||
# Get bed types for filter dropdown
|
# Get bed types for filter dropdown
|
||||||
context['bed_types'] = Bed.BED_TYPE_CHOICES
|
context['bed_types'] = Bed.BedType.choices
|
||||||
|
|
||||||
# Get room types for filter dropdown
|
# Get room types for filter dropdown
|
||||||
context['room_types'] = Bed.ROOM_TYPE_CHOICES
|
context['room_types'] = Bed.RoomType.choices
|
||||||
|
|
||||||
# Get statuses for filter dropdown
|
# Get statuses for filter dropdown
|
||||||
context['statuses'] = Bed.STATUS_CHOICES
|
context['statuses'] = Bed.BedStatus.choices
|
||||||
|
|
||||||
# Add search query to context
|
# Add search query to context
|
||||||
context['search_query'] = self.request.GET.get('search', '')
|
context['search_query'] = self.request.GET.get('search', '')
|
||||||
@ -426,7 +428,7 @@ class BedDetailView(LoginRequiredMixin, DetailView):
|
|||||||
return Bed.objects.filter(
|
return Bed.objects.filter(
|
||||||
ward__tenant=self.request.user.tenant
|
ward__tenant=self.request.user.tenant
|
||||||
).select_related(
|
).select_related(
|
||||||
'ward', 'current_patient', 'current_admission',
|
'ward', 'current_admission',
|
||||||
'cleaned_by', 'blocked_by', 'created_by'
|
'cleaned_by', 'blocked_by', 'created_by'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -436,13 +438,12 @@ class BedDetailView(LoginRequiredMixin, DetailView):
|
|||||||
|
|
||||||
# Get bed history - admissions that used this bed
|
# Get bed history - admissions that used this bed
|
||||||
context['admission_history'] = Admission.objects.filter(
|
context['admission_history'] = Admission.objects.filter(
|
||||||
Q(current_bed=bed) | Q(current_bed=bed)
|
current_bed=bed).select_related(
|
||||||
).select_related(
|
'patient', 'admitting_physician',
|
||||||
'patient', 'admitting_physician'
|
).order_by('-admission_datetime')
|
||||||
).order_by('-admission_datetime')[:10]
|
|
||||||
|
|
||||||
# Get maintenance history if available
|
|
||||||
# This would require a model to track maintenance events
|
# context['maintenance_history'] = bed.maintenance_history.all().order_by('-scheduled_date')[:10]
|
||||||
|
|
||||||
return context
|
return context
|
||||||
|
|
||||||
@ -562,7 +563,7 @@ class BedManagementView(LoginRequiredMixin, ListView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return queryset.select_related(
|
return queryset.select_related(
|
||||||
'ward', 'current_patient', 'current_admission'
|
'ward', 'current_admission'
|
||||||
).order_by('ward__name', 'room_number', 'bed_number')
|
).order_by('ward__name', 'room_number', 'bed_number')
|
||||||
|
|
||||||
def get_context_data(self, **kwargs):
|
def get_context_data(self, **kwargs):
|
||||||
@ -583,8 +584,8 @@ class BedManagementView(LoginRequiredMixin, ListView):
|
|||||||
)
|
)
|
||||||
context.update({
|
context.update({
|
||||||
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
||||||
'bed_statuses': Bed.STATUS_CHOICES,
|
'bed_statuses': Bed.BedStatus.choices,
|
||||||
'bed_types': Bed.BED_TYPE_CHOICES,
|
'bed_types': Bed.BedType.choices,
|
||||||
'total_beds': Bed.objects.filter(ward__tenant=tenant).count(),
|
'total_beds': Bed.objects.filter(ward__tenant=tenant).count(),
|
||||||
'available_beds': Bed.objects.filter(ward__tenant=tenant, status='AVAILABLE').count(),
|
'available_beds': Bed.objects.filter(ward__tenant=tenant, status='AVAILABLE').count(),
|
||||||
'occupied_beds': Bed.objects.filter(ward__tenant=tenant, status='OCCUPIED').count(),
|
'occupied_beds': Bed.objects.filter(ward__tenant=tenant, status='OCCUPIED').count(),
|
||||||
@ -642,8 +643,8 @@ class AdmissionListView(LoginRequiredMixin, ListView):
|
|||||||
total_admissions = Admission.objects.filter(tenant=tenant).count()
|
total_admissions = Admission.objects.filter(tenant=tenant).count()
|
||||||
active_admissions = Admission.objects.filter(tenant=tenant, status='ADMITTED').count()
|
active_admissions = Admission.objects.filter(tenant=tenant, status='ADMITTED').count()
|
||||||
context.update({
|
context.update({
|
||||||
'admission_statuses': Admission.STATUS_CHOICES,
|
'admission_statuses': Admission.AdmissionStatus.choices,
|
||||||
'admission_types': Admission.ADMISSION_TYPE_CHOICES,
|
'admission_types': Admission.AdmissionType.choices,
|
||||||
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
||||||
'total_admissions': total_admissions,
|
'total_admissions': total_admissions,
|
||||||
'active_admissions': active_admissions,
|
'active_admissions': active_admissions,
|
||||||
@ -880,7 +881,7 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
|||||||
model = SurgerySchedule
|
model = SurgerySchedule
|
||||||
template_name = 'inpatients/surgeries/surgery_schedule.html'
|
template_name = 'inpatients/surgeries/surgery_schedule.html'
|
||||||
context_object_name = 'surgeries'
|
context_object_name = 'surgeries'
|
||||||
paginate_by = 20
|
paginate_by = 5
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""Filter surgeries by tenant and search query."""
|
"""Filter surgeries by tenant and search query."""
|
||||||
@ -959,10 +960,10 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
|||||||
is_active=True,
|
is_active=True,
|
||||||
employee_profile__role__in=['SURGEON', 'PHYSICIAN_ASSISTANT']
|
employee_profile__role__in=['SURGEON', 'PHYSICIAN_ASSISTANT']
|
||||||
).order_by('last_name', 'first_name')
|
).order_by('last_name', 'first_name')
|
||||||
|
from operating_theatre.models import OperatingRoom
|
||||||
# Get operating rooms for filter dropdown
|
# Get operating rooms for filter dropdown
|
||||||
try:
|
try:
|
||||||
from operating_theatre.models import OperatingRoom
|
|
||||||
context['operating_rooms'] = OperatingRoom.objects.filter(
|
context['operating_rooms'] = OperatingRoom.objects.filter(
|
||||||
tenant=tenant,
|
tenant=tenant,
|
||||||
is_active=True
|
is_active=True
|
||||||
@ -1102,11 +1103,11 @@ def bed_grid(request):
|
|||||||
beds = Bed.objects.filter(
|
beds = Bed.objects.filter(
|
||||||
ward_id=ward_id,
|
ward_id=ward_id,
|
||||||
ward__tenant=request.user.tenant
|
ward__tenant=request.user.tenant
|
||||||
).select_related('current_patient', 'current_admission').order_by('room_number', 'bed_number')
|
).select_related( 'current_admission').order_by('room_number', 'bed_number')
|
||||||
else:
|
else:
|
||||||
beds = Bed.objects.filter(
|
beds = Bed.objects.filter(
|
||||||
ward__tenant=request.user.tenant
|
ward__tenant=request.user.tenant
|
||||||
).select_related('ward', 'current_patient', 'current_admission').order_by(
|
).select_related('ward', 'current_admission').order_by(
|
||||||
'ward__name', 'room_number', 'bed_number'
|
'ward__name', 'room_number', 'bed_number'
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1271,7 +1272,7 @@ def transfer_patient(request, admission_id):
|
|||||||
'priorities': Transfer._meta.get_field('priority').choices
|
'priorities': Transfer._meta.get_field('priority').choices
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'inpatients/patient_transfer.html', context)
|
return render(request, 'inpatients/transfers/transfer_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -1279,7 +1280,6 @@ def approve_transfer(request, transfer_id):
|
|||||||
"""
|
"""
|
||||||
HTMX endpoint for approving a transfer.
|
HTMX endpoint for approving a transfer.
|
||||||
"""
|
"""
|
||||||
print("transfer clicked")
|
|
||||||
transfer = get_object_or_404(Transfer, id=transfer_id, admission__tenant=request.user.tenant)
|
transfer = get_object_or_404(Transfer, id=transfer_id, admission__tenant=request.user.tenant)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -1303,7 +1303,7 @@ def approve_transfer(request, transfer_id):
|
|||||||
|
|
||||||
# Log the action
|
# Log the action
|
||||||
AuditLogger.log_event(
|
AuditLogger.log_event(
|
||||||
actor=request.user,
|
user=request.user,
|
||||||
action='TRANSFER_APPROVED',
|
action='TRANSFER_APPROVED',
|
||||||
target=transfer,
|
target=transfer,
|
||||||
target_repr=str(transfer),
|
target_repr=str(transfer),
|
||||||
@ -1321,6 +1321,72 @@ def approve_transfer(request, transfer_id):
|
|||||||
return render(request, 'inpatients/partials/approve_transfer_form.html', context)
|
return render(request, 'inpatients/partials/approve_transfer_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def cancel_transfer(request, transfer_id):
|
||||||
|
"""
|
||||||
|
Cancel a transfer.
|
||||||
|
"""
|
||||||
|
transfer = get_object_or_404(
|
||||||
|
Transfer,
|
||||||
|
pk=transfer_id,
|
||||||
|
admission__tenant=request.user.tenant
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cannot cancel completed or already cancelled transfers
|
||||||
|
if transfer.status in ['COMPLETED', 'CANCELLED']:
|
||||||
|
messages.error(request, _('Cannot cancel a completed or already cancelled transfer'))
|
||||||
|
return redirect('inpatients:transfer_management')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
reason = request.POST.get('reason')
|
||||||
|
|
||||||
|
transfer.status = 'CANCELLED'
|
||||||
|
if reason:
|
||||||
|
transfer.notes = (
|
||||||
|
transfer.notes or "") + f"\n\nCancellation Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
||||||
|
transfer.save()
|
||||||
|
|
||||||
|
messages.success(request, _('Transfer cancelled successfully'))
|
||||||
|
return redirect('inpatients:transfer_management')
|
||||||
|
|
||||||
|
return render(request, 'inpatients/transfers/cancel_transfer.html', {
|
||||||
|
'transfer': transfer
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def reject_transfer(request, transfer_id):
|
||||||
|
"""
|
||||||
|
Cancel a transfer.
|
||||||
|
"""
|
||||||
|
transfer = get_object_or_404(
|
||||||
|
Transfer,
|
||||||
|
pk=transfer_id,
|
||||||
|
admission__tenant=request.user.tenant
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cannot cancel completed or already cancelled transfers
|
||||||
|
if transfer.status in ['COMPLETED', 'CANCELLED']:
|
||||||
|
messages.error(request, _('Cannot cancel a completed or already cancelled transfer'))
|
||||||
|
return redirect('inpatients:transfer_management')
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
reason = request.POST.get('reason')
|
||||||
|
|
||||||
|
transfer.status = 'REJECTED'
|
||||||
|
if reason:
|
||||||
|
transfer.notes = (
|
||||||
|
transfer.notes or "") + f"\n\nRejection Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
||||||
|
transfer.save()
|
||||||
|
|
||||||
|
messages.success(request, _('Transfer rejected successfully'))
|
||||||
|
return redirect('inpatients:transfer_management')
|
||||||
|
|
||||||
|
return render(request, 'inpatients/transfers/cancel_transfer.html', {
|
||||||
|
'transfer': transfer
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def complete_transfer(request, transfer_id):
|
def complete_transfer(request, transfer_id):
|
||||||
"""
|
"""
|
||||||
@ -1517,11 +1583,19 @@ def bed_status_board(request):
|
|||||||
'occupied_beds': beds.filter(status='OCCUPIED').count()
|
'occupied_beds': beds.filter(status='OCCUPIED').count()
|
||||||
})
|
})
|
||||||
|
|
||||||
return render(request, 'inpatients/bed_status_board.html', {
|
return render(request, 'inpatients/partials/update_bed_status_form.html', {
|
||||||
'ward_data': ward_data
|
'ward_data': ward_data
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def bed_details(request, bed_id):
|
||||||
|
tenant = request.user.tenant
|
||||||
|
bed = get_object_or_404(Bed, ward__tenant=tenant,id=bed_id)
|
||||||
|
|
||||||
|
return render(request, 'inpatients/partials/bed_details.html', {'object':bed})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def clean_bed(request, pk):
|
def clean_bed(request, pk):
|
||||||
"""
|
"""
|
||||||
@ -2261,8 +2335,7 @@ class AdmissionDetailView(LoginRequiredMixin, DetailView):
|
|||||||
context['has_discharge_summary'] = hasattr(admission, 'discharge_summary')
|
context['has_discharge_summary'] = hasattr(admission, 'discharge_summary')
|
||||||
|
|
||||||
return context
|
return context
|
||||||
#
|
|
||||||
#
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -2541,38 +2614,7 @@ class TransferUpdateView(LoginRequiredMixin, UpdateView):
|
|||||||
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
|
return reverse('inpatients:transfer_detail', kwargs={'pk': self.object.pk})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
# @permission_required('inpatients.change_transfer')
|
|
||||||
def cancel_transfer(request, pk):
|
|
||||||
"""
|
|
||||||
Cancel a transfer.
|
|
||||||
"""
|
|
||||||
transfer = get_object_or_404(
|
|
||||||
Transfer,
|
|
||||||
pk=pk,
|
|
||||||
admission__tenant=request.user.tenant
|
|
||||||
)
|
|
||||||
|
|
||||||
# Cannot cancel completed or already cancelled transfers
|
|
||||||
if transfer.status in ['COMPLETED', 'CANCELLED']:
|
|
||||||
messages.error(request, _('Cannot cancel a completed or already cancelled transfer'))
|
|
||||||
return redirect('inpatients:transfer_detail', pk=transfer.pk)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
|
||||||
reason = request.POST.get('reason')
|
|
||||||
|
|
||||||
transfer.status = 'CANCELLED'
|
|
||||||
if reason:
|
|
||||||
transfer.notes = (
|
|
||||||
transfer.notes or "") + f"\n\nCancellation Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
|
||||||
transfer.save()
|
|
||||||
|
|
||||||
messages.success(request, _('Transfer cancelled successfully'))
|
|
||||||
return redirect('inpatients:transfer_detail', pk=transfer.pk)
|
|
||||||
|
|
||||||
return render(request, 'inpatients/transfers/cancel_transfer.html', {
|
|
||||||
'transfer': transfer
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -2676,6 +2718,109 @@ def cancel_surgery(request, pk):
|
|||||||
'surgery': surgery
|
'surgery': surgery
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
# @permission_required('inpatients.change_surgeryschedule')
|
||||||
|
def confirm_surgery(request, pk):
|
||||||
|
"""
|
||||||
|
Confirm a surgery.
|
||||||
|
"""
|
||||||
|
surgery = get_object_or_404(
|
||||||
|
SurgerySchedule,
|
||||||
|
pk=pk,
|
||||||
|
admission__tenant=request.user.tenant
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cannot cancel completed or already cancelled surgeries
|
||||||
|
if surgery.status in ['CONFIRMED','IN_PROGRESS' ]:
|
||||||
|
messages.error(request, _('Cannot confirm a confirmed or already in progress surgery'))
|
||||||
|
return redirect('inpatients:surgery_detail', pk=surgery.pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
reason = request.POST.get('reason')
|
||||||
|
|
||||||
|
surgery.status = 'CONFIRMED'
|
||||||
|
# if reason:
|
||||||
|
# surgery.notes = (surgery.notes or "") + f"\n\nCancellation Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
||||||
|
|
||||||
|
surgery.save()
|
||||||
|
|
||||||
|
messages.success(request, _('Surgery confirmed successfully'))
|
||||||
|
return redirect('inpatients:surgery_list')
|
||||||
|
|
||||||
|
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
|
||||||
|
'surgery': surgery
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
# @permission_required('inpatients.change_surgeryschedule')
|
||||||
|
def prep_surgery(request, pk):
|
||||||
|
"""
|
||||||
|
Prep a surgery.
|
||||||
|
"""
|
||||||
|
surgery = get_object_or_404(
|
||||||
|
SurgerySchedule,
|
||||||
|
pk=pk,
|
||||||
|
admission__tenant=request.user.tenant
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cannot cancel completed or already cancelled surgeries
|
||||||
|
if surgery.status in ['PREP','IN_PROGRESS' ]:
|
||||||
|
messages.error(request, _('Cannot prep already in progress surgery'))
|
||||||
|
return redirect('inpatients:surgery_detail', pk=surgery.pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
reason = request.POST.get('reason')
|
||||||
|
|
||||||
|
surgery.status = 'PREP'
|
||||||
|
# if reason:
|
||||||
|
# surgery.notes = (surgery.notes or "") + f"\n\nCancellation Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
||||||
|
|
||||||
|
surgery.save()
|
||||||
|
|
||||||
|
messages.success(request, _('Surgery marked as prep successfully'))
|
||||||
|
return redirect('inpatients:surgery_list')
|
||||||
|
|
||||||
|
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
|
||||||
|
'surgery': surgery
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
# @permission_required('inpatients.change_surgeryschedule')
|
||||||
|
def postpone_surgery(request, pk):
|
||||||
|
"""
|
||||||
|
Postpone a surgery.
|
||||||
|
"""
|
||||||
|
surgery = get_object_or_404(
|
||||||
|
SurgerySchedule,
|
||||||
|
pk=pk,
|
||||||
|
admission__tenant=request.user.tenant
|
||||||
|
)
|
||||||
|
|
||||||
|
# Cannot cancel completed or already cancelled surgeries
|
||||||
|
if surgery.status in ['COMPLETED','CANCELLED' ]:
|
||||||
|
messages.error(request, _('Cannot postpone completed or cancelled surgery'))
|
||||||
|
return redirect('inpatients:surgery_detail', pk=surgery.pk)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
reason = request.POST.get('reason')
|
||||||
|
|
||||||
|
surgery.status = 'POSTPONED'
|
||||||
|
if reason:
|
||||||
|
surgery.notes = (surgery.notes or "") + f"\n\nPostpone Reason ({timezone.now().strftime('%Y-%m-%d %H:%M')}):\n{reason}"
|
||||||
|
|
||||||
|
surgery.save()
|
||||||
|
|
||||||
|
messages.success(request, _('Surgery postponed successfully'))
|
||||||
|
return redirect('inpatients:surgery_list')
|
||||||
|
|
||||||
|
return render(request, 'inpatients/surgeries/surgery_schedule.html', {
|
||||||
|
'surgery': surgery
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def inpatient_stats(request):
|
def inpatient_stats(request):
|
||||||
"""
|
"""
|
||||||
@ -2775,3 +2920,6 @@ def inpatient_stats(request):
|
|||||||
'date_to': date_to
|
'date_to': date_to
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# class DischargeManagement(LoginRequiredMixin, ListView):
|
||||||
|
# model = DischargeSummary
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
51
or_data.py
51
or_data.py
@ -736,47 +736,34 @@ class OperatingTheatreDataGenerator:
|
|||||||
|
|
||||||
# Arabic-influenced room names
|
# Arabic-influenced room names
|
||||||
self.room_names = [
|
self.room_names = [
|
||||||
"غرفة العمليات الأولى", "غرفة العمليات الثانية", "غرفة العمليات الثالثة",
|
"First Operating Room", "Second Operating Room", "Third Operating Room",
|
||||||
"جناح الجراحة العامة", "جناح جراحة القلب", "جناح جراحة الأعصاب",
|
"General Surgery Wing", "Cardiac Surgery Wing", "Neurosurgery Wing",
|
||||||
"وحدة الجراحة الطارئة", "مركز الجراحة الروبوتية", "قسم جراحة العظام"
|
"Emergency Surgery Unit", "Robotic Surgery Center", "Orthopedic Surgery Department"
|
||||||
]
|
]
|
||||||
|
|
||||||
# Common Saudi procedures (Arabic + English)
|
# Common Saudi procedures (Arabic + English)
|
||||||
self.procedures = [
|
self.procedures = [
|
||||||
"استئصال المرارة بالمنظار", "Laparoscopic Cholecystectomy",
|
"Laparoscopic Cholecystectomy","Open Heart Surgery",
|
||||||
"جراحة القلب المفتوح", "Open Heart Surgery",
|
"Total Knee Replacement", "Inguinal Hernia Repair",
|
||||||
"استبدال مفصل الركبة", "Total Knee Replacement",
|
"Appendectomy", "Diagnostic Laparoscopy",
|
||||||
"جراحة الفتق الإربي", "Inguinal Hernia Repair",
|
"Thyroidectomy", "Cataract Surgery",
|
||||||
"استئصال الزائدة الدودية", "Appendectomy",
|
"Tonsillectomy", "Hemorrhoidectomy"
|
||||||
"جراحة المنظار التشخيصي", "Diagnostic Laparoscopy",
|
|
||||||
"استئصال الغدة الدرقية", "Thyroidectomy",
|
|
||||||
"جراحة الساد", "Cataract Surgery",
|
|
||||||
"استئصال اللوزتين", "Tonsillectomy",
|
|
||||||
"جراحة البواسير", "Hemorrhoidectomy"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Saudi medical equipment (common in Saudi hospitals)
|
# Saudi medical equipment (common in Saudi hospitals)
|
||||||
self.equipment = [
|
self.equipment = [
|
||||||
"منظار البطن كارل زايس", "Carl Zeiss Laparoscope",
|
"Carl Zeiss Laparoscope","Electrocautery Unit",
|
||||||
"جهاز القطع الكهربائي", "Electrocautery Unit",
|
"Drager Anesthesia Machine", "Leica Surgical Microscope",
|
||||||
"جهاز التخدير دريجر", "Drager Anesthesia Machine",
|
"da Vinci Surgical Robot", "Portable CT Scanner",
|
||||||
"مجهر جراحي ليكا", "Leica Surgical Microscope",
|
"Cystoscope", "Heart-Lung Machine"
|
||||||
"روبوت دافنشي", "da Vinci Surgical Robot",
|
|
||||||
"جهاز الأشعة المقطعية المحمول", "Portable CT Scanner",
|
|
||||||
"منظار المثانة", "Cystoscope",
|
|
||||||
"مضخة القلب الرئة", "Heart-Lung Machine"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
# Common diagnoses in Arabic and English
|
# Common diagnoses in Arabic and English
|
||||||
self.diagnoses = [
|
self.diagnoses = [
|
||||||
"التهاب المرارة الحاد", "Acute Cholecystitis",
|
"Acute Cholecystitis", "Bowel Obstruction",
|
||||||
"انسداد الأمعاء", "Bowel Obstruction",
|
"Femur Fracture", "Thyroid Nodules",
|
||||||
"كسر في عظم الفخذ", "Femur Fracture",
|
"Kidney Stones", "Appendicitis",
|
||||||
"أورام الغدة الدرقية", "Thyroid Nodules",
|
"Inguinal Hernia", "Colorectal Cancer"
|
||||||
"حصى الكلى", "Kidney Stones",
|
|
||||||
"التهاب الزائدة الدودية", "Appendicitis",
|
|
||||||
"الفتق الإربي", "Inguinal Hernia",
|
|
||||||
"سرطان القولون", "Colorectal Cancer"
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_existing_data(self):
|
def get_existing_data(self):
|
||||||
@ -795,15 +782,15 @@ class OperatingTheatreDataGenerator:
|
|||||||
|
|
||||||
# Try to categorize users by role/specialty
|
# Try to categorize users by role/specialty
|
||||||
surgeons = list(User.objects.filter(
|
surgeons = list(User.objects.filter(
|
||||||
groups__name__icontains='surgeon'
|
groups__name__icontains='SURGEON'
|
||||||
).distinct()) or users[:max(1, len(users) // 3)]
|
).distinct()) or users[:max(1, len(users) // 3)]
|
||||||
|
|
||||||
anesthesiologists = list(User.objects.filter(
|
anesthesiologists = list(User.objects.filter(
|
||||||
groups__name__icontains='anesthesia'
|
groups__name__icontains='ANESTHESIA'
|
||||||
).distinct()) or users[len(users) // 3:2 * len(users) // 3]
|
).distinct()) or users[len(users) // 3:2 * len(users) // 3]
|
||||||
|
|
||||||
nurses = list(User.objects.filter(
|
nurses = list(User.objects.filter(
|
||||||
groups__name__icontains='nurse'
|
groups__name__icontains='NURSE'
|
||||||
).distinct()) or users[2 * len(users) // 3:]
|
).distinct()) or users[2 * len(users) // 3:]
|
||||||
|
|
||||||
# Get patients
|
# Get patients
|
||||||
|
|||||||
Binary file not shown.
@ -224,7 +224,7 @@ class InsuranceInfoAdmin(admin.ModelAdmin):
|
|||||||
('Subscriber Information', {
|
('Subscriber Information', {
|
||||||
'fields': (
|
'fields': (
|
||||||
'subscriber_name', 'subscriber_relationship',
|
'subscriber_name', 'subscriber_relationship',
|
||||||
'subscriber_dob', 'subscriber_ssn'
|
'subscriber_dob',
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
('Coverage', {
|
('Coverage', {
|
||||||
|
|||||||
BIN
static/.DS_Store
vendored
BIN
static/.DS_Store
vendored
Binary file not shown.
BIN
static/css/.DS_Store
vendored
Normal file
BIN
static/css/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/data/.DS_Store
vendored
Normal file
BIN
static/data/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/img/.DS_Store
vendored
BIN
static/img/.DS_Store
vendored
Binary file not shown.
BIN
static/plugins/.DS_Store
vendored
Normal file
BIN
static/plugins/.DS_Store
vendored
Normal file
Binary file not shown.
BIN
static/plugins/ionicons/dist/.DS_Store
vendored
Normal file
BIN
static/plugins/ionicons/dist/.DS_Store
vendored
Normal file
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user