@contractspec/example.crm-pipeline 3.7.6 → 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 (105) hide show
  1. package/.turbo/turbo-build.log +8 -8
  2. package/AGENTS.md +51 -33
  3. package/README.md +66 -148
  4. package/dist/browser/events/contact.event.js +1 -1
  5. package/dist/browser/events/deal.event.js +1 -1
  6. package/dist/browser/events/index.js +3 -3
  7. package/dist/browser/events/task.event.js +1 -1
  8. package/dist/browser/index.js +293 -293
  9. package/dist/browser/ui/CrmDashboard.js +221 -221
  10. package/dist/browser/ui/CrmDealCard.js +5 -5
  11. package/dist/browser/ui/CrmPipelineBoard.js +13 -13
  12. package/dist/browser/ui/hooks/index.js +2 -2
  13. package/dist/browser/ui/hooks/useDealList.js +1 -1
  14. package/dist/browser/ui/hooks/useDealMutations.js +1 -1
  15. package/dist/browser/ui/index.js +290 -290
  16. package/dist/browser/ui/modals/CreateDealModal.js +12 -12
  17. package/dist/browser/ui/modals/DealActionsModal.js +21 -21
  18. package/dist/browser/ui/modals/index.js +33 -33
  19. package/dist/browser/ui/renderers/index.js +116 -116
  20. package/dist/browser/ui/renderers/pipeline.renderer.js +97 -97
  21. package/dist/deal/index.d.ts +2 -2
  22. package/dist/events/contact.event.js +1 -1
  23. package/dist/events/deal.event.js +1 -1
  24. package/dist/events/index.js +3 -3
  25. package/dist/events/task.event.js +1 -1
  26. package/dist/handlers/index.d.ts +2 -2
  27. package/dist/index.d.ts +3 -3
  28. package/dist/index.js +293 -293
  29. package/dist/node/events/contact.event.js +1 -1
  30. package/dist/node/events/deal.event.js +1 -1
  31. package/dist/node/events/index.js +3 -3
  32. package/dist/node/events/task.event.js +1 -1
  33. package/dist/node/index.js +293 -293
  34. package/dist/node/ui/CrmDashboard.js +221 -221
  35. package/dist/node/ui/CrmDealCard.js +5 -5
  36. package/dist/node/ui/CrmPipelineBoard.js +13 -13
  37. package/dist/node/ui/hooks/index.js +2 -2
  38. package/dist/node/ui/hooks/useDealList.js +1 -1
  39. package/dist/node/ui/hooks/useDealMutations.js +1 -1
  40. package/dist/node/ui/index.js +290 -290
  41. package/dist/node/ui/modals/CreateDealModal.js +12 -12
  42. package/dist/node/ui/modals/DealActionsModal.js +21 -21
  43. package/dist/node/ui/modals/index.js +33 -33
  44. package/dist/node/ui/renderers/index.js +116 -116
  45. package/dist/node/ui/renderers/pipeline.renderer.js +97 -97
  46. package/dist/operations/index.d.ts +1 -1
  47. package/dist/ui/CrmDashboard.js +221 -221
  48. package/dist/ui/CrmDealCard.js +5 -5
  49. package/dist/ui/CrmPipelineBoard.js +13 -13
  50. package/dist/ui/hooks/index.d.ts +2 -2
  51. package/dist/ui/hooks/index.js +2 -2
  52. package/dist/ui/hooks/useDealList.js +1 -1
  53. package/dist/ui/hooks/useDealMutations.d.ts +9 -0
  54. package/dist/ui/hooks/useDealMutations.js +1 -1
  55. package/dist/ui/index.d.ts +3 -3
  56. package/dist/ui/index.js +290 -290
  57. package/dist/ui/modals/CreateDealModal.js +12 -12
  58. package/dist/ui/modals/DealActionsModal.js +21 -21
  59. package/dist/ui/modals/index.js +33 -33
  60. package/dist/ui/renderers/index.d.ts +1 -1
  61. package/dist/ui/renderers/index.js +116 -116
  62. package/dist/ui/renderers/pipeline.renderer.d.ts +1 -1
  63. package/dist/ui/renderers/pipeline.renderer.js +97 -97
  64. package/package.json +10 -10
  65. package/src/crm-pipeline.feature.ts +86 -86
  66. package/src/deal/deal.enum.ts +8 -8
  67. package/src/deal/deal.operation.ts +255 -255
  68. package/src/deal/deal.schema.ts +92 -92
  69. package/src/deal/deal.test-spec.ts +48 -48
  70. package/src/deal/index.ts +17 -19
  71. package/src/docs/crm-pipeline.docblock.ts +43 -43
  72. package/src/entities/company.entity.ts +52 -52
  73. package/src/entities/contact.entity.ts +67 -67
  74. package/src/entities/deal.entity.ts +134 -134
  75. package/src/entities/index.ts +27 -27
  76. package/src/entities/task.entity.ts +105 -105
  77. package/src/events/contact.event.ts +22 -22
  78. package/src/events/deal.event.ts +77 -77
  79. package/src/events/task.event.ts +19 -19
  80. package/src/example.ts +32 -32
  81. package/src/handlers/crm.handlers.ts +358 -357
  82. package/src/handlers/deal.handlers.ts +179 -179
  83. package/src/handlers/index.ts +18 -19
  84. package/src/handlers/mock-data.ts +167 -167
  85. package/src/index.ts +11 -11
  86. package/src/operations/index.ts +16 -16
  87. package/src/presentations/dashboard.presentation.ts +45 -45
  88. package/src/presentations/pipeline.presentation.ts +90 -90
  89. package/src/seeders/index.ts +26 -26
  90. package/src/shared/overlay-types.ts +23 -23
  91. package/src/ui/CrmDashboard.tsx +256 -256
  92. package/src/ui/CrmDealCard.tsx +64 -64
  93. package/src/ui/CrmPipelineBoard.tsx +105 -105
  94. package/src/ui/hooks/index.ts +3 -3
  95. package/src/ui/hooks/useDealList.ts +85 -85
  96. package/src/ui/hooks/useDealMutations.ts +151 -150
  97. package/src/ui/index.ts +5 -10
  98. package/src/ui/modals/CreateDealModal.tsx +217 -217
  99. package/src/ui/modals/DealActionsModal.tsx +390 -390
  100. package/src/ui/overlays/demo-overlays.ts +43 -43
  101. package/src/ui/renderers/index.ts +4 -3
  102. package/src/ui/renderers/pipeline.markdown.ts +165 -165
  103. package/src/ui/renderers/pipeline.renderer.tsx +17 -16
  104. package/tsconfig.json +7 -8
  105. 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
  }