@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.
- package/CRM_PLAN.md +343 -0
- package/about.md +122 -0
- package/config/app.config.ts +185 -0
- package/config/billing.config.ts +187 -0
- package/config/dashboard.config.ts +372 -0
- package/config/dev.config.ts +55 -0
- package/config/features.config.ts +336 -0
- package/config/flows.config.ts +511 -0
- package/config/permissions.config.ts +297 -0
- package/config/theme.config.ts +111 -0
- package/entities/activities/activities.config.ts +61 -0
- package/entities/activities/activities.fields.ts +362 -0
- package/entities/activities/activities.service.ts +503 -0
- package/entities/activities/activities.types.ts +117 -0
- package/entities/activities/messages/en.json +123 -0
- package/entities/activities/messages/es.json +123 -0
- package/entities/activities/migrations/020_activities_table.sql +123 -0
- package/entities/activities/migrations/021_activities_metas.sql +114 -0
- package/entities/activities/migrations/022_activities_sample_data.sql +420 -0
- package/entities/campaigns/campaigns.config.ts +61 -0
- package/entities/campaigns/campaigns.fields.ts +413 -0
- package/entities/campaigns/campaigns.service.ts +426 -0
- package/entities/campaigns/campaigns.types.ts +124 -0
- package/entities/campaigns/messages/en.json +145 -0
- package/entities/campaigns/messages/es.json +145 -0
- package/entities/campaigns/migrations/001_campaigns_table.sql +127 -0
- package/entities/campaigns/migrations/002_campaigns_metas.sql +114 -0
- package/entities/campaigns/migrations/003_campaigns_sample_data.sql +364 -0
- package/entities/companies/companies.config.ts +61 -0
- package/entities/companies/companies.fields.ts +429 -0
- package/entities/companies/companies.service.ts +566 -0
- package/entities/companies/companies.types.ts +125 -0
- package/entities/companies/messages/en.json +146 -0
- package/entities/companies/messages/es.json +146 -0
- package/entities/companies/migrations/001_companies_table.sql +150 -0
- package/entities/companies/migrations/002_companies_metas.sql +114 -0
- package/entities/companies/migrations/003_companies_sample_data.sql +246 -0
- package/entities/contacts/contacts.config.ts +61 -0
- package/entities/contacts/contacts.fields.ts +359 -0
- package/entities/contacts/contacts.service.ts +509 -0
- package/entities/contacts/contacts.types.ts +108 -0
- package/entities/contacts/messages/en.json +117 -0
- package/entities/contacts/messages/es.json +117 -0
- package/entities/contacts/migrations/001_contacts_table.sql +134 -0
- package/entities/contacts/migrations/002_contacts_metas.sql +114 -0
- package/entities/contacts/migrations/003_contacts_sample_data.sql +421 -0
- package/entities/leads/leads.config.ts +61 -0
- package/entities/leads/leads.fields.ts +336 -0
- package/entities/leads/leads.service.ts +496 -0
- package/entities/leads/leads.types.ts +114 -0
- package/entities/leads/messages/en.json +132 -0
- package/entities/leads/messages/es.json +132 -0
- package/entities/leads/migrations/001_leads_table.sql +150 -0
- package/entities/leads/migrations/002_leads_metas.sql +120 -0
- package/entities/leads/migrations/003_leads_sample_data.sql +242 -0
- package/entities/notes/messages/en.json +114 -0
- package/entities/notes/messages/es.json +114 -0
- package/entities/notes/migrations/020_notes_table.sql +118 -0
- package/entities/notes/migrations/021_notes_metas.sql +114 -0
- package/entities/notes/migrations/022_notes_sample_data.sql +275 -0
- package/entities/notes/notes.config.ts +61 -0
- package/entities/notes/notes.fields.ts +283 -0
- package/entities/notes/notes.service.ts +320 -0
- package/entities/notes/notes.types.ts +102 -0
- package/entities/opportunities/messages/en.json +107 -0
- package/entities/opportunities/messages/es.json +107 -0
- package/entities/opportunities/migrations/010_opportunities_table.sql +145 -0
- package/entities/opportunities/migrations/011_opportunities_metas.sql +114 -0
- package/entities/opportunities/migrations/012_opportunities_sample_data.sql +438 -0
- package/entities/opportunities/opportunities.config.ts +61 -0
- package/entities/opportunities/opportunities.fields.ts +416 -0
- package/entities/opportunities/opportunities.service.ts +525 -0
- package/entities/opportunities/opportunities.types.ts +135 -0
- package/entities/pipelines/messages/en.json +115 -0
- package/entities/pipelines/messages/es.json +115 -0
- package/entities/pipelines/migrations/001_pipelines_table.sql +106 -0
- package/entities/pipelines/migrations/002_pipelines_metas.sql +114 -0
- package/entities/pipelines/migrations/003_pipelines_sample_data.sql +91 -0
- package/entities/pipelines/pipelines.config.ts +62 -0
- package/entities/pipelines/pipelines.fields.ts +193 -0
- package/entities/pipelines/pipelines.service.ts +383 -0
- package/entities/pipelines/pipelines.types.ts +78 -0
- package/entities/products/messages/en.json +135 -0
- package/entities/products/messages/es.json +135 -0
- package/entities/products/migrations/001_products_table.sql +117 -0
- package/entities/products/migrations/002_products_metas.sql +114 -0
- package/entities/products/migrations/003_products_sample_data.sql +247 -0
- package/entities/products/products.config.ts +62 -0
- package/entities/products/products.fields.ts +361 -0
- package/entities/products/products.service.ts +437 -0
- package/entities/products/products.types.ts +125 -0
- package/lib/crm-constants.ts +77 -0
- package/lib/crm-utils.ts +185 -0
- package/lib/selectors.ts +333 -0
- package/messages/en.json +131 -0
- package/messages/es.json +131 -0
- package/migrations/999_theme_sample_data.sql +473 -0
- package/package.json +18 -0
- package/pendings.md +205 -0
- package/permissions-matrix.md +216 -0
- package/styles/components.css +414 -0
- package/styles/crm-theme.css +358 -0
- package/styles/globals.css +576 -0
- package/styles/variables.css +111 -0
- package/templates/dashboard/(main)/activities/components/ActivityCard.tsx +169 -0
- package/templates/dashboard/(main)/activities/components/ActivityTimeline.tsx +165 -0
- package/templates/dashboard/(main)/activities/page.tsx +297 -0
- package/templates/dashboard/(main)/campaigns/page.tsx +373 -0
- package/templates/dashboard/(main)/companies/page.tsx +296 -0
- package/templates/dashboard/(main)/contacts/page.tsx +347 -0
- package/templates/dashboard/(main)/layout.tsx +98 -0
- package/templates/dashboard/(main)/leads/page.tsx +335 -0
- package/templates/dashboard/(main)/opportunities/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/opportunities/create/page.tsx +94 -0
- package/templates/dashboard/(main)/opportunities/page.tsx +350 -0
- package/templates/dashboard/(main)/pipelines/[id]/edit/page.tsx +95 -0
- package/templates/dashboard/(main)/pipelines/[id]/page.tsx +143 -0
- package/templates/dashboard/(main)/pipelines/create/page.tsx +94 -0
- package/templates/dashboard/(main)/pipelines/page.tsx +234 -0
- package/templates/dashboard/(main)/products/[id]/edit/page.tsx +97 -0
- package/templates/dashboard/(main)/products/[id]/page.tsx +509 -0
- package/templates/dashboard/(main)/products/create/page.tsx +96 -0
- package/templates/dashboard/(main)/products/page.tsx +308 -0
- package/templates/shared/ActionButtons.tsx +41 -0
- package/templates/shared/CRMDashboard.tsx +519 -0
- package/templates/shared/CRMDataTable.tsx +441 -0
- package/templates/shared/CRMMetricCard.tsx +76 -0
- package/templates/shared/CRMMobileNav.tsx +172 -0
- package/templates/shared/CRMSidebar.tsx +346 -0
- package/templates/shared/CRMTopBar.tsx +265 -0
- package/templates/shared/DealCard.tsx +123 -0
- package/templates/shared/EntityCard.tsx +58 -0
- package/templates/shared/OpportunityForm.tsx +649 -0
- package/templates/shared/PipelineForm.tsx +367 -0
- package/templates/shared/PipelineKanban.tsx +194 -0
- package/templates/shared/QuickFilters.tsx +47 -0
- package/templates/shared/StageColumn.tsx +175 -0
- package/templates/shared/StageSelect.tsx +177 -0
- package/templates/shared/StagesRepeater.tsx +317 -0
- 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
|
+
}
|