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
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:
690
backend/src/modules/healthcare/services/appointment_service.py
Normal file
690
backend/src/modules/healthcare/services/appointment_service.py
Normal 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
|
||||
)
|
||||
Reference in New Issue
Block a user