Seth commited on
Commit
16abd4a
·
1 Parent(s): c65992e
Files changed (1) hide show
  1. frontend/src/pages/Deals.jsx +353 -270
frontend/src/pages/Deals.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
  import { Search, Loader2, LayoutGrid, Pencil } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
@@ -18,6 +18,15 @@ const STAGES = [
18
  { value: 'lost', label: 'Lost', className: 'bg-red-500 text-white' },
19
  ];
20
 
 
 
 
 
 
 
 
 
 
21
  function stageMeta(v) {
22
  return STAGES.find((s) => s.value === v) || STAGES[0];
23
  }
@@ -52,6 +61,250 @@ function focusFirstEditableInRow(tr) {
52
  el?.focus?.();
53
  }
54
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
55
  export default function Deals() {
56
  const [deals, setDeals] = useState([]);
57
  const [total, setTotal] = useState(0);
@@ -62,6 +315,7 @@ export default function Deals() {
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);
@@ -90,6 +344,20 @@ export default function Deals() {
90
  return () => clearTimeout(t);
91
  }, [fetchDeals]);
92
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  const seedDemo = async () => {
94
  setSeedBusy(true);
95
  try {
@@ -154,8 +422,16 @@ export default function Deals() {
154
  >
155
  <MainTableWorkspace
156
  tabs={[
157
- { label: 'Main table', active: true },
158
- { label: 'By Stage', active: false, disabled: true },
 
 
 
 
 
 
 
 
159
  { label: 'Pipeline', active: false, disabled: true },
160
  ]}
161
  primaryAction={{ label: 'New deal', onClick: () => {} }}
@@ -210,281 +486,88 @@ export default function Deals() {
210
  <th className="px-3 py-2 font-medium">Country</th>
211
  </tr>
212
  </thead>
213
- <tbody>
214
- {deals.map((deal) => {
215
- const meta = stageMeta(deal.stage);
216
- const safeStage = STAGES.some((s) => s.value === deal.stage)
217
- ? deal.stage
218
- : 'new';
219
- return (
220
- <tr
221
  key={deal.id}
222
- role="button"
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
- }}
237
- className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
238
- >
239
- <td
240
- className="px-1 py-1.5 w-[4.25rem]"
241
- onClick={(e) => e.stopPropagation()}
242
- >
243
- <div className="flex flex-col items-center gap-0.5">
244
- <input type="checkbox" className="rounded border-slate-300" />
245
- <Button
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
291
- value={safeStage}
292
- onValueChange={(v) => updateStage(deal.id, v)}
293
- >
294
- <SelectTrigger
295
- className={cn(
296
- 'h-8 w-[min(100%,150px)] border-slate-200 shadow-none'
297
- )}
298
- >
299
  <span
300
  className={cn(
301
- 'rounded-full px-2 py-0.5 text-xs font-medium',
302
- meta.className
303
  )}
304
  >
305
- {meta.label}
 
 
 
 
306
  </span>
307
- </SelectTrigger>
308
- <SelectContent className="min-w-[180px]">
309
- {STAGES.map((s) => (
310
- <SelectItem key={s.value} value={s.value}>
311
- <span
312
- className={cn(
313
- 'rounded-full px-2 py-0.5 text-xs font-medium inline-block',
314
- s.className
315
- )}
316
- >
317
- {s.label}
318
- </span>
319
- </SelectItem>
320
- ))}
321
- </SelectContent>
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"
452
- title="Forecast = deal value × close %"
453
- >
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
  );
486
- })}
487
- </tbody>
488
  </table>
489
  )}
490
  </div>
 
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react';
2
  import { Link } from 'react-router-dom';
3
  import { Search, Loader2, LayoutGrid, Pencil } from 'lucide-react';
4
  import { Button } from '@/components/ui/button';
 
18
  { value: 'lost', label: 'Lost', className: 'bg-red-500 text-white' },
19
  ];
20
 
21
+ const STAGE_GROUP_HEADER = {
22
+ new: 'border-l-[6px] border-l-violet-600 bg-violet-50/90',
23
+ discovery: 'border-l-[6px] border-l-cyan-500 bg-cyan-50/70',
24
+ proposal: 'border-l-[6px] border-l-sky-500 bg-sky-50/70',
25
+ negotiation: 'border-l-[6px] border-l-teal-600 bg-teal-50/70',
26
+ won: 'border-l-[6px] border-l-emerald-600 bg-emerald-50/70',
27
+ lost: 'border-l-[6px] border-l-red-600 bg-red-50/70',
28
+ };
29
+
30
  function stageMeta(v) {
31
  return STAGES.find((s) => s.value === v) || STAGES[0];
32
  }
 
61
  el?.focus?.();
62
  }
63
 
64
+ function sumNumeric(arr, key) {
65
+ return arr.reduce((a, d) => {
66
+ const v = d[key];
67
+ return a + (typeof v === 'number' && Number.isFinite(v) ? v : 0);
68
+ }, 0);
69
+ }
70
+
71
+ function DealRow({ deal, tableEditRowId, setTableEditRowId, patchDeal, updateStage, openDeal }) {
72
+ const meta = stageMeta(deal.stage);
73
+ const safeStage = STAGES.some((s) => s.value === deal.stage) ? deal.stage : 'new';
74
+ return (
75
+ <tr
76
+ role="button"
77
+ tabIndex={0}
78
+ onClick={(e) => {
79
+ if (isRowUiTarget(e)) return;
80
+ setTableEditRowId(null);
81
+ openDeal(deal);
82
+ }}
83
+ onKeyDown={(e) => {
84
+ if (isRowUiTarget(e)) return;
85
+ if (e.key === 'Enter' || e.key === ' ') {
86
+ e.preventDefault();
87
+ setTableEditRowId(null);
88
+ openDeal(deal);
89
+ }
90
+ }}
91
+ className="border-b border-slate-100 hover:bg-violet-50/40 cursor-pointer"
92
+ >
93
+ <td className="px-1 py-1.5 w-[4.25rem]" onClick={(e) => e.stopPropagation()}>
94
+ <div className="flex flex-col items-center gap-0.5">
95
+ <input type="checkbox" className="rounded border-slate-300" />
96
+ <Button
97
+ type="button"
98
+ variant="ghost"
99
+ size="icon"
100
+ className={cn(
101
+ 'h-7 w-7 text-slate-500 hover:text-violet-700',
102
+ tableEditRowId === deal.id && 'bg-violet-100 text-violet-800 hover:bg-violet-100'
103
+ )}
104
+ onClick={(e) => {
105
+ e.stopPropagation();
106
+ const tr = e.currentTarget.closest('tr');
107
+ if (tableEditRowId === deal.id) {
108
+ setTableEditRowId(null);
109
+ return;
110
+ }
111
+ setTableEditRowId(deal.id);
112
+ requestAnimationFrame(() => focusFirstEditableInRow(tr));
113
+ }}
114
+ aria-label={
115
+ tableEditRowId === deal.id
116
+ ? `Stop editing row for ${deal.name || 'deal'}`
117
+ : `Edit fields in row for ${deal.name || 'deal'}`
118
+ }
119
+ >
120
+ <Pencil className="h-3.5 w-3.5" aria-hidden />
121
+ </Button>
122
+ </div>
123
+ </td>
124
+ <td className="px-3 py-2 align-top max-w-[220px] font-medium">
125
+ {tableEditRowId === deal.id ? (
126
+ <EditableCell
127
+ value={deal.name || ''}
128
+ onCommit={(v) => patchDeal(deal.id, { name: v })}
129
+ inputClassName="font-medium text-slate-900"
130
+ />
131
+ ) : (
132
+ <div className="min-h-[2rem] py-1 text-sm font-medium text-slate-900 truncate">
133
+ {deal.name || '—'}
134
+ </div>
135
+ )}
136
+ </td>
137
+ <td className="px-3 py-2" onClick={(e) => e.stopPropagation()}>
138
+ <Select value={safeStage} onValueChange={(v) => updateStage(deal.id, v)}>
139
+ <SelectTrigger className={cn('h-8 w-[min(100%,150px)] border-slate-200 shadow-none')}>
140
+ <span className={cn('rounded-full px-2 py-0.5 text-xs font-medium', meta.className)}>
141
+ {meta.label}
142
+ </span>
143
+ </SelectTrigger>
144
+ <SelectContent className="min-w-[180px]">
145
+ {STAGES.map((s) => (
146
+ <SelectItem key={s.value} value={s.value}>
147
+ <span
148
+ className={cn(
149
+ 'rounded-full px-2 py-0.5 text-xs font-medium inline-block',
150
+ s.className
151
+ )}
152
+ >
153
+ {s.label}
154
+ </span>
155
+ </SelectItem>
156
+ ))}
157
+ </SelectContent>
158
+ </Select>
159
+ </td>
160
+ <td className="px-3 py-2 align-top">
161
+ {tableEditRowId === deal.id ? (
162
+ <EditableCell
163
+ value={deal.owner_initials || ''}
164
+ onCommit={(v) =>
165
+ patchDeal(deal.id, {
166
+ owner_initials: (v || '').slice(0, 8).toUpperCase(),
167
+ })
168
+ }
169
+ inputClassName="text-center uppercase tracking-wide max-w-[4.5rem] font-semibold"
170
+ />
171
+ ) : (
172
+ <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">
173
+ {deal.owner_initials || '?'}
174
+ </div>
175
+ )}
176
+ </td>
177
+ <td className="px-3 py-2 align-top tabular-nums max-w-[120px]">
178
+ {tableEditRowId === deal.id ? (
179
+ <EditableCell
180
+ type="number"
181
+ value={deal.deal_value != null ? String(deal.deal_value) : ''}
182
+ onCommit={(v) => {
183
+ if (v.trim() === '') {
184
+ patchDeal(deal.id, { deal_value: null });
185
+ return;
186
+ }
187
+ const n = Math.round(Number(v));
188
+ if (!Number.isFinite(n)) return;
189
+ patchDeal(deal.id, { deal_value: n });
190
+ }}
191
+ />
192
+ ) : (
193
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700 tabular-nums">
194
+ {fmtMoney(deal.deal_value)}
195
+ </div>
196
+ )}
197
+ </td>
198
+ <td className="px-3 py-2 align-top max-w-[180px]">
199
+ {tableEditRowId === deal.id ? (
200
+ <EditableCell
201
+ value={deal.contact_display || ''}
202
+ onCommit={(v) => patchDeal(deal.id, { contact_display: v })}
203
+ inputClassName="text-xs"
204
+ />
205
+ ) : (
206
+ <div className="min-h-[2rem] py-1">
207
+ {deal.contact_display ? (
208
+ <span className="inline-flex rounded-md bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-800">
209
+ {deal.contact_display}
210
+ </span>
211
+ ) : (
212
+ <span className="text-slate-500">—</span>
213
+ )}
214
+ </div>
215
+ )}
216
+ </td>
217
+ <td className="px-3 py-2 align-top max-w-[200px]">
218
+ {tableEditRowId === deal.id ? (
219
+ <EditableCell
220
+ value={deal.account_name || ''}
221
+ onCommit={(v) => patchDeal(deal.id, { account_name: v })}
222
+ inputClassName="text-xs"
223
+ />
224
+ ) : (
225
+ <div className="min-h-[2rem] py-1">
226
+ {deal.account_name ? (
227
+ <span className="inline-flex rounded-md bg-violet-50 px-2 py-0.5 text-xs font-medium text-violet-800">
228
+ {deal.account_name}
229
+ </span>
230
+ ) : (
231
+ <span className="text-slate-500">—</span>
232
+ )}
233
+ </div>
234
+ )}
235
+ </td>
236
+ <td className="px-3 py-2 align-top">
237
+ {tableEditRowId === deal.id ? (
238
+ <EditableDateCell
239
+ value={deal.expected_close_date}
240
+ onCommit={(dateStr) =>
241
+ patchDeal(deal.id, {
242
+ expected_close_date: dateStr || null,
243
+ })
244
+ }
245
+ />
246
+ ) : (
247
+ <div className="min-h-[2rem] py-1 text-sm text-slate-600">
248
+ {fmtDate(deal.expected_close_date)}
249
+ </div>
250
+ )}
251
+ </td>
252
+ <td className="px-3 py-2 align-top tabular-nums max-w-[90px]">
253
+ {tableEditRowId === deal.id ? (
254
+ <EditableCell
255
+ type="number"
256
+ value={
257
+ deal.close_probability != null ? String(deal.close_probability) : ''
258
+ }
259
+ onCommit={(v) => {
260
+ if (v.trim() === '') {
261
+ patchDeal(deal.id, { close_probability: 0 });
262
+ return;
263
+ }
264
+ const n = Math.min(100, Math.max(0, Math.round(Number(v))));
265
+ if (!Number.isFinite(n)) return;
266
+ patchDeal(deal.id, { close_probability: n });
267
+ }}
268
+ />
269
+ ) : (
270
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700 tabular-nums">
271
+ {deal.close_probability ?? '—'}%
272
+ </div>
273
+ )}
274
+ </td>
275
+ <td className="px-3 py-2 tabular-nums text-slate-700" title="Forecast = deal value × close %">
276
+ {fmtMoney(deal.forecast_value)}
277
+ </td>
278
+ <td className="px-3 py-2 align-top">
279
+ {tableEditRowId === deal.id ? (
280
+ <EditableDateCell
281
+ value={deal.last_interaction_at}
282
+ onCommit={(dateStr) =>
283
+ patchDeal(deal.id, {
284
+ last_interaction_at: dateStr || null,
285
+ })
286
+ }
287
+ />
288
+ ) : (
289
+ <div className="min-h-[2rem] py-1 text-sm text-slate-600">
290
+ {fmtDate(deal.last_interaction_at)}
291
+ </div>
292
+ )}
293
+ </td>
294
+ <td className="px-3 py-2 align-top max-w-[120px]">
295
+ {tableEditRowId === deal.id ? (
296
+ <EditableCell
297
+ value={deal.country || ''}
298
+ onCommit={(v) => patchDeal(deal.id, { country: v })}
299
+ />
300
+ ) : (
301
+ <div className="min-h-[2rem] py-1 text-sm text-slate-700">{deal.country || '—'}</div>
302
+ )}
303
+ </td>
304
+ </tr>
305
+ );
306
+ }
307
+
308
  export default function Deals() {
309
  const [deals, setDeals] = useState([]);
310
  const [total, setTotal] = useState(0);
 
315
  const [panelOpen, setPanelOpen] = useState(false);
316
  const [dealDetail, setDealDetail] = useState(null);
317
  const [tableEditRowId, setTableEditRowId] = useState(null);
318
+ const [dealsView, setDealsView] = useState('main');
319
 
320
  const fetchDeals = useCallback(async () => {
321
  setLoading(true);
 
344
  return () => clearTimeout(t);
345
  }, [fetchDeals]);
346
 
347
+ useEffect(() => {
348
+ setTableEditRowId(null);
349
+ }, [dealsView]);
350
+
351
+ const dealsByStage = useMemo(() => {
352
+ return STAGES.map((s) => ({
353
+ ...s,
354
+ deals: deals.filter((d) => {
355
+ const st = STAGES.some((x) => x.value === d.stage) ? d.stage : 'new';
356
+ return st === s.value;
357
+ }),
358
+ })).filter((g) => g.deals.length > 0);
359
+ }, [deals]);
360
+
361
  const seedDemo = async () => {
362
  setSeedBusy(true);
363
  try {
 
422
  >
423
  <MainTableWorkspace
424
  tabs={[
425
+ {
426
+ label: 'Main table',
427
+ active: dealsView === 'main',
428
+ onClick: () => setDealsView('main'),
429
+ },
430
+ {
431
+ label: 'By Stage',
432
+ active: dealsView === 'byStage',
433
+ onClick: () => setDealsView('byStage'),
434
+ },
435
  { label: 'Pipeline', active: false, disabled: true },
436
  ]}
437
  primaryAction={{ label: 'New deal', onClick: () => {} }}
 
486
  <th className="px-3 py-2 font-medium">Country</th>
487
  </tr>
488
  </thead>
489
+ {dealsView === 'main' ? (
490
+ <tbody>
491
+ {deals.map((deal) => (
492
+ <DealRow
 
 
 
 
493
  key={deal.id}
494
+ deal={deal}
495
+ tableEditRowId={tableEditRowId}
496
+ setTableEditRowId={setTableEditRowId}
497
+ patchDeal={patchDeal}
498
+ updateStage={updateStage}
499
+ openDeal={openDeal}
500
+ />
501
+ ))}
502
+ </tbody>
503
+ ) : (
504
+ dealsByStage.map((group) => {
505
+ const sumDeal = sumNumeric(group.deals, 'deal_value');
506
+ const sumForecast = sumNumeric(group.deals, 'forecast_value');
507
+ const bar =
508
+ STAGE_GROUP_HEADER[group.value] ||
509
+ 'border-l-[6px] border-l-slate-400 bg-slate-50';
510
+ return (
511
+ <tbody key={group.value} className="border-b border-slate-200">
512
+ <tr className={cn(bar)}>
513
+ <td colSpan={12} className="px-3 py-2.5">
514
+ <div className="flex flex-wrap items-center gap-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
515
  <span
516
  className={cn(
517
+ 'rounded-full px-2.5 py-0.5 text-xs font-semibold',
518
+ group.className
519
  )}
520
  >
521
+ {group.label}
522
+ </span>
523
+ <span className="text-sm text-slate-600">
524
+ {group.deals.length}{' '}
525
+ {group.deals.length === 1 ? 'deal' : 'deals'}
526
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  </div>
528
+ </td>
529
+ </tr>
530
+ {group.deals.map((deal) => (
531
+ <DealRow
532
+ key={deal.id}
533
+ deal={deal}
534
+ tableEditRowId={tableEditRowId}
535
+ setTableEditRowId={setTableEditRowId}
536
+ patchDeal={patchDeal}
537
+ updateStage={updateStage}
538
+ openDeal={openDeal}
539
+ />
540
+ ))}
541
+ <tr className="bg-slate-50/90 text-slate-700 border-b border-slate-100">
542
+ <td colSpan={4} className="px-3 py-2 text-right text-xs text-slate-500">
543
+ Group total
544
+ </td>
545
+ <td className="px-3 py-2 font-semibold tabular-nums">
546
+ {fmtMoney(sumDeal)}
547
+ </td>
548
+ <td colSpan={4} />
549
+ <td className="px-3 py-2 font-semibold tabular-nums">
550
+ {fmtMoney(sumForecast)}
551
+ </td>
552
+ <td colSpan={2} />
553
+ </tr>
554
+ <tr>
555
+ <td colSpan={12} className="px-3 py-1.5">
556
+ <Button
557
+ type="button"
558
+ variant="ghost"
559
+ size="sm"
560
+ className="h-8 text-violet-700 hover:text-violet-900 hover:bg-violet-50"
561
+ onClick={() => {}}
562
+ >
563
+ + Add deal
564
+ </Button>
565
+ </td>
566
+ </tr>
567
+ </tbody>
568
  );
569
+ })
570
+ )}
571
  </table>
572
  )}
573
  </div>