import { useState } from 'react' import { useParams, Link } from 'react-router-dom' import { useSSE } from '../hooks/useSSE' import ChecklistItem from '../components/ChecklistItem' import { EditableTitle } from '../components/EditableTitle' import { exportChecklistToJSON } from '../hooks/useLocalStorage' import type { ChecklistItem as ChecklistItemType } from '../types' import { Flex, Box, Text, Button, TextField, Heading, Badge, Card, IconButton } from '@radix-ui/themes' import { ArrowLeftIcon, DownloadIcon, ChevronDownIcon, ChevronUpIcon } from '@radix-ui/react-icons' interface ItemGroup { title: string items: ChecklistItemType[] isCollapsible?: boolean isCollapsed?: boolean onToggleCollapse?: () => void } export default function Checklist() { const { uuid } = useParams<{ uuid: string }>() const { items, checkListName, isConnected, error } = useSSE(uuid || '') const [newItemContent, setNewItemContent] = useState('') const [isAddingItem, setIsAddingItem] = useState(false) const [completedItemsCollapsed, setCompletedItemsCollapsed] = useState(false) const [isExporting, setIsExporting] = useState(false) const buildItemTree = (items: ChecklistItemType[]): ChecklistItemType[] => { const itemMap = new Map() const rootItems: ChecklistItemType[] = [] // Create a map of all items items.forEach(item => { itemMap.set(item.id, { ...item, children: [] }) }) // Build the tree structure items.forEach(item => { if (item.parent_id === null) { rootItems.push(itemMap.get(item.id)!) } else { const parent = itemMap.get(item.parent_id) if (parent) { if (!parent.children) parent.children = [] parent.children.push(itemMap.get(item.id)!) } } }) return rootItems } const categorizeItems = (items: ChecklistItemType[]): ItemGroup[] => { const now = new Date() // Helper function to check if an item can be completed const canComplete = (item: ChecklistItemType): boolean => { // Check dependencies const dependenciesCompleted = item.dependencies?.every(depId => { const depItem = items.find(i => i.id === depId) return depItem?.checked }) ?? true // Check date constraints const notBeforeDate = item.not_before ? new Date(item.not_before) : null const notAfterDate = item.not_after ? new Date(item.not_after) : null const dateConstraintsMet = (!notBeforeDate || now >= notBeforeDate) && (!notAfterDate || now <= notAfterDate) return dependenciesCompleted && dateConstraintsMet } // Helper function to check if an item is locked by constraints const isLockedByConstraints = (item: ChecklistItemType): boolean => { if (item.checked) return false // Check dependencies const dependenciesCompleted = item.dependencies?.every(depId => { const depItem = items.find(i => i.id === depId) return depItem?.checked }) ?? true // Check date constraints const notBeforeDate = item.not_before ? new Date(item.not_before) : null const notAfterDate = item.not_after ? new Date(item.not_after) : null const dateConstraintsMet = (!notBeforeDate || now >= notBeforeDate) && (!notAfterDate || now <= notAfterDate) return !dependenciesCompleted || !dateConstraintsMet } const completedItems = items.filter(item => item.checked) const availableItems = items.filter(item => !item.checked && canComplete(item)) const lockedItems = items.filter(item => !item.checked && isLockedByConstraints(item)) const groups: ItemGroup[] = [] // Completed items (collapsible) if (completedItems.length > 0) { groups.push({ title: `Completed (${completedItems.length})`, items: completedItems, isCollapsible: true, isCollapsed: completedItemsCollapsed, onToggleCollapse: () => setCompletedItemsCollapsed(!completedItemsCollapsed) }) } // Available items (can be completed) if (availableItems.length > 0) { groups.push({ title: `Available (${availableItems.length})`, items: availableItems }) } // Locked items (cannot be completed due to constraints) if (lockedItems.length > 0) { groups.push({ title: `Locked by Constraints (${lockedItems.length})`, items: lockedItems }) } return groups } const addItem = async (content: string, parentId?: number) => { if (!content.trim() || !uuid) return setIsAddingItem(true) try { const response = await fetch(`/api/checklists/${uuid}/items`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ content: content.trim(), parent_id: parentId || null }), }) if (!response.ok) { throw new Error('Failed to add item') } } catch (error) { console.error('Error adding item:', error) alert('Failed to add item') } finally { setIsAddingItem(false) } } const updateItem = async (id: number, updates: Partial) => { if (!uuid) return try { const response = await fetch(`/api/checklists/${uuid}/items/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(updates), }) if (!response.ok) { throw new Error('Failed to update item') } } catch (error) { console.error('Error updating item:', error) throw error } } const deleteItem = async (id: number) => { if (!uuid) return try { const response = await fetch(`/api/checklists/${uuid}/items/${id}`, { method: 'DELETE', }) if (!response.ok) { throw new Error('Failed to delete item') } } catch (error) { console.error('Error deleting item:', error) throw error } } const lockItem = async (id: number, user: string) => { if (!uuid) return try { const response = await fetch(`/api/checklists/${uuid}/items/${id}/lock`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ user }), }) if (!response.ok) { throw new Error('Failed to lock item') } } catch (error) { console.error('Error locking item:', error) throw error } } const updateChecklistName = async (newName: string) => { if (!uuid) throw new Error('No checklist UUID') const response = await fetch(`/api/checklists/${uuid}/name`, { method: 'PATCH', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ name: newName }), }) if (!response.ok) { throw new Error('Failed to update checklist name') } } const handleAddItem = async () => { await addItem(newItemContent) setNewItemContent('') } const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleAddItem() } } const handleExportChecklist = async () => { if (!uuid || !checkListName) return setIsExporting(true) try { await exportChecklistToJSON(uuid, checkListName) } catch { alert('Failed to export checklist') } finally { setIsExporting(false) } } const renderGroupedItems = (allItems: ChecklistItemType[]) => { const itemTree = buildItemTree(allItems) if (itemTree.length === 0) { return ( 📝 No items yet Add your first item above to get started! ) } const groups = categorizeItems(allItems) return ( {groups.map((group, groupIndex) => ( {group.title} {group.isCollapsible && ( {group.isCollapsed ? : } )} {(!group.isCollapsible || !group.isCollapsed) && ( {group.items.map(item => { const findItemWithChildren = (items: ChecklistItemType[], targetId: number): ChecklistItemType | null => { for (const item of items) { if (item.id === targetId) { return item } if (item.children) { const found = findItemWithChildren(item.children, targetId) if (found) return found } } return null } const itemWithChildren = findItemWithChildren(itemTree, item.id) return ( ) })} )} ))} ) } if (!uuid) { return ( Invalid checklist ID ) } return ( {isConnected ? 'Connected' : 'Disconnected'} {error && ( ⚠️ {error} )} setNewItemContent(e.target.value)} onKeyDown={handleKeyPress} disabled={isAddingItem || !isConnected} /> {renderGroupedItems(items)} ) }