diff --git a/frontend/src/components/ChecklistItem.tsx b/frontend/src/components/ChecklistItem.tsx index 5860431..f942dc2 100644 --- a/frontend/src/components/ChecklistItem.tsx +++ b/frontend/src/components/ChecklistItem.tsx @@ -191,7 +191,7 @@ export default function ChecklistItem({ } return ( -
  • +
  • {/* Checkbox */}
    diff --git a/frontend/src/index.css b/frontend/src/index.css index a461c50..15d863b 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1 +1,6 @@ -@import "tailwindcss"; \ No newline at end of file +@import "tailwindcss"; + +/* Remove list styling from checklist items */ +li { + list-style: none; +} \ No newline at end of file diff --git a/frontend/src/pages/Checklist.tsx b/frontend/src/pages/Checklist.tsx index 1fa0a19..7877bbe 100644 --- a/frontend/src/pages/Checklist.tsx +++ b/frontend/src/pages/Checklist.tsx @@ -4,11 +4,20 @@ 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[] => { @@ -36,6 +45,79 @@ export default function Checklist() { 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 @@ -147,6 +229,117 @@ export default function Checklist() { )) } + 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 (
    @@ -229,17 +422,7 @@ export default function Checklist() { {/* Items Section */}
    - {itemTree.length === 0 ? ( -
    -
    📝
    -

    No items yet

    -

    Add your first item above to get started!

    -
    - ) : ( -
      - {renderItems(itemTree, items)} -
    - )} + {renderGroupedItems(items)}