Merge pull request #361 from lucaam/add_span_days_feature
Support multi-day spanning for reservations
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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') && (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user