Add trip linking and names to Schengen compliance report

- Add trip_date and trip_name fields to SchengenStay dataclass
- Implement extract_schengen_stays_with_trip_info() to associate stays with trips
- Update schengen_report.html to show trip names with clickable links
- Add Trip column to stays table and trip name column to compliance history
- Links navigate to individual trip pages using existing URL structure

Closes #197

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2025-07-15 15:06:09 +02:00
parent 1d37f92cfb
commit 8363a581c7
3 changed files with 87 additions and 11 deletions

View file

@ -1,17 +1,14 @@
"""Integration of Schengen calculator with the existing trip system.""" """Integration of Schengen calculator with the existing trip system."""
import logging
import typing import typing
from datetime import date, timedelta from datetime import date, timedelta
import flask import flask
from . import trip from . import trip
from .schengen import ( from .schengen import calculate_schengen_time, extract_schengen_stays_from_travel
SchengenCalculation, from .types import SchengenCalculation, SchengenStay, StrDict, Trip
calculate_schengen_time,
extract_schengen_stays_from_travel,
)
from .types import StrDict, Trip
def add_schengen_compliance_to_trip(trip_obj: Trip) -> 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 trip_obj.schengen_compliance = calculation
except Exception as e: except Exception as e:
# Log the error but don't fail the trip loading # Log the error but don't fail the trip loading
import logging logging.warning(
logging.warning(f"Failed to calculate Schengen compliance for trip {trip_obj.start}: {e}") f"Failed to calculate Schengen compliance for trip {trip_obj.start}: {e}"
)
trip_obj.schengen_compliance = None trip_obj.schengen_compliance = None
return trip_obj return trip_obj
@ -82,6 +80,47 @@ def generate_schengen_warnings(trip_list: list[Trip]) -> list[dict[str, typing.A
return warnings 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]: def schengen_dashboard_data(data_dir: str | None = None) -> dict[str, typing.Any]:
"""Generate dashboard data for Schengen compliance.""" """Generate dashboard data for Schengen compliance."""
if data_dir is None: 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 # Load all trips
trip_list = trip.build_trip_list(data_dir) trip_list = trip.build_trip_list(data_dir)
# Calculate current compliance # Calculate current compliance with trip information
all_travel_items = [] all_travel_items = []
for trip_obj in trip_list: for trip_obj in trip_list:
all_travel_items.extend(trip_obj.travel) all_travel_items.extend(trip_obj.travel)
current_calculation = calculate_schengen_time(all_travel_items) 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 # Generate warnings
warnings = generate_schengen_warnings(trip_list) 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.""" """Flask route for Schengen compliance report."""
data_dir = flask.current_app.config["PERSONAL_DATA"] data_dir = flask.current_app.config["PERSONAL_DATA"]
dashboard_data = schengen_dashboard_data(data_dir) 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( def export_schengen_data_for_external_calculator(

View file

@ -377,6 +377,8 @@ class SchengenStay:
exit_date: typing.Optional[date] # None if currently in Schengen exit_date: typing.Optional[date] # None if currently in Schengen
country: str country: str
days: int 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: def __post_init__(self) -> None:
if self.exit_date is None: if self.exit_date is None:

View file

@ -72,6 +72,7 @@
<th>Exit Date</th> <th>Exit Date</th>
<th>Country</th> <th>Country</th>
<th>Days</th> <th>Days</th>
<th>Trip</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -87,6 +88,15 @@
</td> </td>
<td>{{ stay.country | country_flag }} {{ stay.country.upper() }}</td> <td>{{ stay.country | country_flag }} {{ stay.country.upper() }}</td>
<td>{{ stay.days }}</td> <td>{{ stay.days }}</td>
<td>
{% if stay.trip_date and stay.trip_name %}
<a href="{{ url_for('trip_page', start=stay.trip_date.strftime('%Y-%m-%d')) }}" class="text-decoration-none">
{{ stay.trip_name }}
</a>
{% else %}
<span class="text-muted">-</span>
{% endif %}
</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -139,6 +149,7 @@
<thead> <thead>
<tr> <tr>
<th>Trip Date</th> <th>Trip Date</th>
<th>Trip Name</th>
<th>Days Used</th> <th>Days Used</th>
<th>Days Remaining</th> <th>Days Remaining</th>
<th>Status</th> <th>Status</th>
@ -148,6 +159,13 @@
{% for trip_date, calculation in trips_with_compliance.items() %} {% for trip_date, calculation in trips_with_compliance.items() %}
<tr> <tr>
<td>{{ trip_date.strftime('%Y-%m-%d') }}</td> <td>{{ trip_date.strftime('%Y-%m-%d') }}</td>
<td>
<a href="{{ url_for('trip_page', start=trip_date.strftime('%Y-%m-%d')) }}" class="text-decoration-none">
{% for trip_obj in trip_list if trip_obj.start == trip_date %}
{{ trip_obj.title }}
{% endfor %}
</a>
</td>
<td>{{ calculation.total_days_used }}/90</td> <td>{{ calculation.total_days_used }}/90</td>
<td>{{ calculation.days_remaining }}</td> <td>{{ calculation.days_remaining }}</td>
<td> <td>