From ae79ad12c3e7d092b4a6ba8554b1f523e2083ead Mon Sep 17 00:00:00 2001 From: James Date: Mon, 23 Feb 2026 02:51:04 -0500 Subject: [PATCH] feat: deal room folder management Add folder create, rename, delete, and reorder handlers. Add New Folder modal to deal room detail with parent folder selector. Add sort_order field to folders for drag-to-reorder. Order folders by sort_order in folder tree. Co-Authored-By: Claude Opus 4.6 --- internal/db/migrate.go | 2 + internal/handler/deals.go | 92 ++++++++++++++++++++++++++++++++++++- internal/handler/handler.go | 6 ++- internal/model/models.go | 1 + templates/dealroom.templ | 30 +++++++++++- 5 files changed, 128 insertions(+), 3 deletions(-) diff --git a/internal/db/migrate.go b/internal/db/migrate.go index 4f833ce..9953519 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -229,6 +229,8 @@ var additiveMigrationStmts = []string{ `ALTER TABLE deals ADD COLUMN buyer_can_comment INTEGER DEFAULT 1`, `ALTER TABLE deals ADD COLUMN seller_can_comment INTEGER DEFAULT 1`, `ALTER TABLE profiles ADD COLUMN buyer_group TEXT DEFAULT ''`, + // Section 6: folder sort order + `ALTER TABLE folders ADD COLUMN sort_order INTEGER DEFAULT 0`, } func seed(db *sql.DB) error { diff --git a/internal/handler/deals.go b/internal/handler/deals.go index c0ce8e7..6c45027 100644 --- a/internal/handler/deals.go +++ b/internal/handler/deals.go @@ -212,6 +212,96 @@ func (h *Handler) handleFileStatusUpdate(w http.ResponseWriter, r *http.Request, json.NewEncoder(w).Encode(map[string]string{"status": req.Status}) } +func (h *Handler) handleFolderCreate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + profile := getProfile(r.Context()) + dealID := r.FormValue("deal_id") + name := strings.TrimSpace(r.FormValue("name")) + parentID := r.FormValue("parent_id") + + if dealID == "" || name == "" { + http.Error(w, "Deal ID and name are required", 400) + return + } + + folderID := generateID("folder") + h.db.Exec("INSERT INTO folders (id, deal_id, parent_id, name, created_by) VALUES (?, ?, ?, ?, ?)", + folderID, dealID, parentID, name, profile.ID) + + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + +func (h *Handler) handleFolderRename(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + folderID := r.FormValue("folder_id") + dealID := r.FormValue("deal_id") + name := strings.TrimSpace(r.FormValue("name")) + + if folderID == "" || name == "" { + http.Error(w, "Missing fields", 400) + return + } + + h.db.Exec("UPDATE folders SET name = ? WHERE id = ?", name, folderID) + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + +func (h *Handler) handleFolderDelete(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + folderID := r.FormValue("folder_id") + dealID := r.FormValue("deal_id") + + if folderID == "" { + http.Error(w, "Missing folder ID", 400) + return + } + + // Delete child folders + h.db.Exec("DELETE FROM folders WHERE parent_id = ?", folderID) + // Move files to root + h.db.Exec("UPDATE files SET folder_id = '' WHERE folder_id = ?", folderID) + h.db.Exec("DELETE FROM folders WHERE id = ?", folderID) + + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + +func (h *Handler) handleFolderReorder(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + folderID := r.FormValue("folder_id") + dealID := r.FormValue("deal_id") + direction := r.FormValue("direction") + + if folderID == "" { + http.Error(w, "Missing folder ID", 400) + return + } + + var currentOrder int + h.db.QueryRow("SELECT COALESCE(sort_order, 0) FROM folders WHERE id = ?", folderID).Scan(¤tOrder) + + if direction == "up" { + h.db.Exec("UPDATE folders SET sort_order = sort_order + 1 WHERE deal_id = ? AND COALESCE(sort_order, 0) = ?", dealID, currentOrder-1) + h.db.Exec("UPDATE folders SET sort_order = ? WHERE id = ?", currentOrder-1, folderID) + } else { + h.db.Exec("UPDATE folders SET sort_order = sort_order - 1 WHERE deal_id = ? AND COALESCE(sort_order, 0) = ?", dealID, currentOrder+1) + h.db.Exec("UPDATE folders SET sort_order = ? WHERE id = ?", currentOrder+1, folderID) + } + + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + func (h *Handler) getLastActivityByDeal(dealID string) *time.Time { var t time.Time err := h.db.QueryRow("SELECT MAX(created_at) FROM deal_activity WHERE deal_id = ?", dealID).Scan(&t) @@ -241,7 +331,7 @@ func (h *Handler) getDeals(profile *model.Profile) []*model.Deal { } func (h *Handler) getFolders(dealID string) []*model.Folder { - rows, err := h.db.Query("SELECT id, deal_id, parent_id, name, description FROM folders WHERE deal_id = ? ORDER BY name", dealID) + rows, err := h.db.Query("SELECT id, deal_id, parent_id, name, description FROM folders WHERE deal_id = ? ORDER BY COALESCE(sort_order, 0), name", dealID) if err != nil { return nil } diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 47536a5..f99eab8 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -75,8 +75,12 @@ mux.HandleFunc("/auth/logout", h.handleLogout) mux.HandleFunc("/invites/accept", h.handleInviteAcceptPage) mux.HandleFunc("/invites/accept-submit", h.handleInviteAccept) - // Deal creation + // Deal creation & folder management mux.HandleFunc("/deals/create", h.requireAuth(h.handleCreateDeal)) + mux.HandleFunc("/deals/folders/create", h.requireAuth(h.handleFolderCreate)) + mux.HandleFunc("/deals/folders/rename", h.requireAuth(h.handleFolderRename)) + mux.HandleFunc("/deals/folders/delete", h.requireAuth(h.handleFolderDelete)) + mux.HandleFunc("/deals/folders/reorder", h.requireAuth(h.handleFolderReorder)) // HTMX partials mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment)) diff --git a/internal/model/models.go b/internal/model/models.go index d23403f..ac8e386 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -67,6 +67,7 @@ type Folder struct { ParentID string Name string Description string + SortOrder int CreatedBy string CreatedAt time.Time } diff --git a/templates/dealroom.templ b/templates/dealroom.templ index 0db7852..3f0e06b 100644 --- a/templates/dealroom.templ +++ b/templates/dealroom.templ @@ -95,7 +95,7 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model. } if profile.Role != "buyer" {
- @@ -207,6 +207,34 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
+ + +