feat: search within deal room
Add debounced search input in deal detail header. HTMX-powered search across files, folders, and request items. Returns categorized results with links. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2d6e9fc79a
commit
cc9e7eeff1
|
|
@ -302,6 +302,83 @@ func (h *Handler) handleFolderReorder(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (h *Handler) handleDealSearch(w http.ResponseWriter, r *http.Request) {
|
||||
// /deals/search/{dealID}?q=...
|
||||
dealID := strings.TrimPrefix(r.URL.Path, "/deals/search/")
|
||||
q := r.URL.Query().Get("q")
|
||||
|
||||
if q == "" {
|
||||
w.Write([]byte(`<p class="text-sm text-gray-500 italic">Type to search files, folders, and requests.</p>`))
|
||||
return
|
||||
}
|
||||
|
||||
pattern := "%" + q + "%"
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
html := `<div class="space-y-3">`
|
||||
|
||||
// Search files
|
||||
rows, _ := h.db.Query("SELECT id, name, folder_id FROM files WHERE deal_id = ? AND name LIKE ?", dealID, pattern)
|
||||
if rows != nil {
|
||||
hasFiles := false
|
||||
for rows.Next() {
|
||||
if !hasFiles {
|
||||
html += `<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider">Files</h3>`
|
||||
hasFiles = true
|
||||
}
|
||||
var id, name, folderID string
|
||||
rows.Scan(&id, &name, &folderID)
|
||||
html += fmt.Sprintf(`<div class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-800/50">
|
||||
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
|
||||
<a href="/deals/files/download/%s" class="text-sm text-teal-400 hover:underline">%s</a>
|
||||
</div>`, id, name)
|
||||
}
|
||||
rows.Close()
|
||||
}
|
||||
|
||||
// Search folders
|
||||
rows2, _ := h.db.Query("SELECT id, name FROM folders WHERE deal_id = ? AND name LIKE ?", dealID, pattern)
|
||||
if rows2 != nil {
|
||||
hasFolders := false
|
||||
for rows2.Next() {
|
||||
if !hasFolders {
|
||||
html += `<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mt-3">Folders</h3>`
|
||||
hasFolders = true
|
||||
}
|
||||
var id, name string
|
||||
rows2.Scan(&id, &name)
|
||||
html += fmt.Sprintf(`<div class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-800/50">
|
||||
<svg class="w-4 h-4 text-teal-400/70" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z"></path></svg>
|
||||
<a href="/deals/%s?folder=%s" class="text-sm">%s</a>
|
||||
</div>`, dealID, id, name)
|
||||
}
|
||||
rows2.Close()
|
||||
}
|
||||
|
||||
// Search requests
|
||||
rows3, _ := h.db.Query("SELECT id, item_number, description FROM diligence_requests WHERE deal_id = ? AND description LIKE ?", dealID, pattern)
|
||||
if rows3 != nil {
|
||||
hasReqs := false
|
||||
for rows3.Next() {
|
||||
if !hasReqs {
|
||||
html += `<h3 class="text-xs font-medium text-gray-500 uppercase tracking-wider mt-3">Request Items</h3>`
|
||||
hasReqs = true
|
||||
}
|
||||
var id, itemNum, desc string
|
||||
rows3.Scan(&id, &itemNum, &desc)
|
||||
html += fmt.Sprintf(`<div class="flex items-center gap-2 p-2 rounded-lg hover:bg-gray-800/50">
|
||||
<svg class="w-4 h-4 text-amber-400/70" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"></path></svg>
|
||||
<span class="text-xs text-gray-500">%s</span>
|
||||
<span class="text-sm">%s</span>
|
||||
</div>`, itemNum, desc)
|
||||
}
|
||||
rows3.Close()
|
||||
}
|
||||
|
||||
html += `</div>`
|
||||
w.Write([]byte(html))
|
||||
}
|
||||
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -87,6 +87,7 @@ mux.HandleFunc("/auth/logout", h.handleLogout)
|
|||
mux.HandleFunc("/deals/files/delete", h.requireAuth(h.handleFileDelete))
|
||||
mux.HandleFunc("/deals/files/download/", h.requireAuth(h.handleFileDownload))
|
||||
mux.HandleFunc("/deals/files/comments/", h.requireAuth(h.handleFileComments))
|
||||
mux.HandleFunc("/deals/search/", h.requireAuth(h.handleDealSearch))
|
||||
|
||||
// HTMX partials
|
||||
mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment))
|
||||
|
|
|
|||
|
|
@ -21,6 +21,17 @@ templ DealRoomDetail(profile *model.Profile, deal *model.Deal, folders []*model.
|
|||
<p class="text-sm text-gray-500 mt-1">{ deal.TargetCompany } · { deal.Description }</p>
|
||||
</div>
|
||||
|
||||
<!-- Search -->
|
||||
<div class="relative">
|
||||
<input type="text" placeholder="Search files, folders, requests..."
|
||||
hx-get={ fmt.Sprintf("/deals/search/%s", deal.ID) }
|
||||
hx-trigger="input changed delay:300ms"
|
||||
hx-target="#searchResults"
|
||||
name="q"
|
||||
class="w-full px-4 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-gray-100 focus:border-teal-500 focus:outline-none placeholder-gray-600"/>
|
||||
<div id="searchResults" class="mt-2"></div>
|
||||
</div>
|
||||
|
||||
<!-- Deal Info Cards -->
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="bg-gray-900 rounded-lg border border-gray-800 p-4">
|
||||
|
|
|
|||
Loading…
Reference in New Issue