import { useState, useMemo, useRef } from 'react' import { useTripStore } from '../../store/tripStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { CheckSquare, Square, Trash2, Plus, ChevronDown, ChevronRight, Sparkles, X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, Luggage, } from 'lucide-react' import type { PackingItem } from '../../types' const VORSCHLAEGE = [ { name: 'Passport', category: 'Documents' }, { name: 'Travel Insurance', category: 'Documents' }, { name: 'Visa Documents', category: 'Documents' }, { name: 'Flight Tickets', category: 'Documents' }, { name: 'Hotel Bookings', category: 'Documents' }, { name: 'Vaccination Card', category: 'Documents' }, { name: 'T-Shirts (5x)', category: 'Clothing' }, { name: 'Pants (2x)', category: 'Clothing' }, { name: 'Underwear (7x)', category: 'Clothing' }, { name: 'Socks (7x)', category: 'Clothing' }, { name: 'Jacket', category: 'Clothing' }, { name: 'Swimwear', category: 'Clothing' }, { name: 'Sport Shoes', category: 'Clothing' }, { name: 'Toothbrush', category: 'Toiletries' }, { name: 'Toothpaste', category: 'Toiletries' }, { name: 'Shampoo', category: 'Toiletries' }, { name: 'Sunscreen', category: 'Toiletries' }, { name: 'Deodorant', category: 'Toiletries' }, { name: 'Razor', category: 'Toiletries' }, { name: 'Phone Charger', category: 'Electronics' }, { name: 'Travel Adapter', category: 'Electronics' }, { name: 'Headphones', category: 'Electronics' }, { name: 'Camera', category: 'Electronics' }, { name: 'Power Bank', category: 'Electronics' }, { name: 'First Aid Kit', category: 'Health' }, { name: 'Prescription Medication', category: 'Health' }, { name: 'Pain Medication', category: 'Health' }, { name: 'Insect Repellent', category: 'Health' }, { name: 'Cash', category: 'Finances' }, { name: 'Credit Card', category: 'Finances' }, ] // Cycling color palette — works in light & dark mode const KAT_COLORS = [ '#3b82f6', // blue '#a855f7', // purple '#ec4899', // pink '#22c55e', // green '#f97316', // orange '#06b6d4', // cyan '#ef4444', // red '#eab308', // yellow '#8b5cf6', // violet '#14b8a6', // teal ] // Stable color assignment: category name → index via simple hash function katColor(kat, allCategories) { const idx = allCategories ? allCategories.indexOf(kat) : -1 if (idx >= 0) return KAT_COLORS[idx % KAT_COLORS.length] // Fallback: hash-based let h = 0 for (let i = 0; i < kat.length; i++) h = ((h << 5) - h + kat.charCodeAt(i)) | 0 return KAT_COLORS[Math.abs(h) % KAT_COLORS.length] } // ── Artikel-Zeile ────────────────────────────────────────────────────────── interface ArtikelZeileProps { item: PackingItem tripId: number categories: string[] onCategoryChange: () => void } function ArtikelZeile({ item, tripId, categories, onCategoryChange }: ArtikelZeileProps) { const [editing, setEditing] = useState(false) const [editName, setEditName] = useState(item.name) const [hovered, setHovered] = useState(false) const [showCatPicker, setShowCatPicker] = useState(false) const { togglePackingItem, updatePackingItem, deletePackingItem } = useTripStore() const toast = useToast() const { t } = useTranslation() const handleToggle = () => togglePackingItem(tripId, item.id, !item.checked) const handleSaveName = async () => { if (!editName.trim()) { setEditing(false); setEditName(item.name); return } try { await updatePackingItem(tripId, item.id, { name: editName.trim() }); setEditing(false) } catch { toast.error(t('packing.toast.saveError')) } } const handleDelete = async () => { try { await deletePackingItem(tripId, item.id) } catch { toast.error(t('packing.toast.deleteError')) } } const handleCatChange = async (cat) => { setShowCatPicker(false) if (cat === item.category) return try { await updatePackingItem(tripId, item.id, { category: cat }) } catch { toast.error(t('common.error')) } } return (
setHovered(true)} onMouseLeave={() => { setHovered(false); setShowCatPicker(false) }} style={{ display: 'flex', alignItems: 'center', gap: 8, padding: '6px 10px', borderRadius: 10, position: 'relative', background: hovered ? 'var(--bg-secondary)' : 'transparent', transition: 'background 0.1s', }} > {editing ? ( setEditName(e.target.value)} onBlur={handleSaveName} onKeyDown={e => { if (e.key === 'Enter') handleSaveName(); if (e.key === 'Escape') { setEditing(false); setEditName(item.name) } }} style={{ flex: 1, fontSize: 13.5, padding: '2px 8px', borderRadius: 6, border: '1px solid var(--border-primary)', outline: 'none', fontFamily: 'inherit' }} /> ) : ( !item.checked && setEditing(true)} style={{ flex: 1, fontSize: 13.5, cursor: item.checked ? 'default' : 'text', color: item.checked ? 'var(--text-faint)' : 'var(--text-primary)', textDecoration: item.checked ? 'line-through' : 'none', }} > {item.name} )}
{showCatPicker && (
{categories.map(cat => ( ))}
)}
) } // ── Kategorie-Gruppe ─────────────────────────────────────────────────────── interface KategorieGruppeProps { kategorie: string items: PackingItem[] tripId: number allCategories: string[] onRename: (oldName: string, newName: string) => Promise onDeleteAll: (items: PackingItem[]) => Promise } function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll }: KategorieGruppeProps) { const [offen, setOffen] = useState(true) const [editingName, setEditingName] = useState(false) const [editKatName, setEditKatName] = useState(kategorie) const [showMenu, setShowMenu] = useState(false) const { togglePackingItem } = useTripStore() const toast = useToast() const { t } = useTranslation() const abgehakt = items.filter(i => i.checked).length const alleAbgehakt = abgehakt === items.length const dot = katColor(kategorie, allCategories) const handleSaveKatName = async () => { const neu = editKatName.trim() if (!neu || neu === kategorie) { setEditingName(false); setEditKatName(kategorie); return } try { await onRename(kategorie, neu); setEditingName(false) } catch { toast.error(t('packing.toast.renameError')) } } const handleCheckAll = async () => { for (const item of Array.from(items)) { if (!item.checked) await togglePackingItem(tripId, item.id, true) } } const handleUncheckAll = async () => { for (const item of Array.from(items)) { if (item.checked) await togglePackingItem(tripId, item.id, false) } } const handleDeleteAll = async () => { await onDeleteAll(items) setShowMenu(false) } return (
{editingName ? ( setEditKatName(e.target.value)} onBlur={handleSaveKatName} onKeyDown={e => { if (e.key === 'Enter') handleSaveKatName(); if (e.key === 'Escape') { setEditingName(false); setEditKatName(kategorie) } }} style={{ flex: 1, fontSize: 12.5, fontWeight: 600, border: 'none', borderBottom: '2px solid var(--text-primary)', outline: 'none', background: 'transparent', fontFamily: 'inherit', color: 'var(--text-primary)', padding: '0 2px' }} /> ) : ( {kategorie} )} {abgehakt}/{items.length}
{showMenu && (
setShowMenu(false)}> } label={t('packing.menuRename')} onClick={() => { setEditingName(true); setShowMenu(false) }} /> } label={t('packing.menuCheckAll')} onClick={() => { handleCheckAll(); setShowMenu(false) }} /> } label={t('packing.menuUncheckAll')} onClick={() => { handleUncheckAll(); setShowMenu(false) }} />
} label={t('packing.menuDeleteCat')} danger onClick={handleDeleteAll} />
)}
{offen && (
{items.map(item => ( {}} /> ))}
)}
) } interface MenuItemProps { icon: React.ReactNode label: string onClick: () => void danger: boolean } function MenuItem({ icon, label, onClick, danger }: MenuItemProps) { return ( ) } // ── Haupt-Panel ──────────────────────────────────────────────────────────── interface PackingListPanelProps { tripId: number items: PackingItem[] } export default function PackingListPanel({ tripId, items }: PackingListPanelProps) { const [neuerName, setNeuerName] = useState('') const [neueKategorie, setNeueKategorie] = useState('') const [zeigeVorschlaege, setZeigeVorschlaege] = useState(false) const [filter, setFilter] = useState('alle') // 'alle' | 'offen' | 'erledigt' const [showKatDropdown, setShowKatDropdown] = useState(false) const katInputRef = useRef(null) const { addPackingItem, updatePackingItem, deletePackingItem } = useTripStore() const toast = useToast() const { t } = useTranslation() const allCategories = useMemo(() => { const cats = new Set(items.map(i => i.category || t('packing.defaultCategory'))) return Array.from(cats).sort() }, [items, t]) const gruppiert = useMemo(() => { const filtered = items.filter(i => { if (filter === 'offen') return !i.checked if (filter === 'erledigt') return i.checked return true }) const groups = {} for (const item of filtered) { const kat = item.category || t('packing.defaultCategory') if (!groups[kat]) groups[kat] = [] groups[kat].push(item) } return groups }, [items, filter, t]) const abgehakt = items.filter(i => i.checked).length const fortschritt = items.length > 0 ? Math.round((abgehakt / items.length) * 100) : 0 const handleAdd = async (e) => { e.preventDefault() if (!neuerName.trim()) return const kat = neueKategorie.trim() || (allCategories[0] || t('packing.defaultCategory')) try { await addPackingItem(tripId, { name: neuerName.trim(), category: kat }) setNeuerName('') } catch { toast.error(t('packing.toast.addError')) } } const vorschlaege = t('packing.suggestions.items') || VORSCHLAEGE const handleVorschlag = async (v) => { try { await addPackingItem(tripId, { name: v.name, category: v.category || v.kategorie }) } catch { toast.error(t('packing.toast.addError')) } } const handleRenameCategory = async (oldName, newName) => { const toUpdate = items.filter(i => (i.category || t('packing.defaultCategory')) === oldName) for (const item of toUpdate) { await updatePackingItem(tripId, item.id, { category: newName }) } } const handleDeleteCategory = async (catItems) => { for (const item of catItems) { try { await deletePackingItem(tripId, item.id) } catch {} } } const handleClearChecked = async () => { if (!confirm(t('packing.confirm.clearChecked', { count: abgehakt }))) return for (const item of items.filter(i => i.checked)) { try { await deletePackingItem(tripId, item.id) } catch {} } } const vorhandeneNamen = new Set(items.map(i => i.name.toLowerCase())) const verfuegbareVorschlaege = vorschlaege.filter(v => !vorhandeneNamen.has(v.name.toLowerCase())) const font = { fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif" } return (
{/* ── Header ── */}

{t('packing.title')}

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

{abgehakt > 0 && ( )}
{items.length > 0 && (
{fortschritt === 100 && (

{t('packing.allPacked')}

)}
)}
setNeuerName(e.target.value)} placeholder={t('packing.addPlaceholder')} style={{ flex: 1, padding: '8px 12px', borderRadius: 10, border: '1px solid var(--border-primary)', fontSize: 13.5, fontFamily: 'inherit', outline: 'none', color: 'var(--text-primary)' }} />
{ setNeueKategorie(e.target.value); setShowKatDropdown(true) }} onFocus={() => setShowKatDropdown(true)} onBlur={() => setTimeout(() => setShowKatDropdown(false), 150)} placeholder={allCategories[0] || t('packing.categoryPlaceholder')} style={{ width: 120, padding: '8px 10px', borderRadius: 10, border: '1px solid var(--border-primary)', fontSize: 13, fontFamily: 'inherit', outline: 'none', color: 'var(--text-secondary)' }} /> {showKatDropdown && allCategories.length > 0 && (
{allCategories.filter(c => !neueKategorie || c.toLowerCase().includes(neueKategorie.toLowerCase())).map(cat => ( ))}
)}
{/* ── Vorschläge ── */} {zeigeVorschlaege && (
{t('packing.suggestionsTitle')}
{verfuegbareVorschlaege.map((v, i) => ( ))} {verfuegbareVorschlaege.length === 0 &&

{t('packing.allSuggested')}

}
)} {/* ── Filter-Tabs ── */} {items.length > 0 && (
{[['alle', t('packing.filterAll')], ['offen', t('packing.filterOpen')], ['erledigt', t('packing.filterDone')]].map(([id, label]) => ( ))}
)} {/* ── Liste ── */}
{items.length === 0 ? (

{t('packing.emptyTitle')}

{t('packing.emptyHint')}

) : Object.keys(gruppiert).length === 0 ? (

{t('packing.emptyFiltered')}

) : (
{Object.entries(gruppiert).map(([kat, katItems]) => ( ))}
)}
) }