import { useState, useMemo, useEffect, useRef } from 'react' import ReactDOM from 'react-dom' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useToast } from '../shared/Toast' import { useTranslation } from '../../i18n' import { tripsApi } from '../../api/client' import apiClient from '../../api/client' import CustomSelect from '../shared/CustomSelect' import { CustomDatePicker } from '../shared/CustomDateTimePicker' import { formatDate as fmtDate } from '../../utils/formatters' import { CheckSquare, Square, Plus, ChevronRight, Flag, X, Check, Calendar, User, FolderPlus, AlertCircle, ListChecks, Inbox, CheckCheck, Trash2, } from 'lucide-react' import type { TodoItem } from '../../types' import { KAT_COLORS, PRIO_CONFIG, katColor, type FilterType, type Member } from './todoListModel' import { useTodoList } from './useTodoList' import TodoRow from './TodoRow' export default function TodoListPanel({ tripId, items, addItemSignal = 0 }: { tripId: number; items: TodoItem[]; addItemSignal?: number }) { // Layout component: state/effects/derived/handlers live in useTodoList. const { canEdit, t, formatDate, toggleTodoItem, isMobile, filter, setFilter, selectedId, setSelectedId, isAddingNew, setIsAddingNew, sortByPrio, setSortByPrio, addingCategory, setAddingCategory, newCategoryName, setNewCategoryName, members, categories, today, filtered, selectedItem, totalCount, doneCount, overdueCount, myCount, addCategory, catCount, } = useTodoList(tripId, items, addItemSignal) // Sidebar filter item const SidebarItem = ({ id, icon: Icon, label, count, color }: { id: string; icon: any; label: string; count: number; color?: string }) => ( ) // Filter title const filterTitle = (() => { if (filter === 'all') return t('todo.filter.all') if (filter === 'done') return t('todo.filter.done') if (filter === 'my') return t('todo.filter.my') if (filter === 'overdue') return t('todo.filter.overdue') return filter })() return (
{/* ── Left Sidebar ── */}
{/* Progress Card */} {!isMobile &&
{totalCount > 0 ? Math.round((doneCount / totalCount) * 100) : 0}%
0 ? `${Math.round((doneCount / totalCount) * 100)}%` : '0%', background: '#22c55e', borderRadius: 2, transition: 'width 0.3s' }} />
{doneCount} / {totalCount} {t('todo.completed')}
} {/* Smart filters */} {!isMobile &&
{t('todo.sidebar.tasks')}
} !i.checked).length} /> {/* Sort by */} {!isMobile &&
{t('todo.sidebar.sortBy')}
} {/* Categories */} {!isMobile &&
{t('todo.sidebar.categories')}
} {isMobile &&
} {categories.map(cat => ( ))} {canEdit && ( addingCategory && !isMobile ? (
setNewCategoryName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') addCategory(); if (e.key === 'Escape') { setAddingCategory(false); setNewCategoryName('') } }} placeholder={t('todo.newCategory')} style={{ flex: 1, fontSize: 12, padding: '4px 6px', border: '1px solid var(--border-primary)', borderRadius: 5, background: 'var(--bg-hover)', color: 'var(--text-primary)', fontFamily: 'inherit', minWidth: 0 }} />
) : ( ) )}
{/* ── Middle: Task List ── */}
{/* Header */}

{filterTitle}

{filtered.length}
{/* Task list */}
{filtered.length === 0 ? null : ( filtered.map(item => ( { setSelectedId(id); setIsAddingNew(false) }} onToggle={(id, checked) => toggleTodoItem(tripId, id, checked)} /> )) )}
{/* ── Right: Detail Pane ── */} {selectedItem && !isAddingNew && !isMobile && ( setSelectedId(null)} /> )} {selectedItem && !isAddingNew && isMobile && (
{ if (e.target === e.currentTarget) setSelectedId(null) }} style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }}>
{ if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px 16px 0 0' } } }}> setSelectedId(null)} />
)} {isAddingNew && !selectedItem && !isMobile && ReactDOM.createPortal(
{ if (e.target === e.currentTarget) setIsAddingNew(false) }} className="trek-modal-backdrop" style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(15,23,42,0.5)', display: 'flex', justifyContent: 'center', alignItems: 'flex-start', paddingTop: 'calc(var(--nav-h) + 60px)', paddingBottom: 40 }}>
{ if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px' } } }}> { setIsAddingNew(false); setSelectedId(id) }} onClose={() => setIsAddingNew(false)} />
, document.body )} {isAddingNew && !selectedItem && isMobile && ReactDOM.createPortal(
{ if (e.target === e.currentTarget) setIsAddingNew(false) }} className="trek-modal-backdrop" style={{ position: 'fixed', inset: 0, zIndex: 1000, background: 'rgba(0,0,0,0.4)', display: 'flex', justifyContent: 'center', alignItems: 'flex-end', paddingBottom: 'var(--bottom-nav-h)' }}>
{ if (el) { const child = el.firstElementChild as HTMLElement; if (child) { child.style.width = '100%'; child.style.borderLeft = 'none'; child.style.borderRadius = '16px 16px 0 0' } } }}> { setIsAddingNew(false); setSelectedId(id) }} onClose={() => setIsAddingNew(false)} />
, document.body )}
) } // ── Detail Pane (right side) ────────────────────────────────────────────── function DetailPane({ item, tripId, categories, members, onClose }: { item: TodoItem; tripId: number; categories: string[]; members: Member[]; onClose: () => void; }) { const { updateTodoItem, deleteTodoItem } = useTripStore() const trip = useTripStore((s) => s.trip) const can = useCanDo() const canEdit = can('packing_edit', trip) const toast = useToast() const { t } = useTranslation() const [name, setName] = useState(item.name) const [desc, setDesc] = useState(item.description || '') const [dueDate, setDueDate] = useState(item.due_date || '') const [category, setCategory] = useState(item.category || '') const [addingCategory, setAddingCategoryInline] = useState(false) const [assignedUserId, setAssignedUserId] = useState(item.assigned_user_id) const [priority, setPriority] = useState(item.priority || 0) const [saving, setSaving] = useState(false) // Sync when selected item changes useEffect(() => { setName(item.name) setDesc(item.description || '') setDueDate(item.due_date || '') setCategory(item.category || '') setAssignedUserId(item.assigned_user_id) setPriority(item.priority || 0) }, [item.id, item.name, item.description, item.due_date, item.category, item.assigned_user_id, item.priority]) const hasChanges = name !== item.name || desc !== (item.description || '') || dueDate !== (item.due_date || '') || category !== (item.category || '') || assignedUserId !== item.assigned_user_id || priority !== (item.priority || 0) const save = async () => { if (!name.trim() || !hasChanges) return setSaving(true) try { await updateTodoItem(tripId, item.id, { name: name.trim(), description: desc || null, due_date: dueDate || null, category: category || null, assigned_user_id: assignedUserId, priority, } as any) } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } setSaving(false) } const handleDelete = async () => { try { await deleteTodoItem(tripId, item.id) onClose() } catch (err: unknown) { toast.error(err instanceof Error ? err.message : t('common.error')) } } const labelClass = 'block text-xs font-medium text-content-secondary mb-1' const inputStyle: React.CSSProperties = { width: '100%', fontSize: 13, padding: '8px 10px', border: '1px solid var(--border-primary)', borderRadius: 8, background: 'var(--bg-primary)', color: 'var(--text-primary)', fontFamily: 'inherit', } return (
{/* Header */}
{t('todo.detail.title')}
{/* Form */}
{/* Name */}
setName(e.target.value)} disabled={!canEdit} style={{ ...inputStyle, fontSize: 15, fontWeight: 600, border: 'none', padding: '4px 0', background: 'transparent' }} placeholder={t('todo.namePlaceholder')} />
{/* Description */}