Merge pull request #361 from lucaam/add_span_days_feature

Support multi-day spanning for reservations
This commit is contained in:
Maurice
2026-04-04 13:58:16 +02:00
committed by GitHub
16 changed files with 526 additions and 73 deletions

View File

@@ -211,13 +211,67 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const TRANSPORT_TYPES = new Set(['flight', 'train', 'bus', 'car', 'cruise'])
// Determine if a reservation's end_time represents a different date (multi-day)
const getEndDate = (r: Reservation) => {
const endStr = r.reservation_end_time || ''
return endStr.includes('T') ? endStr.split('T')[0] : null
}
// Get span phase: how a reservation relates to a specific day's date
const getSpanPhase = (r: Reservation, dayDate: string): 'single' | 'start' | 'middle' | 'end' => {
if (!r.reservation_time) return 'single'
const startDate = r.reservation_time.split('T')[0]
const endDate = getEndDate(r) || startDate
if (startDate === endDate) return 'single'
if (dayDate === startDate) return 'start'
if (dayDate === endDate) return 'end'
return 'middle'
}
// Get the appropriate display time for a reservation on a specific day
const getDisplayTimeForDay = (r: Reservation, dayDate: string): string | null => {
const phase = getSpanPhase(r, dayDate)
if (phase === 'end') return r.reservation_end_time || null
if (phase === 'middle') return null
return r.reservation_time || null
}
// Get phase label for multi-day badge
const getSpanLabel = (r: Reservation, phase: string): string | null => {
if (phase === 'single') return null
if (r.type === 'flight') return t(`reservations.span.${phase === 'start' ? 'departure' : phase === 'end' ? 'arrival' : 'inTransit'}`)
if (r.type === 'car') return t(`reservations.span.${phase === 'start' ? 'pickup' : phase === 'end' ? 'return' : 'active'}`)
return t(`reservations.span.${phase === 'start' ? 'start' : phase === 'end' ? 'end' : 'ongoing'}`)
}
const getTransportForDay = (dayId: number) => {
const day = days.find(d => d.id === dayId)
if (!day?.date) return []
return reservations.filter(r => {
if (!r.reservation_time || !TRANSPORT_TYPES.has(r.type)) return false
const resDate = r.reservation_time.split('T')[0]
return resDate === day.date
if (!r.reservation_time || r.type === 'hotel') return false
const startDate = r.reservation_time.split('T')[0]
const endDate = getEndDate(r)
if (endDate && endDate !== startDate) {
// Multi-day: show on any day in range (car middle handled elsewhere)
return day.date >= startDate && day.date <= endDate
} else {
// Single-day: show all non-hotel reservations that match this day's date
return startDate === day.date
}
})
}
// Get car rentals that are in "active" (middle) phase for a day — shown in day header, not timeline
const getActiveRentalsForDay = (dayId: number) => {
const day = days.find(d => d.id === dayId)
if (!day?.date) return []
return reservations.filter(r => {
if (r.type !== 'car' || !r.reservation_time) return false
const startDate = r.reservation_time.split('T')[0]
const endDate = getEndDate(r)
if (!endDate || endDate === startDate) return false
return day.date > startDate && day.date < endDate
})
}
@@ -279,6 +333,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
const da = getDayAssignments(dayId)
const dn = (dayNotes[String(dayId)] || []).slice().sort((a, b) => a.sort_order - b.sort_order)
const transport = getTransportForDay(dayId)
const dayDate = days.find(d => d.id === dayId)?.date || ''
// Initialize positions for transports that don't have one yet
if (transport.some(r => r.day_plan_position == null)) {
@@ -295,9 +350,14 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
].sort((a, b) => a.sortKey - b.sortKey)
// Timed places + transports: compute sortKeys based on time, inserted among base items
// For multi-day transports, use the appropriate display time for this day
const allTimed = [
...timedPlaces.map(a => ({ type: 'place' as const, data: a, minutes: parseTimeToMinutes(a.place?.place_time)! })),
...transport.map(r => ({ type: 'transport' as const, data: r, minutes: parseTimeToMinutes(r.reservation_time) ?? 0 })),
...transport.map(r => ({
type: 'transport' as const,
data: r,
minutes: parseTimeToMinutes(getDisplayTimeForDay(r, dayDate)) ?? 0,
})),
].sort((a, b) => a.minutes - b.minutes)
if (allTimed.length === 0) return baseItems
@@ -968,6 +1028,17 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
)
})
})()}
{/* Active rental car badges */}
{(() => {
const activeRentals = getActiveRentalsForDay(day.id)
if (activeRentals.length === 0) return null
return activeRentals.map(r => (
<span key={`rental-${r.id}`} onClick={e => { e.stopPropagation(); setTransportDetail(r) }} style={{ display: 'inline-flex', alignItems: 'center', gap: 3, padding: '2px 7px', borderRadius: 5, background: 'rgba(59,130,246,0.08)', border: '1px solid rgba(59,130,246,0.2)', flexShrink: 1, minWidth: 0, maxWidth: '40%', cursor: 'pointer' }}>
<Car size={8} style={{ color: '#3b82f6', flexShrink: 0 }} />
<span style={{ fontSize: 9, color: 'var(--text-muted)', fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.title}</span>
</span>
))
})()}
</div>
)}
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 2, flexWrap: 'wrap' }}>
@@ -1291,6 +1362,11 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
// Transport booking (flight, train, bus, car, cruise)
if (item.type === 'transport') {
const res = item.data
const spanPhase = getSpanPhase(res, day.date)
// Car "active" (middle) days are shown in the day header, skip here
if (res.type === 'car' && spanPhase === 'middle') return null
const TransportIcon = RES_ICONS[res.type] || Ticket
const color = '#3b82f6'
const meta = typeof res.metadata === 'string' ? JSON.parse(res.metadata || '{}') : (res.metadata || {})
@@ -1307,8 +1383,12 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
subtitle = [meta.train_number, meta.platform ? `Gl. ${meta.platform}` : '', meta.seat ? `Sitz ${meta.seat}` : ''].filter(Boolean).join(' · ')
}
// Multi-day span phase
const spanLabel = getSpanLabel(res, spanPhase)
const displayTime = getDisplayTimeForDay(res, day.date)
return (
<React.Fragment key={`transport-${res.id}`}>
<React.Fragment key={`transport-${res.id}-${day.id}`}>
{showDropLine && <div style={{ height: 2, background: 'var(--text-primary)', borderRadius: 1, margin: '2px 8px' }} />}
<div
onClick={() => setTransportDetail(res)}
@@ -1340,6 +1420,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
background: isTransportHovered ? `${color}12` : `${color}08`,
cursor: 'pointer', userSelect: 'none',
transition: 'background 0.1s',
opacity: spanPhase === 'middle' ? 0.65 : 1,
}}
>
<div style={{
@@ -1350,14 +1431,27 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar({
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
{spanLabel && (
<span style={{
fontSize: 9, fontWeight: 700, padding: '1px 5px', borderRadius: 4, flexShrink: 0,
background: `${color}20`, color: color, textTransform: 'uppercase', letterSpacing: '0.03em',
}}>
{spanLabel}
</span>
)}
<span style={{ fontSize: 12.5, fontWeight: 500, color: 'var(--text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{res.title}
</span>
{res.reservation_time?.includes('T') && (
{displayTime?.includes('T') && (
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 3, flexShrink: 0, fontSize: 10, color: 'var(--text-faint)', fontWeight: 400, marginLeft: 6 }}>
<Clock size={9} strokeWidth={2} />
{new Date(res.reservation_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}
{res.reservation_end_time?.includes('T') && ` ${new Date(res.reservation_end_time).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}`}
{new Date(displayTime).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}
{spanPhase === 'single' && res.reservation_end_time && (() => {
const endStr = res.reservation_end_time.includes('T') ? res.reservation_end_time : (displayTime.split('T')[0] + 'T' + res.reservation_end_time)
return ` ${new Date(endStr).toLocaleTimeString(locale, { hour: '2-digit', minute: '2-digit', hour12: timeFormat === '12h' })}`
})()}
{meta.departure_timezone && spanPhase === 'start' && ` ${meta.departure_timezone}`}
{meta.arrival_timezone && spanPhase === 'end' && ` ${meta.arrival_timezone}`}
</span>
)}
</div>

View File

@@ -73,9 +73,10 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
const [form, setForm] = useState({
title: '', type: 'other', status: 'pending',
reservation_time: '', reservation_end_time: '', location: '', confirmation_number: '',
reservation_time: '', reservation_end_time: '', end_date: '', location: '', confirmation_number: '',
notes: '', assignment_id: '', accommodation_id: '',
meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '',
meta_departure_timezone: '', meta_arrival_timezone: '',
meta_train_number: '', meta_platform: '', meta_seat: '',
meta_check_in_time: '', meta_check_out_time: '',
hotel_place_id: '', hotel_start_day: '', hotel_end_day: '',
@@ -95,12 +96,21 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
useEffect(() => {
if (reservation) {
const meta = typeof reservation.metadata === 'string' ? JSON.parse(reservation.metadata || '{}') : (reservation.metadata || {})
// Parse end_date from reservation_end_time if it's a full ISO datetime
const rawEnd = reservation.reservation_end_time || ''
let endDate = ''
let endTime = rawEnd
if (rawEnd.includes('T')) {
endDate = rawEnd.split('T')[0]
endTime = rawEnd.split('T')[1]?.slice(0, 5) || ''
}
setForm({
title: reservation.title || '',
type: reservation.type || 'other',
status: reservation.status || 'pending',
reservation_time: reservation.reservation_time ? reservation.reservation_time.slice(0, 16) : '',
reservation_end_time: reservation.reservation_end_time || '',
reservation_end_time: endTime,
end_date: endDate,
location: reservation.location || '',
confirmation_number: reservation.confirmation_number || '',
notes: reservation.notes || '',
@@ -110,6 +120,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
meta_flight_number: meta.flight_number || '',
meta_departure_airport: meta.departure_airport || '',
meta_arrival_airport: meta.arrival_airport || '',
meta_departure_timezone: meta.departure_timezone || '',
meta_arrival_timezone: meta.arrival_timezone || '',
meta_train_number: meta.train_number || '',
meta_platform: meta.platform || '',
meta_seat: meta.seat || '',
@@ -122,9 +134,10 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
} else {
setForm({
title: '', type: 'other', status: 'pending',
reservation_time: '', reservation_end_time: '', location: '', confirmation_number: '',
reservation_time: '', reservation_end_time: '', end_date: '', location: '', confirmation_number: '',
notes: '', assignment_id: '', accommodation_id: '',
meta_airline: '', meta_flight_number: '', meta_departure_airport: '', meta_arrival_airport: '',
meta_departure_timezone: '', meta_arrival_timezone: '',
meta_train_number: '', meta_platform: '', meta_seat: '',
meta_check_in_time: '', meta_check_out_time: '',
})
@@ -134,9 +147,21 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
const set = (field, value) => setForm(prev => ({ ...prev, [field]: value }))
// Validate that end datetime is after start datetime
const isEndBeforeStart = (() => {
if (!form.end_date || !form.reservation_time) return false
const startDate = form.reservation_time.split('T')[0]
const startTime = form.reservation_time.split('T')[1] || '00:00'
const endTime = form.reservation_end_time || '00:00'
const startFull = `${startDate}T${startTime}`
const endFull = `${form.end_date}T${endTime}`
return endFull <= startFull
})()
const handleSubmit = async (e) => {
e.preventDefault()
if (!form.title.trim()) return
if (isEndBeforeStart) { toast.error(t('reservations.validation.endBeforeStart')); return }
setIsSaving(true)
try {
const metadata: Record<string, string> = {}
@@ -145,6 +170,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
if (form.meta_flight_number) metadata.flight_number = form.meta_flight_number
if (form.meta_departure_airport) metadata.departure_airport = form.meta_departure_airport
if (form.meta_arrival_airport) metadata.arrival_airport = form.meta_arrival_airport
if (form.meta_departure_timezone) metadata.departure_timezone = form.meta_departure_timezone
if (form.meta_arrival_timezone) metadata.arrival_timezone = form.meta_arrival_timezone
} else if (form.type === 'hotel') {
if (form.meta_check_in_time) metadata.check_in_time = form.meta_check_in_time
if (form.meta_check_out_time) metadata.check_out_time = form.meta_check_out_time
@@ -153,9 +180,14 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
if (form.meta_platform) metadata.platform = form.meta_platform
if (form.meta_seat) metadata.seat = form.meta_seat
}
// Combine end_date + end_time into reservation_end_time
let combinedEndTime = form.reservation_end_time
if (form.end_date) {
combinedEndTime = form.reservation_end_time ? `${form.end_date}T${form.reservation_end_time}` : form.end_date
}
const saveData: Record<string, any> = {
title: form.title, type: form.type, status: form.status,
reservation_time: form.reservation_time, reservation_end_time: form.reservation_end_time,
reservation_time: form.reservation_time, reservation_end_time: combinedEndTime,
location: form.location, confirmation_number: form.confirmation_number,
notes: form.notes,
assignment_id: form.assignment_id || null,
@@ -257,10 +289,9 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
placeholder={t('reservations.titlePlaceholder')} style={inputStyle} />
</div>
{/* Assignment Picker + Date (hidden for hotels) */}
{form.type !== 'hotel' && (
<div style={{ display: 'flex', gap: 8 }}>
{assignmentOptions.length > 0 && (
{/* Assignment Picker (hidden for hotels) */}
{form.type !== 'hotel' && assignmentOptions.length > 0 && (
<div>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>
<Link2 size={10} style={{ display: 'inline', verticalAlign: '-1px', marginRight: 3 }} />
@@ -287,54 +318,81 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
size="sm"
/>
</div>
)}
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.date')}</label>
<CustomDatePicker
value={(() => { const [d] = (form.reservation_time || '').split('T'); return d || '' })()}
onChange={d => {
const [, t] = (form.reservation_time || '').split('T')
set('reservation_time', d ? (t ? `${d}T${t}` : d) : '')
}}
/>
</div>
</div>
)}
{/* Start Time + End Time + Status */}
<div style={{ display: 'flex', gap: 8 }}>
{form.type !== 'hotel' && (
<>
{/* Start Date/Time + End Date/Time + Status (hidden for hotels) */}
{form.type !== 'hotel' && (
<>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{form.type === 'flight' ? t('reservations.departureDate') : form.type === 'car' ? t('reservations.pickupDate') : t('reservations.date')}</label>
<CustomDatePicker
value={(() => { const [d] = (form.reservation_time || '').split('T'); return d || '' })()}
onChange={d => {
const [, t] = (form.reservation_time || '').split('T')
set('reservation_time', d ? (t ? `${d}T${t}` : d) : '')
}}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{form.type === 'flight' ? t('reservations.departureTime') : form.type === 'car' ? t('reservations.pickupTime') : t('reservations.startTime')}</label>
<CustomTimePicker
value={(() => { const [, t] = (form.reservation_time || '').split('T'); return t || '' })()}
onChange={t => {
const [d] = (form.reservation_time || '').split('T')
const date = d || new Date().toISOString().split('T')[0]
set('reservation_time', t ? `${date}T${t}` : date)
}}
/>
</div>
{form.type === 'flight' && (
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.startTime')}</label>
<CustomTimePicker
value={(() => { const [, t] = (form.reservation_time || '').split('T'); return t || '' })()}
onChange={t => {
const [d] = (form.reservation_time || '').split('T')
const date = d || new Date().toISOString().split('T')[0]
set('reservation_time', t ? `${date}T${t}` : date)
}}
/>
<label style={labelStyle}>{t('reservations.meta.departureTimezone')}</label>
<input type="text" value={form.meta_departure_timezone} onChange={e => set('meta_departure_timezone', e.target.value)}
placeholder="e.g. CET, UTC+1" style={inputStyle} />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.endTime')}</label>
<CustomTimePicker value={form.reservation_end_time} onChange={v => set('reservation_end_time', v)} />
</div>
</>
)}
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.status')}</label>
<CustomSelect
value={form.status}
onChange={value => set('status', value)}
options={[
{ value: 'pending', label: t('reservations.pending') },
{ value: 'confirmed', label: t('reservations.confirmed') },
]}
size="sm"
/>
)}
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{form.type === 'flight' ? t('reservations.arrivalDate') : form.type === 'car' ? t('reservations.returnDate') : t('reservations.endDate')}</label>
<CustomDatePicker
value={form.end_date}
onChange={d => set('end_date', d || '')}
/>
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{form.type === 'flight' ? t('reservations.arrivalTime') : form.type === 'car' ? t('reservations.returnTime') : t('reservations.endTime')}</label>
<CustomTimePicker value={form.reservation_end_time} onChange={v => set('reservation_end_time', v)} />
</div>
{form.type === 'flight' && (
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.meta.arrivalTimezone')}</label>
<input type="text" value={form.meta_arrival_timezone} onChange={e => set('meta_arrival_timezone', e.target.value)}
placeholder="e.g. JST, UTC+9" style={inputStyle} />
</div>
)}
</div>
{isEndBeforeStart && (
<div style={{ fontSize: 11, color: '#ef4444', marginTop: -6 }}>{t('reservations.validation.endBeforeStart')}</div>
)}
<div style={{ display: 'flex', gap: 8 }}>
<div style={{ flex: 1, minWidth: 0 }}>
<label style={labelStyle}>{t('reservations.status')}</label>
<CustomSelect
value={form.status}
onChange={value => set('status', value)}
options={[
{ value: 'pending', label: t('reservations.pending') },
{ value: 'confirmed', label: t('reservations.confirmed') },
]}
size="sm"
/>
</div>
</div>
</>
)}
{/* Location + Booking Code */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -422,8 +480,8 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
/>
</div>
</div>
{/* Check-in/out times */}
<div className="grid grid-cols-2 gap-3">
{/* Check-in/out times + Status */}
<div className="grid grid-cols-3 gap-3">
<div>
<label style={labelStyle}>{t('reservations.meta.checkIn')}</label>
<CustomTimePicker value={form.meta_check_in_time} onChange={v => set('meta_check_in_time', v)} />
@@ -432,6 +490,18 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
<label style={labelStyle}>{t('reservations.meta.checkOut')}</label>
<CustomTimePicker value={form.meta_check_out_time} onChange={v => set('meta_check_out_time', v)} />
</div>
<div>
<label style={labelStyle}>{t('reservations.status')}</label>
<CustomSelect
value={form.status}
onChange={value => set('status', value)}
options={[
{ value: 'pending', label: t('reservations.pending') },
{ value: 'confirmed', label: t('reservations.confirmed') },
]}
size="sm"
/>
</div>
</div>
</>
)}
@@ -561,7 +631,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
<button type="button" onClick={onClose} style={{ padding: '8px 16px', borderRadius: 10, border: '1px solid var(--border-primary)', background: 'none', fontSize: 12, cursor: 'pointer', fontFamily: 'inherit', color: 'var(--text-muted)' }}>
{t('common.cancel')}
</button>
<button type="submit" disabled={isSaving || !form.title.trim()} style={{ padding: '8px 20px', borderRadius: 10, border: 'none', background: 'var(--text-primary)', color: 'var(--bg-primary)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() ? 0.5 : 1 }}>
<button type="submit" disabled={isSaving || !form.title.trim() || isEndBeforeStart} style={{ padding: '8px 20px', borderRadius: 10, border: 'none', background: 'var(--text-primary)', color: 'var(--bg-primary)', fontSize: 12, fontWeight: 600, cursor: 'pointer', fontFamily: 'inherit', opacity: isSaving || !form.title.trim() || isEndBeforeStart ? 0.5 : 1 }}>
{isSaving ? t('common.saving') : reservation ? t('common.update') : t('common.add')}
</button>
</div>

View File

@@ -136,7 +136,12 @@ function ReservationCard({ r, tripId, onEdit, onDelete, files = [], onNavigateTo
{r.reservation_time && (
<div style={{ flex: 1, padding: '5px 10px', textAlign: 'center', borderRight: '1px solid var(--border-faint)' }}>
<div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-faint)', textTransform: 'uppercase', letterSpacing: '0.03em' }}>{t('reservations.date')}</div>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1 }}>{fmtDate(r.reservation_time)}</div>
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-primary)', marginTop: 1 }}>
{fmtDate(r.reservation_time)}
{r.reservation_end_time?.includes('T') && r.reservation_end_time.split('T')[0] !== r.reservation_time.split('T')[0] && (
<> {fmtDate(r.reservation_end_time)}</>
)}
</div>
</div>
)}
{r.reservation_time?.includes('T') && (

View File

@@ -936,6 +936,27 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'ربط بخطة اليوم',
'reservations.pickAssignment': 'اختر عنصرًا من خطتك...',
'reservations.noAssignment': 'بلا ربط',
'reservations.departureDate': 'المغادرة',
'reservations.arrivalDate': 'الوصول',
'reservations.departureTime': 'وقت المغادرة',
'reservations.arrivalTime': 'وقت الوصول',
'reservations.pickupDate': 'الاستلام',
'reservations.returnDate': 'الإرجاع',
'reservations.pickupTime': 'وقت الاستلام',
'reservations.returnTime': 'وقت الإرجاع',
'reservations.endDate': 'تاريخ الانتهاء',
'reservations.meta.departureTimezone': 'TZ المغادرة',
'reservations.meta.arrivalTimezone': 'TZ الوصول',
'reservations.span.departure': 'المغادرة',
'reservations.span.arrival': 'الوصول',
'reservations.span.inTransit': 'في الطريق',
'reservations.span.pickup': 'الاستلام',
'reservations.span.return': 'الإرجاع',
'reservations.span.active': 'نشط',
'reservations.span.start': 'البداية',
'reservations.span.end': 'النهاية',
'reservations.span.ongoing': 'جارٍ',
'reservations.validation.endBeforeStart': 'يجب أن يكون تاريخ/وقت الانتهاء بعد تاريخ/وقت البدء',
// Budget
'budget.title': 'الميزانية',
@@ -1546,4 +1567,5 @@ const ar: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'إشعار تجريبي للرحلة "{trip}".',
}
export default ar
export default ar

View File

@@ -917,6 +917,27 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Vincular à atribuição do dia',
'reservations.pickAssignment': 'Selecione uma atribuição do seu plano...',
'reservations.noAssignment': 'Sem vínculo (avulsa)',
'reservations.departureDate': 'Partida',
'reservations.arrivalDate': 'Chegada',
'reservations.departureTime': 'Hora partida',
'reservations.arrivalTime': 'Hora chegada',
'reservations.pickupDate': 'Retirada',
'reservations.returnDate': 'Devolução',
'reservations.pickupTime': 'Hora retirada',
'reservations.returnTime': 'Hora devolução',
'reservations.endDate': 'Data final',
'reservations.meta.departureTimezone': 'TZ partida',
'reservations.meta.arrivalTimezone': 'TZ chegada',
'reservations.span.departure': 'Partida',
'reservations.span.arrival': 'Chegada',
'reservations.span.inTransit': 'Em trânsito',
'reservations.span.pickup': 'Retirada',
'reservations.span.return': 'Devolução',
'reservations.span.active': 'Ativo',
'reservations.span.start': 'Início',
'reservations.span.end': 'Fim',
'reservations.span.ongoing': 'Em andamento',
'reservations.validation.endBeforeStart': 'A data/hora final deve ser posterior à data/hora inicial',
// Budget
'budget.title': 'Orçamento',
@@ -1541,4 +1562,5 @@ const br: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'Notificação de teste para a viagem "{trip}".',
}
export default br
export default br

View File

@@ -934,6 +934,27 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Propojit s přiřazením dne',
'reservations.pickAssignment': 'Vyberte přiřazení z vašeho plánu...',
'reservations.noAssignment': 'Bez propojení (samostatné)',
'reservations.departureDate': 'Odlet',
'reservations.arrivalDate': 'Přílet',
'reservations.departureTime': 'Čas odletu',
'reservations.arrivalTime': 'Čas příletu',
'reservations.pickupDate': 'Vyzvednutí',
'reservations.returnDate': 'Vrácení',
'reservations.pickupTime': 'Čas vyzvednutí',
'reservations.returnTime': 'Čas vrácení',
'reservations.endDate': 'Datum konce',
'reservations.meta.departureTimezone': 'TZ odletu',
'reservations.meta.arrivalTimezone': 'TZ příletu',
'reservations.span.departure': 'Odlet',
'reservations.span.arrival': 'Přílet',
'reservations.span.inTransit': 'Na cestě',
'reservations.span.pickup': 'Vyzvednutí',
'reservations.span.return': 'Vrácení',
'reservations.span.active': 'Aktivní',
'reservations.span.start': 'Začátek',
'reservations.span.end': 'Konec',
'reservations.span.ongoing': 'Probíhá',
'reservations.validation.endBeforeStart': 'Datum/čas konce musí být po datu/čase začátku',
// Rozpočet (Budget)
'budget.title': 'Rozpočet',
@@ -1546,4 +1567,5 @@ const cs: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'Testovací oznámení pro výlet "{trip}".',
}
export default cs
export default cs

View File

@@ -933,6 +933,27 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Mit Tagesplanung verknüpfen',
'reservations.pickAssignment': 'Zuordnung aus dem Plan wählen...',
'reservations.noAssignment': 'Keine Verknüpfung',
'reservations.departureDate': 'Abflug',
'reservations.arrivalDate': 'Ankunft',
'reservations.departureTime': 'Abflugzeit',
'reservations.arrivalTime': 'Ankunftszeit',
'reservations.pickupDate': 'Abholung',
'reservations.returnDate': 'Rückgabe',
'reservations.pickupTime': 'Abholzeit',
'reservations.returnTime': 'Rückgabezeit',
'reservations.endDate': 'Enddatum',
'reservations.meta.departureTimezone': 'Abfl. TZ',
'reservations.meta.arrivalTimezone': 'Ank. TZ',
'reservations.span.departure': 'Abflug',
'reservations.span.arrival': 'Ankunft',
'reservations.span.inTransit': 'Unterwegs',
'reservations.span.pickup': 'Abholung',
'reservations.span.return': 'Rückgabe',
'reservations.span.active': 'Aktiv',
'reservations.span.start': 'Start',
'reservations.span.end': 'Ende',
'reservations.span.ongoing': 'Laufend',
'reservations.validation.endBeforeStart': 'Enddatum/-zeit muss nach dem Startdatum/-zeit liegen',
// Budget
'budget.title': 'Budget',
@@ -1543,4 +1564,5 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'Testbenachrichtigung für Reise "{trip}".',
}
export default de
export default de

View File

@@ -930,6 +930,27 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Link to day assignment',
'reservations.pickAssignment': 'Select an assignment from your plan...',
'reservations.noAssignment': 'No link (standalone)',
'reservations.departureDate': 'Departure',
'reservations.arrivalDate': 'Arrival',
'reservations.departureTime': 'Dep. time',
'reservations.arrivalTime': 'Arr. time',
'reservations.pickupDate': 'Pickup',
'reservations.returnDate': 'Return',
'reservations.pickupTime': 'Pickup time',
'reservations.returnTime': 'Return time',
'reservations.endDate': 'End date',
'reservations.meta.departureTimezone': 'Dep. TZ',
'reservations.meta.arrivalTimezone': 'Arr. TZ',
'reservations.span.departure': 'Departure',
'reservations.span.arrival': 'Arrival',
'reservations.span.inTransit': 'In transit',
'reservations.span.pickup': 'Pickup',
'reservations.span.return': 'Return',
'reservations.span.active': 'Active',
'reservations.span.start': 'Start',
'reservations.span.end': 'End',
'reservations.span.ongoing': 'Ongoing',
'reservations.validation.endBeforeStart': 'End date/time must be after start date/time',
// Budget
'budget.title': 'Budget',

View File

@@ -893,6 +893,27 @@ const es: Record<string, string> = {
'reservations.linkAssignment': 'Vincular a una asignación del día',
'reservations.pickAssignment': 'Selecciona una asignación de tu plan...',
'reservations.noAssignment': 'Sin vínculo (independiente)',
'reservations.departureDate': 'Salida',
'reservations.arrivalDate': 'Llegada',
'reservations.departureTime': 'Hora salida',
'reservations.arrivalTime': 'Hora llegada',
'reservations.pickupDate': 'Recogida',
'reservations.returnDate': 'Devolución',
'reservations.pickupTime': 'Hora recogida',
'reservations.returnTime': 'Hora devolución',
'reservations.endDate': 'Fecha fin',
'reservations.meta.departureTimezone': 'TZ salida',
'reservations.meta.arrivalTimezone': 'TZ llegada',
'reservations.span.departure': 'Salida',
'reservations.span.arrival': 'Llegada',
'reservations.span.inTransit': 'En tránsito',
'reservations.span.pickup': 'Recogida',
'reservations.span.return': 'Devolución',
'reservations.span.active': 'Activo',
'reservations.span.start': 'Inicio',
'reservations.span.end': 'Fin',
'reservations.span.ongoing': 'En curso',
'reservations.validation.endBeforeStart': 'La fecha/hora de fin debe ser posterior a la de inicio',
// Budget
'budget.title': 'Presupuesto',
@@ -1548,4 +1569,5 @@ const es: Record<string, string> = {
'notifications.test.tripText': 'Notificación de prueba para el viaje "{trip}".',
}
export default es
export default es

View File

@@ -932,6 +932,27 @@ const fr: Record<string, string> = {
'reservations.linkAssignment': 'Lier à l\'affectation du jour',
'reservations.pickAssignment': 'Sélectionnez une affectation de votre plan…',
'reservations.noAssignment': 'Aucun lien (autonome)',
'reservations.departureDate': 'Départ',
'reservations.arrivalDate': 'Arrivée',
'reservations.departureTime': 'Heure dép.',
'reservations.arrivalTime': 'Heure arr.',
'reservations.pickupDate': 'Prise en charge',
'reservations.returnDate': 'Restitution',
'reservations.pickupTime': 'Heure prise en charge',
'reservations.returnTime': 'Heure restitution',
'reservations.endDate': 'Date de fin',
'reservations.meta.departureTimezone': 'TZ dép.',
'reservations.meta.arrivalTimezone': 'TZ arr.',
'reservations.span.departure': 'Départ',
'reservations.span.arrival': 'Arrivée',
'reservations.span.inTransit': 'En transit',
'reservations.span.pickup': 'Prise en charge',
'reservations.span.return': 'Restitution',
'reservations.span.active': 'Actif',
'reservations.span.start': 'Début',
'reservations.span.end': 'Fin',
'reservations.span.ongoing': 'En cours',
'reservations.validation.endBeforeStart': 'La date/heure de fin doit être postérieure à la date/heure de début',
// Budget
'budget.title': 'Budget',
@@ -1542,4 +1563,5 @@ const fr: Record<string, string> = {
'notifications.test.tripText': 'Notification de test pour le voyage "{trip}".',
}
export default fr
export default fr

View File

@@ -933,6 +933,27 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Összekapcsolás napi tervvel',
'reservations.pickAssignment': 'Válassz hozzárendelést a tervedből...',
'reservations.noAssignment': 'Nincs összekapcsolás (önálló)',
'reservations.departureDate': 'Indulás',
'reservations.arrivalDate': 'Érkezés',
'reservations.departureTime': 'Indulási idő',
'reservations.arrivalTime': 'Érkezési idő',
'reservations.pickupDate': 'Felvétel',
'reservations.returnDate': 'Visszaadás',
'reservations.pickupTime': 'Felvétel ideje',
'reservations.returnTime': 'Visszaadás ideje',
'reservations.endDate': 'Befejezés dátuma',
'reservations.meta.departureTimezone': 'TZ indulás',
'reservations.meta.arrivalTimezone': 'TZ érkezés',
'reservations.span.departure': 'Indulás',
'reservations.span.arrival': 'Érkezés',
'reservations.span.inTransit': 'Úton',
'reservations.span.pickup': 'Felvétel',
'reservations.span.return': 'Visszaadás',
'reservations.span.active': 'Aktív',
'reservations.span.start': 'Kezdés',
'reservations.span.end': 'Vége',
'reservations.span.ongoing': 'Folyamatban',
'reservations.validation.endBeforeStart': 'A befejezés dátuma/időpontja a kezdés utáni kell legyen',
// Költségvetés
'budget.title': 'Költségvetés',
@@ -1543,4 +1564,5 @@ const hu: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'Teszt értesítés a(z) "{trip}" utazáshoz.',
}
export default hu
export default hu

View File

@@ -933,6 +933,27 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Collega all\'assegnazione del giorno',
'reservations.pickAssignment': 'Seleziona un\'assegnazione dal tuo programma...',
'reservations.noAssignment': 'Nessun collegamento (autonomo)',
'reservations.departureDate': 'Partenza',
'reservations.arrivalDate': 'Arrivo',
'reservations.departureTime': 'Ora part.',
'reservations.arrivalTime': 'Ora arr.',
'reservations.pickupDate': 'Ritiro',
'reservations.returnDate': 'Riconsegna',
'reservations.pickupTime': 'Ora ritiro',
'reservations.returnTime': 'Ora riconsegna',
'reservations.endDate': 'Data fine',
'reservations.meta.departureTimezone': 'TZ part.',
'reservations.meta.arrivalTimezone': 'TZ arr.',
'reservations.span.departure': 'Partenza',
'reservations.span.arrival': 'Arrivo',
'reservations.span.inTransit': 'In transito',
'reservations.span.pickup': 'Ritiro',
'reservations.span.return': 'Riconsegna',
'reservations.span.active': 'Attivo',
'reservations.span.start': 'Inizio',
'reservations.span.end': 'Fine',
'reservations.span.ongoing': 'In corso',
'reservations.validation.endBeforeStart': 'La data/ora di fine deve essere successiva alla data/ora di inizio',
// Budget
'budget.title': 'Budget',
@@ -1543,4 +1564,5 @@ const it: Record<string, string | { name: string; category: string }[]> = {
'notifications.test.tripText': 'Notifica di test per il viaggio "{trip}".',
}
export default it
export default it

View File

@@ -932,6 +932,27 @@ const nl: Record<string, string> = {
'reservations.linkAssignment': 'Koppelen aan dagtoewijzing',
'reservations.pickAssignment': 'Selecteer een toewijzing uit je plan...',
'reservations.noAssignment': 'Geen koppeling (zelfstandig)',
'reservations.departureDate': 'Vertrek',
'reservations.arrivalDate': 'Aankomst',
'reservations.departureTime': 'Vertrektijd',
'reservations.arrivalTime': 'Aankomsttijd',
'reservations.pickupDate': 'Ophalen',
'reservations.returnDate': 'Inleveren',
'reservations.pickupTime': 'Ophaaltijd',
'reservations.returnTime': 'Inlevertijd',
'reservations.endDate': 'Einddatum',
'reservations.meta.departureTimezone': 'TZ vertrek',
'reservations.meta.arrivalTimezone': 'TZ aankomst',
'reservations.span.departure': 'Vertrek',
'reservations.span.arrival': 'Aankomst',
'reservations.span.inTransit': 'Onderweg',
'reservations.span.pickup': 'Ophalen',
'reservations.span.return': 'Inleveren',
'reservations.span.active': 'Actief',
'reservations.span.start': 'Start',
'reservations.span.end': 'Einde',
'reservations.span.ongoing': 'Lopend',
'reservations.validation.endBeforeStart': 'Einddatum/-tijd moet na de startdatum/-tijd liggen',
// Budget
'budget.title': 'Budget',
@@ -1542,4 +1563,5 @@ const nl: Record<string, string> = {
'notifications.test.tripText': 'Testmelding voor reis "{trip}".',
}
export default nl
export default nl

View File

@@ -888,6 +888,27 @@ const pl: Record<string, string | { name: string; category: string }[]> = {
'reservations.linkAssignment': 'Przypisz do miejsca',
'reservations.pickAssignment': 'Wybierz miejsce z planu...',
'reservations.noAssignment': 'Brak przypisania (samodzielna)',
'reservations.departureDate': 'Wylot',
'reservations.arrivalDate': 'Przylot',
'reservations.departureTime': 'Godz. wylotu',
'reservations.arrivalTime': 'Godz. przylotu',
'reservations.pickupDate': 'Odbiór',
'reservations.returnDate': 'Zwrot',
'reservations.pickupTime': 'Godz. odbioru',
'reservations.returnTime': 'Godz. zwrotu',
'reservations.endDate': 'Data końca',
'reservations.meta.departureTimezone': 'TZ wylotu',
'reservations.meta.arrivalTimezone': 'TZ przylotu',
'reservations.span.departure': 'Wylot',
'reservations.span.arrival': 'Przylot',
'reservations.span.inTransit': 'W tranzycie',
'reservations.span.pickup': 'Odbiór',
'reservations.span.return': 'Zwrot',
'reservations.span.active': 'Aktywny',
'reservations.span.start': 'Start',
'reservations.span.end': 'Koniec',
'reservations.span.ongoing': 'W trakcie',
'reservations.validation.endBeforeStart': 'Data/godzina zakończenia musi być późniejsza niż data/godzina rozpoczęcia',
// Budget
'budget.title': 'Budżet',

View File

@@ -932,6 +932,27 @@ const ru: Record<string, string> = {
'reservations.linkAssignment': 'Привязать к назначению дня',
'reservations.pickAssignment': 'Выберите назначение из вашего плана...',
'reservations.noAssignment': 'Без привязки (самостоятельное)',
'reservations.departureDate': 'Вылет',
'reservations.arrivalDate': 'Прилёт',
'reservations.departureTime': 'Время вылета',
'reservations.arrivalTime': 'Время прилёта',
'reservations.pickupDate': 'Получение',
'reservations.returnDate': 'Возврат',
'reservations.pickupTime': 'Время получения',
'reservations.returnTime': 'Время возврата',
'reservations.endDate': 'Дата окончания',
'reservations.meta.departureTimezone': 'TZ вылета',
'reservations.meta.arrivalTimezone': 'TZ прилёта',
'reservations.span.departure': 'Вылет',
'reservations.span.arrival': 'Прилёт',
'reservations.span.inTransit': 'В пути',
'reservations.span.pickup': 'Получение',
'reservations.span.return': 'Возврат',
'reservations.span.active': 'Активно',
'reservations.span.start': 'Начало',
'reservations.span.end': 'Конец',
'reservations.span.ongoing': 'Продолжается',
'reservations.validation.endBeforeStart': 'Дата/время окончания должны быть позже даты/времени начала',
// Budget
'budget.title': 'Бюджет',
@@ -1542,4 +1563,5 @@ const ru: Record<string, string> = {
'notifications.test.tripText': 'Тестовое уведомление для поездки "{trip}".',
}
export default ru
export default ru

View File

@@ -932,6 +932,27 @@ const zh: Record<string, string> = {
'reservations.linkAssignment': '关联日程分配',
'reservations.pickAssignment': '从计划中选择一个分配...',
'reservations.noAssignment': '无关联(独立)',
'reservations.departureDate': '出发',
'reservations.arrivalDate': '到达',
'reservations.departureTime': '出发时间',
'reservations.arrivalTime': '到达时间',
'reservations.pickupDate': '取车',
'reservations.returnDate': '还车',
'reservations.pickupTime': '取车时间',
'reservations.returnTime': '还车时间',
'reservations.endDate': '结束日期',
'reservations.meta.departureTimezone': '出发时区',
'reservations.meta.arrivalTimezone': '到达时区',
'reservations.span.departure': '出发',
'reservations.span.arrival': '到达',
'reservations.span.inTransit': '途中',
'reservations.span.pickup': '取车',
'reservations.span.return': '还车',
'reservations.span.active': '使用中',
'reservations.span.start': '开始',
'reservations.span.end': '结束',
'reservations.span.ongoing': '进行中',
'reservations.validation.endBeforeStart': '结束日期/时间必须晚于开始日期/时间',
// Budget
'budget.title': '预算',
@@ -1542,4 +1563,5 @@ const zh: Record<string, string> = {
'notifications.test.tripText': '行程"{trip}"的测试通知。',
}
export default zh
export default zh