diff --git a/api/handlers.go b/api/handlers.go
index d0174e3..04a1e21 100644
--- a/api/handlers.go
+++ b/api/handlers.go
@@ -1365,6 +1365,12 @@ func (h *Handlers) orgToMap(org *lib.Entry) map[string]any {
result["website"] = orgData.Website
result["description"] = orgData.Description
result["industry"] = orgData.Industry
+ result["phone"] = orgData.Phone
+ result["address"] = orgData.Address
+ result["city"] = orgData.City
+ result["state"] = orgData.State
+ result["founded"] = orgData.Founded
+ result["linkedin"] = orgData.LinkedIn
result["contact_name"] = orgData.ContactName
result["contact_email"] = orgData.ContactEmail
}
@@ -1641,6 +1647,96 @@ func (h *Handlers) UpdateOrg(w http.ResponseWriter, r *http.Request) {
// ---------------------------------------------------------------------------
// ListDealOrgs handles GET /api/projects/{projectID}/orgs — list orgs in this deal
+// UpdateDealOrg handles PATCH /api/projects/{projectID}/orgs/{dealOrgID}
+// Updates org details (on the organization entry) and deal-specific role (on the deal_org entry).
+func (h *Handlers) UpdateDealOrg(w http.ResponseWriter, r *http.Request) {
+ actorID := UserIDFromContext(r.Context())
+ projectID := chi.URLParam(r, "projectID")
+ dealOrgID := chi.URLParam(r, "dealOrgID")
+
+ if err := lib.CheckAccessWrite(h.DB, actorID, projectID, ""); err != nil {
+ ErrorResponse(w, http.StatusForbidden, "access_denied", "Access denied")
+ return
+ }
+
+ var body struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Role string `json:"role"`
+ Industry string `json:"industry"`
+ Website string `json:"website"`
+ 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"`
+ Version int `json:"version"`
+ }
+ if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
+ ErrorResponse(w, http.StatusBadRequest, "invalid_body", "Invalid request body")
+ return
+ }
+ if strings.TrimSpace(body.Name) == "" {
+ ErrorResponse(w, http.StatusBadRequest, "name_required", "Name is required")
+ return
+ }
+
+ // Load the deal_org entry to get org_id and current data
+ dealOrgEntry, err := lib.EntryByID(h.DB, h.Cfg, dealOrgID)
+ if err != nil || dealOrgEntry == nil {
+ ErrorResponse(w, http.StatusNotFound, "not_found", "Deal org not found")
+ return
+ }
+ var dealOrgData lib.DealOrgData
+ json.Unmarshal([]byte(dealOrgEntry.DataText), &dealOrgData)
+
+ // Update role on deal_org if changed
+ if body.Role != "" && body.Role != dealOrgData.Role {
+ dealOrgData.Role = body.Role
+ dealOrgJSON, _ := json.Marshal(dealOrgData)
+ dealOrgEntry.DataText = string(dealOrgJSON)
+ if err := lib.EntryWrite(h.DB, h.Cfg, actorID, dealOrgEntry); err != nil {
+ ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to update deal org role")
+ return
+ }
+ }
+
+ // Load the organization entry and update its details
+ orgEntry, err := lib.EntryByID(h.DB, h.Cfg, dealOrgData.OrgID)
+ if err != nil || orgEntry == nil {
+ ErrorResponse(w, http.StatusNotFound, "not_found", "Organization not found")
+ return
+ }
+ var orgData lib.OrgData
+ json.Unmarshal([]byte(orgEntry.DataText), &orgData)
+
+ orgData.Name = strings.TrimSpace(body.Name)
+ orgData.Description = body.Description
+ if body.Role != "" { orgData.Role = body.Role }
+ orgData.Industry = body.Industry
+ orgData.Website = body.Website
+ orgData.Phone = body.Phone
+ orgData.Address = body.Address
+ orgData.City = body.City
+ orgData.State = body.State
+ orgData.Founded = body.Founded
+ orgData.LinkedIn = body.LinkedIn
+ if body.Logo != "" { orgData.Logo = body.Logo }
+
+ orgJSON, _ := json.Marshal(orgData)
+ orgEntry.DataText = string(orgJSON)
+ orgEntry.SummaryText = orgData.Name
+
+ if err := lib.EntryWrite(h.DB, h.Cfg, actorID, orgEntry); err != nil {
+ ErrorResponse(w, http.StatusInternalServerError, "internal", "Failed to update organization")
+ return
+ }
+
+ JSONResponse(w, http.StatusOK, map[string]any{"ok": true})
+}
+
func (h *Handlers) ListDealOrgs(w http.ResponseWriter, r *http.Request) {
actorID := UserIDFromContext(r.Context())
projectID := chi.URLParam(r, "projectID")
@@ -1711,6 +1807,12 @@ func (h *Handlers) ListDealOrgs(w http.ResponseWriter, r *http.Request) {
dealOrgMap["org_description"] = orgDetails["description"]
dealOrgMap["org_industry"] = orgDetails["industry"]
dealOrgMap["org_website"] = orgDetails["website"]
+ dealOrgMap["org_phone"] = orgDetails["phone"]
+ dealOrgMap["org_address"] = orgDetails["address"]
+ dealOrgMap["org_city"] = orgDetails["city"]
+ dealOrgMap["org_state"] = orgDetails["state"]
+ dealOrgMap["org_founded"] = orgDetails["founded"]
+ dealOrgMap["org_linkedin"] = orgDetails["linkedin"]
}
}
}
diff --git a/api/routes.go b/api/routes.go
index bbf2c60..a56d465 100644
--- a/api/routes.go
+++ b/api/routes.go
@@ -115,6 +115,7 @@ func NewRouter(db *lib.DB, cfg *lib.Config, store lib.ObjectStore, websiteFS fs.
r.Post("/projects/{projectID}/orgs", h.CreateDealOrg)
r.Delete("/projects/{projectID}/orgs/{dealOrgID}", h.DeleteDealOrg)
r.Post("/projects/{projectID}/orgs/add", h.AddOrgToDeal)
+ r.Patch("/projects/{projectID}/orgs/{dealOrgID}", h.UpdateDealOrg)
// Scrape (LLM-powered org lookup)
r.Post("/scrape/org", h.ScrapeOrg)
diff --git a/portal/templates/app/project.html b/portal/templates/app/project.html
index 088b2e2..6d9e9db 100644
--- a/portal/templates/app/project.html
+++ b/portal/templates/app/project.html
@@ -73,6 +73,86 @@
+
+
+
+
+
Edit Organization
+
+
+
+
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1201,6 +1281,13 @@
_dealOrgsMap = {};
if (Array.isArray(orgs)) orgs.forEach(o => {
o._raw = { org_id: o.org_id, role: o.role, domain_lock: o.domain_lock, permissions: o.permissions, members: o.members };
+ // Store org detail fields for edit modal
+ o.org_details = {
+ name: o.org_name || '', description: o.org_description || '', role: o.role || '',
+ industry: o.org_industry || '', website: o.org_website || '', phone: o.org_phone || '',
+ address: o.org_address || '', city: o.org_city || '', state: o.org_state || '',
+ founded: o.org_founded || '', linkedin: o.org_linkedin || '', logo: o.org_logo || '',
+ };
_dealOrgsMap[o.deal_org_id] = o;
});
const list = document.getElementById('orgList');
@@ -1247,6 +1334,8 @@
+
@@ -1609,11 +1698,13 @@
}
function renderPeople() {
+ try {
const people = scrapedData?.people || [];
const list = document.getElementById('peopleList');
const none = document.getElementById('noPeople');
- if (people.length === 0) { list.innerHTML = ''; none.classList.remove('hidden'); return; }
- none.classList.add('hidden');
+ if (!list) { console.error('peopleList element not found'); return; }
+ if (people.length === 0) { list.innerHTML = ''; if(none) none.classList.remove('hidden'); return; }
+ if(none) none.classList.add('hidden');
list.innerHTML = people.map((p, i) => {
const hasDetail = p.name && p.title;
return `
@@ -1632,8 +1723,10 @@
`;
}).join('');
- document.getElementById('selectAllPeople').checked = true;
+ const sa = document.getElementById('selectAllPeople');
+ if(sa) sa.checked = true;
updateSelectedCount();
+ } catch(err) { alert('renderPeople error: ' + err.message + '\n' + err.stack); }
}
function updatePerson(el) {
@@ -1645,24 +1738,29 @@
}
function addManualPerson() {
- const name = document.getElementById('manualName').value.trim();
- const email = document.getElementById('manualEmail').value.trim();
- const title = document.getElementById('manualTitle').value.trim();
- if (!name && !email) {
- const nameEl = document.getElementById('manualName');
- nameEl.classList.add('field-error');
- nameEl.placeholder = 'Enter a name or email';
- setTimeout(() => { nameEl.classList.remove('field-error'); nameEl.placeholder = 'Name'; }, 2500);
- nameEl.focus();
- return;
+ try {
+ const name = document.getElementById('manualName').value.trim();
+ const email = document.getElementById('manualEmail').value.trim();
+ const title = document.getElementById('manualTitle').value.trim();
+ if (!name && !email) {
+ const nameEl = document.getElementById('manualName');
+ nameEl.classList.add('field-error');
+ nameEl.placeholder = 'Enter a name or email';
+ setTimeout(() => { nameEl.classList.remove('field-error'); nameEl.placeholder = 'Name'; }, 2500);
+ nameEl.focus();
+ return;
+ }
+ if (!scrapedData) scrapedData = { domain: '', people: [] };
+ if (!scrapedData.people) scrapedData.people = [];
+ scrapedData.people.push({ name: name || email, email, title, phone: '', photo: '', bio: '', linkedin: '' });
+ document.getElementById('manualName').value = '';
+ document.getElementById('manualEmail').value = '';
+ document.getElementById('manualTitle').value = '';
+ renderPeople();
+ document.getElementById('manualName').focus();
+ } catch(err) {
+ alert('addManualPerson error: ' + err.message + '\n' + err.stack);
}
- if (!scrapedData) scrapedData = { domain: '', people: [] };
- scrapedData.people.push({ name: name || email, email, title, phone: '', photo: '', bio: '', linkedin: '' });
- document.getElementById('manualName').value = '';
- document.getElementById('manualEmail').value = '';
- document.getElementById('manualTitle').value = '';
- renderPeople();
- document.getElementById('manualName').focus();
}
function toggleAllPeople(checked) {
@@ -1867,6 +1965,122 @@
});
// ---- Permissions Modal ----
+ // ---- Edit Org Modal ----
+ function openEditOrgModal(dealOrgId) {
+ const o = _dealOrgsMap[dealOrgId];
+ if (!o) return;
+ const d = o.org_details || {};
+
+ document.getElementById('editOrgDealOrgId').value = dealOrgId;
+ document.getElementById('editOrgVersion').value = o.version || 1;
+ document.getElementById('editOrgName').value = d.name || o.org_name || '';
+ document.getElementById('editOrgDesc').value = d.description || '';
+ document.getElementById('editOrgRole').value = d.role || o.role || '';
+ document.getElementById('editOrgIndustry').value = d.industry || '';
+ document.getElementById('editOrgWebsite').value = d.website || '';
+ document.getElementById('editOrgPhone').value = d.phone || '';
+ document.getElementById('editOrgLinkedIn').value = d.linkedin || '';
+ document.getElementById('editOrgFounded').value = d.founded || '';
+ document.getElementById('editOrgAddress').value = d.address || '';
+ document.getElementById('editOrgCity').value = d.city || '';
+ document.getElementById('editOrgState').value = d.state || '';
+ document.getElementById('editOrgLogo').value = d.logo || '';
+ document.getElementById('editOrgError').classList.add('hidden');
+
+ const logo = d.logo || '';
+ if (logo) {
+ document.getElementById('editOrgLogoImg').src = logo;
+ document.getElementById('editOrgLogoWrap').classList.remove('hidden');
+ } else {
+ document.getElementById('editOrgLogoWrap').classList.add('hidden');
+ }
+
+ document.getElementById('editOrgModal').classList.remove('hidden');
+ }
+
+ function closeEditOrgModal() {
+ document.getElementById('editOrgModal').classList.add('hidden');
+ }
+
+ function previewEditOrgLogo() {
+ const url = document.getElementById('editOrgLogo').value.trim();
+ const img = document.getElementById('editOrgLogoImg');
+ const wrap = document.getElementById('editOrgLogoWrap');
+ if (url) {
+ img.src = url;
+ wrap.classList.remove('hidden');
+ img.onerror = () => wrap.classList.add('hidden');
+ } else {
+ wrap.classList.add('hidden');
+ }
+ }
+
+ async function saveEditOrg() {
+ const dealOrgId = document.getElementById('editOrgDealOrgId').value;
+ const version = parseInt(document.getElementById('editOrgVersion').value) || 1;
+ const name = document.getElementById('editOrgName').value.trim();
+ const errEl = document.getElementById('editOrgError');
+ errEl.classList.add('hidden');
+
+ if (!name) {
+ document.getElementById('editOrgName').classList.add('field-error');
+ errEl.textContent = 'Name is required.';
+ errEl.classList.remove('hidden');
+ return;
+ }
+
+ const o = _dealOrgsMap[dealOrgId];
+ if (!o) return;
+
+ const updatedRaw = {
+ ...(o._raw || {}),
+ name,
+ description: document.getElementById('editOrgDesc').value.trim(),
+ role: document.getElementById('editOrgRole').value,
+ industry: document.getElementById('editOrgIndustry').value.trim(),
+ website: document.getElementById('editOrgWebsite').value.trim(),
+ phone: document.getElementById('editOrgPhone').value.trim(),
+ linkedin: document.getElementById('editOrgLinkedIn').value.trim(),
+ founded: document.getElementById('editOrgFounded').value.trim(),
+ address: document.getElementById('editOrgAddress').value.trim(),
+ city: document.getElementById('editOrgCity').value.trim(),
+ state: document.getElementById('editOrgState').value.trim(),
+ logo: document.getElementById('editOrgLogo').value.trim(),
+ };
+
+ const btn = document.getElementById('editOrgSaveBtn');
+ btn.disabled = true; btn.textContent = 'Saving...';
+
+ try {
+ const res = await fetchAPI('/api/projects/' + projectID + '/orgs/' + dealOrgId, {
+ method: 'PATCH',
+ body: JSON.stringify({
+ name: updatedRaw.name,
+ description: updatedRaw.description,
+ role: updatedRaw.role,
+ industry: updatedRaw.industry,
+ website: updatedRaw.website,
+ phone: updatedRaw.phone,
+ address: updatedRaw.address,
+ city: updatedRaw.city,
+ state: updatedRaw.state,
+ founded: updatedRaw.founded,
+ linkedin: updatedRaw.linkedin,
+ logo: updatedRaw.logo,
+ version,
+ })
+ });
+ if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Save failed'); }
+ closeEditOrgModal();
+ loadOrgs();
+ } catch(e) {
+ errEl.textContent = e.message;
+ errEl.classList.remove('hidden');
+ } finally {
+ btn.disabled = false; btn.textContent = 'Save Changes';
+ }
+ }
+
// ---- Org card menu ----
function toggleOrgMenu(dealOrgId) {
const drop = document.getElementById('orgMenuDrop_' + dealOrgId);