666 lines
22 KiB
Go
666 lines
22 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, 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
|
|
}
|
|
|
|
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 = "prospect"
|
|
}
|
|
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")
|
|
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, 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)
|
|
}
|
|
|
|
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) handleFolderCreate(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
profile := getProfile(r.Context())
|
|
dealID := r.FormValue("deal_id")
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
parentID := r.FormValue("parent_id")
|
|
|
|
if dealID == "" || name == "" {
|
|
http.Error(w, "Deal ID and name are required", 400)
|
|
return
|
|
}
|
|
|
|
folderID := generateID("folder")
|
|
h.db.Exec("INSERT INTO folders (id, deal_id, parent_id, name, created_by) VALUES (?, ?, ?, ?, ?)",
|
|
folderID, dealID, parentID, name, profile.ID)
|
|
|
|
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleFolderRename(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
folderID := r.FormValue("folder_id")
|
|
dealID := r.FormValue("deal_id")
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
|
|
if folderID == "" || name == "" {
|
|
http.Error(w, "Missing fields", 400)
|
|
return
|
|
}
|
|
|
|
h.db.Exec("UPDATE folders SET name = ? WHERE id = ?", name, folderID)
|
|
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleFolderDelete(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
folderID := r.FormValue("folder_id")
|
|
dealID := r.FormValue("deal_id")
|
|
|
|
if folderID == "" {
|
|
http.Error(w, "Missing folder ID", 400)
|
|
return
|
|
}
|
|
|
|
// Delete child folders
|
|
h.db.Exec("DELETE FROM folders WHERE parent_id = ?", folderID)
|
|
// Move files to root
|
|
h.db.Exec("UPDATE files SET folder_id = '' WHERE folder_id = ?", folderID)
|
|
h.db.Exec("DELETE FROM folders WHERE id = ?", folderID)
|
|
|
|
http.Redirect(w, r, "/deals/"+dealID, http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleFolderReorder(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
|
return
|
|
}
|
|
folderID := r.FormValue("folder_id")
|
|
dealID := r.FormValue("deal_id")
|
|
direction := r.FormValue("direction")
|
|
|
|
if folderID == "" {
|
|
http.Error(w, "Missing folder ID", 400)
|
|
return
|
|
}
|
|
|
|
var currentOrder int
|
|
h.db.QueryRow("SELECT COALESCE(sort_order, 0) FROM folders WHERE id = ?", folderID).Scan(¤tOrder)
|
|
|
|
if direction == "up" {
|
|
h.db.Exec("UPDATE folders SET sort_order = sort_order + 1 WHERE deal_id = ? AND COALESCE(sort_order, 0) = ?", dealID, currentOrder-1)
|
|
h.db.Exec("UPDATE folders SET sort_order = ? WHERE id = ?", currentOrder-1, folderID)
|
|
} else {
|
|
h.db.Exec("UPDATE folders SET sort_order = sort_order - 1 WHERE deal_id = ? AND COALESCE(sort_order, 0) = ?", dealID, currentOrder+1)
|
|
h.db.Exec("UPDATE folders SET sort_order = ? WHERE id = ?", currentOrder+1, folderID)
|
|
}
|
|
|
|
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)
|
|
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 COALESCE(sort_order, 0), 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, linked_file_ids, COALESCE(is_buyer_specific, 0), COALESCE(visible_to_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)
|
|
}
|
|
// Show general requests (not buyer-specific) AND buyer-specific ones for this group
|
|
query += " AND (COALESCE(is_buyer_specific, 0) = 0 OR COALESCE(visible_to_buyer_group, '') IN (" + strings.Join(placeholders, ",") + "))"
|
|
// Also filter by buyer_group for general requests
|
|
query += " AND (buyer_group IN (" + strings.Join(placeholders, ",") + ") OR buyer_group = '')"
|
|
for _, g := range groups {
|
|
args = append(args, g)
|
|
}
|
|
}
|
|
}
|
|
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, &r.LinkedFileIDs, &r.IsBuyerSpecific, &r.VisibleToBuyerGroup)
|
|
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 {
|
|
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')
|
|
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)
|
|
}
|
|
if buyerGroup != "" {
|
|
query += " AND a.buyer_group = ?"
|
|
args = append(args, buyerGroup)
|
|
}
|
|
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
|
|
}
|