fix(planner): edit details handoff from the transit journey dialog (#2148)

This commit is contained in:
Maurice
2026-09-01 15:07:26 +02:00
committed by Maurice
parent 2e00c998c0
commit 2e0c496a93
28 changed files with 94 additions and 4 deletions
@@ -1,4 +1,4 @@
// FE-PLANNER-TRANSITJOURNEY-001 to 016 — the journey view for a saved transit entry.
// FE-PLANNER-TRANSITJOURNEY-001 to 018 — the journey view for a saved transit entry.
import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render'
import userEvent from '@testing-library/user-event'
import { resetAllStores, seedStore } from '../../../tests/helpers/store'
@@ -113,6 +113,21 @@ describe('TransitJourneyModal', () => {
expect(onChangeRoute).toHaveBeenCalled()
})
it('FE-PLANNER-TRANSITJOURNEY-017: edit details hands off to the full transport editor', async () => {
const user = userEvent.setup()
const onEditDetails = vi.fn()
render(<TransitJourneyModal {...makeProps({ onEditDetails })} />)
await user.click(screen.getByRole('button', { name: /Edit details/ }))
expect(onEditDetails).toHaveBeenCalled()
})
it('FE-PLANNER-TRANSITJOURNEY-018: edit details stays hidden without the callback or edit rights', () => {
const { rerender } = render(<TransitJourneyModal {...makeProps()} />)
expect(screen.queryByRole('button', { name: /Edit details/ })).not.toBeInTheDocument()
rerender(<TransitJourneyModal {...makeProps({ onEditDetails: vi.fn(), canEdit: false })} />)
expect(screen.queryByRole('button', { name: /Edit details/ })).not.toBeInTheDocument()
})
it('FE-PLANNER-TRANSITJOURNEY-004: delete asks for confirmation, then calls onDelete', async () => {
const user = userEvent.setup()
const onDelete = vi.fn().mockResolvedValue({})
@@ -15,7 +15,9 @@ import type { Reservation } from '../../types'
* The journey view for an automated public-transit entry (#1065): a roomy modal
* around the stop-by-stop itinerary. The title renames inline right in the
* header, notes get the full width with markdown support, and "Change route"
* re-enters the transit search pre-seeded with this journey's route.
* re-enters the transit search pre-seeded with this journey's route. "Edit
* details" hands off to the full transport editor for the booking fields
* (travelers, costs, files, code, status — #2148).
*/
interface TransitLegMeta {
@@ -38,10 +40,12 @@ interface TransitJourneyModalProps {
onSave: (fields: { title: string; notes: string | null }) => Promise<unknown>
onDelete: () => Promise<unknown>
onChangeRoute: () => void
/** Switch to the full transport editor (travelers, costs, files, code, status). */
onEditDetails?: () => void
canEdit: boolean
}
export default function TransitJourneyModal({ reservation, onClose, onSave, onDelete, onChangeRoute, canEdit }: TransitJourneyModalProps) {
export default function TransitJourneyModal({ reservation, onClose, onSave, onDelete, onChangeRoute, onEditDetails, canEdit }: TransitJourneyModalProps) {
const { t, locale } = useTranslation()
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
const timeFormat = useSettingsStore(st => st.settings.time_format) || '24h'
@@ -160,6 +164,15 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
<RefreshCw size={13} /> {t('transit.changeRoute')}
</button>
)}
{canEdit && onEditDetails && (
<button type="button" onClick={onEditDetails} className="text-content-muted" style={{
display: 'inline-flex', alignItems: 'center', gap: 6, padding: '8px 16px', borderRadius: 10,
border: '1px solid var(--border-primary)', background: 'none',
fontSize: 'calc(12px * var(--fs-scale-body, 1))', cursor: 'pointer', fontFamily: 'inherit',
}}>
<Pencil size={13} /> {t('transit.editDetails')}
</button>
)}
{canEdit ? (
<button type="button" onClick={save} disabled={saving || !title.trim() || !dirty} className="bg-[var(--text-primary)] text-[var(--bg-primary)]" style={{
padding: '8px 20px', borderRadius: 10, border: 'none',
@@ -110,6 +110,17 @@ export default function MTripSheets({ planner, shell }: MTripSheetsProps) {
planner.setTransitJourney(null)
planner.setShowTransportModal(true)
}}
onEditDetails={() => {
// Hand off to the full transport editor for the booking fields —
// same target as the transports tab's pencil (#2148).
const journey = planner.reservations.find(r => r.id === planner.transitJourney!.id) ?? planner.transitJourney!
planner.setEditingTransport(journey)
planner.setTransportModalDayId(journey.day_id ?? null)
planner.setTransportModalAutomated(false)
planner.setTransitPrefill(null)
planner.setTransitJourney(null)
planner.setShowTransportModal(true)
}}
/>
)}
+12
View File
@@ -859,6 +859,18 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
setTransitJourney(null)
setShowTransportModal(true)
}}
onEditDetails={() => {
// Hand off to the full transport editor (travelers, costs, files,
// booking code, status) — the same modal mobile opens; an
// unchanged-endpoints save keeps the stored itinerary (#2148).
const current = reservations.find(r => r.id === transitJourney.id) ?? transitJourney
setEditingTransport(current)
setTransportModalDayId(current.day_id ?? null)
setTransportModalAutomated(false)
setTransitPrefill(null)
setTransitJourney(null)
setShowTransportModal(true)
}}
/>
)}
{bookingExpense && (
@@ -1,4 +1,4 @@
// FE-PAGE-TPW-001 to FE-PAGE-TPW-060
// FE-PAGE-TPW-001 to FE-PAGE-TPW-061
//
// The planner page is a wiring container: everything stateful lives in
// useTripPlanner (covered in src/pages/tripPlanner/useTripPlanner.test.tsx).
@@ -1117,6 +1117,22 @@ describe('TripPlannerPage — modals', () => {
expect(hookState.setTransportModalDayId).toHaveBeenCalledWith(null)
})
it('FE-PAGE-TPW-061: edit details hands the fresh reservation to the full transport editor', () => {
const stale = buildReservation({ id: 9, type: 'transit', day_id: 7 })
// The store copy may be newer than the journey held in state — the fresh
// one must win.
const fresh = { ...stale, title: 'Kyoto → Osaka' }
renderPage({ transitJourney: stale, reservations: [fresh] })
act(() => { props('transitModal').onEditDetails() })
expect(hookState.setEditingTransport).toHaveBeenCalledWith(fresh)
expect(hookState.setTransportModalDayId).toHaveBeenCalledWith(7)
expect(hookState.setTransportModalAutomated).toHaveBeenCalledWith(false)
expect(hookState.setTransitPrefill).toHaveBeenCalledWith(null)
expect(hookState.setTransitJourney).toHaveBeenCalledWith(null)
expect(hookState.setShowTransportModal).toHaveBeenCalledWith(true)
})
it('FE-PAGE-TPW-057: a booking opens the expense editor, prefilled or on an existing item', async () => {
setSettings({ default_currency: 'eur' })
renderPage()
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'تلقائي',
'transit.sectionTitle': 'النقل العام التلقائي',
'transit.changeRoute': 'تغيير المسار',
'transit.editDetails': 'تعديل التفاصيل',
'transit.journey': 'رحلة بالنقل العام',
'transit.pickDay': 'اختر يومًا للبحث عن الرحلات المتاحة.',
'transit.noItinerary': 'لا يحتوي هذا الإدخال على خط سير محفوظ — استخدم "تغيير المسار" للبحث عن أحدها.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automático',
'transit.sectionTitle': 'Transporte público automático',
'transit.changeRoute': 'Alterar trajeto',
'transit.editDetails': 'Editar detalhes',
'transit.journey': 'Viagem de transporte público',
'transit.pickDay': 'Escolha um dia para buscar conexões.',
'transit.noItinerary': 'Esta entrada não tem itinerário salvo — use "Alterar trajeto" para buscar um.',
+1
View File
@@ -74,6 +74,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Transport automatitzat',
'transit.sectionTitle': 'Transport públic automatitzat',
'transit.changeRoute': 'Canviar de ruta',
'transit.editDetails': 'Editar detalls',
'transit.journey': 'Trajecte en transport públic',
'transit.pickDay': 'Tria un dia per cercar connexions.',
'transit.noItinerary': 'Aquesta entrada no té cap itinerari desat — utilitza «Canviar de ruta» per cercar-ne un.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automaticky',
'transit.sectionTitle': 'Automatické spojení MHD',
'transit.changeRoute': 'Změnit trasu',
'transit.editDetails': 'Upravit podrobnosti',
'transit.journey': 'Cesta veřejnou dopravou',
'transit.pickDay': 'Vyberte den, pro který chcete vyhledat spojení.',
'transit.noItinerary': 'Tato položka nemá uložené spojení – vyhledejte ho pomocí tlačítka „Změnit trasu“.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatisch',
'transit.sectionTitle': 'Automatische ÖPNV-Verbindungen',
'transit.changeRoute': 'Route ändern',
'transit.editDetails': 'Details bearbeiten',
'transit.journey': 'ÖPNV-Verbindung',
'transit.pickDay': 'Wähle einen Tag, für den Verbindungen gesucht werden sollen.',
'transit.noItinerary': 'Dieser Eintrag hat keine gespeicherte Verbindung — über „Route ändern" eine suchen.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automated',
'transit.sectionTitle': 'Automated public transit',
'transit.changeRoute': 'Change route',
'transit.editDetails': 'Edit details',
'transit.journey': 'Public transit journey',
'transit.pickDay': 'Pick a day to search connections for.',
'transit.noItinerary': 'This entry has no stored itinerary — use "Change route" to search one.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automático',
'transit.sectionTitle': 'Transporte público automático',
'transit.changeRoute': 'Cambiar ruta',
'transit.editDetails': 'Editar detalles',
'transit.journey': 'Trayecto en transporte público',
'transit.pickDay': 'Elige un día para buscar conexiones.',
'transit.noItinerary': 'Esta entrada no tiene ningún itinerario guardado: usa «Cambiar ruta» para buscar uno.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatique',
'transit.sectionTitle': 'Transports en commun automatisés',
'transit.changeRoute': "Modifier l'itinéraire",
'transit.editDetails': 'Modifier les détails',
'transit.journey': 'Trajet en transports en commun',
'transit.pickDay': 'Choisissez un jour pour rechercher des connexions.',
'transit.noItinerary':
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Αυτόματα',
'transit.sectionTitle': 'Αυτόματες δημόσιες συγκοινωνίες',
'transit.changeRoute': 'Αλλαγή διαδρομής',
'transit.editDetails': 'Επεξεργασία λεπτομερειών',
'transit.journey': 'Διαδρομή με δημόσιες συγκοινωνίες',
'transit.pickDay': 'Επιλέξτε μια ημέρα για αναζήτηση συνδέσεων.',
'transit.noItinerary':
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatikus',
'transit.sectionTitle': 'Automatikus tömegközlekedés',
'transit.changeRoute': 'Útvonal módosítása',
'transit.editDetails': 'Részletek szerkesztése',
'transit.journey': 'Tömegközlekedési útvonal',
'transit.pickDay': 'Válassz egy napot a kapcsolatok kereséséhez.',
'transit.noItinerary':
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Otomatis',
'transit.sectionTitle': 'Transportasi umum otomatis',
'transit.changeRoute': 'Ubah rute',
'transit.editDetails': 'Sunting detail',
'transit.journey': 'Perjalanan transportasi umum',
'transit.pickDay': 'Pilih hari untuk mencari koneksi.',
'transit.noItinerary':
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatico',
'transit.sectionTitle': 'Trasporto pubblico automatico',
'transit.changeRoute': 'Cambia percorso',
'transit.editDetails': 'Modifica dettagli',
'transit.journey': 'Viaggio in trasporto pubblico',
'transit.pickDay': 'Scegli un giorno per cui cercare i collegamenti.',
'transit.noItinerary': 'Questa voce non ha un itinerario salvato: usa "Cambia percorso" per cercarne uno.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': '自動',
'transit.sectionTitle': '公共交通機関の自動検索',
'transit.changeRoute': '経路を変更',
'transit.editDetails': '詳細を編集',
'transit.journey': '公共交通機関の経路',
'transit.pickDay': '接続を検索する日を選択してください。',
'transit.noItinerary': 'この項目には保存された行程がありません。「経路を変更」から検索してください。',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': '자동',
'transit.sectionTitle': '자동 대중교통',
'transit.changeRoute': '경로 변경',
'transit.editDetails': '세부 정보 편집',
'transit.journey': '대중교통 경로',
'transit.pickDay': '연결편을 검색할 날짜를 선택하세요.',
'transit.noItinerary': '이 항목에는 저장된 경로가 없습니다. "경로 변경"을 사용하여 검색하세요.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatisch',
'transit.sectionTitle': 'Automatisch openbaar vervoer',
'transit.changeRoute': 'Route wijzigen',
'transit.editDetails': 'Details bewerken',
'transit.journey': 'Reis met openbaar vervoer',
'transit.pickDay': 'Kies een dag om verbindingen voor te zoeken.',
'transit.noItinerary': 'Deze invoer heeft geen opgeslagen reisplan — gebruik "Route wijzigen" om er een te zoeken.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatycznie',
'transit.sectionTitle': 'Automatyczny transport publiczny',
'transit.changeRoute': 'Zmień trasę',
'transit.editDetails': 'Edytuj szczegóły',
'transit.journey': 'Podróż transportem publicznym',
'transit.pickDay': 'Wybierz dzień, dla którego wyszukać połączenia.',
'transit.noItinerary': 'Ten wpis nie ma zapisanej trasy — użyj opcji „Zmień trasę”, aby ją wyszukać.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Автоматически',
'transit.sectionTitle': 'Автоматический общественный транспорт',
'transit.changeRoute': 'Изменить маршрут',
'transit.editDetails': 'Редактировать детали',
'transit.journey': 'Поездка на общественном транспорте',
'transit.pickDay': 'Выберите день для поиска соединений.',
'transit.noItinerary': 'Для этой записи нет сохранённого маршрута — нажмите «Изменить маршрут», чтобы найти его.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Automatisk',
'transit.sectionTitle': 'Automatisk kollektivtrafik',
'transit.changeRoute': 'Ändra resväg',
'transit.editDetails': 'Redigera detaljer',
'transit.journey': 'Kollektivtrafikresa',
'transit.pickDay': 'Välj en dag att söka förbindelser för.',
'transit.noItinerary': 'Den här posten har ingen sparad resplan – använd ”Ändra resväg” för att söka en.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Otomatik',
'transit.sectionTitle': 'Otomatik toplu taşıma',
'transit.changeRoute': 'Rotayı değiştir',
'transit.editDetails': 'Ayrıntıları düzenle',
'transit.journey': 'Toplu taşıma yolculuğu',
'transit.pickDay': 'Bağlantı aramak için bir gün seçin.',
'transit.noItinerary':
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Автоматично',
'transit.sectionTitle': 'Автоматичний громадський транспорт',
'transit.changeRoute': 'Змінити маршрут',
'transit.editDetails': 'Редагувати деталі',
'transit.journey': 'Поїздка громадським транспортом',
'transit.pickDay': 'Виберіть день для пошуку сполучень.',
'transit.noItinerary': 'Для цього запису немає збереженого маршруту — натисніть «Змінити маршрут», щоб знайти його.',
+1
View File
@@ -78,6 +78,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': 'Tự động',
'transit.sectionTitle': 'Giao thông công cộng tự động',
'transit.changeRoute': 'Đổi tuyến',
'transit.editDetails': 'Chỉnh sửa chi tiết',
'transit.journey': 'Hành trình giao thông công cộng',
'transit.pickDay': 'Chọn một ngày để tìm chuyến kết nối.',
'transit.noItinerary': 'Mục này chưa có lộ trình được lưu — dùng “Đổi tuyến” để tìm một lộ trình.',
+1
View File
@@ -77,6 +77,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': '自動',
'transit.sectionTitle': '自動大眾運輸',
'transit.changeRoute': '變更路線',
'transit.editDetails': '編輯詳細資料',
'transit.journey': '大眾運輸行程',
'transit.pickDay': '請選擇要查詢班次的日期。',
'transit.noItinerary': '此項目尚未儲存行程——請使用「變更路線」查詢。',
+1
View File
@@ -77,6 +77,7 @@ const trip: TranslationStrings = {
'transport.modeAutomated': '自动',
'transit.sectionTitle': '自动公共交通',
'transit.changeRoute': '更改路线',
'transit.editDetails': '编辑详情',
'transit.journey': '公共交通行程',
'transit.pickDay': '请选择要查询班次的日期。',
'transit.noItinerary': '此条目尚未保存行程 — 请使用"更改路线"查询班次。',