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