@contractspec/example.crm-pipeline 3.7.5 → 3.7.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/AGENTS.md +51 -33
  3. package/CHANGELOG.md +16 -0
  4. package/README.md +66 -148
  5. package/dist/browser/events/contact.event.js +1 -1
  6. package/dist/browser/events/deal.event.js +1 -1
  7. package/dist/browser/events/index.js +3 -3
  8. package/dist/browser/events/task.event.js +1 -1
  9. package/dist/browser/index.js +293 -293
  10. package/dist/browser/ui/CrmDashboard.js +221 -221
  11. package/dist/browser/ui/CrmDealCard.js +5 -5
  12. package/dist/browser/ui/CrmPipelineBoard.js +13 -13
  13. package/dist/browser/ui/hooks/index.js +2 -2
  14. package/dist/browser/ui/hooks/useDealList.js +1 -1
  15. package/dist/browser/ui/hooks/useDealMutations.js +1 -1
  16. package/dist/browser/ui/index.js +290 -290
  17. package/dist/browser/ui/modals/CreateDealModal.js +12 -12
  18. package/dist/browser/ui/modals/DealActionsModal.js +21 -21
  19. package/dist/browser/ui/modals/index.js +33 -33
  20. package/dist/browser/ui/renderers/index.js +116 -116
  21. package/dist/browser/ui/renderers/pipeline.renderer.js +97 -97
  22. package/dist/deal/index.d.ts +2 -2
  23. package/dist/events/contact.event.js +1 -1
  24. package/dist/events/deal.event.js +1 -1
  25. package/dist/events/index.js +3 -3
  26. package/dist/events/task.event.js +1 -1
  27. package/dist/handlers/index.d.ts +2 -2
  28. package/dist/index.d.ts +3 -3
  29. package/dist/index.js +293 -293
  30. package/dist/node/events/contact.event.js +1 -1
  31. package/dist/node/events/deal.event.js +1 -1
  32. package/dist/node/events/index.js +3 -3
  33. package/dist/node/events/task.event.js +1 -1
  34. package/dist/node/index.js +293 -293
  35. package/dist/node/ui/CrmDashboard.js +221 -221
  36. package/dist/node/ui/CrmDealCard.js +5 -5
  37. package/dist/node/ui/CrmPipelineBoard.js +13 -13
  38. package/dist/node/ui/hooks/index.js +2 -2
  39. package/dist/node/ui/hooks/useDealList.js +1 -1
  40. package/dist/node/ui/hooks/useDealMutations.js +1 -1
  41. package/dist/node/ui/index.js +290 -290
  42. package/dist/node/ui/modals/CreateDealModal.js +12 -12
  43. package/dist/node/ui/modals/DealActionsModal.js +21 -21
  44. package/dist/node/ui/modals/index.js +33 -33
  45. package/dist/node/ui/renderers/index.js +116 -116
  46. package/dist/node/ui/renderers/pipeline.renderer.js +97 -97
  47. package/dist/operations/index.d.ts +1 -1
  48. package/dist/ui/CrmDashboard.js +221 -221
  49. package/dist/ui/CrmDealCard.js +5 -5
  50. package/dist/ui/CrmPipelineBoard.js +13 -13
  51. package/dist/ui/hooks/index.d.ts +2 -2
  52. package/dist/ui/hooks/index.js +2 -2
  53. package/dist/ui/hooks/useDealList.js +1 -1
  54. package/dist/ui/hooks/useDealMutations.d.ts +9 -0
  55. package/dist/ui/hooks/useDealMutations.js +1 -1
  56. package/dist/ui/index.d.ts +3 -3
  57. package/dist/ui/index.js +290 -290
  58. package/dist/ui/modals/CreateDealModal.js +12 -12
  59. package/dist/ui/modals/DealActionsModal.js +21 -21
  60. package/dist/ui/modals/index.js +33 -33
  61. package/dist/ui/renderers/index.d.ts +1 -1
  62. package/dist/ui/renderers/index.js +116 -116
  63. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  64. package/dist/ui/renderers/pipeline.renderer.js +97 -97
  65. package/package.json +14 -14
  66. package/src/crm-pipeline.feature.ts +86 -86
  67. package/src/deal/deal.enum.ts +8 -8
  68. package/src/deal/deal.operation.ts +255 -255
  69. package/src/deal/deal.schema.ts +92 -92
  70. package/src/deal/deal.test-spec.ts +48 -48
  71. package/src/deal/index.ts +17 -19
  72. package/src/docs/crm-pipeline.docblock.ts +43 -43
  73. package/src/entities/company.entity.ts +52 -52
  74. package/src/entities/contact.entity.ts +67 -67
  75. package/src/entities/deal.entity.ts +134 -134
  76. package/src/entities/index.ts +27 -27
  77. package/src/entities/task.entity.ts +105 -105
  78. package/src/events/contact.event.ts +22 -22
  79. package/src/events/deal.event.ts +77 -77
  80. package/src/events/task.event.ts +19 -19
  81. package/src/example.ts +32 -32
  82. package/src/handlers/crm.handlers.ts +358 -357
  83. package/src/handlers/deal.handlers.ts +179 -179
  84. package/src/handlers/index.ts +18 -19
  85. package/src/handlers/mock-data.ts +167 -167
  86. package/src/index.ts +11 -11
  87. package/src/operations/index.ts +16 -16
  88. package/src/presentations/dashboard.presentation.ts +45 -45
  89. package/src/presentations/pipeline.presentation.ts +90 -90
  90. package/src/seeders/index.ts +26 -26
  91. package/src/shared/overlay-types.ts +23 -23
  92. package/src/ui/CrmDashboard.tsx +256 -256
  93. package/src/ui/CrmDealCard.tsx +64 -64
  94. package/src/ui/CrmPipelineBoard.tsx +105 -105
  95. package/src/ui/hooks/index.ts +3 -3
  96. package/src/ui/hooks/useDealList.ts +85 -85
  97. package/src/ui/hooks/useDealMutations.ts +151 -150
  98. package/src/ui/index.ts +5 -10
  99. package/src/ui/modals/CreateDealModal.tsx +217 -217
  100. package/src/ui/modals/DealActionsModal.tsx +390 -390
  101. package/src/ui/overlays/demo-overlays.ts +43 -43
  102. package/src/ui/renderers/index.ts +4 -3
  103. package/src/ui/renderers/pipeline.markdown.ts +165 -165
  104. package/src/ui/renderers/pipeline.renderer.tsx +17 -16
  105. package/tsconfig.json +7 -8
  106. package/tsdown.config.js +7 -3
@@ -6,78 +6,78 @@
6
6
  import type { Deal } from './hooks/useDealList';
7
7
 
8
8
  interface CrmDealCardProps {
9
- deal: Deal;
10
- onClick?: () => void;
9
+ deal: Deal;
10
+ onClick?: () => void;
11
11
  }
12
12
 
13
13
  function formatCurrency(value: number, currency: string): string {
14
- return new Intl.NumberFormat('en-US', {
15
- style: 'currency',
16
- currency,
17
- minimumFractionDigits: 0,
18
- maximumFractionDigits: 0,
19
- }).format(value);
14
+ return new Intl.NumberFormat('en-US', {
15
+ style: 'currency',
16
+ currency,
17
+ minimumFractionDigits: 0,
18
+ maximumFractionDigits: 0,
19
+ }).format(value);
20
20
  }
21
21
 
22
22
  export function CrmDealCard({ deal, onClick }: CrmDealCardProps) {
23
- const daysUntilClose = deal.expectedCloseDate
24
- ? Math.ceil(
25
- (deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
26
- )
27
- : null;
23
+ const daysUntilClose = deal.expectedCloseDate
24
+ ? Math.ceil(
25
+ (deal.expectedCloseDate.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
26
+ )
27
+ : null;
28
28
 
29
- return (
30
- <div
31
- onClick={onClick}
32
- className="border-border bg-card cursor-pointer rounded-lg border p-3 shadow-sm transition-shadow hover:shadow-md"
33
- role="button"
34
- tabIndex={0}
35
- onKeyDown={(e) => {
36
- if (e.key === 'Enter' || e.key === ' ') onClick?.();
37
- }}
38
- >
39
- {/* Deal Name */}
40
- <h4 className="leading-snug font-medium">{deal.name}</h4>
29
+ return (
30
+ <div
31
+ onClick={onClick}
32
+ className="cursor-pointer rounded-lg border border-border bg-card p-3 shadow-sm transition-shadow hover:shadow-md"
33
+ role="button"
34
+ tabIndex={0}
35
+ onKeyDown={(e) => {
36
+ if (e.key === 'Enter' || e.key === ' ') onClick?.();
37
+ }}
38
+ >
39
+ {/* Deal Name */}
40
+ <h4 className="font-medium leading-snug">{deal.name}</h4>
41
41
 
42
- {/* Deal Value */}
43
- <div className="text-primary mt-2 text-lg font-semibold">
44
- {formatCurrency(deal.value, deal.currency)}
45
- </div>
42
+ {/* Deal Value */}
43
+ <div className="mt-2 font-semibold text-lg text-primary">
44
+ {formatCurrency(deal.value, deal.currency)}
45
+ </div>
46
46
 
47
- {/* Meta Info */}
48
- <div className="text-muted-foreground mt-3 flex items-center justify-between text-xs">
49
- {/* Expected Close */}
50
- {daysUntilClose !== null && (
51
- <span
52
- className={
53
- daysUntilClose < 0
54
- ? 'text-red-500'
55
- : daysUntilClose <= 7
56
- ? 'text-yellow-600 dark:text-yellow-500'
57
- : ''
58
- }
59
- >
60
- {daysUntilClose < 0
61
- ? `${Math.abs(daysUntilClose)}d overdue`
62
- : daysUntilClose === 0
63
- ? 'Due today'
64
- : `${daysUntilClose}d left`}
65
- </span>
66
- )}
47
+ {/* Meta Info */}
48
+ <div className="mt-3 flex items-center justify-between text-muted-foreground text-xs">
49
+ {/* Expected Close */}
50
+ {daysUntilClose !== null && (
51
+ <span
52
+ className={
53
+ daysUntilClose < 0
54
+ ? 'text-red-500'
55
+ : daysUntilClose <= 7
56
+ ? 'text-yellow-600 dark:text-yellow-500'
57
+ : ''
58
+ }
59
+ >
60
+ {daysUntilClose < 0
61
+ ? `${Math.abs(daysUntilClose)}d overdue`
62
+ : daysUntilClose === 0
63
+ ? 'Due today'
64
+ : `${daysUntilClose}d left`}
65
+ </span>
66
+ )}
67
67
 
68
- {/* Status Badge */}
69
- <span
70
- className={`rounded px-1.5 py-0.5 text-xs font-medium ${
71
- deal.status === 'WON'
72
- ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
73
- : deal.status === 'LOST'
74
- ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
75
- : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
76
- }`}
77
- >
78
- {deal.status}
79
- </span>
80
- </div>
81
- </div>
82
- );
68
+ {/* Status Badge */}
69
+ <span
70
+ className={`rounded px-1.5 py-0.5 font-medium text-xs ${
71
+ deal.status === 'WON'
72
+ ? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
73
+ : deal.status === 'LOST'
74
+ ? 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'
75
+ : 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
76
+ }`}
77
+ >
78
+ {deal.status}
79
+ </span>
80
+ </div>
81
+ </div>
82
+ );
83
83
  }
@@ -10,127 +10,127 @@
10
10
  * - Drag-and-drop ready (UI only, no lib dependency)
11
11
  */
12
12
  import { useState } from 'react';
13
+ import { CrmDealCard } from './CrmDealCard';
13
14
  // import { Button } from '@contractspec/lib.design-system';
14
15
  import type { Deal } from './hooks/useDealList';
15
- import { CrmDealCard } from './CrmDealCard';
16
16
 
17
17
  interface CrmPipelineBoardProps {
18
- dealsByStage: Record<string, Deal[]>;
19
- stages: { id: string; name: string; position: number }[];
20
- onDealClick?: (dealId: string) => void;
21
- onDealMove?: (dealId: string, toStageId: string) => void;
18
+ dealsByStage: Record<string, Deal[]>;
19
+ stages: { id: string; name: string; position: number }[];
20
+ onDealClick?: (dealId: string) => void;
21
+ onDealMove?: (dealId: string, toStageId: string) => void;
22
22
  }
23
23
 
24
24
  function formatCurrency(value: number): string {
25
- if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
26
- if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`;
27
- return `$${value}`;
25
+ if (value >= 1000000) return `$${(value / 1000000).toFixed(1)}M`;
26
+ if (value >= 1000) return `$${(value / 1000).toFixed(0)}K`;
27
+ return `$${value}`;
28
28
  }
29
29
 
30
30
  export function CrmPipelineBoard({
31
- dealsByStage,
32
- stages,
33
- onDealClick,
34
- onDealMove,
31
+ dealsByStage,
32
+ stages,
33
+ onDealClick,
34
+ onDealMove,
35
35
  }: CrmPipelineBoardProps) {
36
- // Track which deal has the quick-move dropdown open
37
- const [quickMoveOpen, setQuickMoveOpen] = useState<string | null>(null);
36
+ // Track which deal has the quick-move dropdown open
37
+ const [quickMoveOpen, setQuickMoveOpen] = useState<string | null>(null);
38
38
 
39
- // Sort stages by position
40
- const sortedStages = [...stages].sort((a, b) => a.position - b.position);
39
+ // Sort stages by position
40
+ const sortedStages = [...stages].sort((a, b) => a.position - b.position);
41
41
 
42
- const handleQuickMove = (dealId: string, toStageId: string) => {
43
- onDealMove?.(dealId, toStageId);
44
- setQuickMoveOpen(null);
45
- };
42
+ const handleQuickMove = (dealId: string, toStageId: string) => {
43
+ onDealMove?.(dealId, toStageId);
44
+ setQuickMoveOpen(null);
45
+ };
46
46
 
47
- return (
48
- <div className="flex gap-4 overflow-x-auto pb-4">
49
- {sortedStages.map((stage) => {
50
- const deals = dealsByStage[stage.id] ?? [];
51
- const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
47
+ return (
48
+ <div className="flex gap-4 overflow-x-auto pb-4">
49
+ {sortedStages.map((stage) => {
50
+ const deals = dealsByStage[stage.id] ?? [];
51
+ const stageValue = deals.reduce((sum, d) => sum + d.value, 0);
52
52
 
53
- return (
54
- <div
55
- key={stage.id}
56
- className="bg-muted/30 flex w-72 flex-shrink-0 flex-col rounded-lg"
57
- >
58
- {/* Stage Header */}
59
- <div className="border-border flex items-center justify-between border-b px-3 py-2">
60
- <div>
61
- <h3 className="font-medium">{stage.name}</h3>
62
- <p className="text-muted-foreground text-xs">
63
- {deals.length} deals · {formatCurrency(stageValue)}
64
- </p>
65
- </div>
66
- <span className="bg-muted flex h-6 w-6 items-center justify-center rounded-full text-xs font-medium">
67
- {deals.length}
68
- </span>
69
- </div>
53
+ return (
54
+ <div
55
+ key={stage.id}
56
+ className="flex w-72 flex-shrink-0 flex-col rounded-lg bg-muted/30"
57
+ >
58
+ {/* Stage Header */}
59
+ <div className="flex items-center justify-between border-border border-b px-3 py-2">
60
+ <div>
61
+ <h3 className="font-medium">{stage.name}</h3>
62
+ <p className="text-muted-foreground text-xs">
63
+ {deals.length} deals · {formatCurrency(stageValue)}
64
+ </p>
65
+ </div>
66
+ <span className="flex h-6 w-6 items-center justify-center rounded-full bg-muted font-medium text-xs">
67
+ {deals.length}
68
+ </span>
69
+ </div>
70
70
 
71
- {/* Deals Column */}
72
- <div className="flex flex-1 flex-col gap-2 p-2">
73
- {deals.length === 0 ? (
74
- <div className="border-muted-foreground/20 text-muted-foreground flex h-24 items-center justify-center rounded-md border-2 border-dashed text-xs">
75
- No deals
76
- </div>
77
- ) : (
78
- deals.map((deal) => (
79
- <div key={deal.id} className="group relative">
80
- <CrmDealCard
81
- deal={deal}
82
- onClick={() => onDealClick?.(deal.id)}
83
- />
71
+ {/* Deals Column */}
72
+ <div className="flex flex-1 flex-col gap-2 p-2">
73
+ {deals.length === 0 ? (
74
+ <div className="flex h-24 items-center justify-center rounded-md border-2 border-muted-foreground/20 border-dashed text-muted-foreground text-xs">
75
+ No deals
76
+ </div>
77
+ ) : (
78
+ deals.map((deal) => (
79
+ <div key={deal.id} className="group relative">
80
+ <CrmDealCard
81
+ deal={deal}
82
+ onClick={() => onDealClick?.(deal.id)}
83
+ />
84
84
 
85
- {/* Quick Move Button */}
86
- {deal.status === 'OPEN' && onDealMove && (
87
- <div className="absolute top-1 right-1 opacity-0 transition-opacity group-hover:opacity-100">
88
- <button
89
- type="button"
90
- onClick={(e) => {
91
- e.stopPropagation();
92
- setQuickMoveOpen(
93
- quickMoveOpen === deal.id ? null : deal.id
94
- );
95
- }}
96
- className="bg-background border-border hover:bg-muted flex h-6 w-6 items-center justify-center rounded border text-xs shadow-sm"
97
- title="Quick move"
98
- >
99
- ➡️
100
- </button>
85
+ {/* Quick Move Button */}
86
+ {deal.status === 'OPEN' && onDealMove && (
87
+ <div className="absolute top-1 right-1 opacity-0 transition-opacity group-hover:opacity-100">
88
+ <button
89
+ type="button"
90
+ onClick={(e) => {
91
+ e.stopPropagation();
92
+ setQuickMoveOpen(
93
+ quickMoveOpen === deal.id ? null : deal.id
94
+ );
95
+ }}
96
+ className="flex h-6 w-6 items-center justify-center rounded border border-border bg-background text-xs shadow-sm hover:bg-muted"
97
+ title="Quick move"
98
+ >
99
+ ➡️
100
+ </button>
101
101
 
102
- {/* Quick Move Dropdown */}
103
- {quickMoveOpen === deal.id && (
104
- <div className="bg-card border-border absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border py-1 shadow-lg">
105
- <p className="text-muted-foreground px-3 py-1 text-xs font-medium">
106
- Move to:
107
- </p>
108
- {sortedStages
109
- .filter((s) => s.id !== deal.stageId)
110
- .map((s) => (
111
- <button
112
- key={s.id}
113
- type="button"
114
- onClick={(e) => {
115
- e.stopPropagation();
116
- handleQuickMove(deal.id, s.id);
117
- }}
118
- className="hover:bg-muted w-full px-3 py-1.5 text-left text-sm"
119
- >
120
- {s.name}
121
- </button>
122
- ))}
123
- </div>
124
- )}
125
- </div>
126
- )}
127
- </div>
128
- ))
129
- )}
130
- </div>
131
- </div>
132
- );
133
- })}
134
- </div>
135
- );
102
+ {/* Quick Move Dropdown */}
103
+ {quickMoveOpen === deal.id && (
104
+ <div className="absolute top-7 right-0 z-20 min-w-[140px] rounded-lg border border-border bg-card py-1 shadow-lg">
105
+ <p className="px-3 py-1 font-medium text-muted-foreground text-xs">
106
+ Move to:
107
+ </p>
108
+ {sortedStages
109
+ .filter((s) => s.id !== deal.stageId)
110
+ .map((s) => (
111
+ <button
112
+ key={s.id}
113
+ type="button"
114
+ onClick={(e) => {
115
+ e.stopPropagation();
116
+ handleQuickMove(deal.id, s.id);
117
+ }}
118
+ className="w-full px-3 py-1.5 text-left text-sm hover:bg-muted"
119
+ >
120
+ {s.name}
121
+ </button>
122
+ ))}
123
+ </div>
124
+ )}
125
+ </div>
126
+ )}
127
+ </div>
128
+ ))
129
+ )}
130
+ </div>
131
+ </div>
132
+ );
133
+ })}
134
+ </div>
135
+ );
136
136
  }
@@ -1,9 +1,9 @@
1
1
  'use client';
2
2
 
3
- export { useDealList, type UseDealListOptions } from './useDealList';
3
+ export { type UseDealListOptions, useDealList } from './useDealList';
4
4
  export {
5
- useDealMutations,
6
- type UseDealMutationsOptions,
5
+ type UseDealMutationsOptions,
6
+ useDealMutations,
7
7
  } from './useDealMutations';
8
8
 
9
9
  // Note: For deal types (CreateDealInput, MoveDealInput, etc.), import directly from:
@@ -1,17 +1,17 @@
1
1
  'use client';
2
2
 
3
+ import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
3
4
  /**
4
5
  * Hook for fetching and managing deal list data
5
6
  *
6
7
  * Uses runtime-local database-backed handlers.
7
8
  */
8
9
  import { useCallback, useEffect, useMemo, useState } from 'react';
9
- import { useTemplateRuntime } from '@contractspec/lib.example-shared-ui';
10
10
  import {
11
- type CrmHandlers,
12
- type Deal as RuntimeDeal,
13
- type ListDealsOutput as RuntimeListDealsOutput,
14
- type Stage,
11
+ type CrmHandlers,
12
+ type Deal as RuntimeDeal,
13
+ type ListDealsOutput as RuntimeListDealsOutput,
14
+ type Stage,
15
15
  } from '../../handlers/crm.handlers';
16
16
 
17
17
  // Re-export types for convenience
@@ -19,95 +19,95 @@ export type Deal = RuntimeDeal;
19
19
  export type ListDealsOutput = RuntimeListDealsOutput;
20
20
 
21
21
  export interface UseDealListOptions {
22
- pipelineId?: string;
23
- stageId?: string;
24
- status?: 'OPEN' | 'WON' | 'LOST' | 'all';
25
- search?: string;
26
- limit?: number;
22
+ pipelineId?: string;
23
+ stageId?: string;
24
+ status?: 'OPEN' | 'WON' | 'LOST' | 'all';
25
+ search?: string;
26
+ limit?: number;
27
27
  }
28
28
 
29
29
  export function useDealList(options: UseDealListOptions = {}) {
30
- const { handlers, projectId } = useTemplateRuntime<{ crm: CrmHandlers }>();
31
- const { crm } = handlers;
30
+ const { handlers, projectId } = useTemplateRuntime<{ crm: CrmHandlers }>();
31
+ const { crm } = handlers;
32
32
 
33
- const [data, setData] = useState<ListDealsOutput | null>(null);
34
- const [dealsByStage, setDealsByStage] = useState<Record<string, Deal[]>>({});
35
- const [stages, setStages] = useState<Stage[]>([]);
36
- const [loading, setLoading] = useState(true);
37
- const [error, setError] = useState<Error | null>(null);
38
- const [page, setPage] = useState(1);
33
+ const [data, setData] = useState<ListDealsOutput | null>(null);
34
+ const [dealsByStage, setDealsByStage] = useState<Record<string, Deal[]>>({});
35
+ const [stages, setStages] = useState<Stage[]>([]);
36
+ const [loading, setLoading] = useState(true);
37
+ const [error, setError] = useState<Error | null>(null);
38
+ const [page, setPage] = useState(1);
39
39
 
40
- const pipelineId = options.pipelineId ?? 'pipeline-1';
40
+ const pipelineId = options.pipelineId ?? 'pipeline-1';
41
41
 
42
- const fetchData = useCallback(async () => {
43
- setLoading(true);
44
- setError(null);
42
+ const fetchData = useCallback(async () => {
43
+ setLoading(true);
44
+ setError(null);
45
45
 
46
- try {
47
- const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
48
- crm.listDeals({
49
- projectId,
50
- pipelineId,
51
- stageId: options.stageId,
52
- status: options.status === 'all' ? undefined : options.status,
53
- search: options.search,
54
- limit: options.limit ?? 50,
55
- offset: (page - 1) * (options.limit ?? 50),
56
- }),
57
- crm.getDealsByStage({ projectId, pipelineId }),
58
- crm.getPipelineStages({ pipelineId }),
59
- ]);
60
- setData(dealsResult);
61
- setDealsByStage(stageDealsResult);
62
- setStages(stagesResult);
63
- } catch (err) {
64
- setError(err instanceof Error ? err : new Error('Unknown error'));
65
- } finally {
66
- setLoading(false);
67
- }
68
- }, [
69
- crm,
70
- projectId,
71
- pipelineId,
72
- options.stageId,
73
- options.status,
74
- options.search,
75
- options.limit,
76
- page,
77
- ]);
46
+ try {
47
+ const [dealsResult, stageDealsResult, stagesResult] = await Promise.all([
48
+ crm.listDeals({
49
+ projectId,
50
+ pipelineId,
51
+ stageId: options.stageId,
52
+ status: options.status === 'all' ? undefined : options.status,
53
+ search: options.search,
54
+ limit: options.limit ?? 50,
55
+ offset: (page - 1) * (options.limit ?? 50),
56
+ }),
57
+ crm.getDealsByStage({ projectId, pipelineId }),
58
+ crm.getPipelineStages({ pipelineId }),
59
+ ]);
60
+ setData(dealsResult);
61
+ setDealsByStage(stageDealsResult);
62
+ setStages(stagesResult);
63
+ } catch (err) {
64
+ setError(err instanceof Error ? err : new Error('Unknown error'));
65
+ } finally {
66
+ setLoading(false);
67
+ }
68
+ }, [
69
+ crm,
70
+ projectId,
71
+ pipelineId,
72
+ options.stageId,
73
+ options.status,
74
+ options.search,
75
+ options.limit,
76
+ page,
77
+ ]);
78
78
 
79
- useEffect(() => {
80
- fetchData();
81
- }, [fetchData]);
79
+ useEffect(() => {
80
+ fetchData();
81
+ }, [fetchData]);
82
82
 
83
- // Calculate stats
84
- const stats = useMemo(() => {
85
- if (!data) return null;
86
- const open = data.deals.filter((d: Deal) => d.status === 'OPEN');
87
- const won = data.deals.filter((d: Deal) => d.status === 'WON');
88
- const lost = data.deals.filter((d: Deal) => d.status === 'LOST');
83
+ // Calculate stats
84
+ const stats = useMemo(() => {
85
+ if (!data) return null;
86
+ const open = data.deals.filter((d: Deal) => d.status === 'OPEN');
87
+ const won = data.deals.filter((d: Deal) => d.status === 'WON');
88
+ const lost = data.deals.filter((d: Deal) => d.status === 'LOST');
89
89
 
90
- return {
91
- total: data.total,
92
- totalValue: data.totalValue,
93
- openCount: open.length,
94
- openValue: open.reduce((sum: number, d: Deal) => sum + d.value, 0),
95
- wonCount: won.length,
96
- wonValue: won.reduce((sum: number, d: Deal) => sum + d.value, 0),
97
- lostCount: lost.length,
98
- };
99
- }, [data]);
90
+ return {
91
+ total: data.total,
92
+ totalValue: data.totalValue,
93
+ openCount: open.length,
94
+ openValue: open.reduce((sum: number, d: Deal) => sum + d.value, 0),
95
+ wonCount: won.length,
96
+ wonValue: won.reduce((sum: number, d: Deal) => sum + d.value, 0),
97
+ lostCount: lost.length,
98
+ };
99
+ }, [data]);
100
100
 
101
- return {
102
- data,
103
- dealsByStage,
104
- stages,
105
- loading,
106
- error,
107
- stats,
108
- page,
109
- refetch: fetchData,
110
- nextPage: () => setPage((p) => p + 1),
111
- prevPage: () => page > 1 && setPage((p) => p - 1),
112
- };
101
+ return {
102
+ data,
103
+ dealsByStage,
104
+ stages,
105
+ loading,
106
+ error,
107
+ stats,
108
+ page,
109
+ refetch: fetchData,
110
+ nextPage: () => setPage((p) => p + 1),
111
+ prevPage: () => page > 1 && setPage((p) => p - 1),
112
+ };
113
113
  }