@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,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaigns Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for campaigns.
|
|
5
|
+
* Campaigns is a private entity - users only see campaigns in their team.
|
|
6
|
+
*
|
|
7
|
+
* All methods require authentication (use RLS with userId filter).
|
|
8
|
+
*
|
|
9
|
+
* @module CampaignsService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
13
|
+
|
|
14
|
+
// Campaign type
|
|
15
|
+
export type CampaignType = 'email' | 'social' | 'webinar' | 'event' | 'other'
|
|
16
|
+
|
|
17
|
+
// Campaign status type
|
|
18
|
+
export type CampaignStatus = 'draft' | 'active' | 'paused' | 'completed'
|
|
19
|
+
|
|
20
|
+
// Campaign interface
|
|
21
|
+
export interface Campaign {
|
|
22
|
+
id: string
|
|
23
|
+
name: string
|
|
24
|
+
type: CampaignType
|
|
25
|
+
status: CampaignStatus
|
|
26
|
+
startDate?: string
|
|
27
|
+
endDate?: string
|
|
28
|
+
budget?: number
|
|
29
|
+
actualCost?: number
|
|
30
|
+
expectedRevenue?: number
|
|
31
|
+
description?: string
|
|
32
|
+
createdAt: string
|
|
33
|
+
updatedAt: string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// List options
|
|
37
|
+
export interface CampaignListOptions {
|
|
38
|
+
limit?: number
|
|
39
|
+
offset?: number
|
|
40
|
+
type?: CampaignType
|
|
41
|
+
status?: CampaignStatus
|
|
42
|
+
orderBy?: 'name' | 'startDate' | 'budget' | 'createdAt' | 'updatedAt'
|
|
43
|
+
orderDir?: 'asc' | 'desc'
|
|
44
|
+
teamId?: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// List result
|
|
48
|
+
export interface CampaignListResult {
|
|
49
|
+
campaigns: Campaign[]
|
|
50
|
+
total: number
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Create data
|
|
54
|
+
export interface CampaignCreateData {
|
|
55
|
+
name: string
|
|
56
|
+
type: CampaignType
|
|
57
|
+
status?: CampaignStatus
|
|
58
|
+
startDate?: string
|
|
59
|
+
endDate?: string
|
|
60
|
+
budget?: number
|
|
61
|
+
actualCost?: number
|
|
62
|
+
expectedRevenue?: number
|
|
63
|
+
description?: string
|
|
64
|
+
teamId: string
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Update data
|
|
68
|
+
export interface CampaignUpdateData {
|
|
69
|
+
name?: string
|
|
70
|
+
type?: CampaignType
|
|
71
|
+
status?: CampaignStatus
|
|
72
|
+
startDate?: string
|
|
73
|
+
endDate?: string
|
|
74
|
+
budget?: number
|
|
75
|
+
actualCost?: number
|
|
76
|
+
expectedRevenue?: number
|
|
77
|
+
description?: string
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Database row type
|
|
81
|
+
interface DbCampaign {
|
|
82
|
+
id: string
|
|
83
|
+
name: string
|
|
84
|
+
type: CampaignType
|
|
85
|
+
status: CampaignStatus
|
|
86
|
+
startDate: string | null
|
|
87
|
+
endDate: string | null
|
|
88
|
+
budget: number | null
|
|
89
|
+
actualCost: number | null
|
|
90
|
+
expectedRevenue: number | null
|
|
91
|
+
description: string | null
|
|
92
|
+
createdAt: string
|
|
93
|
+
updatedAt: string
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export class CampaignsService {
|
|
97
|
+
// ============================================
|
|
98
|
+
// READ METHODS
|
|
99
|
+
// ============================================
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Get a campaign by ID
|
|
103
|
+
*/
|
|
104
|
+
static async getById(id: string, userId: string): Promise<Campaign | null> {
|
|
105
|
+
try {
|
|
106
|
+
if (!id?.trim()) throw new Error('Campaign ID is required')
|
|
107
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
108
|
+
|
|
109
|
+
const campaign = await queryOneWithRLS<DbCampaign>(
|
|
110
|
+
`SELECT id, name, type, status, "startDate", "endDate", budget, "actualCost", "expectedRevenue", description, "createdAt", "updatedAt"
|
|
111
|
+
FROM campaigns WHERE id = $1`,
|
|
112
|
+
[id],
|
|
113
|
+
userId
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (!campaign) return null
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: campaign.id,
|
|
120
|
+
name: campaign.name,
|
|
121
|
+
type: campaign.type,
|
|
122
|
+
status: campaign.status,
|
|
123
|
+
startDate: campaign.startDate ?? undefined,
|
|
124
|
+
endDate: campaign.endDate ?? undefined,
|
|
125
|
+
budget: campaign.budget ?? undefined,
|
|
126
|
+
actualCost: campaign.actualCost ?? undefined,
|
|
127
|
+
expectedRevenue: campaign.expectedRevenue ?? undefined,
|
|
128
|
+
description: campaign.description ?? undefined,
|
|
129
|
+
createdAt: campaign.createdAt,
|
|
130
|
+
updatedAt: campaign.updatedAt,
|
|
131
|
+
}
|
|
132
|
+
} catch (error) {
|
|
133
|
+
console.error('CampaignsService.getById error:', error)
|
|
134
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch campaign')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* List campaigns with pagination and filtering
|
|
140
|
+
*/
|
|
141
|
+
static async list(userId: string, options: CampaignListOptions = {}): Promise<CampaignListResult> {
|
|
142
|
+
try {
|
|
143
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
limit = 10,
|
|
147
|
+
offset = 0,
|
|
148
|
+
type,
|
|
149
|
+
status,
|
|
150
|
+
orderBy = 'createdAt',
|
|
151
|
+
orderDir = 'desc',
|
|
152
|
+
teamId,
|
|
153
|
+
} = options
|
|
154
|
+
|
|
155
|
+
// Build WHERE clause
|
|
156
|
+
const conditions: string[] = []
|
|
157
|
+
const params: unknown[] = []
|
|
158
|
+
let paramIndex = 1
|
|
159
|
+
|
|
160
|
+
if (type) {
|
|
161
|
+
conditions.push(`type = $${paramIndex++}`)
|
|
162
|
+
params.push(type)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (status) {
|
|
166
|
+
conditions.push(`status = $${paramIndex++}`)
|
|
167
|
+
params.push(status)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (teamId) {
|
|
171
|
+
conditions.push(`"teamId" = $${paramIndex++}`)
|
|
172
|
+
params.push(teamId)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
176
|
+
|
|
177
|
+
// Validate orderBy
|
|
178
|
+
const validOrderBy = ['name', 'startDate', 'budget', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
|
|
179
|
+
const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
|
|
180
|
+
const orderColumnMap: Record<string, string> = {
|
|
181
|
+
name: 'name',
|
|
182
|
+
startDate: '"startDate"',
|
|
183
|
+
budget: 'budget',
|
|
184
|
+
createdAt: '"createdAt"',
|
|
185
|
+
updatedAt: '"updatedAt"',
|
|
186
|
+
}
|
|
187
|
+
const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
|
|
188
|
+
|
|
189
|
+
// Get total count
|
|
190
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
191
|
+
`SELECT COUNT(*)::text as count FROM campaigns ${whereClause}`,
|
|
192
|
+
params,
|
|
193
|
+
userId
|
|
194
|
+
)
|
|
195
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
196
|
+
|
|
197
|
+
// Get campaigns
|
|
198
|
+
params.push(limit, offset)
|
|
199
|
+
const campaigns = await queryWithRLS<DbCampaign>(
|
|
200
|
+
`SELECT id, name, type, status, "startDate", "endDate", budget, "actualCost", "expectedRevenue", description, "createdAt", "updatedAt"
|
|
201
|
+
FROM campaigns ${whereClause}
|
|
202
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
203
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
204
|
+
params,
|
|
205
|
+
userId
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
campaigns: campaigns.map((campaign) => ({
|
|
210
|
+
id: campaign.id,
|
|
211
|
+
name: campaign.name,
|
|
212
|
+
type: campaign.type,
|
|
213
|
+
status: campaign.status,
|
|
214
|
+
startDate: campaign.startDate ?? undefined,
|
|
215
|
+
endDate: campaign.endDate ?? undefined,
|
|
216
|
+
budget: campaign.budget ?? undefined,
|
|
217
|
+
actualCost: campaign.actualCost ?? undefined,
|
|
218
|
+
expectedRevenue: campaign.expectedRevenue ?? undefined,
|
|
219
|
+
description: campaign.description ?? undefined,
|
|
220
|
+
createdAt: campaign.createdAt,
|
|
221
|
+
updatedAt: campaign.updatedAt,
|
|
222
|
+
})),
|
|
223
|
+
total,
|
|
224
|
+
}
|
|
225
|
+
} catch (error) {
|
|
226
|
+
console.error('CampaignsService.list error:', error)
|
|
227
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to list campaigns')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get campaigns by status
|
|
233
|
+
*/
|
|
234
|
+
static async getByStatus(userId: string, status: CampaignStatus, limit = 50): Promise<Campaign[]> {
|
|
235
|
+
const { campaigns } = await this.list(userId, {
|
|
236
|
+
status,
|
|
237
|
+
limit,
|
|
238
|
+
orderBy: 'startDate',
|
|
239
|
+
orderDir: 'desc',
|
|
240
|
+
})
|
|
241
|
+
return campaigns
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get active campaigns
|
|
246
|
+
*/
|
|
247
|
+
static async getActive(userId: string, limit = 20): Promise<Campaign[]> {
|
|
248
|
+
return this.getByStatus(userId, 'active', limit)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Get campaigns by type
|
|
253
|
+
*/
|
|
254
|
+
static async getByType(userId: string, type: CampaignType, limit = 50): Promise<Campaign[]> {
|
|
255
|
+
const { campaigns } = await this.list(userId, {
|
|
256
|
+
type,
|
|
257
|
+
limit,
|
|
258
|
+
orderBy: 'startDate',
|
|
259
|
+
orderDir: 'desc',
|
|
260
|
+
})
|
|
261
|
+
return campaigns
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// ============================================
|
|
265
|
+
// WRITE METHODS
|
|
266
|
+
// ============================================
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Create a new campaign
|
|
270
|
+
*/
|
|
271
|
+
static async create(userId: string, data: CampaignCreateData): Promise<Campaign> {
|
|
272
|
+
try {
|
|
273
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
274
|
+
if (!data.name?.trim()) throw new Error('Campaign name is required')
|
|
275
|
+
if (!data.teamId?.trim()) throw new Error('Team ID is required')
|
|
276
|
+
|
|
277
|
+
const id = crypto.randomUUID()
|
|
278
|
+
const now = new Date().toISOString()
|
|
279
|
+
|
|
280
|
+
const result = await mutateWithRLS<DbCampaign>(
|
|
281
|
+
`INSERT INTO campaigns (id, "userId", "teamId", name, type, status, "startDate", "endDate", budget, "actualCost", "expectedRevenue", description, "createdAt", "updatedAt")
|
|
282
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
283
|
+
RETURNING id, name, type, status, "startDate", "endDate", budget, "actualCost", "expectedRevenue", description, "createdAt", "updatedAt"`,
|
|
284
|
+
[
|
|
285
|
+
id,
|
|
286
|
+
userId,
|
|
287
|
+
data.teamId,
|
|
288
|
+
data.name,
|
|
289
|
+
data.type,
|
|
290
|
+
data.status || 'draft',
|
|
291
|
+
data.startDate || null,
|
|
292
|
+
data.endDate || null,
|
|
293
|
+
data.budget || null,
|
|
294
|
+
data.actualCost || null,
|
|
295
|
+
data.expectedRevenue || null,
|
|
296
|
+
data.description || null,
|
|
297
|
+
now,
|
|
298
|
+
now,
|
|
299
|
+
],
|
|
300
|
+
userId
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if (!result.rows[0]) throw new Error('Failed to create campaign')
|
|
304
|
+
|
|
305
|
+
const campaign = result.rows[0]
|
|
306
|
+
return {
|
|
307
|
+
id: campaign.id,
|
|
308
|
+
name: campaign.name,
|
|
309
|
+
type: campaign.type,
|
|
310
|
+
status: campaign.status,
|
|
311
|
+
startDate: campaign.startDate ?? undefined,
|
|
312
|
+
endDate: campaign.endDate ?? undefined,
|
|
313
|
+
budget: campaign.budget ?? undefined,
|
|
314
|
+
actualCost: campaign.actualCost ?? undefined,
|
|
315
|
+
expectedRevenue: campaign.expectedRevenue ?? undefined,
|
|
316
|
+
description: campaign.description ?? undefined,
|
|
317
|
+
createdAt: campaign.createdAt,
|
|
318
|
+
updatedAt: campaign.updatedAt,
|
|
319
|
+
}
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.error('CampaignsService.create error:', error)
|
|
322
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to create campaign')
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Update an existing campaign
|
|
328
|
+
*/
|
|
329
|
+
static async update(userId: string, id: string, data: CampaignUpdateData): Promise<Campaign> {
|
|
330
|
+
try {
|
|
331
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
332
|
+
if (!id?.trim()) throw new Error('Campaign ID is required')
|
|
333
|
+
|
|
334
|
+
const updates: string[] = []
|
|
335
|
+
const values: unknown[] = []
|
|
336
|
+
let paramIndex = 1
|
|
337
|
+
|
|
338
|
+
if (data.name !== undefined) {
|
|
339
|
+
updates.push(`name = $${paramIndex++}`)
|
|
340
|
+
values.push(data.name)
|
|
341
|
+
}
|
|
342
|
+
if (data.type !== undefined) {
|
|
343
|
+
updates.push(`type = $${paramIndex++}`)
|
|
344
|
+
values.push(data.type)
|
|
345
|
+
}
|
|
346
|
+
if (data.status !== undefined) {
|
|
347
|
+
updates.push(`status = $${paramIndex++}`)
|
|
348
|
+
values.push(data.status)
|
|
349
|
+
}
|
|
350
|
+
if (data.startDate !== undefined) {
|
|
351
|
+
updates.push(`"startDate" = $${paramIndex++}`)
|
|
352
|
+
values.push(data.startDate || null)
|
|
353
|
+
}
|
|
354
|
+
if (data.endDate !== undefined) {
|
|
355
|
+
updates.push(`"endDate" = $${paramIndex++}`)
|
|
356
|
+
values.push(data.endDate || null)
|
|
357
|
+
}
|
|
358
|
+
if (data.budget !== undefined) {
|
|
359
|
+
updates.push(`budget = $${paramIndex++}`)
|
|
360
|
+
values.push(data.budget)
|
|
361
|
+
}
|
|
362
|
+
if (data.actualCost !== undefined) {
|
|
363
|
+
updates.push(`"actualCost" = $${paramIndex++}`)
|
|
364
|
+
values.push(data.actualCost)
|
|
365
|
+
}
|
|
366
|
+
if (data.expectedRevenue !== undefined) {
|
|
367
|
+
updates.push(`"expectedRevenue" = $${paramIndex++}`)
|
|
368
|
+
values.push(data.expectedRevenue)
|
|
369
|
+
}
|
|
370
|
+
if (data.description !== undefined) {
|
|
371
|
+
updates.push(`description = $${paramIndex++}`)
|
|
372
|
+
values.push(data.description || null)
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (updates.length === 0) throw new Error('No fields to update')
|
|
376
|
+
|
|
377
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
378
|
+
values.push(new Date().toISOString())
|
|
379
|
+
values.push(id)
|
|
380
|
+
|
|
381
|
+
const result = await mutateWithRLS<DbCampaign>(
|
|
382
|
+
`UPDATE campaigns SET ${updates.join(', ')} WHERE id = $${paramIndex}
|
|
383
|
+
RETURNING id, name, type, status, "startDate", "endDate", budget, "actualCost", "expectedRevenue", description, "createdAt", "updatedAt"`,
|
|
384
|
+
values,
|
|
385
|
+
userId
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
if (!result.rows[0]) throw new Error('Campaign not found or update failed')
|
|
389
|
+
|
|
390
|
+
const campaign = result.rows[0]
|
|
391
|
+
return {
|
|
392
|
+
id: campaign.id,
|
|
393
|
+
name: campaign.name,
|
|
394
|
+
type: campaign.type,
|
|
395
|
+
status: campaign.status,
|
|
396
|
+
startDate: campaign.startDate ?? undefined,
|
|
397
|
+
endDate: campaign.endDate ?? undefined,
|
|
398
|
+
budget: campaign.budget ?? undefined,
|
|
399
|
+
actualCost: campaign.actualCost ?? undefined,
|
|
400
|
+
expectedRevenue: campaign.expectedRevenue ?? undefined,
|
|
401
|
+
description: campaign.description ?? undefined,
|
|
402
|
+
createdAt: campaign.createdAt,
|
|
403
|
+
updatedAt: campaign.updatedAt,
|
|
404
|
+
}
|
|
405
|
+
} catch (error) {
|
|
406
|
+
console.error('CampaignsService.update error:', error)
|
|
407
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to update campaign')
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Delete a campaign
|
|
413
|
+
*/
|
|
414
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
415
|
+
try {
|
|
416
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
417
|
+
if (!id?.trim()) throw new Error('Campaign ID is required')
|
|
418
|
+
|
|
419
|
+
const result = await mutateWithRLS(`DELETE FROM campaigns WHERE id = $1`, [id], userId)
|
|
420
|
+
return result.rowCount > 0
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('CampaignsService.delete error:', error)
|
|
423
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to delete campaign')
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Campaign Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the CampaignService.
|
|
5
|
+
* Defines types for marketing campaign management including budget tracking,
|
|
6
|
+
* lead generation metrics, and ROI calculations.
|
|
7
|
+
*
|
|
8
|
+
* @module CampaignTypes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Type literals for select fields
|
|
12
|
+
export type CampaignType =
|
|
13
|
+
| 'email'
|
|
14
|
+
| 'social'
|
|
15
|
+
| 'event'
|
|
16
|
+
| 'webinar'
|
|
17
|
+
| 'advertising'
|
|
18
|
+
| 'content'
|
|
19
|
+
| 'other'
|
|
20
|
+
|
|
21
|
+
export type CampaignStatus = 'planned' | 'active' | 'paused' | 'completed' | 'cancelled'
|
|
22
|
+
|
|
23
|
+
export type CampaignChannel =
|
|
24
|
+
| 'email'
|
|
25
|
+
| 'social_media'
|
|
26
|
+
| 'web'
|
|
27
|
+
| 'print'
|
|
28
|
+
| 'tv'
|
|
29
|
+
| 'radio'
|
|
30
|
+
| 'other'
|
|
31
|
+
|
|
32
|
+
// Main entity interface
|
|
33
|
+
export interface Campaign {
|
|
34
|
+
id: string
|
|
35
|
+
teamId: string
|
|
36
|
+
name: string
|
|
37
|
+
type?: CampaignType | null
|
|
38
|
+
status?: CampaignStatus | null
|
|
39
|
+
objective?: string | null
|
|
40
|
+
description?: string | null
|
|
41
|
+
startDate: string
|
|
42
|
+
endDate: string
|
|
43
|
+
budget?: number | null
|
|
44
|
+
actualCost?: number | null
|
|
45
|
+
targetAudience?: string | null
|
|
46
|
+
targetLeads?: number | null
|
|
47
|
+
actualLeads?: number | null
|
|
48
|
+
targetRevenue?: number | null
|
|
49
|
+
actualRevenue?: number | null
|
|
50
|
+
roi?: number | null
|
|
51
|
+
channel?: CampaignChannel | null
|
|
52
|
+
assignedTo?: string | null
|
|
53
|
+
createdAt: string
|
|
54
|
+
updatedAt: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// List options
|
|
58
|
+
export interface CampaignListOptions {
|
|
59
|
+
limit?: number
|
|
60
|
+
offset?: number
|
|
61
|
+
teamId?: string
|
|
62
|
+
type?: CampaignType
|
|
63
|
+
status?: CampaignStatus
|
|
64
|
+
channel?: CampaignChannel
|
|
65
|
+
assignedTo?: string
|
|
66
|
+
orderBy?:
|
|
67
|
+
| 'name'
|
|
68
|
+
| 'startDate'
|
|
69
|
+
| 'endDate'
|
|
70
|
+
| 'budget'
|
|
71
|
+
| 'actualCost'
|
|
72
|
+
| 'actualLeads'
|
|
73
|
+
| 'roi'
|
|
74
|
+
| 'createdAt'
|
|
75
|
+
| 'updatedAt'
|
|
76
|
+
orderDir?: 'asc' | 'desc'
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// List result
|
|
80
|
+
export interface CampaignListResult {
|
|
81
|
+
campaigns: Campaign[]
|
|
82
|
+
total: number
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Create data (required fields + teamId + optional fields)
|
|
86
|
+
export interface CampaignCreateData {
|
|
87
|
+
name: string
|
|
88
|
+
startDate: string
|
|
89
|
+
endDate: string
|
|
90
|
+
teamId: string
|
|
91
|
+
type?: CampaignType
|
|
92
|
+
status?: CampaignStatus
|
|
93
|
+
objective?: string
|
|
94
|
+
description?: string
|
|
95
|
+
budget?: number
|
|
96
|
+
actualCost?: number
|
|
97
|
+
targetAudience?: string
|
|
98
|
+
targetLeads?: number
|
|
99
|
+
actualLeads?: number
|
|
100
|
+
targetRevenue?: number
|
|
101
|
+
actualRevenue?: number
|
|
102
|
+
channel?: CampaignChannel
|
|
103
|
+
assignedTo?: string
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Update data (all fields optional)
|
|
107
|
+
export interface CampaignUpdateData {
|
|
108
|
+
name?: string
|
|
109
|
+
type?: CampaignType | null
|
|
110
|
+
status?: CampaignStatus | null
|
|
111
|
+
objective?: string | null
|
|
112
|
+
description?: string | null
|
|
113
|
+
startDate?: string
|
|
114
|
+
endDate?: string
|
|
115
|
+
budget?: number | null
|
|
116
|
+
actualCost?: number | null
|
|
117
|
+
targetAudience?: string | null
|
|
118
|
+
targetLeads?: number | null
|
|
119
|
+
actualLeads?: number | null
|
|
120
|
+
targetRevenue?: number | null
|
|
121
|
+
actualRevenue?: number | null
|
|
122
|
+
channel?: CampaignChannel | null
|
|
123
|
+
assignedTo?: string | null
|
|
124
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
{
|
|
2
|
+
"entity": {
|
|
3
|
+
"name": "Campaign",
|
|
4
|
+
"plural": "Campaigns",
|
|
5
|
+
"description": "Manage marketing campaigns and outreach"
|
|
6
|
+
},
|
|
7
|
+
"fields": {
|
|
8
|
+
"name": {
|
|
9
|
+
"label": "Campaign Name",
|
|
10
|
+
"description": "Campaign name or title",
|
|
11
|
+
"placeholder": "Enter campaign name..."
|
|
12
|
+
},
|
|
13
|
+
"type": {
|
|
14
|
+
"label": "Campaign Type",
|
|
15
|
+
"description": "Type of marketing campaign",
|
|
16
|
+
"placeholder": "Select type..."
|
|
17
|
+
},
|
|
18
|
+
"status": {
|
|
19
|
+
"label": "Status",
|
|
20
|
+
"description": "Current campaign status",
|
|
21
|
+
"placeholder": "Select status..."
|
|
22
|
+
},
|
|
23
|
+
"channel": {
|
|
24
|
+
"label": "Channel",
|
|
25
|
+
"description": "Marketing channel used",
|
|
26
|
+
"placeholder": "Select channel..."
|
|
27
|
+
},
|
|
28
|
+
"budget": {
|
|
29
|
+
"label": "Budget",
|
|
30
|
+
"description": "Campaign budget amount",
|
|
31
|
+
"placeholder": "0.00"
|
|
32
|
+
},
|
|
33
|
+
"spend": {
|
|
34
|
+
"label": "Amount Spent",
|
|
35
|
+
"description": "Amount already spent",
|
|
36
|
+
"placeholder": "0.00"
|
|
37
|
+
},
|
|
38
|
+
"targetAudience": {
|
|
39
|
+
"label": "Target Audience",
|
|
40
|
+
"description": "Target audience description",
|
|
41
|
+
"placeholder": "Enter target audience..."
|
|
42
|
+
},
|
|
43
|
+
"startDate": {
|
|
44
|
+
"label": "Start Date",
|
|
45
|
+
"description": "Campaign start date",
|
|
46
|
+
"placeholder": "Select date..."
|
|
47
|
+
},
|
|
48
|
+
"endDate": {
|
|
49
|
+
"label": "End Date",
|
|
50
|
+
"description": "Campaign end date",
|
|
51
|
+
"placeholder": "Select date..."
|
|
52
|
+
},
|
|
53
|
+
"impressions": {
|
|
54
|
+
"label": "Impressions",
|
|
55
|
+
"description": "Number of impressions",
|
|
56
|
+
"placeholder": "0"
|
|
57
|
+
},
|
|
58
|
+
"clicks": {
|
|
59
|
+
"label": "Clicks",
|
|
60
|
+
"description": "Number of clicks",
|
|
61
|
+
"placeholder": "0"
|
|
62
|
+
},
|
|
63
|
+
"conversions": {
|
|
64
|
+
"label": "Conversions",
|
|
65
|
+
"description": "Number of conversions",
|
|
66
|
+
"placeholder": "0"
|
|
67
|
+
},
|
|
68
|
+
"ctr": {
|
|
69
|
+
"label": "CTR (%)",
|
|
70
|
+
"description": "Click-through rate percentage",
|
|
71
|
+
"placeholder": "0.00"
|
|
72
|
+
},
|
|
73
|
+
"conversionRate": {
|
|
74
|
+
"label": "Conversion Rate (%)",
|
|
75
|
+
"description": "Conversion rate percentage",
|
|
76
|
+
"placeholder": "0.00"
|
|
77
|
+
},
|
|
78
|
+
"roi": {
|
|
79
|
+
"label": "ROI (%)",
|
|
80
|
+
"description": "Return on investment percentage",
|
|
81
|
+
"placeholder": "0.00"
|
|
82
|
+
},
|
|
83
|
+
"assignedTo": {
|
|
84
|
+
"label": "Campaign Manager",
|
|
85
|
+
"description": "User managing this campaign",
|
|
86
|
+
"placeholder": "Select user..."
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
"options": {
|
|
90
|
+
"type": {
|
|
91
|
+
"email": "Email Marketing",
|
|
92
|
+
"social": "Social Media",
|
|
93
|
+
"ppc": "Pay-Per-Click",
|
|
94
|
+
"content": "Content Marketing",
|
|
95
|
+
"webinar": "Webinar",
|
|
96
|
+
"event": "Event",
|
|
97
|
+
"referral": "Referral",
|
|
98
|
+
"affiliate": "Affiliate",
|
|
99
|
+
"retargeting": "Retargeting"
|
|
100
|
+
},
|
|
101
|
+
"status": {
|
|
102
|
+
"draft": "Draft",
|
|
103
|
+
"scheduled": "Scheduled",
|
|
104
|
+
"active": "Active",
|
|
105
|
+
"paused": "Paused",
|
|
106
|
+
"completed": "Completed",
|
|
107
|
+
"cancelled": "Cancelled"
|
|
108
|
+
},
|
|
109
|
+
"channel": {
|
|
110
|
+
"email": "Email",
|
|
111
|
+
"facebook": "Facebook",
|
|
112
|
+
"instagram": "Instagram",
|
|
113
|
+
"linkedin": "LinkedIn",
|
|
114
|
+
"twitter": "Twitter",
|
|
115
|
+
"google_ads": "Google Ads",
|
|
116
|
+
"youtube": "YouTube",
|
|
117
|
+
"tiktok": "TikTok",
|
|
118
|
+
"website": "Website",
|
|
119
|
+
"blog": "Blog",
|
|
120
|
+
"seo": "SEO",
|
|
121
|
+
"other": "Other"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"actions": {
|
|
125
|
+
"create": "Create Campaign",
|
|
126
|
+
"edit": "Edit Campaign",
|
|
127
|
+
"delete": "Delete Campaign",
|
|
128
|
+
"view": "View Campaign",
|
|
129
|
+
"list": "List Campaigns",
|
|
130
|
+
"launch": "Launch Campaign",
|
|
131
|
+
"pause": "Pause Campaign",
|
|
132
|
+
"resume": "Resume Campaign",
|
|
133
|
+
"duplicate": "Duplicate Campaign"
|
|
134
|
+
},
|
|
135
|
+
"messages": {
|
|
136
|
+
"created": "Campaign created successfully",
|
|
137
|
+
"updated": "Campaign updated successfully",
|
|
138
|
+
"deleted": "Campaign deleted successfully",
|
|
139
|
+
"launched": "Campaign launched successfully",
|
|
140
|
+
"paused": "Campaign paused successfully",
|
|
141
|
+
"resumed": "Campaign resumed successfully",
|
|
142
|
+
"notFound": "Campaign not found",
|
|
143
|
+
"error": "An error occurred while processing the campaign"
|
|
144
|
+
}
|
|
145
|
+
}
|