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>
|
||||
<!-- Problems Table -->
|
||||
<div class="card">
|
||||
<div class="card border border-primary-subtle">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover mb-0 align-middle">
|
||||
<thead class="table-primary">
|
||||
<thead class="table-primary ">
|
||||
<tr>
|
||||
<th>Patient</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:
|
||||
model = Room
|
||||
fields = [
|
||||
'floor', 'room_number', 'name', 'room_type', 'area_sqm',
|
||||
'capacity', 'occupancy_status', 'is_accessible', 'current_tenant',
|
||||
'lease_start_date', 'lease_end_date', 'monthly_rent',
|
||||
'has_hvac', 'has_electrical', 'has_plumbing', 'has_internet', 'notes'
|
||||
'floor', 'room_number', 'name', 'area_sqm',
|
||||
'capacity', 'occupancy_status', 'is_accessible', 'notes'
|
||||
]
|
||||
widgets = {
|
||||
'floor': forms.Select(attrs={'class': 'form-select'}),
|
||||
@ -81,26 +79,13 @@ class RoomForm(forms.ModelForm):
|
||||
'room_type': forms.Select(attrs={'class': 'form-select'}),
|
||||
'area_sqm': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
|
||||
'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'}),
|
||||
'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}),
|
||||
}
|
||||
|
||||
def clean(self):
|
||||
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
|
||||
|
||||
@ -132,7 +117,7 @@ class AssetForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Asset
|
||||
fields = [
|
||||
'asset_id', 'name', 'category', 'building', 'floor', 'room',
|
||||
'name', 'category', 'building', 'floor', 'room',
|
||||
'location_description', 'manufacturer', 'model', 'serial_number',
|
||||
'purchase_date', 'purchase_cost', 'current_value', 'depreciation_rate',
|
||||
'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'
|
||||
]
|
||||
widgets = {
|
||||
'asset_id': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'category': forms.Select(attrs={'class': 'form-select'}),
|
||||
'building': forms.Select(attrs={'class': 'form-select'}),
|
||||
@ -271,7 +255,7 @@ class MaintenanceScheduleForm(forms.ModelForm):
|
||||
model = MaintenanceSchedule
|
||||
fields = [
|
||||
'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'
|
||||
]
|
||||
widgets = {
|
||||
@ -322,8 +306,7 @@ class VendorForm(forms.ModelForm):
|
||||
model = Vendor
|
||||
fields = [
|
||||
'name', 'vendor_type', 'contact_person', 'email', 'phone',
|
||||
'address', 'license_number', 'insurance_policy',
|
||||
'insurance_expiry', 'rating', 'is_active'
|
||||
'address', 'crn', 'vrn', 'rating', 'is_active'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
@ -332,9 +315,8 @@ class VendorForm(forms.ModelForm):
|
||||
'email': forms.EmailInput(attrs={'class': 'form-control'}),
|
||||
'phone': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
||||
'license_number': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'insurance_policy': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'insurance_expiry': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
||||
'crn': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'vrn': forms.TextInput(attrs={'class': 'form-control'}),
|
||||
'rating': forms.NumberInput(attrs={'class': 'form-control', 'step': '0.1', 'min': '0', 'max': '5'}),
|
||||
'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
|
||||
occupied_rooms = Room.objects.filter(
|
||||
floor__building=self,
|
||||
occupancy_status='occupied'
|
||||
occupancy_status=Room.OccupancyStatus.OCCUPIED
|
||||
).count()
|
||||
return (occupied_rooms / total_rooms) * 100
|
||||
|
||||
|
||||
@ -268,7 +268,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
{% include 'pagination.html' %}
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
|
||||
@ -11,7 +11,7 @@ urlpatterns = [
|
||||
path('buildings/', views.BuildingListView.as_view(), name='building_list'),
|
||||
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>/edit/', views.BuildingUpdateView.as_view(), name='building_edit'),
|
||||
path('buildings/<int:pk>/edit/', views.BuildingUpdateView.as_view(), name='building_update'),
|
||||
|
||||
# Asset Management
|
||||
path('assets/', views.AssetListView.as_view(), name='asset_list'),
|
||||
|
||||
@ -29,7 +29,7 @@ class FacilityDashboardView(LoginRequiredMixin, TemplateView):
|
||||
context['stats'] = {
|
||||
'total_buildings': Building.objects.filter(is_active=True).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(),
|
||||
'open_work_orders': MaintenanceRequest.objects.filter(status__in=['submitted', 'assigned', 'in_progress']).count(),
|
||||
'completed_today': MaintenanceRequest.objects.filter(completed_date__date=today).count(),
|
||||
@ -90,7 +90,7 @@ class BuildingListView(LoginRequiredMixin, ListView):
|
||||
|
||||
def get_queryset(self):
|
||||
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 = self.request.GET.get('search')
|
||||
@ -107,16 +107,16 @@ class BuildingListView(LoginRequiredMixin, ListView):
|
||||
queryset = queryset.filter(building_type=building_type)
|
||||
|
||||
# Filter by airport
|
||||
airport = self.request.GET.get('airport')
|
||||
if airport:
|
||||
queryset = queryset.filter(airport_code=airport)
|
||||
# airport = self.request.GET.get('airport')
|
||||
# if airport:
|
||||
# queryset = queryset.filter(airport_code=airport)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
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
|
||||
|
||||
|
||||
|
||||
Binary file not shown.
@ -40,6 +40,7 @@ urlpatterns += i18n_patterns(
|
||||
path('blood-bank/', include('blood_bank.urls')),
|
||||
path('patients/', include('patients.urls')),
|
||||
path('appointments/', include('appointments.urls')),
|
||||
path('facility-management/', include('facility_management.urls')),
|
||||
path('inpatients/', include('inpatients.urls')),
|
||||
path('emr/', include('emr.urls')),
|
||||
path('pharmacy/', include('pharmacy.urls')),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -90,72 +90,120 @@
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-users me-2"></i>Human Resources Dashboard
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
||||
<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()">
|
||||
<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>Human<span class="fw-light">Resources</span>
|
||||
</h1>
|
||||
<p class="text-muted">Manage all HR from one place.</p>
|
||||
</div>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshDashboard()">
|
||||
<i class="fas fa-sync-alt me-2"></i>Refresh
|
||||
</button>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<i class="fas fa-cog me-2"></i>Quick Actions
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="{% url 'hr:employee_create' %}">
|
||||
<i class="fas fa-user-plus me-2"></i>Add Employee
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'hr:department_create' %}">
|
||||
<i class="fas fa-building me-2"></i>Add Department
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="{% url 'hr:schedule_create' %}">
|
||||
<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>
|
||||
<a class="btn btn-sm btn-outline-primary" href="{% url 'hr:employee_create' %}">
|
||||
<i class="fas fa-user-plus me-2"></i>Add Employee
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-secondary" href="{% url 'hr:department_create' %}">
|
||||
<i class="fas fa-building me-2"></i>Add Department
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-theme" href="{% url 'hr:schedule_create' %}">
|
||||
<i class="fas fa-calendar-plus me-2"></i>Create Schedule
|
||||
</a>
|
||||
<a class="btn btn-sm btn-outline-warning" href="#">
|
||||
<i class="fas fa-chart-bar me-2"></i>Generate Report
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4" id="hr-stats">
|
||||
{% include "hr/partials/hr_stats.html" %}
|
||||
<div class="row mb-2">
|
||||
<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 class="row">
|
||||
<!-- Recent Activity -->
|
||||
<div class="col-lg-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-clock me-2"></i>Recent Activity
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshActivity()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<a href="#" class="btn btn-outline-primary">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</a>
|
||||
</div>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-clock me-2"></i>Recent Activity
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<button type="button" class="btn btn-xs btn-outline-secondary me-2" onclick="refreshActivity()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<a href="#" class="btn btn-xs btn-outline-primary me-2">
|
||||
<i class="fas fa-list me-1"></i>View All
|
||||
</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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="timeline" id="activity-timeline">
|
||||
<!-- Recent Hires -->
|
||||
{% for employee in recent_hires %}
|
||||
@ -217,13 +265,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Attendance Summary -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
{% include "hr/partials/attendance_summary.html" %}
|
||||
</div>
|
||||
</div>
|
||||
@ -232,13 +286,19 @@
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="panel-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'hr:employee_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus me-2"></i>Add Employee
|
||||
@ -266,13 +326,22 @@
|
||||
</div>
|
||||
|
||||
<!-- Department Overview -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-building me-2"></i>Department Overview
|
||||
</h5>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-building me-2"></i>Department Overview
|
||||
</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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% for dept in departments %}
|
||||
<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">
|
||||
<i class="fas fa-building fa-2x text-muted mb-2"></i>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@ -296,13 +363,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Pending Tasks -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-tasks me-2"></i>Pending Tasks
|
||||
</h5>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-tasks me-2"></i>Pending Tasks
|
||||
</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 class="panel-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% 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">
|
||||
@ -383,51 +456,51 @@ function refreshActivity() {
|
||||
}
|
||||
|
||||
// Clock in/out functionality
|
||||
function clockIn() {
|
||||
fetch('{% url "hr:clock_in" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Clocked in successfully!');
|
||||
refreshDashboard();
|
||||
} else {
|
||||
alert('Error clocking in: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error clocking in');
|
||||
});
|
||||
}
|
||||
|
||||
function clockOut() {
|
||||
fetch('{% url "hr:clock_out" %}', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
alert('Clocked out successfully!');
|
||||
refreshDashboard();
|
||||
} else {
|
||||
alert('Error clocking out: ' + data.message);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
alert('Error clocking out');
|
||||
});
|
||||
}
|
||||
{#function clockIn() {#}
|
||||
{# fetch('{% url "hr:clock_in" employee_id%}', {#}
|
||||
{# method: 'POST',#}
|
||||
{# headers: {#}
|
||||
{# 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,#}
|
||||
{# 'Content-Type': 'application/json',#}
|
||||
{# },#}
|
||||
{# })#}
|
||||
{# .then(response => response.json())#}
|
||||
{# .then(data => {#}
|
||||
{# if (data.success) {#}
|
||||
{# alert('Clocked in successfully!');#}
|
||||
{# refreshDashboard();#}
|
||||
{# } else {#}
|
||||
{# alert('Error clocking in: ' + data.message);#}
|
||||
{# }#}
|
||||
{# })#}
|
||||
{# .catch(error => {#}
|
||||
{# console.error('Error:', error);#}
|
||||
{# alert('Error clocking in');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
{##}
|
||||
{#function clockOut() {#}
|
||||
{# fetch('{% url "hr:clock_out" %}', {#}
|
||||
{# method: 'POST',#}
|
||||
{# headers: {#}
|
||||
{# 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,#}
|
||||
{# 'Content-Type': 'application/json',#}
|
||||
{# },#}
|
||||
{# })#}
|
||||
{# .then(response => response.json())#}
|
||||
{# .then(data => {#}
|
||||
{# if (data.success) {#}
|
||||
{# alert('Clocked out successfully!');#}
|
||||
{# refreshDashboard();#}
|
||||
{# } else {#}
|
||||
{# alert('Error clocking out: ' + data.message);#}
|
||||
{# }#}
|
||||
{# })#}
|
||||
{# .catch(error => {#}
|
||||
{# console.error('Error:', error);#}
|
||||
{# alert('Error clocking out');#}
|
||||
{# });#}
|
||||
{# }#}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
@ -28,235 +28,240 @@
|
||||
{% endblock %}
|
||||
|
||||
{% 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 -->
|
||||
<h1 class="page-header">Department Management <small>view and manage hospital departments</small></h1>
|
||||
<!-- end page-header -->
|
||||
|
||||
<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 -->
|
||||
<div class="container-fluid">
|
||||
<!-- begin row -->
|
||||
<div class="row">
|
||||
<!-- begin col-12 -->
|
||||
<div class="col-xl-12">
|
||||
<!-- begin panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<!-- begin panel-heading -->
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Departments</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>
|
||||
</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
|
||||
<div class="row">
|
||||
<!-- begin col-12 -->
|
||||
<div class="col-xl-12">
|
||||
<!-- begin panel -->
|
||||
<div class="panel panel-inverse">
|
||||
<!-- begin panel-heading -->
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">Departments</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"></i> Add Department
|
||||
</a>
|
||||
<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>
|
||||
</div>
|
||||
<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 class="d-flex">
|
||||
<form method="get" class="me-2">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search departments..." name="search" value="{{ search }}">
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</div>
|
||||
<!-- end panel-heading -->
|
||||
|
||||
<!-- begin panel-body -->
|
||||
<div class="panel-body">
|
||||
<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>
|
||||
</div>
|
||||
</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 class="d-flex">
|
||||
<form method="get" class="me-2">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" placeholder="Search departments..." name="search" value="{{ search }}">
|
||||
<button class="btn btn-outline-secondary" type="submit">
|
||||
<i class="fas fa-search"></i>
|
||||
</button>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- Table View -->
|
||||
<div id="table-view">
|
||||
<table id="departments-table" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Department Name</th>
|
||||
<th>Code</th>
|
||||
<th>Type</th>
|
||||
<th>Head</th>
|
||||
<th>Employees</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for department in departments %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ department.name }}</strong>
|
||||
{% if department.parent_department %}
|
||||
<small class="d-block text-muted">
|
||||
<i class="fas fa-level-up-alt fa-flip-horizontal"></i> {{ department.parent_department.name }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ department.department_code }}</td>
|
||||
<td>{{ department.get_department_type_display }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ department.employee_count }}</span>
|
||||
</td>
|
||||
<td>
|
||||
|
||||
<!-- Table View -->
|
||||
<div id="table-view">
|
||||
<table id="departments-table" class="table table-striped table-bordered align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Department Name</th>
|
||||
<th>Code</th>
|
||||
<th>Type</th>
|
||||
<th>Head</th>
|
||||
<th>Employees</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for department in departments %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ department.name }}</strong>
|
||||
{% if department.parent_department %}
|
||||
<small class="d-block text-muted">
|
||||
<i class="fas fa-level-up-alt fa-flip-horizontal"></i> {{ department.parent_department.name }}
|
||||
</small>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ department.department_code }}</td>
|
||||
<td>{{ department.get_department_type_display }}</td>
|
||||
<td>
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-primary">{{ department.employee_count }}</span>
|
||||
</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 %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
<i class="fas fa-trash"></i> Delete
|
||||
</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 %}
|
||||
<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>
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
No departments found.
|
||||
{% empty %}
|
||||
<div class="col-12">
|
||||
<div class="alert alert-info">
|
||||
No departments found.
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Pagination for Card View -->
|
||||
{% if is_paginated %}
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
<!-- Pagination for Card View -->
|
||||
{% if is_paginated %}
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- end panel-body -->
|
||||
</div>
|
||||
<!-- end panel-body -->
|
||||
<!-- end panel -->
|
||||
</div>
|
||||
<!-- end panel -->
|
||||
<!-- end col-12 -->
|
||||
</div>
|
||||
<!-- end col-12 -->
|
||||
</div>
|
||||
<!-- end row -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block js %}
|
||||
|
||||
@ -11,13 +11,14 @@
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-user me-2"></i>{{ employee.get_full_name }}
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:dashboard' %}">HR</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'hr:employee_list' %}">Employees</a></li>
|
||||
<li class="breadcrumb-item active">{{ employee.get_full_name }}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
id="clock-controls-{{ employee.id }}"
|
||||
hx-get="{% url 'hr:clock_controls' employee.id %}"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML">
|
||||
<!-- Optional skeleton while loading -->
|
||||
<button class="btn btn-outline-secondary" disabled>Loading…</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'hr:employee_list' %}" class="btn btn-outline-secondary">
|
||||
@ -60,15 +61,16 @@
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fa fa-user-alt me-2 "></i>Employee Details</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-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="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="card border border-primary mb-4">
|
||||
<div class="card-header bg-gradient bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-id-card me-2"></i>Employee Information
|
||||
</h5>
|
||||
@ -135,8 +137,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Contact Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="card border border-success mb-4">
|
||||
<div class="card-header bg-gradient bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-address-book me-2"></i>Contact Information
|
||||
</h5>
|
||||
@ -195,9 +197,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Emergency Contact -->
|
||||
{% if employee.emergency_contact_name %}
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
|
||||
<div class="card border border-danger mb-4">
|
||||
<div class="card-header bg-gradient bg-danger text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>Emergency Contact
|
||||
</h5>
|
||||
@ -228,11 +230,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card border border-warning mb-4">
|
||||
<div class="card-header bg-gradient bg-warning">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-history me-2"></i>Recent Activity
|
||||
</h5>
|
||||
@ -285,13 +287,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Stats -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-chart-bar me-2"></i>Quick Stats
|
||||
</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-chart-bar me-2"></i>Quick Stats
|
||||
</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 class="card-body">
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<span class="text-muted">Years of Service</span>
|
||||
<span class="badge bg-primary">{{ employee.years_of_service|floatformat:1 }}</span>
|
||||
@ -312,14 +321,31 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-bolt me-2"></i>Quick Actions
|
||||
</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-bolt me-2"></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 class="card-body">
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<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">
|
||||
<i class="fas fa-clock me-2"></i>View Schedule
|
||||
</a>
|
||||
|
||||
@ -74,7 +74,6 @@
|
||||
{{ form.father_name.errors.0 }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">Name the employee prefers to be called</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
|
||||
@ -4,112 +4,54 @@
|
||||
{% block title %}Employee Management - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<!-- Page Header -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-users me-2"></i>Employee Management
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<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="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>Employee<span class="fw-light">Management</span>
|
||||
</h1>
|
||||
<p class="text-muted">Review employees and add new ones.</p>
|
||||
</div>
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown">
|
||||
<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">
|
||||
<a href="{% url 'hr:employee_create' %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-plus me-2"></i>Add Employee
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics Cards -->
|
||||
<div class="row mb-4">
|
||||
<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">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 class="btn-group">
|
||||
<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
|
||||
</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>
|
||||
</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 -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="card border border-success mb-4">
|
||||
<div class="card-header bg-success text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-filter me-2"></i>Filters
|
||||
</h5>
|
||||
@ -165,8 +107,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Employees List -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="card border border-primary">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-list me-2"></i>Employees
|
||||
@ -175,10 +117,10 @@
|
||||
{% endif %}
|
||||
</h5>
|
||||
<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
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
@ -247,7 +189,7 @@
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="font-monospace">{{ employee.employee_number }}</span>
|
||||
<span class="fw-bold">{{ employee.employee_id }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if employee.department %}
|
||||
@ -326,7 +268,8 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
// Employee list functionality
|
||||
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>
|
||||
</td>
|
||||
<td>{{ entry.date|date:"M d, Y" }}</td>
|
||||
<td>{{ entry.start_time|time:"H:i" }} - {{ entry.end_time|time:"H:i" }}</td>
|
||||
<td>{{ entry.hours }} hrs</td>
|
||||
<td>{{ entry.work_date|date:"M d, Y" }}</td>
|
||||
<td>{{ entry.clock_in_time|time:"H:i" }} - {{ entry.clock_out_time|time:"H:i" }}</td>
|
||||
<td>{{ entry.regular_hours }} hrs</td>
|
||||
<td>
|
||||
{% if entry.is_overtime %}
|
||||
<span class="badge bg-danger">Overtime</span>
|
||||
|
||||
@ -94,8 +94,9 @@ urlpatterns = [
|
||||
# ============================================================================
|
||||
# ACTION URLS FOR WORKFLOW OPERATIONS
|
||||
# ============================================================================
|
||||
path('clock-in/', views.clock_in, name='clock_in'),
|
||||
path('clock-out/', views.clock_out, name='clock_out'),
|
||||
path('employees/<int:employee_id>/clock-controls/', views.clock_controls, name='clock_controls'),
|
||||
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('schedules/<int:schedule_id>/publish/', views.publish_schedule, name='publish_schedule'),
|
||||
|
||||
|
||||
701
hr/views.py
701
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.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 import messages
|
||||
from django.views.generic import (
|
||||
@ -29,6 +31,7 @@ from .forms import (
|
||||
from core.utils import AuditLogger
|
||||
|
||||
|
||||
|
||||
class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||
"""
|
||||
Main HR dashboard with comprehensive statistics and recent activity.
|
||||
@ -45,7 +48,7 @@ class HRDashboardView(LoginRequiredMixin, TemplateView):
|
||||
tenant=self.request.user.tenant,
|
||||
employment_status='ACTIVE'
|
||||
).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(
|
||||
employee__tenant=self.request.user.tenant,
|
||||
status='PENDING'
|
||||
@ -1098,103 +1101,193 @@ def attendance_summary(request):
|
||||
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:
|
||||
employee = Employee.objects.get(
|
||||
id=employee_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Check if already clocked in today
|
||||
today = timezone.now().date()
|
||||
existing_entry = TimeEntry.objects.filter(
|
||||
employee=employee,
|
||||
work_date=today,
|
||||
clock_out_time__isnull=True
|
||||
).first()
|
||||
|
||||
if existing_entry:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'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.'})
|
||||
|
||||
@require_GET
|
||||
def clock_controls(request, employee_id):
|
||||
"""Return the clock controls partial for today's state (HTMX GET)."""
|
||||
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||
today = timezone.localdate()
|
||||
|
||||
# Prefer the open entry for today; otherwise the last entry today (finished)
|
||||
time_entry = (
|
||||
TimeEntry.objects
|
||||
.filter(employee=employee, work_date=today)
|
||||
.order_by('clock_out_time', '-clock_in_time') # open first (clock_out_time NULL), else latest finished
|
||||
.first()
|
||||
)
|
||||
|
||||
return render(request, 'hr/partials/clock_controls.html', {
|
||||
'employee': employee,
|
||||
'time_entry': time_entry,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def clock_out(request):
|
||||
"""
|
||||
Clock out an employee.
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
employee_id = request.POST.get('employee_id')
|
||||
|
||||
try:
|
||||
employee = Employee.objects.get(
|
||||
id=employee_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Find active time 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:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'No active clock-in found for this employee.'
|
||||
})
|
||||
|
||||
# Update time entry
|
||||
time_entry.clock_out_time = timezone.now()
|
||||
time_entry.save() # This will trigger the save method to calculate hours
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'{employee.get_full_name()} clocked out successfully.',
|
||||
'time_entry_id': time_entry.id,
|
||||
'hours_worked': float(time_entry.total_hours)
|
||||
})
|
||||
|
||||
except Employee.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'message': 'Employee not found.'
|
||||
})
|
||||
|
||||
return JsonResponse({'success': False, 'message': 'Invalid request.'})
|
||||
def _render_controls(request, employee, time_entry):
|
||||
html = render_to_string('hr/partials/clock_controls.html',
|
||||
{'employee': employee, 'time_entry': time_entry},
|
||||
request=request)
|
||||
return HttpResponse(html)
|
||||
|
||||
@require_POST
|
||||
def clock_in(request, employee_id):
|
||||
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||
today = timezone.localdate()
|
||||
|
||||
open_entry = TimeEntry.objects.filter(
|
||||
employee=employee, work_date=today, clock_out_time__isnull=True
|
||||
).first()
|
||||
if open_entry:
|
||||
return _render_controls(request, employee, open_entry)
|
||||
|
||||
time_entry = TimeEntry.objects.create(
|
||||
employee=employee, work_date=today, clock_in_time=timezone.now(), status='DRAFT'
|
||||
)
|
||||
|
||||
if request.headers.get('HX-Request'):
|
||||
return _render_controls(request, employee, time_entry)
|
||||
return JsonResponse({'success': True, 'time_entry_id': time_entry.id})
|
||||
|
||||
@require_POST
|
||||
def clock_out(request, employee_id):
|
||||
employee = get_object_or_404(Employee, id=employee_id, tenant=request.user.tenant)
|
||||
today = timezone.localdate()
|
||||
|
||||
time_entry = TimeEntry.objects.filter(
|
||||
employee=employee, work_date=today, clock_out_time__isnull=True
|
||||
).first()
|
||||
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)
|
||||
|
||||
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
|
||||
@ -2644,111 +2737,111 @@ def assign_department_head(request, pk):
|
||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def mark_time_entry_paid(request, pk):
|
||||
# """
|
||||
# Mark a time entry as paid.
|
||||
# """
|
||||
# time_entry = get_object_or_404(
|
||||
# TimeEntry,
|
||||
# pk=pk,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# if request.method == 'POST':
|
||||
# if not request.user.has_perm('hr.change_timeentry'):
|
||||
# messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||
#
|
||||
# time_entry.is_paid = True
|
||||
# time_entry.payment_date = timezone.now().date()
|
||||
# time_entry.save()
|
||||
#
|
||||
# messages.success(
|
||||
# request,
|
||||
# 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}))
|
||||
# return redirect(redirect_url)
|
||||
#
|
||||
# return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def bulk_approve_time_entries(request):
|
||||
# """
|
||||
# Bulk approve time entries.
|
||||
# """
|
||||
# if not request.user.has_perm('hr.approve_timeentry'):
|
||||
# messages.error(request, "You don't have permission to approve time entries.")
|
||||
# return redirect('hr:time_entry_list')
|
||||
#
|
||||
# if request.method == 'POST':
|
||||
# entry_ids = request.POST.getlist('entry_ids')
|
||||
#
|
||||
# if not entry_ids:
|
||||
# messages.warning(request, "No time entries selected for approval.")
|
||||
# return redirect('hr:time_entry_list')
|
||||
#
|
||||
# # Get entries that belong to this tenant
|
||||
# entries = TimeEntry.objects.filter(
|
||||
# tenant=request.user.tenant,
|
||||
# pk__in=entry_ids,
|
||||
# is_approved=False
|
||||
# )
|
||||
#
|
||||
# # Update all entries
|
||||
# updated_count = entries.update(
|
||||
# is_approved=True,
|
||||
# approved_by=request.user,
|
||||
# approval_date=timezone.now()
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f"{updated_count} time entries approved successfully.")
|
||||
#
|
||||
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||
# return redirect(redirect_url)
|
||||
#
|
||||
# return redirect('hr:time_entry_list')
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def bulk_mark_time_entries_paid(request):
|
||||
# """
|
||||
# Bulk mark time entries as paid.
|
||||
# """
|
||||
# if not request.user.has_perm('hr.change_timeentry'):
|
||||
# messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||
# return redirect('hr:time_entry_list')
|
||||
#
|
||||
# if request.method == 'POST':
|
||||
# entry_ids = request.POST.getlist('entry_ids')
|
||||
#
|
||||
# if not entry_ids:
|
||||
# messages.warning(request, "No time entries selected for payment.")
|
||||
# return redirect('hr:time_entry_list')
|
||||
#
|
||||
# # Get entries that belong to this tenant
|
||||
# entries = TimeEntry.objects.filter(
|
||||
# tenant=request.user.tenant,
|
||||
# pk__in=entry_ids,
|
||||
# is_approved=True,
|
||||
# is_paid=False
|
||||
# )
|
||||
#
|
||||
# # Update all entries
|
||||
# updated_count = entries.update(
|
||||
# is_paid=True,
|
||||
# payment_date=timezone.now().date()
|
||||
# )
|
||||
#
|
||||
# messages.success(request, f"{updated_count} time entries marked as paid successfully.")
|
||||
#
|
||||
# redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||
# return redirect(redirect_url)
|
||||
#
|
||||
# return redirect('hr:time_entry_list')
|
||||
@login_required
|
||||
def mark_time_entry_paid(request, pk):
|
||||
"""
|
||||
Mark a time entry as paid.
|
||||
"""
|
||||
time_entry = get_object_or_404(
|
||||
TimeEntry,
|
||||
pk=pk,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not request.user.has_perm('hr.change_timeentry'):
|
||||
messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||
return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||
|
||||
time_entry.is_paid = True
|
||||
time_entry.payment_date = timezone.now().date()
|
||||
time_entry.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
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}))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect('hr:time_entry_detail', pk=time_entry.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def bulk_approve_time_entries(request):
|
||||
"""
|
||||
Bulk approve time entries.
|
||||
"""
|
||||
if not request.user.has_perm('hr.approve_timeentry'):
|
||||
messages.error(request, "You don't have permission to approve time entries.")
|
||||
return redirect('hr:time_entry_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
entry_ids = request.POST.getlist('entry_ids')
|
||||
|
||||
if not entry_ids:
|
||||
messages.warning(request, "No time entries selected for approval.")
|
||||
return redirect('hr:time_entry_list')
|
||||
|
||||
# Get entries that belong to this tenant
|
||||
entries = TimeEntry.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
pk__in=entry_ids,
|
||||
is_approved=False
|
||||
)
|
||||
|
||||
# Update all entries
|
||||
updated_count = entries.update(
|
||||
is_approved=True,
|
||||
approved_by=request.user,
|
||||
approval_date=timezone.now()
|
||||
)
|
||||
|
||||
messages.success(request, f"{updated_count} time entries approved successfully.")
|
||||
|
||||
redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect('hr:time_entry_list')
|
||||
|
||||
|
||||
@login_required
|
||||
def bulk_mark_time_entries_paid(request):
|
||||
"""
|
||||
Bulk mark time entries as paid.
|
||||
"""
|
||||
if not request.user.has_perm('hr.change_timeentry'):
|
||||
messages.error(request, "You don't have permission to mark time entries as paid.")
|
||||
return redirect('hr:time_entry_list')
|
||||
|
||||
if request.method == 'POST':
|
||||
entry_ids = request.POST.getlist('entry_ids')
|
||||
|
||||
if not entry_ids:
|
||||
messages.warning(request, "No time entries selected for payment.")
|
||||
return redirect('hr:time_entry_list')
|
||||
|
||||
# Get entries that belong to this tenant
|
||||
entries = TimeEntry.objects.filter(
|
||||
tenant=request.user.tenant,
|
||||
pk__in=entry_ids,
|
||||
is_approved=True,
|
||||
is_paid=False
|
||||
)
|
||||
|
||||
# Update all entries
|
||||
updated_count = entries.update(
|
||||
is_paid=True,
|
||||
payment_date=timezone.now().date()
|
||||
)
|
||||
|
||||
messages.success(request, f"{updated_count} time entries marked as paid successfully.")
|
||||
|
||||
redirect_url = request.POST.get('next', reverse('hr:time_entry_list'))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect('hr:time_entry_list')
|
||||
#
|
||||
#
|
||||
# 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})
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def acknowledge_review(request, pk):
|
||||
# """
|
||||
# Acknowledge a performance review.
|
||||
# """
|
||||
# review = get_object_or_404(
|
||||
# PerformanceReview,
|
||||
# pk=pk,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# # Check if the user is the employee being reviewed
|
||||
# if hasattr(request.user, 'employee_profile') and request.user.employee_profile == review.employee:
|
||||
# if request.method == 'POST':
|
||||
# review.is_acknowledged = True
|
||||
# review.acknowledgment_date = timezone.now()
|
||||
# review.save()
|
||||
#
|
||||
# messages.success(request, "You have acknowledged this performance review.")
|
||||
#
|
||||
# redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
||||
# return redirect(redirect_url)
|
||||
# else:
|
||||
# messages.error(request, "Only the employee being reviewed can acknowledge this review.")
|
||||
#
|
||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def complete_review(request, pk):
|
||||
# """
|
||||
# Mark a performance review as completed.
|
||||
# """
|
||||
# review = get_object_or_404(
|
||||
# PerformanceReview,
|
||||
# pk=pk,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# if request.method == 'POST':
|
||||
# if not request.user.has_perm('hr.change_performancereview'):
|
||||
# messages.error(request, "You don't have permission to complete reviews.")
|
||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
#
|
||||
# # Check if reviewer is the one completing the review
|
||||
# if review.reviewer != request.user:
|
||||
# messages.warning(request, "Only the assigned reviewer should complete this review.")
|
||||
#
|
||||
# # Update review status
|
||||
# review.status = 'COMPLETED'
|
||||
# review.completion_date = timezone.now().date()
|
||||
# review.save()
|
||||
#
|
||||
# # Update employee's last and next review dates
|
||||
# employee = review.employee
|
||||
# employee.last_review_date = review.completion_date
|
||||
# employee.next_review_date = review.next_review_date
|
||||
# employee.save()
|
||||
#
|
||||
# messages.success(
|
||||
# request,
|
||||
# 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}))
|
||||
# return redirect(redirect_url)
|
||||
#
|
||||
# return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
#
|
||||
#
|
||||
@login_required
|
||||
def acknowledge_review(request, pk):
|
||||
"""
|
||||
Acknowledge a performance review.
|
||||
"""
|
||||
review = get_object_or_404(
|
||||
PerformanceReview,
|
||||
pk=pk,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Check if the user is the employee being reviewed
|
||||
if hasattr(request.user, 'employee_profile') and request.user.employee_profile == review.employee:
|
||||
if request.method == 'POST':
|
||||
review.is_acknowledged = True
|
||||
review.acknowledgment_date = timezone.now()
|
||||
review.save()
|
||||
|
||||
messages.success(request, "You have acknowledged this performance review.")
|
||||
|
||||
redirect_url = request.POST.get('next', reverse('hr:performance_review_detail', kwargs={'pk': review.pk}))
|
||||
return redirect(redirect_url)
|
||||
else:
|
||||
messages.error(request, "Only the employee being reviewed can acknowledge this review.")
|
||||
|
||||
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
|
||||
|
||||
@login_required
|
||||
def complete_review(request, pk):
|
||||
"""
|
||||
Mark a performance review as completed.
|
||||
"""
|
||||
review = get_object_or_404(
|
||||
PerformanceReview,
|
||||
pk=pk,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
if request.method == 'POST':
|
||||
if not request.user.has_perm('hr.change_performancereview'):
|
||||
messages.error(request, "You don't have permission to complete reviews.")
|
||||
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
|
||||
# Check if reviewer is the one completing the review
|
||||
if review.reviewer != request.user:
|
||||
messages.warning(request, "Only the assigned reviewer should complete this review.")
|
||||
|
||||
# Update review status
|
||||
review.status = 'COMPLETED'
|
||||
review.completion_date = timezone.now().date()
|
||||
review.save()
|
||||
|
||||
# Update employee's last and next review dates
|
||||
employee = review.employee
|
||||
employee.last_review_date = review.completion_date
|
||||
employee.next_review_date = review.next_review_date
|
||||
employee.save()
|
||||
|
||||
messages.success(
|
||||
request,
|
||||
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}))
|
||||
return redirect(redirect_url)
|
||||
|
||||
return redirect('hr:performance_review_detail', pk=review.pk)
|
||||
|
||||
|
||||
# class TrainingRecordListView(LoginRequiredMixin, PermissionRequiredMixin, ListView):
|
||||
# """
|
||||
# List view for training records.
|
||||
@ -3349,36 +3442,36 @@ def assign_department_head(request, pk):
|
||||
# return redirect('hr:dashboard')
|
||||
#
|
||||
#
|
||||
# @login_required
|
||||
# def employee_schedule(request, employee_id):
|
||||
# """
|
||||
# View an employee's schedule.
|
||||
# """
|
||||
# employee = get_object_or_404(
|
||||
# Employee,
|
||||
# pk=employee_id,
|
||||
# tenant=request.user.tenant
|
||||
# )
|
||||
#
|
||||
# # Get current schedule
|
||||
# current_schedule = Schedule.objects.filter(
|
||||
# employee=employee,
|
||||
# is_current=True,
|
||||
# is_active=True
|
||||
# ).first()
|
||||
#
|
||||
# # Get upcoming assignments
|
||||
# today = timezone.now().date()
|
||||
# upcoming_assignments = ScheduleAssignment.objects.filter(
|
||||
# schedule__employee=employee,
|
||||
# assignment_date__gte=today
|
||||
# ).order_by('assignment_date', 'start_time')[:14] # Next 2 weeks
|
||||
#
|
||||
# context = {
|
||||
# 'employee': employee,
|
||||
# 'current_schedule': current_schedule,
|
||||
# 'upcoming_assignments': upcoming_assignments,
|
||||
# }
|
||||
#
|
||||
# return render(request, 'hr/employee_schedule.html', context)
|
||||
#
|
||||
@login_required
|
||||
def employee_schedule(request, employee_id):
|
||||
"""
|
||||
View an employee's schedule.
|
||||
"""
|
||||
employee = get_object_or_404(
|
||||
Employee,
|
||||
pk=employee_id,
|
||||
tenant=request.user.tenant
|
||||
)
|
||||
|
||||
# Get current schedule
|
||||
current_schedule = Schedule.objects.filter(
|
||||
employee=employee,
|
||||
is_current=True,
|
||||
is_active=True
|
||||
).first()
|
||||
|
||||
# Get upcoming assignments
|
||||
today = timezone.now().date()
|
||||
upcoming_assignments = ScheduleAssignment.objects.filter(
|
||||
schedule__employee=employee,
|
||||
assignment_date__gte=today
|
||||
).order_by('assignment_date', 'start_time')[:14] # Next 2 weeks
|
||||
|
||||
context = {
|
||||
'employee': employee,
|
||||
'current_schedule': current_schedule,
|
||||
'upcoming_assignments': upcoming_assignments,
|
||||
}
|
||||
|
||||
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')
|
||||
}),
|
||||
('Current Status', {
|
||||
'fields': ('status', 'current_patient', 'current_admission',
|
||||
'fields': ('status', 'current_admission',
|
||||
'occupied_since', 'reserved_until')
|
||||
}),
|
||||
('Equipment & Features', {
|
||||
@ -451,6 +451,9 @@ class SurgeryScheduleAdmin(admin.ModelAdmin):
|
||||
('Basic Information', {
|
||||
'fields': ('tenant', 'surgery_id', 'patient', 'admission')
|
||||
}),
|
||||
# ('Operating Room',{
|
||||
# 'fields': ('operating_room')
|
||||
# }),
|
||||
('Surgery Details', {
|
||||
'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(
|
||||
tenant=user.tenant,
|
||||
is_active=True,
|
||||
role__in=['NURSE', 'NURSE_MANAGER']
|
||||
employee_profile__role__in=['NURSE', 'NURSE_MANAGER']
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
def clean_total_beds(self):
|
||||
|
||||
@ -1157,6 +1157,7 @@ class Transfer(models.Model):
|
||||
('COMPLETED', 'Completed'),
|
||||
('CANCELLED', 'Cancelled'),
|
||||
('DELAYED', 'Delayed'),
|
||||
('REJECTED', 'Rejected'),
|
||||
]
|
||||
PRIORITY_CHOICES = [
|
||||
('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>Status</td><td>
|
||||
{% if admission.status == 'ADMITTED' %}
|
||||
<span class="badge bg-success">{{ admission.get_status_display }}</span>
|
||||
<span class="badge bg-success">
|
||||
{% elif admission.status == 'PENDING' %}
|
||||
<span class="badge bg-warning">{{ admission.get_status_display }}</span>
|
||||
<span class="badge bg-warning">
|
||||
{% elif admission.status == 'TRANSFERRED' %}
|
||||
<span class="badge bg-info">{{ admission.get_status_display }}</span>
|
||||
<span class="badge bg-info">
|
||||
{% elif admission.status == 'DISCHARGED' %}
|
||||
<span class="badge bg-secondary">{{ admission.get_status_display }}</span>
|
||||
<span class="badge bg-secondary">
|
||||
{% 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 %}
|
||||
{{ admission.get_status_display }}</span>
|
||||
</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>
|
||||
|
||||
@ -4,414 +4,432 @@
|
||||
{% block title %}Bed {{ object.bed_number }} Details{% endblock %}
|
||||
|
||||
{% 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>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:bed_management' %}">Bed Management</a></li>
|
||||
<li class="breadcrumb-item active">Bed {{ object.bed_number }}</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">Bed {{ object.bed_number }} Details</h1>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-bed me-2"></i>Bed<span class="fw-light">Details</span>
|
||||
</h1>
|
||||
<p class="text-muted">{{ object.bed_number }}</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'inpatients:bed_update' object.pk %}" class="btn btn-primary">
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="btn-group me-2">
|
||||
<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
|
||||
</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>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('available')">
|
||||
<i class="fas fa-check text-success me-2"></i>Mark Available
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('occupied')">
|
||||
<i class="fas fa-user text-warning me-2"></i>Mark Occupied
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#" onclick="updateBedStatus('maintenance')">
|
||||
<i class="fas fa-tools text-info me-2"></i>Mark for Maintenance
|
||||
</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>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{# <li><a class="dropdown-item text-danger" href="{% url 'inpatients:bed_confirm_delete' object.pk %}">#}
|
||||
{# <i class="fas fa-trash me-2"></i>Delete Bed#}
|
||||
{# </a></li>#}
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="updateBedStatus('available')">
|
||||
<i class="fas fa-check text-success me-2"></i>Mark Available
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="updateBedStatus('occupied')">
|
||||
<i class="fas fa-user text-warning me-2"></i>Mark Occupied
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" onclick="updateBedStatus('maintenance')">
|
||||
<i class="fas fa-tools text-info me-2"></i>Mark for Maintenance
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-8">
|
||||
<!-- Bed Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
<i class="fas fa-bed me-2"></i>
|
||||
Bed Information
|
||||
</h4>
|
||||
<div class="card-toolbar">
|
||||
<span class="badge bg-{% if object.status == 'available' %}success{% elif object.status == 'occupied' %}warning{% elif object.status == 'maintenance' %}danger{% else %}info{% endif %} fs-6">
|
||||
{{ object.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
</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 class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-xl-9">
|
||||
<!-- Bed Information -->
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title"><i class="fa fa-bed me-2"></i>Bed Information</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<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">
|
||||
{{ object.get_status_display }}
|
||||
</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>
|
||||
|
||||
{% if object.notes %}
|
||||
<div class="mt-3">
|
||||
<h6>Notes:</h6>
|
||||
<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>
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<table class="table table-borderless">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Action</th>
|
||||
<th>Patient</th>
|
||||
<th>Duration</th>
|
||||
<th>Notes</th>
|
||||
<td class="fw-bold">Bed Number:</td>
|
||||
<td>{{ object.bed_number }}</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for history in bed_history %}
|
||||
<tr>
|
||||
<td class="fw-bold">Ward:</td>
|
||||
<td>
|
||||
<div>{{ history.date|date:"M d, Y" }}</div>
|
||||
<div class="small text-muted">{{ history.date|time:"g:i A" }}</div>
|
||||
<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 history.action == 'admission' %}success{% elif history.action == 'discharge' %}info{% elif history.action == 'transfer' %}warning{% else %}secondary{% endif %}">
|
||||
{{ history.get_action_display }}
|
||||
<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 history.patient %}
|
||||
<a href="{% url 'patients:patient_detail' history.patient.pk %}">
|
||||
{{ 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>
|
||||
{% if object.has_oxygen %}
|
||||
<span class="badge bg-info me-1">Oxygen</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if history.duration %}
|
||||
{{ history.duration }}
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% if object.has_suction %}
|
||||
<span class="badge bg-info me-1">Suction</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>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<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>
|
||||
{% 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">
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-bolt me-2"></i>
|
||||
Quick Actions
|
||||
</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>
|
||||
{% if object.notes %}
|
||||
<div class="mt-3">
|
||||
<h6>Notes:</h6>
|
||||
<div class="note alert-info">
|
||||
{{ object.notes }}
|
||||
</div>
|
||||
</div>
|
||||
{% 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>
|
||||
|
||||
<!-- Bed Statistics -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-chart-bar me-2"></i>
|
||||
Statistics
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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>
|
||||
<!-- Current Patient -->
|
||||
{% if object.current_admission.patient %}
|
||||
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-user-injured me-2"></i>Current Patient
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<a href="{% url 'inpatients:admission_detail' object.current_admission.pk %}" class="btn btn-xs btn-outline-theme">
|
||||
<i class="fas fa-eye me-2"></i>View Admission
|
||||
</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 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>
|
||||
|
||||
<!-- 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="panel-body">
|
||||
<div class="d-flex align-items-center">
|
||||
<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>
|
||||
<h5 class="mb-1">
|
||||
<a href="{% url 'patients:patient_detail' object.current_admission.patient.pk %}">
|
||||
{{ object.current_admission.patient.get_full_name }}
|
||||
</a>
|
||||
</h5>
|
||||
<div class="text-muted mb-1">
|
||||
<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>
|
||||
{% 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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- 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>
|
||||
{% 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>
|
||||
|
||||
<!-- Ward Information -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="card-title">
|
||||
<i class="fas fa-hospital me-2"></i>
|
||||
Ward Information
|
||||
</h5>
|
||||
<div class="col-xl-3">
|
||||
<!-- Quick Actions -->
|
||||
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-bolt me-2"></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 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 class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong>Ward:</strong>
|
||||
<a href="{% url 'inpatients:ward_detail' object.ward.pk %}">
|
||||
{{ object.ward.name }}
|
||||
</a>
|
||||
|
||||
<!-- Bed Statistics -->
|
||||
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="mb-2">
|
||||
<strong>Department:</strong> {{ object.ward.department.name }}
|
||||
<div class="panel-body">
|
||||
<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 class="mb-2">
|
||||
<strong>Capacity:</strong> {{ object.ward.total_beds }} beds
|
||||
</div>
|
||||
|
||||
<!-- 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 class="mb-2">
|
||||
<strong>Available:</strong> {{ object.ward.available_beds }} beds
|
||||
<div class="panel-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="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>
|
||||
<strong>Occupancy:</strong> {{ object.ward.occupancy_rate }}%
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- Update Status Modal -->
|
||||
<div class="modal fade" id="updateStatusModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
|
||||
@ -4,21 +4,22 @@
|
||||
{% block title %}Bed Management{% endblock %}
|
||||
|
||||
{% 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>
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'inpatients:dashboard' %}">Inpatients</a></li>
|
||||
<li class="breadcrumb-item active">Bed Management</li>
|
||||
</ol>
|
||||
<h1 class="page-header mb-0">Bed Management</h1>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-list me-2"></i>Bed<span class="fw-light">Management</span>
|
||||
</h1>
|
||||
<p class="text-muted">Real-time bed occupancy and availability management.</p>
|
||||
</div>
|
||||
<div class="ms-auto">
|
||||
<div class="btn-toolbar mb-2 mb-md-0">
|
||||
<div class="ms-auto">
|
||||
<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
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary dropdown-toggle dropdown-toggle-split" data-bs-toggle="dropdown">
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
<i class="fas fa-ellipsis-v"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#" onclick="exportBeds()">
|
||||
@ -34,94 +35,27 @@
|
||||
</ul>
|
||||
</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 class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title">
|
||||
<i class="fas fa-bed me-2"></i>
|
||||
Bed List
|
||||
</h4>
|
||||
<div class="card-toolbar">
|
||||
<div class="d-flex align-items-center">
|
||||
<!-- View Toggle -->
|
||||
<div class="btn-group btn-group-sm me-3" role="group">
|
||||
<input type="radio" class="btn-check" name="viewType" id="gridView" autocomplete="off" checked>
|
||||
<label class="btn btn-outline-secondary" for="gridView">
|
||||
<i class="fas fa-th"></i>
|
||||
</label>
|
||||
<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>
|
||||
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fa fa-bed me-2"></i>Bed List
|
||||
</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-flex align-items-center mb-3">
|
||||
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="d-flex align-items-center">
|
||||
<select class="form-select form-select-sm me-2" id="wardFilter" onchange="filterBeds()">
|
||||
@ -132,127 +66,24 @@
|
||||
</select>
|
||||
<select class="form-select form-select-sm me-2" id="statusFilter" onchange="filterBeds()">
|
||||
<option value="">All Status</option>
|
||||
<option value="available">Available</option>
|
||||
<option value="occupied">Occupied</option>
|
||||
<option value="maintenance">Maintenance</option>
|
||||
<option value="cleaning">Cleaning</option>
|
||||
<option value="AVAILABLE">Available</option>
|
||||
<option value="OCCUPIED">Occupied</option>
|
||||
<option value="MAINTENANCE">Maintenance</option>
|
||||
<option value="CLEANING">Cleaning</option>
|
||||
</select>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="clearFilters()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</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 -->
|
||||
<div id="listViewContent" style="display: none;">
|
||||
<div id="listViewContent">
|
||||
{% if object_list %}
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive border border-primary-subtle">
|
||||
<table class="table table-hover" id="bedTable">
|
||||
<thead>
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>
|
||||
<input type="checkbox" id="selectAll" onchange="toggleSelectAll()">
|
||||
@ -269,8 +100,8 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for bed in object_list %}
|
||||
<tr class="bed-row"
|
||||
data-ward="{{ bed.ward.id }}"
|
||||
<tr class="bed-row"
|
||||
data-ward="{{ bed.ward.id }}"
|
||||
data-status="{{ bed.status }}"
|
||||
data-bed-type="{{ bed.bed_type }}">
|
||||
<td>
|
||||
@ -290,11 +121,11 @@
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if bed.current_patient %}
|
||||
<a href="{% url 'patients:patient_detail' bed.current_patient.pk %}">
|
||||
{{ bed.current_patient.get_full_name }}
|
||||
{% if bed.current_admission.patient %}
|
||||
<a href="{% url 'patients:patient_detail' bed.current_admission.patient.pk %}">
|
||||
{{ bed.current_admission.patient.get_full_name }}
|
||||
</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 %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
@ -318,7 +149,7 @@
|
||||
<a href="{% url 'inpatients:bed_detail' bed.pk %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye"></i>
|
||||
</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>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" onclick="deleteBed('{{ bed.pk }}')">
|
||||
@ -342,29 +173,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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() {
|
||||
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="col-12">
|
||||
<div class="planning-header">
|
||||
<div class="row">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-8">
|
||||
<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>
|
||||
@ -189,33 +189,46 @@
|
||||
<p class="mt-2 mb-0">Expected Discharge: {{ admission.anticipated_discharge_date}}</p>
|
||||
</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 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 -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<!-- Discharge Timeline -->
|
||||
<div class="panel panel-inverse border border-primary">
|
||||
<div class="panel-heading bg-primary">
|
||||
<h4 class="panel-title"><i class="fas fa-timeline"></i> Discharge Timeline</h4>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="panel-body">
|
||||
<div class="timeline">
|
||||
{% %}
|
||||
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-time">
|
||||
<span class="date">today</span>
|
||||
@ -242,11 +255,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Discharge Checklist -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-tasks"></i> Discharge Checklist</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="mb-2">
|
||||
<h6>Medical Clearance</h6>
|
||||
<div class="checklist-item">
|
||||
@ -332,11 +353,19 @@
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- Risk Assessment -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-exclamation-triangle"></i> Risk Assessment</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-exclamation-triangle"></i> Risk Assessment
|
||||
</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">
|
||||
<strong>Readmission Risk:</strong> Moderate
|
||||
<br><small>Age >65, cardiac condition, multiple medications</small>
|
||||
@ -357,11 +386,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Care Team -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-users"></i> Discharge Care Team</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="care-team">
|
||||
<div class="team-member">
|
||||
<i class="fas fa-user-md fa-2x text-primary mb-2"></i>
|
||||
@ -388,11 +425,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-bolt"></i> Quick Actions</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="d-grid gap-2">
|
||||
<button class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-calendar"></i> Schedule Follow-up
|
||||
@ -415,11 +460,19 @@
|
||||
<!-- Discharge Medications -->
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-pills"></i> Discharge Medications</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="medication-item">
|
||||
<h6>Aspirin 81mg</h6>
|
||||
<p>Take 1 tablet daily by mouth</p>
|
||||
@ -445,11 +498,19 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5><i class="fas fa-calendar-check"></i> Follow-up Appointments</h5>
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="appointment-item">
|
||||
<h6>Cardiology Follow-up</h6>
|
||||
<p><strong>Date:</strong> January 30, 2024 at 2:00 PM</p>
|
||||
@ -481,25 +542,13 @@
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<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>
|
||||
|
||||
|
||||
{% 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>
|
||||
</div>
|
||||
<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 %}
|
||||
|
||||
<div class="bed-info mb-3">
|
||||
@ -26,9 +26,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if bed.current_patient %}
|
||||
{% if bed.current_admission.patient %}
|
||||
<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.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -4,17 +4,35 @@
|
||||
{% block title %}Surgery Schedule - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<div>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-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="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-procedures me-2"></i>Surgery Schedule
|
||||
<div class="panel panel-inverse">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-list-dots"></i> List
|
||||
</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 class="panel-body">
|
||||
<!-- Filters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2">
|
||||
@ -52,12 +70,13 @@
|
||||
</div>
|
||||
|
||||
<!-- Surgery Schedule Table -->
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive border border-primary-subtle">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>Date/Time</th>
|
||||
<th>Patient</th>
|
||||
<th>Room</th>
|
||||
<th>Procedure</th>
|
||||
<th>Surgeon</th>
|
||||
<th>Type</th>
|
||||
@ -81,6 +100,12 @@
|
||||
</small>
|
||||
</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>
|
||||
{% if surgery.procedure_code %}
|
||||
<br><small class="text-muted">Code: {{ surgery.procedure_code }}</small>
|
||||
@ -125,9 +150,9 @@
|
||||
<i class="fas fa-check"></i>
|
||||
</button>
|
||||
{% elif surgery.status == 'CONFIRMED' %}
|
||||
<button class="btn btn-outline-primary"
|
||||
<button class="btn btn-outline-primary"
|
||||
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-swap="none"
|
||||
hx-headers='{"X-CSRFToken":"{{ csrf_token }}"}'>
|
||||
@ -153,18 +178,18 @@
|
||||
</button>
|
||||
{% 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>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
{% if surgery.status not in 'COMPLETED,CANCELLED' %}
|
||||
{# <button class="btn btn-outline-warning" #}
|
||||
{# title="Postpone"#}
|
||||
{# hx-post="{% url 'inpatients:s' surgery.id %}"#}
|
||||
{# hx-confirm="Postpone this surgery?"#}
|
||||
{# hx-swap="none">#}
|
||||
{# <i class="fas fa-clock"></i>#}
|
||||
{# </button>#}
|
||||
<button class="btn btn-outline-warning"
|
||||
title="Postpone"
|
||||
hx-post="{% url 'inpatients:postpone_surgery' surgery.id %}"
|
||||
hx-confirm="Postpone this surgery?"
|
||||
hx-swap="none">
|
||||
<i class="fas fa-clock"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger"
|
||||
title="Cancel"
|
||||
hx-post="{% url 'inpatients:cancel_surgery' surgery.id %}"
|
||||
@ -188,38 +213,11 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Surgery pagination">
|
||||
<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>
|
||||
{% include 'partial/pagination.html' %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,11 +1,8 @@
|
||||
{% 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>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="transferForm" method="post" action="{% url 'inpatients:request_transfer' admission_id=admission.id %}">
|
||||
|
||||
|
||||
<form id="transferForm" method="post" action="{% url 'inpatients:transfer_patient' admission.id %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="patient-info mb-3">
|
||||
@ -103,7 +100,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="submit" form="transferForm" class="btn btn-primary">
|
||||
|
||||
@ -4,17 +4,35 @@
|
||||
{% block title %}Transfer Management - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
|
||||
<div>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-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="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h4 class="card-title mb-0">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Management
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-exchange me-2"></i>Transfer 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="card-body">
|
||||
<div class="panel-body">
|
||||
<!-- Filters -->
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-2">
|
||||
@ -51,9 +69,9 @@
|
||||
</div>
|
||||
|
||||
<!-- Transfers Table -->
|
||||
<div class="table-responsive">
|
||||
<div class="table-responsive border border-primary-subtle">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<thead class="table-primary">
|
||||
<tr>
|
||||
<th>Patient</th>
|
||||
<th>Requested</th>
|
||||
@ -121,18 +139,23 @@
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm">
|
||||
{% if transfer.status == 'REQUESTED' %}
|
||||
<a class="btn btn-outline-success"
|
||||
<button class="btn btn-outline-success"
|
||||
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>
|
||||
</a>
|
||||
{# <button class="btn btn-outline-danger" #}
|
||||
{# title="Reject"#}
|
||||
{# hx-post="{% url 'inpatients:reject_transfer' transfer.id %}"#}
|
||||
{# hx-confirm="Reject this transfer?"#}
|
||||
{# hx-swap="none">#}
|
||||
{# <i class="fas fa-times"></i>#}
|
||||
{# </button>#}
|
||||
</button>
|
||||
|
||||
<button class="btn btn-outline-danger"
|
||||
title="Reject"
|
||||
hx-post="{% url 'inpatients:reject_transfer' transfer.id %}"
|
||||
hx-confirm="Reject this transfer?"
|
||||
hx-swap="none"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
{% elif transfer.status == 'APPROVED' or transfer.status == 'SCHEDULED' %}
|
||||
{# <button class="btn btn-outline-primary" #}
|
||||
{# title="Execute Transfer"#}
|
||||
@ -143,18 +166,19 @@
|
||||
{# </button>#}
|
||||
{% 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>
|
||||
</button>
|
||||
</a>
|
||||
|
||||
{% if transfer.status not in 'COMPLETED,CANCELLED,REJECTED' %}
|
||||
{# <button class="btn btn-outline-secondary" #}
|
||||
{# title="Cancel"#}
|
||||
{# hx-post="{% url 'inpatients:cancel_transfer' transfer.id %}"#}
|
||||
{# hx-confirm="Cancel this transfer?"#}
|
||||
{# hx-swap="none">#}
|
||||
{# <i class="fas fa-ban"></i>#}
|
||||
{# </button>#}
|
||||
<button class="btn btn-outline-warning"
|
||||
title="Cancel"
|
||||
hx-post="{% url 'inpatients:cancel_transfer' transfer.id %}"
|
||||
hx-confirm="Cancel this transfer?"
|
||||
hx-swap="none"
|
||||
hx-headers='{"X-CSRFToken": "{{ csrf_token }}"}'>
|
||||
<i class="fas fa-ban"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
@ -170,12 +194,11 @@
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
{% include 'partial/pagination.html'%}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -4,20 +4,14 @@
|
||||
{% block title %}{{ ward.name }} - Ward Details - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- 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>
|
||||
<h1 class="h3 mb-1">
|
||||
<i class="fas fa-hospital me-2"></i>{{ ward.name }}
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<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>
|
||||
<h1 class="h2">
|
||||
<i class="fas fa-hospital me-2"></i>{{ ward.name }}<span class="fw-light">Details</span>
|
||||
</h1>
|
||||
<p class="text-muted">Manage ward aspects</p>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'inpatients:ward_list' %}" class="btn btn-outline-secondary">
|
||||
@ -48,18 +42,25 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Main Content -->
|
||||
<div class="col-lg-8">
|
||||
<!-- Ward Information -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-1">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<dl class="row">
|
||||
@ -132,35 +133,37 @@
|
||||
</div>
|
||||
|
||||
<!-- Bed Layout -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-bed me-2"></i>Bed Layout
|
||||
</h5>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-secondary" onclick="refreshBedLayout()">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary" onclick="toggleBedView()">
|
||||
<i class="fas fa-th me-1"></i>Grid View
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-2">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-bed me-2"></i>Bed Layout
|
||||
</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 class="panel-body">
|
||||
<div id="bed-layout" class="bed-grid">
|
||||
{% 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-number">{{ bed.bed_number }}</div>
|
||||
<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 }}
|
||||
</span>
|
||||
</div>
|
||||
{% if bed.current_patient %}
|
||||
{% if bed.current_admission.patient %}
|
||||
<div class="patient-info">
|
||||
<small>{{ bed.current_patient.get_full_name }}</small>
|
||||
<small>{{ bed.current_admission.patient.get_full_name }}</small>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -179,16 +182,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Current Patients -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-users me-2"></i>Current Patients
|
||||
</h5>
|
||||
<span class="badge bg-primary">{{ current_patients.count }} patients</span>
|
||||
<div class="panel panel-inverse" data-sortable-id="index-3">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<i class="fas fa-users me-2"></i>Current Patients
|
||||
</h4>
|
||||
<div class="panel-heading-btn">
|
||||
<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 class="card-body p-0">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead class="table-light">
|
||||
@ -202,10 +209,12 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for admission in current_patients %}
|
||||
{% for admission in recent_admissions %}
|
||||
<tr>
|
||||
<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>
|
||||
<div class="d-flex align-items-center">
|
||||
@ -216,17 +225,17 @@
|
||||
</div>
|
||||
<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>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ admission.admission_date|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ admission.admission_date|timesince }} ago</small>
|
||||
<div>{{ admission.admission_datetime|date:"M d, Y" }}</div>
|
||||
<small class="text-muted">{{ admission.admission_datetime|timesince }} ago</small>
|
||||
</td>
|
||||
<td>
|
||||
{% if admission.primary_diagnosis %}
|
||||
{{ admission.primary_diagnosis|truncatechars:30 }}
|
||||
{% if admission.admitting_diagnosis %}
|
||||
{{ admission.admitting_diagnosis|truncatechars:30 }}
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
@ -244,21 +253,15 @@
|
||||
class="btn btn-outline-primary btn-sm" title="View Details">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm 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="#">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Transfer
|
||||
</a></li>
|
||||
<li><a class="dropdown-item" href="#">
|
||||
<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>
|
||||
<a class="btn btn-outline-warning btn-sm" onclick="showTransfer({{ admission.id }})">
|
||||
<i class="fas fa-exchange-alt"></i>
|
||||
</a>
|
||||
<a class="btn btn-outline-danger btn-sm" href="{% url 'inpatients:discharge_patient' admission.id %}">
|
||||
<i class="fas fa-sign-out-alt"></i>
|
||||
</a>
|
||||
<a class="btn btn-outline-info btn-sm" href="#">
|
||||
<i class="fas fa-notes-medical"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -281,18 +284,24 @@
|
||||
<!-- Sidebar -->
|
||||
<div class="col-lg-4">
|
||||
<!-- Ward Statistics -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-4">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="row text-center">
|
||||
<div class="col-6 mb-3">
|
||||
<div class="card bg-success bg-gradient text-white">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-1">{{ ward.available_beds }}</h4>
|
||||
<h4 class="mb-1">{{ available_beds }}</h4>
|
||||
<small>Available</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -300,7 +309,7 @@
|
||||
<div class="col-6 mb-3">
|
||||
<div class="card bg-primary bg-gradient text-white">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-1">{{ ward.occupied_beds }}</h4>
|
||||
<h4 class="mb-1">{{ occupied_beds }}</h4>
|
||||
<small>Occupied</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -308,15 +317,15 @@
|
||||
<div class="col-6 mb-3">
|
||||
<div class="card bg-warning bg-gradient text-white">
|
||||
<div class="card-body">
|
||||
<h4 class="mb-1">{{ ward.maintenance_beds }}</h4>
|
||||
<h4 class="mb-1">{{ maintenance_beds }}</h4>
|
||||
<small>Maintenance</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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">
|
||||
<h4 class="mb-1">{{ ward.reserved_beds }}</h4>
|
||||
<h4 class="mb-1">{{ reserved_beds }}</h4>
|
||||
<small>Reserved</small>
|
||||
</div>
|
||||
</div>
|
||||
@ -328,7 +337,7 @@
|
||||
<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 %}"
|
||||
style="width: {{ ward.occupancy_rate }}%">
|
||||
{{ ward.occupancy_rate|floatformat:1 }}%
|
||||
{{ ward.occupancy_rate|floatformat:0 }}%
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ ward.occupied_beds }} of {{ ward.total_beds }} beds occupied</small>
|
||||
@ -337,13 +346,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Actions -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-5">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="panel-body">
|
||||
<div class="d-grid gap-2">
|
||||
<a href="#" class="btn btn-primary">
|
||||
<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">
|
||||
<i class="fas fa-bed me-2"></i>Manage Beds
|
||||
</a>
|
||||
<a href="#" class="btn btn-outline-info">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Patient
|
||||
</a>
|
||||
<a href="#" class="btn btn-outline-success">
|
||||
<i class="fas fa-sign-out-alt me-2"></i>Discharge Patient
|
||||
<a href="{% url 'inpatients:transfer_management' %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-exchange-alt me-2"></i>Transfer Management
|
||||
</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">
|
||||
<i class="fas fa-chart-bar me-2"></i>Ward Report
|
||||
</a>
|
||||
@ -368,13 +383,19 @@
|
||||
</div>
|
||||
|
||||
<!-- Ward Staff -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">
|
||||
<div class="panel panel-inverse" data-sortable-id="index-6">
|
||||
<div class="panel-heading">
|
||||
<h4 class="panel-title">
|
||||
<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 class="card-body">
|
||||
<div class="panel-body">
|
||||
<div class="list-group list-group-flush">
|
||||
{% if ward.nurse_manager %}
|
||||
<div class="list-group-item d-flex justify-content-between align-items-center px-0">
|
||||
@ -422,6 +443,19 @@
|
||||
</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>
|
||||
// Ward detail functionality
|
||||
function refreshBedLayout() {
|
||||
@ -436,7 +470,7 @@ function toggleBedView() {
|
||||
|
||||
function showBedDetails(bedId) {
|
||||
// Load bed details in modal
|
||||
fetch(`/inpatients/beds/${bedId}/details/`)
|
||||
fetch(`{% url 'inpatients:bed_details' 0%}`.replace('0', bedId))
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
document.getElementById('bedDetailsContent').innerHTML = html;
|
||||
@ -446,6 +480,18 @@ function showBedDetails(bedId) {
|
||||
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() {
|
||||
window.print();
|
||||
|
||||
@ -83,12 +83,12 @@
|
||||
class="btn btn-outline-primary btn-sm">
|
||||
<i class="fas fa-bed me-1"></i>Beds
|
||||
</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
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm">
|
||||
</a>
|
||||
<a href="{% url 'inpatients:ward_update' ward.id %}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-edit me-1"></i>Edit
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -13,6 +13,8 @@ urlpatterns = [
|
||||
# wards
|
||||
path('wards/', views.WardListView.as_view(), name='ward_list'),
|
||||
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'),
|
||||
|
||||
# Admissions
|
||||
@ -30,11 +32,12 @@ urlpatterns = [
|
||||
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>/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.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/<int:pk>/', views.BedDetailView.as_view(), name='bed_detail'),
|
||||
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>/maintenance/', views.maintenance_bed, name='maintenance_bed'),
|
||||
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/calendar/', views.surgery_calendar, name='surgery_calendar'),
|
||||
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
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
"""
|
||||
Views for inpatients app.
|
||||
"""
|
||||
|
||||
from django.conf.locale import te
|
||||
from django.shortcuts import render, get_object_or_404, redirect
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
@ -172,13 +173,13 @@ class WardListView(LoginRequiredMixin, ListView):
|
||||
|
||||
context['buildings'] = Ward.objects.filter(
|
||||
tenant=tenant
|
||||
).exclude(building__isnull=True).exclude(building='').values_list(
|
||||
).values_list(
|
||||
'building', flat=True
|
||||
).distinct()
|
||||
|
||||
context['floors'] = Ward.objects.filter(
|
||||
tenant=tenant
|
||||
).exclude(floor__isnull=True).exclude(floor='').values_list(
|
||||
).values_list(
|
||||
'floor', flat=True
|
||||
).distinct()
|
||||
|
||||
@ -215,7 +216,7 @@ class WardDetailView(LoginRequiredMixin, DetailView):
|
||||
context['beds'] = Bed.objects.filter(
|
||||
ward=ward
|
||||
).select_related(
|
||||
'current_patient', 'current_admission'
|
||||
'current_admission'
|
||||
).order_by('room_number', 'bed_number')
|
||||
|
||||
# Group beds by room for display
|
||||
@ -234,6 +235,7 @@ class WardDetailView(LoginRequiredMixin, DetailView):
|
||||
context['maintenance_beds'] = context['beds'].filter(
|
||||
status__in=['MAINTENANCE', 'OUT_OF_ORDER', 'CLEANING']
|
||||
).count()
|
||||
context['reserved_beds'] = context['beds'].filter(status='RESERVED').count()
|
||||
|
||||
if context['total_beds'] > 0:
|
||||
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."""
|
||||
queryset = Bed.objects.filter(
|
||||
ward__tenant=self.request.user.tenant
|
||||
).select_related('ward', 'current_patient')
|
||||
).select_related('ward', 'current_admission')
|
||||
|
||||
# Handle search query
|
||||
search_query = self.request.GET.get('search', '')
|
||||
@ -394,13 +396,13 @@ class BedListView(LoginRequiredMixin, ListView):
|
||||
).order_by('name')
|
||||
|
||||
# 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
|
||||
context['room_types'] = Bed.ROOM_TYPE_CHOICES
|
||||
context['room_types'] = Bed.RoomType.choices
|
||||
|
||||
# Get statuses for filter dropdown
|
||||
context['statuses'] = Bed.STATUS_CHOICES
|
||||
context['statuses'] = Bed.BedStatus.choices
|
||||
|
||||
# Add search query to context
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
@ -426,7 +428,7 @@ class BedDetailView(LoginRequiredMixin, DetailView):
|
||||
return Bed.objects.filter(
|
||||
ward__tenant=self.request.user.tenant
|
||||
).select_related(
|
||||
'ward', 'current_patient', 'current_admission',
|
||||
'ward', 'current_admission',
|
||||
'cleaned_by', 'blocked_by', 'created_by'
|
||||
)
|
||||
|
||||
@ -436,13 +438,12 @@ class BedDetailView(LoginRequiredMixin, DetailView):
|
||||
|
||||
# Get bed history - admissions that used this bed
|
||||
context['admission_history'] = Admission.objects.filter(
|
||||
Q(current_bed=bed) | Q(current_bed=bed)
|
||||
).select_related(
|
||||
'patient', 'admitting_physician'
|
||||
).order_by('-admission_datetime')[:10]
|
||||
current_bed=bed).select_related(
|
||||
'patient', 'admitting_physician',
|
||||
).order_by('-admission_datetime')
|
||||
|
||||
# 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
|
||||
|
||||
@ -562,7 +563,7 @@ class BedManagementView(LoginRequiredMixin, ListView):
|
||||
)
|
||||
|
||||
return queryset.select_related(
|
||||
'ward', 'current_patient', 'current_admission'
|
||||
'ward', 'current_admission'
|
||||
).order_by('ward__name', 'room_number', 'bed_number')
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
@ -583,8 +584,8 @@ class BedManagementView(LoginRequiredMixin, ListView):
|
||||
)
|
||||
context.update({
|
||||
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
||||
'bed_statuses': Bed.STATUS_CHOICES,
|
||||
'bed_types': Bed.BED_TYPE_CHOICES,
|
||||
'bed_statuses': Bed.BedStatus.choices,
|
||||
'bed_types': Bed.BedType.choices,
|
||||
'total_beds': Bed.objects.filter(ward__tenant=tenant).count(),
|
||||
'available_beds': Bed.objects.filter(ward__tenant=tenant, status='AVAILABLE').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()
|
||||
active_admissions = Admission.objects.filter(tenant=tenant, status='ADMITTED').count()
|
||||
context.update({
|
||||
'admission_statuses': Admission.STATUS_CHOICES,
|
||||
'admission_types': Admission.ADMISSION_TYPE_CHOICES,
|
||||
'admission_statuses': Admission.AdmissionStatus.choices,
|
||||
'admission_types': Admission.AdmissionType.choices,
|
||||
'wards': Ward.objects.filter(tenant=tenant, is_active=True),
|
||||
'total_admissions': total_admissions,
|
||||
'active_admissions': active_admissions,
|
||||
@ -880,7 +881,7 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
||||
model = SurgerySchedule
|
||||
template_name = 'inpatients/surgeries/surgery_schedule.html'
|
||||
context_object_name = 'surgeries'
|
||||
paginate_by = 20
|
||||
paginate_by = 5
|
||||
|
||||
def get_queryset(self):
|
||||
"""Filter surgeries by tenant and search query."""
|
||||
@ -959,10 +960,10 @@ class SurgeryScheduleListView(LoginRequiredMixin, ListView):
|
||||
is_active=True,
|
||||
employee_profile__role__in=['SURGEON', 'PHYSICIAN_ASSISTANT']
|
||||
).order_by('last_name', 'first_name')
|
||||
|
||||
from operating_theatre.models import OperatingRoom
|
||||
# Get operating rooms for filter dropdown
|
||||
try:
|
||||
from operating_theatre.models import OperatingRoom
|
||||
|
||||
context['operating_rooms'] = OperatingRoom.objects.filter(
|
||||
tenant=tenant,
|
||||
is_active=True
|
||||
@ -1102,11 +1103,11 @@ def bed_grid(request):
|
||||
beds = Bed.objects.filter(
|
||||
ward_id=ward_id,
|
||||
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:
|
||||
beds = Bed.objects.filter(
|
||||
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'
|
||||
)
|
||||
|
||||
@ -1271,7 +1272,7 @@ def transfer_patient(request, admission_id):
|
||||
'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
|
||||
@ -1279,7 +1280,6 @@ def approve_transfer(request, transfer_id):
|
||||
"""
|
||||
HTMX endpoint for approving a transfer.
|
||||
"""
|
||||
print("transfer clicked")
|
||||
transfer = get_object_or_404(Transfer, id=transfer_id, admission__tenant=request.user.tenant)
|
||||
|
||||
if request.method == 'POST':
|
||||
@ -1303,7 +1303,7 @@ def approve_transfer(request, transfer_id):
|
||||
|
||||
# Log the action
|
||||
AuditLogger.log_event(
|
||||
actor=request.user,
|
||||
user=request.user,
|
||||
action='TRANSFER_APPROVED',
|
||||
target=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)
|
||||
|
||||
|
||||
@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
|
||||
def complete_transfer(request, transfer_id):
|
||||
"""
|
||||
@ -1517,11 +1583,19 @@ def bed_status_board(request):
|
||||
'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
|
||||
})
|
||||
|
||||
|
||||
@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
|
||||
def clean_bed(request, pk):
|
||||
"""
|
||||
@ -2261,8 +2335,7 @@ class AdmissionDetailView(LoginRequiredMixin, DetailView):
|
||||
context['has_discharge_summary'] = hasattr(admission, 'discharge_summary')
|
||||
|
||||
return context
|
||||
#
|
||||
#
|
||||
|
||||
|
||||
|
||||
|
||||
@ -2541,38 +2614,7 @@ class TransferUpdateView(LoginRequiredMixin, UpdateView):
|
||||
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
|
||||
@ -2676,6 +2718,109 @@ def cancel_surgery(request, pk):
|
||||
'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
|
||||
def inpatient_stats(request):
|
||||
"""
|
||||
@ -2775,3 +2920,6 @@ def inpatient_stats(request):
|
||||
'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
|
||||
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)
|
||||
self.procedures = [
|
||||
"استئصال المرارة بالمنظار", "Laparoscopic Cholecystectomy",
|
||||
"جراحة القلب المفتوح", "Open Heart Surgery",
|
||||
"استبدال مفصل الركبة", "Total Knee Replacement",
|
||||
"جراحة الفتق الإربي", "Inguinal Hernia Repair",
|
||||
"استئصال الزائدة الدودية", "Appendectomy",
|
||||
"جراحة المنظار التشخيصي", "Diagnostic Laparoscopy",
|
||||
"استئصال الغدة الدرقية", "Thyroidectomy",
|
||||
"جراحة الساد", "Cataract Surgery",
|
||||
"استئصال اللوزتين", "Tonsillectomy",
|
||||
"جراحة البواسير", "Hemorrhoidectomy"
|
||||
"Laparoscopic Cholecystectomy","Open Heart Surgery",
|
||||
"Total Knee Replacement", "Inguinal Hernia Repair",
|
||||
"Appendectomy", "Diagnostic Laparoscopy",
|
||||
"Thyroidectomy", "Cataract Surgery",
|
||||
"Tonsillectomy", "Hemorrhoidectomy"
|
||||
]
|
||||
|
||||
# Saudi medical equipment (common in Saudi hospitals)
|
||||
self.equipment = [
|
||||
"منظار البطن كارل زايس", "Carl Zeiss Laparoscope",
|
||||
"جهاز القطع الكهربائي", "Electrocautery Unit",
|
||||
"جهاز التخدير دريجر", "Drager Anesthesia Machine",
|
||||
"مجهر جراحي ليكا", "Leica Surgical Microscope",
|
||||
"روبوت دافنشي", "da Vinci Surgical Robot",
|
||||
"جهاز الأشعة المقطعية المحمول", "Portable CT Scanner",
|
||||
"منظار المثانة", "Cystoscope",
|
||||
"مضخة القلب الرئة", "Heart-Lung Machine"
|
||||
"Carl Zeiss Laparoscope","Electrocautery Unit",
|
||||
"Drager Anesthesia Machine", "Leica Surgical Microscope",
|
||||
"da Vinci Surgical Robot", "Portable CT Scanner",
|
||||
"Cystoscope", "Heart-Lung Machine"
|
||||
]
|
||||
|
||||
# Common diagnoses in Arabic and English
|
||||
self.diagnoses = [
|
||||
"التهاب المرارة الحاد", "Acute Cholecystitis",
|
||||
"انسداد الأمعاء", "Bowel Obstruction",
|
||||
"كسر في عظم الفخذ", "Femur Fracture",
|
||||
"أورام الغدة الدرقية", "Thyroid Nodules",
|
||||
"حصى الكلى", "Kidney Stones",
|
||||
"التهاب الزائدة الدودية", "Appendicitis",
|
||||
"الفتق الإربي", "Inguinal Hernia",
|
||||
"سرطان القولون", "Colorectal Cancer"
|
||||
"Acute Cholecystitis", "Bowel Obstruction",
|
||||
"Femur Fracture", "Thyroid Nodules",
|
||||
"Kidney Stones", "Appendicitis",
|
||||
"Inguinal Hernia", "Colorectal Cancer"
|
||||
]
|
||||
|
||||
def get_existing_data(self):
|
||||
@ -795,15 +782,15 @@ class OperatingTheatreDataGenerator:
|
||||
|
||||
# Try to categorize users by role/specialty
|
||||
surgeons = list(User.objects.filter(
|
||||
groups__name__icontains='surgeon'
|
||||
groups__name__icontains='SURGEON'
|
||||
).distinct()) or users[:max(1, len(users) // 3)]
|
||||
|
||||
anesthesiologists = list(User.objects.filter(
|
||||
groups__name__icontains='anesthesia'
|
||||
groups__name__icontains='ANESTHESIA'
|
||||
).distinct()) or users[len(users) // 3:2 * len(users) // 3]
|
||||
|
||||
nurses = list(User.objects.filter(
|
||||
groups__name__icontains='nurse'
|
||||
groups__name__icontains='NURSE'
|
||||
).distinct()) or users[2 * len(users) // 3:]
|
||||
|
||||
# Get patients
|
||||
|
||||
Binary file not shown.
@ -224,7 +224,7 @@ class InsuranceInfoAdmin(admin.ModelAdmin):
|
||||
('Subscriber Information', {
|
||||
'fields': (
|
||||
'subscriber_name', 'subscriber_relationship',
|
||||
'subscriber_dob', 'subscriber_ssn'
|
||||
'subscriber_dob',
|
||||
)
|
||||
}),
|
||||
('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