From e11dd4ed1f54c12be2d12569826451c304c1573f Mon Sep 17 00:00:00 2001 From: lubiana Date: Fri, 25 Jul 2025 16:27:30 +0200 Subject: [PATCH] datestuff --- README.md | 62 +++++++++- frontend/src/components/ChecklistItem.tsx | 83 ++++++++++++- .../src/components/DateConstraintManager.tsx | 92 ++++++++++++++ frontend/src/types.ts | 2 + main.go | 114 +++++++++++++++--- 5 files changed, 326 insertions(+), 27 deletions(-) create mode 100644 frontend/src/components/DateConstraintManager.tsx diff --git a/README.md b/README.md index f1f6471..fa0a670 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ persistence. - Item locking mechanism to prevent conflicts - Hierarchical items (parent-child relationships) - **Item dependencies: Define prerequisites for completing items** +- **Date constraints: Set "not before" and "not after" times for item + completion** - SQLite database for data persistence - **Modern web frontend with React and TypeScript** - **Real-time updates across multiple browser tabs/windows** @@ -295,7 +297,9 @@ All API endpoints return JSON responses with a consistent format: "checked": false, "parent_id": null, "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", - "dependencies": [] + "dependencies": [], + "not_before": "2025-07-25T16:20:00Z", + "not_after": "2025-07-25T18:00:00Z" } } ``` @@ -312,7 +316,9 @@ All API endpoints return JSON responses with a consistent format: "checked": true, "parent_id": null, "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", - "dependencies": [2, 3] + "dependencies": [2, 3], + "not_before": "2025-07-25T16:20:00Z", + "not_after": "2025-07-25T18:00:00Z" } } ``` @@ -352,7 +358,9 @@ All API endpoints return JSON responses with a consistent format: "checked": false, "parent_id": null, "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", - "dependencies": [] + "dependencies": [], + "not_before": null, + "not_after": null }, { "id": 2, @@ -360,7 +368,9 @@ All API endpoints return JSON responses with a consistent format: "checked": true, "parent_id": null, "checklist_uuid": "123e4567-e89b-12d3-a456-426614174000", - "dependencies": [1] + "dependencies": [1], + "not_before": "2025-07-25T16:20:00Z", + "not_after": null } ] } @@ -376,7 +386,8 @@ All API endpoints return JSON responses with a consistent format: The application uses SQLite with the following schema: - `checklist_info` table: Stores checklist metadata (uuid, name) -- `items` table: Stores checklist items with hierarchical relationships +- `items` table: Stores checklist items with hierarchical relationships and date + constraints - `dependencies` table: Stores item dependencies (item_id, dependency_id) ## Real-time Features @@ -422,6 +433,47 @@ that must be completed before an item can be marked as done. - **Clear Feedback**: Users see exactly which items need to be completed first - **Error Prevention**: System prevents circular dependencies automatically +## Item Date Constraints + +The application supports date constraints, allowing you to set time windows when +items can be completed. + +### How Date Constraints Work + +- **Not Before**: Set the earliest time an item can be completed +- **Not After**: Set the latest time an item can be completed +- **Validation**: Items cannot be completed outside their allowed time window +- **Visual Indicators**: Items show date constraint status with color-coded + indicators +- **Real-time Updates**: Constraint status updates in real-time + +### Managing Date Constraints + +1. **Set Constraints**: Click the clock icon (🕐) on any item to open the date + constraint manager +2. **Configure Times**: Set "not before" and/or "not after" times +3. **Visual Feedback**: Items show constraint status: + - 🟢 Green: Time constraints met (shows "Ready") + - 🔴 Red: Time constraints not met (shows "Time locked") +4. **Completion Prevention**: Items cannot be checked off outside their time + window + +### Date Constraint Features + +- **Flexible Scheduling**: Set any combination of start and end times +- **ISO 8601 Format**: Uses standard datetime format for precision +- **Real-time Validation**: Constraint status updates immediately +- **Clear Feedback**: Users see exactly when items can be completed +- **Error Prevention**: System prevents completion outside allowed times + +### Example Use Cases + +- **Meeting Preparation**: Set items to be completed before a meeting starts +- **Deadline Management**: Set items to be completed by a specific deadline +- **Time-sensitive Tasks**: Ensure tasks are completed within business hours +- **Sequential Workflows**: Use in combination with dependencies for complex + workflows + ## Development ### Local Development diff --git a/frontend/src/components/ChecklistItem.tsx b/frontend/src/components/ChecklistItem.tsx index 3c57f5e..5860431 100644 --- a/frontend/src/components/ChecklistItem.tsx +++ b/frontend/src/components/ChecklistItem.tsx @@ -1,6 +1,7 @@ import { useState, useRef, useEffect } from 'react' import type { ChecklistItem as ChecklistItemType } from '../types' import DependencyManager from './DependencyManager' +import { DateConstraintManager } from './DateConstraintManager' interface ChecklistItemProps { item: ChecklistItemType @@ -24,6 +25,7 @@ export default function ChecklistItem({ const [isEditing, setIsEditing] = useState(false) const [isDeleting, setIsDeleting] = useState(false) const [isDependencyModalOpen, setIsDependencyModalOpen] = useState(false) + const [isDateConstraintModalOpen, setIsDateConstraintModalOpen] = useState(false) const [userName, setUserName] = useState('') const contentRef = useRef(null) @@ -36,8 +38,14 @@ export default function ChecklistItem({ return depItem?.checked }) ?? true - // Check if item can be completed (all dependencies met) - const canComplete = dependenciesCompleted || item.checked + // Check date constraints + const now = new Date() + 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) + + // Check if item can be completed (all dependencies met and date constraints met) + const canComplete = (dependenciesCompleted && dateConstraintsMet) || item.checked // Get dependency items for display const dependencyItems = item.dependencies?.map(depId => @@ -131,11 +139,24 @@ export default function ChecklistItem({ return } + // Check if date constraints are met + if (!dateConstraintsMet) { + let message = 'Cannot complete this item due to date constraints:\n\n' + if (notBeforeDate && now < notBeforeDate) { + message += `• Cannot complete before ${notBeforeDate.toLocaleString()}\n` + } + if (notAfterDate && now > notAfterDate) { + message += `• Cannot complete after ${notAfterDate.toLocaleString()}\n` + } + alert(message) + return + } + try { await onUpdate(item.id, { checked: true }) } catch (error) { console.error('Failed to toggle item:', error) - if (error instanceof Error && error.message.includes('dependency')) { + if (error instanceof Error && error.message.includes('cannot complete item:')) { alert(error.message) } } @@ -160,6 +181,15 @@ export default function ChecklistItem({ }, 100) } + const handleDateConstraintSave = async (notBefore?: string, notAfter?: string) => { + try { + await onUpdate(item.id, { not_before: notBefore, not_after: notAfter }) + setIsDateConstraintModalOpen(false) + } catch (error) { + console.error('Failed to update date constraints:', error) + } + } + return (
  • @@ -207,6 +237,19 @@ export default function ChecklistItem({ ⚠️ Depends on: {dependencyItems.map(dep => dep?.content).join(', ')}
    )} + + {/* Date constraint warning */} + {!item.checked && !dateConstraintsMet && ( +
    + ⏰ Date constraint: { + notBeforeDate && now < notBeforeDate + ? `Cannot complete before ${notBeforeDate.toLocaleString()}` + : notAfterDate && now > notAfterDate + ? `Cannot complete after ${notAfterDate.toLocaleString()}` + : 'Date constraint not met' + } +
    + )} {/* Actions */} @@ -227,6 +270,22 @@ export default function ChecklistItem({ )} + {/* Date constraint indicators */} + {(item.not_before || item.not_after) && ( +
    + + + + + {dateConstraintsMet ? 'Ready' : 'Time locked'} + +
    + )} + {isLocked && !isLockedByMe && ( @@ -247,6 +306,15 @@ export default function ChecklistItem({ +
  • ) } \ No newline at end of file diff --git a/frontend/src/components/DateConstraintManager.tsx b/frontend/src/components/DateConstraintManager.tsx new file mode 100644 index 0000000..46618fd --- /dev/null +++ b/frontend/src/components/DateConstraintManager.tsx @@ -0,0 +1,92 @@ +import React, { useState } from 'react' +import { ChecklistItem } from '../types' + +interface DateConstraintManagerProps { + item: ChecklistItem + onSave: (notBefore?: string, notAfter?: string) => void + onCancel: () => void +} + +export const DateConstraintManager: React.FC = ({ + item, + onSave, + onCancel +}) => { + const [notBefore, setNotBefore] = useState( + item.not_before ? new Date(item.not_before).toISOString().slice(0, 16) : '' + ) + const [notAfter, setNotAfter] = useState( + item.not_after ? new Date(item.not_after).toISOString().slice(0, 16) : '' + ) + + const handleSave = () => { + const notBeforeDate = notBefore ? new Date(notBefore).toISOString() : undefined + const notAfterDate = notAfter ? new Date(notAfter).toISOString() : undefined + onSave(notBeforeDate, notAfterDate) + } + + const handleClear = () => { + setNotBefore('') + setNotAfter('') + } + + return ( +
    +
    +

    Manage Date Constraints

    +

    + Set when this item can be completed. Leave empty to remove constraints. +

    + +
    +
    + + setNotBefore(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
    + +
    + + setNotAfter(e.target.value)} + className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" + /> +
    +
    + +
    + +
    + + +
    +
    +
    +
    + ) +} \ No newline at end of file diff --git a/frontend/src/types.ts b/frontend/src/types.ts index d0f1a0d..30eea99 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -8,6 +8,8 @@ export interface ChecklistItem { checklist_uuid: string children?: ChecklistItem[] dependencies?: number[] + not_before?: string + not_after?: string } export interface SavedChecklist { diff --git a/main.go b/main.go index d11eb35..808881c 100644 --- a/main.go +++ b/main.go @@ -37,6 +37,8 @@ type ChecklistItem struct { LockUntil *time.Time `json:"lock_until,omitempty"` Checklist string `json:"checklist_uuid"` Dependencies []int `json:"dependencies,omitempty"` + NotBefore *time.Time `json:"not_before,omitempty"` + NotAfter *time.Time `json:"not_after,omitempty"` } type ItemLock struct { @@ -78,6 +80,8 @@ func getChecklistDB(uuid string) (*sql.DB, error) { content TEXT NOT NULL, checked INTEGER NOT NULL, parent_id INTEGER, + not_before TEXT, + not_after TEXT, FOREIGN KEY(parent_id) REFERENCES items(id) );`, `CREATE TABLE IF NOT EXISTS dependencies ( @@ -173,7 +177,7 @@ func loadChecklistItems(uuid string) ([]ChecklistItem, error) { } defer db.Close() - rows, err := db.Query(`SELECT id, content, checked, parent_id FROM items`) + rows, err := db.Query(`SELECT id, content, checked, parent_id, not_before, not_after FROM items`) if err != nil { return nil, err } @@ -183,7 +187,9 @@ func loadChecklistItems(uuid string) ([]ChecklistItem, error) { var it ChecklistItem var checked int var parentID sql.NullInt64 - err = rows.Scan(&it.ID, &it.Content, &checked, &parentID) + var notBefore sql.NullString + var notAfter sql.NullString + err = rows.Scan(&it.ID, &it.Content, &checked, &parentID, ¬Before, ¬After) if err != nil { return nil, err } @@ -192,6 +198,16 @@ func loadChecklistItems(uuid string) ([]ChecklistItem, error) { v := int(parentID.Int64) it.ParentID = &v } + if notBefore.Valid { + if t, err := time.Parse(time.RFC3339, notBefore.String); err == nil { + it.NotBefore = &t + } + } + if notAfter.Valid { + if t, err := time.Parse(time.RFC3339, notAfter.String); err == nil { + it.NotAfter = &t + } + } it.Checklist = uuid // Load dependencies for this item @@ -227,15 +243,25 @@ func addChecklist(name string) (string, error) { return uuidStr, err } -func addItem(uuid, content string, parentID *int) (ChecklistItem, error) { +func addItem(uuid, content string, parentID *int, notBefore *time.Time, notAfter *time.Time) (ChecklistItem, error) { db, err := getChecklistDB(uuid) if err != nil { return ChecklistItem{}, err } defer db.Close() - res, err := db.Exec(`INSERT INTO items (content, checked, parent_id) VALUES (?, 0, ?)`, - content, parentID) + var notBeforeStr, notAfterStr *string + if notBefore != nil { + s := notBefore.Format(time.RFC3339) + notBeforeStr = &s + } + if notAfter != nil { + s := notAfter.Format(time.RFC3339) + notAfterStr = &s + } + + res, err := db.Exec(`INSERT INTO items (content, checked, parent_id, not_before, not_after) VALUES (?, 0, ?, ?, ?)`, + content, parentID, notBeforeStr, notAfterStr) if err != nil { return ChecklistItem{}, err } @@ -245,11 +271,13 @@ func addItem(uuid, content string, parentID *int) (ChecklistItem, error) { Content: content, Checked: false, ParentID: parentID, + NotBefore: notBefore, + NotAfter: notAfter, Checklist: uuid, }, nil } -func updateItem(uuid string, id int, content *string, checked *bool, parentID *int, dependencies *[]int) (ChecklistItem, error) { +func updateItem(uuid string, id int, content *string, checked *bool, parentID *int, dependencies *[]int, notBefore *time.Time, notAfter *time.Time) (ChecklistItem, error) { log.Printf("updateItem called with uuid: %s, id: %d", uuid, id) log.Printf("Parameters: content=%v, checked=%v, parentID=%v, dependencies=%v", content, checked, parentID, dependencies) @@ -262,7 +290,7 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i log.Printf("Database connection successful for uuid: %s", uuid) - // If trying to check an item, validate dependencies first + // If trying to check an item, validate dependencies and date constraints first if checked != nil && *checked { log.Printf("Validating dependencies for item %d", id) deps, err := loadItemDependencies(db, id) @@ -283,8 +311,32 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i return ChecklistItem{}, fmt.Errorf("cannot complete item: dependency %d is not completed", depID) } } + + // Validate date constraints + now := time.Now() + var notBeforeStr, notAfterStr sql.NullString + err = db.QueryRow(`SELECT not_before, not_after FROM items WHERE id = ?`, id).Scan(¬BeforeStr, ¬AfterStr) + if err != nil { + log.Printf("Failed to get date constraints: %v", err) + return ChecklistItem{}, err + } + + if notBeforeStr.Valid { + if notBefore, err := time.Parse(time.RFC3339, notBeforeStr.String); err == nil { + if now.Before(notBefore) { + return ChecklistItem{}, fmt.Errorf("cannot complete item: not before %s", notBefore.Format("2006-01-02 15:04:05")) + } + } + } + if notAfterStr.Valid { + if notAfter, err := time.Parse(time.RFC3339, notAfterStr.String); err == nil { + if now.After(notAfter) { + return ChecklistItem{}, fmt.Errorf("cannot complete item: not after %s", notAfter.Format("2006-01-02 15:04:05")) + } + } + } } else { - log.Printf("Skipping dependency validation - checked is %v", checked) + log.Printf("Skipping dependency and date validation - checked is %v", checked) } log.Printf("About to check dependencies parameter") @@ -331,6 +383,14 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i set = append(set, "parent_id = ?") args = append(args, *parentID) } + if notBefore != nil { + set = append(set, "not_before = ?") + args = append(args, notBefore.Format(time.RFC3339)) + } + if notAfter != nil { + set = append(set, "not_after = ?") + args = append(args, notAfter.Format(time.RFC3339)) + } if len(set) > 0 { q := "UPDATE items SET " + strings.Join(set, ", ") + " WHERE id = ?" args = append(args, id) @@ -347,7 +407,7 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i // Return updated item log.Printf("Querying updated item %d", id) - rows, err := db.Query(`SELECT id, content, checked, parent_id FROM items WHERE id = ?`, id) + rows, err := db.Query(`SELECT id, content, checked, parent_id, not_before, not_after FROM items WHERE id = ?`, id) if err != nil { log.Printf("Failed to query updated item: %v", err) return ChecklistItem{}, err @@ -358,7 +418,9 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i var it ChecklistItem var checkedInt int var parentID sql.NullInt64 - err = rows.Scan(&it.ID, &it.Content, &checkedInt, &parentID) + var notBefore sql.NullString + var notAfter sql.NullString + err = rows.Scan(&it.ID, &it.Content, &checkedInt, &parentID, ¬Before, ¬After) if err != nil { log.Printf("Failed to scan updated item: %v", err) return ChecklistItem{}, err @@ -368,6 +430,16 @@ func updateItem(uuid string, id int, content *string, checked *bool, parentID *i v := int(parentID.Int64) it.ParentID = &v } + if notBefore.Valid { + if t, err := time.Parse(time.RFC3339, notBefore.String); err == nil { + it.NotBefore = &t + } + } + if notAfter.Valid { + if t, err := time.Parse(time.RFC3339, notAfter.String); err == nil { + it.NotAfter = &t + } + } it.Checklist = uuid // Load dependencies @@ -477,15 +549,17 @@ func handleAddItem(w http.ResponseWriter, r *http.Request) { } type Req struct { - Content string `json:"content"` - ParentID *int `json:"parent_id"` + Content string `json:"content"` + ParentID *int `json:"parent_id"` + NotBefore *time.Time `json:"not_before"` + NotAfter *time.Time `json:"not_after"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil || strings.TrimSpace(req.Content) == "" { http.Error(w, "Missing content", 400) return } - item, err := addItem(uuid, req.Content, req.ParentID) + item, err := addItem(uuid, req.Content, req.ParentID, req.NotBefore, req.NotAfter) if err != nil { http.Error(w, "Failed to add item", 500) return @@ -521,19 +595,21 @@ func handleUpdateItem(w http.ResponseWriter, r *http.Request) { } type Req struct { - Content *string `json:"content"` - Checked *bool `json:"checked"` - ParentID *int `json:"parent_id"` - Dependencies *[]int `json:"dependencies"` + Content *string `json:"content"` + Checked *bool `json:"checked"` + ParentID *int `json:"parent_id"` + Dependencies *[]int `json:"dependencies"` + NotBefore *time.Time `json:"not_before"` + NotAfter *time.Time `json:"not_after"` } var req Req if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "Bad body", 400) return } - item, err := updateItem(uuid, id, req.Content, req.Checked, req.ParentID, req.Dependencies) + item, err := updateItem(uuid, id, req.Content, req.Checked, req.ParentID, req.Dependencies, req.NotBefore, req.NotAfter) if err != nil { - if strings.Contains(err.Error(), "cannot complete item: dependency") { + if strings.Contains(err.Error(), "cannot complete item:") { http.Error(w, err.Error(), 400) } else { http.Error(w, "Not found", 404)