Seth commited on
Commit
e27778b
·
1 Parent(s): 45ad5e2
frontend/src/components/workspace/EditableCell.jsx CHANGED
@@ -41,7 +41,6 @@ export function EditableCell({
41
  onChange={(e) => setLocal(e.target.value)}
42
  onBlur={commit}
43
  onKeyDown={stop}
44
- onClick={stop}
45
  disabled={disabled}
46
  rows={2}
47
  />
@@ -62,7 +61,6 @@ export function EditableCell({
62
  e.currentTarget.blur();
63
  }
64
  }}
65
- onClick={stop}
66
  disabled={disabled}
67
  />
68
  );
@@ -99,7 +97,6 @@ export function EditableDateCell({ value, onCommit, className = '', disabled = f
99
  onChange={(e) => setLocal(e.target.value)}
100
  onBlur={commit}
101
  onKeyDown={(e) => e.stopPropagation()}
102
- onClick={(e) => e.stopPropagation()}
103
  disabled={disabled}
104
  />
105
  );
 
41
  onChange={(e) => setLocal(e.target.value)}
42
  onBlur={commit}
43
  onKeyDown={stop}
 
44
  disabled={disabled}
45
  rows={2}
46
  />
 
61
  e.currentTarget.blur();
62
  }
63
  }}
 
64
  disabled={disabled}
65
  />
66
  );
 
97
  onChange={(e) => setLocal(e.target.value)}
98
  onBlur={commit}
99
  onKeyDown={(e) => e.stopPropagation()}
 
100
  disabled={disabled}
101
  />
102
  );
frontend/src/pages/Contacts.jsx CHANGED
@@ -16,6 +16,7 @@ import {
16
  Sparkles,
17
  Trash2,
18
  Handshake,
 
19
  } from 'lucide-react';
20
  import { Input } from '@/components/ui/input';
21
  import { Badge } from '@/components/ui/badge';
@@ -461,6 +462,13 @@ export default function Contacts() {
461
  setPage(1);
462
  };
463
 
 
 
 
 
 
 
 
464
  const SortHeader = ({ field, label, className = '' }) => {
465
  const active = sortBy === field;
466
  return (
@@ -604,6 +612,7 @@ export default function Contacts() {
604
  aria-label="Select all on page"
605
  />
606
  </th>
 
607
  <SortHeader field="first_name" label="Name" />
608
  <SortHeader field="email" label="Email" />
609
  <SortHeader field="company" label="Company" />
@@ -617,6 +626,7 @@ export default function Contacts() {
617
  onClick={(e) => e.stopPropagation()}
618
  >
619
  <td className="px-3 py-2 w-10 align-top" aria-hidden />
 
620
  <td className="px-3 py-2 align-top">
621
  <div className="flex flex-col gap-1.5 min-w-[160px]">
622
  <Input
@@ -742,8 +752,12 @@ export default function Contacts() {
742
  key={contact.id}
743
  role="button"
744
  tabIndex={0}
745
- onClick={() => openContact(contact)}
 
 
 
746
  onKeyDown={(e) => {
 
747
  if (e.key === 'Enter' || e.key === ' ') {
748
  e.preventDefault();
749
  openContact(contact);
@@ -764,9 +778,21 @@ export default function Contacts() {
764
  />
765
  </td>
766
  <td
767
- className="px-3 py-2 align-top"
768
  onClick={(e) => e.stopPropagation()}
769
  >
 
 
 
 
 
 
 
 
 
 
 
 
770
  <div className="flex flex-col gap-1 min-w-[140px] max-w-[200px]">
771
  <EditableCell
772
  value={contact.first_name || ''}
@@ -784,29 +810,20 @@ export default function Contacts() {
784
  />
785
  </div>
786
  </td>
787
- <td
788
- className="px-3 py-2 align-top max-w-[240px]"
789
- onClick={(e) => e.stopPropagation()}
790
- >
791
  <EditableCell
792
  type="email"
793
  value={contact.email || ''}
794
  onCommit={(v) => patchContact(contact.id, { email: v })}
795
  />
796
  </td>
797
- <td
798
- className="px-3 py-2 align-top max-w-[220px]"
799
- onClick={(e) => e.stopPropagation()}
800
- >
801
  <EditableCell
802
  value={contact.company || ''}
803
  onCommit={(v) => patchContact(contact.id, { company: v })}
804
  />
805
  </td>
806
- <td
807
- className="px-3 py-2 align-top max-w-[200px]"
808
- onClick={(e) => e.stopPropagation()}
809
- >
810
  <EditableCell
811
  value={contact.title || ''}
812
  onCommit={(v) => patchContact(contact.id, { title: v })}
 
16
  Sparkles,
17
  Trash2,
18
  Handshake,
19
+ PanelRight,
20
  } from 'lucide-react';
21
  import { Input } from '@/components/ui/input';
22
  import { Badge } from '@/components/ui/badge';
 
462
  setPage(1);
463
  };
464
 
465
+ const isRowUiTarget = (e) =>
466
+ Boolean(
467
+ e.target.closest(
468
+ 'input, textarea, button, a, [role="combobox"], [role="listbox"], [data-radix-collection-item]'
469
+ )
470
+ );
471
+
472
  const SortHeader = ({ field, label, className = '' }) => {
473
  const active = sortBy === field;
474
  return (
 
612
  aria-label="Select all on page"
613
  />
614
  </th>
615
+ <th className="w-9 px-0 py-2" aria-label="Details" />
616
  <SortHeader field="first_name" label="Name" />
617
  <SortHeader field="email" label="Email" />
618
  <SortHeader field="company" label="Company" />
 
626
  onClick={(e) => e.stopPropagation()}
627
  >
628
  <td className="px-3 py-2 w-10 align-top" aria-hidden />
629
+ <td className="w-9 align-top" aria-hidden />
630
  <td className="px-3 py-2 align-top">
631
  <div className="flex flex-col gap-1.5 min-w-[160px]">
632
  <Input
 
752
  key={contact.id}
753
  role="button"
754
  tabIndex={0}
755
+ onClick={(e) => {
756
+ if (isRowUiTarget(e)) return;
757
+ openContact(contact);
758
+ }}
759
  onKeyDown={(e) => {
760
+ if (isRowUiTarget(e)) return;
761
  if (e.key === 'Enter' || e.key === ' ') {
762
  e.preventDefault();
763
  openContact(contact);
 
778
  />
779
  </td>
780
  <td
781
+ className="px-0 py-1.5 align-middle w-9"
782
  onClick={(e) => e.stopPropagation()}
783
  >
784
+ <Button
785
+ type="button"
786
+ variant="ghost"
787
+ size="icon"
788
+ className="h-8 w-8 text-slate-500 hover:text-violet-700"
789
+ onClick={() => openContact(contact)}
790
+ aria-label={`Open details for ${displayName}`}
791
+ >
792
+ <PanelRight className="h-4 w-4" aria-hidden />
793
+ </Button>
794
+ </td>
795
+ <td className="px-3 py-2 align-top">
796
  <div className="flex flex-col gap-1 min-w-[140px] max-w-[200px]">
797
  <EditableCell
798
  value={contact.first_name || ''}
 
810
  />
811
  </div>
812
  </td>
813
+ <td className="px-3 py-2 align-top max-w-[240px]">
 
 
 
814
  <EditableCell
815
  type="email"
816
  value={contact.email || ''}
817
  onCommit={(v) => patchContact(contact.id, { email: v })}
818
  />
819
  </td>
820
+ <td className="px-3 py-2 align-top max-w-[220px]">
 
 
 
821
  <EditableCell
822
  value={contact.company || ''}
823
  onCommit={(v) => patchContact(contact.id, { company: v })}
824
  />
825
  </td>
826
+ <td className="px-3 py-2 align-top max-w-[200px]">
 
 
 
827
  <EditableCell
828
  value={contact.title || ''}
829
  onCommit={(v) => patchContact(contact.id, { title: v })}
frontend/src/pages/Deals.jsx CHANGED
@@ -1,6 +1,6 @@
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
- import { Search, Loader2, LayoutGrid } 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';
@@ -38,6 +38,14 @@ function fmtDate(iso) {
38
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
39
  }
40
 
 
 
 
 
 
 
 
 
41
  export default function Deals() {
42
  const [deals, setDeals] = useState([]);
43
  const [total, setTotal] = useState(0);
@@ -179,7 +187,7 @@ export default function Deals() {
179
  <table className="w-full text-sm min-w-[960px]">
180
  <thead>
181
  <tr className="bg-slate-50 text-left text-slate-600 border-b border-slate-200">
182
- <th className="px-2 py-2 w-10" />
183
  <th className="px-3 py-2 font-medium">Deal</th>
184
  <th className="px-3 py-2 font-medium">Stage</th>
185
  <th className="px-3 py-2 font-medium">Owner</th>
@@ -204,8 +212,12 @@ export default function Deals() {
204
  key={deal.id}
205
  role="button"
206
  tabIndex={0}
207
- onClick={() => openDeal(deal)}
 
 
 
208
  onKeyDown={(e) => {
 
209
  if (e.key === 'Enter' || e.key === ' ') {
210
  e.preventDefault();
211
  openDeal(deal);
@@ -213,13 +225,25 @@ export default function Deals() {
213
  }}
214
  className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
215
  >
216
- <td className="px-2 py-2" onClick={(e) => e.stopPropagation()}>
217
- <input type="checkbox" className="rounded border-slate-300" />
218
- </td>
219
  <td
220
- className="px-3 py-2 align-top max-w-[220px] font-medium"
221
  onClick={(e) => e.stopPropagation()}
222
  >
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  <EditableCell
224
  value={deal.name || ''}
225
  onCommit={(v) => patchDeal(deal.id, { name: v })}
@@ -261,7 +285,7 @@ export default function Deals() {
261
  </SelectContent>
262
  </Select>
263
  </td>
264
- <td className="px-3 py-2 align-top" onClick={(e) => e.stopPropagation()}>
265
  <EditableCell
266
  value={deal.owner_initials || ''}
267
  onCommit={(v) =>
@@ -272,10 +296,7 @@ export default function Deals() {
272
  inputClassName="text-center uppercase tracking-wide max-w-[4.5rem] font-semibold"
273
  />
274
  </td>
275
- <td
276
- className="px-3 py-2 align-top tabular-nums max-w-[120px]"
277
- onClick={(e) => e.stopPropagation()}
278
- >
279
  <EditableCell
280
  type="number"
281
  value={deal.deal_value != null ? String(deal.deal_value) : ''}
@@ -290,27 +311,21 @@ export default function Deals() {
290
  }}
291
  />
292
  </td>
293
- <td
294
- className="px-3 py-2 align-top max-w-[180px]"
295
- onClick={(e) => e.stopPropagation()}
296
- >
297
  <EditableCell
298
  value={deal.contact_display || ''}
299
  onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
300
  inputClassName="text-xs"
301
  />
302
  </td>
303
- <td
304
- className="px-3 py-2 align-top max-w-[200px]"
305
- onClick={(e) => e.stopPropagation()}
306
- >
307
  <EditableCell
308
  value={deal.account_name || ''}
309
  onCommit={(v) => patchDeal(deal.id, { account_name: v })}
310
  inputClassName="text-xs"
311
  />
312
  </td>
313
- <td className="px-3 py-2 align-top" onClick={(e) => e.stopPropagation()}>
314
  <EditableDateCell
315
  value={deal.expected_close_date}
316
  onCommit={(dateStr) =>
@@ -320,10 +335,7 @@ export default function Deals() {
320
  }
321
  />
322
  </td>
323
- <td
324
- className="px-3 py-2 align-top tabular-nums max-w-[90px]"
325
- onClick={(e) => e.stopPropagation()}
326
- >
327
  <EditableCell
328
  type="number"
329
  value={
@@ -348,7 +360,7 @@ export default function Deals() {
348
  >
349
  {fmtMoney(deal.forecast_value)}
350
  </td>
351
- <td className="px-3 py-2 align-top" onClick={(e) => e.stopPropagation()}>
352
  <EditableDateCell
353
  value={deal.last_interaction_at}
354
  onCommit={(dateStr) =>
@@ -358,10 +370,7 @@ export default function Deals() {
358
  }
359
  />
360
  </td>
361
- <td
362
- className="px-3 py-2 align-top max-w-[120px]"
363
- onClick={(e) => e.stopPropagation()}
364
- >
365
  <EditableCell
366
  value={deal.country || ''}
367
  onCommit={(v) => patchDeal(deal.id, { country: v })}
 
1
  import React, { useCallback, useEffect, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
+ import { Search, Loader2, LayoutGrid, PanelRight } 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';
 
38
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
39
  }
40
 
41
+ function isRowUiTarget(e) {
42
+ return Boolean(
43
+ e.target.closest(
44
+ 'input, textarea, button, a, [role="combobox"], [role="listbox"], [data-radix-collection-item]'
45
+ )
46
+ );
47
+ }
48
+
49
  export default function Deals() {
50
  const [deals, setDeals] = useState([]);
51
  const [total, setTotal] = useState(0);
 
187
  <table className="w-full text-sm min-w-[960px]">
188
  <thead>
189
  <tr className="bg-slate-50 text-left text-slate-600 border-b border-slate-200">
190
+ <th className="px-2 py-2 w-[4.25rem]" aria-label="Select and details" />
191
  <th className="px-3 py-2 font-medium">Deal</th>
192
  <th className="px-3 py-2 font-medium">Stage</th>
193
  <th className="px-3 py-2 font-medium">Owner</th>
 
212
  key={deal.id}
213
  role="button"
214
  tabIndex={0}
215
+ onClick={(e) => {
216
+ if (isRowUiTarget(e)) return;
217
+ openDeal(deal);
218
+ }}
219
  onKeyDown={(e) => {
220
+ if (isRowUiTarget(e)) return;
221
  if (e.key === 'Enter' || e.key === ' ') {
222
  e.preventDefault();
223
  openDeal(deal);
 
225
  }}
226
  className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
227
  >
 
 
 
228
  <td
229
+ className="px-1 py-1.5 w-[4.25rem]"
230
  onClick={(e) => e.stopPropagation()}
231
  >
232
+ <div className="flex flex-col items-center gap-0.5">
233
+ <input type="checkbox" className="rounded border-slate-300" />
234
+ <Button
235
+ type="button"
236
+ variant="ghost"
237
+ size="icon"
238
+ className="h-7 w-7 text-slate-500 hover:text-violet-700"
239
+ onClick={() => openDeal(deal)}
240
+ aria-label={`Open details for ${deal.name || 'deal'}`}
241
+ >
242
+ <PanelRight className="h-3.5 w-3.5" aria-hidden />
243
+ </Button>
244
+ </div>
245
+ </td>
246
+ <td className="px-3 py-2 align-top max-w-[220px] font-medium">
247
  <EditableCell
248
  value={deal.name || ''}
249
  onCommit={(v) => patchDeal(deal.id, { name: v })}
 
285
  </SelectContent>
286
  </Select>
287
  </td>
288
+ <td className="px-3 py-2 align-top">
289
  <EditableCell
290
  value={deal.owner_initials || ''}
291
  onCommit={(v) =>
 
296
  inputClassName="text-center uppercase tracking-wide max-w-[4.5rem] font-semibold"
297
  />
298
  </td>
299
+ <td className="px-3 py-2 align-top tabular-nums max-w-[120px]">
 
 
 
300
  <EditableCell
301
  type="number"
302
  value={deal.deal_value != null ? String(deal.deal_value) : ''}
 
311
  }}
312
  />
313
  </td>
314
+ <td className="px-3 py-2 align-top max-w-[180px]">
 
 
 
315
  <EditableCell
316
  value={deal.contact_display || ''}
317
  onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
318
  inputClassName="text-xs"
319
  />
320
  </td>
321
+ <td className="px-3 py-2 align-top max-w-[200px]">
 
 
 
322
  <EditableCell
323
  value={deal.account_name || ''}
324
  onCommit={(v) => patchDeal(deal.id, { account_name: v })}
325
  inputClassName="text-xs"
326
  />
327
  </td>
328
+ <td className="px-3 py-2 align-top">
329
  <EditableDateCell
330
  value={deal.expected_close_date}
331
  onCommit={(dateStr) =>
 
335
  }
336
  />
337
  </td>
338
+ <td className="px-3 py-2 align-top tabular-nums max-w-[90px]">
 
 
 
339
  <EditableCell
340
  type="number"
341
  value={
 
360
  >
361
  {fmtMoney(deal.forecast_value)}
362
  </td>
363
+ <td className="px-3 py-2 align-top">
364
  <EditableDateCell
365
  value={deal.last_interaction_at}
366
  onCommit={(dateStr) =>
 
370
  }
371
  />
372
  </td>
373
+ <td className="px-3 py-2 align-top max-w-[120px]">
 
 
 
374
  <EditableCell
375
  value={deal.country || ''}
376
  onCommit={(v) => patchDeal(deal.id, { country: v })}
frontend/src/pages/Leads.jsx CHANGED
@@ -10,6 +10,7 @@ import {
10
  UserPlus,
11
  Trash2,
12
  Handshake,
 
13
  } from 'lucide-react';
14
  import { Button } from '@/components/ui/button';
15
  import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select';
@@ -37,6 +38,14 @@ function statusValueForSelect(leadStatus) {
37
  return CRM_STATUSES.some((s) => s.value === v) ? v : 'new_lead';
38
  }
39
 
 
 
 
 
 
 
 
 
40
  export default function Leads() {
41
  const navigate = useNavigate();
42
  const [leads, setLeads] = useState([]);
@@ -407,6 +416,7 @@ export default function Leads() {
407
  aria-label="Select all on page"
408
  />
409
  </th>
 
410
  <th className="px-3 py-2 font-medium">Lead</th>
411
  <th className="px-3 py-2 font-medium">Status</th>
412
  <th className="px-3 py-2 font-medium">Company</th>
@@ -425,8 +435,12 @@ export default function Leads() {
425
  key={lead.id}
426
  role="button"
427
  tabIndex={0}
428
- onClick={() => openDetail(lead)}
 
 
 
429
  onKeyDown={(e) => {
 
430
  if (e.key === 'Enter' || e.key === ' ') {
431
  e.preventDefault();
432
  openDetail(lead);
@@ -447,9 +461,21 @@ export default function Leads() {
447
  />
448
  </td>
449
  <td
450
- className="px-3 py-2 align-top max-w-[200px]"
451
  onClick={(e) => e.stopPropagation()}
452
  >
 
 
 
 
 
 
 
 
 
 
 
 
453
  <div className="flex flex-col gap-1">
454
  <EditableCell
455
  value={lead.first_name || ''}
@@ -507,10 +533,7 @@ export default function Leads() {
507
  </SelectContent>
508
  </Select>
509
  </td>
510
- <td
511
- className="px-3 py-2 align-top max-w-[220px]"
512
- onClick={(e) => e.stopPropagation()}
513
- >
514
  <span className="inline-flex items-start gap-1.5 w-full">
515
  <Building2 className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
516
  <EditableCell
@@ -519,10 +542,7 @@ export default function Leads() {
519
  />
520
  </span>
521
  </td>
522
- <td
523
- className="px-3 py-2 align-top max-w-[200px]"
524
- onClick={(e) => e.stopPropagation()}
525
- >
526
  <span className="inline-flex items-start gap-1.5 w-full">
527
  <Briefcase className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
528
  <EditableCell
@@ -531,7 +551,7 @@ export default function Leads() {
531
  />
532
  </span>
533
  </td>
534
- <td className="px-3 py-2 align-top max-w-[260px]" onClick={(e) => e.stopPropagation()}>
535
  <div className="flex items-start gap-1.5">
536
  <EditableCell
537
  type="email"
@@ -551,10 +571,7 @@ export default function Leads() {
551
  ) : null}
552
  </div>
553
  </td>
554
- <td
555
- className="px-3 py-2 align-top text-slate-600 max-w-[280px]"
556
- onClick={(e) => e.stopPropagation()}
557
- >
558
  <EditableCell
559
  multiline
560
  value={lead.last_reply_body || ''}
 
10
  UserPlus,
11
  Trash2,
12
  Handshake,
13
+ PanelRight,
14
  } from 'lucide-react';
15
  import { Button } from '@/components/ui/button';
16
  import { Select, SelectTrigger, SelectContent, SelectItem } from '@/components/ui/select';
 
38
  return CRM_STATUSES.some((s) => s.value === v) ? v : 'new_lead';
39
  }
40
 
41
+ function isRowUiTarget(e) {
42
+ return Boolean(
43
+ e.target.closest(
44
+ 'input, textarea, button, a, [role="combobox"], [role="listbox"], [data-radix-collection-item]'
45
+ )
46
+ );
47
+ }
48
+
49
  export default function Leads() {
50
  const navigate = useNavigate();
51
  const [leads, setLeads] = useState([]);
 
416
  aria-label="Select all on page"
417
  />
418
  </th>
419
+ <th className="w-9 px-0 py-2" aria-label="Details" />
420
  <th className="px-3 py-2 font-medium">Lead</th>
421
  <th className="px-3 py-2 font-medium">Status</th>
422
  <th className="px-3 py-2 font-medium">Company</th>
 
435
  key={lead.id}
436
  role="button"
437
  tabIndex={0}
438
+ onClick={(e) => {
439
+ if (isRowUiTarget(e)) return;
440
+ openDetail(lead);
441
+ }}
442
  onKeyDown={(e) => {
443
+ if (isRowUiTarget(e)) return;
444
  if (e.key === 'Enter' || e.key === ' ') {
445
  e.preventDefault();
446
  openDetail(lead);
 
461
  />
462
  </td>
463
  <td
464
+ className="px-0 py-1.5 align-middle w-9"
465
  onClick={(e) => e.stopPropagation()}
466
  >
467
+ <Button
468
+ type="button"
469
+ variant="ghost"
470
+ size="icon"
471
+ className="h-8 w-8 text-slate-500 hover:text-violet-700"
472
+ onClick={() => openDetail(lead)}
473
+ aria-label={`Open details for ${displayName}`}
474
+ >
475
+ <PanelRight className="h-4 w-4" aria-hidden />
476
+ </Button>
477
+ </td>
478
+ <td className="px-3 py-2 align-top max-w-[200px]">
479
  <div className="flex flex-col gap-1">
480
  <EditableCell
481
  value={lead.first_name || ''}
 
533
  </SelectContent>
534
  </Select>
535
  </td>
536
+ <td className="px-3 py-2 align-top max-w-[220px]">
 
 
 
537
  <span className="inline-flex items-start gap-1.5 w-full">
538
  <Building2 className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
539
  <EditableCell
 
542
  />
543
  </span>
544
  </td>
545
+ <td className="px-3 py-2 align-top max-w-[200px]">
 
 
 
546
  <span className="inline-flex items-start gap-1.5 w-full">
547
  <Briefcase className="h-3.5 w-3.5 text-slate-400 shrink-0 mt-1" />
548
  <EditableCell
 
551
  />
552
  </span>
553
  </td>
554
+ <td className="px-3 py-2 align-top max-w-[260px]">
555
  <div className="flex items-start gap-1.5">
556
  <EditableCell
557
  type="email"
 
571
  ) : null}
572
  </div>
573
  </td>
574
+ <td className="px-3 py-2 align-top text-slate-600 max-w-[280px]">
 
 
 
575
  <EditableCell
576
  multiline
577
  value={lead.last_reply_body || ''}