import { useState } from 'react' import { useParams, Link } from 'react-router-dom' import { useSSE } from '../hooks/useSSE' import ChecklistItem from '../components/ChecklistItem' import type { ChecklistItem as ChecklistItemType } from '../types' 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 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 handleAddItem = async () => { await addItem(newItemContent) setNewItemContent('') } const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { handleAddItem() } } 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 Header */}

{group.title.includes('Completed') && ( )} {group.title.includes('Available') && ( )} {group.title.includes('Locked') && ( )} {group.title}

{group.isCollapsible && ( )}
{/* Group Items */} {(!group.isCollapsible || !group.isCollapsed) && (
{group.items.map(item => { // Find the item with its children from the tree 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 (
{/* Header */}

{checkListName}

{isConnected ? ( Connected ) : ( Disconnected )}
{/* Error Message */} {error && (
⚠️ {error}
)} {/* Main Content */}
{/* Add Item Section */}
setNewItemContent(e.target.value)} onKeyDown={handleKeyPress} disabled={isAddingItem || !isConnected} className="flex-1 px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-100 dark:disabled:bg-gray-700 disabled:text-gray-500 dark:disabled:text-gray-400 bg-white dark:bg-gray-700 text-gray-900 dark:text-white placeholder-gray-500 dark:placeholder-gray-400" />
{/* Items Section */}
{renderGroupedItems(items)}
) }