feat: new room modal enhancements

Add industry field and exclusivity end date to new room modal.
Add folder structure textarea for auto-creating nested folders.
Add initial team invite textarea for inviting members on deal creation.
Add New Room button and modal to deal rooms page.
Add industry field to admin deal form.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
James 2026-02-23 02:48:22 -05:00
parent 99de1d4979
commit f1c2a0ef84
7 changed files with 148 additions and 61 deletions

View File

@ -206,6 +206,8 @@ CREATE TABLE IF NOT EXISTS invites (
var additiveMigrationStmts = []string{
// Section 1: org_type
`ALTER TABLE organizations ADD COLUMN org_type TEXT DEFAULT 'company'`,
// Section 4: industry
`ALTER TABLE deals ADD COLUMN industry TEXT DEFAULT ''`,
}
func seed(db *sql.DB) error {

View File

@ -168,8 +168,8 @@ func (h *Handler) handleAdminDealForm(w http.ResponseWriter, r *http.Request) {
var deal model.Deal
deal.Currency = "USD"
if id != "" {
h.db.QueryRow("SELECT id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, is_archived FROM deals WHERE id = ? AND organization_id = ?", id, profile.OrganizationID).Scan(
&deal.ID, &deal.Name, &deal.Description, &deal.TargetCompany, &deal.Stage, &deal.DealSize, &deal.Currency, &deal.IOIDate, &deal.LOIDate, &deal.ExclusivityEnd, &deal.ExpectedCloseDate, &deal.CloseProbability, &deal.IsArchived)
h.db.QueryRow("SELECT id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, expected_close_date, close_probability, COALESCE(industry, ''), is_archived FROM deals WHERE id = ? AND organization_id = ?", id, profile.OrganizationID).Scan(
&deal.ID, &deal.Name, &deal.Description, &deal.TargetCompany, &deal.Stage, &deal.DealSize, &deal.Currency, &deal.IOIDate, &deal.LOIDate, &deal.ExclusivityEnd, &deal.ExpectedCloseDate, &deal.CloseProbability, &deal.Industry, &deal.IsArchived)
}
templates.AdminDealForm(profile, &deal).Render(r.Context(), w)
@ -191,6 +191,7 @@ func (h *Handler) handleAdminDealSave(w http.ResponseWriter, r *http.Request) {
exclusivityEnd := r.FormValue("exclusivity_end")
expectedClose := r.FormValue("expected_close_date")
closeProbability, _ := strconv.Atoi(r.FormValue("close_probability"))
industry := strings.TrimSpace(r.FormValue("industry"))
isArchived := r.FormValue("is_archived") == "on"
if name == "" {
@ -206,15 +207,15 @@ func (h *Handler) handleAdminDealSave(w http.ResponseWriter, r *http.Request) {
if id == "" {
id = generateID("deal")
_, err := h.db.Exec(`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, is_archived, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, profile.OrganizationID, name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, expectedClose, closeProbability, isArchived, profile.ID)
_, err := h.db.Exec(`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, industry, is_archived, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, profile.OrganizationID, name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, expectedClose, closeProbability, industry, isArchived, profile.ID)
if err != nil {
http.Error(w, fmt.Sprintf("Error creating deal: %v", err), 500)
return
}
} else {
_, err := h.db.Exec(`UPDATE deals SET name=?, description=?, target_company=?, stage=?, deal_size=?, currency=?, ioi_date=?, loi_date=?, exclusivity_end_date=?, expected_close_date=?, close_probability=?, is_archived=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND organization_id=?`,
name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, expectedClose, closeProbability, isArchived, id, profile.OrganizationID)
_, err := h.db.Exec(`UPDATE deals SET name=?, description=?, target_company=?, stage=?, deal_size=?, currency=?, ioi_date=?, loi_date=?, exclusivity_end_date=?, expected_close_date=?, close_probability=?, industry=?, is_archived=?, updated_at=CURRENT_TIMESTAMP WHERE id=? AND organization_id=?`,
name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, expectedClose, closeProbability, industry, isArchived, id, profile.OrganizationID)
if err != nil {
http.Error(w, fmt.Sprintf("Error updating deal: %v", err), 500)
return

View File

@ -126,16 +126,58 @@ func (h *Handler) handleCreateDeal(w http.ResponseWriter, r *http.Request) {
}
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"))
folderStructure := strings.TrimSpace(r.FormValue("folder_structure"))
inviteEmails := strings.TrimSpace(r.FormValue("invite_emails"))
id := generateID("deal")
_, err := h.db.Exec(`INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, profile.OrganizationID, name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, profile.ID)
_, err := h.db.Exec(`INSERT INTO deals (id, organization_id, name, description, target_company, stage, deal_size, currency, ioi_date, loi_date, exclusivity_end_date, industry, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
id, profile.OrganizationID, name, description, targetCompany, stage, dealSize, currency, ioiDate, loiDate, exclusivityEnd, industry, profile.ID)
if err != nil {
http.Error(w, fmt.Sprintf("Error creating deal: %v", err), 500)
return
}
// Create folder structure from textarea
if folderStructure != "" {
lines := strings.Split(folderStructure, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line == "" {
continue
}
parts := strings.Split(line, "/")
parentID := ""
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
folderID := generateID("folder")
h.db.Exec("INSERT INTO folders (id, deal_id, parent_id, name, created_by) VALUES (?, ?, ?, ?, ?)",
folderID, id, parentID, part, profile.ID)
parentID = folderID
}
}
}
// Create invites for initial team
if inviteEmails != "" {
emails := strings.Split(inviteEmails, "\n")
for _, email := range emails {
email = strings.TrimSpace(email)
if email == "" {
continue
}
token := generateToken()
expiresAt := time.Now().Add(7 * 24 * time.Hour)
h.db.Exec("INSERT INTO invites (token, org_id, email, role, invited_by, expires_at) VALUES (?, ?, ?, ?, ?, ?)",
token, profile.OrganizationID, email, "member", profile.ID, expiresAt)
}
}
http.Redirect(w, r, "/deals/"+id, http.StatusSeeOther)
}

View File

@ -49,6 +49,7 @@ type Deal struct {
ExclusivityEnd string
ExpectedCloseDate string
CloseProbability int
Industry string
IsArchived bool
CreatedBy string
CreatedAt time.Time

View File

@ -235,6 +235,7 @@ templ AdminDealForm(profile *model.Profile, deal *model.Deal) {
@formField("name", "Deal Name", "text", deal.Name, true)
@formTextarea("description", "Description", deal.Description)
@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"},

View File

@ -106,59 +106,7 @@ templ Dashboard(profile *model.Profile, deals []*model.Deal, activities []*model
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
<form action="/deals/create" method="POST" class="space-y-4">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Project Name <span class="text-red-400">*</span></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">Company Name</label>
<input type="text" name="target_company" 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">Stage</label>
<select name="stage" 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="pipeline">Pipeline</option>
<option value="loi">LOI Stage</option>
<option value="initial_review">Initial Review</option>
<option value="due_diligence">Due Diligence</option>
<option value="final_negotiation">Final Negotiation</option>
<option value="closed">Closed</option>
</select>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Deal Size</label>
<input type="number" name="deal_size" step="0.01" 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">Currency</label>
<select name="currency" 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="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
</select>
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">IOI Date</label>
<input type="date" name="ioi_date" 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">LOI Date</label>
<input type="date" name="loi_date" 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>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Description</label>
<textarea name="description" rows="2" 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 resize-none"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('newRoomModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Create Room</button>
</div>
</form>
@newRoomForm()
</div>
</div>
}
@ -233,6 +181,80 @@ templ statCard(label, value, subtitle, iconType string) {
</div>
}
templ newRoomForm() {
<form action="/deals/create" method="POST" class="space-y-4 max-h-[70vh] overflow-y-auto pr-1">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Project Name <span class="text-red-400">*</span></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">Company Name</label>
<input type="text" name="target_company" 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 class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Stage</label>
<select name="stage" 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="pipeline">Pipeline</option>
<option value="loi">LOI Stage</option>
<option value="initial_review">Initial Review</option>
<option value="due_diligence">Due Diligence</option>
<option value="final_negotiation">Final Negotiation</option>
<option value="closed">Closed</option>
</select>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Industry</label>
<input type="text" name="industry" placeholder="e.g. Healthcare, Fintech" 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>
<div class="grid grid-cols-2 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Deal Size</label>
<input type="number" name="deal_size" step="0.01" 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">Currency</label>
<select name="currency" 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="USD">USD</option>
<option value="EUR">EUR</option>
<option value="GBP">GBP</option>
</select>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">IOI Date</label>
<input type="date" name="ioi_date" 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">LOI Date</label>
<input type="date" name="loi_date" 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">Exclusivity End</label>
<input type="date" name="exclusivity_end" 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>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Description</label>
<textarea name="description" rows="2" 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 resize-none"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Folder Structure <span class="text-gray-600">(one path per line, e.g. Financial/Q4 Reports)</span></label>
<textarea name="folder_structure" rows="3" placeholder="Financial Documents&#10;Legal Documents&#10;Technical DD/Architecture&#10;Technical DD/Security" 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 resize-none font-mono"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-400 mb-1">Initial Team <span class="text-gray-600">(one email per line)</span></label>
<textarea name="invite_emails" rows="2" placeholder="analyst@company.com&#10;associate@company.com" 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 resize-none font-mono"></textarea>
</div>
<div class="flex justify-end gap-3 pt-2">
<button type="button" onclick="document.getElementById('newRoomModal').classList.add('hidden')" class="px-4 py-2 rounded-lg text-sm text-gray-400 hover:text-gray-200 border border-gray-700 hover:border-gray-600 transition">Cancel</button>
<button type="submit" class="px-4 py-2 rounded-lg bg-teal-500 text-white text-sm font-medium hover:bg-teal-600 transition">Create Room</button>
</div>
</form>
}
templ activityIcon(actType string) {
<div class={ "w-6 h-6 rounded-full flex items-center justify-center shrink-0",
templ.KV("bg-teal-500/20 text-teal-400", actType == "upload"),

View File

@ -11,6 +11,10 @@ templ DealRooms(profile *model.Profile, deals []*model.Deal) {
<h1 class="text-2xl font-bold">Deal Rooms</h1>
<p class="text-sm text-gray-500 mt-1">{ fmt.Sprintf("%d deal rooms", len(deals)) } across your organization.</p>
</div>
<button onclick="document.getElementById('newRoomModal').classList.remove('hidden')" class="h-9 px-4 rounded-lg bg-teal-500 text-white text-sm font-medium flex items-center gap-1.5 hover:bg-teal-600 transition">
<svg class="w-3.5 h-3.5" 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 Room
</button>
</div>
<div class="bg-gray-900 rounded-lg border border-gray-800">
@ -49,6 +53,20 @@ templ DealRooms(profile *model.Profile, deals []*model.Deal) {
</table>
</div>
</div>
<!-- New Room Modal -->
<div id="newRoomModal" class="hidden fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/60" onclick="document.getElementById('newRoomModal').classList.add('hidden')"></div>
<div class="relative bg-gray-900 border border-gray-700 rounded-xl shadow-2xl w-full max-w-lg mx-4 p-6">
<div class="flex items-center justify-between mb-5">
<h2 class="text-lg font-bold">Create New Room</h2>
<button onclick="document.getElementById('newRoomModal').classList.add('hidden')" class="text-gray-500 hover:text-gray-300">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>
</button>
</div>
@newRoomForm()
</div>
</div>
}
}