From e6c4c22a1d9beab4f288ac60070a283dc70bb361 Mon Sep 17 00:00:00 2001 From: Maurice Date: Mon, 30 Mar 2026 12:16:00 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20bulk=20import=20for=20packing=20lists?= =?UTF-8?q?=20+=20complete=20i18n=20sync=20=E2=80=94=20closes=20#133?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Packing list bulk import: - Import button in packing list header opens a modal - Paste items or load CSV/TXT file - Format: Category, Name, Weight (g), Bag, checked/unchecked - Bags are auto-created if they don't exist - Server endpoint POST /packing/import with transaction i18n sync: - Added all missing translation keys to fr, es, nl, ru, zh, ar - All 8 language files now have matching key sets - Includes memories, vacay weekdays, packing import, settlement, GPX import, blur booking codes, transport timeline keys --- client/src/api/client.ts | 1 + .../components/Packing/PackingListPanel.tsx | 105 +++++++++++++++++- client/src/i18n/translations/ar.ts | 45 ++++++++ client/src/i18n/translations/de.ts | 9 ++ client/src/i18n/translations/en.ts | 9 ++ client/src/i18n/translations/es.ts | 45 ++++++++ client/src/i18n/translations/fr.ts | 45 ++++++++ client/src/i18n/translations/nl.ts | 45 ++++++++ client/src/i18n/translations/ru.ts | 45 ++++++++ client/src/i18n/translations/zh.ts | 45 ++++++++ server/src/routes/packing.ts | 47 ++++++++ 11 files changed, 440 insertions(+), 1 deletion(-) diff --git a/client/src/api/client.ts b/client/src/api/client.ts index 3301a26..8f09515 100644 --- a/client/src/api/client.ts +++ b/client/src/api/client.ts @@ -112,6 +112,7 @@ export const assignmentsApi = { export const packingApi = { list: (tripId: number | string) => apiClient.get(`/trips/${tripId}/packing`).then(r => r.data), create: (tripId: number | string, data: Record) => apiClient.post(`/trips/${tripId}/packing`, data).then(r => r.data), + bulkImport: (tripId: number | string, items: { name: string; category?: string; quantity?: number }[]) => apiClient.post(`/trips/${tripId}/packing/import`, { items }).then(r => r.data), update: (tripId: number | string, id: number, data: Record) => apiClient.put(`/trips/${tripId}/packing/${id}`, data).then(r => r.data), delete: (tripId: number | string, id: number) => apiClient.delete(`/trips/${tripId}/packing/${id}`).then(r => r.data), reorder: (tripId: number | string, orderedIds: number[]) => apiClient.put(`/trips/${tripId}/packing/reorder`, { orderedIds }).then(r => r.data), diff --git a/client/src/components/Packing/PackingListPanel.tsx b/client/src/components/Packing/PackingListPanel.tsx index 7d670db..d1cc0a0 100644 --- a/client/src/components/Packing/PackingListPanel.tsx +++ b/client/src/components/Packing/PackingListPanel.tsx @@ -3,9 +3,10 @@ import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { packingApi, tripsApi, adminApi } from '../../api/client' +import ReactDOM from 'react-dom' import { CheckSquare, Square, Trash2, Plus, ChevronDown, ChevronRight, - X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, UserPlus, Package, FolderPlus, + X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, UserPlus, Package, FolderPlus, Upload, } from 'lucide-react' import type { PackingItem } from '../../types' @@ -727,6 +728,9 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp const [availableTemplates, setAvailableTemplates] = useState<{ id: number; name: string; item_count: number }[]>([]) const [showTemplateDropdown, setShowTemplateDropdown] = useState(false) const [applyingTemplate, setApplyingTemplate] = useState(false) + const [showImportModal, setShowImportModal] = useState(false) + const [importText, setImportText] = useState('') + const csvInputRef = useRef(null) const templateDropdownRef = useRef(null) useEffect(() => { @@ -757,6 +761,44 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp } } + const parseImportLines = (text: string) => { + return text.split('\n').map(line => line.trim()).filter(Boolean).map(line => { + // Format: Category, Name, Weight (optional), Bag (optional), checked/unchecked (optional) + const parts = line.split(/[,;\t]/).map(s => s.trim()) + if (parts.length >= 2) { + const category = parts[0] + const name = parts[1] + const weight_grams = parts[2] || undefined + const bag = parts[3] || undefined + const checked = parts[4]?.toLowerCase() === 'checked' || parts[4] === '1' + return { name, category, weight_grams, bag, checked } + } + // Single value = just a name + return { name: parts[0], category: undefined, weight_grams: undefined, bag: undefined, checked: false } + }).filter(i => i.name) + } + + const handleBulkImport = async () => { + const parsed = parseImportLines(importText) + if (parsed.length === 0) { toast.error(t('packing.importEmpty')); return } + try { + const result = await packingApi.bulkImport(tripId, parsed) + toast.success(t('packing.importSuccess', { count: result.count })) + setImportText('') + setShowImportModal(false) + window.location.reload() + } catch { toast.error(t('packing.importError')) } + } + + const handleCsvFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + e.target.value = '' + const reader = new FileReader() + reader.onload = () => { if (typeof reader.result === 'string') setImportText(reader.result) } + reader.readAsText(file) + } + const font = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" } return ( @@ -781,6 +823,13 @@ export default function PackingListPanel({ tripId, items }: PackingListPanelProp {t('packing.clearCheckedShort', { count: abgehakt })} )} + {availableTemplates.length > 0 && (