@nextsparkjs/theme-crm 0.1.0-beta.1

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 (140) hide show
  1. package/CRM_PLAN.md +343 -0
  2. package/about.md +122 -0
  3. package/config/app.config.ts +185 -0
  4. package/config/billing.config.ts +187 -0
  5. package/config/dashboard.config.ts +372 -0
  6. package/config/dev.config.ts +55 -0
  7. package/config/features.config.ts +336 -0
  8. package/config/flows.config.ts +511 -0
  9. package/config/permissions.config.ts +297 -0
  10. package/config/theme.config.ts +111 -0
  11. package/entities/activities/activities.config.ts +61 -0
  12. package/entities/activities/activities.fields.ts +362 -0
  13. package/entities/activities/activities.service.ts +503 -0
  14. package/entities/activities/activities.types.ts +117 -0
  15. package/entities/activities/messages/en.json +123 -0
  16. package/entities/activities/messages/es.json +123 -0
  17. package/entities/activities/migrations/020_activities_table.sql +123 -0
  18. package/entities/activities/migrations/021_activities_metas.sql +114 -0
  19. package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
  20. package/entities/campaigns/campaigns.config.ts +61 -0
  21. package/entities/campaigns/campaigns.fields.ts +413 -0
  22. package/entities/campaigns/campaigns.service.ts +426 -0
  23. package/entities/campaigns/campaigns.types.ts +124 -0
  24. package/entities/campaigns/messages/en.json +145 -0
  25. package/entities/campaigns/messages/es.json +145 -0
  26. package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
  27. package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
  28. package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
  29. package/entities/companies/companies.config.ts +61 -0
  30. package/entities/companies/companies.fields.ts +429 -0
  31. package/entities/companies/companies.service.ts +566 -0
  32. package/entities/companies/companies.types.ts +125 -0
  33. package/entities/companies/messages/en.json +146 -0
  34. package/entities/companies/messages/es.json +146 -0
  35. package/entities/companies/migrations/001_companies_table.sql +150 -0
  36. package/entities/companies/migrations/002_companies_metas.sql +114 -0
  37. package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
  38. package/entities/contacts/contacts.config.ts +61 -0
  39. package/entities/contacts/contacts.fields.ts +359 -0
  40. package/entities/contacts/contacts.service.ts +509 -0
  41. package/entities/contacts/contacts.types.ts +108 -0
  42. package/entities/contacts/messages/en.json +117 -0
  43. package/entities/contacts/messages/es.json +117 -0
  44. package/entities/contacts/migrations/001_contacts_table.sql +134 -0
  45. package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
  46. package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
  47. package/entities/leads/leads.config.ts +61 -0
  48. package/entities/leads/leads.fields.ts +336 -0
  49. package/entities/leads/leads.service.ts +496 -0
  50. package/entities/leads/leads.types.ts +114 -0
  51. package/entities/leads/messages/en.json +132 -0
  52. package/entities/leads/messages/es.json +132 -0
  53. package/entities/leads/migrations/001_leads_table.sql +150 -0
  54. package/entities/leads/migrations/002_leads_metas.sql +120 -0
  55. package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
  56. package/entities/notes/messages/en.json +114 -0
  57. package/entities/notes/messages/es.json +114 -0
  58. package/entities/notes/migrations/020_notes_table.sql +118 -0
  59. package/entities/notes/migrations/021_notes_metas.sql +114 -0
  60. package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
  61. package/entities/notes/notes.config.ts +61 -0
  62. package/entities/notes/notes.fields.ts +283 -0
  63. package/entities/notes/notes.service.ts +320 -0
  64. package/entities/notes/notes.types.ts +102 -0
  65. package/entities/opportunities/messages/en.json +107 -0
  66. package/entities/opportunities/messages/es.json +107 -0
  67. package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
  68. package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
  69. package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
  70. package/entities/opportunities/opportunities.config.ts +61 -0
  71. package/entities/opportunities/opportunities.fields.ts +416 -0
  72. package/entities/opportunities/opportunities.service.ts +525 -0
  73. package/entities/opportunities/opportunities.types.ts +135 -0
  74. package/entities/pipelines/messages/en.json +115 -0
  75. package/entities/pipelines/messages/es.json +115 -0
  76. package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
  77. package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
  78. package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
  79. package/entities/pipelines/pipelines.config.ts +62 -0
  80. package/entities/pipelines/pipelines.fields.ts +193 -0
  81. package/entities/pipelines/pipelines.service.ts +383 -0
  82. package/entities/pipelines/pipelines.types.ts +78 -0
  83. package/entities/products/messages/en.json +135 -0
  84. package/entities/products/messages/es.json +135 -0
  85. package/entities/products/migrations/001_products_table.sql +117 -0
  86. package/entities/products/migrations/002_products_metas.sql +114 -0
  87. package/entities/products/migrations/003_products_sample_data.sql +247 -0
  88. package/entities/products/products.config.ts +62 -0
  89. package/entities/products/products.fields.ts +361 -0
  90. package/entities/products/products.service.ts +437 -0
  91. package/entities/products/products.types.ts +125 -0
  92. package/lib/crm-constants.ts +77 -0
  93. package/lib/crm-utils.ts +185 -0
  94. package/lib/selectors.ts +333 -0
  95. package/messages/en.json +131 -0
  96. package/messages/es.json +131 -0
  97. package/migrations/999_theme_sample_data.sql +473 -0
  98. package/package.json +18 -0
  99. package/pendings.md +205 -0
  100. package/permissions-matrix.md +216 -0
  101. package/styles/components.css +414 -0
  102. package/styles/crm-theme.css +358 -0
  103. package/styles/globals.css +576 -0
  104. package/styles/variables.css +111 -0
  105. package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
  106. package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
  107. package/templates/dashboard/(main)/activities/page.tsx +297 -0
  108. package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
  109. package/templates/dashboard/(main)/companies/page.tsx +296 -0
  110. package/templates/dashboard/(main)/contacts/page.tsx +347 -0
  111. package/templates/dashboard/(main)/layout.tsx +98 -0
  112. package/templates/dashboard/(main)/leads/page.tsx +335 -0
  113. package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
  114. package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
  115. package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
  116. package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
  117. package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
  118. package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
  119. package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
  120. package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
  121. package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
  122. package/templates/dashboard/(main)/products/create/page.tsx +96 -0
  123. package/templates/dashboard/(main)/products/page.tsx +308 -0
  124. package/templates/shared/ActionButtons.tsx +41 -0
  125. package/templates/shared/CRMDashboard.tsx +519 -0
  126. package/templates/shared/CRMDataTable.tsx +441 -0
  127. package/templates/shared/CRMMetricCard.tsx +76 -0
  128. package/templates/shared/CRMMobileNav.tsx +172 -0
  129. package/templates/shared/CRMSidebar.tsx +346 -0
  130. package/templates/shared/CRMTopBar.tsx +265 -0
  131. package/templates/shared/DealCard.tsx +123 -0
  132. package/templates/shared/EntityCard.tsx +58 -0
  133. package/templates/shared/OpportunityForm.tsx +649 -0
  134. package/templates/shared/PipelineForm.tsx +367 -0
  135. package/templates/shared/PipelineKanban.tsx +194 -0
  136. package/templates/shared/QuickFilters.tsx +47 -0
  137. package/templates/shared/StageColumn.tsx +175 -0
  138. package/templates/shared/StageSelect.tsx +177 -0
  139. package/templates/shared/StagesRepeater.tsx +317 -0
  140. package/templates/shared/index.ts +9 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Pipelines Entity Fields Configuration
3
+ *
4
+ * Separated from main config according to new refactoring plan.
5
+ * Contains all field definitions for the pipelines entity.
6
+ */
7
+
8
+ import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
9
+
10
+ export const pipelinesFields: EntityField[] = [
11
+ {
12
+ name: 'name',
13
+ type: 'text',
14
+ required: true,
15
+ display: {
16
+ label: 'Pipeline Name',
17
+ description: 'Pipeline name',
18
+ placeholder: 'Enter pipeline name...',
19
+ showInList: true,
20
+ showInDetail: true,
21
+ showInForm: true,
22
+ order: 1,
23
+ columnWidth: 6,
24
+ },
25
+ api: {
26
+ searchable: true,
27
+ sortable: true,
28
+ readOnly: false,
29
+ },
30
+ },
31
+ {
32
+ name: 'description',
33
+ type: 'textarea',
34
+ required: false,
35
+ display: {
36
+ label: 'Description',
37
+ description: 'Pipeline description',
38
+ placeholder: 'Enter description...',
39
+ showInList: false,
40
+ showInDetail: true,
41
+ showInForm: true,
42
+ order: 2,
43
+ columnWidth: 6,
44
+ },
45
+ api: {
46
+ searchable: true,
47
+ sortable: false,
48
+ readOnly: false,
49
+ },
50
+ },
51
+ {
52
+ name: 'type',
53
+ type: 'select',
54
+ required: false,
55
+ options: [
56
+ { value: 'sales', label: 'Sales' },
57
+ { value: 'support', label: 'Support' },
58
+ { value: 'project', label: 'Project' },
59
+ { value: 'custom', label: 'Custom' },
60
+ ],
61
+ display: {
62
+ label: 'Pipeline Type',
63
+ description: 'Type of pipeline',
64
+ placeholder: 'Select type...',
65
+ showInList: true,
66
+ showInDetail: true,
67
+ showInForm: true,
68
+ order: 3,
69
+ columnWidth: 4,
70
+ },
71
+ api: {
72
+ searchable: false,
73
+ sortable: true,
74
+ readOnly: false,
75
+ },
76
+ },
77
+ {
78
+ name: 'isDefault',
79
+ type: 'boolean',
80
+ required: false,
81
+ display: {
82
+ label: 'Default Pipeline',
83
+ description: 'Is this the default pipeline',
84
+ showInList: true,
85
+ showInDetail: true,
86
+ showInForm: true,
87
+ order: 4,
88
+ columnWidth: 4,
89
+ },
90
+ api: {
91
+ searchable: false,
92
+ sortable: true,
93
+ readOnly: false,
94
+ },
95
+ },
96
+ {
97
+ name: 'isActive',
98
+ type: 'boolean',
99
+ required: false,
100
+ display: {
101
+ label: 'Active',
102
+ description: 'Is pipeline currently active',
103
+ showInList: true,
104
+ showInDetail: true,
105
+ showInForm: true,
106
+ order: 5,
107
+ columnWidth: 4,
108
+ },
109
+ api: {
110
+ searchable: false,
111
+ sortable: true,
112
+ readOnly: false,
113
+ },
114
+ },
115
+ {
116
+ name: 'stages',
117
+ type: 'json',
118
+ required: true,
119
+ display: {
120
+ label: 'Pipeline Stages',
121
+ description: 'Pipeline stages configuration as JSON array',
122
+ placeholder: 'Configure stages...',
123
+ showInList: false,
124
+ showInDetail: true,
125
+ showInForm: true,
126
+ order: 6,
127
+ columnWidth: 12,
128
+ },
129
+ api: {
130
+ searchable: false,
131
+ sortable: false,
132
+ readOnly: false,
133
+ },
134
+ },
135
+ {
136
+ name: 'dealRottenDays',
137
+ type: 'number',
138
+ required: false,
139
+ display: {
140
+ label: 'Deal Rotten Days',
141
+ description: 'Days until deal is considered stale',
142
+ placeholder: '30',
143
+ showInList: false,
144
+ showInDetail: true,
145
+ showInForm: true,
146
+ order: 7,
147
+ columnWidth: 4,
148
+ },
149
+ api: {
150
+ searchable: false,
151
+ sortable: true,
152
+ readOnly: false,
153
+ },
154
+ },
155
+ {
156
+ name: 'createdAt',
157
+ type: 'datetime',
158
+ required: false,
159
+ display: {
160
+ label: 'Created At',
161
+ description: 'When the pipeline was created',
162
+ showInList: true,
163
+ showInDetail: true,
164
+ showInForm: false,
165
+ order: 98,
166
+ columnWidth: 4,
167
+ },
168
+ api: {
169
+ searchable: false,
170
+ sortable: true,
171
+ readOnly: true,
172
+ },
173
+ },
174
+ {
175
+ name: 'updatedAt',
176
+ type: 'datetime',
177
+ required: false,
178
+ display: {
179
+ label: 'Updated At',
180
+ description: 'When the pipeline was last updated',
181
+ showInList: false,
182
+ showInDetail: true,
183
+ showInForm: false,
184
+ order: 99,
185
+ columnWidth: 4,
186
+ },
187
+ api: {
188
+ searchable: false,
189
+ sortable: true,
190
+ readOnly: true,
191
+ },
192
+ },
193
+ ]
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Pipelines Service
3
+ *
4
+ * Provides data access methods for pipelines.
5
+ * Pipelines is a private entity - users only see pipelines in their team.
6
+ *
7
+ * All methods require authentication (use RLS with userId filter).
8
+ *
9
+ * @module PipelinesService
10
+ */
11
+
12
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
13
+
14
+ // Pipeline stage interface
15
+ export interface PipelineStage {
16
+ id: string
17
+ name: string
18
+ order: number
19
+ probability?: number
20
+ }
21
+
22
+ // Pipeline interface
23
+ export interface Pipeline {
24
+ id: string
25
+ name: string
26
+ description?: string
27
+ isDefault?: boolean
28
+ stages?: PipelineStage[]
29
+ createdAt: string
30
+ updatedAt: string
31
+ }
32
+
33
+ // List options
34
+ export interface PipelineListOptions {
35
+ limit?: number
36
+ offset?: number
37
+ isDefault?: boolean
38
+ orderBy?: 'name' | 'createdAt' | 'updatedAt'
39
+ orderDir?: 'asc' | 'desc'
40
+ teamId?: string
41
+ }
42
+
43
+ // List result
44
+ export interface PipelineListResult {
45
+ pipelines: Pipeline[]
46
+ total: number
47
+ }
48
+
49
+ // Create data
50
+ export interface PipelineCreateData {
51
+ name: string
52
+ description?: string
53
+ isDefault?: boolean
54
+ stages?: PipelineStage[]
55
+ teamId: string
56
+ }
57
+
58
+ // Update data
59
+ export interface PipelineUpdateData {
60
+ name?: string
61
+ description?: string
62
+ isDefault?: boolean
63
+ stages?: PipelineStage[]
64
+ }
65
+
66
+ // Database row type
67
+ interface DbPipeline {
68
+ id: string
69
+ name: string
70
+ description: string | null
71
+ isDefault: boolean | null
72
+ stages: string | null // JSON string
73
+ createdAt: string
74
+ updatedAt: string
75
+ }
76
+
77
+ export class PipelinesService {
78
+ // ============================================
79
+ // READ METHODS
80
+ // ============================================
81
+
82
+ /**
83
+ * Get a pipeline by ID
84
+ */
85
+ static async getById(id: string, userId: string): Promise<Pipeline | null> {
86
+ try {
87
+ if (!id?.trim()) throw new Error('Pipeline ID is required')
88
+ if (!userId?.trim()) throw new Error('User ID is required')
89
+
90
+ const pipeline = await queryOneWithRLS<DbPipeline>(
91
+ `SELECT id, name, description, "isDefault", stages, "createdAt", "updatedAt"
92
+ FROM pipelines WHERE id = $1`,
93
+ [id],
94
+ userId
95
+ )
96
+
97
+ if (!pipeline) return null
98
+
99
+ // Parse stages JSON
100
+ let stages: PipelineStage[] | undefined
101
+ if (pipeline.stages) {
102
+ try {
103
+ stages = JSON.parse(pipeline.stages)
104
+ } catch (error) {
105
+ console.error('Failed to parse pipeline stages:', error)
106
+ stages = undefined
107
+ }
108
+ }
109
+
110
+ return {
111
+ id: pipeline.id,
112
+ name: pipeline.name,
113
+ description: pipeline.description ?? undefined,
114
+ isDefault: pipeline.isDefault ?? undefined,
115
+ stages,
116
+ createdAt: pipeline.createdAt,
117
+ updatedAt: pipeline.updatedAt,
118
+ }
119
+ } catch (error) {
120
+ console.error('PipelinesService.getById error:', error)
121
+ throw new Error(error instanceof Error ? error.message : 'Failed to fetch pipeline')
122
+ }
123
+ }
124
+
125
+ /**
126
+ * List pipelines with pagination and filtering
127
+ */
128
+ static async list(userId: string, options: PipelineListOptions = {}): Promise<PipelineListResult> {
129
+ try {
130
+ if (!userId?.trim()) throw new Error('User ID is required')
131
+
132
+ const {
133
+ limit = 10,
134
+ offset = 0,
135
+ isDefault,
136
+ orderBy = 'createdAt',
137
+ orderDir = 'desc',
138
+ teamId,
139
+ } = options
140
+
141
+ // Build WHERE clause
142
+ const conditions: string[] = []
143
+ const params: unknown[] = []
144
+ let paramIndex = 1
145
+
146
+ if (isDefault !== undefined) {
147
+ conditions.push(`"isDefault" = $${paramIndex++}`)
148
+ params.push(isDefault)
149
+ }
150
+
151
+ if (teamId) {
152
+ conditions.push(`"teamId" = $${paramIndex++}`)
153
+ params.push(teamId)
154
+ }
155
+
156
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
157
+
158
+ // Validate orderBy
159
+ const validOrderBy = ['name', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
160
+ const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
161
+ const orderColumnMap: Record<string, string> = {
162
+ name: 'name',
163
+ createdAt: '"createdAt"',
164
+ updatedAt: '"updatedAt"',
165
+ }
166
+ const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
167
+
168
+ // Get total count
169
+ const countResult = await queryWithRLS<{ count: string }>(
170
+ `SELECT COUNT(*)::text as count FROM pipelines ${whereClause}`,
171
+ params,
172
+ userId
173
+ )
174
+ const total = parseInt(countResult[0]?.count || '0', 10)
175
+
176
+ // Get pipelines
177
+ params.push(limit, offset)
178
+ const pipelines = await queryWithRLS<DbPipeline>(
179
+ `SELECT id, name, description, "isDefault", stages, "createdAt", "updatedAt"
180
+ FROM pipelines ${whereClause}
181
+ ORDER BY ${orderColumn} ${validOrderDir}
182
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
183
+ params,
184
+ userId
185
+ )
186
+
187
+ return {
188
+ pipelines: pipelines.map((pipeline) => {
189
+ // Parse stages JSON
190
+ let stages: PipelineStage[] | undefined
191
+ if (pipeline.stages) {
192
+ try {
193
+ stages = JSON.parse(pipeline.stages)
194
+ } catch (error) {
195
+ console.error('Failed to parse pipeline stages:', error)
196
+ stages = undefined
197
+ }
198
+ }
199
+
200
+ return {
201
+ id: pipeline.id,
202
+ name: pipeline.name,
203
+ description: pipeline.description ?? undefined,
204
+ isDefault: pipeline.isDefault ?? undefined,
205
+ stages,
206
+ createdAt: pipeline.createdAt,
207
+ updatedAt: pipeline.updatedAt,
208
+ }
209
+ }),
210
+ total,
211
+ }
212
+ } catch (error) {
213
+ console.error('PipelinesService.list error:', error)
214
+ throw new Error(error instanceof Error ? error.message : 'Failed to list pipelines')
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Get default pipeline
220
+ */
221
+ static async getDefault(userId: string): Promise<Pipeline | null> {
222
+ const { pipelines } = await this.list(userId, {
223
+ isDefault: true,
224
+ limit: 1,
225
+ })
226
+ return pipelines[0] || null
227
+ }
228
+
229
+ // ============================================
230
+ // WRITE METHODS
231
+ // ============================================
232
+
233
+ /**
234
+ * Create a new pipeline
235
+ */
236
+ static async create(userId: string, data: PipelineCreateData): Promise<Pipeline> {
237
+ try {
238
+ if (!userId?.trim()) throw new Error('User ID is required')
239
+ if (!data.name?.trim()) throw new Error('Pipeline name is required')
240
+ if (!data.teamId?.trim()) throw new Error('Team ID is required')
241
+
242
+ const id = crypto.randomUUID()
243
+ const now = new Date().toISOString()
244
+
245
+ // Serialize stages to JSON
246
+ const stagesJson = data.stages ? JSON.stringify(data.stages) : null
247
+
248
+ const result = await mutateWithRLS<DbPipeline>(
249
+ `INSERT INTO pipelines (id, "userId", "teamId", name, description, "isDefault", stages, "createdAt", "updatedAt")
250
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
251
+ RETURNING id, name, description, "isDefault", stages, "createdAt", "updatedAt"`,
252
+ [
253
+ id,
254
+ userId,
255
+ data.teamId,
256
+ data.name,
257
+ data.description || null,
258
+ data.isDefault || false,
259
+ stagesJson,
260
+ now,
261
+ now,
262
+ ],
263
+ userId
264
+ )
265
+
266
+ if (!result.rows[0]) throw new Error('Failed to create pipeline')
267
+
268
+ const pipeline = result.rows[0]
269
+
270
+ // Parse stages JSON
271
+ let stages: PipelineStage[] | undefined
272
+ if (pipeline.stages) {
273
+ try {
274
+ stages = JSON.parse(pipeline.stages)
275
+ } catch (error) {
276
+ console.error('Failed to parse pipeline stages:', error)
277
+ stages = undefined
278
+ }
279
+ }
280
+
281
+ return {
282
+ id: pipeline.id,
283
+ name: pipeline.name,
284
+ description: pipeline.description ?? undefined,
285
+ isDefault: pipeline.isDefault ?? undefined,
286
+ stages,
287
+ createdAt: pipeline.createdAt,
288
+ updatedAt: pipeline.updatedAt,
289
+ }
290
+ } catch (error) {
291
+ console.error('PipelinesService.create error:', error)
292
+ throw new Error(error instanceof Error ? error.message : 'Failed to create pipeline')
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Update an existing pipeline
298
+ */
299
+ static async update(userId: string, id: string, data: PipelineUpdateData): Promise<Pipeline> {
300
+ try {
301
+ if (!userId?.trim()) throw new Error('User ID is required')
302
+ if (!id?.trim()) throw new Error('Pipeline ID is required')
303
+
304
+ const updates: string[] = []
305
+ const values: unknown[] = []
306
+ let paramIndex = 1
307
+
308
+ if (data.name !== undefined) {
309
+ updates.push(`name = $${paramIndex++}`)
310
+ values.push(data.name)
311
+ }
312
+ if (data.description !== undefined) {
313
+ updates.push(`description = $${paramIndex++}`)
314
+ values.push(data.description || null)
315
+ }
316
+ if (data.isDefault !== undefined) {
317
+ updates.push(`"isDefault" = $${paramIndex++}`)
318
+ values.push(data.isDefault)
319
+ }
320
+ if (data.stages !== undefined) {
321
+ updates.push(`stages = $${paramIndex++}`)
322
+ values.push(data.stages ? JSON.stringify(data.stages) : null)
323
+ }
324
+
325
+ if (updates.length === 0) throw new Error('No fields to update')
326
+
327
+ updates.push(`"updatedAt" = $${paramIndex++}`)
328
+ values.push(new Date().toISOString())
329
+ values.push(id)
330
+
331
+ const result = await mutateWithRLS<DbPipeline>(
332
+ `UPDATE pipelines SET ${updates.join(', ')} WHERE id = $${paramIndex}
333
+ RETURNING id, name, description, "isDefault", stages, "createdAt", "updatedAt"`,
334
+ values,
335
+ userId
336
+ )
337
+
338
+ if (!result.rows[0]) throw new Error('Pipeline not found or update failed')
339
+
340
+ const pipeline = result.rows[0]
341
+
342
+ // Parse stages JSON
343
+ let stages: PipelineStage[] | undefined
344
+ if (pipeline.stages) {
345
+ try {
346
+ stages = JSON.parse(pipeline.stages)
347
+ } catch (error) {
348
+ console.error('Failed to parse pipeline stages:', error)
349
+ stages = undefined
350
+ }
351
+ }
352
+
353
+ return {
354
+ id: pipeline.id,
355
+ name: pipeline.name,
356
+ description: pipeline.description ?? undefined,
357
+ isDefault: pipeline.isDefault ?? undefined,
358
+ stages,
359
+ createdAt: pipeline.createdAt,
360
+ updatedAt: pipeline.updatedAt,
361
+ }
362
+ } catch (error) {
363
+ console.error('PipelinesService.update error:', error)
364
+ throw new Error(error instanceof Error ? error.message : 'Failed to update pipeline')
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Delete a pipeline
370
+ */
371
+ static async delete(userId: string, id: string): Promise<boolean> {
372
+ try {
373
+ if (!userId?.trim()) throw new Error('User ID is required')
374
+ if (!id?.trim()) throw new Error('Pipeline ID is required')
375
+
376
+ const result = await mutateWithRLS(`DELETE FROM pipelines WHERE id = $1`, [id], userId)
377
+ return result.rowCount > 0
378
+ } catch (error) {
379
+ console.error('PipelinesService.delete error:', error)
380
+ throw new Error(error instanceof Error ? error.message : 'Failed to delete pipeline')
381
+ }
382
+ }
383
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Pipeline Service Types
3
+ *
4
+ * Type definitions for the PipelineService.
5
+ * Defines types for sales pipeline management including stage configuration,
6
+ * deal progression tracking, and pipeline customization.
7
+ *
8
+ * @module PipelineTypes
9
+ */
10
+
11
+ // Type literals for select fields
12
+ export type PipelineType = 'sales' | 'support' | 'project' | 'custom'
13
+
14
+ // Pipeline stage interface
15
+ export interface PipelineStage {
16
+ id: string
17
+ name: string
18
+ probability: number
19
+ order: number
20
+ isClosed?: boolean
21
+ isWon?: boolean
22
+ }
23
+
24
+ // Main entity interface
25
+ export interface Pipeline {
26
+ id: string
27
+ teamId: string
28
+ name: string
29
+ description?: string | null
30
+ type?: PipelineType | null
31
+ isDefault?: boolean | null
32
+ isActive?: boolean | null
33
+ stages: PipelineStage[]
34
+ dealRottenDays?: number | null
35
+ createdAt: string
36
+ updatedAt: string
37
+ }
38
+
39
+ // List options
40
+ export interface PipelineListOptions {
41
+ limit?: number
42
+ offset?: number
43
+ teamId?: string
44
+ type?: PipelineType
45
+ isDefault?: boolean
46
+ isActive?: boolean
47
+ orderBy?: 'name' | 'type' | 'isDefault' | 'isActive' | 'createdAt' | 'updatedAt'
48
+ orderDir?: 'asc' | 'desc'
49
+ }
50
+
51
+ // List result
52
+ export interface PipelineListResult {
53
+ pipelines: Pipeline[]
54
+ total: number
55
+ }
56
+
57
+ // Create data (required fields + teamId + optional fields)
58
+ export interface PipelineCreateData {
59
+ name: string
60
+ stages: PipelineStage[]
61
+ teamId: string
62
+ description?: string
63
+ type?: PipelineType
64
+ isDefault?: boolean
65
+ isActive?: boolean
66
+ dealRottenDays?: number
67
+ }
68
+
69
+ // Update data (all fields optional)
70
+ export interface PipelineUpdateData {
71
+ name?: string
72
+ description?: string | null
73
+ type?: PipelineType | null
74
+ isDefault?: boolean | null
75
+ isActive?: boolean | null
76
+ stages?: PipelineStage[]
77
+ dealRottenDays?: number | null
78
+ }