📄 event_conflict.py 5,838 bytes Apr 29, 2026 📋 Raw

"""
Conflict detection module for family calendar events.

Detects when events share the same day and have overlapping family members.
"""

from datetime import datetime
from typing import List, Dict, Any, Optional

def parse_date(date_str: str) -> datetime:
"""Parse a date string (YYYY-MM-DD) into a datetime object."""
return datetime.strptime(date_str, "%Y-%m-%d")

def get_day_label(start_date: str, reference_date: Optional[datetime] = None) -> str:
"""
Compute a dynamic day label from a start date.

Returns:
    - "Today" if the date is today
    - "Tomorrow" if the date is tomorrow
    - "Mon May 05" format for other dates
"""
if reference_date is None:
    reference_date = datetime.now()

event_date = parse_date(start_date)

# Strip time for day comparison
ref_date = reference_date.replace(hour=0, minute=0, second=0, microsecond=0)
evt_date = event_date.replace(hour=0, minute=0, second=0, microsecond=0)

delta_days = (evt_date - ref_date).days

if delta_days == 0:
    return "Today"
elif delta_days == 1:
    return "Tomorrow"
else:
    return evt_date.strftime("%a %b %d")

def same_day(date1: str, date2: str) -> bool:
"""Check if two date strings represent the same day."""
d1 = parse_date(date1)
d2 = parse_date(date2)
return d1.date() == d2.date()

def has_shared_members(event1: Dict[str, Any], event2: Dict[str, Any]) -> bool:
"""
Check if two events share any family members.

If either event lacks family_members field, returns False (no person conflict).
"""
members1 = event1.get("family_members", [])
members2 = event2.get("family_members", [])

# If either has no family members defined, no person-level conflict
if not members1 or not members2:
    return False

set1 = set(members1)
set2 = set(members2)

return bool(set1 & set2)

def find_conflicts(event: Dict[str, Any], all_events: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""
Find all events that conflict with the given event.

Conflict rules:
1. Events must be on the same day (naive date-only check for now)
2. Events must NOT be the same event (different IDs)
3. If both events have family_members, they must share at least one member
4. If neither has family_members, it's a same-day conflict (fallback)

Args:
    event: The event to check for conflicts
    all_events: List of all events to check against

Returns:
    List of conflicting events (minimal dict with id, title, family_members)
"""
conflicts = []

event_id = event.get("id")
event_date = event.get("start")
event_members = event.get("family_members", [])

if not event_date:
    return conflicts

for other_event in all_events:
    other_id = other_event.get("id")
    other_date = other_event.get("start")
    other_members = other_event.get("family_members", [])

    # Skip self
    if other_id == event_id:
        continue

    # Skip if no date
    if not other_date:
        continue

    # Check same day
    if not same_day(event_date, other_date):
        continue

    # Determine conflict type
    if event_members and other_members:
        # Both have family members - check for shared members
        if has_shared_members(event, other_event):
            conflicts.append({
                "id": other_id,
                "title": other_event.get("title", "Untitled"),
                "family_members": other_members,
                "conflict_type": "person"  # Same person, same day
            })
    elif event_members or other_members:
        # One has family members, one doesn't - only same-day conflict
        # This is a "same-day" conflict (yellow warning)
        conflicts.append({
            "id": other_id,
            "title": other_event.get("title", "Untitled"),
            "family_members": other_members,
            "conflict_type": "same_day"
        })
    else:
        # Neither has family members - same-day conflict only
        conflicts.append({
            "id": other_id,
            "title": other_event.get("title", "Untitled"),
            "family_members": [],
            "conflict_type": "same_day"
        })

return conflicts

def add_conflict_data(event: Dict[str, Any], all_events: List[Dict[str, Any]], reference_date: Optional[datetime] = None) -> Dict[str, Any]:
"""
Enrich an event with conflict data and dynamic day label.

Args:
    event: The event to enrich
    all_events: List of all events for conflict detection
    reference_date: Optional reference date for day label calculation

Returns:
    Event dict with added 'conflicts' array and computed 'day_label'
"""
enriched = event.copy()

# Compute dynamic day label
start_date = event.get("start", "")
if start_date:
    enriched["day_label"] = get_day_label(start_date, reference_date)

# Find conflicts
enriched["conflicts"] = find_conflicts(event, all_events)

return enriched

def add_conflicts_to_all_events(events: List[Dict[str, Any]], reference_date: Optional[datetime] = None) -> List[Dict[str, Any]]:
"""
Process all events, adding conflict data and dynamic day labels to each.

Args:
    events: List of all events
    reference_date: Optional reference date for day label calculation

Returns:
    List of enriched event dicts
"""
return [add_conflict_data(event, events, reference_date) for event in events]