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:
parent
1d37f92cfb
commit
8363a581c7
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@
|
|||
<th>Exit Date</th>
|
||||
<th>Country</th>
|
||||
<th>Days</th>
|
||||
<th>Trip</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
|
@ -87,6 +88,15 @@
|
|||
</td>
|
||||
<td>{{ stay.country | country_flag }} {{ stay.country.upper() }}</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>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
|
@ -139,6 +149,7 @@
|
|||
<thead>
|
||||
<tr>
|
||||
<th>Trip Date</th>
|
||||
<th>Trip Name</th>
|
||||
<th>Days Used</th>
|
||||
<th>Days Remaining</th>
|
||||
<th>Status</th>
|
||||
|
|
@ -148,6 +159,13 @@
|
|||
{% for trip_date, calculation in trips_with_compliance.items() %}
|
||||
<tr>
|
||||
<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.days_remaining }}</td>
|
||||
<td>
|
||||
|
|
|
|||
Loading…
Reference in a new issue