Files
TREK/client/src/components/Planner/DayPlanSidebarToolbar.tsx
T
Maurice 0dd07befa7 feat(planner): bring the plan tab and every trip dialog into the new look
The Plan tab was the last trip tab still in the old design. It now reads
like Bookings, Costs, Lists, Files and Collab, without losing a function:

- Day plan: each day is a card with a head band (day tile with weather,
  stay and rental pills, a "+" menu for place, stay, transport, transit
  and note), rows with "..." menus, white badges and a white route bar.
- Places column: add, search and filters in three rows, a show dropdown,
  category and rating filters, and a selection bar at the foot.
- Place inspector, day details and the stay editor on the shared dialog
  frame; the place form in the editor look with the name in the head band.
- Clicking a booking or transport anywhere on the plan opens the booking
  detail popup first; Edit leads to the editor. The popup is one host
  shared with the Bookings and Transports tabs.
- Files and Collab dialogs, notes, polls, links and chat moved to the same
  frame; shared DialogShell and dialogParts hold the building blocks.
- The map hover card shows the place photo and follows the theme.

Every new and reworded string is translated into all locales, and a few
older English leftovers (help center, cost split, import hints) with them.
Phone screens keep their own look.
2026-09-27 21:00:19 +02:00

159 lines
6.6 KiB
TypeScript

import { useState } from 'react'
import { ChevronsDownUp, ChevronsUpDown, Download, Undo2, ArrowUpDown, Route as RouteIcon } from 'lucide-react'
import { DayReorderPopup } from './DayReorderPopup'
import Tooltip from '../shared/Tooltip'
import { NEUTRAL_TINT, fs } from '../shared/DialogShell'
import { BarButton, PANEL_BAR } from './planParts'
import { useToast } from '../shared/Toast'
import { TripExportModal } from './TripExportModal'
import { isRoutableReservation } from '../../utils/reservationRoutes'
import type { DayAddControls } from '../../utils/dayAdd'
import type { DayDeleteQuestion } from '../../utils/dayImpactLines'
import type { Trip, Day, Place, Category, AssignmentsMap, Reservation, DayNote } from '../../types'
interface DayPlanSidebarToolbarProps {
tripId: number
trip: Trip
days: Day[]
places: Place[]
categories: Category[]
assignments: AssignmentsMap
reservations: Reservation[]
allConnectionsShown?: boolean
onToggleAllConnections?: () => void
dayNotes: Record<string, DayNote[]>
t: (key: string, params?: Record<string, any>) => string
locale: string
toast: ReturnType<typeof useToast>
expandedDays: Set<number>
setExpandedDays: (next: Set<number>) => void
onUndo?: () => void
canUndo: boolean
undoHover: boolean
setUndoHover: (v: boolean) => void
lastActionLabel: string | null
canEditDays?: boolean
/**
* Gates "Subscribe to calendar" in the export dialog only. Defaults to true so
* a caller that has not wired the permission through keeps today's entries
* rather than silently losing one.
*/
canManageShare?: boolean
onReorderDays?: (orderedIds: number[]) => void
onAddDay?: (position?: number) => void
/** The planner's add controls: on a trip with dates the dialog offers the next date as well. */
dayAdd?: DayAddControls
/** Asks to delete a day from the reorder dialog; without it the dialog has no delete buttons. */
onDeleteDay?: (dayId: number) => void
/** The open delete question; the dialog asks it in place of its day list. */
deleteDayQuestion?: DayDeleteQuestion | null
}
export function DayPlanSidebarToolbar({
tripId, trip, days, places, categories, assignments, reservations, dayNotes,
allConnectionsShown = false, onToggleAllConnections,
t, locale, toast,
expandedDays, setExpandedDays, onUndo, canUndo, undoHover, setUndoHover, lastActionLabel,
canEditDays, canManageShare = true, onReorderDays, onAddDay, dayAdd, onDeleteDay, deleteDayQuestion,
}: DayPlanSidebarToolbarProps) {
const [reorderOpen, setReorderOpen] = useState(false)
const [exportOpen, setExportOpen] = useState(false)
const allExpanded = days.length > 0 && days.every(d => expandedDays.has(d.id))
const expandLabel = allExpanded ? t('dayplan.collapseAll') : t('dayplan.expandAll')
const undoTip = canUndo && lastActionLabel ? t('undo.tooltip', { action: lastActionLabel }) : t('undo.button')
const connectionsLabel = t(allConnectionsShown ? 'map.hideAllConnections' : 'map.showAllConnections')
// The panel's head band: the export as its one labelled action, the tools as
// quiet icons that rise like a filter tab when they are on.
return (
<div className={PANEL_BAR} style={{ background: NEUTRAL_TINT }}>
{/* One export button instead of three: PDF, ICS and GPX each carried
their own hover menu, and on a narrower sidebar the row ran out of
width and pushed them off the edge. The dialog holds every option. */}
<Tooltip label={t('dayplan.exportIntro')} placement="bottom">
<button
type="button"
onClick={() => setExportOpen(true)}
aria-haspopup="dialog"
aria-expanded={exportOpen}
className="inline-flex h-8 flex-none items-center gap-1.5 rounded-[10px] bg-accent px-3 font-semibold text-accent-text hover:opacity-90"
style={fs(12, 'body')}
>
<Download size={13} strokeWidth={2.2} />
{t('dayplan.export')}
</button>
</Tooltip>
<TripExportModal
isOpen={exportOpen}
onClose={() => setExportOpen(false)}
tripId={tripId}
trip={trip}
days={days}
places={places}
categories={categories}
assignments={assignments}
reservations={reservations}
dayNotes={dayNotes}
t={t}
locale={locale}
toast={toast}
canManageShare={canManageShare}
/>
<span className="flex-1" />
{onUndo && (
<Tooltip label={undoTip} placement="bottom">
<button type="button"
onClick={onUndo}
disabled={!canUndo}
aria-label={t('undo.button')}
onMouseEnter={() => setUndoHover(true)}
onMouseLeave={() => setUndoHover(false)}
className="grid h-8 w-8 flex-none place-items-center rounded-[10px] text-content-muted transition-colors enabled:hover:bg-surface-card enabled:hover:text-content disabled:cursor-default disabled:opacity-40"
>
<Undo2 size={15} strokeWidth={2} />
</button>
</Tooltip>
)}
<BarButton
label={expandLabel}
ariaPressed={allExpanded}
onClick={() => {
const next = allExpanded ? new Set<number>() : new Set(days.map(d => d.id))
setExpandedDays(next)
// Same store the sidebar reads on mount — a sessionStorage write
// here left the persisted set behind after a reload.
try { localStorage.setItem(`day-expanded-${tripId}`, JSON.stringify([...next])) } catch {}
}}
>
{allExpanded ? <ChevronsDownUp size={15} strokeWidth={2} /> : <ChevronsUpDown size={15} strokeWidth={2} />}
</BarButton>
{canEditDays && onReorderDays && onAddDay && days.length > 0 && (
<>
<BarButton label={t('dayplan.reorderDays')} ariaPressed={reorderOpen} active={reorderOpen} onClick={() => setReorderOpen(v => !v)}>
<ArrowUpDown size={15} strokeWidth={2} />
</BarButton>
<DayReorderPopup
isOpen={reorderOpen}
days={days}
t={t}
locale={locale}
onReorder={onReorderDays}
onAddDay={() => onAddDay()}
dayAdd={dayAdd}
onDeleteDay={onDeleteDay}
deleteQuestion={deleteDayQuestion}
onClose={() => setReorderOpen(false)}
/>
</>
)}
{onToggleAllConnections && reservations.some(isRoutableReservation) && (
<BarButton label={connectionsLabel} ariaPressed={allConnectionsShown} active={allConnectionsShown}
className={allConnectionsShown ? 'text-info' : ''} onClick={onToggleAllConnections}>
<RouteIcon size={15} strokeWidth={2} />
</BarButton>
)}
</div>
)
}