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:
parent
99de1d4979
commit
f1c2a0ef84
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ type Deal struct {
|
|||
ExclusivityEnd string
|
||||
ExpectedCloseDate string
|
||||
CloseProbability int
|
||||
Industry string
|
||||
IsArchived bool
|
||||
CreatedBy string
|
||||
CreatedAt time.Time
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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 Legal Documents Technical DD/Architecture 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 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"),
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue