dealroom/internal/handler/deals.go

313 lines
9.6 KiB
Go

package handler
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"dealroom/internal/model"
"dealroom/internal/rbac"
"dealroom/templates"
)
func (h *Handler) handleDashboard(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
profile := getProfile(r.Context())
deals := h.getDeals(profile)
activities := h.getActivities(profile.OrganizationID, 8)
// Count files per deal
fileCounts := make(map[string]int)
for _, d := range deals {
var count int
h.db.QueryRow("SELECT COUNT(*) FROM files WHERE deal_id = ?", d.ID).Scan(&count)
fileCounts[d.ID] = count
d.FileCount = count
}
// Build deal name map and populate DealName on activities
dealNames := make(map[string]string)
for _, d := range deals {
dealNames[d.ID] = d.Name
}
for _, act := range activities {
if name, ok := dealNames[act.DealID]; ok {
act.DealName = name
}
}
// Get last activity time per deal
lastActivity := make(map[string]*time.Time)
for _, d := range deals {
t := h.getLastActivityByDeal(d.ID)
if t != nil {
lastActivity[d.ID] = t
}
}
templates.Dashboard(profile, deals, activities, fileCounts, lastActivity).Render(r.Context(), w)
}
func (h *Handler) handleDealRooms(w http.ResponseWriter, r *http.Request) {
profile := getProfile(r.Context())
deals := h.getDeals(profile)
for _, d := range deals {
var count int
h.db.QueryRow("SELECT COUNT(*) FROM files WHERE deal_id = ?", d.ID).Scan(&count)
d.FileCount = count
}
templates.DealRooms(profile, deals).Render(r.Context(), w)
}
func (h *Handler) handleDealRoom(w http.ResponseWriter, r *http.Request) {
profile := getProfile(r.Context())
dealID := strings.TrimPrefix(r.URL.Path, "/deals/")
// Handle file status PATCH: /deals/{id}/files/{fileID}/status
if strings.Contains(dealID, "/files/") {
parts := strings.SplitN(dealID, "/files/", 2)
dealID = parts[0]
if r.Method == http.MethodPatch {
h.handleFileStatusUpdate(w, r, dealID, parts[1])
return
}
}
if dealID == "" {
http.Redirect(w, r, "/deals", http.StatusSeeOther)
return
}
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)
if err != nil {
http.NotFound(w, r)
return
}
folders := h.getFolders(dealID)
folderParam := r.URL.Query().Get("folder")
files := h.getFiles(dealID)
requests := h.getRequests(dealID, profile)
templates.DealRoomDetail(profile, &deal, folders, files, requests, folderParam).Render(r.Context(), w)
}
func (h *Handler) handleCreateDeal(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
profile := getProfile(r.Context())
r.ParseForm()
name := strings.TrimSpace(r.FormValue("name"))
if name == "" {
http.Error(w, "Project name is required", 400)
return
}
targetCompany := strings.TrimSpace(r.FormValue("target_company"))
stage := r.FormValue("stage")
if stage == "" {
stage = "pipeline"
}
dealSize, _ := strconv.ParseFloat(r.FormValue("deal_size"), 64)
currency := r.FormValue("currency")
if currency == "" {
currency = "USD"
}
ioiDate := r.FormValue("ioi_date")
loiDate := r.FormValue("loi_date")
description := strings.TrimSpace(r.FormValue("description"))
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)
if err != nil {
http.Error(w, fmt.Sprintf("Error creating deal: %v", err), 500)
return
}
http.Redirect(w, r, "/deals/"+id, http.StatusSeeOther)
}
func (h *Handler) handleFileStatusUpdate(w http.ResponseWriter, r *http.Request, dealID string, remaining string) {
fileID := strings.TrimSuffix(remaining, "/status")
if fileID == "" {
http.Error(w, "Missing file ID", 400)
return
}
var req struct {
Status string `json:"status"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", 400)
return
}
validStatuses := map[string]bool{"uploaded": true, "processing": true, "reviewed": true, "flagged": true, "archived": true}
if !validStatuses[req.Status] {
http.Error(w, "Invalid status", 400)
return
}
_, err := h.db.Exec("UPDATE files SET status = ? WHERE id = ? AND deal_id = ?", req.Status, fileID, dealID)
if err != nil {
http.Error(w, "Error updating status", 500)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": req.Status})
}
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)
if err != nil {
return nil
}
if t.IsZero() {
return nil
}
return &t
}
func (h *Handler) getDeals(profile *model.Profile) []*model.Deal {
rows, err := h.db.Query("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, created_at FROM deals WHERE organization_id = ? AND is_archived = 0 ORDER BY created_at DESC", profile.OrganizationID)
if err != nil {
return nil
}
defer rows.Close()
var deals []*model.Deal
for rows.Next() {
d := &model.Deal{}
rows.Scan(&d.ID, &d.OrganizationID, &d.Name, &d.Description, &d.TargetCompany, &d.Stage, &d.DealSize, &d.Currency, &d.IOIDate, &d.LOIDate, &d.ExclusivityEnd, &d.ExpectedCloseDate, &d.CloseProbability, &d.CreatedBy, &d.CreatedAt)
deals = append(deals, d)
}
return deals
}
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)
if err != nil {
return nil
}
defer rows.Close()
var folders []*model.Folder
for rows.Next() {
f := &model.Folder{}
rows.Scan(&f.ID, &f.DealID, &f.ParentID, &f.Name, &f.Description)
folders = append(folders, f)
}
return folders
}
func (h *Handler) getFiles(dealID string) []*model.File {
rows, err := h.db.Query("SELECT id, deal_id, folder_id, name, file_size, mime_type, status, uploaded_by, created_at FROM files WHERE deal_id = ? ORDER BY name", dealID)
if err != nil {
return nil
}
defer rows.Close()
var files []*model.File
for rows.Next() {
f := &model.File{}
rows.Scan(&f.ID, &f.DealID, &f.FolderID, &f.Name, &f.FileSize, &f.MimeType, &f.Status, &f.UploadedBy, &f.CreatedAt)
files = append(files, f)
}
return files
}
func (h *Handler) getRequests(dealID string, profile *model.Profile) []*model.DiligenceRequest {
query := "SELECT id, deal_id, item_number, section, description, priority, atlas_status, atlas_note, confidence, buyer_comment, seller_comment, buyer_group FROM diligence_requests WHERE deal_id = ?"
args := []interface{}{dealID}
if rbac.EffectiveIsBuyer(profile) {
groups := rbac.BuyerGroups(profile)
if len(groups) > 0 {
placeholders := make([]string, len(groups))
for i, g := range groups {
placeholders[i] = "?"
args = append(args, g)
}
query += " AND buyer_group IN (" + strings.Join(placeholders, ",") + ")"
}
}
query += " ORDER BY item_number"
rows, err := h.db.Query(query, args...)
if err != nil {
return nil
}
defer rows.Close()
var reqs []*model.DiligenceRequest
for rows.Next() {
r := &model.DiligenceRequest{}
rows.Scan(&r.ID, &r.DealID, &r.ItemNumber, &r.Section, &r.Description, &r.Priority, &r.AtlasStatus, &r.AtlasNote, &r.Confidence, &r.BuyerComment, &r.SellerComment, &r.BuyerGroup)
reqs = append(reqs, r)
}
return reqs
}
func (h *Handler) getActivities(orgID string, limit int) []*model.DealActivity {
rows, err := h.db.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')
FROM deal_activity a LEFT JOIN profiles p ON a.user_id = p.id
WHERE a.organization_id = ?
ORDER BY a.created_at DESC LIMIT ?
`, orgID, limit)
if err != nil {
return nil
}
defer rows.Close()
var acts []*model.DealActivity
for rows.Next() {
a := &model.DealActivity{}
rows.Scan(&a.ID, &a.DealID, &a.UserID, &a.ActivityType, &a.ResourceType, &a.ResourceName, &a.CreatedAt, &a.UserName)
acts = append(acts, a)
}
return acts
}
func (h *Handler) getActivitiesFiltered(orgID string, dealID 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')
FROM deal_activity a LEFT JOIN profiles p ON a.user_id = p.id
WHERE a.organization_id = ?`
args := []interface{}{orgID}
if dealID != "" {
query += " AND a.deal_id = ?"
args = append(args, dealID)
}
query += " ORDER BY a.created_at DESC LIMIT ?"
args = append(args, limit)
rows, err := h.db.Query(query, args...)
if err != nil {
return nil
}
defer rows.Close()
var acts []*model.DealActivity
for rows.Next() {
a := &model.DealActivity{}
rows.Scan(&a.ID, &a.DealID, &a.UserID, &a.ActivityType, &a.ResourceType, &a.ResourceName, &a.CreatedAt, &a.UserName)
acts = append(acts, a)
}
return acts
}