project initialization
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled

This commit is contained in:
2025-10-05 02:37:33 +08:00
parent 2cbb6d5fa1
commit b3fff546e9
226 changed files with 97805 additions and 35 deletions

View File

@@ -0,0 +1,690 @@
"""
Healthcare Appointment Service
Handles appointment scheduling, management, and healthcare operations
"""
from django.db import transaction, models
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.contrib.auth import get_user_model
from datetime import datetime, timedelta
from core.models.tenant import Tenant
from ..models.patient import Patient
from ..models.appointment import (
Appointment, AppointmentResource, AppointmentNote
)
User = get_user_model()
class AppointmentService:
"""
Service class for managing healthcare appointments
"""
@transaction.atomic
def create_appointment(self, tenant: Tenant, appointment_data: dict, created_by=None) -> Appointment:
"""
Create a new appointment with validation
Args:
tenant: The tenant organization
appointment_data: Appointment information dictionary
created_by: User creating the appointment
Returns:
Appointment: Created appointment instance
Raises:
ValidationError: If appointment data is invalid
"""
try:
# Generate unique appointment ID
appointment_id = self._generate_appointment_id(tenant)
# Extract and validate resources and notes
resources = appointment_data.pop('resources', [])
notes = appointment_data.pop('notes', [])
# Validate appointment time
self._validate_appointment_time(appointment_data)
# Create appointment
appointment = Appointment.objects.create(
tenant=tenant,
appointment_id=appointment_id,
created_by=created_by,
**appointment_data
)
# Create resources
for resource_data in resources:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
# Create notes
for note_data in notes:
AppointmentNote.objects.create(
appointment=appointment,
created_by=created_by,
**note_data
)
return appointment
except Exception as e:
raise ValidationError(f"Failed to create appointment: {str(e)}")
def update_appointment(self, appointment: Appointment, appointment_data: dict, updated_by=None) -> Appointment:
"""
Update appointment information
Args:
appointment: Appointment instance to update
appointment_data: Updated appointment information
updated_by: User updating the appointment
Returns:
Appointment: Updated appointment instance
"""
try:
# Validate appointment time if being updated
if 'appointment_time' in appointment_data or 'appointment_date' in appointment_data:
temp_data = appointment_data.copy()
if 'appointment_time' not in temp_data:
temp_data['appointment_time'] = appointment.appointment_time
if 'appointment_date' not in temp_data:
temp_data['appointment_date'] = appointment.appointment_date
self._validate_appointment_time(temp_data)
# Handle resources updates
if 'resources' in appointment_data:
self._update_resources(appointment, appointment_data.pop('resources'))
# Handle notes updates
if 'notes' in appointment_data:
self._update_notes(appointment, appointment_data.pop('notes'), updated_by)
# Update appointment fields
for field, value in appointment_data.items():
if hasattr(appointment, field):
setattr(appointment, field, value)
appointment.updated_by = updated_by
appointment.save()
return appointment
except Exception as e:
raise ValidationError(f"Failed to update appointment: {str(e)}")
def get_appointment_by_id(self, tenant: Tenant, appointment_id: str) -> Appointment:
"""
Get appointment by ID within tenant
Args:
tenant: Tenant organization
appointment_id: Appointment identifier
Returns:
Appointment: Appointment instance
Raises:
Appointment.DoesNotExist: If appointment not found
"""
return Appointment.objects.get(
tenant=tenant,
appointment_id=appointment_id
)
def get_patient_appointments(self, patient: Patient, start_date=None, end_date=None, status=None):
"""
Get appointments for a specific patient
Args:
patient: Patient instance
start_date: Start date filter
end_date: End date filter
status: Status filter
Returns:
QuerySet: Filtered appointments
"""
queryset = Appointment.objects.filter(patient=patient)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
if status:
queryset = queryset.filter(status=status)
return queryset.order_by('appointment_date', 'appointment_time')
def get_doctor_appointments(self, doctor: User, start_date=None, end_date=None, status=None):
"""
Get appointments for a specific doctor
Args:
doctor: Doctor user instance
start_date: Start date filter
end_date: End date filter
status: Status filter
Returns:
QuerySet: Filtered appointments
"""
queryset = Appointment.objects.filter(doctor=doctor)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
if status:
queryset = queryset.filter(status=status)
return queryset.order_by('appointment_date', 'appointment_time')
def get_available_time_slots(self, tenant: Tenant, doctor: User, date: datetime.date):
"""
Get available time slots for a doctor on a specific date
Args:
tenant: Tenant organization
doctor: Doctor user instance
date: Date to check availability
Returns:
list: Available time slots
"""
# Get existing appointments for the day
existing_appointments = Appointment.objects.filter(
tenant=tenant,
doctor=doctor,
appointment_date=date,
status__in=['scheduled', 'confirmed', 'in_progress']
)
# Define working hours (9 AM to 5 PM)
working_hours_start = datetime.time(9, 0)
working_hours_end = datetime.time(17, 0)
slot_duration = 30 # 30-minute slots
available_slots = []
current_time = datetime.combine(date, working_hours_start)
end_time = datetime.combine(date, working_hours_end)
while current_time < end_time:
slot_end = current_time + timedelta(minutes=slot_duration)
# Check if slot is available
is_available = True
for appointment in existing_appointments:
appt_start = datetime.combine(date, appointment.appointment_time)
appt_end = appt_start + timedelta(minutes=appointment.duration)
if not (slot_end <= appt_start or current_time >= appt_end):
is_available = False
break
if is_available:
available_slots.append({
'start_time': current_time.time(),
'end_time': slot_end.time(),
'duration': slot_duration
})
current_time = slot_end
return available_slots
def check_appointment_conflicts(self, tenant: Tenant, doctor: User, appointment_date: datetime.date,
appointment_time: datetime.time, duration: int, exclude_appointment=None):
"""
Check for appointment conflicts
Args:
tenant: Tenant organization
doctor: Doctor user instance
appointment_date: Appointment date
appointment_time: Appointment time
duration: Appointment duration in minutes
exclude_appointment: Appointment to exclude from conflict check
Returns:
list: List of conflicting appointments
"""
start_datetime = datetime.combine(appointment_date, appointment_time)
end_datetime = start_datetime + timedelta(minutes=duration)
# Get overlapping appointments
conflicting_appointments = Appointment.objects.filter(
tenant=tenant,
doctor=doctor,
appointment_date=appointment_date,
status__in=['scheduled', 'confirmed', 'in_progress']
)
if exclude_appointment:
conflicting_appointments = conflicting_appointments.exclude(id=exclude_appointment.id)
conflicts = []
for appointment in conflicting_appointments:
appt_start = datetime.combine(appointment.appointment_date, appointment.appointment_time)
appt_end = appt_start + timedelta(minutes=appointment.duration)
# Check for overlap
if not (end_datetime <= appt_start or start_datetime >= appt_end):
conflicts.append(appointment)
return conflicts
def schedule_appointment(self, tenant: Tenant, patient: Patient, doctor: User,
appointment_date: datetime.date, appointment_time: datetime.time,
duration: int = 30, appointment_type: str = 'consultation',
reason_for_visit: str = '', created_by=None) -> Appointment:
"""
Schedule a new appointment with conflict checking
Args:
tenant: Tenant organization
patient: Patient instance
doctor: Doctor user instance
appointment_date: Appointment date
appointment_time: Appointment time
duration: Appointment duration in minutes
appointment_type: Type of appointment
reason_for_visit: Reason for visit
created_by: User creating the appointment
Returns:
Appointment: Created appointment
Raises:
ValidationError: If there are scheduling conflicts
"""
# Check for conflicts
conflicts = self.check_appointment_conflicts(
tenant, doctor, appointment_date, appointment_time, duration
)
if conflicts:
conflict_times = [f"{conf.appointment_time} ({conf.duration}min)" for conf in conflicts]
raise ValidationError(
f"Scheduling conflict with appointments at: {', '.join(conflict_times)}"
)
# Create appointment
appointment_data = {
'patient': patient,
'doctor': doctor,
'appointment_date': appointment_date,
'appointment_time': appointment_time,
'duration': duration,
'appointment_type': appointment_type,
'reason_for_visit': reason_for_visit,
'status': 'scheduled'
}
return self.create_appointment(tenant, appointment_data, created_by)
def reschedule_appointment(self, appointment: Appointment, new_date: datetime.date,
new_time: datetime.time, rescheduled_by=None):
"""
Reschedule an existing appointment
Args:
appointment: Appointment to reschedule
new_date: New appointment date
new_time: New appointment time
rescheduled_by: User rescheduling the appointment
Returns:
Appointment: Updated appointment
Raises:
ValidationError: If there are scheduling conflicts
"""
# Check for conflicts
conflicts = self.check_appointment_conflicts(
appointment.tenant, appointment.doctor, new_date, new_time,
appointment.duration, exclude_appointment=appointment
)
if conflicts:
conflict_times = [f"{conf.appointment_time} ({conf.duration}min)" for conf in conflicts]
raise ValidationError(
f"Scheduling conflict with appointments at: {', '.join(conflict_times)}"
)
# Create a new appointment record
old_appointment_data = {
'patient': appointment.patient,
'doctor': appointment.doctor,
'appointment_date': new_date,
'appointment_time': new_time,
'duration': appointment.duration,
'appointment_type': appointment.appointment_type,
'reason_for_visit': appointment.reason_for_visit,
'status': 'scheduled',
'notes': f'Rescheduled from {appointment.appointment_date} at {appointment.appointment_time}',
'rescheduled_from': appointment
}
new_appointment = self.create_appointment(
appointment.tenant, old_appointment_data, rescheduled_by
)
# Cancel the old appointment
appointment.cancel_appointment('Rescheduled', rescheduled_by)
return new_appointment
def cancel_appointment(self, appointment: Appointment, reason: str, cancelled_by=None):
"""
Cancel an appointment
Args:
appointment: Appointment to cancel
reason: Cancellation reason
cancelled_by: User cancelling the appointment
"""
if not appointment.can_be_cancelled():
raise ValidationError("This appointment cannot be cancelled")
appointment.cancel_appointment(reason, cancelled_by)
def send_appointment_reminders(self, tenant: Tenant, hours_before: int = 24):
"""
Send appointment reminders
Args:
tenant: Tenant organization
hours_before: Hours before appointment to send reminder
Returns:
dict: Reminder sending statistics
"""
reminder_time = timezone.now() + timedelta(hours=hours_before)
# Get appointments that need reminders
appointments = Appointment.objects.filter(
tenant=tenant,
appointment_date=reminder_time.date(),
status__in=['scheduled', 'confirmed'],
reminder_sent=False
).filter(
models.Q(appointment_time__hour=reminder_time.hour) |
models.Q(appointment_time__hour=reminder_time.hour - 1)
)
sent_count = 0
failed_count = 0
for appointment in appointments:
try:
# Send reminder (integrate with email/SMS service)
appointment.send_reminder()
sent_count += 1
except Exception as e:
failed_count += 1
# Log error
print(f"Failed to send reminder for appointment {appointment.appointment_id}: {e}")
return {
'total_appointments': appointments.count(),
'sent': sent_count,
'failed': failed_count
}
def get_appointment_statistics(self, tenant: Tenant, start_date=None, end_date=None):
"""
Get appointment statistics for a tenant
Args:
tenant: Tenant organization
start_date: Start date for statistics
end_date: End date for statistics
Returns:
dict: Appointment statistics
"""
queryset = Appointment.objects.filter(tenant=tenant)
if start_date:
queryset = queryset.filter(appointment_date__gte=start_date)
if end_date:
queryset = queryset.filter(appointment_date__lte=end_date)
# Basic counts
total_appointments = queryset.count()
completed_appointments = queryset.filter(status='completed').count()
cancelled_appointments = queryset.filter(status='cancelled').count()
no_show_appointments = queryset.filter(status='no_show').count()
# Status distribution
status_distribution = {}
for status_choice in Appointment.STATUS_CHOICES:
status_code = status_choice[0]
count = queryset.filter(status=status_code).count()
status_distribution[status_code] = {
'name': status_choice[1],
'count': count,
'percentage': (count / total_appointments * 100) if total_appointments > 0 else 0
}
# Type distribution
type_distribution = {}
for type_choice in Appointment.APPOINTMENT_TYPE_CHOICES:
type_code = type_choice[0]
count = queryset.filter(appointment_type=type_code).count()
type_distribution[type_code] = {
'name': type_choice[1],
'count': count,
'percentage': (count / total_appointments * 100) if total_appointments > 0 else 0
}
# Doctor workload
doctor_workload = {}
doctors = User.objects.filter(
id__in=queryset.values_list('doctor', flat=True)
).distinct()
for doctor in doctors:
doctor_appointments = queryset.filter(doctor=doctor)
doctor_workload[doctor.id] = {
'name': f"{doctor.first_name} {doctor.last_name}",
'total_appointments': doctor_appointments.count(),
'completed_appointments': doctor_appointments.filter(status='completed').count(),
'average_duration': doctor_appointments.aggregate(
avg_duration=models.Avg('duration')
)['avg_duration'] or 0
}
# Daily trends
daily_trends = {}
if start_date and end_date:
current_date = start_date
while current_date <= end_date:
day_count = queryset.filter(appointment_date=current_date).count()
daily_trends[current_date.strftime('%Y-%m-%d')] = day_count
current_date += timedelta(days=1)
return {
'total_appointments': total_appointments,
'completed_appointments': completed_appointments,
'cancelled_appointments': cancelled_appointments,
'no_show_appointments': no_show_appointments,
'completion_rate': (completed_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'cancellation_rate': (cancelled_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'no_show_rate': (no_show_appointments / total_appointments * 100) if total_appointments > 0 else 0,
'status_distribution': status_distribution,
'type_distribution': type_distribution,
'doctor_workload': doctor_workload,
'daily_trends': daily_trends
}
def check_in_patient(self, appointment: Appointment, checked_in_by=None):
"""
Check in patient for appointment
Args:
appointment: Appointment instance
checked_in_by: User checking in the patient
"""
appointment.check_in(checked_in_by)
def start_appointment(self, appointment: Appointment, started_by=None):
"""
Start appointment
Args:
appointment: Appointment instance
started_by: User starting the appointment
"""
appointment.start_appointment(started_by)
def complete_appointment(self, appointment: Appointment, completed_by=None,
diagnosis: str = '', treatment_plan: str = '', notes: str = ''):
"""
Complete appointment
Args:
appointment: Appointment instance
completed_by: User completing the appointment
diagnosis: Diagnosis information
treatment_plan: Treatment plan
notes: Additional notes
"""
# Update appointment with completion details
if diagnosis:
appointment.diagnosis = diagnosis
if treatment_plan:
appointment.treatment_plan = treatment_plan
if notes:
appointment.notes = notes
appointment.complete_appointment(completed_by)
def add_appointment_note(self, appointment: Appointment, note_type: str, title: str,
content: str, created_by=None, is_confidential=False):
"""
Add note to appointment
Args:
appointment: Appointment instance
note_type: Type of note
title: Note title
content: Note content
created_by: User creating the note
is_confidential: Whether note is confidential
"""
AppointmentNote.objects.create(
appointment=appointment,
note_type=note_type,
title=title,
content=content,
is_confidential=is_confidential,
created_by=created_by
)
def _generate_appointment_id(self, tenant: Tenant) -> str:
"""
Generate unique appointment ID for tenant
Args:
tenant: Tenant organization
Returns:
str: Unique appointment ID
"""
# Use tenant slug + date + sequence
tenant_slug = tenant.slug.upper()
today = timezone.now().strftime('%Y%m%d')
# Get today's appointment count
today_count = Appointment.objects.filter(
tenant=tenant,
appointment_date=timezone.now().date()
).count()
sequence = today_count + 1
return f"{tenant_slug}-{today}-{sequence:03d}"
def _validate_appointment_time(self, appointment_data: dict):
"""
Validate appointment time
Args:
appointment_data: Appointment data dictionary
Raises:
ValidationError: If appointment time is invalid
"""
appointment_date = appointment_data.get('appointment_date')
appointment_time = appointment_data.get('appointment_time')
if not appointment_date or not appointment_time:
raise ValidationError("Appointment date and time are required")
# Check if appointment is in the past
appointment_datetime = datetime.combine(appointment_date, appointment_time)
if appointment_datetime < timezone.now():
raise ValidationError("Appointment cannot be scheduled in the past")
# Check if appointment is outside working hours (9 AM to 6 PM)
if appointment_time.hour < 9 or appointment_time.hour >= 18:
raise ValidationError("Appointment must be scheduled between 9 AM and 6 PM")
def _update_resources(self, appointment: Appointment, resources_data: list):
"""
Update resources for appointment
Args:
appointment: Appointment instance
resources_data: List of resource data
"""
# Remove existing resources not in the update
existing_ids = [resource.get('id') for resource in resources_data if 'id' in resource]
appointment.resources.exclude(id__in=existing_ids).delete()
# Update or create resources
for resource_data in resources_data:
resource_id = resource_data.pop('id', None)
if resource_id:
try:
resource = appointment.resources.get(id=resource_id)
for field, value in resource_data.items():
setattr(resource, field, value)
resource.save()
except AppointmentResource.DoesNotExist:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
else:
AppointmentResource.objects.create(
appointment=appointment,
**resource_data
)
def _update_notes(self, appointment: Appointment, notes_data: list, created_by=None):
"""
Update notes for appointment
Args:
appointment: Appointment instance
notes_data: List of note data
created_by: User creating notes
"""
# Add new notes (notes are not updated, only added)
for note_data in notes_data:
if 'id' not in note_data: # Only create new notes
AppointmentNote.objects.create(
appointment=appointment,
created_by=created_by,
**note_data
)