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."""
|
"""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(
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue