diff --git a/agenda/trip_schengen.py b/agenda/trip_schengen.py index 6a1e2d8..83b35d9 100644 --- a/agenda/trip_schengen.py +++ b/agenda/trip_schengen.py @@ -1,17 +1,14 @@ """Integration of Schengen calculator with the existing trip system.""" +import logging import typing from datetime import date, timedelta import flask from . import trip -from .schengen import ( - SchengenCalculation, - calculate_schengen_time, - extract_schengen_stays_from_travel, -) -from .types import StrDict, Trip +from .schengen import calculate_schengen_time, extract_schengen_stays_from_travel +from .types import SchengenCalculation, SchengenStay, StrDict, Trip def add_schengen_compliance_to_trip(trip_obj: Trip) -> Trip: @@ -24,8 +21,9 @@ def add_schengen_compliance_to_trip(trip_obj: Trip) -> Trip: trip_obj.schengen_compliance = calculation except Exception as e: # Log the error but don't fail the trip loading - import logging - logging.warning(f"Failed to calculate Schengen compliance for trip {trip_obj.start}: {e}") + logging.warning( + f"Failed to calculate Schengen compliance for trip {trip_obj.start}: {e}" + ) trip_obj.schengen_compliance = None return trip_obj @@ -82,6 +80,47 @@ def generate_schengen_warnings(trip_list: list[Trip]) -> list[dict[str, typing.A return warnings +def extract_schengen_stays_with_trip_info( + trip_list: list[Trip], +) -> list[SchengenStay]: + """Extract Schengen stays from travel items with trip information.""" + # Create a mapping of travel items to their trips + travel_to_trip_map = {} + for trip_obj in trip_list: + for travel_item in trip_obj.travel: + travel_to_trip_map[id(travel_item)] = trip_obj + + # Collect all travel items + all_travel_items = [] + for trip_obj in trip_list: + all_travel_items.extend(trip_obj.travel) + + # Get stays with trip information + stays = extract_schengen_stays_from_travel(all_travel_items) + + # Try to associate stays with trips based on entry dates + for stay in stays: + # Find the trip that contains this stay's entry date + for trip_obj in trip_list: + if trip_obj.start <= stay.entry_date <= (trip_obj.end or stay.entry_date): + stay.trip_date = trip_obj.start + stay.trip_name = trip_obj.title + break + + # If no exact match, find the closest trip by start date + if stay.trip_date is None: + closest_trip = min( + trip_list, + key=lambda t: abs((t.start - stay.entry_date).days), + default=None, + ) + if closest_trip: + stay.trip_date = closest_trip.start + stay.trip_name = closest_trip.title + + return stays + + def schengen_dashboard_data(data_dir: str | None = None) -> dict[str, typing.Any]: """Generate dashboard data for Schengen compliance.""" if data_dir is None: @@ -90,13 +129,24 @@ def schengen_dashboard_data(data_dir: str | None = None) -> dict[str, typing.Any # Load all trips trip_list = trip.build_trip_list(data_dir) - # Calculate current compliance + # Calculate current compliance with trip information all_travel_items = [] for trip_obj in trip_list: all_travel_items.extend(trip_obj.travel) current_calculation = calculate_schengen_time(all_travel_items) + # Get stays with trip information + stays_with_trip_info = extract_schengen_stays_with_trip_info(trip_list) + + # Update current calculation with trip information + current_calculation.stays_in_period = [ + stay + for stay in stays_with_trip_info + if stay.entry_date >= current_calculation.current_180_day_period[0] + and stay.entry_date <= current_calculation.current_180_day_period[1] + ] + # Generate warnings warnings = generate_schengen_warnings(trip_list) @@ -122,11 +172,17 @@ def schengen_dashboard_data(data_dir: str | None = None) -> dict[str, typing.Any } -def flask_route_schengen_report(): +def flask_route_schengen_report() -> str: """Flask route for Schengen compliance report.""" data_dir = flask.current_app.config["PERSONAL_DATA"] dashboard_data = schengen_dashboard_data(data_dir) - return flask.render_template("schengen_report.html", **dashboard_data) + + # Load trips for the template + trip_list = trip.build_trip_list(data_dir) + + return flask.render_template( + "schengen_report.html", trip_list=trip_list, **dashboard_data + ) def export_schengen_data_for_external_calculator( diff --git a/agenda/types.py b/agenda/types.py index 789c37c..9c217b1 100644 --- a/agenda/types.py +++ b/agenda/types.py @@ -377,6 +377,8 @@ class SchengenStay: exit_date: typing.Optional[date] # None if currently in Schengen country: str days: int + trip_date: typing.Optional[date] = None # Trip start date for linking + trip_name: typing.Optional[str] = None # Trip name for display def __post_init__(self) -> None: if self.exit_date is None: diff --git a/templates/schengen_report.html b/templates/schengen_report.html index 32adfc3..d162a69 100644 --- a/templates/schengen_report.html +++ b/templates/schengen_report.html @@ -72,6 +72,7 @@ Exit Date Country Days + Trip @@ -87,6 +88,15 @@ {{ stay.country | country_flag }} {{ stay.country.upper() }} {{ stay.days }} + + {% if stay.trip_date and stay.trip_name %} + + {{ stay.trip_name }} + + {% else %} + - + {% endif %} + {% endfor %} @@ -139,6 +149,7 @@ Trip Date + Trip Name Days Used Days Remaining Status @@ -148,6 +159,13 @@ {% for trip_date, calculation in trips_with_compliance.items() %} {{ trip_date.strftime('%Y-%m-%d') }} + + + {% for trip_obj in trip_list if trip_obj.start == trip_date %} + {{ trip_obj.title }} + {% endfor %} + + {{ calculation.total_days_used }}/90 {{ calculation.days_remaining }}