Seth commited on
Commit
edc0890
·
1 Parent(s): 65a736f
frontend/src/pages/Contacts.jsx CHANGED
@@ -64,6 +64,8 @@ export default function Contacts() {
64
  const [enrichError, setEnrichError] = useState('');
65
  const [rowSelection, setRowSelection] = useState({});
66
  const [bulkBusy, setBulkBusy] = useState(null);
 
 
67
 
68
  const selectedIds = useMemo(
69
  () => Object.keys(rowSelection).filter((id) => rowSelection[id]).map(Number),
@@ -176,6 +178,7 @@ export default function Contacts() {
176
  };
177
 
178
  const openContact = async (contact) => {
 
179
  setSelectedContact(contact);
180
  setSelectedContactDetails(null);
181
  setEnrichError('');
@@ -202,6 +205,7 @@ export default function Contacts() {
202
  };
203
 
204
  const closePanel = () => {
 
205
  setSelectedContact(null);
206
  setSelectedContactDetails(null);
207
  setSequences([]);
@@ -760,12 +764,14 @@ export default function Contacts() {
760
  tabIndex={0}
761
  onClick={(e) => {
762
  if (isRowUiTarget(e)) return;
 
763
  openContact(contact);
764
  }}
765
  onKeyDown={(e) => {
766
  if (isRowUiTarget(e)) return;
767
  if (e.key === 'Enter' || e.key === ' ') {
768
  e.preventDefault();
 
769
  openContact(contact);
770
  }
771
  }}
@@ -791,54 +797,95 @@ export default function Contacts() {
791
  type="button"
792
  variant="ghost"
793
  size="icon"
794
- className="h-8 w-8 text-slate-500 hover:text-violet-700"
 
 
 
 
795
  onClick={(e) => {
796
  e.stopPropagation();
797
- focusFirstEditableInRow(
798
- e.currentTarget.closest('tr')
 
 
 
 
 
 
 
799
  );
800
  }}
801
- aria-label={`Edit fields in row for ${displayName}`}
 
 
 
 
802
  >
803
  <Pencil className="h-4 w-4" aria-hidden />
804
  </Button>
805
  </td>
806
  <td className="px-3 py-2 align-top">
807
- <div className="flex flex-col gap-1 min-w-[140px] max-w-[200px]">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
808
  <EditableCell
809
- value={contact.first_name || ''}
810
- onCommit={(v) =>
811
- patchContact(contact.id, { first_name: v })
812
- }
813
- inputClassName="font-medium text-violet-800"
814
  />
 
 
 
 
 
 
 
 
815
  <EditableCell
816
- value={contact.last_name || ''}
817
  onCommit={(v) =>
818
- patchContact(contact.id, { last_name: v })
819
  }
820
- inputClassName="font-medium text-violet-800"
821
  />
822
- </div>
823
- </td>
824
- <td className="px-3 py-2 align-top max-w-[240px]">
825
- <EditableCell
826
- type="email"
827
- value={contact.email || ''}
828
- onCommit={(v) => patchContact(contact.id, { email: v })}
829
- />
830
- </td>
831
- <td className="px-3 py-2 align-top max-w-[220px]">
832
- <EditableCell
833
- value={contact.company || ''}
834
- onCommit={(v) => patchContact(contact.id, { company: v })}
835
- />
836
  </td>
837
  <td className="px-3 py-2 align-top max-w-[200px]">
838
- <EditableCell
839
- value={contact.title || ''}
840
- onCommit={(v) => patchContact(contact.id, { title: v })}
841
- />
 
 
 
 
 
 
842
  </td>
843
  </tr>
844
  );
 
64
  const [enrichError, setEnrichError] = useState('');
65
  const [rowSelection, setRowSelection] = useState({});
66
  const [bulkBusy, setBulkBusy] = useState(null);
67
+ /** Inline inputs only for this row id; pencil toggles. Row click opens slide-over. */
68
+ const [tableEditRowId, setTableEditRowId] = useState(null);
69
 
70
  const selectedIds = useMemo(
71
  () => Object.keys(rowSelection).filter((id) => rowSelection[id]).map(Number),
 
178
  };
179
 
180
  const openContact = async (contact) => {
181
+ setTableEditRowId(null);
182
  setSelectedContact(contact);
183
  setSelectedContactDetails(null);
184
  setEnrichError('');
 
205
  };
206
 
207
  const closePanel = () => {
208
+ setTableEditRowId(null);
209
  setSelectedContact(null);
210
  setSelectedContactDetails(null);
211
  setSequences([]);
 
764
  tabIndex={0}
765
  onClick={(e) => {
766
  if (isRowUiTarget(e)) return;
767
+ setTableEditRowId(null);
768
  openContact(contact);
769
  }}
770
  onKeyDown={(e) => {
771
  if (isRowUiTarget(e)) return;
772
  if (e.key === 'Enter' || e.key === ' ') {
773
  e.preventDefault();
774
+ setTableEditRowId(null);
775
  openContact(contact);
776
  }
777
  }}
 
797
  type="button"
798
  variant="ghost"
799
  size="icon"
800
+ className={cn(
801
+ 'h-8 w-8 text-slate-500 hover:text-violet-700',
802
+ tableEditRowId === contact.id &&
803
+ 'bg-violet-100 text-violet-800 hover:bg-violet-100'
804
+ )}
805
  onClick={(e) => {
806
  e.stopPropagation();
807
+ const tr = e.currentTarget.closest('tr');
808
+ const off = tableEditRowId === contact.id;
809
+ if (off) {
810
+ setTableEditRowId(null);
811
+ return;
812
+ }
813
+ setTableEditRowId(contact.id);
814
+ requestAnimationFrame(() =>
815
+ focusFirstEditableInRow(tr)
816
  );
817
  }}
818
+ aria-label={
819
+ tableEditRowId === contact.id
820
+ ? `Stop editing row for ${displayName}`
821
+ : `Edit fields in row for ${displayName}`
822
+ }
823
  >
824
  <Pencil className="h-4 w-4" aria-hidden />
825
  </Button>
826
  </td>
827
  <td className="px-3 py-2 align-top">
828
+ {tableEditRowId === contact.id ? (
829
+ <div className="flex flex-col gap-1 min-w-[140px] max-w-[200px]">
830
+ <EditableCell
831
+ value={contact.first_name || ''}
832
+ onCommit={(v) =>
833
+ patchContact(contact.id, { first_name: v })
834
+ }
835
+ inputClassName="font-medium text-violet-800"
836
+ />
837
+ <EditableCell
838
+ value={contact.last_name || ''}
839
+ onCommit={(v) =>
840
+ patchContact(contact.id, { last_name: v })
841
+ }
842
+ inputClassName="font-medium text-violet-800"
843
+ />
844
+ </div>
845
+ ) : (
846
+ <div className="min-h-[2.25rem] py-1 text-sm font-medium text-violet-800 leading-snug">
847
+ {displayName}
848
+ </div>
849
+ )}
850
+ </td>
851
+ <td className="px-3 py-2 align-top max-w-[240px]">
852
+ {tableEditRowId === contact.id ? (
853
  <EditableCell
854
+ type="email"
855
+ value={contact.email || ''}
856
+ onCommit={(v) => patchContact(contact.id, { email: v })}
 
 
857
  />
858
+ ) : (
859
+ <div className="min-h-[2.25rem] py-1 text-sm text-slate-700 truncate">
860
+ {contact.email || '—'}
861
+ </div>
862
+ )}
863
+ </td>
864
+ <td className="px-3 py-2 align-top max-w-[220px]">
865
+ {tableEditRowId === contact.id ? (
866
  <EditableCell
867
+ value={contact.company || ''}
868
  onCommit={(v) =>
869
+ patchContact(contact.id, { company: v })
870
  }
 
871
  />
872
+ ) : (
873
+ <div className="min-h-[2.25rem] py-1 text-sm text-slate-700 truncate">
874
+ {contact.company || '—'}
875
+ </div>
876
+ )}
 
 
 
 
 
 
 
 
 
877
  </td>
878
  <td className="px-3 py-2 align-top max-w-[200px]">
879
+ {tableEditRowId === contact.id ? (
880
+ <EditableCell
881
+ value={contact.title || ''}
882
+ onCommit={(v) => patchContact(contact.id, { title: v })}
883
+ />
884
+ ) : (
885
+ <div className="min-h-[2.25rem] py-1 text-sm text-slate-600 truncate">
886
+ {contact.title || '—'}
887
+ </div>
888
+ )}
889
  </td>
890
  </tr>
891
  );
frontend/src/pages/Deals.jsx CHANGED
@@ -61,6 +61,7 @@ export default function Deals() {
61
  const [seedBusy, setSeedBusy] = useState(false);
62
  const [panelOpen, setPanelOpen] = useState(false);
63
  const [dealDetail, setDealDetail] = useState(null);
 
64
 
65
  const fetchDeals = useCallback(async () => {
66
  setLoading(true);
@@ -126,6 +127,7 @@ export default function Deals() {
126
  const updateStage = (dealId, stage) => patchDeal(dealId, { stage });
127
 
128
  const openDeal = async (deal) => {
 
129
  setPanelOpen(true);
130
  setDealDetail(deal);
131
  try {
@@ -140,6 +142,7 @@ export default function Deals() {
140
  };
141
 
142
  const closePanel = () => {
 
143
  setPanelOpen(false);
144
  setDealDetail(null);
145
  };
@@ -220,12 +223,14 @@ export default function Deals() {
220
  tabIndex={0}
221
  onClick={(e) => {
222
  if (isRowUiTarget(e)) return;
 
223
  openDeal(deal);
224
  }}
225
  onKeyDown={(e) => {
226
  if (isRowUiTarget(e)) return;
227
  if (e.key === 'Enter' || e.key === ' ') {
228
  e.preventDefault();
 
229
  openDeal(deal);
230
  }
231
  }}
@@ -241,25 +246,45 @@ export default function Deals() {
241
  type="button"
242
  variant="ghost"
243
  size="icon"
244
- className="h-7 w-7 text-slate-500 hover:text-violet-700"
 
 
 
 
245
  onClick={(e) => {
246
  e.stopPropagation();
247
- focusFirstEditableInRow(
248
- e.currentTarget.closest('tr')
 
 
 
 
 
 
249
  );
250
  }}
251
- aria-label={`Edit fields in row for ${deal.name || 'deal'}`}
 
 
 
 
252
  >
253
  <Pencil className="h-3.5 w-3.5" aria-hidden />
254
  </Button>
255
  </div>
256
  </td>
257
  <td className="px-3 py-2 align-top max-w-[220px] font-medium">
258
- <EditableCell
259
- value={deal.name || ''}
260
- onCommit={(v) => patchDeal(deal.id, { name: v })}
261
- inputClassName="font-medium text-slate-900"
262
- />
 
 
 
 
 
 
263
  </td>
264
  <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
265
  <Select
@@ -297,73 +322,130 @@ export default function Deals() {
297
  </Select>
298
  </td>
299
  <td className="px-3 py-2 align-top">
300
- <EditableCell
301
- value={deal.owner_initials || ''}
302
- onCommit={(v) =>
303
- patchDeal(deal.id, {
304
- owner_initials: (v || '').slice(0, 8).toUpperCase(),
305
- })
306
- }
307
- inputClassName="text-center uppercase tracking-wide max-w-[4.5rem] font-semibold"
308
- />
 
 
 
 
 
 
 
 
309
  </td>
310
  <td className="px-3 py-2 align-top tabular-nums max-w-[120px]">
311
- <EditableCell
312
- type="number"
313
- value={deal.deal_value != null ? String(deal.deal_value) : ''}
314
- onCommit={(v) => {
315
- if (v.trim() === '') {
316
- patchDeal(deal.id, { deal_value: null });
317
- return;
318
  }
319
- const n = Math.round(Number(v));
320
- if (!Number.isFinite(n)) return;
321
- patchDeal(deal.id, { deal_value: n });
322
- }}
323
- />
 
 
 
 
 
 
 
 
 
 
324
  </td>
325
  <td className="px-3 py-2 align-top max-w-[180px]">
326
- <EditableCell
327
- value={deal.contact_display || ''}
328
- onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
329
- inputClassName="text-xs"
330
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
331
  </td>
332
  <td className="px-3 py-2 align-top max-w-[200px]">
333
- <EditableCell
334
- value={deal.account_name || ''}
335
- onCommit={(v) => patchDeal(deal.id, { account_name: v })}
336
- inputClassName="text-xs"
337
- />
 
 
 
 
 
 
 
 
 
 
 
 
338
  </td>
339
  <td className="px-3 py-2 align-top">
340
- <EditableDateCell
341
- value={deal.expected_close_date}
342
- onCommit={(dateStr) =>
343
- patchDeal(deal.id, {
344
- expected_close_date: dateStr || null,
345
- })
346
- }
347
- />
 
 
 
 
 
 
348
  </td>
349
  <td className="px-3 py-2 align-top tabular-nums max-w-[90px]">
350
- <EditableCell
351
- type="number"
352
- value={
353
- deal.close_probability != null
354
- ? String(deal.close_probability)
355
- : ''
356
- }
357
- onCommit={(v) => {
358
- if (v.trim() === '') {
359
- patchDeal(deal.id, { close_probability: 0 });
360
- return;
361
  }
362
- const n = Math.min(100, Math.max(0, Math.round(Number(v))));
363
- if (!Number.isFinite(n)) return;
364
- patchDeal(deal.id, { close_probability: n });
365
- }}
366
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  </td>
368
  <td
369
  className="px-3 py-2 tabular-nums text-slate-700"
@@ -372,20 +454,32 @@ export default function Deals() {
372
  {fmtMoney(deal.forecast_value)}
373
  </td>
374
  <td className="px-3 py-2 align-top">
375
- <EditableDateCell
376
- value={deal.last_interaction_at}
377
- onCommit={(dateStr) =>
378
- patchDeal(deal.id, {
379
- last_interaction_at: dateStr || null,
380
- })
381
- }
382
- />
 
 
 
 
 
 
383
  </td>
384
  <td className="px-3 py-2 align-top max-w-[120px]">
385
- <EditableCell
386
- value={deal.country || ''}
387
- onCommit={(v) => patchDeal(deal.id, { country: v })}
388
- />
 
 
 
 
 
 
389
  </td>
390
  </tr>
391
  );
 
61
  const [seedBusy, setSeedBusy] = useState(false);
62
  const [panelOpen, setPanelOpen] = useState(false);
63
  const [dealDetail, setDealDetail] = useState(null);
64
+ const [tableEditRowId, setTableEditRowId] = useState(null);
65
 
66
  const fetchDeals = useCallback(async () => {
67
  setLoading(true);
 
127
  const updateStage = (dealId, stage) => patchDeal(dealId, { stage });
128
 
129
  const openDeal = async (deal) => {
130
+ setTableEditRowId(null);
131
  setPanelOpen(true);
132
  setDealDetail(deal);
133
  try {
 
142
  };
143
 
144
  const closePanel = () => {
145
+ setTableEditRowId(null);
146
  setPanelOpen(false);
147
  setDealDetail(null);
148
  };
 
223
  tabIndex={0}
224
  onClick={(e) => {
225
  if (isRowUiTarget(e)) return;
226
+ setTableEditRowId(null);
227
  openDeal(deal);
228
  }}
229
  onKeyDown={(e) => {
230
  if (isRowUiTarget(e)) return;
231
  if (e.key === 'Enter' || e.key === ' ') {
232
  e.preventDefault();
233
+ setTableEditRowId(null);
234
  openDeal(deal);
235
  }
236
  }}
 
246
  type="button"
247
  variant="ghost"
248
  size="icon"
249
+ className={cn(
250
+ 'h-7 w-7 text-slate-500 hover:text-violet-700',
251
+ tableEditRowId === deal.id &&
252
+ 'bg-violet-100 text-violet-800 hover:bg-violet-100'
253
+ )}
254
  onClick={(e) => {
255
  e.stopPropagation();
256
+ const tr = e.currentTarget.closest('tr');
257
+ if (tableEditRowId === deal.id) {
258
+ setTableEditRowId(null);
259
+ return;
260
+ }
261
+ setTableEditRowId(deal.id);
262
+ requestAnimationFrame(() =>
263
+ focusFirstEditableInRow(tr)
264
  );
265
  }}
266
+ aria-label={
267
+ tableEditRowId === deal.id
268
+ ? `Stop editing row for ${deal.name || 'deal'}`
269
+ : `Edit fields in row for ${deal.name || 'deal'}`
270
+ }
271
  >
272
  <Pencil className="h-3.5 w-3.5" aria-hidden />
273
  </Button>
274
  </div>
275
  </td>
276
  <td className="px-3 py-2 align-top max-w-[220px] font-medium">
277
+ {tableEditRowId === deal.id ? (
278
+ <EditableCell
279
+ value={deal.name || ''}
280
+ onCommit={(v) => patchDeal(deal.id, { name: v })}
281
+ inputClassName="font-medium text-slate-900"
282
+ />
283
+ ) : (
284
+ <div className="min-h-[2rem] py-1 text-sm font-medium text-slate-900 truncate">
285
+ {deal.name || '—'}
286
+ </div>
287
+ )}
288
  </td>
289
  <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
290
  <Select
 
322
  </Select>
323
  </td>
324
  <td className="px-3 py-2 align-top">
325
+ {tableEditRowId === deal.id ? (
326
+ <EditableCell
327
+ value={deal.owner_initials || ''}
328
+ onCommit={(v) =>
329
+ patchDeal(deal.id, {
330
+ owner_initials: (v || '')
331
+ .slice(0, 8)
332
+ .toUpperCase(),
333
+ })
334
+ }
335
+ inputClassName="text-center uppercase tracking-wide max-w-[4.5rem] font-semibold"
336
+ />
337
+ ) : (
338
+ <div className="h-8 w-8 rounded-full bg-violet-100 text-violet-700 text-xs font-semibold flex items-center justify-center border border-violet-200">
339
+ {deal.owner_initials || '?'}
340
+ </div>
341
+ )}
342
  </td>
343
  <td className="px-3 py-2 align-top tabular-nums max-w-[120px]">
344
+ {tableEditRowId === deal.id ? (
345
+ <EditableCell
346
+ type="number"
347
+ value={
348
+ deal.deal_value != null ? String(deal.deal_value) : ''
 
 
349
  }
350
+ onCommit={(v) => {
351
+ if (v.trim() === '') {
352
+ patchDeal(deal.id, { deal_value: null });
353
+ return;
354
+ }
355
+ const n = Math.round(Number(v));
356
+ if (!Number.isFinite(n)) return;
357
+ patchDeal(deal.id, { deal_value: n });
358
+ }}
359
+ />
360
+ ) : (
361
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700 tabular-nums">
362
+ {fmtMoney(deal.deal_value)}
363
+ </div>
364
+ )}
365
  </td>
366
  <td className="px-3 py-2 align-top max-w-[180px]">
367
+ {tableEditRowId === deal.id ? (
368
+ <EditableCell
369
+ value={deal.contact_display || ''}
370
+ onCommit={(v) =>
371
+ patchDeal(deal.id, { contact_display: v })
372
+ }
373
+ inputClassName="text-xs"
374
+ />
375
+ ) : (
376
+ <div className="min-h-[2rem] py-1">
377
+ {deal.contact_display ? (
378
+ <span className="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-800">
379
+ {deal.contact_display}
380
+ </span>
381
+ ) : (
382
+ <span className="text-slate-500">—</span>
383
+ )}
384
+ </div>
385
+ )}
386
  </td>
387
  <td className="px-3 py-2 align-top max-w-[200px]">
388
+ {tableEditRowId === deal.id ? (
389
+ <EditableCell
390
+ value={deal.account_name || ''}
391
+ onCommit={(v) => patchDeal(deal.id, { account_name: v })}
392
+ inputClassName="text-xs"
393
+ />
394
+ ) : (
395
+ <div className="min-h-[2rem] py-1">
396
+ {deal.account_name ? (
397
+ <span className="inline-flex rounded-md bg-violet-50 px-2 py-0.5 text-xs font-medium text-violet-800">
398
+ {deal.account_name}
399
+ </span>
400
+ ) : (
401
+ <span className="text-slate-500">—</span>
402
+ )}
403
+ </div>
404
+ )}
405
  </td>
406
  <td className="px-3 py-2 align-top">
407
+ {tableEditRowId === deal.id ? (
408
+ <EditableDateCell
409
+ value={deal.expected_close_date}
410
+ onCommit={(dateStr) =>
411
+ patchDeal(deal.id, {
412
+ expected_close_date: dateStr || null,
413
+ })
414
+ }
415
+ />
416
+ ) : (
417
+ <div className="min-h-[2rem] py-1 text-sm text-slate-600">
418
+ {fmtDate(deal.expected_close_date)}
419
+ </div>
420
+ )}
421
  </td>
422
  <td className="px-3 py-2 align-top tabular-nums max-w-[90px]">
423
+ {tableEditRowId === deal.id ? (
424
+ <EditableCell
425
+ type="number"
426
+ value={
427
+ deal.close_probability != null
428
+ ? String(deal.close_probability)
429
+ : ''
 
 
 
 
430
  }
431
+ onCommit={(v) => {
432
+ if (v.trim() === '') {
433
+ patchDeal(deal.id, { close_probability: 0 });
434
+ return;
435
+ }
436
+ const n = Math.min(
437
+ 100,
438
+ Math.max(0, Math.round(Number(v)))
439
+ );
440
+ if (!Number.isFinite(n)) return;
441
+ patchDeal(deal.id, { close_probability: n });
442
+ }}
443
+ />
444
+ ) : (
445
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700 tabular-nums">
446
+ {deal.close_probability ?? '—'}%
447
+ </div>
448
+ )}
449
  </td>
450
  <td
451
  className="px-3 py-2 tabular-nums text-slate-700"
 
454
  {fmtMoney(deal.forecast_value)}
455
  </td>
456
  <td className="px-3 py-2 align-top">
457
+ {tableEditRowId === deal.id ? (
458
+ <EditableDateCell
459
+ value={deal.last_interaction_at}
460
+ onCommit={(dateStr) =>
461
+ patchDeal(deal.id, {
462
+ last_interaction_at: dateStr || null,
463
+ })
464
+ }
465
+ />
466
+ ) : (
467
+ <div className="min-h-[2rem] py-1 text-sm text-slate-600">
468
+ {fmtDate(deal.last_interaction_at)}
469
+ </div>
470
+ )}
471
  </td>
472
  <td className="px-3 py-2 align-top max-w-[120px]">
473
+ {tableEditRowId === deal.id ? (
474
+ <EditableCell
475
+ value={deal.country || ''}
476
+ onCommit={(v) => patchDeal(deal.id, { country: v })}
477
+ />
478
+ ) : (
479
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700">
480
+ {deal.country || '—'}
481
+ </div>
482
+ )}
483
  </td>
484
  </tr>
485
  );
frontend/src/pages/Leads.jsx CHANGED
@@ -66,6 +66,7 @@ export default function Leads() {
66
  const [seedBusy, setSeedBusy] = useState(false);
67
  const [rowSelection, setRowSelection] = useState({});
68
  const [bulkBusy, setBulkBusy] = useState(null);
 
69
 
70
  const selectedIds = useMemo(
71
  () => Object.keys(rowSelection).filter((id) => rowSelection[id]).map(Number),
@@ -165,6 +166,7 @@ export default function Leads() {
165
  };
166
 
167
  const openDetail = async (lead) => {
 
168
  setSelected(lead);
169
  setThreadData(null);
170
  try {
@@ -179,6 +181,7 @@ export default function Leads() {
179
  };
180
 
181
  const closePanel = () => {
 
182
  setSelected(null);
183
  setThreadData(null);
184
  };
@@ -443,12 +446,14 @@ export default function Leads() {
443
  tabIndex={0}
444
  onClick={(e) => {
445
  if (isRowUiTarget(e)) return;
 
446
  openDetail(lead);
447
  }}
448
  onKeyDown={(e) => {
449
  if (isRowUiTarget(e)) return;
450
  if (e.key === 'Enter' || e.key === ' ') {
451
  e.preventDefault();
 
452
  openDetail(lead);
453
  }
454
  }}
@@ -474,29 +479,51 @@ export default function Leads() {
474
  type="button"
475
  variant="ghost"
476
  size="icon"
477
- className="h-8 w-8 text-slate-500 hover:text-violet-700"
 
 
 
 
478
  onClick={(e) => {
479
  e.stopPropagation();
480
- focusFirstEditableInRow(e.currentTarget.closest('tr'));
 
 
 
 
 
 
 
 
481
  }}
482
- aria-label={`Edit fields in row for ${displayName}`}
 
 
 
 
483
  >
484
  <Pencil className="h-4 w-4" aria-hidden />
485
  </Button>
486
  </td>
487
  <td className="px-3 py-2 align-top max-w-[200px]">
488
- <div className="flex flex-col gap-1">
489
- <EditableCell
490
- value={lead.first_name || ''}
491
- onCommit={(v) => patchLead(lead.id, { first_name: v })}
492
- inputClassName="font-medium text-violet-800"
493
- />
494
- <EditableCell
495
- value={lead.last_name || ''}
496
- onCommit={(v) => patchLead(lead.id, { last_name: v })}
497
- inputClassName="font-medium text-violet-800"
498
- />
499
- </div>
 
 
 
 
 
 
500
  {lead.contact_id ? (
501
  <span className="mt-1 text-xs text-emerald-600">
502
  (in Contacts)
@@ -545,48 +572,91 @@ export default function Leads() {
545
  <td className="px-3 py-2 align-top max-w-[220px]">
546
  <span className="inline-flex items-start gap-1.5 w-full">
547
  <Building2 className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
548
- <EditableCell
549
- value={lead.company_name || ''}
550
- onCommit={(v) => patchLead(lead.id, { company_name: v })}
551
- />
 
 
 
 
 
 
552
  </span>
553
  </td>
554
  <td className="px-3 py-2 align-top max-w-[200px]">
555
  <span className="inline-flex items-start gap-1.5 w-full">
556
  <Briefcase className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
557
- <EditableCell
558
- value={lead.title || ''}
559
- onCommit={(v) => patchLead(lead.id, { title: v })}
560
- />
 
 
 
 
 
 
561
  </span>
562
  </td>
563
  <td className="px-3 py-2 align-top max-w-[260px]">
564
- <div className="flex items-start gap-1.5">
565
- <EditableCell
566
- type="email"
567
- value={lead.email || ''}
568
- onCommit={(v) => patchLead(lead.id, { email: v })}
569
- className="min-w-0 flex-1"
570
- />
571
- {lead.email ? (
572
- <a
573
- href={`mailto:${lead.email}`}
574
- className="mt-1 shrink-0 text-violet-600 hover:text-violet-800"
575
- title="Send email"
576
- onClick={(e) => e.stopPropagation()}
577
- >
578
- <Mail className="h-3.5 w-3.5" />
579
- </a>
580
- ) : null}
581
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
582
  </td>
583
  <td className="px-3 py-2 align-top text-slate-600 max-w-[280px]">
584
- <EditableCell
585
- multiline
586
- value={lead.last_reply_body || ''}
587
- onCommit={(v) => patchLead(lead.id, { last_reply_body: v })}
588
- inputClassName="text-xs"
589
- />
 
 
 
 
 
 
 
 
 
 
 
 
590
  </td>
591
  </tr>
592
  );
 
66
  const [seedBusy, setSeedBusy] = useState(false);
67
  const [rowSelection, setRowSelection] = useState({});
68
  const [bulkBusy, setBulkBusy] = useState(null);
69
+ const [tableEditRowId, setTableEditRowId] = useState(null);
70
 
71
  const selectedIds = useMemo(
72
  () => Object.keys(rowSelection).filter((id) => rowSelection[id]).map(Number),
 
166
  };
167
 
168
  const openDetail = async (lead) => {
169
+ setTableEditRowId(null);
170
  setSelected(lead);
171
  setThreadData(null);
172
  try {
 
181
  };
182
 
183
  const closePanel = () => {
184
+ setTableEditRowId(null);
185
  setSelected(null);
186
  setThreadData(null);
187
  };
 
446
  tabIndex={0}
447
  onClick={(e) => {
448
  if (isRowUiTarget(e)) return;
449
+ setTableEditRowId(null);
450
  openDetail(lead);
451
  }}
452
  onKeyDown={(e) => {
453
  if (isRowUiTarget(e)) return;
454
  if (e.key === 'Enter' || e.key === ' ') {
455
  e.preventDefault();
456
+ setTableEditRowId(null);
457
  openDetail(lead);
458
  }
459
  }}
 
479
  type="button"
480
  variant="ghost"
481
  size="icon"
482
+ className={cn(
483
+ 'h-8 w-8 text-slate-500 hover:text-violet-700',
484
+ tableEditRowId === lead.id &&
485
+ 'bg-violet-100 text-violet-800 hover:bg-violet-100'
486
+ )}
487
  onClick={(e) => {
488
  e.stopPropagation();
489
+ const tr = e.currentTarget.closest('tr');
490
+ if (tableEditRowId === lead.id) {
491
+ setTableEditRowId(null);
492
+ return;
493
+ }
494
+ setTableEditRowId(lead.id);
495
+ requestAnimationFrame(() =>
496
+ focusFirstEditableInRow(tr)
497
+ );
498
  }}
499
+ aria-label={
500
+ tableEditRowId === lead.id
501
+ ? `Stop editing row for ${displayName}`
502
+ : `Edit fields in row for ${displayName}`
503
+ }
504
  >
505
  <Pencil className="h-4 w-4" aria-hidden />
506
  </Button>
507
  </td>
508
  <td className="px-3 py-2 align-top max-w-[200px]">
509
+ {tableEditRowId === lead.id ? (
510
+ <div className="flex flex-col gap-1">
511
+ <EditableCell
512
+ value={lead.first_name || ''}
513
+ onCommit={(v) => patchLead(lead.id, { first_name: v })}
514
+ inputClassName="font-medium text-violet-800"
515
+ />
516
+ <EditableCell
517
+ value={lead.last_name || ''}
518
+ onCommit={(v) => patchLead(lead.id, { last_name: v })}
519
+ inputClassName="font-medium text-violet-800"
520
+ />
521
+ </div>
522
+ ) : (
523
+ <div className="min-h-[2.25rem] py-1 text-sm font-medium text-violet-800 leading-snug">
524
+ {displayName}
525
+ </div>
526
+ )}
527
  {lead.contact_id ? (
528
  <span className="mt-1 text-xs text-emerald-600">
529
  (in Contacts)
 
572
  <td className="px-3 py-2 align-top max-w-[220px]">
573
  <span className="inline-flex items-start gap-1.5 w-full">
574
  <Building2 className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
575
+ {tableEditRowId === lead.id ? (
576
+ <EditableCell
577
+ value={lead.company_name || ''}
578
+ onCommit={(v) => patchLead(lead.id, { company_name: v })}
579
+ />
580
+ ) : (
581
+ <span className="min-h-[2.25rem] py-1 text-sm text-slate-700">
582
+ {lead.company_name || '—'}
583
+ </span>
584
+ )}
585
  </span>
586
  </td>
587
  <td className="px-3 py-2 align-top max-w-[200px]">
588
  <span className="inline-flex items-start gap-1.5 w-full">
589
  <Briefcase className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
590
+ {tableEditRowId === lead.id ? (
591
+ <EditableCell
592
+ value={lead.title || ''}
593
+ onCommit={(v) => patchLead(lead.id, { title: v })}
594
+ />
595
+ ) : (
596
+ <span className="min-h-[2.25rem] py-1 text-sm text-slate-700">
597
+ {lead.title || '—'}
598
+ </span>
599
+ )}
600
  </span>
601
  </td>
602
  <td className="px-3 py-2 align-top max-w-[260px]">
603
+ {tableEditRowId === lead.id ? (
604
+ <div className="flex items-start gap-1.5">
605
+ <EditableCell
606
+ type="email"
607
+ value={lead.email || ''}
608
+ onCommit={(v) => patchLead(lead.id, { email: v })}
609
+ className="min-w-0 flex-1"
610
+ />
611
+ {lead.email ? (
612
+ <a
613
+ href={`mailto:${lead.email}`}
614
+ className="mt-1 shrink-0 text-violet-600 hover:text-violet-800"
615
+ title="Send email"
616
+ onClick={(e) => e.stopPropagation()}
617
+ >
618
+ <Mail className="h-3.5 w-3.5" />
619
+ </a>
620
+ ) : null}
621
+ </div>
622
+ ) : (
623
+ <div className="flex items-center gap-1.5 min-h-[2.25rem] py-1 text-sm text-slate-700 truncate">
624
+ {lead.email ? (
625
+ <>
626
+ <Mail className="h-3.5 w-3.5 shrink-0 text-slate-400" />
627
+ <a
628
+ href={`mailto:${lead.email}`}
629
+ className="text-violet-600 hover:underline truncate"
630
+ onClick={(e) => e.stopPropagation()}
631
+ >
632
+ {lead.email}
633
+ </a>
634
+ </>
635
+ ) : (
636
+ '—'
637
+ )}
638
+ </div>
639
+ )}
640
  </td>
641
  <td className="px-3 py-2 align-top text-slate-600 max-w-[280px]">
642
+ {tableEditRowId === lead.id ? (
643
+ <EditableCell
644
+ multiline
645
+ value={lead.last_reply_body || ''}
646
+ onCommit={(v) => patchLead(lead.id, { last_reply_body: v })}
647
+ inputClassName="text-xs"
648
+ />
649
+ ) : (
650
+ <div
651
+ className="min-h-[2.25rem] max-h-[5.5rem] overflow-hidden py-1 text-xs"
652
+ title={lead.last_reply_body || ''}
653
+ >
654
+ {lead.last_reply_body
655
+ ? lead.last_reply_body.slice(0, 200) +
656
+ (lead.last_reply_body.length > 200 ? '…' : '')
657
+ : '—'}
658
+ </div>
659
+ )}
660
  </td>
661
  </tr>
662
  );