feat: orgs page — full edit (all fields), delete with deal-membership warning, OrgDeals + DeleteOrg endpoints
This commit is contained in:
parent
83a3a43bff
commit
412f9f7b12
158
api/handlers.go
158
api/handlers.go
|
|
@ -1550,6 +1550,140 @@ func (h *Handlers) GetOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateOrg handles PATCH /api/orgs/{orgID} — update an organization
|
// UpdateOrg handles PATCH /api/orgs/{orgID} — update an organization
|
||||||
|
// OrgDeals returns a list of deals (projects) this org is currently part of.
|
||||||
|
func (h *Handlers) OrgDeals(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actorID := UserIDFromContext(r.Context())
|
||||||
|
orgID := chi.URLParam(r, "orgID")
|
||||||
|
|
||||||
|
isSuperAdmin, _ := lib.IsSuperAdmin(h.DB, actorID)
|
||||||
|
isIBAdmin := h.isIBAdminAnywhere(actorID)
|
||||||
|
if !isSuperAdmin && !isIBAdmin {
|
||||||
|
ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all deal_org entries referencing this org_id
|
||||||
|
rows, err := h.DB.Conn.Query(
|
||||||
|
`SELECT e.project_id FROM entries e
|
||||||
|
WHERE e.type = 'deal_org' AND e.deleted_at IS NULL`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(w, http.StatusInternalServerError, "internal", "Query failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var projectIDs []string
|
||||||
|
for rows.Next() {
|
||||||
|
var pid string
|
||||||
|
rows.Scan(&pid)
|
||||||
|
// We need to decrypt each to check org_id — collect all first
|
||||||
|
projectIDs = append(projectIDs, pid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// More efficient: query deal_orgs and decrypt to find org matches
|
||||||
|
rows2, err := h.DB.Conn.Query(
|
||||||
|
`SELECT e.entry_id, e.project_id, e.data, e.key_version FROM entries e
|
||||||
|
WHERE e.type = 'deal_org' AND e.deleted_at IS NULL`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(w, http.StatusInternalServerError, "internal", "Query failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer rows2.Close()
|
||||||
|
|
||||||
|
matchedProjectIDs := map[string]bool{}
|
||||||
|
for rows2.Next() {
|
||||||
|
var entryID, projID string
|
||||||
|
var data []byte
|
||||||
|
var keyVer int
|
||||||
|
rows2.Scan(&entryID, &projID, &data, &keyVer)
|
||||||
|
if len(data) == 0 { continue }
|
||||||
|
key, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projID)
|
||||||
|
if err != nil { continue }
|
||||||
|
dataText, err := lib.Unpack(key, data)
|
||||||
|
if err != nil { continue }
|
||||||
|
var dod lib.DealOrgData
|
||||||
|
if json.Unmarshal([]byte(dataText), &dod) == nil && dod.OrgID == orgID {
|
||||||
|
matchedProjectIDs[projID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch project names for matched project IDs
|
||||||
|
type dealRef struct {
|
||||||
|
ProjectID string `json:"project_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
var deals []dealRef
|
||||||
|
for projID := range matchedProjectIDs {
|
||||||
|
proj, err := lib.EntryByID(h.DB, h.Cfg, projID)
|
||||||
|
if err != nil || proj == nil { continue }
|
||||||
|
name := proj.SummaryText
|
||||||
|
if name == "" { name = projID }
|
||||||
|
deals = append(deals, dealRef{ProjectID: projID, Name: name})
|
||||||
|
}
|
||||||
|
if deals == nil { deals = []dealRef{} }
|
||||||
|
JSONResponse(w, http.StatusOK, deals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteOrg hard-deletes an organization and all its deal_org links.
|
||||||
|
func (h *Handlers) DeleteOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
actorID := UserIDFromContext(r.Context())
|
||||||
|
orgID := chi.URLParam(r, "orgID")
|
||||||
|
|
||||||
|
isSuperAdmin, _ := lib.IsSuperAdmin(h.DB, actorID)
|
||||||
|
isIBAdmin := h.isIBAdminAnywhere(actorID)
|
||||||
|
if !isSuperAdmin && !isIBAdmin {
|
||||||
|
ErrorResponse(w, http.StatusForbidden, "access_denied", "Only IB admins or super admins can delete organizations")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
org, err := lib.EntryByID(h.DB, h.Cfg, orgID)
|
||||||
|
if err != nil || org == nil || org.Type != lib.TypeOrganization {
|
||||||
|
ErrorResponse(w, http.StatusNotFound, "not_found", "Organization not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft-delete the org entry
|
||||||
|
now := time.Now().UnixMilli()
|
||||||
|
_, err = h.DB.Conn.Exec(
|
||||||
|
`UPDATE entries SET deleted_at = ?, deleted_by = ?, updated_at = ? WHERE entry_id = ? AND deleted_at IS NULL`,
|
||||||
|
now, actorID, now, orgID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to delete organization")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also soft-delete all deal_org entries that reference this org
|
||||||
|
// (need to scan since org_id is encrypted in data)
|
||||||
|
rows, err := h.DB.Conn.Query(
|
||||||
|
`SELECT entry_id, project_id, data FROM entries WHERE type = 'deal_org' AND deleted_at IS NULL`,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var entryID, projID string
|
||||||
|
var data []byte
|
||||||
|
rows.Scan(&entryID, &projID, &data)
|
||||||
|
if len(data) == 0 { continue }
|
||||||
|
key, err := lib.DeriveProjectKey(h.Cfg.MasterKey, projID)
|
||||||
|
if err != nil { continue }
|
||||||
|
dataText, err := lib.Unpack(key, data)
|
||||||
|
if err != nil { continue }
|
||||||
|
var dod lib.DealOrgData
|
||||||
|
if json.Unmarshal([]byte(dataText), &dod) == nil && dod.OrgID == orgID {
|
||||||
|
h.DB.Conn.Exec(
|
||||||
|
`UPDATE entries SET deleted_at = ?, deleted_by = ?, updated_at = ? WHERE entry_id = ? AND deleted_at IS NULL`,
|
||||||
|
now, actorID, now, entryID,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JSONResponse(w, http.StatusOK, map[string]any{"ok": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handlers) UpdateOrg(w http.ResponseWriter, r *http.Request) {
|
func (h *Handlers) UpdateOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
actorID := UserIDFromContext(r.Context())
|
actorID := UserIDFromContext(r.Context())
|
||||||
orgID := chi.URLParam(r, "orgID")
|
orgID := chi.URLParam(r, "orgID")
|
||||||
|
|
@ -1575,6 +1709,14 @@ func (h *Handlers) UpdateOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
Role *string `json:"role"`
|
Role *string `json:"role"`
|
||||||
Website *string `json:"website"`
|
Website *string `json:"website"`
|
||||||
Description *string `json:"description"`
|
Description *string `json:"description"`
|
||||||
|
Industry *string `json:"industry"`
|
||||||
|
Phone *string `json:"phone"`
|
||||||
|
Address *string `json:"address"`
|
||||||
|
City *string `json:"city"`
|
||||||
|
State *string `json:"state"`
|
||||||
|
Founded *string `json:"founded"`
|
||||||
|
LinkedIn *string `json:"linkedin"`
|
||||||
|
Logo *string `json:"logo"`
|
||||||
ContactName *string `json:"contact_name"`
|
ContactName *string `json:"contact_name"`
|
||||||
ContactEmail *string `json:"contact_email"`
|
ContactEmail *string `json:"contact_email"`
|
||||||
Version int `json:"version"`
|
Version int `json:"version"`
|
||||||
|
|
@ -1618,12 +1760,16 @@ func (h *Handlers) UpdateOrg(w http.ResponseWriter, r *http.Request) {
|
||||||
if req.Description != nil {
|
if req.Description != nil {
|
||||||
orgData.Description = *req.Description
|
orgData.Description = *req.Description
|
||||||
}
|
}
|
||||||
if req.ContactName != nil {
|
if req.Industry != nil { orgData.Industry = *req.Industry }
|
||||||
orgData.ContactName = *req.ContactName
|
if req.Phone != nil { orgData.Phone = *req.Phone }
|
||||||
}
|
if req.Address != nil { orgData.Address = *req.Address }
|
||||||
if req.ContactEmail != nil {
|
if req.City != nil { orgData.City = *req.City }
|
||||||
orgData.ContactEmail = *req.ContactEmail
|
if req.State != nil { orgData.State = *req.State }
|
||||||
}
|
if req.Founded != nil { orgData.Founded = *req.Founded }
|
||||||
|
if req.LinkedIn != nil { orgData.LinkedIn = *req.LinkedIn }
|
||||||
|
if req.Logo != nil { orgData.Logo = *req.Logo }
|
||||||
|
if req.ContactName != nil { orgData.ContactName = *req.ContactName }
|
||||||
|
if req.ContactEmail != nil { orgData.ContactEmail = *req.ContactEmail }
|
||||||
|
|
||||||
dataJSON, _ := json.Marshal(orgData)
|
dataJSON, _ := json.Marshal(orgData)
|
||||||
org.DataText = string(dataJSON)
|
org.DataText = string(dataJSON)
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,9 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
|
||||||
// Organizations (platform level)
|
// Organizations (platform level)
|
||||||
r.Get("/orgs", h.ListOrgs)
|
r.Get("/orgs", h.ListOrgs)
|
||||||
r.Post("/orgs", h.CreateOrg)
|
r.Post("/orgs", h.CreateOrg)
|
||||||
|
r.Get("/orgs/{orgID}/deals", h.OrgDeals)
|
||||||
r.Put("/orgs/{orgID}", h.UpdateOrg)
|
r.Put("/orgs/{orgID}", h.UpdateOrg)
|
||||||
|
r.Delete("/orgs/{orgID}", h.DeleteOrg)
|
||||||
r.Put("/admin/test-role", h.SetTestRole)
|
r.Put("/admin/test-role", h.SetTestRole)
|
||||||
r.Get("/orgs/{orgID}", h.GetOrg)
|
r.Get("/orgs/{orgID}", h.GetOrg)
|
||||||
r.Patch("/orgs/{orgID}", h.UpdateOrg)
|
r.Patch("/orgs/{orgID}", h.UpdateOrg)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
|
<h2 class="text-xl font-semibold text-white mb-6">New Organization</h2>
|
||||||
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
<div id="modalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
|
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name <span class="text-red-400">*</span></label>
|
||||||
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
<input id="oName" type="text" placeholder="Blackstone Group" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
|
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains <span class="text-red-400">*</span></label>
|
||||||
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
<input id="oDomains" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
|
@ -43,30 +43,102 @@
|
||||||
|
|
||||||
<!-- Edit Org Modal -->
|
<!-- Edit Org Modal -->
|
||||||
<div id="editOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
<div id="editOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
||||||
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-8 w-full max-w-md">
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<div class="flex items-center justify-between mb-6">
|
<div class="flex items-center justify-between px-6 pt-5 pb-4 border-b border-white/[0.08]">
|
||||||
<h2 class="text-xl font-semibold text-white">Edit Organization</h2>
|
<div class="flex items-center gap-3">
|
||||||
|
<div id="eLogoWrap" class="hidden w-10 h-10 rounded-lg border border-white/[0.08] overflow-hidden bg-white flex items-center justify-center shrink-0">
|
||||||
|
<img id="eLogoImg" src="" class="max-w-full max-h-full object-contain">
|
||||||
|
</div>
|
||||||
|
<h2 class="text-lg font-semibold text-white">Edit Organization</h2>
|
||||||
|
</div>
|
||||||
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">×</button>
|
<button onclick="closeEditModal()" class="text-[#b0bec5] hover:text-white text-2xl leading-none">×</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="editModalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
<div class="p-6">
|
||||||
<div class="space-y-4">
|
<input type="hidden" id="eOrgId">
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Name</label>
|
<input type="hidden" id="eVersion">
|
||||||
<input id="eName" type="text" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]"></div>
|
<div id="editModalError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Email Domains</label>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]">
|
<div class="col-span-2">
|
||||||
<p class="text-[#8899a6] text-xs mt-1">Comma-separated.</p></div>
|
<label class="block text-xs text-[#b0bec5] mb-1">Name <span class="text-red-400">*</span></label>
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Role</label>
|
<input id="eName" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
<select id="eRole" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white focus:outline-none focus:border-[#c9a84c]">
|
</div>
|
||||||
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
<div class="col-span-2">
|
||||||
</select></div>
|
<label class="block text-xs text-[#b0bec5] mb-1">Description</label>
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Website</label>
|
<textarea id="eDesc" rows="3" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c] resize-none"></textarea>
|
||||||
<input id="eWebsite" type="text" placeholder="blackstone.com" class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c]"></div>
|
</div>
|
||||||
<div><label class="block text-sm font-medium text-[#b0bec5] mb-1.5">Description</label>
|
<div>
|
||||||
<textarea id="eDesc" rows="2" placeholder="Optional notes..." class="w-full px-4 py-2.5 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white placeholder-[#8899a6] focus:outline-none focus:border-[#c9a84c] resize-none"></textarea></div>
|
<label class="block text-xs text-[#b0bec5] mb-1">Role</label>
|
||||||
|
<select id="eRole" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
<option value="seller">Seller</option><option value="buyer">Buyer</option><option value="ib">IB Advisor</option><option value="advisor">Advisor</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Industry</label>
|
||||||
|
<input id="eIndustry" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Website</label>
|
||||||
|
<input id="eWebsite" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Phone</label>
|
||||||
|
<input id="ePhone" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">LinkedIn</label>
|
||||||
|
<input id="eLinkedIn" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Founded</label>
|
||||||
|
<input id="eFounded" type="text" placeholder="e.g. 1985" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Address</label>
|
||||||
|
<input id="eAddress" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">City</label>
|
||||||
|
<input id="eCity" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">State</label>
|
||||||
|
<input id="eState" type="text" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Email Domains</label>
|
||||||
|
<input id="eDomains" type="text" placeholder="blackstone.com, pe.blackstone.com" class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]">
|
||||||
|
<p class="text-[#8899a6] text-xs mt-1">Comma-separated. Used to validate invite emails.</p>
|
||||||
|
</div>
|
||||||
|
<div class="col-span-2">
|
||||||
|
<label class="block text-xs text-[#b0bec5] mb-1">Logo URL</label>
|
||||||
|
<input id="eLogo" type="text" placeholder="https://..." class="w-full px-3 py-2 bg-[#0a1628] border border-white/[0.08] rounded-lg text-white text-sm focus:outline-none focus:border-[#c9a84c]"
|
||||||
|
oninput="previewEditLogo()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-3 mt-6">
|
<div class="flex gap-3 px-6 pb-6">
|
||||||
<button onclick="closeEditModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
<button onclick="confirmDeleteOrg()" class="px-4 py-2.5 rounded-lg text-sm font-medium transition" style="background:rgba(239,68,68,0.1);color:#ef4444;border:1px solid rgba(239,68,68,0.2)">Delete org</button>
|
||||||
<button id="saveOrgBtn" onclick="saveOrg()" class="flex-1 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Save</button>
|
<div class="flex-1"></div>
|
||||||
|
<button onclick="closeEditModal()" class="px-4 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
||||||
|
<button id="saveOrgBtn" onclick="saveOrg()" class="px-6 py-2.5 bg-[#c9a84c] hover:bg-[#b8973f] text-[#0a1628] font-semibold rounded-lg text-sm transition">Save Changes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<div id="deleteOrgModal" class="hidden fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-4">
|
||||||
|
<div class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl w-full max-w-md p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-white mb-2">Delete Organization</h2>
|
||||||
|
<p class="text-[#b0bec5] text-sm mb-4">You are about to permanently delete <strong id="deleteOrgName" class="text-white"></strong> from the company registry.</p>
|
||||||
|
<div id="deleteDealsWrap" class="hidden mb-4 p-4 rounded-lg" style="background:rgba(239,68,68,0.08);border:1px solid rgba(239,68,68,0.2)">
|
||||||
|
<p class="text-red-400 text-sm font-medium mb-2">⚠️ This org is currently active in the following deals:</p>
|
||||||
|
<ul id="dealsList" class="text-sm text-[#b0bec5] space-y-1 list-disc list-inside"></ul>
|
||||||
|
<p class="text-red-400 text-xs mt-2">Deleting will also remove them from all these deals.</p>
|
||||||
|
</div>
|
||||||
|
<div id="deleteOrgError" class="hidden mb-4 p-3 bg-red-500/10 border border-red-500/20 rounded-lg text-red-400 text-sm"></div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button onclick="closeDeleteModal()" class="flex-1 py-2.5 bg-white/[0.05] hover:bg-white/[0.08] text-white rounded-lg text-sm font-medium transition">Cancel</button>
|
||||||
|
<button id="confirmDeleteBtn" onclick="executeDeleteOrg()" class="flex-1 py-2.5 rounded-lg text-sm font-semibold transition" style="background:#ef4444;color:white">Delete permanently</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -74,7 +146,7 @@
|
||||||
|
|
||||||
{{define "scripts"}}
|
{{define "scripts"}}
|
||||||
<script>
|
<script>
|
||||||
if (user.is_super_admin) document.getElementById('newOrgBtn').classList.remove('hidden');
|
if (user.is_super_admin || user.role === 'ib_admin') document.getElementById('newOrgBtn').classList.remove('hidden');
|
||||||
|
|
||||||
const roleColors = {
|
const roleColors = {
|
||||||
seller: 'bg-blue-500/20 text-blue-300',
|
seller: 'bg-blue-500/20 text-blue-300',
|
||||||
|
|
@ -93,7 +165,6 @@
|
||||||
document.getElementById('emptyState').classList.remove('hidden');
|
document.getElementById('emptyState').classList.remove('hidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Store orgs in map for safe click lookup
|
|
||||||
window._orgsMap = {};
|
window._orgsMap = {};
|
||||||
orgs.forEach(o => { window._orgsMap[o.entry_id] = o; });
|
orgs.forEach(o => { window._orgsMap[o.entry_id] = o; });
|
||||||
|
|
||||||
|
|
@ -102,14 +173,19 @@
|
||||||
const role = o.role || '';
|
const role = o.role || '';
|
||||||
const rc = roleColors[role] || 'bg-gray-500/20 text-gray-300';
|
const rc = roleColors[role] || 'bg-gray-500/20 text-gray-300';
|
||||||
const domains = Array.isArray(o.domains) ? o.domains : (o.domains ? [o.domains] : []);
|
const domains = Array.isArray(o.domains) ? o.domains : (o.domains ? [o.domains] : []);
|
||||||
|
const logo = o.logo || '';
|
||||||
return `<div onclick="openEditModal('${o.entry_id}')"
|
return `<div onclick="openEditModal('${o.entry_id}')"
|
||||||
class="card bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-6 cursor-pointer hover:border-white/[0.2] transition">
|
class="bg-[#0d1f3c] border border-white/[0.08] rounded-xl p-5 cursor-pointer hover:border-white/[0.2] transition">
|
||||||
<div class="flex items-start justify-between mb-3">
|
<div class="flex items-start justify-between gap-3 mb-3">
|
||||||
<h3 class="text-white font-semibold leading-tight">${escHtml(name)}</h3>
|
<div class="flex items-center gap-3 min-w-0">
|
||||||
<span class="ml-2 shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '—'}</span>
|
${logo ? `<div class="w-10 h-10 rounded-lg border border-white/[0.08] overflow-hidden bg-white shrink-0 flex items-center justify-center"><img src="${escHtml(logo)}" class="max-w-full max-h-full object-contain" onerror="this.parentElement.style.display='none'"></div>` : ''}
|
||||||
|
<h3 class="text-white font-semibold leading-tight truncate">${escHtml(name)}</h3>
|
||||||
|
</div>
|
||||||
|
<span class="shrink-0 px-2 py-0.5 rounded-full text-xs font-medium capitalize ${rc}">${role || '—'}</span>
|
||||||
</div>
|
</div>
|
||||||
|
${o.description ? `<p class="text-xs text-[#94a3b8] mb-2 line-clamp-2">${escHtml(o.description)}</p>` : ''}
|
||||||
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
<div class="flex gap-1.5 flex-wrap">${domains.map(dm => `<span class="text-xs font-mono text-[#b0bec5] bg-white/[0.05] px-2 py-0.5 rounded">@${escHtml(dm)}</span>`).join('')}</div>
|
||||||
${o.website ? `<div class="mt-2 text-xs" style="color:var(--ds-tx3)">${escHtml(o.website)}</div>` : ''}
|
${o.website ? `<div class="mt-2 text-xs text-[#64748b]">${escHtml(o.website)}</div>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
|
|
@ -150,12 +226,29 @@
|
||||||
const o = (window._orgsMap || {})[entryId];
|
const o = (window._orgsMap || {})[entryId];
|
||||||
if (!o) return;
|
if (!o) return;
|
||||||
editingOrgId = o.entry_id;
|
editingOrgId = o.entry_id;
|
||||||
|
document.getElementById('eOrgId').value = o.entry_id;
|
||||||
|
document.getElementById('eVersion').value = o.version || 1;
|
||||||
document.getElementById('eName').value = o.name || '';
|
document.getElementById('eName').value = o.name || '';
|
||||||
document.getElementById('eRole').value = o.role || 'seller';
|
|
||||||
document.getElementById('eDomains').value = Array.isArray(o.domains) ? o.domains.join(', ') : (o.domains || '');
|
|
||||||
document.getElementById('eWebsite').value = o.website || '';
|
|
||||||
document.getElementById('eDesc').value = o.description || '';
|
document.getElementById('eDesc').value = o.description || '';
|
||||||
|
document.getElementById('eRole').value = o.role || 'seller';
|
||||||
|
document.getElementById('eIndustry').value = o.industry || '';
|
||||||
|
document.getElementById('eWebsite').value = o.website || '';
|
||||||
|
document.getElementById('ePhone').value = o.phone || '';
|
||||||
|
document.getElementById('eLinkedIn').value = o.linkedin || '';
|
||||||
|
document.getElementById('eFounded').value = o.founded || '';
|
||||||
|
document.getElementById('eAddress').value = o.address || '';
|
||||||
|
document.getElementById('eCity').value = o.city || '';
|
||||||
|
document.getElementById('eState').value = o.state || '';
|
||||||
|
document.getElementById('eDomains').value = Array.isArray(o.domains) ? o.domains.join(', ') : (o.domains || '');
|
||||||
|
document.getElementById('eLogo').value = o.logo || '';
|
||||||
document.getElementById('editModalError').classList.add('hidden');
|
document.getElementById('editModalError').classList.add('hidden');
|
||||||
|
const logo = o.logo || '';
|
||||||
|
if (logo) {
|
||||||
|
document.getElementById('eLogoImg').src = logo;
|
||||||
|
document.getElementById('eLogoWrap').classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
document.getElementById('eLogoWrap').classList.add('hidden');
|
||||||
|
}
|
||||||
document.getElementById('editOrgModal').classList.remove('hidden');
|
document.getElementById('editOrgModal').classList.remove('hidden');
|
||||||
document.getElementById('eName').focus();
|
document.getElementById('eName').focus();
|
||||||
}
|
}
|
||||||
|
|
@ -165,27 +258,88 @@
|
||||||
editingOrgId = null;
|
editingOrgId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function previewEditLogo() {
|
||||||
|
const url = document.getElementById('eLogo').value.trim();
|
||||||
|
const img = document.getElementById('eLogoImg');
|
||||||
|
const wrap = document.getElementById('eLogoWrap');
|
||||||
|
if (url) { img.src = url; wrap.classList.remove('hidden'); img.onerror = () => wrap.classList.add('hidden'); }
|
||||||
|
else { wrap.classList.add('hidden'); }
|
||||||
|
}
|
||||||
|
|
||||||
async function saveOrg() {
|
async function saveOrg() {
|
||||||
const name = document.getElementById('eName').value.trim();
|
const name = document.getElementById('eName').value.trim();
|
||||||
const domainsRaw = document.getElementById('eDomains').value.trim();
|
const domainsRaw = document.getElementById('eDomains').value.trim();
|
||||||
const role = document.getElementById('eRole').value;
|
|
||||||
const website = document.getElementById('eWebsite').value.trim().replace(/^https?:\/\//, '');
|
|
||||||
const description = document.getElementById('eDesc').value.trim();
|
|
||||||
const errEl = document.getElementById('editModalError');
|
const errEl = document.getElementById('editModalError');
|
||||||
const btn = document.getElementById('saveOrgBtn');
|
const btn = document.getElementById('saveOrgBtn');
|
||||||
if (!name) { errEl.textContent = 'Name is required'; errEl.classList.remove('hidden'); return; }
|
if (!name) { errEl.textContent = 'Name is required'; errEl.classList.remove('hidden'); return; }
|
||||||
const domains = domainsRaw.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean);
|
const domains = domainsRaw ? domainsRaw.split(',').map(d => d.trim().replace(/^@/, '')).filter(Boolean) : undefined;
|
||||||
btn.disabled = true; btn.textContent = 'Saving...'; errEl.classList.add('hidden');
|
btn.disabled = true; btn.textContent = 'Saving...'; errEl.classList.add('hidden');
|
||||||
try {
|
try {
|
||||||
const res = await fetchAPI('/api/orgs/' + editingOrgId, {
|
const res = await fetchAPI('/api/orgs/' + editingOrgId, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ name, domains, role, website, description })
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
domains,
|
||||||
|
role: document.getElementById('eRole').value,
|
||||||
|
description: document.getElementById('eDesc').value.trim(),
|
||||||
|
industry: document.getElementById('eIndustry').value.trim(),
|
||||||
|
website: document.getElementById('eWebsite').value.trim(),
|
||||||
|
phone: document.getElementById('ePhone').value.trim(),
|
||||||
|
linkedin: document.getElementById('eLinkedIn').value.trim(),
|
||||||
|
founded: document.getElementById('eFounded').value.trim(),
|
||||||
|
address: document.getElementById('eAddress').value.trim(),
|
||||||
|
city: document.getElementById('eCity').value.trim(),
|
||||||
|
state: document.getElementById('eState').value.trim(),
|
||||||
|
logo: document.getElementById('eLogo').value.trim(),
|
||||||
|
version: parseInt(document.getElementById('eVersion').value) || 1,
|
||||||
|
})
|
||||||
});
|
});
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
if (!res.ok) throw new Error(data.error || 'Failed to save');
|
if (!res.ok) throw new Error(data.error || 'Failed to save');
|
||||||
closeEditModal();
|
closeEditModal();
|
||||||
loadOrgs();
|
loadOrgs();
|
||||||
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Save'; }
|
} catch(e) { errEl.textContent = e.message; errEl.classList.remove('hidden'); btn.disabled = false; btn.textContent = 'Save Changes'; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Delete Org ----
|
||||||
|
async function confirmDeleteOrg() {
|
||||||
|
if (!editingOrgId) return;
|
||||||
|
const o = (window._orgsMap || {})[editingOrgId];
|
||||||
|
document.getElementById('deleteOrgName').textContent = o?.name || editingOrgId;
|
||||||
|
document.getElementById('deleteOrgError').classList.add('hidden');
|
||||||
|
document.getElementById('deleteDealsWrap').classList.add('hidden');
|
||||||
|
document.getElementById('dealsList').innerHTML = '';
|
||||||
|
document.getElementById('deleteOrgModal').classList.remove('hidden');
|
||||||
|
|
||||||
|
// Fetch deals this org is in
|
||||||
|
try {
|
||||||
|
const res = await fetchAPI('/api/orgs/' + editingOrgId + '/deals');
|
||||||
|
const deals = await res.json();
|
||||||
|
if (deals && deals.length > 0) {
|
||||||
|
document.getElementById('dealsList').innerHTML = deals.map(d => `<li>${escHtml(d.name)}</li>`).join('');
|
||||||
|
document.getElementById('deleteDealsWrap').classList.remove('hidden');
|
||||||
|
}
|
||||||
|
} catch(e) { /* non-fatal */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('deleteOrgModal').classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function executeDeleteOrg() {
|
||||||
|
const btn = document.getElementById('confirmDeleteBtn');
|
||||||
|
const errEl = document.getElementById('deleteOrgError');
|
||||||
|
btn.disabled = true; btn.textContent = 'Deleting...'; errEl.classList.add('hidden');
|
||||||
|
try {
|
||||||
|
const res = await fetchAPI('/api/orgs/' + editingOrgId, { method: 'DELETE' });
|
||||||
|
if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Delete failed'); }
|
||||||
|
closeDeleteModal();
|
||||||
|
closeEditModal();
|
||||||
|
loadOrgs();
|
||||||
|
} catch(e) {
|
||||||
|
errEl.textContent = e.message; errEl.classList.remove('hidden');
|
||||||
|
btn.disabled = false; btn.textContent = 'Delete permanently';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOrgs();
|
loadOrgs();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue