""" Calendar service for aggregating date-based events from the foundation management system """ from datetime import date, timedelta import calendar as cal try: from django.utils import timezone except ImportError: from datetime import datetime as timezone timezone.now = datetime.now try: from stiftung.models import ( DestinataerUnterstuetzung, LandVerpachtung, Destinataer, StiftungsKalenderEintrag ) except ImportError: # Fallback for when Django apps aren't ready DestinataerUnterstuetzung = None LandVerpachtung = None Destinataer = None StiftungsKalenderEintrag = None class StiftungsKalenderService: """Service to aggregate calendar events from various models""" def __init__(self): self.today = timezone.now().date() def get_calendar_events(self, start_date=None, end_date=None): """ Get custom calendar entries only within date range Returns list of event objects """ if not start_date: start_date = self.today if not end_date: end_date = start_date + timedelta(days=30) return self._get_custom_calendar_events(start_date, end_date) def get_all_events(self, start_date=None, end_date=None): """ Get all calendar events from all sources within date range Returns list of event dictionaries """ if not start_date: start_date = self.today if not end_date: end_date = start_date + timedelta(days=30) events = [] # Add support payment due dates events.extend(self.get_support_payment_events(start_date, end_date)) # Add lease expiration dates events.extend(self.get_lease_events(start_date, end_date)) # Add birthdays events.extend(self.get_birthday_events(start_date, end_date)) # Add custom calendar entries events.extend(self.get_calendar_events(start_date, end_date)) # Sort by date events.sort(key=lambda x: (x.date, getattr(x, 'time', '00:00'))) return events def get_support_payment_events(self, start_date, end_date): """Get support payment due dates""" if not DestinataerUnterstuetzung: return [] events = [] payments = DestinataerUnterstuetzung.objects.filter( faellig_am__range=[start_date, end_date], status__in=['geplant', 'faellig'] ).select_related('destinataer') for payment in payments: is_overdue = payment.is_overdue() if hasattr(payment, 'is_overdue') else False class PaymentEvent: def __init__(self, payment_obj, overdue): self.id = f'payment_{payment_obj.id}' self.title = f'Zahlung an {payment_obj.destinataer.get_full_name()}' self.description = f'€{payment_obj.betrag} - {payment_obj.beschreibung}' self.date = payment_obj.faellig_am self.time = None self.category = 'zahlung' self.category_display = 'Zahlung' self.priority = 'hoch' if overdue else 'normal' self.priority_display = 'Hoch' if overdue else 'Normal' self.icon = 'fas fa-euro-sign' self.color = 'danger' if overdue else 'warning' self.source = 'payment' self.overdue = overdue self.completed = False events.append(PaymentEvent(payment, is_overdue)) return events def get_lease_events(self, start_date, end_date): """Get lease start/end dates""" if not LandVerpachtung: return [] events = [] # Lease expirations leases = LandVerpachtung.objects.filter( pachtende__range=[start_date, end_date], status='aktiv' ).select_related('paechter', 'land') for lease in leases: # Check if expiring soon (within 90 days) days_until_expiry = (lease.pachtende - self.today).days if lease.pachtende else 999 priority = 'hoch' if days_until_expiry <= 30 else 'mittel' if days_until_expiry <= 90 else 'normal' class LeaseEvent: def __init__(self, lease_obj, priority_val): self.id = f'lease_end_{lease_obj.id}' self.title = f'Pachtende: {lease_obj.paechter.get_full_name()}' self.description = f'{lease_obj.land.bezeichnung} - {lease_obj.verpachtete_flaeche}m²' self.date = lease_obj.pachtende self.time = None self.category = 'vertrag' self.category_display = 'Vertrag' self.priority = priority_val self.priority_display = 'Hoch' if priority_val == 'hoch' else 'Mittel' if priority_val == 'mittel' else 'Normal' self.icon = 'fas fa-file-contract' self.color = 'danger' if priority_val == 'hoch' else 'warning' if priority_val == 'mittel' else 'info' self.source = 'lease' self.overdue = False self.completed = False events.append(LeaseEvent(lease, priority)) return events def get_birthday_events(self, start_date, end_date): """Get birthdays within date range""" if not Destinataer: return [] events = [] # Get all destinatäre with birth dates destinataere = Destinataer.objects.filter( geburtsdatum__isnull=False ) for destinataer in destinataere: # Calculate birthday for each year in range birth_date = destinataer.geburtsdatum for year in range(start_date.year, end_date.year + 1): try: birthday_this_year = birth_date.replace(year=year) if start_date <= birthday_this_year <= end_date: age = year - birth_date.year class BirthdayEvent: def __init__(self, person, birthday_date, age_val): self.id = f'birthday_{person.id}_{year}' self.title = f'🎂 {person.get_full_name()}' self.description = f'{age_val}. Geburtstag' self.date = birthday_date self.time = None self.category = 'geburtstag' self.category_display = 'Geburtstag' self.priority = 'normal' self.priority_display = 'Normal' self.icon = 'fas fa-birthday-cake' self.color = 'success' self.source = 'birthday' self.overdue = False self.completed = False events.append(BirthdayEvent(destinataer, birthday_this_year, age)) except ValueError: # Handle leap year edge case (Feb 29) pass return events def _get_custom_calendar_events(self, start_date, end_date): """Get custom calendar entries""" if not StiftungsKalenderEintrag: return [] events = [] calendar_entries = StiftungsKalenderEintrag.objects.filter( datum__range=[start_date, end_date] ).select_related('destinataer', 'verpachtung') for entry in calendar_entries: class CustomEvent: def __init__(self, entry_obj, today_date): self.id = entry_obj.id self.title = entry_obj.titel self.description = entry_obj.beschreibung self.date = entry_obj.datum self.time = entry_obj.uhrzeit self.category = entry_obj.kategorie self.category_display = entry_obj.get_kategorie_display() self.priority = entry_obj.prioritaet self.priority_display = entry_obj.get_prioritaet_display() self.icon = self._get_kategorie_icon(entry_obj.kategorie) self.color = self._get_prioritaet_color(entry_obj.prioritaet) self.source = 'custom' self.completed = entry_obj.erledigt self.overdue = entry_obj.datum < today_date if entry_obj.datum else False def _get_kategorie_icon(self, kategorie): icons = { 'termin': 'fas fa-calendar-alt', 'zahlung': 'fas fa-euro-sign', 'deadline': 'fas fa-exclamation-triangle', 'geburtstag': 'fas fa-birthday-cake', 'vertrag': 'fas fa-file-contract', 'pruefung': 'fas fa-search', } return icons.get(kategorie, 'fas fa-calendar') def _get_prioritaet_color(self, prioritaet): colors = { 'niedrig': 'secondary', 'normal': 'primary', 'mittel': 'warning', 'hoch': 'danger', } return colors.get(prioritaet, 'primary') events.append(CustomEvent(entry, self.today)) return events def get_upcoming_events(self, days=7): """Get upcoming events within specified days""" end_date = self.today + timedelta(days=days) return self.get_calendar_events(self.today, end_date) def get_overdue_events(self): """Get overdue/past due events""" events = self.get_calendar_events( start_date=self.today - timedelta(days=30), end_date=self.today - timedelta(days=1) ) return [event for event in events if event.get('overdue', False)] def get_events_for_month(self, year, month): """Get all events for a specific month""" from calendar import monthrange start_date = date(year, month, 1) _, last_day = monthrange(year, month) end_date = date(year, month, last_day) return self.get_calendar_events(start_date, end_date)