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 && (