Add per-direction NR ticket and Eurostar class selectors for return journeys
Return pages now show separate NR/Eurostar fare class buttons for outbound and inbound, so you can compare walk-on vs advance for each leg independently. URL uses nr_class_out/nr_class_in/es_class_out/es_class_in params for returns; single-direction pages keep the existing nr_class/es_class params. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8d998aad71
commit
298ce14812
2 changed files with 145 additions and 53 deletions
53
app.py
53
app.py
|
|
@ -357,6 +357,17 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
if es_class not in VALID_ES_CLASSES:
|
||||
es_class = DEFAULT_ES_CLASS
|
||||
|
||||
if journey_type == "return":
|
||||
def _p(raw, default, valid):
|
||||
return raw if raw in valid else default
|
||||
nr_class_out = _p(request.args.get("nr_class_out"), DEFAULT_NR_CLASS, VALID_NR_CLASSES)
|
||||
nr_class_in = _p(request.args.get("nr_class_in"), DEFAULT_NR_CLASS, VALID_NR_CLASSES)
|
||||
es_class_out = _p(request.args.get("es_class_out"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
|
||||
es_class_in = _p(request.args.get("es_class_in"), DEFAULT_ES_CLASS, VALID_ES_CLASSES)
|
||||
else:
|
||||
nr_class_out = nr_class_in = nr_class
|
||||
es_class_out = es_class_in = es_class
|
||||
|
||||
if (
|
||||
request.args.get("render") != "full"
|
||||
and not (
|
||||
|
|
@ -613,6 +624,15 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
else:
|
||||
sections = [build_section("main", journey_type, travel_date)]
|
||||
|
||||
nr_classes = {}
|
||||
es_classes = {}
|
||||
section_directions = {}
|
||||
for section in sections:
|
||||
direction = section["direction"]
|
||||
section_directions[section["id"]] = direction
|
||||
nr_classes[section["id"]] = nr_class_out if direction == "outbound" else nr_class_in
|
||||
es_classes[section["id"]] = es_class_out if direction == "outbound" else es_class_in
|
||||
|
||||
no_prices_note = None
|
||||
all_es_prices = [
|
||||
row.get("eurostar_price")
|
||||
|
|
@ -646,14 +666,26 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
url_max = None if max_connection == default_max else max_connection
|
||||
url_nr = None if nr_class == DEFAULT_NR_CLASS else nr_class
|
||||
url_es = None if es_class == DEFAULT_ES_CLASS else es_class
|
||||
common_url_args = {
|
||||
"journey_type": journey_type,
|
||||
"return_date": return_date,
|
||||
"min_connection": url_min,
|
||||
"max_connection": url_max,
|
||||
"nr_class": url_nr,
|
||||
"es_class": url_es,
|
||||
}
|
||||
if journey_type == "return":
|
||||
common_url_args = {
|
||||
"journey_type": journey_type,
|
||||
"return_date": return_date,
|
||||
"min_connection": url_min,
|
||||
"max_connection": url_max,
|
||||
"nr_class_out": None if nr_class_out == DEFAULT_NR_CLASS else nr_class_out,
|
||||
"nr_class_in": None if nr_class_in == DEFAULT_NR_CLASS else nr_class_in,
|
||||
"es_class_out": None if es_class_out == DEFAULT_ES_CLASS else es_class_out,
|
||||
"es_class_in": None if es_class_in == DEFAULT_ES_CLASS else es_class_in,
|
||||
}
|
||||
else:
|
||||
common_url_args = {
|
||||
"journey_type": journey_type,
|
||||
"return_date": return_date,
|
||||
"min_connection": url_min,
|
||||
"max_connection": url_max,
|
||||
"nr_class": url_nr,
|
||||
"es_class": url_es,
|
||||
}
|
||||
prev_results_url = _results_url(
|
||||
station_crs,
|
||||
slug,
|
||||
|
|
@ -813,6 +845,11 @@ def _results(station_crs, slug, travel_date, journey_type, return_date):
|
|||
es_class=es_class,
|
||||
url_nr_class=url_nr,
|
||||
url_es_class=url_es,
|
||||
nr_classes=nr_classes,
|
||||
es_classes=es_classes,
|
||||
nr_classes_json=json.dumps(nr_classes),
|
||||
es_classes_json=json.dumps(es_classes),
|
||||
section_directions_json=json.dumps(section_directions),
|
||||
trip_fares_json=json.dumps(trip_fares),
|
||||
advance_fares_json=json.dumps(advance_fares),
|
||||
walkon_api_urls_json=json.dumps(walkon_api_urls),
|
||||
|
|
|
|||
|
|
@ -75,28 +75,56 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-row" style="margin-top:0.5rem">
|
||||
<div>
|
||||
<span class="filter-label">NR ticket:</span>
|
||||
<span id="nr-type-select" style="display:none"></span>
|
||||
<span style="display:none">Load advance prices</span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if nr_class == 'walkon' %}active{% endif %}" onclick="setNrClass('walkon')">Walk-on Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_class == 'advance_std' %}active{% endif %}" onclick="setNrClass('advance_std')">Advance Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_class == 'advance_1st' %}active{% endif %}" onclick="setNrClass('advance_1st')">Advance 1st</button>
|
||||
{% if journey_type == 'return' %}
|
||||
{% for section in sections %}
|
||||
<div class="filter-row" style="margin-top:0.5rem">
|
||||
<span class="filter-label" style="min-width:5.5rem">{{ 'Outbound' if section.direction == 'outbound' else 'Return' }}:</span>
|
||||
<div>
|
||||
<span class="filter-label">NR:</span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'walkon' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="walkon" onclick="setNrClass('walkon', '{{ section.id }}')">Walk-on Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'advance_std' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="advance_std" onclick="setNrClass('advance_std', '{{ section.id }}')">Advance Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'advance_1st' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="advance_1st" onclick="setNrClass('advance_1st', '{{ section.id }}')">Advance 1st</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="filter-label">Eurostar:</span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if es_classes[section.id] == 'standard' %}active{% endif %}" data-section="{{ section.id }}" data-es-class="standard" onclick="setEsClass('standard', '{{ section.id }}')">Standard</button>
|
||||
<button type="button" class="btn-group-option {% if es_classes[section.id] == 'plus' %}active{% endif %}" data-section="{{ section.id }}" data-es-class="plus" onclick="setEsClass('plus', '{{ section.id }}')">Standard Premier</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div style="font-size:0.82rem;color:#718096;margin-top:0.4rem">
|
||||
<span id="walkon-loading"><span class="spinner spinner-inline" aria-hidden="true"></span>Loading fares</span>
|
||||
<span id="advance-loading" style="display:none"><span class="spinner spinner-inline" aria-hidden="true"></span>Loading advance fares</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="filter-label">Eurostar:</span>
|
||||
<span id="es-type-select" style="display:none"></span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if es_class == 'standard' %}active{% endif %}" onclick="setEsClass('standard')">Standard</button>
|
||||
<button type="button" class="btn-group-option {% if es_class == 'plus' %}active{% endif %}" onclick="setEsClass('plus')">Standard Premier</button>
|
||||
{% else %}
|
||||
{% set section = sections[0] %}
|
||||
<div class="filter-row" style="margin-top:0.5rem">
|
||||
<div>
|
||||
<span class="filter-label">NR ticket:</span>
|
||||
<span id="nr-type-select" style="display:none"></span>
|
||||
<span style="display:none">Load advance prices</span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'walkon' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="walkon" onclick="setNrClass('walkon', '{{ section.id }}')">Walk-on Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'advance_std' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="advance_std" onclick="setNrClass('advance_std', '{{ section.id }}')">Advance Std</button>
|
||||
<button type="button" class="btn-group-option {% if nr_classes[section.id] == 'advance_1st' %}active{% endif %}" data-section="{{ section.id }}" data-nr-class="advance_1st" onclick="setNrClass('advance_1st', '{{ section.id }}')">Advance 1st</button>
|
||||
</div>
|
||||
<span id="walkon-loading"><span class="spinner spinner-inline" aria-hidden="true"></span>Loading fares</span>
|
||||
<span id="advance-loading" style="display:none"><span class="spinner spinner-inline" aria-hidden="true"></span>Loading advance fares</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="filter-label">Eurostar:</span>
|
||||
<span id="es-type-select" style="display:none"></span>
|
||||
<div class="btn-group">
|
||||
<button type="button" class="btn-group-option {% if es_classes[section.id] == 'standard' %}active{% endif %}" data-section="{{ section.id }}" data-es-class="standard" onclick="setEsClass('standard', '{{ section.id }}')">Standard</button>
|
||||
<button type="button" class="btn-group-option {% if es_classes[section.id] == 'plus' %}active{% endif %}" data-section="{{ section.id }}" data-es-class="plus" onclick="setEsClass('plus', '{{ section.id }}')">Standard Premier</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<script>
|
||||
const RESULTS_BASE = '{{ results_base_url }}';
|
||||
const DEFAULT_MIN_CONN = {{ default_min_connection }};
|
||||
|
|
@ -109,8 +137,9 @@
|
|||
const TIMETABLE_REFRESH_URL = {{ timetable_refresh_url|tojson }};
|
||||
const HAS_PROVISIONAL_TIMETABLE = {{ 'true' if provisional_timetable else 'false' }};
|
||||
let cachedAdvanceFares = ADVANCE_FARES;
|
||||
let currentNrClass = '{{ nr_class }}';
|
||||
let currentEsClass = '{{ es_class }}';
|
||||
let currentNrClasses = {{ nr_classes_json | safe }};
|
||||
let currentEsClasses = {{ es_classes_json | safe }};
|
||||
const SECTION_DIRECTIONS = {{ section_directions_json | safe }};
|
||||
let advanceLoadingSections = {};
|
||||
|
||||
function updateAdvanceLoadingStatus() {
|
||||
|
|
@ -121,37 +150,51 @@
|
|||
if (el) el.style.display = loading ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
function buildUrl(nrCls, esCls) {
|
||||
function buildUrl() {
|
||||
var min = parseInt(document.getElementById('min_conn_select').value);
|
||||
var max = parseInt(document.getElementById('max_conn_select').value);
|
||||
var params = [];
|
||||
if (min !== DEFAULT_MIN_CONN) params.push('min_connection=' + min);
|
||||
if (max !== DEFAULT_MAX_CONN) params.push('max_connection=' + max);
|
||||
if (nrCls !== 'walkon') params.push('nr_class=' + nrCls);
|
||||
if (esCls !== 'standard') params.push('es_class=' + esCls);
|
||||
var sectionIds = Object.keys(currentNrClasses);
|
||||
if (sectionIds.length === 1) {
|
||||
var nrCls = currentNrClasses[sectionIds[0]];
|
||||
var esCls = currentEsClasses[sectionIds[0]];
|
||||
if (nrCls !== 'walkon') params.push('nr_class=' + nrCls);
|
||||
if (esCls !== 'standard') params.push('es_class=' + esCls);
|
||||
} else {
|
||||
sectionIds.forEach(function(sid) {
|
||||
var dir = SECTION_DIRECTIONS[sid];
|
||||
var suffix = dir === 'outbound' ? '_out' : '_in';
|
||||
var nrC = currentNrClasses[sid];
|
||||
var esC = currentEsClasses[sid];
|
||||
if (nrC !== 'walkon') params.push('nr_class' + suffix + '=' + nrC);
|
||||
if (esC !== 'standard') params.push('es_class' + suffix + '=' + esC);
|
||||
});
|
||||
}
|
||||
return params.length ? RESULTS_BASE + '?' + params.join('&') : RESULTS_BASE;
|
||||
}
|
||||
|
||||
function applyConnectionFilter() {
|
||||
window.location = buildUrl(currentNrClass, currentEsClass);
|
||||
window.location = buildUrl();
|
||||
}
|
||||
|
||||
function setNrClass(cls) {
|
||||
currentNrClass = cls;
|
||||
document.querySelectorAll('.btn-group-option[onclick^="setNrClass"]').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('onclick') === "setNrClass('" + cls + "')");
|
||||
function setNrClass(cls, sectionId) {
|
||||
currentNrClasses[sectionId] = cls;
|
||||
document.querySelectorAll('.btn-group-option[data-section="' + sectionId + '"][data-nr-class]').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-nr-class') === cls);
|
||||
});
|
||||
history.replaceState(null, '', buildUrl(cls, currentEsClass));
|
||||
if (cls === 'advance_std' || cls === 'advance_1st') loadMissingAdvanceFares();
|
||||
history.replaceState(null, '', buildUrl());
|
||||
if (cls === 'advance_std' || cls === 'advance_1st') loadAdvanceFaresForSectionStreaming(sectionId);
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
function setEsClass(cls) {
|
||||
currentEsClass = cls;
|
||||
document.querySelectorAll('.btn-group-option[onclick^="setEsClass"]').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('onclick') === "setEsClass('" + cls + "')");
|
||||
function setEsClass(cls, sectionId) {
|
||||
currentEsClasses[sectionId] = cls;
|
||||
document.querySelectorAll('.btn-group-option[data-section="' + sectionId + '"][data-es-class]').forEach(function(btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-es-class') === cls);
|
||||
});
|
||||
history.replaceState(null, '', buildUrl(currentNrClass, cls));
|
||||
history.replaceState(null, '', buildUrl());
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
|
|
@ -198,14 +241,15 @@
|
|||
}
|
||||
|
||||
function sectionNeedsAdvance(sectionId) {
|
||||
var nrClass = currentNrClasses[sectionId] || 'walkon';
|
||||
for (var key in TRIP_FARES) {
|
||||
var row = TRIP_FARES[key];
|
||||
if (row.section !== sectionId || !row.advance_key) continue;
|
||||
var sectionFares = ADVANCE_FARES && ADVANCE_FARES[sectionId];
|
||||
var advanceFares = sectionFares && sectionFares[row.advance_key];
|
||||
if (!advanceFares) return true;
|
||||
if (currentNrClass === 'advance_std' && !advanceFares.advance_std) return true;
|
||||
if (currentNrClass === 'advance_1st' && !advanceFares.advance_1st) return true;
|
||||
if (nrClass === 'advance_std' && !advanceFares.advance_std) return true;
|
||||
if (nrClass === 'advance_1st' && !advanceFares.advance_1st) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
|
@ -270,16 +314,20 @@
|
|||
|
||||
function loadMissingAdvanceFares() {
|
||||
for (var sectionId in ADVANCE_STREAM_URLS) {
|
||||
if (sectionNeedsAdvance(sectionId)) loadAdvanceFaresForSectionStreaming(sectionId);
|
||||
var nrClass = currentNrClasses[sectionId] || 'walkon';
|
||||
if ((nrClass === 'advance_std' || nrClass === 'advance_1st') && sectionNeedsAdvance(sectionId)) {
|
||||
loadAdvanceFaresForSectionStreaming(sectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function currentNrFare(row) {
|
||||
if (currentNrClass === 'walkon') return row.walkon;
|
||||
var nrClass = currentNrClasses[row.section] || 'walkon';
|
||||
if (nrClass === 'walkon') return row.walkon;
|
||||
var sectionFares = ADVANCE_FARES && ADVANCE_FARES[row.section];
|
||||
var advFares = sectionFares && row.advance_key ? sectionFares[row.advance_key] : null;
|
||||
if (!advFares) return row.walkon;
|
||||
return (currentNrClass === 'advance_std' ? advFares.advance_std : advFares.advance_1st) || row.walkon;
|
||||
return (nrClass === 'advance_std' ? advFares.advance_std : advFares.advance_1st) || row.walkon;
|
||||
}
|
||||
|
||||
function updateDisplay() {
|
||||
|
|
@ -289,7 +337,8 @@
|
|||
var row = TRIP_FARES[key];
|
||||
if (!row) return;
|
||||
var nrFare = currentNrFare(row);
|
||||
var esFare = currentEsClass === 'standard' ? row.es_standard : row.es_plus;
|
||||
var esClass = currentEsClasses[row.section] || 'standard';
|
||||
var esFare = esClass === 'standard' ? row.es_standard : row.es_plus;
|
||||
if (nrFare && esFare) totals[key] = nrFare.price + esFare.price + (row.circle_fare || 0);
|
||||
});
|
||||
var totalValues = Object.values(totals);
|
||||
|
|
@ -301,24 +350,26 @@
|
|||
var row = TRIP_FARES[key];
|
||||
if (!row) return;
|
||||
|
||||
var nrClass = currentNrClasses[row.section] || 'walkon';
|
||||
var esClass = currentEsClasses[row.section] || 'standard';
|
||||
var nrFare = currentNrFare(row);
|
||||
var walkonEl = tr.querySelector('.nr-walkon');
|
||||
var advStdEl = tr.querySelector('.nr-advance-std');
|
||||
var adv1stEl = tr.querySelector('.nr-advance-1st');
|
||||
if (walkonEl) {
|
||||
walkonEl.innerHTML = row.walkon ? fareHtml(row.walkon) : '<span class="text-sm text-muted">\u2013</span>';
|
||||
walkonEl.classList.toggle('fare-inactive', currentNrClass !== 'walkon');
|
||||
walkonEl.classList.toggle('fare-inactive', nrClass !== 'walkon');
|
||||
}
|
||||
if (advStdEl || adv1stEl) {
|
||||
var sectionFares = ADVANCE_FARES && ADVANCE_FARES[row.section];
|
||||
var advFares = sectionFares && row.advance_key ? sectionFares[row.advance_key] : null;
|
||||
if (advStdEl) {
|
||||
advStdEl.innerHTML = advFares && advFares.advance_std ? fareHtml(advFares.advance_std) : '';
|
||||
advStdEl.classList.toggle('fare-inactive', currentNrClass !== 'advance_std');
|
||||
advStdEl.classList.toggle('fare-inactive', nrClass !== 'advance_std');
|
||||
}
|
||||
if (adv1stEl) {
|
||||
adv1stEl.innerHTML = advFares && advFares.advance_1st ? fareHtml(advFares.advance_1st) : '';
|
||||
adv1stEl.classList.toggle('fare-inactive', currentNrClass !== 'advance_1st');
|
||||
adv1stEl.classList.toggle('fare-inactive', nrClass !== 'advance_1st');
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -326,11 +377,11 @@
|
|||
var esPlusEl = tr.querySelector('.es-plus');
|
||||
if (esStdEl) {
|
||||
esStdEl.innerHTML = row.es_standard ? fareHtml(row.es_standard) : '<span class="text-sm text-muted">\u2013</span>';
|
||||
esStdEl.classList.toggle('fare-inactive', currentEsClass !== 'standard');
|
||||
esStdEl.classList.toggle('fare-inactive', esClass !== 'standard');
|
||||
}
|
||||
if (esPlusEl) {
|
||||
esPlusEl.innerHTML = row.es_plus ? fareHtml(row.es_plus) : '<span class="text-sm text-muted">\u2013</span>';
|
||||
esPlusEl.classList.toggle('fare-inactive', currentEsClass !== 'plus');
|
||||
esPlusEl.classList.toggle('fare-inactive', esClass !== 'plus');
|
||||
}
|
||||
|
||||
var totalSpan = tr.querySelector('.total-price');
|
||||
|
|
@ -374,7 +425,11 @@
|
|||
}
|
||||
|
||||
function initialiseResultsPage() {
|
||||
if (currentNrClass === 'advance_std' || currentNrClass === 'advance_1st') loadMissingAdvanceFares();
|
||||
var needsAdvance = Object.keys(currentNrClasses).some(function(sid) {
|
||||
var c = currentNrClasses[sid];
|
||||
return c === 'advance_std' || c === 'advance_1st';
|
||||
});
|
||||
if (needsAdvance) loadMissingAdvanceFares();
|
||||
updateDisplay();
|
||||
loadWalkonFares();
|
||||
startTimetableRefresh();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue