package database import ( "database/sql" "fmt" "log" "os" "strings" "time" "github.com/google/uuid" _ "github.com/mattn/go-sqlite3" "gocheck/models" ) func GetChecklistDB(uuid string) (*sql.DB, error) { // Ensure data directory exists if err := os.MkdirAll("data", 0755); err != nil { return nil, err } dbPath := fmt.Sprintf("data/%s.db", uuid) db, err := sql.Open("sqlite3", dbPath) if err != nil { return nil, err } // Setup schema for this checklist queries := []string{ `CREATE TABLE IF NOT EXISTS checklist_info ( uuid TEXT PRIMARY KEY, name TEXT NOT NULL );`, `CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, 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 ( item_id INTEGER NOT NULL, dependency_id INTEGER NOT NULL, PRIMARY KEY (item_id, dependency_id), FOREIGN KEY(item_id) REFERENCES items(id) ON DELETE CASCADE, FOREIGN KEY(dependency_id) REFERENCES items(id) ON DELETE CASCADE, CHECK(item_id != dependency_id) );`, } for _, q := range queries { if _, err := db.Exec(q); err != nil { db.Close() return nil, err } } return db, nil } func LoadItemDependencies(db *sql.DB, itemID int) ([]int, error) { rows, err := db.Query(`SELECT dependency_id FROM dependencies WHERE item_id = ?`, itemID) if err != nil { return nil, err } defer rows.Close() var deps []int for rows.Next() { var depID int err = rows.Scan(&depID) if err != nil { return nil, err } deps = append(deps, depID) } return deps, nil } // EnsureChecklistExists creates a checklist if it doesn't exist func EnsureChecklistExists(uuid string) error { db, err := GetChecklistDB(uuid) if err != nil { return err } defer db.Close() // Check if checklist_info table has any data var count int err = db.QueryRow(`SELECT COUNT(*) FROM checklist_info`).Scan(&count) if err != nil { return err } // If no checklist exists, create one with a default name if count == 0 { _, err = db.Exec(`INSERT INTO checklist_info (uuid, name) VALUES (?, ?)`, uuid, "Untitled Checklist") if err != nil { return err } } return nil } func LoadChecklistName(uuid string) (string, error) { db, err := GetChecklistDB(uuid) if err != nil { return "", err } defer db.Close() rows, err := db.Query(`SELECT name FROM checklist_info WHERE uuid = ?`, uuid) if err != nil { return "", err } defer rows.Close() if rows.Next() { var name string err = rows.Scan(&name) if err != nil { return "", err } return name, nil } return "", fmt.Errorf("not found") } func LoadChecklistItems(uuid string, itemLocks map[int]*models.ItemLock) ([]models.ChecklistItem, error) { db, err := GetChecklistDB(uuid) if err != nil { return nil, err } defer db.Close() rows, err := db.Query(`SELECT id, content, checked, parent_id, not_before, not_after FROM items`) if err != nil { return nil, err } defer rows.Close() var items []models.ChecklistItem for rows.Next() { var it models.ChecklistItem var checked int var parentID sql.NullInt64 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 } it.Checked = checked != 0 if parentID.Valid { 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 deps, err := LoadItemDependencies(db, it.ID) if err != nil { return nil, err } it.Dependencies = deps // Attach lock info if present if lock, ok := itemLocks[it.ID]; ok && lock.Expires.After(time.Now()) { it.LockedBy = &lock.LockedBy t := lock.Expires it.LockUntil = &t } items = append(items, it) } return items, nil } func AddChecklist(name string) (string, error) { uuidStr := generateUUID() db, err := GetChecklistDB(uuidStr) if err != nil { return "", err } defer db.Close() _, err = db.Exec(`INSERT INTO checklist_info (uuid, name) VALUES (?, ?)`, uuidStr, name) return uuidStr, err } func UpdateChecklistName(uuid string, name string) error { db, err := GetChecklistDB(uuid) if err != nil { return err } defer db.Close() _, err = db.Exec(`UPDATE checklist_info SET name = ? WHERE uuid = ?`, name, uuid) return err } func AddItem(uuid, content string, parentID *int, notBefore *time.Time, notAfter *time.Time) (models.ChecklistItem, error) { db, err := GetChecklistDB(uuid) if err != nil { return models.ChecklistItem{}, err } defer db.Close() 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 models.ChecklistItem{}, err } id, _ := res.LastInsertId() return models.ChecklistItem{ ID: int(id), 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, notBefore *time.Time, notAfter *time.Time) (models.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) db, err := GetChecklistDB(uuid) if err != nil { log.Printf("Failed to get database: %v", err) return models.ChecklistItem{}, err } defer db.Close() log.Printf("Database connection successful for uuid: %s", uuid) // 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) if err != nil { log.Printf("Failed to load dependencies: %v", err) return models.ChecklistItem{}, err } // Check if all dependencies are completed for _, depID := range deps { var depChecked int err = db.QueryRow(`SELECT checked FROM items WHERE id = ?`, depID).Scan(&depChecked) if err != nil { log.Printf("Failed to check dependency %d: %v", depID, err) return models.ChecklistItem{}, err } if depChecked == 0 { return models.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 models.ChecklistItem{}, err } if notBeforeStr.Valid { if notBefore, err := time.Parse(time.RFC3339, notBeforeStr.String); err == nil { if now.Before(notBefore) { return models.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 models.ChecklistItem{}, fmt.Errorf("cannot complete item: not after %s", notAfter.Format("2006-01-02 15:04:05")) } } } } else { log.Printf("Skipping dependency and date validation - checked is %v", checked) } log.Printf("About to check dependencies parameter") // Update dependencies if provided if dependencies != nil { log.Printf("Updating dependencies for item %d: %v", id, *dependencies) // Delete existing dependencies _, err = db.Exec(`DELETE FROM dependencies WHERE item_id = ?`, id) if err != nil { log.Printf("Failed to delete existing dependencies: %v", err) return models.ChecklistItem{}, err } // Add new dependencies for _, depID := range *dependencies { log.Printf("Adding dependency %d for item %d", depID, id) _, err = db.Exec(`INSERT INTO dependencies (item_id, dependency_id) VALUES (?, ?)`, id, depID) if err != nil { log.Printf("Failed to add dependency %d: %v", depID, err) return models.ChecklistItem{}, err } } log.Printf("Dependencies updated successfully") } else { log.Printf("No dependencies to update") } log.Printf("About to build SQL update query") set := []string{} args := []interface{}{} if content != nil { set = append(set, "content = ?") args = append(args, *content) } if checked != nil { set = append(set, "checked = ?") if *checked { args = append(args, 1) } else { args = append(args, 0) } } if parentID != nil { 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) log.Printf("SQL query: %s with args: %v", q, args) _, err = db.Exec(q, args...) if err != nil { log.Printf("Failed to execute SQL update: %v", err) return models.ChecklistItem{}, err } log.Printf("SQL update executed successfully") } else { log.Printf("No fields to update in SQL, skipping update query") } // Return updated item log.Printf("Querying updated item %d", 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 models.ChecklistItem{}, err } defer rows.Close() if rows.Next() { log.Printf("Found item %d in database", id) var it models.ChecklistItem var checkedInt int var parentID sql.NullInt64 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 models.ChecklistItem{}, err } it.Checked = checkedInt != 0 if parentID.Valid { 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 deps, err := LoadItemDependencies(db, it.ID) if err != nil { log.Printf("Failed to load dependencies for return: %v", err) return models.ChecklistItem{}, err } it.Dependencies = deps log.Printf("Successfully updated item %d", id) return it, nil } log.Printf("Item %d not found in database", id) return models.ChecklistItem{}, fmt.Errorf("not found") } func DeleteItem(uuid string, id int) error { db, err := GetChecklistDB(uuid) if err != nil { return err } defer db.Close() // Delete dependencies that reference this item _, err = db.Exec(`DELETE FROM dependencies WHERE dependency_id = ?`, id) if err != nil { return err } // Delete the item itself _, err = db.Exec(`DELETE FROM items WHERE id = ?`, id) return err } // generateUUID generates a new UUID func generateUUID() string { return uuid.New().String() }