@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,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
|
+
}
|