sorty
This commit is contained in:
parent
e11dd4ed1f
commit
e29c2a94ef
3 changed files with 201 additions and 13 deletions
|
@ -191,7 +191,7 @@ export default function ChecklistItem({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="group">
|
<li className="group list-none" style={{ listStyle: 'none' }}>
|
||||||
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all duration-200 border border-transparent hover:border-gray-200 dark:hover:border-gray-700">
|
<div className="flex items-start gap-3 p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-all duration-200 border border-transparent hover:border-gray-200 dark:hover:border-gray-700">
|
||||||
{/* Checkbox */}
|
{/* Checkbox */}
|
||||||
<div className="flex-shrink-0 mt-0.5">
|
<div className="flex-shrink-0 mt-0.5">
|
||||||
|
|
|
@ -1 +1,6 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Remove list styling from checklist items */
|
||||||
|
li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
|
@ -4,11 +4,20 @@ import { useSSE } from '../hooks/useSSE'
|
||||||
import ChecklistItem from '../components/ChecklistItem'
|
import ChecklistItem from '../components/ChecklistItem'
|
||||||
import type { ChecklistItem as ChecklistItemType } from '../types'
|
import type { ChecklistItem as ChecklistItemType } from '../types'
|
||||||
|
|
||||||
|
interface ItemGroup {
|
||||||
|
title: string
|
||||||
|
items: ChecklistItemType[]
|
||||||
|
isCollapsible?: boolean
|
||||||
|
isCollapsed?: boolean
|
||||||
|
onToggleCollapse?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
export default function Checklist() {
|
export default function Checklist() {
|
||||||
const { uuid } = useParams<{ uuid: string }>()
|
const { uuid } = useParams<{ uuid: string }>()
|
||||||
const { items, checkListName, isConnected, error } = useSSE(uuid || '')
|
const { items, checkListName, isConnected, error } = useSSE(uuid || '')
|
||||||
const [newItemContent, setNewItemContent] = useState('')
|
const [newItemContent, setNewItemContent] = useState('')
|
||||||
const [isAddingItem, setIsAddingItem] = useState(false)
|
const [isAddingItem, setIsAddingItem] = useState(false)
|
||||||
|
const [completedItemsCollapsed, setCompletedItemsCollapsed] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
const buildItemTree = (items: ChecklistItemType[]): ChecklistItemType[] => {
|
const buildItemTree = (items: ChecklistItemType[]): ChecklistItemType[] => {
|
||||||
|
@ -36,6 +45,79 @@ export default function Checklist() {
|
||||||
return rootItems
|
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) => {
|
const addItem = async (content: string, parentId?: number) => {
|
||||||
if (!content.trim() || !uuid) return
|
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 (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<div className="text-gray-400 dark:text-gray-500 text-5xl mb-3">📝</div>
|
||||||
|
<p className="text-gray-600 dark:text-gray-300 text-base font-medium">No items yet</p>
|
||||||
|
<p className="text-gray-500 dark:text-gray-400 mt-1">Add your first item above to get started!</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const groups = categorizeItems(allItems)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{groups.map((group, groupIndex) => (
|
||||||
|
<div key={groupIndex} className="space-y-3">
|
||||||
|
{/* Group Header */}
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 pb-2">
|
||||||
|
<h3 className={`text-sm font-semibold flex items-center gap-2 ${
|
||||||
|
group.title.includes('Completed')
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: group.title.includes('Available')
|
||||||
|
? 'text-blue-600 dark:text-blue-400'
|
||||||
|
: 'text-orange-600 dark:text-orange-400'
|
||||||
|
}`}>
|
||||||
|
{group.title.includes('Completed') && (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{group.title.includes('Available') && (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{group.title.includes('Locked') && (
|
||||||
|
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{group.title}
|
||||||
|
</h3>
|
||||||
|
{group.isCollapsible && (
|
||||||
|
<button
|
||||||
|
onClick={group.onToggleCollapse}
|
||||||
|
className="flex items-center gap-1 text-xs text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition-colors"
|
||||||
|
>
|
||||||
|
{group.isCollapsed ? (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
Show
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 15l7-7 7 7" />
|
||||||
|
</svg>
|
||||||
|
Hide
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Group Items */}
|
||||||
|
{(!group.isCollapsible || !group.isCollapsed) && (
|
||||||
|
<div className={`space-y-2 ${
|
||||||
|
group.title.includes('Completed') ? 'opacity-75' : ''
|
||||||
|
}`}>
|
||||||
|
{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 (
|
||||||
|
<ChecklistItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
onUpdate={updateItem}
|
||||||
|
onDelete={deleteItem}
|
||||||
|
onLock={lockItem}
|
||||||
|
children={itemWithChildren?.children || []}
|
||||||
|
allItems={allItems}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (!uuid) {
|
if (!uuid) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-screen">
|
<div className="flex items-center justify-center min-h-screen">
|
||||||
|
@ -229,17 +422,7 @@ export default function Checklist() {
|
||||||
|
|
||||||
{/* Items Section */}
|
{/* Items Section */}
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{itemTree.length === 0 ? (
|
{renderGroupedItems(items)}
|
||||||
<div className="text-center py-10">
|
|
||||||
<div className="text-gray-400 dark:text-gray-500 text-5xl mb-3">📝</div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 text-base font-medium">No items yet</p>
|
|
||||||
<p className="text-gray-500 dark:text-gray-400 mt-1">Add your first item above to get started!</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<ul>
|
|
||||||
{renderItems(itemTree, items)}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue