diff --git a/.DS_Store b/.DS_Store index 946362f6..6d916a35 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/db.sqlite3 b/db.sqlite3 index f686d3df..1a328e1e 100644 Binary files a/db.sqlite3 and b/db.sqlite3 differ diff --git a/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc b/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc index 386b2a09..9e55dd05 100644 Binary files a/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc and b/emr/management/commands/__pycache__/import_icd10.cpython-312.pyc differ diff --git a/emr/templates/emr/problems/problem_list.html b/emr/templates/emr/problems/problem_list.html index 7900b8da..90eaf95c 100644 --- a/emr/templates/emr/problems/problem_list.html +++ b/emr/templates/emr/problems/problem_list.html @@ -67,10 +67,10 @@ -
+
- + diff --git a/facility_management/__pycache__/forms.cpython-312.pyc b/facility_management/__pycache__/forms.cpython-312.pyc new file mode 100644 index 00000000..9d342702 Binary files /dev/null and b/facility_management/__pycache__/forms.cpython-312.pyc differ diff --git a/facility_management/__pycache__/models.cpython-312.pyc b/facility_management/__pycache__/models.cpython-312.pyc index 0a9e5c1f..098825ea 100644 Binary files a/facility_management/__pycache__/models.cpython-312.pyc and b/facility_management/__pycache__/models.cpython-312.pyc differ diff --git a/facility_management/__pycache__/urls.cpython-312.pyc b/facility_management/__pycache__/urls.cpython-312.pyc new file mode 100644 index 00000000..34e9bbbb Binary files /dev/null and b/facility_management/__pycache__/urls.cpython-312.pyc differ diff --git a/facility_management/__pycache__/views.cpython-312.pyc b/facility_management/__pycache__/views.cpython-312.pyc new file mode 100644 index 00000000..d48f1407 Binary files /dev/null and b/facility_management/__pycache__/views.cpython-312.pyc differ diff --git a/facility_management/forms.py b/facility_management/forms.py index d2b12b0e..34b77050 100644 --- a/facility_management/forms.py +++ b/facility_management/forms.py @@ -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'}), } diff --git a/facility_management/management/__init__.py b/facility_management/management/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/facility_management/management/__pycache__/__init__.cpython-312.pyc b/facility_management/management/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..8632f74c Binary files /dev/null and b/facility_management/management/__pycache__/__init__.cpython-312.pyc differ diff --git a/facility_management/management/commands/__init__.py b/facility_management/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc b/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 00000000..7cf7a810 Binary files /dev/null and b/facility_management/management/commands/__pycache__/__init__.cpython-312.pyc differ diff --git a/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc b/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc new file mode 100644 index 00000000..3ffcb7d5 Binary files /dev/null and b/facility_management/management/commands/__pycache__/seed_facility.cpython-312.pyc differ diff --git a/facility_management/management/commands/seed_facility.py b/facility_management/management/commands/seed_facility.py new file mode 100644 index 00000000..23aaf388 --- /dev/null +++ b/facility_management/management/commands/seed_facility.py @@ -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() \ No newline at end of file diff --git a/facility_management/models.py b/facility_management/models.py index 932b510d..6a06289f 100644 --- a/facility_management/models.py +++ b/facility_management/models.py @@ -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 diff --git a/facility_management/templates/facility_management/maintenance/list.html b/facility_management/templates/facility_management/maintenance/list.html index 3d642d8f..9d677899 100644 --- a/facility_management/templates/facility_management/maintenance/list.html +++ b/facility_management/templates/facility_management/maintenance/list.html @@ -268,7 +268,7 @@ {% if is_paginated %} - {% include 'pagination.html' %} + {% include 'partial/pagination.html' %} {% endif %} {% else %}
diff --git a/facility_management/urls.py b/facility_management/urls.py index ad4cb101..93d35566 100644 --- a/facility_management/urls.py +++ b/facility_management/urls.py @@ -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//', views.BuildingDetailView.as_view(), name='building_detail'), - path('buildings//edit/', views.BuildingUpdateView.as_view(), name='building_edit'), + path('buildings//edit/', views.BuildingUpdateView.as_view(), name='building_update'), # Asset Management path('assets/', views.AssetListView.as_view(), name='asset_list'), diff --git a/facility_management/views.py b/facility_management/views.py index 718621dd..1b2296c3 100644 --- a/facility_management/views.py +++ b/facility_management/views.py @@ -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 diff --git a/hospital_management/__pycache__/urls.cpython-312.pyc b/hospital_management/__pycache__/urls.cpython-312.pyc index c9378f24..bd65d38e 100644 Binary files a/hospital_management/__pycache__/urls.cpython-312.pyc and b/hospital_management/__pycache__/urls.cpython-312.pyc differ diff --git a/hospital_management/urls.py b/hospital_management/urls.py index 810507cc..c9437a41 100644 --- a/hospital_management/urls.py +++ b/hospital_management/urls.py @@ -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')), diff --git a/hr/__pycache__/urls.cpython-312.pyc b/hr/__pycache__/urls.cpython-312.pyc index b73a3b6b..6e136fe3 100644 Binary files a/hr/__pycache__/urls.cpython-312.pyc and b/hr/__pycache__/urls.cpython-312.pyc differ diff --git a/hr/__pycache__/views.cpython-312.pyc b/hr/__pycache__/views.cpython-312.pyc index b90705a9..151e6d9a 100644 Binary files a/hr/__pycache__/views.cpython-312.pyc and b/hr/__pycache__/views.cpython-312.pyc differ diff --git a/hr/templates/hr/dashboard.html b/hr/templates/hr/dashboard.html index 3a6d3c97..9be28991 100644 --- a/hr/templates/hr/dashboard.html +++ b/hr/templates/hr/dashboard.html @@ -90,72 +90,120 @@ {% endblock %} {% block content %} -
- -
-
-

- Human Resources Dashboard -

- -
-
- - + + Add Employee + + + Add Department + + + Create Schedule + + + Generate Report +
- +
+
-
- {% include "hr/partials/hr_stats.html" %} +
+
+
+
+
+
Total Employees
+
{{ total_employees }}
+
All staff members
+
+
+
+
+
+
+
+
Active Employees
+
{{ active_employees }}
+
Currently employed
+
+
+
+
+
+
+
+
Present Today
+
{{ employees_clocked_in }}
+
Currently employed
+
+
+
+
+
+
+
+
Departments
+
{{ departments.count }}
+
Active departments
+
+
+
+
+
+
+
+
Pending Reviews
+
{{ pending_reviews }}
+
Awaiting completion
+
+
+
+
+
+
+
+
Total Hours Today
+
{{ total_hours_today|floatformat:1 }}h
+
Logged work hours
+
+
+
-
-
-
-
- Recent Activity -
-
- - - View All - -
+
+
+

+ Recent Activity +

+
+ + + View All + + + + +
-
+
{% for employee in recent_hires %} @@ -217,13 +265,19 @@
-
-
-
+
+
+

Today's Attendance -

+
+
+ + + + +
-
+
{% include "hr/partials/attendance_summary.html" %}
@@ -232,13 +286,19 @@
-
-
-
+
+
+

Quick Actions -

+
+
+ + + + +
-
+
-
-
-
- Department Overview -
+
+ -
+
{% for dept in departments %}
@@ -286,9 +355,7 @@

No departments found

- - Add Department - +
{% endfor %}
@@ -296,13 +363,19 @@
-
-
-
- Pending Tasks -
+
+
+

+ Pending Tasks +

+
+ + + + +
-
+
{% if pending_reviews > 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');#} +{# });#} +{# }#} {% endblock %} diff --git a/hr/templates/hr/departments/department_list.html b/hr/templates/hr/departments/department_list.html index 1ea2f1fc..31303a3a 100644 --- a/hr/templates/hr/departments/department_list.html +++ b/hr/templates/hr/departments/department_list.html @@ -28,235 +28,240 @@ {% endblock %} {% block content %} - - - - -

Department Management view and manage hospital departments

- +
+
+

+ DepartmentManagement +

+

view and manage hospital departments.

+
+
+ +
+
+ +
-
- -
- -
- -
-

Departments

-
- - - -
-
- - - -
-
-
- - Add Department +
+ +
+ +
+ +
+

Departments

+
+ + Add Department -
- - -
+ + + +
-
-
-
- -
+ + + +
+
+
+
+ +
- - +
+
+
+ + +
+ +
-
- - -
-
Patient Problem
- - - - - - - - - - - - - {% for department in departments %} - - - - - - - - - - {% empty %} - - - - {% endfor %} - -
Department NameCodeTypeHeadEmployeesStatusActions
- {{ department.name }} - {% if department.parent_department %} - - {{ department.parent_department.name }} - - {% endif %} - {{ department.department_code }}{{ department.get_department_type_display }} - {% if department.department_head %} - - {{ department.department_head.get_full_name }} - - {% else %} - Not assigned - {% endif %} - - {{ department.employee_count }} - + + +
+ + + + + + + + + + + + + + {% for department in departments %} + + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Department NameCodeTypeHeadEmployeesStatusActions
+ {{ department.name }} + {% if department.parent_department %} + + {{ department.parent_department.name }} + + {% endif %} + {{ department.department_code }}{{ department.get_department_type_display }} + {% if department.department_head %} + + {{ department.department_head.get_full_name }} + + {% else %} + Not assigned + {% endif %} + + {{ department.employee_count }} + + {% if department.is_active %} + Active + {% else %} + Inactive + {% endif %} + + +
No departments found.
+ + + {% if is_paginated %} + {% include 'partial/pagination.html'%} + {% endif %} +
+ + +
-
+
+
+
+ Code: {{ department.department_code }}
+ Type: {{ department.get_department_type_display }} +
+ +
+ Department Head:
+ {% if department.department_head %} + + {{ department.department_head.get_full_name }} + + {% else %} + Not assigned + {% endif %} +
+ +
+ Parent Department:
+ {% if department.parent_department %} + + {{ department.parent_department.name }} + + {% else %} + None (Top Level) + {% endif %} +
+ +
+
+ {{ department.employee_count }} Employees +
+ {% if department.location %} +
+ {{ department.location }} +
+ {% endif %} +
+
+
No departments found.
- - - {% if is_paginated %} - {% include 'partial/pagination.html'%} - {% endif %} -
- - - - +
- +
- - + {% endblock %} {% block js %} diff --git a/hr/templates/hr/employees/employee_detail.html b/hr/templates/hr/employees/employee_detail.html index aacc380f..41043fc6 100644 --- a/hr/templates/hr/employees/employee_detail.html +++ b/hr/templates/hr/employees/employee_detail.html @@ -11,13 +11,14 @@

{{ employee.get_full_name }}

- +
+ + +
@@ -60,15 +61,16 @@

Employee Details

- - - + + + + +
-
-
-
+
+
Employee Information
@@ -135,8 +137,8 @@
-
-
+
+
Contact Information
@@ -195,9 +197,9 @@
- {% if employee.emergency_contact_name %} -
-
+ +
+
Emergency Contact
@@ -228,11 +230,11 @@
- {% endif %} + -
-
+
+
Recent Activity
@@ -285,13 +287,20 @@
-
-
-
- Quick Stats -
+
+
+

+ Quick Stats +

+
+ + + + +
-
+
+
Years of Service {{ employee.years_of_service|floatformat:1 }} @@ -312,14 +321,31 @@
-
-
-
- Quick Actions -
+
+
+

+ Quick Actions +

+
+ + + + +
-
+
+
+{# #} +{# Clock In#} +{# #} + +{# If you don't have open_entry_for_today, pass None and the partial will show "Clock In". #} + View Schedule diff --git a/hr/templates/hr/employees/employee_form.html b/hr/templates/hr/employees/employee_form.html index 4149039a..0999e18a 100644 --- a/hr/templates/hr/employees/employee_form.html +++ b/hr/templates/hr/employees/employee_form.html @@ -74,7 +74,6 @@ {{ form.father_name.errors.0 }}
{% endif %} -
Name the employee prefers to be called
diff --git a/hr/templates/hr/employees/employee_list.html b/hr/templates/hr/employees/employee_list.html index eaa7669e..7705addf 100644 --- a/hr/templates/hr/employees/employee_list.html +++ b/hr/templates/hr/employees/employee_list.html @@ -4,112 +4,54 @@ {% block title %}Employee Management - {{ block.super }}{% endblock %} {% block content %} -
- -
-
-

- Employee Management -

- -
+
+
+

+ EmployeeManagement +

+

Review employees and add new ones.

+
+ - - -
-
-
-
-
-
-
Total Employees
-

{{ total_employees }}

- All staff members -
-
- -
-
-
-
-
-
-
-
-
-
-
Active Employees
-

{{ active_employees }}

- Currently employed -
-
- -
-
-
-
-
-
-
-
-
-
-
New Hires
-

{{ new_hires_count }}

- This month -
-
- -
-
-
-
-
-
-
-
-
-
-
Departments
-

{{ departments_count }}

- Active departments -
-
- -
-
-
+
- +
+
+
+
+

+ Employee Management +

+
+ + + + +
+
+
-
-
+
+
Filters
@@ -165,8 +107,8 @@
-
-
+
+
Employees @@ -175,10 +117,10 @@ {% endif %}
- -
@@ -247,7 +189,7 @@
- {{ employee.employee_number }} + {{ employee.employee_id }} {% if employee.department %} @@ -326,7 +268,8 @@ {% endif %}
- +
+
- {% endblock %} - diff --git a/inpatients/templates/inpatients/discharges/discharge_planning.html b/inpatients/templates/inpatients/discharges/discharge_planning.html index eeb39bbc..f914b8ce 100644 --- a/inpatients/templates/inpatients/discharges/discharge_planning.html +++ b/inpatients/templates/inpatients/discharges/discharge_planning.html @@ -171,7 +171,7 @@
-
+
Patient: {{ admission.patient.get_full_name }}

MRN: {{ admission.patient.mrn }} | Age: {{ admission.patient.age }} | Gender: @@ -189,33 +189,46 @@

Expected Discharge: {{ admission.anticipated_discharge_date}}

+
+
+
+ + + + +
+
-
-
-

- Discharge Plan -

-
- - - - -
-
-
+
-
-
-

Discharge Timeline

+
+
+

+ Discharge Timeline +

+
+ + + + +
- {% %} +
today @@ -242,11 +255,19 @@
-
-
-
Discharge Checklist
+
+
+

+ Discharge Checklist +

+
+ + + + +
-
+
Medical Clearance
@@ -332,11 +353,19 @@
-
-
-
Risk Assessment
-
-
+
+
+

+ Risk Assessment +

+
+ + + + +
+
+
Readmission Risk: Moderate
Age >65, cardiac condition, multiple medications @@ -357,11 +386,19 @@
-
-
-
Discharge Care Team
+
+
+

+ Discharge Care Team +

+
+ + + + +
-
+
@@ -388,11 +425,19 @@
-
-
-
Quick Actions
+
+
+

+ Quick Actions +

+
+ + + + +
-
+
- - - +
-
-
+ {% endblock %} diff --git a/inpatients/templates/inpatients/partials/bed_details.html b/inpatients/templates/inpatients/partials/bed_details.html new file mode 100644 index 00000000..836fb0f9 --- /dev/null +++ b/inpatients/templates/inpatients/partials/bed_details.html @@ -0,0 +1,84 @@ + +
+
+ + + + + + + + + + + + + + + + + + + + + +
Bed Number:{{ object.bed_number }}
Ward: + + {{ object.ward.name }} + +
Room:{{ object.room_number|default:"Not assigned" }}
Bed Type:{{ object.get_bed_type_display }}
Status: + + {{ object.get_status_display }} + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
Features: + {% if object.has_oxygen %} + Oxygen + {% endif %} + {% if object.has_suction %} + Suction + {% endif %} + {% if object.has_monitor %} + Monitor + {% endif %} + {% if object.is_isolation %} + Isolation + {% endif %} + {% if not object.has_oxygen and not object.has_suction and not object.has_monitor and not object.is_isolation %} + Standard bed + {% endif %} +
Last Cleaned:{{ object.last_cleaned|date:"M d, Y g:i A"|default:"Not recorded" }}
Last Maintenance:{{ object.last_maintenance|date:"M d, Y g:i A"|default:"Not recorded" }}
Created:{{ object.created_at|date:"M d, Y g:i A" }}
Last Updated:{{ object.updated_at|date:"M d, Y g:i A" }}
+
+
+ + {% if object.notes %} +
+
Notes:
+
+ {{ object.notes }} +
+
+ {% endif %} diff --git a/inpatients/templates/inpatients/partials/update_bed_status_form.html b/inpatients/templates/inpatients/partials/update_bed_status_form.html index 229bec33..5eae8893 100644 --- a/inpatients/templates/inpatients/partials/update_bed_status_form.html +++ b/inpatients/templates/inpatients/partials/update_bed_status_form.html @@ -5,7 +5,7 @@
- {% if bed.current_patient %} + {% if bed.current_admission.patient %}
- This bed is currently occupied by {{ bed.current_patient.get_full_name }}. + This bed is currently occupied by {{ bed.current_admission.patient.get_full_name }}. Changing the status will affect the patient's admission.
{% endif %} diff --git a/inpatients/templates/inpatients/surgeries/surgery_schedule.html b/inpatients/templates/inpatients/surgeries/surgery_schedule.html index f64f9506..837b736c 100644 --- a/inpatients/templates/inpatients/surgeries/surgery_schedule.html +++ b/inpatients/templates/inpatients/surgeries/surgery_schedule.html @@ -4,17 +4,35 @@ {% block title %}Surgery Schedule - {{ block.super }}{% endblock %} {% block content %} +
+
+

+ SurgeriesSchedule +

+

Comprehensive discharge planning and coordination.

+
+
+
+ +
+
+
-
-
-

- Surgery Schedule +
+
+

+ List

+
+ + + + +
- -
+
@@ -52,12 +70,13 @@
-
+
- + + @@ -81,6 +100,12 @@ +
Date/Time PatientRoom Procedure Surgeon Type + {{ surgery.operating_room.room_name }} + {% if surgery.operating_room.room_name %} +
Room No.: {{ surgery.operating_room.room_number }} + {% endif %} +
{{ surgery.procedure_name }} {% if surgery.procedure_code %}
Code: {{ surgery.procedure_code }} @@ -125,9 +150,9 @@ {% elif surgery.status == 'CONFIRMED' %} - {% endif %} - + {% if surgery.status not in 'COMPLETED,CANCELLED' %} -{# #} +
-
- {% if is_paginated %} - + {% include 'partial/pagination.html' %} {% endif %} +
diff --git a/inpatients/templates/inpatients/transfers/transfer_form.html b/inpatients/templates/inpatients/transfers/transfer_form.html index e9b9ecb8..ff24e4dd 100644 --- a/inpatients/templates/inpatients/transfers/transfer_form.html +++ b/inpatients/templates/inpatients/transfers/transfer_form.html @@ -1,11 +1,8 @@ {% load static %} - - -
+ +
+
+

+ Bed Layout +

+
+ + + + +
-
+
{% for bed in ward.beds.all %}
{{ bed.bed_number }}
- + {{ bed.get_status_display }}
- {% if bed.current_patient %} + {% if bed.current_admission.patient %}
- {{ bed.current_patient.get_full_name }} + {{ bed.current_admission.patient.get_full_name }}
{% endif %}
@@ -179,16 +182,20 @@
-
-
-
-
- Current Patients -
- {{ current_patients.count }} patients +
+
+

+ Current Patients +

+
+ {{ recent_admissions.count }} patients + + + +
-
+
@@ -202,10 +209,12 @@ - {% for admission in current_patients %} + {% for admission in recent_admissions %} @@ -281,18 +284,24 @@
-
-
-
+
+
+

Ward Statistics -

+
+
+ + + + +
-
+
-

{{ ward.available_beds }}

+

{{ available_beds }}

Available
@@ -300,7 +309,7 @@
-

{{ ward.occupied_beds }}

+

{{ occupied_beds }}

Occupied
@@ -308,15 +317,15 @@
-

{{ ward.maintenance_beds }}

+

{{ maintenance_beds }}

Maintenance
-
+
-

{{ ward.reserved_beds }}

+

{{ reserved_beds }}

Reserved
@@ -328,7 +337,7 @@
- {{ ward.occupancy_rate|floatformat:1 }}% + {{ ward.occupancy_rate|floatformat:0 }}%
{{ ward.occupied_beds }} of {{ ward.total_beds }} beds occupied @@ -337,13 +346,19 @@
-
-
-
+
+
+

Quick Actions -

+
+
+ + + + +
-
+
-
-
-
+
+
+

Ward Staff -

+
+
+ + + + +
-
+
{% if ward.nurse_manager %}
@@ -422,6 +443,19 @@
+ +
- {{ admission.bed.bed_number }} +

{{ admission.current_bed.bed_number }} + {{ admission.current_bed.bed_position }} +

@@ -216,17 +225,17 @@
{{ admission.patient.get_full_name }}
- {{ admission.patient.patient_id }} + {{ admission.patient.mrn }}
-
{{ admission.admission_date|date:"M d, Y" }}
- {{ admission.admission_date|timesince }} ago +
{{ admission.admission_datetime|date:"M d, Y" }}
+ {{ admission.admission_datetime|timesince }} ago
- {% if admission.primary_diagnosis %} - {{ admission.primary_diagnosis|truncatechars:30 }} + {% if admission.admitting_diagnosis %} + {{ admission.admitting_diagnosis|truncatechars:30 }} {% else %} Not specified {% endif %} @@ -244,21 +253,15 @@ class="btn btn-outline-primary btn-sm" title="View Details"> - - + + + + + + + + +