From 530550455db0df21ea23280bd1a07340561813a6 Mon Sep 17 00:00:00 2001 From: Maurice Date: Fri, 17 Apr 2026 23:25:38 +0200 Subject: [PATCH] feat(ui): unified toolbar design + redesigned budget widgets + polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trip planner now has a consistent rounded toolbar across bookings, lists, budget and files. Each panel shows title, inline filter pills (with counts where useful) and an accent action button on the right. Moved per-tab controls into the toolbar — lists import, todo add, budget currency/add-category, files trash/filters — and dropped the redundant in-panel headers. Budget sidebar redesigned: total-budget card with indigo-ringed avatars and coloured split bar; settlement flows as paired avatar cards; by-category donut rebuilt in SVG with per-category gradients. Both cards now follow dark/light mode via a widgetTheme helper. Todo: add-new-task is a portalled modal on desktop, the add-task input bar is gone; new SORT BY section in the sidebar; inline category creation in the task editor. Reservations: pending / confirmed sections remember their collapsed state per trip (localStorage). Misc: per-trip connections toggle moved into the day-plan sidebar, booking endpoints fixed to show on map for trains/cruises/cars as well, label localStorage persistence, RESMODAL test updated to the new airport-select flow. i18n: the new booking / map / todo / budget strings are translated into all 15 supported languages. --- client/src/components/Budget/BudgetPanel.tsx | 514 +++++++++++++----- client/src/components/Files/FileManager.tsx | 98 +++- .../components/Packing/PackingListPanel.tsx | 37 +- .../components/Planner/ReservationModal.tsx | 50 +- .../components/Planner/ReservationsPanel.tsx | 20 +- client/src/components/Todo/TodoListPanel.tsx | 144 +++-- client/src/i18n/translations/ar.ts | 17 +- client/src/i18n/translations/br.ts | 17 +- client/src/i18n/translations/cs.ts | 17 +- client/src/i18n/translations/de.ts | 8 +- client/src/i18n/translations/en.ts | 8 +- client/src/i18n/translations/es.ts | 17 +- client/src/i18n/translations/fr.ts | 17 +- client/src/i18n/translations/hu.ts | 17 +- client/src/i18n/translations/id.ts | 17 +- client/src/i18n/translations/it.ts | 17 +- client/src/i18n/translations/nl.ts | 17 +- client/src/i18n/translations/pl.ts | 15 +- client/src/i18n/translations/ru.ts | 17 +- client/src/i18n/translations/zh.ts | 17 +- client/src/i18n/translations/zhTw.ts | 17 +- client/src/pages/TripPlannerPage.tsx | 112 +++- 22 files changed, 894 insertions(+), 316 deletions(-) diff --git a/client/src/components/Budget/BudgetPanel.tsx b/client/src/components/Budget/BudgetPanel.tsx index a41cfb6a..0caa0570 100644 --- a/client/src/components/Budget/BudgetPanel.tsx +++ b/client/src/components/Budget/BudgetPanel.tsx @@ -4,7 +4,69 @@ import DOM from 'react-dom' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useTranslation } from '../../i18n' -import { Plus, Trash2, Calculator, Wallet, Pencil, Users, Check, Info, ChevronDown, ChevronRight, Download, GripVertical } from 'lucide-react' +import { Plus, Trash2, Calculator, Wallet, Pencil, Users, Check, Info, ChevronDown, ChevronRight, Download, GripVertical, TrendingUp, TrendingDown, PieChart as PieChartIcon } from 'lucide-react' + +function useIsDark(): boolean { + const [dark, setDark] = useState(() => typeof document !== 'undefined' && document.documentElement.classList.contains('dark')) + useEffect(() => { + if (typeof document === 'undefined') return + const mo = new MutationObserver(() => setDark(document.documentElement.classList.contains('dark'))) + mo.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }) + return () => mo.disconnect() + }, []) + return dark +} + +function widgetTheme(dark: boolean) { + if (dark) return { + bg: 'linear-gradient(180deg, #17171d 0%, #0d0d12 100%)', + border: 'rgba(255,255,255,0.07)', + text: '#ffffff', + sub: 'rgba(255,255,255,0.6)', + faint: 'rgba(255,255,255,0.4)', + track: 'rgba(255,255,255,0.04)', + divider: 'rgba(255,255,255,0.07)', + iconBg: 'rgba(255,255,255,0.08)', + iconBorder: 'rgba(255,255,255,0.12)', + iconColor: 'rgba(255,255,255,0.9)', + centerBg: '#17171d', + flowBg: 'rgba(255,255,255,0.05)', + flowBorder: 'rgba(255,255,255,0.07)', + flowHoverBg: 'rgba(255,255,255,0.08)', + flowHoverBorder: 'rgba(255,255,255,0.12)', + rowHover: 'rgba(255,255,255,0.03)', + shadow: '0 20px 50px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.04)', + donutShadow: 'drop-shadow(0 0 20px rgba(0,0,0,0.3))', + } + return { + bg: 'linear-gradient(180deg, #ffffff 0%, #f9fafb 100%)', + border: 'rgba(15,23,42,0.08)', + text: '#111827', + sub: 'rgba(17,24,39,0.6)', + faint: 'rgba(17,24,39,0.4)', + track: 'rgba(15,23,42,0.05)', + divider: 'rgba(15,23,42,0.08)', + iconBg: 'rgba(15,23,42,0.05)', + iconBorder: 'rgba(15,23,42,0.1)', + iconColor: 'rgba(17,24,39,0.75)', + centerBg: '#ffffff', + flowBg: 'rgba(15,23,42,0.03)', + flowBorder: 'rgba(15,23,42,0.08)', + flowHoverBg: 'rgba(15,23,42,0.06)', + flowHoverBorder: 'rgba(15,23,42,0.14)', + rowHover: 'rgba(15,23,42,0.04)', + shadow: '0 12px 32px rgba(15,23,42,0.08), 0 2px 6px rgba(0,0,0,0.04)', + donutShadow: 'drop-shadow(0 4px 18px rgba(15,23,42,0.12))', + } +} + +function hexLighten(hex: string, amount: number): string { + const m = hex.replace('#', '').match(/.{2}/g) + if (!m || m.length !== 3) return hex + const mix = (c: number) => Math.min(255, Math.round(c + (255 - c) * amount)) + const [r, g, b] = m.map(x => parseInt(x, 16)) + return `#${[mix(r), mix(g), mix(b)].map(v => v.toString(16).padStart(2, '0')).join('')}` +} import CustomSelect from '../shared/CustomSelect' import { budgetApi } from '../../api/client' import { CustomDatePicker } from '../shared/CustomDateTimePicker' @@ -361,9 +423,47 @@ interface PerPersonInlineProps { locale: string } -function PerPersonInline({ tripId, budgetItems, currency, locale }: PerPersonInlineProps) { - const [data, setData] = useState(null) - const fmt = (v) => fmtNum(v, locale, currency) +const SPLIT_COLORS = [ + { solid: '#6366f1', gradient: 'linear-gradient(135deg, #6366f1, #8b5cf6)' }, + { solid: '#ec4899', gradient: 'linear-gradient(135deg, #ec4899, #f43f5e)' }, + { solid: '#10b981', gradient: 'linear-gradient(135deg, #10b981, #22c55e)' }, + { solid: '#f59e0b', gradient: 'linear-gradient(135deg, #f59e0b, #f97316)' }, + { solid: '#06b6d4', gradient: 'linear-gradient(135deg, #06b6d4, #3b82f6)' }, + { solid: '#a855f7', gradient: 'linear-gradient(135deg, #a855f7, #d946ef)' }, +] + +export function splitColorFor(userId: number, order: number) { + return SPLIT_COLORS[order % SPLIT_COLORS.length] +} + +function colorForUserId(userId: number) { + return SPLIT_COLORS[((userId | 0) - 1 + SPLIT_COLORS.length * 1000) % SPLIT_COLORS.length] +} + +function RingAvatar({ userId, username, avatarUrl, size = 34, innerBg = '#17171d', textColor = '#fff' }: { userId: number; username?: string; avatarUrl?: string | null; size?: number; innerBg?: string; textColor?: string }) { + const color = colorForUserId(userId) + return ( +
+
+ {avatarUrl ? : username?.[0]?.toUpperCase()} +
+
+ ) +} + +function PerPersonInline({ tripId, budgetItems, currency, locale, grandTotal, theme }: PerPersonInlineProps & { grandTotal: number; theme: ReturnType }) { + const [data, setData] = useState(null) + const fmt = (v: number) => fmtNum(v, locale, currency) useEffect(() => { budgetApi.perPersonSummary(tripId).then(d => setData(d.summary)).catch(() => {}) @@ -371,25 +471,38 @@ function PerPersonInline({ tripId, budgetItems, currency, locale }: PerPersonInl if (!data || data.length === 0) return null + const people = data.map((p: any) => ({ ...p, color: colorForUserId(p.user_id) })) + return ( -
- {data.map(person => ( -
-
- {person.avatar_url - ? - : person.username?.[0]?.toUpperCase() - } -
- {person.username} - {fmt(person.total_assigned)} + <> + {grandTotal > 0 && ( +
+ {people.map(p => ( +
+ ))}
- ))} -
+ )} + +
+ {people.map(p => { + const percent = grandTotal > 0 ? Math.round((p.total_assigned / grandTotal) * 100) : 0 + return ( +
+ +
+
{p.username}
+
{percent}%
+
+
{fmt(p.total_assigned)}
+
+ ) + })} +
+ ) } @@ -446,6 +559,8 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro const { trip, budgetItems, addBudgetItem, updateBudgetItem, deleteBudgetItem, loadBudgetItems, updateTrip, setBudgetItemMembers, toggleBudgetMemberPaid, reorderBudgetItems, reorderBudgetCategories } = useTripStore() const can = useCanDo() const { t, locale } = useTranslation() + const isDark = useIsDark() + const theme = useMemo(() => widgetTheme(isDark), [isDark]) const [newCategoryName, setNewCategoryName] = useState('') const [editingCat, setEditingCat] = useState(null) // { name, value } const [settlement, setSettlement] = useState<{ balances: any[]; flows: any[] } | null>(null) @@ -589,20 +704,69 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro } // ── Main Layout ────────────────────────────────────────────────────────── + const totalBudget = budgetItems.reduce((s, x) => s + (x.total_price || 0), 0) return ( -
-
-
- -

{t('budget.title')}

+
+
+
+

+ {t('budget.title')} +

+
+
+ ({ value: c, label: `${c} (${SYMBOLS[c] || c})` }))} + searchable + /> +
+ {canEdit && ( +
+ setNewCategoryName(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') handleAddCategory() }} + placeholder={t('budget.categoryName')} + style={{ flex: 1, minWidth: 0, border: '1px solid var(--border-primary)', borderRadius: 10, padding: '9px 14px', fontSize: 13, outline: 'none', fontFamily: 'inherit', background: 'var(--bg-card)', color: 'var(--text-primary)' }} + /> + +
+ )} + +
-
-
+
{categoryNames.map((cat, ci) => { const items = grouped.get(cat) || [] @@ -811,61 +975,57 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro })}
-
-
- ({ value: c, label: `${c} (${SYMBOLS[c] || c})` }))} - searchable - /> -
- - {canEdit && ( -
- setNewCategoryName(e.target.value)} - onKeyDown={e => { if (e.key === 'Enter') handleAddCategory() }} - placeholder={t('budget.categoryName')} - style={{ flex: 1, border: '1px solid var(--border-primary)', borderRadius: 10, padding: '9px 14px', fontSize: 13, outline: 'none', fontFamily: 'inherit', background: 'var(--bg-input)', color: 'var(--text-primary)' }} - /> - -
- )} +
-
-
- +
+
+
-
-
{t('budget.totalBudget')}
+
+
{t('budget.totalBudget')}
-
- {Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: currencyDecimals(currency), maximumFractionDigits: currencyDecimals(currency) })} + + {(() => { + const decimals = currencyDecimals(currency) + const full = Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + const sep = (0.1).toLocaleString(locale).replace(/\d/g, '') + const [integerPart, decimalPart] = decimals > 0 ? full.split(sep) : [full, ''] + return ( +
+ {integerPart} + {decimalPart && {sep}{decimalPart}} + {SYMBOLS[currency] || currency} +
+ ) + })()} +
+ {currency}
-
{SYMBOLS[currency] || currency} {currency}
+ {hasMultipleMembers && (budgetItems || []).some(i => i.members?.length > 0) && ( - + )} {/* Settlement dropdown inside the total card */} {hasMultipleMembers && settlement && settlement.flows.length > 0 && ( -
+
{settlementOpen && ( -
+
{settlement.flows.map((flow, i) => (
- -
- - + display: 'flex', alignItems: 'center', gap: 14, + padding: '12px 14px', borderRadius: 14, + background: theme.flowBg, + border: `1px solid ${theme.flowBorder}`, + transition: 'all 0.2s', + }} + onMouseEnter={e => { e.currentTarget.style.background = theme.flowHoverBg; e.currentTarget.style.borderColor = theme.flowHoverBorder }} + onMouseLeave={e => { e.currentTarget.style.background = theme.flowBg; e.currentTarget.style.borderColor = theme.flowBorder }} + > + +
+ {fmt(flow.amount, currency)} - +
+
+
- +
))} {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).length > 0 && ( -
-
+
+
{t('budget.netBalances')}
- {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).map(b => ( -
-
- {b.avatar_url - ? - : b.username?.[0]?.toUpperCase() - } -
- - {b.username} - - 0 ? '#4ade80' : '#f87171', - }}> - {b.balance > 0 ? '+' : ''}{fmt(b.balance, currency)} - -
- ))} +
+ {settlement.balances.filter(b => Math.abs(b.balance) > 0.01).map(b => { + const positive = b.balance > 0 + const Trend = positive ? TrendingUp : TrendingDown + return ( +
+ + + {b.username} + + + + {positive ? '+' : ''}{fmt(b.balance, currency)} + +
+ ) + })} +
)}
@@ -945,36 +1112,115 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro )}
- {pieSegments.length > 0 && ( -
-
{t('budget.byCategory')}
+ {pieSegments.length > 0 && (() => { + const decimals = currencyDecimals(currency) + const total = pieSegments.reduce((s, x) => s + x.value, 0) + const totalFmt = Number(total).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + const decimalSep = (0.1).toLocaleString(locale).replace(/\d/g, '') + const [totalInt, totalDec] = decimals > 0 ? totalFmt.split(decimalSep) : [totalFmt, ''] + const R = 80 + const CIRC = 2 * Math.PI * R + let dashOffset = 0 + return ( +
+
+
+ +
+
+
{t('budget.byCategory')}
+
+
- - -
- {pieSegments.map((seg, i) => { - const pct = grandTotal > 0 ? ((seg.value / grandTotal) * 100).toFixed(1) : '0.0' - return ( -
0 ? '1px solid var(--border-secondary)' : 'none' }}> -
-
- {seg.name} -
-
- {fmt(seg.value, currency)} - {pct}% -
+
+ + + {pieSegments.map((seg, i) => { + const c2 = hexLighten(seg.color, 0.2) + return ( + + + + + ) + })} + + + {pieSegments.map((seg, i) => { + const segLen = total > 0 ? (seg.value / total) * CIRC : 0 + const circle = ( + + ) + dashOffset += segLen + return circle + })} + +
+
{t('budget.total')}
+
+ {totalInt} + {totalDec && {decimalSep}{totalDec}}
- ) - })} +
{currency}
+
+
+ +
+ {pieSegments.map((seg, i) => { + const pct = total > 0 ? (seg.value / total) * 100 : 0 + const pctLabel = pct.toFixed(1).replace('.', decimalSep) + '%' + const c2 = hexLighten(seg.color, 0.2) + const chipColor = isDark ? hexLighten(seg.color, 0.35) : seg.color + return ( +
e.currentTarget.style.background = theme.rowHover} + onMouseLeave={e => e.currentTarget.style.background = 'transparent'} + > +
+
+
{seg.name}
+
{fmt(seg.value, currency)}
+
+ {pctLabel} +
+ ) + })} +
-
- )} + ) + })()}
diff --git a/client/src/components/Files/FileManager.tsx b/client/src/components/Files/FileManager.tsx index 6092806a..a5c5b262 100644 --- a/client/src/components/Files/FileManager.tsx +++ b/client/src/components/Files/FileManager.tsx @@ -779,25 +779,81 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate, document.body )} - {/* Header */} -
-
-

{showTrash ? (t('files.trash') || 'Trash') : t('files.title')}

-

- {showTrash - ? `${trashFiles.length} ${trashFiles.length === 1 ? 'file' : 'files'}` - : (files.length === 1 ? t('files.countSingular') : t('files.count', { count: files.length }))} -

-
- +

+ {showTrash ? (t('files.trash') || 'Trash') : t('files.title')} +

+ + {!showTrash && ( + <> +
+
+ {[ + { id: 'all', label: t('files.filterAll') }, + ...(files.some(f => f.starred) ? [{ id: 'starred', icon: Star } as const] : []), + { id: 'pdf', label: t('files.filterPdf') }, + { id: 'image', label: t('files.filterImages') }, + { id: 'doc', label: t('files.filterDocs') }, + ...(files.some(f => f.note_id) ? [{ id: 'collab', label: t('files.filterCollab') || 'Collab' }] : []), + ].map(tab => { + const active = filterType === tab.id + const TabIcon = 'icon' in tab ? tab.icon : null + const count = tab.id === 'all' ? files.length + : tab.id === 'starred' ? files.filter(f => f.starred).length + : tab.id === 'pdf' ? files.filter(f => (f.mime_type || '').includes('pdf') || /\.pdf$/i.test(f.original_name)).length + : tab.id === 'image' ? files.filter(f => (f.mime_type || '').startsWith('image/')).length + : tab.id === 'doc' ? files.filter(f => /\.(docx?|xlsx?|txt|csv)$/i.test(f.original_name)).length + : tab.id === 'collab' ? files.filter(f => f.note_id).length + : 0 + return ( + + ) + })} +
+ + )} + + +
{showTrash ? ( @@ -835,7 +891,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate, {can('file_upload', trip) &&
} {/* Filter tabs */} -
+
{[ { id: 'all', label: t('files.filterAll') }, ...(files.some(f => f.starred) ? [{ id: 'starred', icon: Star }] : []), @@ -883,7 +939,7 @@ export default function FileManager({ files = [], onUpload, onDelete, onUpdate,
{/* File list */} -
+
{filteredFiles.length === 0 ? (
diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx index d5745476..75889262 100644 --- a/client/src/components/Packing/PackingListPanel.tsx +++ b/client/src/components/Packing/PackingListPanel.tsx @@ -729,9 +729,10 @@ function MenuItem({ icon, label, onClick, danger }: MenuItemProps) { interface PackingListPanelProps { tripId: number items: PackingItem[] + openImportSignal?: number } -export default function PackingListPanel({ tripId, items }: PackingListPanelProps) { +export default function PackingListPanel({ tripId, items, openImportSignal = 0 }: PackingListPanelProps) { const [filter, setFilter] = useState('alle') // 'alle' | 'offen' | 'erledigt' const [addingCategory, setAddingCategory] = useState(false) const [newCatName, setNewCatName] = useState('') @@ -896,6 +897,14 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp const [saveTemplateName, setSaveTemplateName] = useState('') const [showImportModal, setShowImportModal] = useState(false) const [importText, setImportText] = useState('') + const lastHandledImportSignal = useRef(openImportSignal) + + useEffect(() => { + if (openImportSignal !== lastHandledImportSignal.current && openImportSignal > 0) { + setShowImportModal(true) + } + lastHandledImportSignal.current = openImportSignal + }, [openImportSignal]) const csvInputRef = useRef(null) const templateDropdownRef = useRef(null) @@ -999,14 +1008,13 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp
{/* ── Header ── */} -
-
-
-

{t('packing.title')}

-

- {items.length === 0 ? t('packing.empty') : t('packing.progress', { packed: abgehakt, total: items.length, percent: fortschritt })} +

+
+ {items.length > 0 ? ( +

+ {t('packing.progress', { packed: abgehakt, total: items.length, percent: fortschritt })}

-
+ ) : }
{canEdit && abgehakt > 0 && ( )} - {canEdit && ( - - )} {canEdit && availableTemplates.length > 0 && (
{/* Categories */} @@ -251,27 +264,6 @@ export default function TodoListPanel({ tripId, items }: { tripId: number; items
- {/* Add task */} - {canEdit && ( -
- -
- )} - {/* Task list */}
{filtered.length === 0 ? null : ( @@ -407,19 +399,28 @@ export default function TodoListPanel({ tripId, items }: { tripId: number; items
)} - {isAddingNew && !selectedItem && !isMobile && ( - { setIsAddingNew(false); setSelectedId(id) }} - onClose={() => setIsAddingNew(false)} - /> - )} - {isAddingNew && !selectedItem && isMobile && ( + {isAddingNew && !selectedItem && !isMobile && ReactDOM.createPortal(
{ if (e.target === e.currentTarget) setIsAddingNew(false) }} - style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end' }}> + className="modal-backdrop" + style={{ position: 'fixed', inset: 0, zIndex: 300, background: 'rgba(15,23,42,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 'calc(var(--nav-h) + 60px)', paddingBottom: 40 }}> +
{ if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px' } } }}> + { setIsAddingNew(false); setSelectedId(id) }} + onClose={() => setIsAddingNew(false)} + /> +
+
, + document.body + )} + {isAddingNew && !selectedItem && isMobile && ReactDOM.createPortal( +
{ if (e.target === e.currentTarget) setIsAddingNew(false) }} + className="modal-backdrop" + style={{ position: 'fixed', inset: 0, zIndex: 300, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end' }}>
{ if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px 16px 0 0' } } }}> setIsAddingNew(false)} />
-
+
, + document.body )}
) @@ -647,6 +649,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated, const [desc, setDesc] = useState('') const [dueDate, setDueDate] = useState('') const [category, setCategory] = useState(defaultCategory || '') + const [addingCategory, setAddingCategoryInline] = useState(false) const [assignedUserId, setAssignedUserId] = useState(null) const [priority, setPriority] = useState(0) const [saving, setSaving] = useState(false) @@ -657,9 +660,10 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated, if (!name.trim()) return setSaving(true) try { + const trimmedCategory = category.trim() const item = await addTodoItem(tripId, { name: name.trim(), description: desc || null, priority, - due_date: dueDate || null, category: category || null, + due_date: dueDate || null, category: trimmedCategory || null, assigned_user_id: assignedUserId, } as any) if (item?.id) onCreated(item.id) @@ -696,19 +700,49 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated,
- setCategory(v)} - options={[ - { value: '', label: t('todo.noCategory') }, - ...categories.map(c => ({ - value: c, label: c, - icon: , - })), - ]} - placeholder={t('todo.noCategory')} - size="sm" - /> + {addingCategory ? ( +
+ setCategory(e.target.value)} + onKeyDown={e => { if (e.key === 'Enter') setAddingCategoryInline(false); if (e.key === 'Escape') { setCategory(''); setAddingCategoryInline(false) } }} + placeholder={t('todo.newCategory')} + style={{ flex: 1, fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', outline: 'none' }} + /> + +
+ ) : ( +
+
+ setCategory(v)} + options={[ + { value: '', label: t('todo.noCategory') }, + ...categories.map(c => ({ + value: c, label: c, + icon: , + })), + ...(category && !categories.includes(category) ? [{ + value: category, label: `${category} (${t('todo.newCategoryLabel') || 'new'})`, + icon: , + }] : []), + ]} + placeholder={t('todo.noCategory')} + size="sm" + /> +
+ +
+ )}
diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 78a252d4..4fee95ba 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -1017,6 +1017,15 @@ const ar: Record = { 'reservations.meta.flightNumber': 'رقم الرحلة', 'reservations.meta.from': 'من', 'reservations.meta.to': 'إلى', + 'reservations.needsReview': 'مراجعة', + 'reservations.needsReviewHint': 'تعذّر مطابقة المطار تلقائياً — يرجى تأكيد الموقع.', + 'reservations.searchLocation': 'ابحث عن محطة، ميناء، عنوان...', + 'airport.searchPlaceholder': 'رمز المطار أو المدينة (مثل FRA)', + 'map.connections': 'الاتصالات', + 'map.showConnections': 'عرض مسارات الحجوزات', + 'map.hideConnections': 'إخفاء مسارات الحجوزات', + 'settings.bookingLabels': 'تسميات مسارات الحجوزات', + 'settings.bookingLabelsHint': 'عرض أسماء المحطات/المطارات على الخريطة. عند الإيقاف، يتم عرض الرمز فقط.', 'reservations.meta.trainNumber': 'رقم القطار', 'reservations.meta.platform': 'المنصة', 'reservations.meta.seat': 'المقعد', @@ -1035,7 +1044,7 @@ const ar: Record = { 'reservations.type.hotel': 'إقامة', 'reservations.type.restaurant': 'مطعم', 'reservations.type.train': 'قطار', - 'reservations.type.car': 'سيارة مستأجرة', + 'reservations.type.car': 'سيارة', 'reservations.type.cruise': 'رحلة بحرية', 'reservations.type.event': 'فعالية', 'reservations.type.tour': 'جولة', @@ -1799,7 +1808,11 @@ const ar: Record = { 'todo.unassigned': 'غير مُسنَد', 'todo.noCategory': 'بدون فئة', 'todo.hasDescription': 'له وصف', - 'todo.addItem': 'إضافة مهمة جديدة...', + 'todo.addItem': 'إضافة مهمة جديدة', + 'todo.sidebar.sortBy': 'ترتيب حسب', + 'todo.priority': 'الأولوية', + 'todo.newCategoryLabel': 'جديد', + 'budget.categoriesLabel': 'فئات', 'todo.newCategory': 'اسم الفئة', 'todo.addCategory': 'إضافة فئة', 'todo.newItem': 'مهمة جديدة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index c332301e..bb79b211 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -986,6 +986,15 @@ const br: Record = { 'reservations.meta.flightNumber': 'Nº do voo', 'reservations.meta.from': 'De', 'reservations.meta.to': 'Para', + 'reservations.needsReview': 'Verificar', + 'reservations.needsReviewHint': 'Aeroporto não pôde ser identificado automaticamente — confirme o local.', + 'reservations.searchLocation': 'Buscar estação, porto, endereço...', + 'airport.searchPlaceholder': 'Código ou cidade do aeroporto (ex. FRA)', + 'map.connections': 'Conexões', + 'map.showConnections': 'Mostrar rotas de reservas', + 'map.hideConnections': 'Ocultar rotas de reservas', + 'settings.bookingLabels': 'Rótulos das rotas de reservas', + 'settings.bookingLabelsHint': 'Mostra nomes de estações / aeroportos no mapa. Desativado, apenas o ícone aparece.', 'reservations.meta.trainNumber': 'Nº do trem', 'reservations.meta.platform': 'Plataforma', 'reservations.meta.seat': 'Assento', @@ -1004,7 +1013,7 @@ const br: Record = { 'reservations.type.hotel': 'Hospedagem', 'reservations.type.restaurant': 'Restaurante', 'reservations.type.train': 'Trem', - 'reservations.type.car': 'Carro alugado', + 'reservations.type.car': 'Carro', 'reservations.type.cruise': 'Cruzeiro', 'reservations.type.event': 'Evento', 'reservations.type.tour': 'Passeio', @@ -1748,7 +1757,11 @@ const br: Record = { 'todo.unassigned': 'Não atribuído', 'todo.noCategory': 'Sem categoria', 'todo.hasDescription': 'Com descrição', - 'todo.addItem': 'Adicionar nova tarefa...', + 'todo.addItem': 'Nova tarefa', + 'todo.sidebar.sortBy': 'Ordenar por', + 'todo.priority': 'Prioridade', + 'todo.newCategoryLabel': 'nova', + 'budget.categoriesLabel': 'categorias', 'todo.newCategory': 'Nome da categoria', 'todo.addCategory': 'Adicionar categoria', 'todo.newItem': 'Nova tarefa', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 9bda2153..101d1ae7 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -1015,6 +1015,15 @@ const cs: Record = { 'reservations.meta.flightNumber': 'Číslo letu', 'reservations.meta.from': 'Z', 'reservations.meta.to': 'Do', + 'reservations.needsReview': 'Zkontrolovat', + 'reservations.needsReviewHint': 'Letiště nebylo možné automaticky rozpoznat — potvrďte prosím místo.', + 'reservations.searchLocation': 'Hledat stanici, přístav, adresu...', + 'airport.searchPlaceholder': 'Kód letiště nebo město (např. FRA)', + 'map.connections': 'Spojení', + 'map.showConnections': 'Zobrazit trasy rezervací', + 'map.hideConnections': 'Skrýt trasy rezervací', + 'settings.bookingLabels': 'Popisky tras rezervací', + 'settings.bookingLabelsHint': 'Zobrazuje názvy stanic / letišť na mapě. Pokud je vypnuto, zobrazí se pouze ikona.', 'reservations.meta.trainNumber': 'Číslo vlaku', 'reservations.meta.platform': 'Nástupiště', 'reservations.meta.seat': 'Sedadlo', @@ -1033,7 +1042,7 @@ const cs: Record = { 'reservations.type.hotel': 'Ubytování', 'reservations.type.restaurant': 'Restaurace', 'reservations.type.train': 'Vlak', - 'reservations.type.car': 'Pronájem auta', + 'reservations.type.car': 'Auto', 'reservations.type.cruise': 'Plavba', 'reservations.type.event': 'Událost', 'reservations.type.tour': 'Prohlídka', @@ -1753,7 +1762,11 @@ const cs: Record = { 'todo.unassigned': 'Nepřiřazeno', 'todo.noCategory': 'Bez kategorie', 'todo.hasDescription': 'Má popis', - 'todo.addItem': 'Přidat nový úkol...', + 'todo.addItem': 'Přidat nový úkol', + 'todo.sidebar.sortBy': 'Řadit podle', + 'todo.priority': 'Priorita', + 'todo.newCategoryLabel': 'nová', + 'budget.categoriesLabel': 'kategorie', 'todo.newCategory': 'Název kategorie', 'todo.addCategory': 'Přidat kategorii', 'todo.newItem': 'Nový úkol', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index 17848b88..8746358e 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -1044,7 +1044,7 @@ const de: Record = { 'reservations.type.hotel': 'Unterkunft', 'reservations.type.restaurant': 'Restaurant', 'reservations.type.train': 'Zug', - 'reservations.type.car': 'Mietwagen', + 'reservations.type.car': 'Auto', 'reservations.type.cruise': 'Kreuzfahrt', 'reservations.type.event': 'Veranstaltung', 'reservations.type.tour': 'Tour', @@ -1765,7 +1765,11 @@ const de: Record = { 'todo.unassigned': 'Nicht zugewiesen', 'todo.noCategory': 'Keine Kategorie', 'todo.hasDescription': 'Hat Beschreibung', - 'todo.addItem': 'Neue Aufgabe hinzufügen...', + 'todo.addItem': 'Neue Aufgabe hinzufügen', + 'todo.sidebar.sortBy': 'Sortieren nach', + 'todo.priority': 'Priorität', + 'todo.newCategoryLabel': 'neu', + 'budget.categoriesLabel': 'Kategorien', 'todo.newCategory': 'Kategoriename', 'todo.addCategory': 'Kategorie hinzufügen', 'todo.newItem': 'Neue Aufgabe', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 2ca59d37..e0fe0459 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -1097,7 +1097,7 @@ const en: Record = { 'reservations.type.hotel': 'Accommodation', 'reservations.type.restaurant': 'Restaurant', 'reservations.type.train': 'Train', - 'reservations.type.car': 'Rental Car', + 'reservations.type.car': 'Car', 'reservations.type.cruise': 'Cruise', 'reservations.type.event': 'Event', 'reservations.type.tour': 'Tour', @@ -1827,7 +1827,11 @@ const en: Record = { 'todo.unassigned': 'Unassigned', 'todo.noCategory': 'No category', 'todo.hasDescription': 'Has description', - 'todo.addItem': 'Add new task...', + 'todo.addItem': 'Add new task', + 'todo.sidebar.sortBy': 'Sort by', + 'todo.priority': 'Priority', + 'todo.newCategoryLabel': 'new', + 'budget.categoriesLabel': 'categories', 'todo.newCategory': 'Category name', 'todo.addCategory': 'Add category', 'todo.newItem': 'New task', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index 2de8f1c7..f776a1ed 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -990,7 +990,7 @@ const es: Record = { 'reservations.type.hotel': 'Alojamiento', 'reservations.type.restaurant': 'Restaurante', 'reservations.type.train': 'Tren', - 'reservations.type.car': 'Coche de alquiler', + 'reservations.type.car': 'Coche', 'reservations.type.cruise': 'Crucero', 'reservations.type.event': 'Evento', 'reservations.type.tour': 'Excursión', @@ -1618,6 +1618,15 @@ const es: Record = { 'reservations.meta.flightNumber': 'N° de vuelo', 'reservations.meta.from': 'Desde', 'reservations.meta.to': 'Hasta', + 'reservations.needsReview': 'Revisar', + 'reservations.needsReviewHint': 'No se pudo identificar el aeropuerto automáticamente — por favor confirma la ubicación.', + 'reservations.searchLocation': 'Buscar estación, puerto, dirección...', + 'airport.searchPlaceholder': 'Código o ciudad del aeropuerto (ej. FRA)', + 'map.connections': 'Conexiones', + 'map.showConnections': 'Mostrar rutas de reservas', + 'map.hideConnections': 'Ocultar rutas de reservas', + 'settings.bookingLabels': 'Etiquetas de rutas de reservas', + 'settings.bookingLabelsHint': 'Muestra nombres de estaciones / aeropuertos en el mapa. Desactivado, solo se muestra el icono.', 'reservations.meta.trainNumber': 'N° de tren', 'reservations.meta.platform': 'Andén', 'reservations.meta.seat': 'Asiento', @@ -1758,7 +1767,11 @@ const es: Record = { 'todo.unassigned': 'Sin asignar', 'todo.noCategory': 'Sin categoría', 'todo.hasDescription': 'Con descripción', - 'todo.addItem': 'Añadir nueva tarea...', + 'todo.addItem': 'Nueva tarea', + 'todo.sidebar.sortBy': 'Ordenar por', + 'todo.priority': 'Prioridad', + 'todo.newCategoryLabel': 'nueva', + 'budget.categoriesLabel': 'categorías', 'todo.newCategory': 'Nombre de la categoría', 'todo.addCategory': 'Añadir categoría', 'todo.newItem': 'Nueva tarea', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index e9de6556..e7b43bc3 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -1013,6 +1013,15 @@ const fr: Record = { 'reservations.meta.flightNumber': 'N° de vol', 'reservations.meta.from': 'De', 'reservations.meta.to': 'À', + 'reservations.needsReview': 'Vérifier', + 'reservations.needsReviewHint': 'L\'aéroport n\'a pas pu être identifié automatiquement — veuillez confirmer l\'emplacement.', + 'reservations.searchLocation': 'Rechercher une gare, un port, une adresse…', + 'airport.searchPlaceholder': 'Code ou ville de l\'aéroport (ex. FRA)', + 'map.connections': 'Connexions', + 'map.showConnections': 'Afficher les itinéraires', + 'map.hideConnections': 'Masquer les itinéraires', + 'settings.bookingLabels': 'Étiquettes des itinéraires', + 'settings.bookingLabelsHint': 'Affiche les noms des gares / aéroports sur la carte. Si désactivé, seule l\'icône est affichée.', 'reservations.meta.trainNumber': 'N° de train', 'reservations.meta.platform': 'Quai', 'reservations.meta.seat': 'Place', @@ -1031,7 +1040,7 @@ const fr: Record = { 'reservations.type.hotel': 'Hébergement', 'reservations.type.restaurant': 'Restaurant', 'reservations.type.train': 'Train', - 'reservations.type.car': 'Voiture de location', + 'reservations.type.car': 'Voiture', 'reservations.type.cruise': 'Croisière', 'reservations.type.event': 'Événement', 'reservations.type.tour': 'Visite', @@ -1752,7 +1761,11 @@ const fr: Record = { 'todo.unassigned': 'Non assigné', 'todo.noCategory': 'Aucune catégorie', 'todo.hasDescription': 'Avec description', - 'todo.addItem': 'Ajouter une tâche...', + 'todo.addItem': 'Nouvelle tâche', + 'todo.sidebar.sortBy': 'Trier par', + 'todo.priority': 'Priorité', + 'todo.newCategoryLabel': 'nouvelle', + 'budget.categoriesLabel': 'catégories', 'todo.newCategory': 'Nom de la catégorie', 'todo.addCategory': 'Ajouter une catégorie', 'todo.newItem': 'Nouvelle tâche', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index f02ed627..a69b39ca 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -1015,6 +1015,15 @@ const hu: Record = { 'reservations.meta.flightNumber': 'Járatszám', 'reservations.meta.from': 'Honnan', 'reservations.meta.to': 'Hová', + 'reservations.needsReview': 'Ellenőrzés', + 'reservations.needsReviewHint': 'A repülőteret nem sikerült automatikusan azonosítani — erősítsd meg a helyet.', + 'reservations.searchLocation': 'Állomás, kikötő, cím keresése...', + 'airport.searchPlaceholder': 'Repülőtér kódja vagy város (pl. FRA)', + 'map.connections': 'Kapcsolatok', + 'map.showConnections': 'Foglalási útvonalak megjelenítése', + 'map.hideConnections': 'Foglalási útvonalak elrejtése', + 'settings.bookingLabels': 'Útvonal-címkék a foglalásokhoz', + 'settings.bookingLabelsHint': 'Állomás- / repülőtér-nevek megjelenítése a térképen. Ha ki van kapcsolva, csak az ikon látszik.', 'reservations.meta.trainNumber': 'Vonatszám', 'reservations.meta.platform': 'Vágány', 'reservations.meta.seat': 'Ülés', @@ -1033,7 +1042,7 @@ const hu: Record = { 'reservations.type.hotel': 'Szálloda', 'reservations.type.restaurant': 'Étterem', 'reservations.type.train': 'Vonat', - 'reservations.type.car': 'Autóbérlés', + 'reservations.type.car': 'Autó', 'reservations.type.cruise': 'Hajóút', 'reservations.type.event': 'Esemény', 'reservations.type.tour': 'Túra', @@ -1750,7 +1759,11 @@ const hu: Record = { 'todo.unassigned': 'Nem hozzárendelt', 'todo.noCategory': 'Nincs kategória', 'todo.hasDescription': 'Van leírás', - 'todo.addItem': 'Új feladat hozzáadása...', + 'todo.addItem': 'Új feladat', + 'todo.sidebar.sortBy': 'Rendezés', + 'todo.priority': 'Prioritás', + 'todo.newCategoryLabel': 'új', + 'budget.categoriesLabel': 'kategóriák', 'todo.newCategory': 'Kategória neve', 'todo.addCategory': 'Kategória hozzáadása', 'todo.newItem': 'Új feladat', diff --git a/client/src/i18n/translations/id.ts b/client/src/i18n/translations/id.ts index d00b12d5..34d2a2ca 100644 --- a/client/src/i18n/translations/id.ts +++ b/client/src/i18n/translations/id.ts @@ -1070,6 +1070,15 @@ const id: Record = { 'reservations.meta.flightNumber': 'No. Penerbangan', 'reservations.meta.from': 'Dari', 'reservations.meta.to': 'Ke', + 'reservations.needsReview': 'Tinjau', + 'reservations.needsReviewHint': 'Bandara tidak dapat dicocokkan otomatis — konfirmasi lokasi.', + 'reservations.searchLocation': 'Cari stasiun, pelabuhan, alamat...', + 'airport.searchPlaceholder': 'Kode bandara atau kota (mis. FRA)', + 'map.connections': 'Koneksi', + 'map.showConnections': 'Tampilkan rute pemesanan', + 'map.hideConnections': 'Sembunyikan rute pemesanan', + 'settings.bookingLabels': 'Label rute pemesanan', + 'settings.bookingLabelsHint': 'Menampilkan nama stasiun / bandara di peta. Jika mati, hanya ikon ditampilkan.', 'reservations.meta.trainNumber': 'No. Kereta', 'reservations.meta.platform': 'Peron', 'reservations.meta.seat': 'Kursi', @@ -1088,7 +1097,7 @@ const id: Record = { 'reservations.type.hotel': 'Akomodasi', 'reservations.type.restaurant': 'Restoran', 'reservations.type.train': 'Kereta', - 'reservations.type.car': 'Mobil Sewa', + 'reservations.type.car': 'Mobil', 'reservations.type.cruise': 'Kapal Pesiar', 'reservations.type.event': 'Acara', 'reservations.type.tour': 'Tur', @@ -1818,7 +1827,11 @@ const id: Record = { 'todo.unassigned': 'Belum ditugaskan', 'todo.noCategory': 'Tanpa kategori', 'todo.hasDescription': 'Ada deskripsi', - 'todo.addItem': 'Tambah tugas baru...', + 'todo.addItem': 'Tugas baru', + 'todo.sidebar.sortBy': 'Urutkan', + 'todo.priority': 'Prioritas', + 'todo.newCategoryLabel': 'baru', + 'budget.categoriesLabel': 'kategori', 'todo.newCategory': 'Nama kategori', 'todo.addCategory': 'Tambah kategori', 'todo.newItem': 'Tugas baru', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index 5c882695..8ae966fa 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -1014,6 +1014,15 @@ const it: Record = { 'reservations.meta.flightNumber': 'N. volo', 'reservations.meta.from': 'Da', 'reservations.meta.to': 'A', + 'reservations.needsReview': 'Verifica', + 'reservations.needsReviewHint': 'L\'aeroporto non è stato riconosciuto automaticamente — conferma la posizione.', + 'reservations.searchLocation': 'Cerca stazione, porto, indirizzo...', + 'airport.searchPlaceholder': 'Codice o città dell\'aeroporto (es. FRA)', + 'map.connections': 'Connessioni', + 'map.showConnections': 'Mostra percorsi prenotati', + 'map.hideConnections': 'Nascondi percorsi prenotati', + 'settings.bookingLabels': 'Etichette percorsi prenotati', + 'settings.bookingLabelsHint': 'Mostra i nomi di stazioni / aeroporti sulla mappa. Se disattivato, viene mostrata solo l\'icona.', 'reservations.meta.trainNumber': 'N. treno', 'reservations.meta.platform': 'Binario', 'reservations.meta.seat': 'Posto', @@ -1032,7 +1041,7 @@ const it: Record = { 'reservations.type.hotel': 'Alloggio', 'reservations.type.restaurant': 'Ristorante', 'reservations.type.train': 'Treno', - 'reservations.type.car': 'Auto a noleggio', + 'reservations.type.car': 'Auto', 'reservations.type.cruise': 'Crociera', 'reservations.type.event': 'Evento', 'reservations.type.tour': 'Tour', @@ -1753,7 +1762,11 @@ const it: Record = { 'todo.unassigned': 'Non assegnato', 'todo.noCategory': 'Nessuna categoria', 'todo.hasDescription': 'Ha descrizione', - 'todo.addItem': 'Aggiungi nuova attività...', + 'todo.addItem': 'Nuova attività', + 'todo.sidebar.sortBy': 'Ordina per', + 'todo.priority': 'Priorità', + 'todo.newCategoryLabel': 'nuova', + 'budget.categoriesLabel': 'categorie', 'todo.newCategory': 'Nome categoria', 'todo.addCategory': 'Aggiungi categoria', 'todo.newItem': 'Nuova attività', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 82620e2d..61c63d9e 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -1013,6 +1013,15 @@ const nl: Record = { 'reservations.meta.flightNumber': 'Vluchtnr.', 'reservations.meta.from': 'Van', 'reservations.meta.to': 'Naar', + 'reservations.needsReview': 'Controleren', + 'reservations.needsReviewHint': 'Luchthaven kon niet automatisch worden herkend — bevestig de locatie.', + 'reservations.searchLocation': 'Station, haven, adres zoeken...', + 'airport.searchPlaceholder': 'Luchthavencode of stad (bijv. FRA)', + 'map.connections': 'Verbindingen', + 'map.showConnections': 'Boekingsroutes tonen', + 'map.hideConnections': 'Boekingsroutes verbergen', + 'settings.bookingLabels': 'Routelabels voor boekingen', + 'settings.bookingLabelsHint': 'Toon station- / luchthavennamen op de kaart. Indien uit, alleen het icoon.', 'reservations.meta.trainNumber': 'Treinnr.', 'reservations.meta.platform': 'Perron', 'reservations.meta.seat': 'Stoel', @@ -1031,7 +1040,7 @@ const nl: Record = { 'reservations.type.hotel': 'Accommodatie', 'reservations.type.restaurant': 'Restaurant', 'reservations.type.train': 'Trein', - 'reservations.type.car': 'Huurauto', + 'reservations.type.car': 'Auto', 'reservations.type.cruise': 'Cruise', 'reservations.type.event': 'Evenement', 'reservations.type.tour': 'Rondleiding', @@ -1752,7 +1761,11 @@ const nl: Record = { 'todo.unassigned': 'Niet toegewezen', 'todo.noCategory': 'Geen categorie', 'todo.hasDescription': 'Heeft beschrijving', - 'todo.addItem': 'Nieuwe taak toevoegen...', + 'todo.addItem': 'Nieuwe taak', + 'todo.sidebar.sortBy': 'Sorteren op', + 'todo.priority': 'Prioriteit', + 'todo.newCategoryLabel': 'nieuw', + 'budget.categoriesLabel': 'categorieën', 'todo.newCategory': 'Categorienaam', 'todo.addCategory': 'Categorie toevoegen', 'todo.newItem': 'Nieuwe taak', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index ce254be0..0d6838b9 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -989,6 +989,15 @@ const pl: Record = { 'reservations.type.restaurant': 'Restauracja', 'reservations.type.train': 'Pociąg', 'reservations.type.car': 'Samochód', + 'reservations.needsReview': 'Sprawdź', + 'reservations.needsReviewHint': 'Nie udało się automatycznie dopasować lotniska — potwierdź lokalizację.', + 'reservations.searchLocation': 'Szukaj stacji, portu, adresu...', + 'airport.searchPlaceholder': 'Kod lotniska lub miasto (np. FRA)', + 'map.connections': 'Połączenia', + 'map.showConnections': 'Pokaż trasy rezerwacji', + 'map.hideConnections': 'Ukryj trasy rezerwacji', + 'settings.bookingLabels': 'Etykiety tras rezerwacji', + 'settings.bookingLabelsHint': 'Pokazuje nazwy stacji / lotnisk na mapie. Gdy wyłączone, wyświetlana jest tylko ikona.', 'reservations.type.cruise': 'Rejs', 'reservations.type.event': 'Wydarzenie', 'reservations.type.tour': 'Wycieczka', @@ -1801,7 +1810,11 @@ const pl: Record = { 'todo.unassigned': 'Nieprzypisane', 'todo.noCategory': 'Brak kategorii', 'todo.hasDescription': 'Ma opis', - 'todo.addItem': 'Dodaj nowe zadanie...', + 'todo.addItem': 'Nowe zadanie', + 'todo.sidebar.sortBy': 'Sortuj wg', + 'todo.priority': 'Priorytet', + 'todo.newCategoryLabel': 'nowa', + 'budget.categoriesLabel': 'kategorie', 'todo.newCategory': 'Nazwa kategorii', 'todo.addCategory': 'Dodaj kategorię', 'todo.newItem': 'Nowe zadanie', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 88668155..def9742b 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -1013,6 +1013,15 @@ const ru: Record = { 'reservations.meta.flightNumber': 'Номер рейса', 'reservations.meta.from': 'Откуда', 'reservations.meta.to': 'Куда', + 'reservations.needsReview': 'Проверить', + 'reservations.needsReviewHint': 'Аэропорт не удалось определить автоматически — подтвердите местоположение.', + 'reservations.searchLocation': 'Искать станцию, порт, адрес...', + 'airport.searchPlaceholder': 'Код аэропорта или город (напр. FRA)', + 'map.connections': 'Соединения', + 'map.showConnections': 'Показать маршруты бронирований', + 'map.hideConnections': 'Скрыть маршруты бронирований', + 'settings.bookingLabels': 'Подписи маршрутов бронирований', + 'settings.bookingLabelsHint': 'Отображает названия станций / аэропортов на карте. Если выключено, показывается только значок.', 'reservations.meta.trainNumber': 'Номер поезда', 'reservations.meta.platform': 'Платформа', 'reservations.meta.seat': 'Место', @@ -1031,7 +1040,7 @@ const ru: Record = { 'reservations.type.hotel': 'Жильё', 'reservations.type.restaurant': 'Ресторан', 'reservations.type.train': 'Поезд', - 'reservations.type.car': 'Аренда авто', + 'reservations.type.car': 'Автомобиль', 'reservations.type.cruise': 'Круиз', 'reservations.type.event': 'Мероприятие', 'reservations.type.tour': 'Экскурсия', @@ -1749,7 +1758,11 @@ const ru: Record = { 'todo.unassigned': 'Не назначено', 'todo.noCategory': 'Без категории', 'todo.hasDescription': 'Есть описание', - 'todo.addItem': 'Добавить новую задачу...', + 'todo.addItem': 'Новая задача', + 'todo.sidebar.sortBy': 'Сортировать по', + 'todo.priority': 'Приоритет', + 'todo.newCategoryLabel': 'новая', + 'budget.categoriesLabel': 'категорий', 'todo.newCategory': 'Название категории', 'todo.addCategory': 'Добавить категорию', 'todo.newItem': 'Новая задача', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index d2dae779..d7d293c7 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -1013,6 +1013,15 @@ const zh: Record = { 'reservations.meta.flightNumber': '航班号', 'reservations.meta.from': '出发', 'reservations.meta.to': '到达', + 'reservations.needsReview': '待确认', + 'reservations.needsReviewHint': '无法自动匹配机场 — 请确认位置。', + 'reservations.searchLocation': '搜索车站、港口、地址...', + 'airport.searchPlaceholder': '机场代码或城市(如 FRA)', + 'map.connections': '连接', + 'map.showConnections': '显示预订路线', + 'map.hideConnections': '隐藏预订路线', + 'settings.bookingLabels': '预订路线标签', + 'settings.bookingLabelsHint': '在地图上显示车站 / 机场名称。关闭时仅显示图标。', 'reservations.meta.trainNumber': '车次', 'reservations.meta.platform': '站台', 'reservations.meta.seat': '座位', @@ -1031,7 +1040,7 @@ const zh: Record = { 'reservations.type.hotel': '住宿', 'reservations.type.restaurant': '餐厅', 'reservations.type.train': '火车', - 'reservations.type.car': '租车', + 'reservations.type.car': '汽车', 'reservations.type.cruise': '邮轮', 'reservations.type.event': '活动', 'reservations.type.tour': '旅游团', @@ -1749,7 +1758,11 @@ const zh: Record = { 'todo.unassigned': '未分配', 'todo.noCategory': '无分类', 'todo.hasDescription': '有描述', - 'todo.addItem': '添加新任务...', + 'todo.addItem': '新建任务', + 'todo.sidebar.sortBy': '排序方式', + 'todo.priority': '优先级', + 'todo.newCategoryLabel': '新建', + 'budget.categoriesLabel': '类别', 'todo.newCategory': '分类名称', 'todo.addCategory': '添加分类', 'todo.newItem': '新任务', diff --git a/client/src/i18n/translations/zhTw.ts b/client/src/i18n/translations/zhTw.ts index e656e804..dbbc7d3c 100644 --- a/client/src/i18n/translations/zhTw.ts +++ b/client/src/i18n/translations/zhTw.ts @@ -1069,6 +1069,15 @@ const zhTw: Record = { 'reservations.meta.flightNumber': '航班號', 'reservations.meta.from': '出發', 'reservations.meta.to': '到達', + 'reservations.needsReview': '待確認', + 'reservations.needsReviewHint': '無法自動匹配機場 — 請確認位置。', + 'reservations.searchLocation': '搜尋車站、港口、地址...', + 'airport.searchPlaceholder': '機場代碼或城市(例如 FRA)', + 'map.connections': '連接', + 'map.showConnections': '顯示預訂路線', + 'map.hideConnections': '隱藏預訂路線', + 'settings.bookingLabels': '預訂路線標籤', + 'settings.bookingLabelsHint': '在地圖上顯示車站 / 機場名稱。關閉時僅顯示圖示。', 'reservations.meta.trainNumber': '車次', 'reservations.meta.platform': '站臺', 'reservations.meta.seat': '座位', @@ -1087,7 +1096,7 @@ const zhTw: Record = { 'reservations.type.hotel': '住宿', 'reservations.type.restaurant': '餐廳', 'reservations.type.train': '火車', - 'reservations.type.car': '租車', + 'reservations.type.car': '汽車', 'reservations.type.cruise': '郵輪', 'reservations.type.event': '活動', 'reservations.type.tour': '旅遊團', @@ -1766,7 +1775,11 @@ const zhTw: Record = { 'todo.unassigned': '未指派', 'todo.noCategory': '無分類', 'todo.hasDescription': '有說明', - 'todo.addItem': '新增任務...', + 'todo.addItem': '新增任務', + 'todo.sidebar.sortBy': '排序方式', + 'todo.priority': '優先順序', + 'todo.newCategoryLabel': '新增', + 'budget.categoriesLabel': '類別', 'todo.newCategory': '分類名稱', 'todo.addCategory': '新增分類', 'todo.newItem': '新任務', diff --git a/client/src/pages/TripPlannerPage.tsx b/client/src/pages/TripPlannerPage.tsx index 3ab627e6..098c3863 100644 --- a/client/src/pages/TripPlannerPage.tsx +++ b/client/src/pages/TripPlannerPage.tsx @@ -35,36 +35,102 @@ import { useRouteCalculation } from '../hooks/useRouteCalculation' import { usePlaceSelection } from '../hooks/usePlaceSelection' import { usePlannerHistory } from '../hooks/usePlannerHistory' import type { Accommodation, TripMember, Day, Place, Reservation, PackingItem, TodoItem } from '../types' -import { ListTodo } from 'lucide-react' +import { ListTodo, Upload, Plus } from 'lucide-react' function ListsContainer({ tripId, packingItems, todoItems }: { tripId: number; packingItems: PackingItem[]; todoItems: TodoItem[] }) { const [subTab, setSubTab] = useState<'packing' | 'todo'>(() => { return (sessionStorage.getItem(`trip-lists-subtab-${tripId}`) as 'packing' | 'todo') || 'packing' }) const setSubTabPersist = (tab: 'packing' | 'todo') => { setSubTab(tab); sessionStorage.setItem(`trip-lists-subtab-${tripId}`, tab) } + const [importPackingSignal, setImportPackingSignal] = useState(0) + const [addTodoSignal, setAddTodoSignal] = useState(0) const { t } = useTranslation() + + const tabs = [ + { id: 'packing' as const, label: t('todo.subtab.packing'), icon: PackageCheck, count: packingItems.length }, + { id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo, count: todoItems.length }, + ] + return (
-
- {([ - { id: 'packing' as const, label: t('todo.subtab.packing'), icon: PackageCheck }, - { id: 'todo' as const, label: t('todo.subtab.todo'), icon: ListTodo }, - ]).map(tab => ( - - ))} +
+
+

+ {t('trip.tabs.lists')} +

+
+
+ {tabs.map(tab => { + const active = subTab === tab.id + const Icon = tab.icon + return ( + + ) + })} +
+ + {subTab === 'packing' && ( + + )} + {subTab === 'todo' && ( + + )} +
+
+
+ {subTab === 'packing' && } + {subTab === 'todo' && }
- {subTab === 'packing' && } - {subTab === 'todo' && }
) } @@ -940,7 +1006,7 @@ export default function TripPlannerPage(): React.ReactElement | null { )} {activeTab === 'buchungen' && ( -
+
+
)} {activeTab === 'finanzplan' && ( -
+
)}