import React, { useState, useEffect, useRef, useMemo } from 'react' import { useTripStore } from '../../store/tripStore' import { useTranslation } from '../../i18n' import { Plus, Trash2, Calculator, Wallet, ArrowRightLeft } from 'lucide-react' import CustomSelect from '../shared/CustomSelect' import { exchangeApi } from '../../api/client' // ── Helpers ────────────────────────────────────────────────────────────────── const CURRENCIES = ['EUR', 'USD', 'GBP', 'JPY', 'CHF', 'CZK', 'PLN', 'SEK', 'NOK', 'DKK', 'TRY', 'THB', 'AUD', 'CAD'] const SYMBOLS = { EUR: '€', USD: '$', GBP: '£', JPY: '¥', CHF: 'CHF', CZK: 'Kč', PLN: 'zł', SEK: 'kr', NOK: 'kr', DKK: 'kr', TRY: '₺', THB: '฿', AUD: 'A$', CAD: 'C$' } const PIE_COLORS = ['#6366f1', '#ec4899', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ef4444', '#14b8a6', '#f97316', '#06b6d4', '#84cc16', '#a855f7'] const fmtNum = (v, locale, cur) => { if (v == null || isNaN(v)) return '-' return Number(v).toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' ' + (SYMBOLS[cur] || cur) } const calcPP = (p, n) => (n > 0 ? p / n : null) const calcPD = (p, d) => (d > 0 ? p / d : null) const calcPPD = (p, n, d) => (n > 0 && d > 0 ? p / (n * d) : null) // ── Inline Edit Cell ───────────────────────────────────────────────────────── function InlineEditCell({ value, onSave, type = 'text', style = {}, placeholder = '', decimals = 2, locale, editTooltip }) { const [editing, setEditing] = useState(false) const [editValue, setEditValue] = useState(value ?? '') const inputRef = useRef(null) useEffect(() => { if (editing && inputRef.current) { inputRef.current.focus(); inputRef.current.select() } }, [editing]) const save = () => { setEditing(false) let v = editValue if (type === 'number') { const p = parseFloat(String(editValue).replace(',', '.')); v = isNaN(p) ? null : p } if (v !== value) onSave(v) } if (editing) { return setEditValue(e.target.value)} onBlur={save} onKeyDown={e => { if (e.key === 'Enter') save(); if (e.key === 'Escape') { setEditValue(value ?? ''); setEditing(false) } }} style={{ width: '100%', border: '1px solid var(--accent)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', background: 'var(--bg-input)', color: 'var(--text-primary)', fontFamily: 'inherit', ...style }} placeholder={placeholder} /> } const display = type === 'number' && value != null ? Number(value).toLocaleString(locale, { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) : (value || '') return (
{ setEditValue(value ?? ''); setEditing(true) }} title={editTooltip} style={{ cursor: 'pointer', padding: '4px 6px', borderRadius: 4, minHeight: 28, display: 'flex', alignItems: 'center', justifyContent: style?.textAlign === 'center' ? 'center' : 'flex-start', transition: 'background 0.15s', color: display ? 'var(--text-primary)' : 'var(--text-faint)', fontSize: 13, ...style }} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> {display || placeholder || '-'}
) } // ── Add Item Row ───────────────────────────────────────────────────────────── function AddItemRow({ onAdd, t }) { const [name, setName] = useState('') const [price, setPrice] = useState('') const [persons, setPersons] = useState('') const [days, setDays] = useState('') const [note, setNote] = useState('') const nameRef = useRef(null) const handleAdd = () => { if (!name.trim()) return onAdd({ name: name.trim(), total_price: parseFloat(String(price).replace(',', '.')) || 0, persons: parseInt(persons) || null, days: parseInt(days) || null, note: note.trim() || null }) setName(''); setPrice(''); setPersons(''); setDays(''); setNote('') setTimeout(() => nameRef.current?.focus(), 50) } const inp = { border: '1px solid var(--border-primary)', borderRadius: 4, padding: '4px 6px', fontSize: 13, outline: 'none', fontFamily: 'inherit', width: '100%', background: 'var(--bg-input)', color: 'var(--text-primary)' } return ( setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder={t('budget.newEntry')} style={inp} /> setPrice(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder="0,00" inputMode="decimal" style={{ ...inp, textAlign: 'center' }} /> setPersons(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 50, margin: '0 auto' }} /> setDays(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 50, margin: '0 auto' }} /> - - - setNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder={t('budget.table.note')} style={inp} /> ) } // ── Pie Chart (pure CSS conic-gradient) ────────────────────────────────────── function PieChart({ segments, size = 200, totalLabel }) { if (!segments.length) return null const total = segments.reduce((s, x) => s + x.value, 0) if (total === 0) return null let cumDeg = 0 const stops = segments.map(seg => { const start = cumDeg const deg = (seg.value / total) * 360 cumDeg += deg return `${seg.color} ${start}deg ${start + deg}deg` }).join(', ') return (
{totalLabel}
) } // ── Main Component ─────────────────────────────────────────────────────────── export default function BudgetPanel({ tripId }) { const { trip, budgetItems, addBudgetItem, updateBudgetItem, deleteBudgetItem, loadBudgetItems, updateTrip } = useTripStore() const { t, locale } = useTranslation() const [newCategoryName, setNewCategoryName] = useState('') const currency = trip?.currency || 'EUR' const [rates, setRates] = useState(null) const [convertTo, setConvertTo] = useState(() => { const saved = localStorage.getItem('budget_convert_to') return saved || (currency === 'EUR' ? 'USD' : 'EUR') }) useEffect(() => { exchangeApi.getRates().then(setRates).catch(() => {}) }, []) const fmt = (v, cur) => fmtNum(v, locale, cur) const setCurrency = (cur) => { if (tripId) updateTrip(tripId, { currency: cur }) } useEffect(() => { if (tripId) loadBudgetItems(tripId) }, [tripId]) const grouped = useMemo(() => (budgetItems || []).reduce((acc, item) => { const cat = item.category || 'Sonstiges' if (!acc[cat]) acc[cat] = [] acc[cat].push(item) return acc }, {}), [budgetItems]) const categoryNames = Object.keys(grouped) const grandTotal = (budgetItems || []).reduce((s, i) => s + (i.total_price || 0), 0) const pieSegments = useMemo(() => categoryNames.map((cat, i) => ({ name: cat, value: grouped[cat].reduce((s, x) => s + (x.total_price || 0), 0), color: PIE_COLORS[i % PIE_COLORS.length], })).filter(s => s.value > 0) , [grouped, categoryNames]) const handleAddItem = async (category, data) => { try { await addBudgetItem(tripId, { ...data, category }) } catch {} } const handleUpdateField = async (id, field, value) => { try { await updateBudgetItem(tripId, id, { [field]: value }) } catch {} } const handleDeleteItem = async (id) => { try { await deleteBudgetItem(tripId, id) } catch {} } const handleDeleteCategory = async (cat) => { const items = grouped[cat] || [] for (const item of items) await deleteBudgetItem(tripId, item.id) } const handleAddCategory = () => { if (!newCategoryName.trim()) return addBudgetItem(tripId, { name: t('budget.defaultEntry'), category: newCategoryName.trim(), total_price: 0 }) setNewCategoryName('') } const th = { padding: '6px 8px', textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid var(--border-primary)', whiteSpace: 'nowrap', background: 'var(--bg-secondary)' } const td = { padding: '2px 6px', borderBottom: '1px solid var(--border-secondary)', fontSize: 13, verticalAlign: 'middle', color: 'var(--text-primary)' } // ── Empty State ────────────────────────────────────────────────────────── if (!budgetItems || budgetItems.length === 0) { return (

{t('budget.emptyTitle')}

{t('budget.emptyText')}

setNewCategoryName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAddCategory()} placeholder={t('budget.emptyPlaceholder')} style={{ flex: 1, padding: '9px 14px', borderRadius: 10, border: '1px solid var(--border-primary)', fontSize: 13, fontFamily: 'inherit', outline: 'none', background: 'var(--bg-input)', color: 'var(--text-primary)', minWidth: 0 }} />
) } // ── Main Layout ────────────────────────────────────────────────────────── return (

{t('budget.title')}

{categoryNames.map((cat, ci) => { const items = grouped[cat] const subtotal = items.reduce((s, x) => s + (x.total_price || 0), 0) const color = PIE_COLORS[ci % PIE_COLORS.length] return (
{cat}
{fmt(subtotal, currency)}
{items.map(item => { const pp = calcPP(item.total_price, item.persons) const pd = calcPD(item.total_price, item.days) const ppd = calcPPD(item.total_price, item.persons, item.days) return ( e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ) })} handleAddItem(cat, data)} t={t} />
{t('budget.table.name')} {t('budget.table.total')} {t('budget.table.persons')} {t('budget.table.days')} {t('budget.table.perPerson')} {t('budget.table.perDay')} {t('budget.table.perPersonDay')} {t('budget.table.note')}
handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={t('budget.editTooltip')} /> handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder="0,00" locale={locale} editTooltip={t('budget.editTooltip')} /> handleUpdateField(item.id, 'persons', v != null ? parseInt(v) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} /> handleUpdateField(item.id, 'days', v != null ? parseInt(v) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} /> {pp != null ? fmt(pp, currency) : '-'} {pd != null ? fmt(pd, currency) : '-'} {ppd != null ? fmt(ppd, currency) : '-'} handleUpdateField(item.id, 'note', v)} placeholder={t('budget.table.note')} locale={locale} editTooltip={t('budget.editTooltip')} />
) })}
({ value: c, label: `${c} (${SYMBOLS[c] || c})` }))} searchable />
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')}
{Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
{SYMBOLS[currency] || currency} {currency}
{/* Live exchange rate conversion */} {rates && (() => { const fromRate = currency === 'EUR' ? 1 : rates.rates?.[currency] const toRate = convertTo === 'EUR' ? 1 : rates.rates?.[convertTo] const converted = fromRate && toRate ? (grandTotal / fromRate) * toRate : null return converted != null ? (
{t('budget.converted')}
{Number(converted).toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}
1 {currency} = {((toRate / fromRate) || 0).toFixed(4)} {convertTo}
) : null })()}
{pieSegments.length > 0 && (
{t('budget.byCategory')}
{pieSegments.map(seg => { const pct = grandTotal > 0 ? ((seg.value / grandTotal) * 100).toFixed(1) : '0.0' return (
{seg.name} {pct}%
) })}
{pieSegments.map(seg => (
{seg.name} {fmt(seg.value, currency)}
))}
)}
) }