Seth commited on
Commit
230079f
Β·
1 Parent(s): efad20e
backend/app/main.py CHANGED
@@ -297,6 +297,7 @@ def _crm_lead_to_dict(row: CrmLead) -> dict:
297
  "contact_id": row.contact_id,
298
  "created_at": row.created_at.isoformat() if row.created_at else None,
299
  "updated_at": row.updated_at.isoformat() if row.updated_at else None,
 
300
  }
301
 
302
 
@@ -728,6 +729,90 @@ async def get_contact(contact_id: int, db: Session = Depends(get_db)):
728
  }
729
 
730
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
731
  def _sync_contact_raw_from_columns(contact: Contact) -> None:
732
  rd = dict(contact.raw_data or {})
733
  rd["First Name"] = contact.first_name or ""
@@ -767,6 +852,7 @@ async def patch_contact(contact_id: int, body: ContactPatchRequest, db: Session
767
  if "title" in data:
768
  contact.title = _safe_str(data["title"])
769
  _sync_contact_raw_from_columns(contact)
 
770
  db.commit()
771
  db.refresh(contact)
772
  return {
@@ -1805,6 +1891,14 @@ async def patch_lead(lead_id: int, body: CrmLeadPatchRequest, db: Session = Depe
1805
  detail="Another lead already uses this email",
1806
  )
1807
  row.email = email
 
 
 
 
 
 
 
 
1808
  row.updated_at = datetime.utcnow()
1809
  db.commit()
1810
  db.refresh(row)
@@ -1964,7 +2058,7 @@ async def get_deal(deal_id: int, db: Session = Depends(get_db)):
1964
  row = db.query(CrmDeal).filter(CrmDeal.id == deal_id).first()
1965
  if not row:
1966
  raise HTTPException(status_code=404, detail="Deal not found")
1967
- return _deal_to_dict(row)
1968
 
1969
 
1970
  @app.patch("/api/deals/{deal_id}")
@@ -2002,10 +2096,27 @@ async def patch_deal(deal_id: int, body: CrmDealPatchRequest, db: Session = Depe
2002
  if "last_interaction_at" in data:
2003
  ts = _to_datetime(data["last_interaction_at"])
2004
  row.last_interaction_at = ts
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2005
  row.updated_at = datetime.utcnow()
2006
  db.commit()
2007
  db.refresh(row)
2008
- return _deal_to_dict(row)
2009
 
2010
 
2011
  @app.post("/api/deals/seed-demo")
 
297
  "contact_id": row.contact_id,
298
  "created_at": row.created_at.isoformat() if row.created_at else None,
299
  "updated_at": row.updated_at.isoformat() if row.updated_at else None,
300
+ "company_details": _lead_company_details_dict(row),
301
  }
302
 
303
 
 
729
  }
730
 
731
 
732
+ # Merged into Contact.raw_data["Company Name for Emails"], etc. (Apollo CSV keys)
733
+ COMPANY_DETAIL_PATCH_FIELDS = {
734
+ "company_name_for_emails": "Company Name for Emails",
735
+ "industry": "Industry",
736
+ "employees": "# Employees",
737
+ "annual_revenue": "Annual Revenue",
738
+ "last_raised_at": "Last Raised At",
739
+ "website": "Website",
740
+ "city": "City",
741
+ "state": "State",
742
+ "country_region": "Country",
743
+ }
744
+
745
+
746
+ def _contact_company_details_dict(c: Contact) -> dict:
747
+ rd = c.raw_data or {}
748
+ return {
749
+ "Company Name": _safe_str(c.company or rd.get("Company Name")),
750
+ "Company Name for Emails": _safe_str(rd.get("Company Name for Emails")),
751
+ "Industry": _safe_str(rd.get("Industry")),
752
+ "# Employees": _safe_str(rd.get("# Employees")),
753
+ "Annual Revenue": _safe_str(rd.get("Annual Revenue")),
754
+ "Last Raised At": _safe_str(rd.get("Last Raised At")),
755
+ "Website": _safe_str(rd.get("Website")),
756
+ "City": _safe_str(rd.get("City")),
757
+ "State": _safe_str(rd.get("State")),
758
+ "Country": _safe_str(rd.get("Country")),
759
+ }
760
+
761
+
762
+ def _merge_contact_raw_company_details(contact: Contact, patch: dict) -> None:
763
+ rd = dict(contact.raw_data or {})
764
+ for py_key, raw_key in COMPANY_DETAIL_PATCH_FIELDS.items():
765
+ if py_key in patch:
766
+ rd[raw_key] = _safe_str(patch[py_key])
767
+ contact.raw_data = rd
768
+
769
+
770
+ def _lead_company_details_dict(row: CrmLead) -> dict:
771
+ rw = row.raw_webhook if isinstance(row.raw_webhook, dict) else {}
772
+ cd = rw.get("company_details") if isinstance(rw.get("company_details"), dict) else {}
773
+ return {
774
+ "Company Name": _safe_str(row.company_name or cd.get("Company Name")),
775
+ "Company Name for Emails": _safe_str(cd.get("Company Name for Emails")),
776
+ "Industry": _safe_str(cd.get("Industry")),
777
+ "# Employees": _safe_str(cd.get("# Employees")),
778
+ "Annual Revenue": _safe_str(cd.get("Annual Revenue")),
779
+ "Last Raised At": _safe_str(cd.get("Last Raised At")),
780
+ "Website": _safe_str(cd.get("Website")),
781
+ "City": _safe_str(cd.get("City")),
782
+ "State": _safe_str(cd.get("State")),
783
+ "Country": _safe_str(cd.get("Country")),
784
+ }
785
+
786
+
787
+ def _deal_fallback_company_details(d: CrmDeal) -> dict:
788
+ return {
789
+ "Company Name": _safe_str(d.account_name),
790
+ "Company Name for Emails": "",
791
+ "Industry": "",
792
+ "# Employees": "",
793
+ "Annual Revenue": "",
794
+ "Last Raised At": "",
795
+ "Website": "",
796
+ "City": "",
797
+ "State": "",
798
+ "Country": _safe_str(d.country),
799
+ }
800
+
801
+
802
+ def _enrich_deal_response(db: Session, row: CrmDeal) -> dict:
803
+ out = _deal_to_dict(row)
804
+ if row.contact_id:
805
+ c = db.query(Contact).filter(Contact.id == row.contact_id).first()
806
+ if c:
807
+ out["company_details"] = _contact_company_details_dict(c)
808
+ out["company_details_contact_id"] = c.id
809
+ else:
810
+ out["company_details"] = _deal_fallback_company_details(row)
811
+ else:
812
+ out["company_details"] = _deal_fallback_company_details(row)
813
+ return out
814
+
815
+
816
  def _sync_contact_raw_from_columns(contact: Contact) -> None:
817
  rd = dict(contact.raw_data or {})
818
  rd["First Name"] = contact.first_name or ""
 
852
  if "title" in data:
853
  contact.title = _safe_str(data["title"])
854
  _sync_contact_raw_from_columns(contact)
855
+ _merge_contact_raw_company_details(contact, data)
856
  db.commit()
857
  db.refresh(contact)
858
  return {
 
1891
  detail="Another lead already uses this email",
1892
  )
1893
  row.email = email
1894
+ rw = dict(row.raw_webhook) if isinstance(row.raw_webhook, dict) else {}
1895
+ cd = dict(rw.get("company_details") or {}) if isinstance(rw.get("company_details"), dict) else {}
1896
+ cd["Company Name"] = row.company_name or ""
1897
+ for py_key, raw_key in COMPANY_DETAIL_PATCH_FIELDS.items():
1898
+ if py_key in data:
1899
+ cd[raw_key] = _safe_str(data[py_key])
1900
+ rw["company_details"] = cd
1901
+ row.raw_webhook = rw
1902
  row.updated_at = datetime.utcnow()
1903
  db.commit()
1904
  db.refresh(row)
 
2058
  row = db.query(CrmDeal).filter(CrmDeal.id == deal_id).first()
2059
  if not row:
2060
  raise HTTPException(status_code=404, detail="Deal not found")
2061
+ return _enrich_deal_response(db, row)
2062
 
2063
 
2064
  @app.patch("/api/deals/{deal_id}")
 
2096
  if "last_interaction_at" in data:
2097
  ts = _to_datetime(data["last_interaction_at"])
2098
  row.last_interaction_at = ts
2099
+
2100
+ c = None
2101
+ if row.contact_id:
2102
+ c = db.query(Contact).filter(Contact.id == row.contact_id).first()
2103
+ if "account_name" in data and c:
2104
+ c.company = row.account_name
2105
+ if c:
2106
+ _sync_contact_raw_from_columns(c)
2107
+ if any(k in data for k in COMPANY_DETAIL_PATCH_FIELDS):
2108
+ _merge_contact_raw_company_details(c, data)
2109
+ if "country_region" in data:
2110
+ row.country = _safe_str(data["country_region"])
2111
+ if "country" in data and "country_region" not in data:
2112
+ rd = dict(c.raw_data or {})
2113
+ rd["Country"] = _safe_str(data["country"])
2114
+ c.raw_data = rd
2115
+ db.add(c)
2116
  row.updated_at = datetime.utcnow()
2117
  db.commit()
2118
  db.refresh(row)
2119
+ return _enrich_deal_response(db, row)
2120
 
2121
 
2122
  @app.post("/api/deals/seed-demo")
backend/app/models.py CHANGED
@@ -42,6 +42,16 @@ class CrmLeadPatchRequest(BaseModel):
42
  title: Optional[str] = None
43
  last_reply_subject: Optional[str] = None
44
  last_reply_body: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
45
 
46
 
47
  class ContactCreateRequest(BaseModel):
@@ -58,6 +68,16 @@ class ContactPatchRequest(BaseModel):
58
  email: Optional[str] = None
59
  company: Optional[str] = None
60
  title: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
61
 
62
 
63
  class BulkLeadIdsRequest(BaseModel):
@@ -86,6 +106,16 @@ class CrmDealPatchRequest(BaseModel):
86
  contact_display: Optional[str] = None
87
  account_name: Optional[str] = None
88
  last_interaction_at: Optional[str] = None # ISO date or datetime
 
 
 
 
 
 
 
 
 
 
89
 
90
 
91
  class SmartleadRunResponse(BaseModel):
 
42
  title: Optional[str] = None
43
  last_reply_subject: Optional[str] = None
44
  last_reply_body: Optional[str] = None
45
+ # Stored under raw_webhook["company_details"] (same Apollo keys as Contacts raw_data)
46
+ company_name_for_emails: Optional[str] = None
47
+ industry: Optional[str] = None
48
+ employees: Optional[str] = None
49
+ annual_revenue: Optional[str] = None
50
+ last_raised_at: Optional[str] = None
51
+ website: Optional[str] = None
52
+ city: Optional[str] = None
53
+ state: Optional[str] = None
54
+ country_region: Optional[str] = None
55
 
56
 
57
  class ContactCreateRequest(BaseModel):
 
68
  email: Optional[str] = None
69
  company: Optional[str] = None
70
  title: Optional[str] = None
71
+ # Merged into raw_data (Apollo CSV-style keys)
72
+ company_name_for_emails: Optional[str] = None
73
+ industry: Optional[str] = None
74
+ employees: Optional[str] = None
75
+ annual_revenue: Optional[str] = None
76
+ last_raised_at: Optional[str] = None
77
+ website: Optional[str] = None
78
+ city: Optional[str] = None
79
+ state: Optional[str] = None
80
+ country_region: Optional[str] = None # raw_data["Country"]
81
 
82
 
83
  class BulkLeadIdsRequest(BaseModel):
 
106
  contact_display: Optional[str] = None
107
  account_name: Optional[str] = None
108
  last_interaction_at: Optional[str] = None # ISO date or datetime
109
+ # When deal has contact_id, merged into that contact's raw_data (Apollo keys)
110
+ company_name_for_emails: Optional[str] = None
111
+ industry: Optional[str] = None
112
+ employees: Optional[str] = None
113
+ annual_revenue: Optional[str] = None
114
+ last_raised_at: Optional[str] = None
115
+ website: Optional[str] = None
116
+ city: Optional[str] = None
117
+ state: Optional[str] = None
118
+ country_region: Optional[str] = None
119
 
120
 
121
  class SmartleadRunResponse(BaseModel):
frontend/src/components/workspace/CompanyDetailsEditor.jsx ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Loader2 } from 'lucide-react';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Input } from '@/components/ui/input';
5
+ import { cn } from '@/lib/utils';
6
+
7
+ function formFromDetails(cd, fallbackCompany = '') {
8
+ const d = cd || {};
9
+ return {
10
+ companyName: d['Company Name'] || fallbackCompany || '',
11
+ companyNameForEmails: d['Company Name for Emails'] || '',
12
+ industry: d['Industry'] || '',
13
+ employees: d['# Employees'] || '',
14
+ annualRevenue: d['Annual Revenue'] || '',
15
+ lastRaisedAt: d['Last Raised At'] || '',
16
+ website: d['Website'] || '',
17
+ city: d['City'] || '',
18
+ state: d['State'] || '',
19
+ country: d['Country'] || '',
20
+ };
21
+ }
22
+
23
+ function buildPatch(variant, form, dealLinkedContact) {
24
+ const detail = {
25
+ company_name_for_emails: form.companyNameForEmails,
26
+ industry: form.industry,
27
+ employees: form.employees,
28
+ annual_revenue: form.annualRevenue,
29
+ last_raised_at: form.lastRaisedAt,
30
+ website: form.website,
31
+ city: form.city,
32
+ state: form.state,
33
+ country_region: form.country,
34
+ };
35
+ if (variant === 'contact') {
36
+ return { company: form.companyName, ...detail };
37
+ }
38
+ if (variant === 'lead') {
39
+ return { company_name: form.companyName, ...detail };
40
+ }
41
+ if (variant === 'deal') {
42
+ if (dealLinkedContact) {
43
+ return { account_name: form.companyName, ...detail };
44
+ }
45
+ return {
46
+ account_name: form.companyName,
47
+ country: form.country,
48
+ };
49
+ }
50
+ return detail;
51
+ }
52
+
53
+ /**
54
+ * Editable β€œCompany details” card (two-column grid) + Save, for Contacts / Leads / Deals slide-overs.
55
+ * `companyDetails` uses Apollo-style keys from the API (`company_details` or contact `raw_data`).
56
+ */
57
+ export default function CompanyDetailsEditor({
58
+ variant,
59
+ companyDetails,
60
+ fallbackCompanyName = '',
61
+ dealLinkedContact = false,
62
+ onSave,
63
+ className,
64
+ }) {
65
+ const [form, setForm] = useState(() => formFromDetails(companyDetails, fallbackCompanyName));
66
+ const [saving, setSaving] = useState(false);
67
+
68
+ useEffect(() => {
69
+ setForm(formFromDetails(companyDetails, fallbackCompanyName));
70
+ }, [companyDetails, fallbackCompanyName]);
71
+
72
+ const dealLimited = variant === 'deal' && !dealLinkedContact;
73
+ const isInputDisabled = (key) => dealLimited && key !== 'companyName' && key !== 'country';
74
+
75
+ const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
76
+
77
+ const handleSave = async () => {
78
+ setSaving(true);
79
+ try {
80
+ await onSave(buildPatch(variant, form, dealLinkedContact));
81
+ } finally {
82
+ setSaving(false);
83
+ }
84
+ };
85
+
86
+ const row = (label, key, opts = {}) => (
87
+ <div className={opts.span === 2 ? 'sm:col-span-2' : ''}>
88
+ <label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
89
+ <Input
90
+ value={form[key]}
91
+ onChange={(e) => setField(key, e.target.value)}
92
+ disabled={isInputDisabled(key)}
93
+ className="text-sm bg-white"
94
+ placeholder={opts.placeholder}
95
+ />
96
+ </div>
97
+ );
98
+
99
+ return (
100
+ <div
101
+ className={cn(
102
+ 'rounded-xl border border-slate-200 bg-slate-50/50 p-4 mb-6 flex flex-col gap-4',
103
+ className
104
+ )}
105
+ >
106
+ <h4 className="text-sm font-semibold text-slate-700">Company details</h4>
107
+
108
+ {dealLimited ? (
109
+ <p className="text-xs text-slate-500">
110
+ Link this deal to a contact to edit full company attributes. Account name and country can be edited
111
+ below.
112
+ </p>
113
+ ) : null}
114
+
115
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
116
+ {row('Company Name', 'companyName')}
117
+ {row('Company Name for Emails', 'companyNameForEmails')}
118
+ {row('Industry', 'industry', { span: 2 })}
119
+ {row('Employees', 'employees', { detail: true })}
120
+ {row('Annual Revenue', 'annualRevenue', { detail: true })}
121
+ {row('Last Raised At', 'lastRaisedAt', { detail: true })}
122
+ {row('Website', 'website', { detail: true, placeholder: 'https://' })}
123
+ {row('City', 'city', { detail: true })}
124
+ {row('State', 'state', { detail: true })}
125
+ {row('Country', 'country', { detail: true })}
126
+ </div>
127
+
128
+ <div className="pt-2 border-t border-slate-200/80">
129
+ <Button type="button" className="w-full" onClick={handleSave} disabled={saving}>
130
+ {saving ? (
131
+ <>
132
+ <Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
133
+ Saving…
134
+ </>
135
+ ) : (
136
+ 'Save'
137
+ )}
138
+ </Button>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
frontend/src/pages/Contacts.jsx CHANGED
@@ -25,6 +25,7 @@ import { Button } from '@/components/ui/button';
25
  import AppShell from '@/components/layout/AppShell';
26
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
27
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
 
28
  import { EditableCell } from '@/components/workspace/EditableCell';
29
  import { cn } from '@/lib/utils';
30
 
@@ -72,6 +73,23 @@ export default function Contacts() {
72
  [rowSelection]
73
  );
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  const allPageSelected = contacts.length > 0 && contacts.every((c) => rowSelection[c.id]);
76
 
77
  useEffect(() => {
@@ -993,68 +1011,15 @@ export default function Contacts() {
993
  </Badge>
994
  </div>
995
 
996
- {selectedContactDetails?.raw_data && (
997
- <div className="mb-6 rounded-xl border border-slate-200 bg-slate-50/50 p-4">
998
- <h4 className="text-sm font-semibold text-slate-700 mb-3">Company details</h4>
999
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm">
1000
- <div>
1001
- <span className="text-slate-500">Company Name:</span>{' '}
1002
- <span className="text-slate-800">
1003
- {selectedContactDetails.raw_data['Company Name'] ||
1004
- selectedContactDetails.company ||
1005
- 'N/A'}
1006
- </span>
1007
- </div>
1008
- <div>
1009
- <span className="text-slate-500">Company Name for Emails:</span>{' '}
1010
- <span className="text-slate-800">
1011
- {selectedContactDetails.raw_data['Company Name for Emails'] || 'N/A'}
1012
- </span>
1013
- </div>
1014
- <div>
1015
- <span className="text-slate-500">Industry:</span>{' '}
1016
- <span className="text-slate-800">
1017
- {selectedContactDetails.raw_data['Industry'] || 'N/A'}
1018
- </span>
1019
- </div>
1020
- <div>
1021
- <span className="text-slate-500">Employees:</span>{' '}
1022
- <span className="text-slate-800">
1023
- {selectedContactDetails.raw_data['# Employees'] || 'N/A'}
1024
- </span>
1025
- </div>
1026
- <div>
1027
- <span className="text-slate-500">Annual Revenue:</span>{' '}
1028
- <span className="text-slate-800">
1029
- {selectedContactDetails.raw_data['Annual Revenue'] || 'N/A'}
1030
- </span>
1031
- </div>
1032
- <div>
1033
- <span className="text-slate-500">Last Raised At:</span>{' '}
1034
- <span className="text-slate-800">
1035
- {selectedContactDetails.raw_data['Last Raised At'] || 'N/A'}
1036
- </span>
1037
- </div>
1038
- <div>
1039
- <span className="text-slate-500">Website:</span>{' '}
1040
- <span className="text-slate-800">
1041
- {selectedContactDetails.raw_data['Website'] || 'N/A'}
1042
- </span>
1043
- </div>
1044
- <div>
1045
- <span className="text-slate-500">Location:</span>{' '}
1046
- <span className="text-slate-800">
1047
- {[
1048
- selectedContactDetails.raw_data['City'],
1049
- selectedContactDetails.raw_data['State'],
1050
- selectedContactDetails.raw_data['Country'],
1051
- ]
1052
- .filter(Boolean)
1053
- .join(', ') || 'N/A'}
1054
- </span>
1055
- </div>
1056
- </div>
1057
- </div>
1058
  )}
1059
 
1060
  <h4 className="text-sm font-semibold text-slate-800 mb-2 flex items-center gap-2">
 
25
  import AppShell from '@/components/layout/AppShell';
26
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
27
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
28
+ import CompanyDetailsEditor from '@/components/workspace/CompanyDetailsEditor';
29
  import { EditableCell } from '@/components/workspace/EditableCell';
30
  import { cn } from '@/lib/utils';
31
 
 
73
  [rowSelection]
74
  );
75
 
76
+ const contactCompanyDetailsForEditor = useMemo(() => {
77
+ if (!selectedContactDetails) return null;
78
+ const rd = selectedContactDetails.raw_data || {};
79
+ return {
80
+ 'Company Name': rd['Company Name'] ?? selectedContactDetails.company ?? '',
81
+ 'Company Name for Emails': rd['Company Name for Emails'] ?? '',
82
+ Industry: rd['Industry'] ?? '',
83
+ '# Employees': rd['# Employees'] ?? '',
84
+ 'Annual Revenue': rd['Annual Revenue'] ?? '',
85
+ 'Last Raised At': rd['Last Raised At'] ?? '',
86
+ Website: rd['Website'] ?? '',
87
+ City: rd['City'] ?? '',
88
+ State: rd['State'] ?? '',
89
+ Country: rd['Country'] ?? '',
90
+ };
91
+ }, [selectedContactDetails]);
92
+
93
  const allPageSelected = contacts.length > 0 && contacts.every((c) => rowSelection[c.id]);
94
 
95
  useEffect(() => {
 
1011
  </Badge>
1012
  </div>
1013
 
1014
+ {contactCompanyDetailsForEditor && selectedContact && (
1015
+ <CompanyDetailsEditor
1016
+ variant="contact"
1017
+ companyDetails={contactCompanyDetailsForEditor}
1018
+ fallbackCompanyName={
1019
+ selectedContactDetails?.company || selectedContact.company || ''
1020
+ }
1021
+ onSave={(patch) => patchContact(selectedContact.id, patch)}
1022
+ />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1023
  )}
1024
 
1025
  <h4 className="text-sm font-semibold text-slate-800 mb-2 flex items-center gap-2">
frontend/src/pages/Deals.jsx CHANGED
@@ -6,6 +6,7 @@ import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/u
6
  import AppShell from '@/components/layout/AppShell';
7
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
8
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
 
9
  import { EditableCell, EditableDateCell } from '@/components/workspace/EditableCell';
10
  import { SearchableCountryPicker } from '@/components/workspace/SearchableCountryPicker';
11
  import { cn } from '@/lib/utils';
@@ -1005,10 +1006,17 @@ export default function Deals() {
1005
  ? `Stage: ${stageMeta(dealDetail.stage).label} Β· Forecast ${fmtMoney(dealDetail.forecast_value)}`
1006
  : ''
1007
  }
1008
- widthClassName="max-w-lg"
1009
  >
1010
  {dealDetail && (
1011
  <div className="space-y-5 text-sm">
 
 
 
 
 
 
 
1012
  <dl className="space-y-3">
1013
  <div>
1014
  <dt className="text-slate-500">Deal value</dt>
@@ -1026,14 +1034,6 @@ export default function Deals() {
1026
  <dt className="text-slate-500">Contact</dt>
1027
  <dd>{dealDetail.contact_display || 'β€”'}</dd>
1028
  </div>
1029
- <div>
1030
- <dt className="text-slate-500">Account</dt>
1031
- <dd>{dealDetail.account_name || 'β€”'}</dd>
1032
- </div>
1033
- <div>
1034
- <dt className="text-slate-500">Country</dt>
1035
- <dd>{dealDetail.country || 'β€”'}</dd>
1036
- </div>
1037
  <div>
1038
  <dt className="text-slate-500">Last interaction</dt>
1039
  <dd>{fmtDate(dealDetail.last_interaction_at)}</dd>
 
6
  import AppShell from '@/components/layout/AppShell';
7
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
8
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
9
+ import CompanyDetailsEditor from '@/components/workspace/CompanyDetailsEditor';
10
  import { EditableCell, EditableDateCell } from '@/components/workspace/EditableCell';
11
  import { SearchableCountryPicker } from '@/components/workspace/SearchableCountryPicker';
12
  import { cn } from '@/lib/utils';
 
1006
  ? `Stage: ${stageMeta(dealDetail.stage).label} Β· Forecast ${fmtMoney(dealDetail.forecast_value)}`
1007
  : ''
1008
  }
1009
+ widthClassName="max-w-xl"
1010
  >
1011
  {dealDetail && (
1012
  <div className="space-y-5 text-sm">
1013
+ <CompanyDetailsEditor
1014
+ variant="deal"
1015
+ dealLinkedContact={!!dealDetail.contact_id}
1016
+ companyDetails={dealDetail.company_details}
1017
+ fallbackCompanyName={dealDetail.account_name || ''}
1018
+ onSave={(patch) => patchDeal(dealDetail.id, patch)}
1019
+ />
1020
  <dl className="space-y-3">
1021
  <div>
1022
  <dt className="text-slate-500">Deal value</dt>
 
1034
  <dt className="text-slate-500">Contact</dt>
1035
  <dd>{dealDetail.contact_display || 'β€”'}</dd>
1036
  </div>
 
 
 
 
 
 
 
 
1037
  <div>
1038
  <dt className="text-slate-500">Last interaction</dt>
1039
  <dd>{fmtDate(dealDetail.last_interaction_at)}</dd>
frontend/src/pages/Leads.jsx CHANGED
@@ -17,6 +17,7 @@ import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/u
17
  import AppShell from '@/components/layout/AppShell';
18
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
19
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
 
20
  import { EditableCell } from '@/components/workspace/EditableCell';
21
  import { cn } from '@/lib/utils';
22
 
@@ -672,9 +673,16 @@ export default function Leads() {
672
  ? `Campaign: ${selected.campaign_name || selected.campaign_id || 'β€”'}`
673
  : ''
674
  }
 
675
  >
676
  {selected && (
677
  <>
 
 
 
 
 
 
678
  <dl className="space-y-3 text-sm">
679
  <div>
680
  <dt className="text-slate-500">Name</dt>
@@ -686,10 +694,6 @@ export default function Leads() {
686
  <dt className="text-slate-500">Email</dt>
687
  <dd>{selected.email || 'β€”'}</dd>
688
  </div>
689
- <div>
690
- <dt className="text-slate-500">Company</dt>
691
- <dd>{selected.company_name || 'β€”'}</dd>
692
- </div>
693
  <div>
694
  <dt className="text-slate-500">Title</dt>
695
  <dd>{selected.title || 'β€”'}</dd>
 
17
  import AppShell from '@/components/layout/AppShell';
18
  import MainTableWorkspace from '@/components/workspace/MainTableWorkspace';
19
  import SlideOverPanel from '@/components/workspace/SlideOverPanel';
20
+ import CompanyDetailsEditor from '@/components/workspace/CompanyDetailsEditor';
21
  import { EditableCell } from '@/components/workspace/EditableCell';
22
  import { cn } from '@/lib/utils';
23
 
 
673
  ? `Campaign: ${selected.campaign_name || selected.campaign_id || 'β€”'}`
674
  : ''
675
  }
676
+ widthClassName="max-w-xl"
677
  >
678
  {selected && (
679
  <>
680
+ <CompanyDetailsEditor
681
+ variant="lead"
682
+ companyDetails={selected.company_details}
683
+ fallbackCompanyName={selected.company_name || ''}
684
+ onSave={(patch) => patchLead(selected.id, patch)}
685
+ />
686
  <dl className="space-y-3 text-sm">
687
  <div>
688
  <dt className="text-slate-500">Name</dt>
 
694
  <dt className="text-slate-500">Email</dt>
695
  <dd>{selected.email || 'β€”'}</dd>
696
  </div>
 
 
 
 
697
  <div>
698
  <dt className="text-slate-500">Title</dt>
699
  <dd>{selected.title || 'β€”'}</dd>