Seth commited on
Commit
9ed5724
·
1 Parent(s): 7debd2c
frontend/src/components/workspace/CompanyDetailsEditor.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
2
  import { Loader2, Sparkles } from 'lucide-react';
3
  import { Button } from '@/components/ui/button';
4
  import { Input } from '@/components/ui/input';
@@ -20,6 +20,10 @@ function formFromDetails(cd, fallbackCompany = '') {
20
  };
21
  }
22
 
 
 
 
 
23
  function buildPatch(variant, form, dealLinkedContact) {
24
  const detail = {
25
  company_name_for_emails: form.companyNameForEmails,
@@ -51,8 +55,8 @@ function buildPatch(variant, form, dealLinkedContact) {
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,
@@ -65,12 +69,16 @@ export default function CompanyDetailsEditor({
65
  onFetch,
66
  fetchLoading = false,
67
  fetchError = '',
 
68
  }) {
69
  const [form, setForm] = useState(() => formFromDetails(companyDetails, fallbackCompanyName));
70
  const [saving, setSaving] = useState(false);
 
71
 
72
  useEffect(() => {
73
- setForm(formFromDetails(companyDetails, fallbackCompanyName));
 
 
74
  }, [companyDetails, fallbackCompanyName]);
75
 
76
  const dealLimited = variant === 'deal' && !dealLinkedContact;
@@ -79,6 +87,7 @@ export default function CompanyDetailsEditor({
79
  const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
80
 
81
  const handleSave = async () => {
 
82
  setSaving(true);
83
  try {
84
  await onSave(buildPatch(variant, form, dealLinkedContact));
@@ -87,34 +96,58 @@ export default function CompanyDetailsEditor({
87
  }
88
  };
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  const row = (label, key, opts = {}) => (
91
  <div className={opts.span === 2 ? 'sm:col-span-2' : ''}>
92
- <label className="block text-xs font-medium text-slate-500 mb-1">{label}</label>
93
  <Input
94
  value={form[key]}
95
  onChange={(e) => setField(key, e.target.value)}
96
  disabled={isInputDisabled(key)}
97
- className="text-sm bg-white"
98
  placeholder={opts.placeholder}
99
  />
100
  </div>
101
  );
102
 
103
  return (
104
- <div className={cn('rounded-xl border border-slate-200 bg-slate-50/50 p-4 flex flex-col gap-4 mb-6', className)}>
105
  <div className="flex flex-wrap items-start justify-between gap-2">
106
- <h4 className="text-sm font-semibold text-slate-700">Company details</h4>
 
 
 
 
 
 
 
 
107
  {showFetch && onFetch ? (
108
  <Button
109
  type="button"
110
  variant="outline"
111
  size="sm"
112
- className="gap-1.5 shrink-0 text-violet-700 border-violet-200 hover:bg-violet-50"
113
  onClick={() => onFetch()}
114
  disabled={fetchLoading}
115
  >
116
  {fetchLoading ? (
117
- <Loader2 className="h-3.5 w-3.5 animate-spin shrink-0" aria-hidden />
118
  ) : (
119
  <Sparkles className="h-3.5 w-3.5 shrink-0" aria-hidden />
120
  )}
@@ -136,31 +169,33 @@ export default function CompanyDetailsEditor({
136
  </p>
137
  ) : null}
138
 
139
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
140
  {row('Company Name', 'companyName')}
141
  {row('Company Name for Emails', 'companyNameForEmails')}
142
  {row('Industry', 'industry', { span: 2 })}
143
- {row('Employees', 'employees', { detail: true })}
144
- {row('Annual Revenue', 'annualRevenue', { detail: true })}
145
- {row('Last Raised At', 'lastRaisedAt', { detail: true })}
146
- {row('Website', 'website', { detail: true, placeholder: 'https://' })}
147
- {row('City', 'city', { detail: true })}
148
- {row('State', 'state', { detail: true })}
149
- {row('Country', 'country', { detail: true })}
150
  </div>
151
 
152
- <div className="pt-2 border-t border-slate-200/80">
153
- <Button type="button" className="w-full" onClick={handleSave} disabled={saving}>
154
- {saving ? (
155
- <>
156
- <Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
157
- Saving…
158
- </>
159
- ) : (
160
- 'Save'
161
- )}
162
- </Button>
163
- </div>
 
 
164
  </div>
165
  );
166
  }
 
1
+ import React, { useEffect, useState, useRef } from 'react';
2
  import { Loader2, Sparkles } from 'lucide-react';
3
  import { Button } from '@/components/ui/button';
4
  import { Input } from '@/components/ui/input';
 
20
  };
21
  }
22
 
23
+ function formsCompanyEqual(a, b) {
24
+ return Object.keys(a).every((k) => (a[k] || '').trim() === (b[k] || '').trim());
25
+ }
26
+
27
  function buildPatch(variant, form, dealLinkedContact) {
28
  const detail = {
29
  company_name_for_emails: form.companyNameForEmails,
 
55
  }
56
 
57
  /**
58
+ * Editable “Company details” card for Contacts / Leads / Deals slide-overs.
59
+ * `autoSave`: debounced save after typing pauses (no Save button).
60
  */
61
  export default function CompanyDetailsEditor({
62
  variant,
 
69
  onFetch,
70
  fetchLoading = false,
71
  fetchError = '',
72
+ autoSave = false,
73
  }) {
74
  const [form, setForm] = useState(() => formFromDetails(companyDetails, fallbackCompanyName));
75
  const [saving, setSaving] = useState(false);
76
+ const baselineRef = useRef(formFromDetails(companyDetails, fallbackCompanyName));
77
 
78
  useEffect(() => {
79
+ const b = formFromDetails(companyDetails, fallbackCompanyName);
80
+ baselineRef.current = b;
81
+ setForm(b);
82
  }, [companyDetails, fallbackCompanyName]);
83
 
84
  const dealLimited = variant === 'deal' && !dealLinkedContact;
 
87
  const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
88
 
89
  const handleSave = async () => {
90
+ if (!onSave) return;
91
  setSaving(true);
92
  try {
93
  await onSave(buildPatch(variant, form, dealLinkedContact));
 
96
  }
97
  };
98
 
99
+ useEffect(() => {
100
+ if (!autoSave || !onSave) return;
101
+ const baseline = baselineRef.current;
102
+ if (formsCompanyEqual(form, baseline)) return;
103
+ const t = setTimeout(async () => {
104
+ setSaving(true);
105
+ try {
106
+ await onSave(buildPatch(variant, form, dealLinkedContact));
107
+ baselineRef.current = { ...form };
108
+ } finally {
109
+ setSaving(false);
110
+ }
111
+ }, 700);
112
+ return () => clearTimeout(t);
113
+ }, [form, autoSave, onSave, variant, dealLinkedContact]);
114
+
115
  const row = (label, key, opts = {}) => (
116
  <div className={opts.span === 2 ? 'sm:col-span-2' : ''}>
117
+ <label className="mb-1 block text-xs font-medium text-slate-500">{label}</label>
118
  <Input
119
  value={form[key]}
120
  onChange={(e) => setField(key, e.target.value)}
121
  disabled={isInputDisabled(key)}
122
+ className="bg-white text-sm"
123
  placeholder={opts.placeholder}
124
  />
125
  </div>
126
  );
127
 
128
  return (
129
+ <div className={cn('mb-6 flex flex-col gap-4 rounded-xl border border-slate-200 bg-slate-50/50 p-4', className)}>
130
  <div className="flex flex-wrap items-start justify-between gap-2">
131
+ <div className="flex items-center gap-2">
132
+ <h4 className="text-sm font-semibold text-slate-700">Company details</h4>
133
+ {autoSave && saving ? (
134
+ <span className="flex items-center gap-1 text-xs text-slate-500">
135
+ <Loader2 className="h-3 w-3 shrink-0 animate-spin" aria-hidden />
136
+ Saving…
137
+ </span>
138
+ ) : null}
139
+ </div>
140
  {showFetch && onFetch ? (
141
  <Button
142
  type="button"
143
  variant="outline"
144
  size="sm"
145
+ className="shrink-0 gap-1.5 border-violet-200 text-violet-700 hover:bg-violet-50"
146
  onClick={() => onFetch()}
147
  disabled={fetchLoading}
148
  >
149
  {fetchLoading ? (
150
+ <Loader2 className="h-3.5 w-3.5 shrink-0 animate-spin" aria-hidden />
151
  ) : (
152
  <Sparkles className="h-3.5 w-3.5 shrink-0" aria-hidden />
153
  )}
 
169
  </p>
170
  ) : null}
171
 
172
+ <div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
173
  {row('Company Name', 'companyName')}
174
  {row('Company Name for Emails', 'companyNameForEmails')}
175
  {row('Industry', 'industry', { span: 2 })}
176
+ {row('Employees', 'employees')}
177
+ {row('Annual Revenue', 'annualRevenue')}
178
+ {row('Last Raised At', 'lastRaisedAt')}
179
+ {row('Website', 'website', { placeholder: 'https://' })}
180
+ {row('City', 'city')}
181
+ {row('State', 'state')}
182
+ {row('Country', 'country')}
183
  </div>
184
 
185
+ {!autoSave ? (
186
+ <div className="border-t border-slate-200/80 pt-2">
187
+ <Button type="button" className="w-full" onClick={handleSave} disabled={saving}>
188
+ {saving ? (
189
+ <>
190
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
191
+ Saving…
192
+ </>
193
+ ) : (
194
+ 'Save'
195
+ )}
196
+ </Button>
197
+ </div>
198
+ ) : null}
199
  </div>
200
  );
201
  }
frontend/src/components/workspace/ContactIdentityEditor.jsx CHANGED
@@ -1,11 +1,31 @@
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
  /**
8
- * Editable person fields (first / last name, email, title) + Save — slide-over top section.
 
 
9
  */
10
  export default function ContactIdentityEditor({
11
  firstName = '',
@@ -16,17 +36,16 @@ export default function ContactIdentityEditor({
16
  disabled = false,
17
  className,
18
  heading = 'Contact details',
 
19
  }) {
20
- const [form, setForm] = useState({
21
- firstName,
22
- lastName,
23
- email,
24
- title,
25
- });
26
  const [saving, setSaving] = useState(false);
 
27
 
28
  useEffect(() => {
29
- setForm({ firstName, lastName, email, title });
 
 
30
  }, [firstName, lastName, email, title]);
31
 
32
  const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
@@ -46,12 +65,46 @@ export default function ContactIdentityEditor({
46
  }
47
  };
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  return (
50
  <div className={cn('rounded-xl border border-slate-200 bg-white p-4 mb-6', className)}>
51
- <h4 className="text-sm font-semibold text-slate-800 mb-3">{heading}</h4>
52
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 text-sm">
 
 
 
 
 
 
 
 
53
  <div>
54
- <label className="block text-xs font-medium text-slate-500 mb-1">First name</label>
55
  <Input
56
  value={form.firstName}
57
  onChange={(e) => setField('firstName', e.target.value)}
@@ -60,7 +113,7 @@ export default function ContactIdentityEditor({
60
  />
61
  </div>
62
  <div>
63
- <label className="block text-xs font-medium text-slate-500 mb-1">Last name</label>
64
  <Input
65
  value={form.lastName}
66
  onChange={(e) => setField('lastName', e.target.value)}
@@ -69,7 +122,7 @@ export default function ContactIdentityEditor({
69
  />
70
  </div>
71
  <div className="sm:col-span-2">
72
- <label className="block text-xs font-medium text-slate-500 mb-1">Email</label>
73
  <Input
74
  type="email"
75
  value={form.email}
@@ -79,7 +132,7 @@ export default function ContactIdentityEditor({
79
  />
80
  </div>
81
  <div className="sm:col-span-2">
82
- <label className="block text-xs font-medium text-slate-500 mb-1">Title</label>
83
  <Input
84
  value={form.title}
85
  onChange={(e) => setField('title', e.target.value)}
@@ -88,18 +141,25 @@ export default function ContactIdentityEditor({
88
  />
89
  </div>
90
  </div>
91
- <div className="mt-4">
92
- <Button type="button" className="w-full sm:w-auto" onClick={handleSave} disabled={disabled || saving}>
93
- {saving ? (
94
- <>
95
- <Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
96
- Saving…
97
- </>
98
- ) : (
99
- 'Save'
100
- )}
101
- </Button>
102
- </div>
 
 
 
 
 
 
 
103
  </div>
104
  );
105
  }
 
1
+ import React, { useEffect, useState, useRef } 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 baselineFromProps(firstName, lastName, email, title) {
8
+ return {
9
+ firstName: firstName || '',
10
+ lastName: lastName || '',
11
+ email: email || '',
12
+ title: title || '',
13
+ };
14
+ }
15
+
16
+ function formsEqual(a, b) {
17
+ return (
18
+ (a.firstName || '').trim() === (b.firstName || '').trim() &&
19
+ (a.lastName || '').trim() === (b.lastName || '').trim() &&
20
+ (a.email || '').trim() === (b.email || '').trim() &&
21
+ (a.title || '').trim() === (b.title || '').trim()
22
+ );
23
+ }
24
+
25
  /**
26
+ * Editable person fields (first / last name, email, title).
27
+ * `autoSave`: debounced PATCH after typing pauses (no Save button).
28
+ * Otherwise: Save button commits all fields.
29
  */
30
  export default function ContactIdentityEditor({
31
  firstName = '',
 
36
  disabled = false,
37
  className,
38
  heading = 'Contact details',
39
+ autoSave = false,
40
  }) {
41
+ const [form, setForm] = useState(() => baselineFromProps(firstName, lastName, email, title));
 
 
 
 
 
42
  const [saving, setSaving] = useState(false);
43
+ const baselineRef = useRef(baselineFromProps(firstName, lastName, email, title));
44
 
45
  useEffect(() => {
46
+ const b = baselineFromProps(firstName, lastName, email, title);
47
+ baselineRef.current = b;
48
+ setForm(b);
49
  }, [firstName, lastName, email, title]);
50
 
51
  const setField = (key, value) => setForm((f) => ({ ...f, [key]: value }));
 
65
  }
66
  };
67
 
68
+ useEffect(() => {
69
+ if (!autoSave || !onSave || disabled) return;
70
+ const baseline = baselineRef.current;
71
+ if (formsEqual(form, baseline)) return;
72
+ const t = setTimeout(async () => {
73
+ setSaving(true);
74
+ try {
75
+ await onSave({
76
+ first_name: form.firstName,
77
+ last_name: form.lastName,
78
+ email: form.email,
79
+ title: form.title,
80
+ });
81
+ baselineRef.current = {
82
+ firstName: form.firstName,
83
+ lastName: form.lastName,
84
+ email: form.email,
85
+ title: form.title,
86
+ };
87
+ } finally {
88
+ setSaving(false);
89
+ }
90
+ }, 650);
91
+ return () => clearTimeout(t);
92
+ }, [form, autoSave, disabled, onSave, firstName, lastName, email, title]);
93
+
94
  return (
95
  <div className={cn('rounded-xl border border-slate-200 bg-white p-4 mb-6', className)}>
96
+ <div className="mb-3 flex items-center justify-between gap-2">
97
+ <h4 className="text-sm font-semibold text-slate-800">{heading}</h4>
98
+ {autoSave && saving ? (
99
+ <span className="flex items-center gap-1 text-xs text-slate-500">
100
+ <Loader2 className="h-3 w-3 animate-spin shrink-0" aria-hidden />
101
+ Saving…
102
+ </span>
103
+ ) : null}
104
+ </div>
105
+ <div className="grid grid-cols-1 gap-3 text-sm sm:grid-cols-2">
106
  <div>
107
+ <label className="mb-1 block text-xs font-medium text-slate-500">First name</label>
108
  <Input
109
  value={form.firstName}
110
  onChange={(e) => setField('firstName', e.target.value)}
 
113
  />
114
  </div>
115
  <div>
116
+ <label className="mb-1 block text-xs font-medium text-slate-500">Last name</label>
117
  <Input
118
  value={form.lastName}
119
  onChange={(e) => setField('lastName', e.target.value)}
 
122
  />
123
  </div>
124
  <div className="sm:col-span-2">
125
+ <label className="mb-1 block text-xs font-medium text-slate-500">Email</label>
126
  <Input
127
  type="email"
128
  value={form.email}
 
132
  />
133
  </div>
134
  <div className="sm:col-span-2">
135
+ <label className="mb-1 block text-xs font-medium text-slate-500">Title</label>
136
  <Input
137
  value={form.title}
138
  onChange={(e) => setField('title', e.target.value)}
 
141
  />
142
  </div>
143
  </div>
144
+ {!autoSave ? (
145
+ <div className="mt-4">
146
+ <Button
147
+ type="button"
148
+ className="w-full sm:w-auto"
149
+ onClick={handleSave}
150
+ disabled={disabled || saving}
151
+ >
152
+ {saving ? (
153
+ <>
154
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden />
155
+ Saving…
156
+ </>
157
+ ) : (
158
+ 'Save'
159
+ )}
160
+ </Button>
161
+ </div>
162
+ ) : null}
163
  </div>
164
  );
165
  }
frontend/src/pages/Contacts.jsx CHANGED
@@ -1100,6 +1100,7 @@ export default function Contacts() {
1100
  title={selectedContactDetails.title || ''}
1101
  onSave={(patch) => patchContact(selectedContact.id, patch)}
1102
  disabled={!selectedContact}
 
1103
  />
1104
  )}
1105
 
@@ -1117,6 +1118,7 @@ export default function Contacts() {
1117
  onFetch={enrichContactFromGpt}
1118
  fetchLoading={enrichLoading}
1119
  fetchError={enrichError}
 
1120
  />
1121
  )}
1122
  </div>
 
1100
  title={selectedContactDetails.title || ''}
1101
  onSave={(patch) => patchContact(selectedContact.id, patch)}
1102
  disabled={!selectedContact}
1103
+ autoSave
1104
  />
1105
  )}
1106
 
 
1118
  onFetch={enrichContactFromGpt}
1119
  fetchLoading={enrichLoading}
1120
  fetchError={enrichError}
1121
+ autoSave
1122
  />
1123
  )}
1124
  </div>
frontend/src/pages/Deals.jsx CHANGED
@@ -1,8 +1,7 @@
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
- import { GitBranch, Loader2, LayoutGrid, MessageSquare, Pencil } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
5
- import { Input } from '@/components/ui/input';
6
  import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select';
7
  import AppShell from '@/components/layout/AppShell';
8
  import { apiFetch } from '@/lib/api';
@@ -86,13 +85,6 @@ function isRowUiTarget(e) {
86
  );
87
  }
88
 
89
- function isoToDateInput(iso) {
90
- if (!iso) return '';
91
- const d = new Date(iso);
92
- if (Number.isNaN(d.getTime())) return '';
93
- return d.toISOString().slice(0, 10);
94
- }
95
-
96
  /** Maps stored probability to Select value: __clear__ or "10"…"100" (nearest 10, min 10). */
97
  function closeProbabilitySelectValue(p) {
98
  if (p == null || p === '') return '__clear__';
@@ -150,8 +142,6 @@ function GroupedDealTbody({
150
  barClassName,
151
  headerContent,
152
  deals,
153
- tableEditRowId,
154
- setTableEditRowId,
155
  patchDeal,
156
  updateStage,
157
  openDeal,
@@ -174,8 +164,6 @@ function GroupedDealTbody({
174
  <DealRow
175
  key={deal.id}
176
  deal={deal}
177
- tableEditRowId={tableEditRowId}
178
- setTableEditRowId={setTableEditRowId}
179
  patchDeal={patchDeal}
180
  updateStage={updateStage}
181
  openDeal={openDeal}
@@ -372,17 +360,7 @@ function PipelineBoard({ columns, openDeal, patchDeal, createDeal, createBusy })
372
  );
373
  }
374
 
375
- function DealRow({
376
- deal,
377
- tableEditRowId,
378
- setTableEditRowId,
379
- patchDeal,
380
- updateStage,
381
- openDeal,
382
- isAdmin,
383
- currentUserId,
384
- tenantMembers,
385
- }) {
386
  const meta = stageMeta(deal.stage);
387
  const safeStage = STAGES.some((s) => s.value === deal.stage) ? deal.stage : 'new';
388
  const closeSel = closeProbabilitySelectValue(deal.close_probability);
@@ -393,62 +371,26 @@ function DealRow({
393
  tabIndex={0}
394
  onClick={(e) => {
395
  if (isRowUiTarget(e)) return;
396
- setTableEditRowId(null);
397
  openDeal(deal);
398
  }}
399
  onKeyDown={(e) => {
400
  if (isRowUiTarget(e)) return;
401
  if (e.key === 'Enter' || e.key === ' ') {
402
  e.preventDefault();
403
- setTableEditRowId(null);
404
  openDeal(deal);
405
  }
406
  }}
407
  className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
408
  >
409
- <td className="px-1 py-1.5 w-[4.25rem]" onClick={(e) => e.stopPropagation()}>
410
- <div className="flex flex-col items-center gap-0.5">
411
- <input type="checkbox" className="rounded border-slate-300" />
412
- <Button
413
- type="button"
414
- variant="ghost"
415
- size="icon"
416
- className={cn(
417
- 'h-7 w-7 text-slate-500 hover:text-violet-700',
418
- tableEditRowId === deal.id && 'bg-violet-100 text-violet-800 hover:bg-violet-100'
419
- )}
420
- onClick={(e) => {
421
- e.stopPropagation();
422
- const tr = e.currentTarget.closest('tr');
423
- if (tableEditRowId === deal.id) {
424
- setTableEditRowId(null);
425
- return;
426
- }
427
- setTableEditRowId(deal.id);
428
- requestAnimationFrame(() => focusFirstEditableInRow(tr));
429
- }}
430
- aria-label={
431
- tableEditRowId === deal.id
432
- ? `Stop editing row for ${deal.name || 'deal'}`
433
- : `Edit fields in row for ${deal.name || 'deal'}`
434
- }
435
- >
436
- <Pencil className="h-3.5 w-3.5" aria-hidden />
437
- </Button>
438
- </div>
439
  </td>
440
- <td className="px-3 py-2 align-top max-w-[220px] font-medium">
441
- {tableEditRowId === deal.id ? (
442
- <EditableCell
443
- value={deal.name || ''}
444
- onCommit={(v) => patchDeal(deal.id, { name: v })}
445
- inputClassName="font-medium text-slate-900"
446
- />
447
- ) : (
448
- <div className="min-h-[2rem] py-1 text-sm font-medium text-slate-900 truncate">
449
- {deal.name || '—'}
450
- </div>
451
- )}
452
  </td>
453
  <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
454
  <Select value={safeStage} onValueChange={(v) => updateStage(deal.id, v)}>
@@ -581,7 +523,7 @@ function DealRow({
581
  )}
582
  </td>
583
  <td
584
- className="px-2 py-2 align-top tabular-nums w-[9rem] max-w-[9rem] shrink-0"
585
  onClick={(e) => e.stopPropagation()}
586
  >
587
  <EditableCurrencyCell
@@ -623,43 +565,19 @@ function DealRow({
623
  </SelectContent>
624
  </Select>
625
  </td>
626
- <td className="px-3 py-2 align-top max-w-[180px]">
627
- {tableEditRowId === deal.id ? (
628
- <EditableCell
629
- value={deal.contact_display || ''}
630
- onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
631
- inputClassName="text-xs"
632
- />
633
- ) : (
634
- <div className="min-h-[2rem] py-1">
635
- {deal.contact_display ? (
636
- <span className="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-800">
637
- {deal.contact_display}
638
- </span>
639
- ) : (
640
- <span className="text-slate-500">—</span>
641
- )}
642
- </div>
643
- )}
644
  </td>
645
- <td className="px-3 py-2 align-top max-w-[200px]">
646
- {tableEditRowId === deal.id ? (
647
- <EditableCell
648
- value={deal.account_name || ''}
649
- onCommit={(v) => patchDeal(deal.id, { account_name: v })}
650
- inputClassName="text-xs"
651
- />
652
- ) : (
653
- <div className="min-h-[2rem] py-1">
654
- {deal.account_name ? (
655
- <span className="inline-flex rounded-md bg-violet-50 px-2 py-0.5 text-xs font-medium text-violet-800">
656
- {deal.account_name}
657
- </span>
658
- ) : (
659
- <span className="text-slate-500">—</span>
660
- )}
661
- </div>
662
- )}
663
  </td>
664
  <td className="px-3 py-2 align-top" onClick={(e) => e.stopPropagation()}>
665
  <EditableDateCell
@@ -737,13 +655,10 @@ export default function Deals() {
737
  const [seedBusy, setSeedBusy] = useState(false);
738
  const [panelOpen, setPanelOpen] = useState(false);
739
  const [dealDetail, setDealDetail] = useState(null);
740
- const [tableEditRowId, setTableEditRowId] = useState(null);
741
  const [dealsView, setDealsView] = useState('main');
742
  const [createBusy, setCreateBusy] = useState(false);
743
  const [companyFetchLoading, setCompanyFetchLoading] = useState(false);
744
  const [companyFetchError, setCompanyFetchError] = useState('');
745
- const [dealPanelForm, setDealPanelForm] = useState(null);
746
- const [dealPanelSaving, setDealPanelSaving] = useState(false);
747
  const [me, setMe] = useState(null);
748
  const [tenantMembers, setTenantMembers] = useState([]);
749
 
@@ -795,10 +710,6 @@ export default function Deals() {
795
  return () => clearTimeout(t);
796
  }, [fetchDeals]);
797
 
798
- useEffect(() => {
799
- setTableEditRowId(null);
800
- }, [dealsView]);
801
-
802
  const dealsByStage = useMemo(() => {
803
  return STAGES.map((s) => ({
804
  ...s,
@@ -909,8 +820,7 @@ export default function Deals() {
909
  throw new Error(msg || 'Could not create deal');
910
  }
911
  await fetchDeals();
912
- setTableEditRowId(data.id);
913
- openDeal(data, { keepTableEdit: true });
914
  requestAnimationFrame(() => {
915
  requestAnimationFrame(() => {
916
  const tr = document.querySelector(`tr[data-deal-id="${data.id}"]`);
@@ -996,11 +906,9 @@ export default function Deals() {
996
 
997
  const updateStage = (dealId, stage) => patchDeal(dealId, { stage });
998
 
999
- const openDeal = async (deal, opts = {}) => {
1000
- if (!opts.keepTableEdit) setTableEditRowId(null);
1001
  setPanelOpen(true);
1002
  setDealDetail(deal);
1003
- setDealPanelForm(null);
1004
  try {
1005
  const res = await apiFetch(`/api/deals/${deal.id}`);
1006
  if (res.ok) {
@@ -1012,58 +920,14 @@ export default function Deals() {
1012
  }
1013
  };
1014
 
1015
- useEffect(() => {
1016
- if (!dealDetail) {
1017
- setDealPanelForm(null);
1018
- return;
1019
- }
1020
- setDealPanelForm({
1021
- name: dealDetail.name || '',
1022
- deal_value: dealDetail.deal_value != null ? String(dealDetail.deal_value) : '',
1023
- revenue_type: revenueTypeSelectValue(dealDetail),
1024
- close_prob: closeProbabilitySelectValue(dealDetail.close_probability),
1025
- expected_close: isoToDateInput(dealDetail.expected_close_date),
1026
- contact_display: dealDetail.contact_display || '',
1027
- last_interaction: isoToDateInput(dealDetail.last_interaction_at),
1028
- });
1029
- }, [dealDetail]);
1030
-
1031
- const saveDealPanel = async () => {
1032
- if (!dealDetail || !dealPanelForm) return;
1033
- setDealPanelSaving(true);
1034
- try {
1035
- let deal_value = null;
1036
- if (dealPanelForm.deal_value.trim() !== '') {
1037
- const n = Math.round(Number(dealPanelForm.deal_value));
1038
- if (Number.isFinite(n)) deal_value = n;
1039
- }
1040
- let close_probability = 0;
1041
- if (dealPanelForm.close_prob !== '__clear__') {
1042
- close_probability = Number(dealPanelForm.close_prob);
1043
- if (!Number.isFinite(close_probability)) close_probability = 0;
1044
- }
1045
- await patchDeal(dealDetail.id, {
1046
- name: dealPanelForm.name.trim() || 'Untitled deal',
1047
- deal_value,
1048
- revenue_type: dealPanelForm.revenue_type,
1049
- close_probability,
1050
- expected_close_date: dealPanelForm.expected_close.trim() ? dealPanelForm.expected_close.trim() : null,
1051
- contact_display: dealPanelForm.contact_display.trim(),
1052
- last_interaction_at: dealPanelForm.last_interaction.trim()
1053
- ? dealPanelForm.last_interaction.trim()
1054
- : null,
1055
- });
1056
- } finally {
1057
- setDealPanelSaving(false);
1058
- }
1059
- };
1060
-
1061
  const closePanel = () => {
1062
- setTableEditRowId(null);
1063
  setPanelOpen(false);
1064
  setDealDetail(null);
1065
  };
1066
 
 
 
 
1067
  return (
1068
  <AppShell
1069
  title="Deals"
@@ -1161,11 +1025,11 @@ export default function Deals() {
1161
  <table className="w-full text-sm min-w-[1040px]">
1162
  <thead>
1163
  <tr className="bg-slate-50 text-left text-slate-600 border-b border-slate-200">
1164
- <th className="px-2 py-2 w-[4.25rem]" aria-label="Select and table edit" />
1165
  <th className="px-3 py-2 font-medium">Deal</th>
1166
  <th className="px-3 py-2 font-medium">Stage</th>
1167
  <th className="px-3 py-2 font-medium">Owner</th>
1168
- <th className="px-2 py-2 font-medium text-right w-[9rem] max-w-[9rem]">
1169
  Deal value
1170
  </th>
1171
  <th className="px-3 py-2 font-medium">Revenue</th>
@@ -1184,8 +1048,6 @@ export default function Deals() {
1184
  <DealRow
1185
  key={deal.id}
1186
  deal={deal}
1187
- tableEditRowId={tableEditRowId}
1188
- setTableEditRowId={setTableEditRowId}
1189
  patchDeal={patchDeal}
1190
  updateStage={updateStage}
1191
  openDeal={openDeal}
@@ -1220,8 +1082,6 @@ export default function Deals() {
1220
  </div>
1221
  }
1222
  deals={group.deals}
1223
- tableEditRowId={tableEditRowId}
1224
- setTableEditRowId={setTableEditRowId}
1225
  patchDeal={patchDeal}
1226
  updateStage={updateStage}
1227
  openDeal={openDeal}
@@ -1246,8 +1106,6 @@ export default function Deals() {
1246
  />
1247
  }
1248
  deals={group.deals}
1249
- tableEditRowId={tableEditRowId}
1250
- setTableEditRowId={setTableEditRowId}
1251
  patchDeal={patchDeal}
1252
  updateStage={updateStage}
1253
  openDeal={openDeal}
@@ -1282,8 +1140,6 @@ export default function Deals() {
1282
  </div>
1283
  }
1284
  deals={group.deals}
1285
- tableEditRowId={tableEditRowId}
1286
- setTableEditRowId={setTableEditRowId}
1287
  patchDeal={patchDeal}
1288
  updateStage={updateStage}
1289
  openDeal={openDeal}
@@ -1304,18 +1160,18 @@ export default function Deals() {
1304
  open={panelOpen && !!dealDetail}
1305
  onClose={closePanel}
1306
  title={
1307
- dealPanelForm ? (
1308
- <Input
1309
- value={dealPanelForm.name}
1310
- onChange={(e) =>
1311
- setDealPanelForm((f) => ({ ...f, name: e.target.value }))
 
1312
  }
1313
- className="text-lg font-semibold text-slate-900 border-slate-200 h-10 px-3 w-full max-w-full shadow-sm"
1314
- placeholder="Deal name"
1315
- aria-label="Deal name"
1316
  />
1317
  ) : (
1318
- dealDetail?.name || 'Deal'
1319
  )
1320
  }
1321
  subtitle={
@@ -1325,45 +1181,43 @@ export default function Deals() {
1325
  }
1326
  widthClassName="max-w-xl"
1327
  >
1328
- {dealDetail && dealPanelForm && (
1329
  <div className="space-y-6 text-sm">
1330
  <section className="rounded-xl border border-slate-200 bg-slate-50/50 p-4">
1331
  <h4 className="text-sm font-semibold text-slate-800 mb-3">Deal information</h4>
1332
- <div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
1333
- <div>
1334
  <label className="block text-xs font-medium text-slate-500 mb-1">
1335
  Deal value (USD)
1336
  </label>
1337
- <Input
1338
- type="number"
1339
- min={0}
1340
- step={1}
1341
- value={dealPanelForm.deal_value}
1342
- onChange={(e) =>
1343
- setDealPanelForm((f) => ({
1344
- ...f,
1345
- deal_value: e.target.value,
1346
- }))
1347
- }
1348
- className="text-sm tabular-nums bg-white"
1349
- placeholder=""
1350
  />
1351
  </div>
1352
- <div>
1353
  <label className="block text-xs font-medium text-slate-500 mb-1">
1354
  Revenue
1355
  </label>
1356
  <Select
1357
- value={dealPanelForm.revenue_type}
1358
- onValueChange={(v) =>
1359
- setDealPanelForm((f) => ({ ...f, revenue_type: v }))
1360
- }
1361
  >
1362
  <SelectTrigger className="h-10 border-slate-200 bg-white shadow-sm text-sm">
1363
  <span className="font-medium">
1364
  {
1365
  REVENUE_TYPES.find(
1366
- (t) => t.value === dealPanelForm.revenue_type
1367
  )?.label
1368
  }
1369
  </span>
@@ -1377,21 +1231,23 @@ export default function Deals() {
1377
  </SelectContent>
1378
  </Select>
1379
  </div>
1380
- <div>
1381
  <label className="block text-xs font-medium text-slate-500 mb-1">
1382
  Close probability
1383
  </label>
1384
  <Select
1385
- value={dealPanelForm.close_prob}
1386
- onValueChange={(v) =>
1387
- setDealPanelForm((f) => ({ ...f, close_prob: v }))
1388
- }
 
 
 
 
1389
  >
1390
  <SelectTrigger className="h-10 border-slate-200 bg-white shadow-sm">
1391
  <span className="tabular-nums">
1392
- {dealPanelForm.close_prob === '__clear__'
1393
- ? '—'
1394
- : `${dealPanelForm.close_prob}%`}
1395
  </span>
1396
  </SelectTrigger>
1397
  <SelectContent>
@@ -1406,70 +1262,48 @@ export default function Deals() {
1406
  </SelectContent>
1407
  </Select>
1408
  </div>
1409
- <div>
1410
  <label className="block text-xs font-medium text-slate-500 mb-1">
1411
  Expected close
1412
  </label>
1413
- <Input
1414
- type="date"
1415
- value={dealPanelForm.expected_close}
1416
- onChange={(e) =>
1417
- setDealPanelForm((f) => ({
1418
- ...f,
1419
- expected_close: e.target.value,
1420
- }))
1421
  }
1422
- className="text-sm bg-white"
1423
  />
1424
  </div>
1425
- <div>
1426
  <label className="block text-xs font-medium text-slate-500 mb-1">
1427
  Last interaction
1428
  </label>
1429
- <Input
1430
- type="date"
1431
- value={dealPanelForm.last_interaction}
1432
- onChange={(e) =>
1433
- setDealPanelForm((f) => ({
1434
- ...f,
1435
- last_interaction: e.target.value,
1436
- }))
1437
  }
1438
- className="text-sm bg-white"
1439
  />
1440
  </div>
1441
- <div className="sm:col-span-2">
1442
  <label className="block text-xs font-medium text-slate-500 mb-1">
1443
  Contact (display)
1444
  </label>
1445
- <Input
1446
- value={dealPanelForm.contact_display}
1447
- onChange={(e) =>
1448
- setDealPanelForm((f) => ({
1449
- ...f,
1450
- contact_display: e.target.value,
1451
- }))
1452
  }
1453
- className="text-sm bg-white"
1454
- placeholder="Name shown on deal card"
1455
  />
1456
  </div>
1457
  </div>
1458
- <Button
1459
- type="button"
1460
- className="w-full sm:w-auto"
1461
- onClick={saveDealPanel}
1462
- disabled={dealPanelSaving}
1463
- >
1464
- {dealPanelSaving ? (
1465
- <>
1466
- <Loader2 className="h-4 w-4 animate-spin mr-2" aria-hidden />
1467
- Saving…
1468
- </>
1469
- ) : (
1470
- 'Save deal'
1471
- )}
1472
- </Button>
1473
  {dealDetail.source_campaign_name ? (
1474
  <p className="mt-3 text-xs text-slate-500">
1475
  Source campaign:{' '}
@@ -1495,6 +1329,7 @@ export default function Deals() {
1495
  patchLinkedContact(dealDetail.linked_contact.id, patch)
1496
  }
1497
  className="mb-0"
 
1498
  />
1499
  <div className="mt-2">
1500
  <Button asChild variant="outline" size="sm">
@@ -1523,10 +1358,11 @@ export default function Deals() {
1523
  onFetch={fetchDealCompanyAi}
1524
  fetchLoading={companyFetchLoading}
1525
  fetchError={companyFetchError}
 
1526
  />
1527
  </section>
1528
  </div>
1529
- )}
1530
  </SlideOverPanel>
1531
  </AppShell>
1532
  );
 
1
  import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
+ import { GitBranch, Loader2, LayoutGrid, MessageSquare } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
 
5
  import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select';
6
  import AppShell from '@/components/layout/AppShell';
7
  import { apiFetch } from '@/lib/api';
 
85
  );
86
  }
87
 
 
 
 
 
 
 
 
88
  /** Maps stored probability to Select value: __clear__ or "10"…"100" (nearest 10, min 10). */
89
  function closeProbabilitySelectValue(p) {
90
  if (p == null || p === '') return '__clear__';
 
142
  barClassName,
143
  headerContent,
144
  deals,
 
 
145
  patchDeal,
146
  updateStage,
147
  openDeal,
 
164
  <DealRow
165
  key={deal.id}
166
  deal={deal}
 
 
167
  patchDeal={patchDeal}
168
  updateStage={updateStage}
169
  openDeal={openDeal}
 
360
  );
361
  }
362
 
363
+ function DealRow({ deal, patchDeal, updateStage, openDeal, isAdmin, currentUserId, tenantMembers }) {
 
 
 
 
 
 
 
 
 
 
364
  const meta = stageMeta(deal.stage);
365
  const safeStage = STAGES.some((s) => s.value === deal.stage) ? deal.stage : 'new';
366
  const closeSel = closeProbabilitySelectValue(deal.close_probability);
 
371
  tabIndex={0}
372
  onClick={(e) => {
373
  if (isRowUiTarget(e)) return;
 
374
  openDeal(deal);
375
  }}
376
  onKeyDown={(e) => {
377
  if (isRowUiTarget(e)) return;
378
  if (e.key === 'Enter' || e.key === ' ') {
379
  e.preventDefault();
 
380
  openDeal(deal);
381
  }
382
  }}
383
  className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
384
  >
385
+ <td className="px-2 py-2 w-10 text-center align-middle" onClick={(e) => e.stopPropagation()}>
386
+ <input type="checkbox" className="rounded border-slate-300" aria-label="Select row" />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
387
  </td>
388
+ <td className="px-3 py-2 align-top max-w-[220px] font-medium" onClick={(e) => e.stopPropagation()}>
389
+ <EditableCell
390
+ value={deal.name || ''}
391
+ onCommit={(v) => patchDeal(deal.id, { name: v })}
392
+ inputClassName="font-medium text-slate-900"
393
+ />
 
 
 
 
 
 
394
  </td>
395
  <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
396
  <Select value={safeStage} onValueChange={(v) => updateStage(deal.id, v)}>
 
523
  )}
524
  </td>
525
  <td
526
+ className="px-2 py-2 align-top tabular-nums w-[min(11rem,100%)] min-w-[10rem] max-w-[12rem] shrink-0"
527
  onClick={(e) => e.stopPropagation()}
528
  >
529
  <EditableCurrencyCell
 
565
  </SelectContent>
566
  </Select>
567
  </td>
568
+ <td className="px-3 py-2 align-top max-w-[180px]" onClick={(e) => e.stopPropagation()}>
569
+ <EditableCell
570
+ value={deal.contact_display || ''}
571
+ onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
572
+ inputClassName="text-xs"
573
+ />
 
 
 
 
 
 
 
 
 
 
 
 
574
  </td>
575
+ <td className="px-3 py-2 align-top max-w-[200px]" onClick={(e) => e.stopPropagation()}>
576
+ <EditableCell
577
+ value={deal.account_name || ''}
578
+ onCommit={(v) => patchDeal(deal.id, { account_name: v })}
579
+ inputClassName="text-xs"
580
+ />
 
 
 
 
 
 
 
 
 
 
 
 
581
  </td>
582
  <td className="px-3 py-2 align-top" onClick={(e) => e.stopPropagation()}>
583
  <EditableDateCell
 
655
  const [seedBusy, setSeedBusy] = useState(false);
656
  const [panelOpen, setPanelOpen] = useState(false);
657
  const [dealDetail, setDealDetail] = useState(null);
 
658
  const [dealsView, setDealsView] = useState('main');
659
  const [createBusy, setCreateBusy] = useState(false);
660
  const [companyFetchLoading, setCompanyFetchLoading] = useState(false);
661
  const [companyFetchError, setCompanyFetchError] = useState('');
 
 
662
  const [me, setMe] = useState(null);
663
  const [tenantMembers, setTenantMembers] = useState([]);
664
 
 
710
  return () => clearTimeout(t);
711
  }, [fetchDeals]);
712
 
 
 
 
 
713
  const dealsByStage = useMemo(() => {
714
  return STAGES.map((s) => ({
715
  ...s,
 
820
  throw new Error(msg || 'Could not create deal');
821
  }
822
  await fetchDeals();
823
+ openDeal(data);
 
824
  requestAnimationFrame(() => {
825
  requestAnimationFrame(() => {
826
  const tr = document.querySelector(`tr[data-deal-id="${data.id}"]`);
 
906
 
907
  const updateStage = (dealId, stage) => patchDeal(dealId, { stage });
908
 
909
+ const openDeal = async (deal) => {
 
910
  setPanelOpen(true);
911
  setDealDetail(deal);
 
912
  try {
913
  const res = await apiFetch(`/api/deals/${deal.id}`);
914
  if (res.ok) {
 
920
  }
921
  };
922
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  const closePanel = () => {
 
924
  setPanelOpen(false);
925
  setDealDetail(null);
926
  };
927
 
928
+ const dealPanelCloseSel =
929
+ dealDetail != null ? closeProbabilitySelectValue(dealDetail.close_probability) : '__clear__';
930
+
931
  return (
932
  <AppShell
933
  title="Deals"
 
1025
  <table className="w-full text-sm min-w-[1040px]">
1026
  <thead>
1027
  <tr className="bg-slate-50 text-left text-slate-600 border-b border-slate-200">
1028
+ <th className="px-2 py-2 w-10" aria-label="Select row" />
1029
  <th className="px-3 py-2 font-medium">Deal</th>
1030
  <th className="px-3 py-2 font-medium">Stage</th>
1031
  <th className="px-3 py-2 font-medium">Owner</th>
1032
+ <th className="px-2 py-2 font-medium text-right w-[min(11rem,100%)] min-w-[10rem] max-w-[12rem]">
1033
  Deal value
1034
  </th>
1035
  <th className="px-3 py-2 font-medium">Revenue</th>
 
1048
  <DealRow
1049
  key={deal.id}
1050
  deal={deal}
 
 
1051
  patchDeal={patchDeal}
1052
  updateStage={updateStage}
1053
  openDeal={openDeal}
 
1082
  </div>
1083
  }
1084
  deals={group.deals}
 
 
1085
  patchDeal={patchDeal}
1086
  updateStage={updateStage}
1087
  openDeal={openDeal}
 
1106
  />
1107
  }
1108
  deals={group.deals}
 
 
1109
  patchDeal={patchDeal}
1110
  updateStage={updateStage}
1111
  openDeal={openDeal}
 
1140
  </div>
1141
  }
1142
  deals={group.deals}
 
 
1143
  patchDeal={patchDeal}
1144
  updateStage={updateStage}
1145
  openDeal={openDeal}
 
1160
  open={panelOpen && !!dealDetail}
1161
  onClose={closePanel}
1162
  title={
1163
+ dealDetail ? (
1164
+ <EditableCell
1165
+ key={dealDetail.id}
1166
+ value={dealDetail.name || ''}
1167
+ onCommit={(v) =>
1168
+ patchDeal(dealDetail.id, { name: (v || '').trim() || 'Untitled deal' })
1169
  }
1170
+ className="text-lg font-semibold"
1171
+ inputClassName="text-lg font-semibold text-slate-900 px-2 py-1.5 border border-slate-200 rounded-md bg-white shadow-sm w-full max-w-full"
 
1172
  />
1173
  ) : (
1174
+ 'Deal'
1175
  )
1176
  }
1177
  subtitle={
 
1181
  }
1182
  widthClassName="max-w-xl"
1183
  >
1184
+ {dealDetail ? (
1185
  <div className="space-y-6 text-sm">
1186
  <section className="rounded-xl border border-slate-200 bg-slate-50/50 p-4">
1187
  <h4 className="text-sm font-semibold text-slate-800 mb-3">Deal information</h4>
1188
+ <div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
1189
+ <div onClick={(e) => e.stopPropagation()}>
1190
  <label className="block text-xs font-medium text-slate-500 mb-1">
1191
  Deal value (USD)
1192
  </label>
1193
+ <EditableCurrencyCell
1194
+ value={dealDetail.deal_value != null ? String(dealDetail.deal_value) : ''}
1195
+ onCommit={(v) => {
1196
+ if (v.trim() === '') {
1197
+ patchDeal(dealDetail.id, { deal_value: null });
1198
+ return;
1199
+ }
1200
+ const n = Math.round(Number(v));
1201
+ if (!Number.isFinite(n)) return;
1202
+ patchDeal(dealDetail.id, { deal_value: n });
1203
+ }}
1204
+ className="w-full bg-white rounded-md border border-slate-200 px-2 py-2"
1205
+ inputClassName="text-sm tabular-nums"
1206
  />
1207
  </div>
1208
+ <div onClick={(e) => e.stopPropagation()}>
1209
  <label className="block text-xs font-medium text-slate-500 mb-1">
1210
  Revenue
1211
  </label>
1212
  <Select
1213
+ value={revenueTypeSelectValue(dealDetail)}
1214
+ onValueChange={(v) => patchDeal(dealDetail.id, { revenue_type: v })}
 
 
1215
  >
1216
  <SelectTrigger className="h-10 border-slate-200 bg-white shadow-sm text-sm">
1217
  <span className="font-medium">
1218
  {
1219
  REVENUE_TYPES.find(
1220
+ (t) => t.value === revenueTypeSelectValue(dealDetail)
1221
  )?.label
1222
  }
1223
  </span>
 
1231
  </SelectContent>
1232
  </Select>
1233
  </div>
1234
+ <div onClick={(e) => e.stopPropagation()}>
1235
  <label className="block text-xs font-medium text-slate-500 mb-1">
1236
  Close probability
1237
  </label>
1238
  <Select
1239
+ value={dealPanelCloseSel}
1240
+ onValueChange={(v) => {
1241
+ if (v === '__clear__') {
1242
+ patchDeal(dealDetail.id, { close_probability: 0 });
1243
+ return;
1244
+ }
1245
+ patchDeal(dealDetail.id, { close_probability: Number(v) });
1246
+ }}
1247
  >
1248
  <SelectTrigger className="h-10 border-slate-200 bg-white shadow-sm">
1249
  <span className="tabular-nums">
1250
+ {dealPanelCloseSel === '__clear__' ? '—' : `${dealPanelCloseSel}%`}
 
 
1251
  </span>
1252
  </SelectTrigger>
1253
  <SelectContent>
 
1262
  </SelectContent>
1263
  </Select>
1264
  </div>
1265
+ <div onClick={(e) => e.stopPropagation()}>
1266
  <label className="block text-xs font-medium text-slate-500 mb-1">
1267
  Expected close
1268
  </label>
1269
+ <EditableDateCell
1270
+ value={dealDetail.expected_close_date}
1271
+ onCommit={(dateStr) =>
1272
+ patchDeal(dealDetail.id, {
1273
+ expected_close_date: dateStr || null,
1274
+ })
 
 
1275
  }
1276
+ className="text-sm bg-white rounded-md border border-slate-200 px-2 py-2 w-full"
1277
  />
1278
  </div>
1279
+ <div onClick={(e) => e.stopPropagation()}>
1280
  <label className="block text-xs font-medium text-slate-500 mb-1">
1281
  Last interaction
1282
  </label>
1283
+ <EditableDateCell
1284
+ value={dealDetail.last_interaction_at}
1285
+ onCommit={(dateStr) =>
1286
+ patchDeal(dealDetail.id, {
1287
+ last_interaction_at: dateStr || null,
1288
+ })
 
 
1289
  }
1290
+ className="text-sm bg-white rounded-md border border-slate-200 px-2 py-2 w-full"
1291
  />
1292
  </div>
1293
+ <div className="sm:col-span-2" onClick={(e) => e.stopPropagation()}>
1294
  <label className="block text-xs font-medium text-slate-500 mb-1">
1295
  Contact (display)
1296
  </label>
1297
+ <EditableCell
1298
+ value={dealDetail.contact_display || ''}
1299
+ onCommit={(v) =>
1300
+ patchDeal(dealDetail.id, { contact_display: v.trim() })
 
 
 
1301
  }
1302
+ className="bg-white rounded-md border border-slate-200 px-2 py-2 w-full"
1303
+ inputClassName="text-sm"
1304
  />
1305
  </div>
1306
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1307
  {dealDetail.source_campaign_name ? (
1308
  <p className="mt-3 text-xs text-slate-500">
1309
  Source campaign:{' '}
 
1329
  patchLinkedContact(dealDetail.linked_contact.id, patch)
1330
  }
1331
  className="mb-0"
1332
+ autoSave
1333
  />
1334
  <div className="mt-2">
1335
  <Button asChild variant="outline" size="sm">
 
1358
  onFetch={fetchDealCompanyAi}
1359
  fetchLoading={companyFetchLoading}
1360
  fetchError={companyFetchError}
1361
+ autoSave
1362
  />
1363
  </section>
1364
  </div>
1365
+ ) : null}
1366
  </SlideOverPanel>
1367
  </AppShell>
1368
  );
frontend/src/pages/Leads.jsx CHANGED
@@ -699,6 +699,7 @@ export default function Leads() {
699
  email={selected.email || ''}
700
  title={selected.title || ''}
701
  onSave={(patch) => patchLead(selected.id, patch)}
 
702
  />
703
  {selected.contact ? (
704
  <p className="text-xs text-violet-700 mb-4">
@@ -719,6 +720,7 @@ export default function Leads() {
719
  onFetch={fetchLeadCompanyAi}
720
  fetchLoading={companyFetchLoading}
721
  fetchError={companyFetchError}
 
722
  />
723
  </div>
724
 
 
699
  email={selected.email || ''}
700
  title={selected.title || ''}
701
  onSave={(patch) => patchLead(selected.id, patch)}
702
+ autoSave
703
  />
704
  {selected.contact ? (
705
  <p className="text-xs text-violet-700 mb-4">
 
720
  onFetch={fetchLeadCompanyAi}
721
  fetchLoading={companyFetchLoading}
722
  fetchError={companyFetchError}
723
+ autoSave
724
  />
725
  </div>
726