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 <noreply@anthropic.com>
This commit is contained in:
parent
233b20efa6
commit
ae79ad12c3
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ type Folder struct {
|
|||
ParentID string
|
||||
Name string
|
||||
Description string
|
||||
SortOrder int
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
|
|||
}
|
||||
if profile.Role != "buyer" {
|
||||
<div class="mt-4 pt-3 border-t border-gray-800">
|
||||
<button class="w-full flex items-center justify-center gap-1.5 py-1.5 px-2 rounded text-sm text-gray-400 hover:text-teal-400 hover:bg-gray-800 transition-colors">
|
||||
<button onclick="document.getElementById('newFolderModal').classList.remove('hidden')" class="w-full flex items-center justify-center gap-1.5 py-1.5 px-2 rounded text-sm text-gray-400 hover:text-teal-400 hover:bg-gray-800 transition-colors">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
|
||||
New Folder
|
||||
</button>
|
||||
|
|
@ -207,6 +207,34 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- New Folder Modal -->
|
||||
<div id="newFolderModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('newFolderModal').classList.add('hidden')"></div>
|
||||
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-sm mx-4 p-6">
|
||||
<h2 class="text-lg font-bold mb-4">New Folder</h2>
|
||||
<form action="/deals/folders/create" method="POST" class="space-y-4">
|
||||
<input type="hidden" name="deal_id" value={ deal.ID }/>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Name</label>
|
||||
<input type="text" name="name" required class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none"/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-medium text-gray-400 mb-1">Parent Folder</label>
|
||||
<select name="parent_id" class="w-full px-3 py-2 bg-gray-800 border border-gray-700 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none">
|
||||
<option value="">Root</option>
|
||||
for _, f := range folders {
|
||||
<option value={ f.ID }>{ f.Name }</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button type="button" onclick="document.getElementById('newFolderModal').classList.add('hidden')" class="px-4 py-2 text-sm text-gray-400 border border-gray-700 rounded-lg hover:bg-gray-800 transition">Cancel</button>
|
||||
<button type="submit" class="px-4 py-2 bg-teal-500 text-white text-sm font-medium rounded-lg hover:bg-teal-600 transition">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function showTab(name) {
|
||||
document.getElementById('panel-documents').style.display = name === 'documents' ? '' : 'none';
|
||||
|
|
|
|||
Loading…
Reference in New Issue