From 24f4702f060b5a46ca24f3d4709eb265b2e48aef Mon Sep 17 00:00:00 2001 From: James Date: Tue, 24 Feb 2026 03:05:36 -0500 Subject: [PATCH] feat: Misha requests 2026-02-24 - Dashboard: replace buckets with stage-based counts (Prospect/Internal/Initial Marketing/IOI/LOI) - New room modal: industry typeahead, stage dropdown, email chip input for deal team - New room flow: Add Users screen with permission groups (admin/contributor/viewer variants) - New room flow: per-group folder visibility checkboxes - New room flow: Folder Structure step (Excel import or manual build with saved templates) - Deal room: edit deal info inline - Deal room: drag-to-reorder folders + right-click up/down - Deal room: context-aware folder pre-selection on upload - Deal room: drag-and-drop file upload (modal + direct folder drop) - Request list: upload with folder structure preview/choice - Request list: auto-assign existing files to requests - Request list: group targeting + multi-list support (replace/append/group-specific) --- MISHA_REQUESTS_2026_02_24.md | 96 ++++++++++ internal/db/migrate.go | 8 +- internal/handler/admin.go | 2 +- internal/handler/deals.go | 136 ++++++++++++- internal/handler/handler.go | 10 + internal/handler/requests.go | 187 ++++++++++++++++++ internal/model/models.go | 27 ++- templates/admin.templ | 11 +- templates/components.templ | 10 +- templates/dashboard.templ | 347 +++++++++++++++++++++++++++------ templates/dealroom.templ | 362 +++++++++++++++++++++++++++++++---- templates/dealrooms.templ | 2 +- 12 files changed, 1071 insertions(+), 127 deletions(-) create mode 100644 MISHA_REQUESTS_2026_02_24.md diff --git a/MISHA_REQUESTS_2026_02_24.md b/MISHA_REQUESTS_2026_02_24.md new file mode 100644 index 0000000..5d0bf06 --- /dev/null +++ b/MISHA_REQUESTS_2026_02_24.md @@ -0,0 +1,96 @@ +# Misha's Feature Requests — 2026-02-24 + +## 1. Dashboard + +### a. Overview Buckets (top) +- **Keep:** Active Rooms +- **Replace other buckets with:** + - Prospect + - Internal + - Initial Marketing + - IOI + - LOI + +## 2. Creating a New Room (modal/popup) + +### a. Industry Selection +- Pre-populated list of standard industries (user can scroll or type to filter) +- Ability to add custom/internal-to-firm industries as a new category in the dropdown + +### b. Stage Categories +- Prospect +- Internal +- Initial Marketing +- IOI +- LOI +- Closed + +### c. Folder Structure +- **Remove from this popup** — set up after initial dataroom is created + +### d. Internal Deal Team (was "Initial Team") +- Rename field to "Internal Deal Team" +- Email tag input: user types email → becomes a tag/chip object → can type next email +- Auto-suggest contacts from the firm as user types (match against existing firm users) +- Click suggestion to add without typing full email + +### e. Replace "Create Dataroom" button with "Add Users" button +- This button advances to the next screen (user permissions setup) + +#### Add Users Screen: +1. **Create groups with roles:** + - Administrator — same permissions as creator + - Contributor — can upload documents + - Viewer (no downloads) — can only view with watermark, cannot download + - Viewer (downloads w/ watermark) — can view and download with watermark + - Viewer (downloads no watermark) — full view and download, no restrictions + +2. **Folder/document visibility per group:** + - Checkbox tree next to each folder/document + - Creator selects which folders/docs each group can see + +### f. After "Add Users" → "Folder Structure" button (bottom right has Back + Folder Structure) + +#### Folder Structure Screen — Two Paths: +**Path 1: Upload Request List (Excel)** +- User uploads an Excel file +- System auto-generates folder structure from it +- User can edit the generated structure before finalizing +- System helps user fix/re-map if Excel format is not recognized correctly + +**Path 2: Build Own Folder Structure** +- User manually creates folders +- Once built, user can **save the structure under a name** for reuse in future datarooms +- All folder structures are editable after dataroom creation (in the dataroom view) + +## 3. Deal Rooms — Within Each Deal + +### a. Document Side + +1. **Edit deal info** — ability to modify the deal info that was entered during creation + +2. **Drag-to-reorder folders** — grab and drag to change priority order; also right-click → move up/move down + +3. **Context-aware folder pre-selection on upload:** + - If user clicked into a folder and then clicks "Upload Document", the folder selector should pre-select that folder + - User can still change it manually + +4. **Drag-and-drop file upload:** + - Upload modal should support drag-and-drop in addition to file picker + - Clicking into a folder: if user has upload permission, they can drag-and-drop files directly into the folder view (no need to click "Upload Document" button) + +### b. Request List Side + +1. **Upload a request/diligence list:** + - User gets choice: convert folder structure to match the request list, OR keep original folder structure + - Show a preview of what the new folder structure would look like before user decides + +2. **Auto-assign existing files to requests:** + - If files already exist in the dataroom when a request list is uploaded, offer to auto-assign existing files to matching request line items + - User reviews and corrects assignments before confirming + +3. **Request list targeting:** + - When uploading, user selects: all groups OR a specific group + - Support multiple request list uploads: + - Options: replace existing list, add to existing list, or create a group-specific list + - One main request list for all groups + separate group-specific lists can coexist diff --git a/internal/db/migrate.go b/internal/db/migrate.go index f11e554..9b3c416 100644 --- a/internal/db/migrate.go +++ b/internal/db/migrate.go @@ -78,7 +78,7 @@ CREATE TABLE IF NOT EXISTS deals ( name TEXT NOT NULL, description TEXT DEFAULT '', target_company TEXT DEFAULT '', - stage TEXT NOT NULL DEFAULT 'pipeline' CHECK (stage IN ('pipeline','loi','initial_review','due_diligence','final_negotiation','closed','dead')), + stage TEXT NOT NULL DEFAULT 'prospect' CHECK (stage IN ('prospect','internal','initial_marketing','ioi','loi','closed','pipeline','initial_review','due_diligence','final_negotiation','dead')), deal_size REAL DEFAULT 0, currency TEXT DEFAULT 'USD', ioi_date TEXT DEFAULT '', @@ -271,10 +271,10 @@ func seed(db *sql.DB) error { // Deals `INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by) VALUES - ('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'due_diligence', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'admin-misha'), - ('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_review', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'admin-misha'), + ('deal-1', 'org-1', 'Project Aurora', 'AI-powered enterprise SaaS platform acquisition', 'TechNova Solutions', 'ioi', 45000000, 'USD', '2025-12-15', '2026-01-10', '2026-03-15', '2026-04-30', 72, 'admin-misha'), + ('deal-2', 'org-1', 'Project Beacon', 'Healthcare technology platform investment', 'MedFlow Health', 'initial_marketing', 28000000, 'USD', '2026-01-05', '', '', '2026-06-15', 45, 'admin-misha'), ('deal-3', 'org-1', 'Project Cascade', 'Fintech payment processing acquisition', 'PayStream Inc', 'loi', 62000000, 'USD', '2025-11-20', '2026-02-01', '2026-04-01', '2026-05-15', 58, 'admin-misha'), - ('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'pipeline', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'admin-misha')`, + ('deal-4', 'org-1', 'Project Delta', 'Industrial IoT platform strategic investment', 'SensorGrid Corp', 'prospect', 15000000, 'USD', '', '', '', '2026-08-01', 30, 'admin-misha')`, // Folders for deal-1 (Project Aurora) `INSERT INTO folders (id, deal_id, parent_id, name, description) VALUES diff --git a/internal/handler/admin.go b/internal/handler/admin.go index 2bfe93d..4b9d101 100644 --- a/internal/handler/admin.go +++ b/internal/handler/admin.go @@ -223,7 +223,7 @@ func (h *Handler) handleAdminDealSave(w http.ResponseWriter, r *http.Request) { currency = "USD" } if stage == "" { - stage = "pipeline" + stage = "prospect" } if id == "" { diff --git a/internal/handler/deals.go b/internal/handler/deals.go index e763cef..ae0b6b9 100644 --- a/internal/handler/deals.go +++ b/internal/handler/deals.go @@ -85,8 +85,8 @@ func (h *Handler) handleDealRoom(w http.ResponseWriter, r *http.Request) { } var deal model.Deal - err := h.db.QueryRow(`SELECT id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by FROM deals WHERE id = ?`, dealID).Scan( - &deal.ID, &deal.OrganizationID, &deal.Name, &deal.Description, &deal.TargetCompany, &deal.Stage, &deal.DealSize, &deal.Currency, &deal.IOIDate, &deal.LOIDate, &deal.ExclusivityEnd, &deal.ExpectedCloseDate, &deal.CloseProbability, &deal.CreatedBy) + err := h.db.QueryRow(`SELECT id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, created_by, COALESCE(industry, '') FROM deals WHERE id = ?`, dealID).Scan( + &deal.ID, &deal.OrganizationID, &deal.Name, &deal.Description, &deal.TargetCompany, &deal.Stage, &deal.DealSize, &deal.Currency, &deal.IOIDate, &deal.LOIDate, &deal.ExclusivityEnd, &deal.ExpectedCloseDate, &deal.CloseProbability, &deal.CreatedBy, &deal.Industry) if err != nil { http.NotFound(w, r) return @@ -117,7 +117,7 @@ func (h *Handler) handleCreateDeal(w http.ResponseWriter, r *http.Request) { targetCompany := strings.TrimSpace(r.FormValue("target_company")) stage := r.FormValue("stage") if stage == "" { - stage = "pipeline" + stage = "prospect" } dealSize, _ := strconv.ParseFloat(r.FormValue("deal_size"), 64) currency := r.FormValue("currency") @@ -502,6 +502,136 @@ func (h *Handler) getActivitiesFiltered(orgID string, dealID string, limit int) return h.getActivitiesFilteredBuyer(orgID, dealID, "", limit) } +func (h *Handler) handleContactSearch(w http.ResponseWriter, r *http.Request) { + profile := getProfile(r.Context()) + q := r.URL.Query().Get("q") + if q == "" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + + pattern := "%" + q + "%" + rows, err := h.db.Query(`SELECT DISTINCT email, full_name FROM profiles WHERE organization_id = ? AND (email LIKE ? OR full_name LIKE ?) LIMIT 10`, profile.OrganizationID, pattern, pattern) + if err != nil { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + defer rows.Close() + + type contact struct { + Email string `json:"email"` + Name string `json:"name"` + } + var contacts []contact + for rows.Next() { + var c contact + rows.Scan(&c.Email, &c.Name) + contacts = append(contacts, c) + } + + // Also search contacts table + rows2, _ := h.db.Query(`SELECT DISTINCT email, full_name FROM contacts WHERE organization_id = ? AND contact_type = 'internal' AND (email LIKE ? OR full_name LIKE ?) LIMIT 10`, profile.OrganizationID, pattern, pattern) + if rows2 != nil { + for rows2.Next() { + var c contact + rows2.Scan(&c.Email, &c.Name) + contacts = append(contacts, c) + } + rows2.Close() + } + + if contacts == nil { + contacts = []contact{} + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(contacts) +} + +func (h *Handler) handleIndustryList(w http.ResponseWriter, r *http.Request) { + // Standard pre-populated industries + industries := []string{ + "Aerospace & Defense", + "Agriculture", + "Automotive", + "Business Services", + "Chemicals", + "Consumer Products", + "Education", + "Energy", + "Financial Services", + "Food & Beverage", + "Healthcare", + "Hospitality & Leisure", + "Industrial Manufacturing", + "Insurance", + "Internet & E-Commerce", + "Logistics & Transportation", + "Media & Entertainment", + "Mining & Metals", + "Pharmaceuticals", + "Real Estate", + "Retail", + "Software & Technology", + "Telecommunications", + "Utilities", + } + + // Fetch custom industries from existing deals + rows, err := h.db.Query(`SELECT DISTINCT industry FROM deals WHERE organization_id = ? AND industry != '' ORDER BY industry`, getProfile(r.Context()).OrganizationID) + if err == nil { + defer rows.Close() + existing := make(map[string]bool) + for _, ind := range industries { + existing[ind] = true + } + for rows.Next() { + var ind string + rows.Scan(&ind) + if !existing[ind] { + industries = append(industries, ind) + } + } + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(industries) +} + +func (h *Handler) handleDealUpdate(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + r.ParseForm() + dealID := r.FormValue("deal_id") + if dealID == "" { + http.Error(w, "Missing deal ID", 400) + return + } + + name := strings.TrimSpace(r.FormValue("name")) + targetCompany := strings.TrimSpace(r.FormValue("target_company")) + stage := r.FormValue("stage") + dealSize, _ := strconv.ParseFloat(r.FormValue("deal_size"), 64) + currency := r.FormValue("currency") + ioiDate := r.FormValue("ioi_date") + loiDate := r.FormValue("loi_date") + exclusivityEnd := r.FormValue("exclusivity_end") + industry := strings.TrimSpace(r.FormValue("industry")) + description := strings.TrimSpace(r.FormValue("description")) + + _, err := h.db.Exec(`UPDATE deals SET name=?, description=?, target_company=?, stage=?, deal_size=?, currency=?, ioi_date=?, loi_date=?, exclusivity_end_date=?, industry=?, updated_at=datetime('now') WHERE id=?`, + name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, industry, dealID) + if err != nil { + http.Error(w, fmt.Sprintf("Error updating deal: %v", err), 500) + return + } + + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + func (h *Handler) getActivitiesFilteredBuyer(orgID string, dealID string, buyerGroup string, limit int) []*model.DealActivity { query := ` SELECT a.id, a.deal_id, a.user_id, a.activity_type, a.resource_type, a.resource_name, a.created_at, COALESCE(p.full_name, 'Unknown') diff --git a/internal/handler/handler.go b/internal/handler/handler.go index 92862ed..e55e67f 100644 --- a/internal/handler/handler.go +++ b/internal/handler/handler.go @@ -91,6 +91,16 @@ mux.HandleFunc("/auth/logout", h.handleLogout) mux.HandleFunc("/deals/search/", h.requireAuth(h.handleDealSearch)) mux.HandleFunc("/analytics/buyers", h.requireAuth(h.handleAnalyticsBuyers)) + // Deal update + mux.HandleFunc("/deals/update", h.requireAuth(h.handleDealUpdate)) + + // API endpoints + mux.HandleFunc("/api/contacts/search", h.requireAuth(h.handleContactSearch)) + mux.HandleFunc("/api/industries", h.requireAuth(h.handleIndustryList)) + + // Request list upload + mux.HandleFunc("/deals/requests/upload", h.requireAuth(h.handleRequestListUpload)) + // HTMX partials mux.HandleFunc("/htmx/request-comment", h.requireAuth(h.handleUpdateComment)) } diff --git a/internal/handler/requests.go b/internal/handler/requests.go index 63432d9..ccb6d5c 100644 --- a/internal/handler/requests.go +++ b/internal/handler/requests.go @@ -1,7 +1,12 @@ package handler import ( + "bufio" + "encoding/csv" + "fmt" + "io" "net/http" + "strings" "dealroom/internal/rbac" "dealroom/templates" @@ -46,6 +51,188 @@ func (h *Handler) handleRequestList(w http.ResponseWriter, r *http.Request) { templates.RequestListPage(profile, deals, dealRequests).Render(r.Context(), w) } +func (h *Handler) handleRequestListUpload(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + profile := getProfile(r.Context()) + err := r.ParseMultipartForm(10 << 20) // 10MB + if err != nil { + http.Error(w, "Error parsing form", 400) + return + } + + dealID := r.FormValue("deal_id") + targetGroup := r.FormValue("target_group") // "all" or specific group name + uploadMode := r.FormValue("upload_mode") // "replace", "add", "group_specific" + convertFolders := r.FormValue("convert_folders") // "yes" or "no" + + if dealID == "" { + http.Error(w, "Deal ID required", 400) + return + } + + file, _, err := r.FormFile("request_list") + if err != nil { + http.Error(w, "File is required", 400) + return + } + defer file.Close() + + // Parse CSV (supports basic CSV format: section, item_number, description, priority) + reader := csv.NewReader(bufio.NewReader(file)) + reader.FieldsPerRecord = -1 // variable fields + reader.TrimLeadingSpace = true + + var items []struct { + section, itemNumber, description, priority string + } + + lineNum := 0 + for { + record, err := reader.Read() + if err == io.EOF { + break + } + if err != nil { + continue + } + lineNum++ + if lineNum == 1 { + // skip header row + continue + } + if len(record) < 3 { + continue + } + item := struct { + section, itemNumber, description, priority string + }{ + section: strings.TrimSpace(record[0]), + itemNumber: strings.TrimSpace(record[1]), + description: strings.TrimSpace(record[2]), + priority: "medium", + } + if len(record) >= 4 { + p := strings.ToLower(strings.TrimSpace(record[3])) + if p == "high" || p == "medium" || p == "low" { + item.priority = p + } + } + items = append(items, item) + } + + if len(items) == 0 { + http.Error(w, "No valid items found in CSV", 400) + return + } + + // Handle upload mode + if uploadMode == "replace" { + if targetGroup == "all" || targetGroup == "" { + h.db.Exec("DELETE FROM diligence_requests WHERE deal_id = ?", dealID) + } else { + h.db.Exec("DELETE FROM diligence_requests WHERE deal_id = ? AND buyer_group = ?", dealID, targetGroup) + } + } + + buyerGroup := "" + if targetGroup != "all" && targetGroup != "" { + buyerGroup = targetGroup + } + isBuyerSpecific := 0 + if uploadMode == "group_specific" && buyerGroup != "" { + isBuyerSpecific = 1 + } + + // Insert request items + for _, item := range items { + id := generateID("req") + h.db.Exec(`INSERT INTO diligence_requests (id, deal_id, item_number, section, description, priority, buyer_group, is_buyer_specific, visible_to_buyer_group, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + id, dealID, item.itemNumber, item.section, item.description, item.priority, buyerGroup, isBuyerSpecific, buyerGroup, profile.ID) + } + + // Optionally convert folder structure + if convertFolders == "yes" { + // Create folders from unique sections + sections := make(map[string]bool) + for _, item := range items { + sections[item.section] = true + } + for section := range sections { + var existing int + h.db.QueryRow("SELECT COUNT(*) FROM folders WHERE deal_id = ? AND name = ?", dealID, section).Scan(&existing) + if existing == 0 { + folderID := generateID("folder") + h.db.Exec("INSERT INTO folders (id, deal_id, parent_id, name, created_by) VALUES (?, ?, '', ?, ?)", + folderID, dealID, section, profile.ID) + } + } + } + + // Auto-assign existing files to matching requests + h.autoAssignFilesToRequests(dealID) + + h.logActivity(dealID, profile.ID, profile.OrganizationID, "upload", "request_list", fmt.Sprintf("%d items", len(items)), "") + + http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther) +} + +func (h *Handler) autoAssignFilesToRequests(dealID string) { + // Get all unlinked requests + rows, err := h.db.Query("SELECT id, description, section FROM diligence_requests WHERE deal_id = ? AND (linked_file_ids = '' OR linked_file_ids IS NULL)", dealID) + if err != nil { + return + } + defer rows.Close() + + type reqInfo struct { + id, description, section string + } + var reqs []reqInfo + for rows.Next() { + var r reqInfo + rows.Scan(&r.id, &r.description, &r.section) + reqs = append(reqs, r) + } + + // Get all files + files, err := h.db.Query("SELECT id, name FROM files WHERE deal_id = ?", dealID) + if err != nil { + return + } + defer files.Close() + + type fileInfo struct { + id, name string + } + var fileList []fileInfo + for files.Next() { + var f fileInfo + files.Scan(&f.id, &f.name) + fileList = append(fileList, f) + } + + // Simple keyword matching + for _, req := range reqs { + words := strings.Fields(strings.ToLower(req.description)) + for _, f := range fileList { + fname := strings.ToLower(f.name) + matchCount := 0 + for _, w := range words { + if len(w) > 3 && strings.Contains(fname, w) { + matchCount++ + } + } + if matchCount >= 2 { + h.db.Exec("UPDATE diligence_requests SET linked_file_ids = ? WHERE id = ?", f.id, req.id) + break + } + } + } +} + func (h *Handler) handleUpdateComment(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) diff --git a/internal/model/models.go b/internal/model/models.go index 657bab8..6ca5e9a 100644 --- a/internal/model/models.go +++ b/internal/model/models.go @@ -172,18 +172,27 @@ type Invite struct { // StageName returns human-readable stage name func StageName(stage string) string { switch stage { - case "pipeline": - return "Pipeline" + case "prospect": + return "Prospect" + case "internal": + return "Internal" + case "initial_marketing": + return "Initial Marketing" + case "ioi": + return "IOI" case "loi": - return "LOI Stage" - case "initial_review": - return "Initial Review" - case "due_diligence": - return "Due Diligence" - case "final_negotiation": - return "Final Negotiation" + return "LOI" case "closed": return "Closed" + // Legacy stages (backward compat) + case "pipeline": + return "Prospect" + case "initial_review": + return "Initial Marketing" + case "due_diligence": + return "IOI" + case "final_negotiation": + return "LOI" case "dead": return "Dead" default: diff --git a/templates/admin.templ b/templates/admin.templ index f90696a..2e8034f 100644 --- a/templates/admin.templ +++ b/templates/admin.templ @@ -252,13 +252,12 @@ templ AdminDealForm(profile *model.Profile, deal *model.Deal) { @formField("target_company", "Target Company", "text", deal.TargetCompany, false) @formField("industry", "Industry", "text", deal.Industry, false) @formSelect("stage", "Stage", deal.Stage, []SelectOption{ - {Value: "pipeline", Label: "Pipeline"}, - {Value: "loi", Label: "LOI Stage"}, - {Value: "initial_review", Label: "Initial Review"}, - {Value: "due_diligence", Label: "Due Diligence"}, - {Value: "final_negotiation", Label: "Final Negotiation"}, + {Value: "prospect", Label: "Prospect"}, + {Value: "internal", Label: "Internal"}, + {Value: "initial_marketing", Label: "Initial Marketing"}, + {Value: "ioi", Label: "IOI"}, + {Value: "loi", Label: "LOI"}, {Value: "closed", Label: "Closed"}, - {Value: "dead", Label: "Dead"}, })
@formField("deal_size", "Deal Size", "number", fmt.Sprintf("%.0f", deal.DealSize), false) diff --git a/templates/components.templ b/templates/components.templ index 41342cf..322f7aa 100644 --- a/templates/components.templ +++ b/templates/components.templ @@ -4,11 +4,11 @@ import "dealroom/internal/model" templ StageBadge(stage string) { { model.StageName(stage) } diff --git a/templates/dashboard.templ b/templates/dashboard.templ index 344c15c..d77a25f 100644 --- a/templates/dashboard.templ +++ b/templates/dashboard.templ @@ -20,11 +20,13 @@ templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model
-
+
@statCard("ACTIVE ROOMS", fmt.Sprintf("%d", countActive(deals)), fmt.Sprintf("%d total", len(deals)), "folder") - @statCard("PRE-MARKETING", fmt.Sprintf("%d", countByStage(deals, "pipeline")), "in pipeline", "file") - @statCard("IOI STAGE", fmt.Sprintf("%d", countIOIStage(deals)), "initial review / LOI", "users") - @statCard("CLOSED", fmt.Sprintf("%d", countByStage(deals, "closed")), "deals closed", "trend") + @statCard("PROSPECT", fmt.Sprintf("%d", countByStages(deals, "prospect", "pipeline")), "prospecting", "file") + @statCard("INTERNAL", fmt.Sprintf("%d", countByStages(deals, "internal", "")), "internal review", "users") + @statCard("INITIAL MARKETING", fmt.Sprintf("%d", countByStages(deals, "initial_marketing", "initial_review")), "marketing", "trend") + @statCard("IOI", fmt.Sprintf("%d", countByStages(deals, "ioi", "due_diligence")), "indication of interest", "trend") + @statCard("LOI", fmt.Sprintf("%d", countByStages(deals, "loi", "final_negotiation")), "letter of intent", "trend")
@@ -99,7 +101,7 @@ templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model