409 lines
14 KiB
Go
409 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"dealroom/internal/model"
|
|
"dealroom/templates"
|
|
)
|
|
|
|
// requireAdmin wraps requireAuth and checks for owner/admin role
|
|
func (h *Handler) requireAdmin(next http.HandlerFunc) http.HandlerFunc {
|
|
return h.requireAuth(func(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
if profile.Role != "owner" && profile.Role != "admin" {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
func generateID(prefix string) string {
|
|
b := make([]byte, 8)
|
|
rand.Read(b)
|
|
return prefix + "-" + hex.EncodeToString(b)
|
|
}
|
|
|
|
// --- Admin Dashboard ---
|
|
|
|
func (h *Handler) handleAdmin(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
var stats templates.AdminStats
|
|
h.db.QueryRow("SELECT COUNT(*) FROM contacts WHERE organization_id = ?", profile.OrganizationID).Scan(&stats.ContactCount)
|
|
h.db.QueryRow("SELECT COUNT(*) FROM deals WHERE organization_id = ?", profile.OrganizationID).Scan(&stats.DealCount)
|
|
h.db.QueryRow("SELECT COUNT(*) FROM profiles WHERE organization_id = ?", profile.OrganizationID).Scan(&stats.UserCount)
|
|
h.db.QueryRow("SELECT COUNT(*) FROM organizations").Scan(&stats.OrgCount)
|
|
|
|
templates.AdminDashboard(profile, stats).Render(r.Context(), w)
|
|
}
|
|
|
|
// --- Contacts CRUD ---
|
|
|
|
func (h *Handler) handleAdminContacts(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
filter := r.URL.Query().Get("type")
|
|
query := "SELECT id, full_name, email, phone, company, title, contact_type, tags, notes FROM contacts WHERE organization_id = ?"
|
|
args := []interface{}{profile.OrganizationID}
|
|
if filter != "" {
|
|
query += " AND contact_type = ?"
|
|
args = append(args, filter)
|
|
}
|
|
query += " ORDER BY full_name"
|
|
|
|
rows, err := h.db.Query(query, args...)
|
|
if err != nil {
|
|
http.Error(w, "Error loading contacts", 500)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var contacts []*model.Contact
|
|
for rows.Next() {
|
|
c := &model.Contact{}
|
|
rows.Scan(&c.ID, &c.FullName, &c.Email, &c.Phone, &c.Company, &c.Title, &c.ContactType, &c.Tags, &c.Notes)
|
|
contacts = append(contacts, c)
|
|
}
|
|
|
|
templates.AdminContacts(profile, contacts, filter).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminContactForm(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
|
|
var contact model.Contact
|
|
if id != "" {
|
|
h.db.QueryRow("SELECT id, full_name, email, phone, company, title, contact_type, tags, notes FROM contacts WHERE id = ? AND organization_id = ?", id, profile.OrganizationID).Scan(
|
|
&contact.ID, &contact.FullName, &contact.Email, &contact.Phone, &contact.Company, &contact.Title, &contact.ContactType, &contact.Tags, &contact.Notes)
|
|
}
|
|
|
|
templates.AdminContactForm(profile, &contact).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminContactSave(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
r.ParseForm()
|
|
|
|
id := r.FormValue("id")
|
|
fullName := strings.TrimSpace(r.FormValue("full_name"))
|
|
email := strings.TrimSpace(r.FormValue("email"))
|
|
phone := strings.TrimSpace(r.FormValue("phone"))
|
|
company := strings.TrimSpace(r.FormValue("company"))
|
|
title := strings.TrimSpace(r.FormValue("title"))
|
|
contactType := r.FormValue("contact_type")
|
|
tags := strings.TrimSpace(r.FormValue("tags"))
|
|
notes := strings.TrimSpace(r.FormValue("notes"))
|
|
|
|
if fullName == "" {
|
|
http.Error(w, "Name is required", 400)
|
|
return
|
|
}
|
|
|
|
if id == "" {
|
|
id = generateID("contact")
|
|
_, err := h.db.Exec("INSERT INTO contacts (id, organization_id, full_name, email, phone, company, title, contact_type, tags, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
id, profile.OrganizationID, fullName, email, phone, company, title, contactType, tags, notes)
|
|
if err != nil {
|
|
http.Error(w, "Error creating contact", 500)
|
|
return
|
|
}
|
|
} else {
|
|
_, err := h.db.Exec("UPDATE contacts SET full_name=?, email=?, phone=?, company=?, title=?, contact_type=?, tags=?, notes=? WHERE id=? AND organization_id=?",
|
|
fullName, email, phone, company, title, contactType, tags, notes, id, profile.OrganizationID)
|
|
if err != nil {
|
|
http.Error(w, "Error updating contact", 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin/contacts", http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleAdminContactDelete(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
http.Error(w, "Missing id", 400)
|
|
return
|
|
}
|
|
|
|
h.db.Exec("DELETE FROM contacts WHERE id = ? AND organization_id = ?", id, profile.OrganizationID)
|
|
http.Redirect(w, r, "/admin/contacts", http.StatusSeeOther)
|
|
}
|
|
|
|
// --- Deals CRUD ---
|
|
|
|
func (h *Handler) handleAdminDeals(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
rows, err := h.db.Query("SELECT id, name, description, target_company, stage, deal_size, currency, is_archived, created_at FROM deals WHERE organization_id = ? ORDER BY created_at DESC", profile.OrganizationID)
|
|
if err != nil {
|
|
http.Error(w, "Error loading deals", 500)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var deals []*model.Deal
|
|
for rows.Next() {
|
|
d := &model.Deal{}
|
|
rows.Scan(&d.ID, &d.Name, &d.Description, &d.TargetCompany, &d.Stage, &d.DealSize, &d.Currency, &d.IsArchived, &d.CreatedAt)
|
|
deals = append(deals, d)
|
|
}
|
|
|
|
templates.AdminDeals(profile, deals).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminDealForm(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
|
|
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)
|
|
}
|
|
|
|
templates.AdminDealForm(profile, &deal).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminDealSave(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
r.ParseForm()
|
|
|
|
id := r.FormValue("id")
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
description := strings.TrimSpace(r.FormValue("description"))
|
|
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")
|
|
expectedClose := r.FormValue("expected_close_date")
|
|
closeProbability, _ := strconv.Atoi(r.FormValue("close_probability"))
|
|
isArchived := r.FormValue("is_archived") == "on"
|
|
|
|
if name == "" {
|
|
http.Error(w, "Name is required", 400)
|
|
return
|
|
}
|
|
if currency == "" {
|
|
currency = "USD"
|
|
}
|
|
if stage == "" {
|
|
stage = "pipeline"
|
|
}
|
|
|
|
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)
|
|
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)
|
|
if err != nil {
|
|
http.Error(w, fmt.Sprintf("Error updating deal: %v", err), 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin/deals", http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleAdminDealDelete(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
http.Error(w, "Missing id", 400)
|
|
return
|
|
}
|
|
|
|
// Delete related data first
|
|
h.db.Exec("DELETE FROM deal_activity WHERE deal_id = ?", id)
|
|
h.db.Exec("DELETE FROM diligence_requests WHERE deal_id = ?", id)
|
|
h.db.Exec("DELETE FROM files WHERE deal_id = ?", id)
|
|
h.db.Exec("DELETE FROM folders WHERE deal_id = ?", id)
|
|
h.db.Exec("DELETE FROM deals WHERE id = ? AND organization_id = ?", id, profile.OrganizationID)
|
|
|
|
http.Redirect(w, r, "/admin/deals", http.StatusSeeOther)
|
|
}
|
|
|
|
// --- Users/Profiles CRUD ---
|
|
|
|
func (h *Handler) handleAdminUsers(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
rows, err := h.db.Query("SELECT id, email, full_name, role, created_at FROM profiles WHERE organization_id = ? ORDER BY full_name", profile.OrganizationID)
|
|
if err != nil {
|
|
http.Error(w, "Error loading users", 500)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var users []*model.Profile
|
|
for rows.Next() {
|
|
u := &model.Profile{}
|
|
rows.Scan(&u.ID, &u.Email, &u.FullName, &u.Role, &u.CreatedAt)
|
|
users = append(users, u)
|
|
}
|
|
|
|
templates.AdminUsers(profile, users).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminUserForm(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
|
|
var user model.Profile
|
|
if id != "" {
|
|
h.db.QueryRow("SELECT id, email, full_name, role FROM profiles WHERE id = ? AND organization_id = ?", id, profile.OrganizationID).Scan(
|
|
&user.ID, &user.Email, &user.FullName, &user.Role)
|
|
}
|
|
|
|
templates.AdminUserForm(profile, &user).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminUserSave(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
r.ParseForm()
|
|
|
|
id := r.FormValue("id")
|
|
email := strings.TrimSpace(r.FormValue("email"))
|
|
fullName := strings.TrimSpace(r.FormValue("full_name"))
|
|
role := r.FormValue("role")
|
|
|
|
if email == "" || fullName == "" {
|
|
http.Error(w, "Email and name are required", 400)
|
|
return
|
|
}
|
|
|
|
if id == "" {
|
|
id = generateID("user")
|
|
_, err := h.db.Exec("INSERT INTO profiles (id, email, full_name, organization_id, role, password_hash) VALUES (?, ?, ?, ?, ?, 'demo')",
|
|
id, email, fullName, profile.OrganizationID, role)
|
|
if err != nil {
|
|
http.Error(w, "Error creating user", 500)
|
|
return
|
|
}
|
|
} else {
|
|
_, err := h.db.Exec("UPDATE profiles SET email=?, full_name=?, role=? WHERE id=? AND organization_id=?",
|
|
email, fullName, role, id, profile.OrganizationID)
|
|
if err != nil {
|
|
http.Error(w, "Error updating user", 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleAdminUserDelete(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" || id == profile.ID {
|
|
http.Error(w, "Cannot delete yourself", 400)
|
|
return
|
|
}
|
|
|
|
h.db.Exec("DELETE FROM sessions WHERE user_id = ?", id)
|
|
h.db.Exec("DELETE FROM profiles WHERE id = ? AND organization_id = ?", id, profile.OrganizationID)
|
|
http.Redirect(w, r, "/admin/users", http.StatusSeeOther)
|
|
}
|
|
|
|
// --- Organizations CRUD ---
|
|
|
|
func (h *Handler) handleAdminOrgs(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
|
|
rows, err := h.db.Query("SELECT id, name, slug, created_at FROM organizations ORDER BY name")
|
|
if err != nil {
|
|
http.Error(w, "Error loading organizations", 500)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var orgs []*model.Organization
|
|
for rows.Next() {
|
|
o := &model.Organization{}
|
|
rows.Scan(&o.ID, &o.Name, &o.Slug, &o.CreatedAt)
|
|
orgs = append(orgs, o)
|
|
}
|
|
|
|
templates.AdminOrgs(profile, orgs).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminOrgForm(w http.ResponseWriter, r *http.Request) {
|
|
profile := getProfile(r.Context())
|
|
id := r.URL.Query().Get("id")
|
|
|
|
var org model.Organization
|
|
if id != "" {
|
|
h.db.QueryRow("SELECT id, name, slug FROM organizations WHERE id = ?", id).Scan(&org.ID, &org.Name, &org.Slug)
|
|
}
|
|
|
|
templates.AdminOrgForm(profile, &org).Render(r.Context(), w)
|
|
}
|
|
|
|
func (h *Handler) handleAdminOrgSave(w http.ResponseWriter, r *http.Request) {
|
|
r.ParseForm()
|
|
|
|
id := r.FormValue("id")
|
|
name := strings.TrimSpace(r.FormValue("name"))
|
|
slug := strings.TrimSpace(r.FormValue("slug"))
|
|
|
|
if name == "" || slug == "" {
|
|
http.Error(w, "Name and slug are required", 400)
|
|
return
|
|
}
|
|
|
|
if id == "" {
|
|
id = generateID("org")
|
|
_, err := h.db.Exec("INSERT INTO organizations (id, name, slug) VALUES (?, ?, ?)", id, name, slug)
|
|
if err != nil {
|
|
http.Error(w, "Error creating organization", 500)
|
|
return
|
|
}
|
|
} else {
|
|
_, err := h.db.Exec("UPDATE organizations SET name=?, slug=? WHERE id=?", name, slug, id)
|
|
if err != nil {
|
|
http.Error(w, "Error updating organization", 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
http.Redirect(w, r, "/admin/organizations", http.StatusSeeOther)
|
|
}
|
|
|
|
func (h *Handler) handleAdminOrgDelete(w http.ResponseWriter, r *http.Request) {
|
|
id := r.URL.Query().Get("id")
|
|
if id == "" {
|
|
http.Error(w, "Missing id", 400)
|
|
return
|
|
}
|
|
|
|
// Check if org has users
|
|
var count int
|
|
h.db.QueryRow("SELECT COUNT(*) FROM profiles WHERE organization_id = ?", id).Scan(&count)
|
|
if count > 0 {
|
|
http.Error(w, "Cannot delete organization with users", 400)
|
|
return
|
|
}
|
|
|
|
h.db.Exec("DELETE FROM organizations WHERE id = ?", id)
|
|
http.Redirect(w, r, "/admin/organizations", http.StatusSeeOther)
|
|
}
|