import type { CSSProperties, Dispatch, SetStateAction } from 'react' import { Trash2, Pencil, GripVertical } from 'lucide-react' import type { BudgetItem } from '../../types' import { currencyDecimals } from '../../utils/formatters' import { CustomDatePicker } from '../shared/CustomDateTimePicker' import { calcPP, calcPD, calcPPD } from './BudgetPanel.helpers' import InlineEditCell from './BudgetPanelInlineEditCell' import AddItemRow from './BudgetPanelAddItemRow' import BudgetMemberChips, { type TripMember } from './BudgetPanelMemberChips' import type { EditingCat, AddItemData } from './useBudgetPanel' interface BudgetCategoryTableProps { cat: string grouped: Map categoryColor: (cat: string) => string canEdit: boolean editingCat: EditingCat | null setEditingCat: Dispatch> dragCat: string | null setDragCat: Dispatch> dragOverCat: string | null setDragOverCat: Dispatch> dragItem: number | null setDragItem: Dispatch> dragOverItem: number | null setDragOverItem: Dispatch> dragItemCat: string | null setDragItemCat: Dispatch> categoryNames: string[] reorderBudgetCategories: (tripId: number | string, orderedCategories: string[]) => Promise reorderBudgetItems: (tripId: number | string, orderedIds: number[]) => Promise handleRenameCategory: (oldName: string, newName: string) => Promise handleDeleteCategory: (cat: string) => Promise handleDeleteItem: (id: number) => Promise handleUpdateField: (id: number, field: string, value: unknown) => Promise handleAddItem: (category: string, data: AddItemData) => Promise tripId: number currency: string locale: string t: (key: string) => string fmt: (v: number | null | undefined, cur: string) => string hasMultipleMembers: boolean tripMembers: TripMember[] setBudgetItemMembers: (tripId: number | string, itemId: number, userIds: number[]) => Promise<{ members: unknown; item: unknown }> toggleBudgetMemberPaid: (tripId: number | string, itemId: number, userId: number, paid: boolean) => Promise th: CSSProperties td: CSSProperties } export default function BudgetCategoryTable({ cat, grouped, categoryColor, canEdit, editingCat, setEditingCat, dragCat, setDragCat, dragOverCat, setDragOverCat, dragItem, setDragItem, dragOverItem, setDragOverItem, dragItemCat, setDragItemCat, categoryNames, reorderBudgetCategories, reorderBudgetItems, handleRenameCategory, handleDeleteCategory, handleDeleteItem, handleUpdateField, handleAddItem, tripId, currency, locale, t, fmt, hasMultipleMembers, tripMembers, setBudgetItemMembers, toggleBudgetMemberPaid, th, td }: BudgetCategoryTableProps) { const items = grouped.get(cat) || [] const subtotal = items.reduce((s, x) => s + (x.total_price || 0), 0) const color = categoryColor(cat) return (
{ if (!dragCat || dragCat === cat || dragItem) return e.preventDefault(); e.dataTransfer.dropEffect = 'move' setDragOverCat(cat) }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverCat(null) }} onDrop={e => { e.preventDefault() if (dragCat && dragCat !== cat) { const newOrder = [...categoryNames] const fromIdx = newOrder.indexOf(dragCat) const toIdx = newOrder.indexOf(cat) newOrder.splice(fromIdx, 1) newOrder.splice(toIdx, 0, dragCat) reorderBudgetCategories(tripId, newOrder) } setDragCat(null); setDragOverCat(null) }} > {dragOverCat === cat &&
}
{canEdit && (
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/x-budget-cat', cat); setDragCat(cat) }} onDragEnd={() => { setDragCat(null); setDragOverCat(null) }} style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'rgba(255,255,255,0.4)', flexShrink: 0 }}>
)}
{canEdit && editingCat?.name === cat ? ( setEditingCat({ ...editingCat, value: e.target.value })} onBlur={() => { handleRenameCategory(cat, editingCat.value); setEditingCat(null) }} onKeyDown={e => { if (e.key === 'Enter') { handleRenameCategory(cat, editingCat.value); setEditingCat(null) } if (e.key === 'Escape') setEditingCat(null) }} style={{ fontWeight: 600, fontSize: 13, background: 'rgba(255,255,255,0.15)', border: 'none', borderRadius: 4, color: '#fff', padding: '1px 6px', outline: 'none', fontFamily: 'inherit', width: '100%' }} /> ) : ( <> {cat} {canEdit && ( )} )}
{fmt(subtotal, currency)} {canEdit && ( )}
{ if (dragCat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move' } }}> {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) const hasMembers = (item.members?.length ?? 0) > 0 return ( { if (dragCat && dragCat !== cat) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; return } if (dragItem && dragItemCat === cat && dragItem !== item.id) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; setDragOverItem(item.id) } }} onDragLeave={e => { if (!e.currentTarget.contains(e.relatedTarget as Node)) setDragOverItem(null) }} onDrop={e => { if (dragItem && dragItemCat === cat && dragItem !== item.id) { e.preventDefault(); e.stopPropagation() const ids = items.map(i => i.id) const fromIdx = ids.indexOf(dragItem) const toIdx = ids.indexOf(item.id) ids.splice(fromIdx, 1) ids.splice(toIdx, 0, dragItem) reorderBudgetItems(tripId, ids) setDragItem(null); setDragOverItem(null); setDragItemCat(null) } }} onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'}> ) })} {canEdit && 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.date')} {t('budget.table.note')}
{canEdit && (
{ e.stopPropagation(); e.dataTransfer.effectAllowed = 'move'; setDragItem(item.id); setDragItemCat(cat) }} onDragEnd={() => { setDragItem(null); setDragOverItem(null); setDragItemCat(null) }} style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0 }}>
)}
handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} /> {hasMultipleMembers && (
setBudgetItemMembers(tripId, item.id, userIds)} onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} compact={false} readOnly={!canEdit} />
)}
handleUpdateField(item.id, 'total_price', v)} style={{ textAlign: 'center' }} placeholder={currencyDecimals(currency) === 0 ? '0' : '0,00'} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> {hasMultipleMembers ? ( setBudgetItemMembers(tripId, item.id, userIds)} onTogglePaid={(userId, paid) => toggleBudgetMemberPaid(tripId, item.id, userId, paid)} readOnly={!canEdit} /> ) : ( handleUpdateField(item.id, 'persons', v != null ? parseInt(v as string) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> )} handleUpdateField(item.id, 'days', v != null ? parseInt(v as string) || null : null)} style={{ textAlign: 'center' }} placeholder="-" locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> {pp != null ? fmt(pp, currency) : '-'} {pd != null ? fmt(pd, currency) : '-'} {ppd != null ? fmt(ppd, currency) : '-'} {canEdit ? (
handleUpdateField(item.id, 'expense_date', v || null)} placeholder="—" compact borderless />
) : ( {item.expense_date || '—'} )}
handleUpdateField(item.id, 'note', v)} placeholder={t('budget.table.note')} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> {canEdit && ( )}
) }