@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,437 @@
1
+ /**
2
+ * Products Service
3
+ *
4
+ * Provides data access methods for products.
5
+ * Products is a private entity - users only see products in their team.
6
+ *
7
+ * All methods require authentication (use RLS with userId filter).
8
+ *
9
+ * @module ProductsService
10
+ */
11
+
12
+ import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
13
+
14
+ // Product interface
15
+ export interface Product {
16
+ id: string
17
+ name: string
18
+ sku?: string
19
+ description?: string
20
+ price: number
21
+ currency?: string
22
+ category?: string
23
+ isActive?: boolean
24
+ createdAt: string
25
+ updatedAt: string
26
+ }
27
+
28
+ // List options
29
+ export interface ProductListOptions {
30
+ limit?: number
31
+ offset?: number
32
+ category?: string
33
+ isActive?: boolean
34
+ minPrice?: number
35
+ maxPrice?: number
36
+ orderBy?: 'name' | 'price' | 'sku' | 'createdAt' | 'updatedAt'
37
+ orderDir?: 'asc' | 'desc'
38
+ teamId?: string
39
+ }
40
+
41
+ // List result
42
+ export interface ProductListResult {
43
+ products: Product[]
44
+ total: number
45
+ }
46
+
47
+ // Create data
48
+ export interface ProductCreateData {
49
+ name: string
50
+ sku?: string
51
+ description?: string
52
+ price: number
53
+ currency?: string
54
+ category?: string
55
+ isActive?: boolean
56
+ teamId: string
57
+ }
58
+
59
+ // Update data
60
+ export interface ProductUpdateData {
61
+ name?: string
62
+ sku?: string
63
+ description?: string
64
+ price?: number
65
+ currency?: string
66
+ category?: string
67
+ isActive?: boolean
68
+ }
69
+
70
+ // Database row type
71
+ interface DbProduct {
72
+ id: string
73
+ name: string
74
+ sku: string | null
75
+ description: string | null
76
+ price: number
77
+ currency: string | null
78
+ category: string | null
79
+ isActive: boolean | null
80
+ createdAt: string
81
+ updatedAt: string
82
+ }
83
+
84
+ export class ProductsService {
85
+ // ============================================
86
+ // READ METHODS
87
+ // ============================================
88
+
89
+ /**
90
+ * Get a product by ID
91
+ */
92
+ static async getById(id: string, userId: string): Promise<Product | null> {
93
+ try {
94
+ if (!id?.trim()) throw new Error('Product ID is required')
95
+ if (!userId?.trim()) throw new Error('User ID is required')
96
+
97
+ const product = await queryOneWithRLS<DbProduct>(
98
+ `SELECT id, name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt"
99
+ FROM products WHERE id = $1`,
100
+ [id],
101
+ userId
102
+ )
103
+
104
+ if (!product) return null
105
+
106
+ return {
107
+ id: product.id,
108
+ name: product.name,
109
+ sku: product.sku ?? undefined,
110
+ description: product.description ?? undefined,
111
+ price: product.price,
112
+ currency: product.currency ?? undefined,
113
+ category: product.category ?? undefined,
114
+ isActive: product.isActive ?? undefined,
115
+ createdAt: product.createdAt,
116
+ updatedAt: product.updatedAt,
117
+ }
118
+ } catch (error) {
119
+ console.error('ProductsService.getById error:', error)
120
+ throw new Error(error instanceof Error ? error.message : 'Failed to fetch product')
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Get a product by SKU
126
+ */
127
+ static async getBySku(sku: string, userId: string): Promise<Product | null> {
128
+ try {
129
+ if (!sku?.trim()) throw new Error('SKU is required')
130
+ if (!userId?.trim()) throw new Error('User ID is required')
131
+
132
+ const product = await queryOneWithRLS<DbProduct>(
133
+ `SELECT id, name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt"
134
+ FROM products WHERE sku = $1`,
135
+ [sku],
136
+ userId
137
+ )
138
+
139
+ if (!product) return null
140
+
141
+ return {
142
+ id: product.id,
143
+ name: product.name,
144
+ sku: product.sku ?? undefined,
145
+ description: product.description ?? undefined,
146
+ price: product.price,
147
+ currency: product.currency ?? undefined,
148
+ category: product.category ?? undefined,
149
+ isActive: product.isActive ?? undefined,
150
+ createdAt: product.createdAt,
151
+ updatedAt: product.updatedAt,
152
+ }
153
+ } catch (error) {
154
+ console.error('ProductsService.getBySku error:', error)
155
+ throw new Error(error instanceof Error ? error.message : 'Failed to fetch product')
156
+ }
157
+ }
158
+
159
+ /**
160
+ * List products with pagination and filtering
161
+ */
162
+ static async list(userId: string, options: ProductListOptions = {}): Promise<ProductListResult> {
163
+ try {
164
+ if (!userId?.trim()) throw new Error('User ID is required')
165
+
166
+ const {
167
+ limit = 10,
168
+ offset = 0,
169
+ category,
170
+ isActive,
171
+ minPrice,
172
+ maxPrice,
173
+ orderBy = 'createdAt',
174
+ orderDir = 'desc',
175
+ teamId,
176
+ } = options
177
+
178
+ // Build WHERE clause
179
+ const conditions: string[] = []
180
+ const params: unknown[] = []
181
+ let paramIndex = 1
182
+
183
+ if (category) {
184
+ conditions.push(`category = $${paramIndex++}`)
185
+ params.push(category)
186
+ }
187
+
188
+ if (isActive !== undefined) {
189
+ conditions.push(`"isActive" = $${paramIndex++}`)
190
+ params.push(isActive)
191
+ }
192
+
193
+ if (minPrice !== undefined) {
194
+ conditions.push(`price >= $${paramIndex++}`)
195
+ params.push(minPrice)
196
+ }
197
+
198
+ if (maxPrice !== undefined) {
199
+ conditions.push(`price <= $${paramIndex++}`)
200
+ params.push(maxPrice)
201
+ }
202
+
203
+ if (teamId) {
204
+ conditions.push(`"teamId" = $${paramIndex++}`)
205
+ params.push(teamId)
206
+ }
207
+
208
+ const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
209
+
210
+ // Validate orderBy
211
+ const validOrderBy = ['name', 'price', 'sku', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
212
+ const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
213
+ const orderColumnMap: Record<string, string> = {
214
+ name: 'name',
215
+ price: 'price',
216
+ sku: 'sku',
217
+ createdAt: '"createdAt"',
218
+ updatedAt: '"updatedAt"',
219
+ }
220
+ const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
221
+
222
+ // Get total count
223
+ const countResult = await queryWithRLS<{ count: string }>(
224
+ `SELECT COUNT(*)::text as count FROM products ${whereClause}`,
225
+ params,
226
+ userId
227
+ )
228
+ const total = parseInt(countResult[0]?.count || '0', 10)
229
+
230
+ // Get products
231
+ params.push(limit, offset)
232
+ const products = await queryWithRLS<DbProduct>(
233
+ `SELECT id, name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt"
234
+ FROM products ${whereClause}
235
+ ORDER BY ${orderColumn} ${validOrderDir}
236
+ LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
237
+ params,
238
+ userId
239
+ )
240
+
241
+ return {
242
+ products: products.map((product) => ({
243
+ id: product.id,
244
+ name: product.name,
245
+ sku: product.sku ?? undefined,
246
+ description: product.description ?? undefined,
247
+ price: product.price,
248
+ currency: product.currency ?? undefined,
249
+ category: product.category ?? undefined,
250
+ isActive: product.isActive ?? undefined,
251
+ createdAt: product.createdAt,
252
+ updatedAt: product.updatedAt,
253
+ })),
254
+ total,
255
+ }
256
+ } catch (error) {
257
+ console.error('ProductsService.list error:', error)
258
+ throw new Error(error instanceof Error ? error.message : 'Failed to list products')
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Get active products
264
+ */
265
+ static async getActive(userId: string, limit = 50): Promise<Product[]> {
266
+ const { products } = await this.list(userId, {
267
+ isActive: true,
268
+ limit,
269
+ orderBy: 'name',
270
+ orderDir: 'asc',
271
+ })
272
+ return products
273
+ }
274
+
275
+ /**
276
+ * Get products by category
277
+ */
278
+ static async getByCategory(userId: string, category: string, limit = 50): Promise<Product[]> {
279
+ const { products } = await this.list(userId, {
280
+ category,
281
+ limit,
282
+ orderBy: 'name',
283
+ orderDir: 'asc',
284
+ })
285
+ return products
286
+ }
287
+
288
+ // ============================================
289
+ // WRITE METHODS
290
+ // ============================================
291
+
292
+ /**
293
+ * Create a new product
294
+ */
295
+ static async create(userId: string, data: ProductCreateData): Promise<Product> {
296
+ try {
297
+ if (!userId?.trim()) throw new Error('User ID is required')
298
+ if (!data.name?.trim()) throw new Error('Product name is required')
299
+ if (data.price === undefined || data.price < 0) throw new Error('Valid price is required')
300
+ if (!data.teamId?.trim()) throw new Error('Team ID is required')
301
+
302
+ const id = crypto.randomUUID()
303
+ const now = new Date().toISOString()
304
+
305
+ const result = await mutateWithRLS<DbProduct>(
306
+ `INSERT INTO products (id, "userId", "teamId", name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt")
307
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
308
+ RETURNING id, name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt"`,
309
+ [
310
+ id,
311
+ userId,
312
+ data.teamId,
313
+ data.name,
314
+ data.sku || null,
315
+ data.description || null,
316
+ data.price,
317
+ data.currency || 'USD',
318
+ data.category || null,
319
+ data.isActive !== undefined ? data.isActive : true,
320
+ now,
321
+ now,
322
+ ],
323
+ userId
324
+ )
325
+
326
+ if (!result.rows[0]) throw new Error('Failed to create product')
327
+
328
+ const product = result.rows[0]
329
+ return {
330
+ id: product.id,
331
+ name: product.name,
332
+ sku: product.sku ?? undefined,
333
+ description: product.description ?? undefined,
334
+ price: product.price,
335
+ currency: product.currency ?? undefined,
336
+ category: product.category ?? undefined,
337
+ isActive: product.isActive ?? undefined,
338
+ createdAt: product.createdAt,
339
+ updatedAt: product.updatedAt,
340
+ }
341
+ } catch (error) {
342
+ console.error('ProductsService.create error:', error)
343
+ throw new Error(error instanceof Error ? error.message : 'Failed to create product')
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Update an existing product
349
+ */
350
+ static async update(userId: string, id: string, data: ProductUpdateData): Promise<Product> {
351
+ try {
352
+ if (!userId?.trim()) throw new Error('User ID is required')
353
+ if (!id?.trim()) throw new Error('Product ID is required')
354
+
355
+ const updates: string[] = []
356
+ const values: unknown[] = []
357
+ let paramIndex = 1
358
+
359
+ if (data.name !== undefined) {
360
+ updates.push(`name = $${paramIndex++}`)
361
+ values.push(data.name)
362
+ }
363
+ if (data.sku !== undefined) {
364
+ updates.push(`sku = $${paramIndex++}`)
365
+ values.push(data.sku || null)
366
+ }
367
+ if (data.description !== undefined) {
368
+ updates.push(`description = $${paramIndex++}`)
369
+ values.push(data.description || null)
370
+ }
371
+ if (data.price !== undefined) {
372
+ updates.push(`price = $${paramIndex++}`)
373
+ values.push(data.price)
374
+ }
375
+ if (data.currency !== undefined) {
376
+ updates.push(`currency = $${paramIndex++}`)
377
+ values.push(data.currency || null)
378
+ }
379
+ if (data.category !== undefined) {
380
+ updates.push(`category = $${paramIndex++}`)
381
+ values.push(data.category || null)
382
+ }
383
+ if (data.isActive !== undefined) {
384
+ updates.push(`"isActive" = $${paramIndex++}`)
385
+ values.push(data.isActive)
386
+ }
387
+
388
+ if (updates.length === 0) throw new Error('No fields to update')
389
+
390
+ updates.push(`"updatedAt" = $${paramIndex++}`)
391
+ values.push(new Date().toISOString())
392
+ values.push(id)
393
+
394
+ const result = await mutateWithRLS<DbProduct>(
395
+ `UPDATE products SET ${updates.join(', ')} WHERE id = $${paramIndex}
396
+ RETURNING id, name, sku, description, price, currency, category, "isActive", "createdAt", "updatedAt"`,
397
+ values,
398
+ userId
399
+ )
400
+
401
+ if (!result.rows[0]) throw new Error('Product not found or update failed')
402
+
403
+ const product = result.rows[0]
404
+ return {
405
+ id: product.id,
406
+ name: product.name,
407
+ sku: product.sku ?? undefined,
408
+ description: product.description ?? undefined,
409
+ price: product.price,
410
+ currency: product.currency ?? undefined,
411
+ category: product.category ?? undefined,
412
+ isActive: product.isActive ?? undefined,
413
+ createdAt: product.createdAt,
414
+ updatedAt: product.updatedAt,
415
+ }
416
+ } catch (error) {
417
+ console.error('ProductsService.update error:', error)
418
+ throw new Error(error instanceof Error ? error.message : 'Failed to update product')
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Delete a product
424
+ */
425
+ static async delete(userId: string, id: string): Promise<boolean> {
426
+ try {
427
+ if (!userId?.trim()) throw new Error('User ID is required')
428
+ if (!id?.trim()) throw new Error('Product ID is required')
429
+
430
+ const result = await mutateWithRLS(`DELETE FROM products WHERE id = $1`, [id], userId)
431
+ return result.rowCount > 0
432
+ } catch (error) {
433
+ console.error('ProductsService.delete error:', error)
434
+ throw new Error(error instanceof Error ? error.message : 'Failed to delete product')
435
+ }
436
+ }
437
+ }
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Product Service Types
3
+ *
4
+ * Type definitions for the ProductService.
5
+ * Defines types for product catalog management including pricing,
6
+ * inventory units, and product classification.
7
+ *
8
+ * @module ProductTypes
9
+ */
10
+
11
+ // Type literals for select fields
12
+ export type ProductType = 'product' | 'service' | 'subscription' | 'bundle' | 'addon'
13
+
14
+ export type Currency =
15
+ | 'USD'
16
+ | 'EUR'
17
+ | 'GBP'
18
+ | 'MXN'
19
+ | 'CAD'
20
+ | 'AUD'
21
+ | 'JPY'
22
+ | 'CNY'
23
+ | 'INR'
24
+ | 'BRL'
25
+
26
+ export type ProductUnit =
27
+ | 'piece'
28
+ | 'hour'
29
+ | 'day'
30
+ | 'week'
31
+ | 'month'
32
+ | 'year'
33
+ | 'kg'
34
+ | 'lb'
35
+ | 'meter'
36
+ | 'foot'
37
+ | 'license'
38
+ | 'user'
39
+
40
+ // Main entity interface
41
+ export interface Product {
42
+ id: string
43
+ teamId: string
44
+ code: string
45
+ name: string
46
+ category?: string | null
47
+ type?: ProductType | null
48
+ description?: string | null
49
+ price: number
50
+ cost?: number | null
51
+ currency?: Currency | null
52
+ unit?: ProductUnit | null
53
+ isActive?: boolean | null
54
+ minimumQuantity?: number | null
55
+ image?: string | null
56
+ brochureUrl?: string | null
57
+ commissionRate?: number | null
58
+ createdAt: string
59
+ updatedAt: string
60
+ }
61
+
62
+ // List options
63
+ export interface ProductListOptions {
64
+ limit?: number
65
+ offset?: number
66
+ teamId?: string
67
+ category?: string
68
+ type?: ProductType
69
+ currency?: Currency
70
+ unit?: ProductUnit
71
+ isActive?: boolean
72
+ orderBy?:
73
+ | 'code'
74
+ | 'name'
75
+ | 'category'
76
+ | 'price'
77
+ | 'cost'
78
+ | 'minimumQuantity'
79
+ | 'createdAt'
80
+ | 'updatedAt'
81
+ orderDir?: 'asc' | 'desc'
82
+ }
83
+
84
+ // List result
85
+ export interface ProductListResult {
86
+ products: Product[]
87
+ total: number
88
+ }
89
+
90
+ // Create data (required fields + teamId + optional fields)
91
+ export interface ProductCreateData {
92
+ code: string
93
+ name: string
94
+ price: number
95
+ teamId: string
96
+ category?: string
97
+ type?: ProductType
98
+ description?: string
99
+ cost?: number
100
+ currency?: Currency
101
+ unit?: ProductUnit
102
+ isActive?: boolean
103
+ minimumQuantity?: number
104
+ image?: string
105
+ brochureUrl?: string
106
+ commissionRate?: number
107
+ }
108
+
109
+ // Update data (all fields optional)
110
+ export interface ProductUpdateData {
111
+ code?: string
112
+ name?: string
113
+ category?: string | null
114
+ type?: ProductType | null
115
+ description?: string | null
116
+ price?: number
117
+ cost?: number | null
118
+ currency?: Currency | null
119
+ unit?: ProductUnit | null
120
+ isActive?: boolean | null
121
+ minimumQuantity?: number | null
122
+ image?: string | null
123
+ brochureUrl?: string | null
124
+ commissionRate?: number | null
125
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * CRM Theme Constants
3
+ * Shared constants for the CRM theme
4
+ */
5
+
6
+ export const PIPELINE_STAGE_COLORS = {
7
+ qualification: '#3B82F6',
8
+ analysis: '#10B981',
9
+ proposal: '#F59E0B',
10
+ negotiation: '#8B5CF6',
11
+ won: '#059669',
12
+ lost: '#DC2626',
13
+ } as const
14
+
15
+ export const ACTIVITY_TYPE_COLORS = {
16
+ call: '#3B82F6',
17
+ email: '#8B5CF6',
18
+ meeting: '#10B981',
19
+ task: '#F59E0B',
20
+ note: '#6B7280',
21
+ } as const
22
+
23
+ export const ACTIVITY_TYPE_ICONS = {
24
+ call: '📞',
25
+ email: '📧',
26
+ meeting: '🤝',
27
+ task: '✅',
28
+ note: '📝',
29
+ demo: '🎬',
30
+ presentation: '📊',
31
+ } as const
32
+
33
+ export const PRIORITY_LEVELS = {
34
+ low: { label: 'Low', color: '#9CA3AF' },
35
+ medium: { label: 'Medium', color: '#F59E0B' },
36
+ high: { label: 'High', color: '#EF4444' },
37
+ urgent: { label: 'Urgent', color: '#DC2626' },
38
+ } as const
39
+
40
+ export const LEAD_STATUS_OPTIONS = [
41
+ { value: 'new', label: 'New', color: '#3B82F6' },
42
+ { value: 'contacted', label: 'Contacted', color: '#10B981' },
43
+ { value: 'qualified', label: 'Qualified', color: '#F59E0B' },
44
+ { value: 'proposal', label: 'Proposal', color: '#8B5CF6' },
45
+ { value: 'negotiation', label: 'Negotiation', color: '#EC4899' },
46
+ { value: 'converted', label: 'Converted', color: '#059669' },
47
+ { value: 'lost', label: 'Lost', color: '#DC2626' },
48
+ ] as const
49
+
50
+ export const OPPORTUNITY_STATUS_OPTIONS = [
51
+ { value: 'open', label: 'Open', color: '#3B82F6' },
52
+ { value: 'won', label: 'Won', color: '#059669' },
53
+ { value: 'lost', label: 'Lost', color: '#DC2626' },
54
+ { value: 'abandoned', label: 'Abandoned', color: '#6B7280' },
55
+ ] as const
56
+
57
+ export const DEFAULT_PIPELINE_STAGES = [
58
+ { order: 1, name: 'Qualification', probability: 10, color: '#3B82F6' },
59
+ { order: 2, name: 'Needs Analysis', probability: 25, color: '#10B981' },
60
+ { order: 3, name: 'Proposal', probability: 50, color: '#F59E0B' },
61
+ { order: 4, name: 'Negotiation', probability: 75, color: '#8B5CF6' },
62
+ { order: 5, name: 'Closed Won', probability: 100, color: '#059669' },
63
+ { order: 6, name: 'Closed Lost', probability: 0, color: '#EF4444' },
64
+ ] as const
65
+
66
+ export const CRM_ROUTES = {
67
+ dashboard: '/dashboard/crm',
68
+ pipelines: '/dashboard/crm/pipelines',
69
+ opportunities: '/dashboard/crm/opportunities',
70
+ activities: '/dashboard/crm/activities',
71
+ leads: '/dashboard/crm/leads',
72
+ contacts: '/dashboard/crm/contacts',
73
+ companies: '/dashboard/crm/companies',
74
+ campaigns: '/dashboard/crm/campaigns',
75
+ products: '/dashboard/crm/products',
76
+ notes: '/dashboard/crm/notes',
77
+ } as const