@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,503 @@
1
+ /**
2
+ * Activities Service
3
+ *
4
+ * Provides data access methods for activities.
5
+ * Activities is a private entity - users only see activities in their team.
6
+ *
7
+ * All methods require authentication (use RLS with userId filter).
8
+ *
9
+ * @module ActivitiesService
10
+ */
11
+
12
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
13
+
14
+ // Activity type
15
+ export type ActivityType = 'call' | 'email' | 'meeting' | 'task'
16
+
17
+ // Activity status type
18
+ export type ActivityStatus = 'scheduled' | 'completed' | 'cancelled'
19
+
20
+ // Activity interface
21
+ export interface Activity {
22
+ id: string
23
+ type: ActivityType
24
+ subject: string
25
+ description?: string
26
+ status: ActivityStatus
27
+ dueDate?: string
28
+ completedAt?: string
29
+ assignedTo?: string
30
+ relatedTo?: string
31
+ relatedId?: string
32
+ createdAt: string
33
+ updatedAt: string
34
+ }
35
+
36
+ // List options
37
+ export interface ActivityListOptions {
38
+ limit?: number
39
+ offset?: number
40
+ type?: ActivityType
41
+ status?: ActivityStatus
42
+ assignedTo?: string
43
+ relatedTo?: string
44
+ relatedId?: string
45
+ orderBy?: 'subject' | 'dueDate' | 'completedAt' | 'createdAt' | 'updatedAt'
46
+ orderDir?: 'asc' | 'desc'
47
+ teamId?: string
48
+ }
49
+
50
+ // List result
51
+ export interface ActivityListResult {
52
+ activities: Activity[]
53
+ total: number
54
+ }
55
+
56
+ // Create data
57
+ export interface ActivityCreateData {
58
+ type: ActivityType
59
+ subject: string
60
+ description?: string
61
+ status?: ActivityStatus
62
+ dueDate?: string
63
+ completedAt?: string
64
+ assignedTo?: string
65
+ relatedTo?: string
66
+ relatedId?: string
67
+ teamId: string
68
+ }
69
+
70
+ // Update data
71
+ export interface ActivityUpdateData {
72
+ type?: ActivityType
73
+ subject?: string
74
+ description?: string
75
+ status?: ActivityStatus
76
+ dueDate?: string
77
+ completedAt?: string
78
+ assignedTo?: string
79
+ relatedTo?: string
80
+ relatedId?: string
81
+ }
82
+
83
+ // Database row type
84
+ interface DbActivity {
85
+ id: string
86
+ type: ActivityType
87
+ subject: string
88
+ description: string | null
89
+ status: ActivityStatus
90
+ dueDate: string | null
91
+ completedAt: string | null
92
+ assignedTo: string | null
93
+ relatedTo: string | null
94
+ relatedId: string | null
95
+ createdAt: string
96
+ updatedAt: string
97
+ }
98
+
99
+ export class ActivitiesService {
100
+ // ============================================
101
+ // READ METHODS
102
+ // ============================================
103
+
104
+ /**
105
+ * Get an activity by ID
106
+ */
107
+ static async getById(id: string, userId: string): Promise<Activity | null> {
108
+ try {
109
+ if (!id?.trim()) throw new Error('Activity ID is required')
110
+ if (!userId?.trim()) throw new Error('User ID is required')
111
+
112
+ const activity = await queryOneWithRLS<DbActivity>(
113
+ `SELECT id, type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt"
114
+ FROM activities WHERE id = $1`,
115
+ [id],
116
+ userId
117
+ )
118
+
119
+ if (!activity) return null
120
+
121
+ return {
122
+ id: activity.id,
123
+ type: activity.type,
124
+ subject: activity.subject,
125
+ description: activity.description ?? undefined,
126
+ status: activity.status,
127
+ dueDate: activity.dueDate ?? undefined,
128
+ completedAt: activity.completedAt ?? undefined,
129
+ assignedTo: activity.assignedTo ?? undefined,
130
+ relatedTo: activity.relatedTo ?? undefined,
131
+ relatedId: activity.relatedId ?? undefined,
132
+ createdAt: activity.createdAt,
133
+ updatedAt: activity.updatedAt,
134
+ }
135
+ } catch (error) {
136
+ console.error('ActivitiesService.getById error:', error)
137
+ throw new Error(error instanceof Error ? error.message : 'Failed to fetch activity')
138
+ }
139
+ }
140
+
141
+ /**
142
+ * List activities with pagination and filtering
143
+ */
144
+ static async list(userId: string, options: ActivityListOptions = {}): Promise<ActivityListResult> {
145
+ try {
146
+ if (!userId?.trim()) throw new Error('User ID is required')
147
+
148
+ const {
149
+ limit = 10,
150
+ offset = 0,
151
+ type,
152
+ status,
153
+ assignedTo,
154
+ relatedTo,
155
+ relatedId,
156
+ orderBy = 'createdAt',
157
+ orderDir = 'desc',
158
+ teamId,
159
+ } = options
160
+
161
+ // Build WHERE clause
162
+ const conditions: string[] = []
163
+ const params: unknown[] = []
164
+ let paramIndex = 1
165
+
166
+ if (type) {
167
+ conditions.push(`type = $${paramIndex++}`)
168
+ params.push(type)
169
+ }
170
+
171
+ if (status) {
172
+ conditions.push(`status = $${paramIndex++}`)
173
+ params.push(status)
174
+ }
175
+
176
+ if (assignedTo) {
177
+ conditions.push(`"assignedTo" = $${paramIndex++}`)
178
+ params.push(assignedTo)
179
+ }
180
+
181
+ if (relatedTo) {
182
+ conditions.push(`"relatedTo" = $${paramIndex++}`)
183
+ params.push(relatedTo)
184
+ }
185
+
186
+ if (relatedId) {
187
+ conditions.push(`"relatedId" = $${paramIndex++}`)
188
+ params.push(relatedId)
189
+ }
190
+
191
+ if (teamId) {
192
+ conditions.push(`"teamId" = $${paramIndex++}`)
193
+ params.push(teamId)
194
+ }
195
+
196
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
197
+
198
+ // Validate orderBy
199
+ const validOrderBy = ['subject', 'dueDate', 'completedAt', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
200
+ const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
201
+ const orderColumnMap: Record<string, string> = {
202
+ subject: 'subject',
203
+ dueDate: '"dueDate"',
204
+ completedAt: '"completedAt"',
205
+ createdAt: '"createdAt"',
206
+ updatedAt: '"updatedAt"',
207
+ }
208
+ const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
209
+
210
+ // Get total count
211
+ const countResult = await queryWithRLS<{ count: string }>(
212
+ `SELECT COUNT(*)::text as count FROM activities ${whereClause}`,
213
+ params,
214
+ userId
215
+ )
216
+ const total = parseInt(countResult[0]?.count || '0', 10)
217
+
218
+ // Get activities
219
+ params.push(limit, offset)
220
+ const activities = await queryWithRLS<DbActivity>(
221
+ `SELECT id, type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt"
222
+ FROM activities ${whereClause}
223
+ ORDER BY ${orderColumn} ${validOrderDir}
224
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
225
+ params,
226
+ userId
227
+ )
228
+
229
+ return {
230
+ activities: activities.map((activity) => ({
231
+ id: activity.id,
232
+ type: activity.type,
233
+ subject: activity.subject,
234
+ description: activity.description ?? undefined,
235
+ status: activity.status,
236
+ dueDate: activity.dueDate ?? undefined,
237
+ completedAt: activity.completedAt ?? undefined,
238
+ assignedTo: activity.assignedTo ?? undefined,
239
+ relatedTo: activity.relatedTo ?? undefined,
240
+ relatedId: activity.relatedId ?? undefined,
241
+ createdAt: activity.createdAt,
242
+ updatedAt: activity.updatedAt,
243
+ })),
244
+ total,
245
+ }
246
+ } catch (error) {
247
+ console.error('ActivitiesService.list error:', error)
248
+ throw new Error(error instanceof Error ? error.message : 'Failed to list activities')
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Get activities by type
254
+ */
255
+ static async getByType(userId: string, type: ActivityType, limit = 50): Promise<Activity[]> {
256
+ const { activities } = await this.list(userId, {
257
+ type,
258
+ limit,
259
+ orderBy: 'dueDate',
260
+ orderDir: 'asc',
261
+ })
262
+ return activities
263
+ }
264
+
265
+ /**
266
+ * Get activities by status
267
+ */
268
+ static async getByStatus(userId: string, status: ActivityStatus, limit = 50): Promise<Activity[]> {
269
+ const { activities } = await this.list(userId, {
270
+ status,
271
+ limit,
272
+ orderBy: 'dueDate',
273
+ orderDir: 'asc',
274
+ })
275
+ return activities
276
+ }
277
+
278
+ /**
279
+ * Get activities related to an entity
280
+ */
281
+ static async getByRelatedEntity(userId: string, relatedTo: string, relatedId: string, limit = 50): Promise<Activity[]> {
282
+ const { activities } = await this.list(userId, {
283
+ relatedTo,
284
+ relatedId,
285
+ limit,
286
+ orderBy: 'createdAt',
287
+ orderDir: 'desc',
288
+ })
289
+ return activities
290
+ }
291
+
292
+ /**
293
+ * Get overdue activities
294
+ */
295
+ static async getOverdue(userId: string, limit = 20): Promise<Activity[]> {
296
+ try {
297
+ if (!userId?.trim()) throw new Error('User ID is required')
298
+
299
+ const activities = await queryWithRLS<DbActivity>(
300
+ `SELECT id, type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt"
301
+ FROM activities
302
+ WHERE status = 'scheduled'
303
+ AND "dueDate" IS NOT NULL
304
+ AND "dueDate" < CURRENT_DATE
305
+ ORDER BY "dueDate" ASC
306
+ LIMIT $1`,
307
+ [limit],
308
+ userId
309
+ )
310
+
311
+ return activities.map((activity) => ({
312
+ id: activity.id,
313
+ type: activity.type,
314
+ subject: activity.subject,
315
+ description: activity.description ?? undefined,
316
+ status: activity.status,
317
+ dueDate: activity.dueDate ?? undefined,
318
+ completedAt: activity.completedAt ?? undefined,
319
+ assignedTo: activity.assignedTo ?? undefined,
320
+ relatedTo: activity.relatedTo ?? undefined,
321
+ relatedId: activity.relatedId ?? undefined,
322
+ createdAt: activity.createdAt,
323
+ updatedAt: activity.updatedAt,
324
+ }))
325
+ } catch (error) {
326
+ console.error('ActivitiesService.getOverdue error:', error)
327
+ throw new Error(error instanceof Error ? error.message : 'Failed to fetch overdue activities')
328
+ }
329
+ }
330
+
331
+ // ============================================
332
+ // WRITE METHODS
333
+ // ============================================
334
+
335
+ /**
336
+ * Create a new activity
337
+ */
338
+ static async create(userId: string, data: ActivityCreateData): Promise<Activity> {
339
+ try {
340
+ if (!userId?.trim()) throw new Error('User ID is required')
341
+ if (!data.subject?.trim()) throw new Error('Subject is required')
342
+ if (!data.teamId?.trim()) throw new Error('Team ID is required')
343
+
344
+ const id = crypto.randomUUID()
345
+ const now = new Date().toISOString()
346
+
347
+ const result = await mutateWithRLS<DbActivity>(
348
+ `INSERT INTO activities (id, "userId", "teamId", type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt")
349
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
350
+ RETURNING id, type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt"`,
351
+ [
352
+ id,
353
+ userId,
354
+ data.teamId,
355
+ data.type,
356
+ data.subject,
357
+ data.description || null,
358
+ data.status || 'scheduled',
359
+ data.dueDate || null,
360
+ data.completedAt || null,
361
+ data.assignedTo || null,
362
+ data.relatedTo || null,
363
+ data.relatedId || null,
364
+ now,
365
+ now,
366
+ ],
367
+ userId
368
+ )
369
+
370
+ if (!result.rows[0]) throw new Error('Failed to create activity')
371
+
372
+ const activity = result.rows[0]
373
+ return {
374
+ id: activity.id,
375
+ type: activity.type,
376
+ subject: activity.subject,
377
+ description: activity.description ?? undefined,
378
+ status: activity.status,
379
+ dueDate: activity.dueDate ?? undefined,
380
+ completedAt: activity.completedAt ?? undefined,
381
+ assignedTo: activity.assignedTo ?? undefined,
382
+ relatedTo: activity.relatedTo ?? undefined,
383
+ relatedId: activity.relatedId ?? undefined,
384
+ createdAt: activity.createdAt,
385
+ updatedAt: activity.updatedAt,
386
+ }
387
+ } catch (error) {
388
+ console.error('ActivitiesService.create error:', error)
389
+ throw new Error(error instanceof Error ? error.message : 'Failed to create activity')
390
+ }
391
+ }
392
+
393
+ /**
394
+ * Update an existing activity
395
+ */
396
+ static async update(userId: string, id: string, data: ActivityUpdateData): Promise<Activity> {
397
+ try {
398
+ if (!userId?.trim()) throw new Error('User ID is required')
399
+ if (!id?.trim()) throw new Error('Activity ID is required')
400
+
401
+ const updates: string[] = []
402
+ const values: unknown[] = []
403
+ let paramIndex = 1
404
+
405
+ if (data.type !== undefined) {
406
+ updates.push(`type = $${paramIndex++}`)
407
+ values.push(data.type)
408
+ }
409
+ if (data.subject !== undefined) {
410
+ updates.push(`subject = $${paramIndex++}`)
411
+ values.push(data.subject)
412
+ }
413
+ if (data.description !== undefined) {
414
+ updates.push(`description = $${paramIndex++}`)
415
+ values.push(data.description || null)
416
+ }
417
+ if (data.status !== undefined) {
418
+ updates.push(`status = $${paramIndex++}`)
419
+ values.push(data.status)
420
+ }
421
+ if (data.dueDate !== undefined) {
422
+ updates.push(`"dueDate" = $${paramIndex++}`)
423
+ values.push(data.dueDate || null)
424
+ }
425
+ if (data.completedAt !== undefined) {
426
+ updates.push(`"completedAt" = $${paramIndex++}`)
427
+ values.push(data.completedAt || null)
428
+ }
429
+ if (data.assignedTo !== undefined) {
430
+ updates.push(`"assignedTo" = $${paramIndex++}`)
431
+ values.push(data.assignedTo || null)
432
+ }
433
+ if (data.relatedTo !== undefined) {
434
+ updates.push(`"relatedTo" = $${paramIndex++}`)
435
+ values.push(data.relatedTo || null)
436
+ }
437
+ if (data.relatedId !== undefined) {
438
+ updates.push(`"relatedId" = $${paramIndex++}`)
439
+ values.push(data.relatedId || null)
440
+ }
441
+
442
+ if (updates.length === 0) throw new Error('No fields to update')
443
+
444
+ updates.push(`"updatedAt" = $${paramIndex++}`)
445
+ values.push(new Date().toISOString())
446
+ values.push(id)
447
+
448
+ const result = await mutateWithRLS<DbActivity>(
449
+ `UPDATE activities SET ${updates.join(', ')} WHERE id = $${paramIndex}
450
+ RETURNING id, type, subject, description, status, "dueDate", "completedAt", "assignedTo", "relatedTo", "relatedId", "createdAt", "updatedAt"`,
451
+ values,
452
+ userId
453
+ )
454
+
455
+ if (!result.rows[0]) throw new Error('Activity not found or update failed')
456
+
457
+ const activity = result.rows[0]
458
+ return {
459
+ id: activity.id,
460
+ type: activity.type,
461
+ subject: activity.subject,
462
+ description: activity.description ?? undefined,
463
+ status: activity.status,
464
+ dueDate: activity.dueDate ?? undefined,
465
+ completedAt: activity.completedAt ?? undefined,
466
+ assignedTo: activity.assignedTo ?? undefined,
467
+ relatedTo: activity.relatedTo ?? undefined,
468
+ relatedId: activity.relatedId ?? undefined,
469
+ createdAt: activity.createdAt,
470
+ updatedAt: activity.updatedAt,
471
+ }
472
+ } catch (error) {
473
+ console.error('ActivitiesService.update error:', error)
474
+ throw new Error(error instanceof Error ? error.message : 'Failed to update activity')
475
+ }
476
+ }
477
+
478
+ /**
479
+ * Delete an activity
480
+ */
481
+ static async delete(userId: string, id: string): Promise<boolean> {
482
+ try {
483
+ if (!userId?.trim()) throw new Error('User ID is required')
484
+ if (!id?.trim()) throw new Error('Activity ID is required')
485
+
486
+ const result = await mutateWithRLS(`DELETE FROM activities WHERE id = $1`, [id], userId)
487
+ return result.rowCount > 0
488
+ } catch (error) {
489
+ console.error('ActivitiesService.delete error:', error)
490
+ throw new Error(error instanceof Error ? error.message : 'Failed to delete activity')
491
+ }
492
+ }
493
+
494
+ /**
495
+ * Mark an activity as completed
496
+ */
497
+ static async complete(userId: string, id: string): Promise<Activity> {
498
+ return this.update(userId, id, {
499
+ status: 'completed',
500
+ completedAt: new Date().toISOString(),
501
+ })
502
+ }
503
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Activity Service Types
3
+ *
4
+ * Type definitions for the ActivityService.
5
+ * Defines types for activity tracking including calls, meetings, tasks,
6
+ * and their relationships to contacts, companies, and opportunities.
7
+ *
8
+ * @module ActivityTypes
9
+ */
10
+
11
+ // Type literals for select fields
12
+ export type ActivityType =
13
+ | 'call'
14
+ | 'email'
15
+ | 'meeting'
16
+ | 'task'
17
+ | 'note'
18
+ | 'demo'
19
+ | 'presentation'
20
+
21
+ export type ActivityStatus =
22
+ | 'scheduled'
23
+ | 'in_progress'
24
+ | 'completed'
25
+ | 'cancelled'
26
+ | 'overdue'
27
+
28
+ export type ActivityPriority = 'low' | 'medium' | 'high' | 'urgent'
29
+
30
+ // Main entity interface
31
+ export interface Activity {
32
+ id: string
33
+ teamId: string
34
+ type: ActivityType
35
+ subject: string
36
+ description?: string | null
37
+ status?: ActivityStatus | null
38
+ priority?: ActivityPriority | null
39
+ dueDate?: string | null
40
+ completedAt?: string | null
41
+ duration?: number | null
42
+ outcome?: string | null
43
+ location?: string | null
44
+ contactId?: string | null
45
+ companyId?: string | null
46
+ opportunityId?: string | null
47
+ assignedTo?: string | null
48
+ createdAt: string
49
+ updatedAt: string
50
+ }
51
+
52
+ // List options
53
+ export interface ActivityListOptions {
54
+ limit?: number
55
+ offset?: number
56
+ teamId?: string
57
+ type?: ActivityType
58
+ status?: ActivityStatus
59
+ priority?: ActivityPriority
60
+ contactId?: string
61
+ companyId?: string
62
+ opportunityId?: string
63
+ assignedTo?: string
64
+ orderBy?:
65
+ | 'type'
66
+ | 'subject'
67
+ | 'status'
68
+ | 'priority'
69
+ | 'dueDate'
70
+ | 'completedAt'
71
+ | 'duration'
72
+ | 'createdAt'
73
+ | 'updatedAt'
74
+ orderDir?: 'asc' | 'desc'
75
+ }
76
+
77
+ // List result
78
+ export interface ActivityListResult {
79
+ activities: Activity[]
80
+ total: number
81
+ }
82
+
83
+ // Create data (required fields + teamId + optional fields)
84
+ export interface ActivityCreateData {
85
+ type: ActivityType
86
+ subject: string
87
+ teamId: string
88
+ description?: string
89
+ status?: ActivityStatus
90
+ priority?: ActivityPriority
91
+ dueDate?: string
92
+ duration?: number
93
+ outcome?: string
94
+ location?: string
95
+ contactId?: string
96
+ companyId?: string
97
+ opportunityId?: string
98
+ assignedTo?: string
99
+ }
100
+
101
+ // Update data (all fields optional)
102
+ export interface ActivityUpdateData {
103
+ type?: ActivityType
104
+ subject?: string
105
+ description?: string | null
106
+ status?: ActivityStatus | null
107
+ priority?: ActivityPriority | null
108
+ dueDate?: string | null
109
+ completedAt?: string | null
110
+ duration?: number | null
111
+ outcome?: string | null
112
+ location?: string | null
113
+ contactId?: string | null
114
+ companyId?: string | null
115
+ opportunityId?: string | null
116
+ assignedTo?: string | null
117
+ }