@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,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipelines Entity Fields Configuration
|
|
3
|
+
*
|
|
4
|
+
* Separated from main config according to new refactoring plan.
|
|
5
|
+
* Contains all field definitions for the pipelines entity.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { EntityField } from '@nextsparkjs/core/lib/entities/types'
|
|
9
|
+
|
|
10
|
+
export const pipelinesFields: EntityField[] = [
|
|
11
|
+
{
|
|
12
|
+
name: 'name',
|
|
13
|
+
type: 'text',
|
|
14
|
+
required: true,
|
|
15
|
+
display: {
|
|
16
|
+
label: 'Pipeline Name',
|
|
17
|
+
description: 'Pipeline name',
|
|
18
|
+
placeholder: 'Enter pipeline name...',
|
|
19
|
+
showInList: true,
|
|
20
|
+
showInDetail: true,
|
|
21
|
+
showInForm: true,
|
|
22
|
+
order: 1,
|
|
23
|
+
columnWidth: 6,
|
|
24
|
+
},
|
|
25
|
+
api: {
|
|
26
|
+
searchable: true,
|
|
27
|
+
sortable: true,
|
|
28
|
+
readOnly: false,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'description',
|
|
33
|
+
type: 'textarea',
|
|
34
|
+
required: false,
|
|
35
|
+
display: {
|
|
36
|
+
label: 'Description',
|
|
37
|
+
description: 'Pipeline description',
|
|
38
|
+
placeholder: 'Enter description...',
|
|
39
|
+
showInList: false,
|
|
40
|
+
showInDetail: true,
|
|
41
|
+
showInForm: true,
|
|
42
|
+
order: 2,
|
|
43
|
+
columnWidth: 6,
|
|
44
|
+
},
|
|
45
|
+
api: {
|
|
46
|
+
searchable: true,
|
|
47
|
+
sortable: false,
|
|
48
|
+
readOnly: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: 'type',
|
|
53
|
+
type: 'select',
|
|
54
|
+
required: false,
|
|
55
|
+
options: [
|
|
56
|
+
{ value: 'sales', label: 'Sales' },
|
|
57
|
+
{ value: 'support', label: 'Support' },
|
|
58
|
+
{ value: 'project', label: 'Project' },
|
|
59
|
+
{ value: 'custom', label: 'Custom' },
|
|
60
|
+
],
|
|
61
|
+
display: {
|
|
62
|
+
label: 'Pipeline Type',
|
|
63
|
+
description: 'Type of pipeline',
|
|
64
|
+
placeholder: 'Select type...',
|
|
65
|
+
showInList: true,
|
|
66
|
+
showInDetail: true,
|
|
67
|
+
showInForm: true,
|
|
68
|
+
order: 3,
|
|
69
|
+
columnWidth: 4,
|
|
70
|
+
},
|
|
71
|
+
api: {
|
|
72
|
+
searchable: false,
|
|
73
|
+
sortable: true,
|
|
74
|
+
readOnly: false,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'isDefault',
|
|
79
|
+
type: 'boolean',
|
|
80
|
+
required: false,
|
|
81
|
+
display: {
|
|
82
|
+
label: 'Default Pipeline',
|
|
83
|
+
description: 'Is this the default pipeline',
|
|
84
|
+
showInList: true,
|
|
85
|
+
showInDetail: true,
|
|
86
|
+
showInForm: true,
|
|
87
|
+
order: 4,
|
|
88
|
+
columnWidth: 4,
|
|
89
|
+
},
|
|
90
|
+
api: {
|
|
91
|
+
searchable: false,
|
|
92
|
+
sortable: true,
|
|
93
|
+
readOnly: false,
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'isActive',
|
|
98
|
+
type: 'boolean',
|
|
99
|
+
required: false,
|
|
100
|
+
display: {
|
|
101
|
+
label: 'Active',
|
|
102
|
+
description: 'Is pipeline currently active',
|
|
103
|
+
showInList: true,
|
|
104
|
+
showInDetail: true,
|
|
105
|
+
showInForm: true,
|
|
106
|
+
order: 5,
|
|
107
|
+
columnWidth: 4,
|
|
108
|
+
},
|
|
109
|
+
api: {
|
|
110
|
+
searchable: false,
|
|
111
|
+
sortable: true,
|
|
112
|
+
readOnly: false,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: 'stages',
|
|
117
|
+
type: 'json',
|
|
118
|
+
required: true,
|
|
119
|
+
display: {
|
|
120
|
+
label: 'Pipeline Stages',
|
|
121
|
+
description: 'Pipeline stages configuration as JSON array',
|
|
122
|
+
placeholder: 'Configure stages...',
|
|
123
|
+
showInList: false,
|
|
124
|
+
showInDetail: true,
|
|
125
|
+
showInForm: true,
|
|
126
|
+
order: 6,
|
|
127
|
+
columnWidth: 12,
|
|
128
|
+
},
|
|
129
|
+
api: {
|
|
130
|
+
searchable: false,
|
|
131
|
+
sortable: false,
|
|
132
|
+
readOnly: false,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'dealRottenDays',
|
|
137
|
+
type: 'number',
|
|
138
|
+
required: false,
|
|
139
|
+
display: {
|
|
140
|
+
label: 'Deal Rotten Days',
|
|
141
|
+
description: 'Days until deal is considered stale',
|
|
142
|
+
placeholder: '30',
|
|
143
|
+
showInList: false,
|
|
144
|
+
showInDetail: true,
|
|
145
|
+
showInForm: true,
|
|
146
|
+
order: 7,
|
|
147
|
+
columnWidth: 4,
|
|
148
|
+
},
|
|
149
|
+
api: {
|
|
150
|
+
searchable: false,
|
|
151
|
+
sortable: true,
|
|
152
|
+
readOnly: false,
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'createdAt',
|
|
157
|
+
type: 'datetime',
|
|
158
|
+
required: false,
|
|
159
|
+
display: {
|
|
160
|
+
label: 'Created At',
|
|
161
|
+
description: 'When the pipeline was created',
|
|
162
|
+
showInList: true,
|
|
163
|
+
showInDetail: true,
|
|
164
|
+
showInForm: false,
|
|
165
|
+
order: 98,
|
|
166
|
+
columnWidth: 4,
|
|
167
|
+
},
|
|
168
|
+
api: {
|
|
169
|
+
searchable: false,
|
|
170
|
+
sortable: true,
|
|
171
|
+
readOnly: true,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
name: 'updatedAt',
|
|
176
|
+
type: 'datetime',
|
|
177
|
+
required: false,
|
|
178
|
+
display: {
|
|
179
|
+
label: 'Updated At',
|
|
180
|
+
description: 'When the pipeline was last updated',
|
|
181
|
+
showInList: false,
|
|
182
|
+
showInDetail: true,
|
|
183
|
+
showInForm: false,
|
|
184
|
+
order: 99,
|
|
185
|
+
columnWidth: 4,
|
|
186
|
+
},
|
|
187
|
+
api: {
|
|
188
|
+
searchable: false,
|
|
189
|
+
sortable: true,
|
|
190
|
+
readOnly: true,
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
]
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipelines Service
|
|
3
|
+
*
|
|
4
|
+
* Provides data access methods for pipelines.
|
|
5
|
+
* Pipelines is a private entity - users only see pipelines in their team.
|
|
6
|
+
*
|
|
7
|
+
* All methods require authentication (use RLS with userId filter).
|
|
8
|
+
*
|
|
9
|
+
* @module PipelinesService
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { queryOneWithRLS, queryWithRLS, mutateWithRLS } from '@nextsparkjs/core/lib/db'
|
|
13
|
+
|
|
14
|
+
// Pipeline stage interface
|
|
15
|
+
export interface PipelineStage {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
order: number
|
|
19
|
+
probability?: number
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Pipeline interface
|
|
23
|
+
export interface Pipeline {
|
|
24
|
+
id: string
|
|
25
|
+
name: string
|
|
26
|
+
description?: string
|
|
27
|
+
isDefault?: boolean
|
|
28
|
+
stages?: PipelineStage[]
|
|
29
|
+
createdAt: string
|
|
30
|
+
updatedAt: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// List options
|
|
34
|
+
export interface PipelineListOptions {
|
|
35
|
+
limit?: number
|
|
36
|
+
offset?: number
|
|
37
|
+
isDefault?: boolean
|
|
38
|
+
orderBy?: 'name' | 'createdAt' | 'updatedAt'
|
|
39
|
+
orderDir?: 'asc' | 'desc'
|
|
40
|
+
teamId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// List result
|
|
44
|
+
export interface PipelineListResult {
|
|
45
|
+
pipelines: Pipeline[]
|
|
46
|
+
total: number
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Create data
|
|
50
|
+
export interface PipelineCreateData {
|
|
51
|
+
name: string
|
|
52
|
+
description?: string
|
|
53
|
+
isDefault?: boolean
|
|
54
|
+
stages?: PipelineStage[]
|
|
55
|
+
teamId: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Update data
|
|
59
|
+
export interface PipelineUpdateData {
|
|
60
|
+
name?: string
|
|
61
|
+
description?: string
|
|
62
|
+
isDefault?: boolean
|
|
63
|
+
stages?: PipelineStage[]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Database row type
|
|
67
|
+
interface DbPipeline {
|
|
68
|
+
id: string
|
|
69
|
+
name: string
|
|
70
|
+
description: string | null
|
|
71
|
+
isDefault: boolean | null
|
|
72
|
+
stages: string | null // JSON string
|
|
73
|
+
createdAt: string
|
|
74
|
+
updatedAt: string
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class PipelinesService {
|
|
78
|
+
// ============================================
|
|
79
|
+
// READ METHODS
|
|
80
|
+
// ============================================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Get a pipeline by ID
|
|
84
|
+
*/
|
|
85
|
+
static async getById(id: string, userId: string): Promise<Pipeline | null> {
|
|
86
|
+
try {
|
|
87
|
+
if (!id?.trim()) throw new Error('Pipeline ID is required')
|
|
88
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
89
|
+
|
|
90
|
+
const pipeline = await queryOneWithRLS<DbPipeline>(
|
|
91
|
+
`SELECT id, name, description, "isDefault", stages, "createdAt", "updatedAt"
|
|
92
|
+
FROM pipelines WHERE id = $1`,
|
|
93
|
+
[id],
|
|
94
|
+
userId
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
if (!pipeline) return null
|
|
98
|
+
|
|
99
|
+
// Parse stages JSON
|
|
100
|
+
let stages: PipelineStage[] | undefined
|
|
101
|
+
if (pipeline.stages) {
|
|
102
|
+
try {
|
|
103
|
+
stages = JSON.parse(pipeline.stages)
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.error('Failed to parse pipeline stages:', error)
|
|
106
|
+
stages = undefined
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
id: pipeline.id,
|
|
112
|
+
name: pipeline.name,
|
|
113
|
+
description: pipeline.description ?? undefined,
|
|
114
|
+
isDefault: pipeline.isDefault ?? undefined,
|
|
115
|
+
stages,
|
|
116
|
+
createdAt: pipeline.createdAt,
|
|
117
|
+
updatedAt: pipeline.updatedAt,
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('PipelinesService.getById error:', error)
|
|
121
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to fetch pipeline')
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* List pipelines with pagination and filtering
|
|
127
|
+
*/
|
|
128
|
+
static async list(userId: string, options: PipelineListOptions = {}): Promise<PipelineListResult> {
|
|
129
|
+
try {
|
|
130
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
131
|
+
|
|
132
|
+
const {
|
|
133
|
+
limit = 10,
|
|
134
|
+
offset = 0,
|
|
135
|
+
isDefault,
|
|
136
|
+
orderBy = 'createdAt',
|
|
137
|
+
orderDir = 'desc',
|
|
138
|
+
teamId,
|
|
139
|
+
} = options
|
|
140
|
+
|
|
141
|
+
// Build WHERE clause
|
|
142
|
+
const conditions: string[] = []
|
|
143
|
+
const params: unknown[] = []
|
|
144
|
+
let paramIndex = 1
|
|
145
|
+
|
|
146
|
+
if (isDefault !== undefined) {
|
|
147
|
+
conditions.push(`"isDefault" = $${paramIndex++}`)
|
|
148
|
+
params.push(isDefault)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (teamId) {
|
|
152
|
+
conditions.push(`"teamId" = $${paramIndex++}`)
|
|
153
|
+
params.push(teamId)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
157
|
+
|
|
158
|
+
// Validate orderBy
|
|
159
|
+
const validOrderBy = ['name', 'createdAt', 'updatedAt'].includes(orderBy) ? orderBy : 'createdAt'
|
|
160
|
+
const validOrderDir = orderDir === 'asc' ? 'ASC' : 'DESC'
|
|
161
|
+
const orderColumnMap: Record<string, string> = {
|
|
162
|
+
name: 'name',
|
|
163
|
+
createdAt: '"createdAt"',
|
|
164
|
+
updatedAt: '"updatedAt"',
|
|
165
|
+
}
|
|
166
|
+
const orderColumn = orderColumnMap[validOrderBy] || '"createdAt"'
|
|
167
|
+
|
|
168
|
+
// Get total count
|
|
169
|
+
const countResult = await queryWithRLS<{ count: string }>(
|
|
170
|
+
`SELECT COUNT(*)::text as count FROM pipelines ${whereClause}`,
|
|
171
|
+
params,
|
|
172
|
+
userId
|
|
173
|
+
)
|
|
174
|
+
const total = parseInt(countResult[0]?.count || '0', 10)
|
|
175
|
+
|
|
176
|
+
// Get pipelines
|
|
177
|
+
params.push(limit, offset)
|
|
178
|
+
const pipelines = await queryWithRLS<DbPipeline>(
|
|
179
|
+
`SELECT id, name, description, "isDefault", stages, "createdAt", "updatedAt"
|
|
180
|
+
FROM pipelines ${whereClause}
|
|
181
|
+
ORDER BY ${orderColumn} ${validOrderDir}
|
|
182
|
+
LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
|
|
183
|
+
params,
|
|
184
|
+
userId
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
pipelines: pipelines.map((pipeline) => {
|
|
189
|
+
// Parse stages JSON
|
|
190
|
+
let stages: PipelineStage[] | undefined
|
|
191
|
+
if (pipeline.stages) {
|
|
192
|
+
try {
|
|
193
|
+
stages = JSON.parse(pipeline.stages)
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Failed to parse pipeline stages:', error)
|
|
196
|
+
stages = undefined
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: pipeline.id,
|
|
202
|
+
name: pipeline.name,
|
|
203
|
+
description: pipeline.description ?? undefined,
|
|
204
|
+
isDefault: pipeline.isDefault ?? undefined,
|
|
205
|
+
stages,
|
|
206
|
+
createdAt: pipeline.createdAt,
|
|
207
|
+
updatedAt: pipeline.updatedAt,
|
|
208
|
+
}
|
|
209
|
+
}),
|
|
210
|
+
total,
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
console.error('PipelinesService.list error:', error)
|
|
214
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to list pipelines')
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Get default pipeline
|
|
220
|
+
*/
|
|
221
|
+
static async getDefault(userId: string): Promise<Pipeline | null> {
|
|
222
|
+
const { pipelines } = await this.list(userId, {
|
|
223
|
+
isDefault: true,
|
|
224
|
+
limit: 1,
|
|
225
|
+
})
|
|
226
|
+
return pipelines[0] || null
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ============================================
|
|
230
|
+
// WRITE METHODS
|
|
231
|
+
// ============================================
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Create a new pipeline
|
|
235
|
+
*/
|
|
236
|
+
static async create(userId: string, data: PipelineCreateData): Promise<Pipeline> {
|
|
237
|
+
try {
|
|
238
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
239
|
+
if (!data.name?.trim()) throw new Error('Pipeline name is required')
|
|
240
|
+
if (!data.teamId?.trim()) throw new Error('Team ID is required')
|
|
241
|
+
|
|
242
|
+
const id = crypto.randomUUID()
|
|
243
|
+
const now = new Date().toISOString()
|
|
244
|
+
|
|
245
|
+
// Serialize stages to JSON
|
|
246
|
+
const stagesJson = data.stages ? JSON.stringify(data.stages) : null
|
|
247
|
+
|
|
248
|
+
const result = await mutateWithRLS<DbPipeline>(
|
|
249
|
+
`INSERT INTO pipelines (id, "userId", "teamId", name, description, "isDefault", stages, "createdAt", "updatedAt")
|
|
250
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
251
|
+
RETURNING id, name, description, "isDefault", stages, "createdAt", "updatedAt"`,
|
|
252
|
+
[
|
|
253
|
+
id,
|
|
254
|
+
userId,
|
|
255
|
+
data.teamId,
|
|
256
|
+
data.name,
|
|
257
|
+
data.description || null,
|
|
258
|
+
data.isDefault || false,
|
|
259
|
+
stagesJson,
|
|
260
|
+
now,
|
|
261
|
+
now,
|
|
262
|
+
],
|
|
263
|
+
userId
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if (!result.rows[0]) throw new Error('Failed to create pipeline')
|
|
267
|
+
|
|
268
|
+
const pipeline = result.rows[0]
|
|
269
|
+
|
|
270
|
+
// Parse stages JSON
|
|
271
|
+
let stages: PipelineStage[] | undefined
|
|
272
|
+
if (pipeline.stages) {
|
|
273
|
+
try {
|
|
274
|
+
stages = JSON.parse(pipeline.stages)
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error('Failed to parse pipeline stages:', error)
|
|
277
|
+
stages = undefined
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
id: pipeline.id,
|
|
283
|
+
name: pipeline.name,
|
|
284
|
+
description: pipeline.description ?? undefined,
|
|
285
|
+
isDefault: pipeline.isDefault ?? undefined,
|
|
286
|
+
stages,
|
|
287
|
+
createdAt: pipeline.createdAt,
|
|
288
|
+
updatedAt: pipeline.updatedAt,
|
|
289
|
+
}
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('PipelinesService.create error:', error)
|
|
292
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to create pipeline')
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Update an existing pipeline
|
|
298
|
+
*/
|
|
299
|
+
static async update(userId: string, id: string, data: PipelineUpdateData): Promise<Pipeline> {
|
|
300
|
+
try {
|
|
301
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
302
|
+
if (!id?.trim()) throw new Error('Pipeline ID is required')
|
|
303
|
+
|
|
304
|
+
const updates: string[] = []
|
|
305
|
+
const values: unknown[] = []
|
|
306
|
+
let paramIndex = 1
|
|
307
|
+
|
|
308
|
+
if (data.name !== undefined) {
|
|
309
|
+
updates.push(`name = $${paramIndex++}`)
|
|
310
|
+
values.push(data.name)
|
|
311
|
+
}
|
|
312
|
+
if (data.description !== undefined) {
|
|
313
|
+
updates.push(`description = $${paramIndex++}`)
|
|
314
|
+
values.push(data.description || null)
|
|
315
|
+
}
|
|
316
|
+
if (data.isDefault !== undefined) {
|
|
317
|
+
updates.push(`"isDefault" = $${paramIndex++}`)
|
|
318
|
+
values.push(data.isDefault)
|
|
319
|
+
}
|
|
320
|
+
if (data.stages !== undefined) {
|
|
321
|
+
updates.push(`stages = $${paramIndex++}`)
|
|
322
|
+
values.push(data.stages ? JSON.stringify(data.stages) : null)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (updates.length === 0) throw new Error('No fields to update')
|
|
326
|
+
|
|
327
|
+
updates.push(`"updatedAt" = $${paramIndex++}`)
|
|
328
|
+
values.push(new Date().toISOString())
|
|
329
|
+
values.push(id)
|
|
330
|
+
|
|
331
|
+
const result = await mutateWithRLS<DbPipeline>(
|
|
332
|
+
`UPDATE pipelines SET ${updates.join(', ')} WHERE id = $${paramIndex}
|
|
333
|
+
RETURNING id, name, description, "isDefault", stages, "createdAt", "updatedAt"`,
|
|
334
|
+
values,
|
|
335
|
+
userId
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if (!result.rows[0]) throw new Error('Pipeline not found or update failed')
|
|
339
|
+
|
|
340
|
+
const pipeline = result.rows[0]
|
|
341
|
+
|
|
342
|
+
// Parse stages JSON
|
|
343
|
+
let stages: PipelineStage[] | undefined
|
|
344
|
+
if (pipeline.stages) {
|
|
345
|
+
try {
|
|
346
|
+
stages = JSON.parse(pipeline.stages)
|
|
347
|
+
} catch (error) {
|
|
348
|
+
console.error('Failed to parse pipeline stages:', error)
|
|
349
|
+
stages = undefined
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
id: pipeline.id,
|
|
355
|
+
name: pipeline.name,
|
|
356
|
+
description: pipeline.description ?? undefined,
|
|
357
|
+
isDefault: pipeline.isDefault ?? undefined,
|
|
358
|
+
stages,
|
|
359
|
+
createdAt: pipeline.createdAt,
|
|
360
|
+
updatedAt: pipeline.updatedAt,
|
|
361
|
+
}
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.error('PipelinesService.update error:', error)
|
|
364
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to update pipeline')
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Delete a pipeline
|
|
370
|
+
*/
|
|
371
|
+
static async delete(userId: string, id: string): Promise<boolean> {
|
|
372
|
+
try {
|
|
373
|
+
if (!userId?.trim()) throw new Error('User ID is required')
|
|
374
|
+
if (!id?.trim()) throw new Error('Pipeline ID is required')
|
|
375
|
+
|
|
376
|
+
const result = await mutateWithRLS(`DELETE FROM pipelines WHERE id = $1`, [id], userId)
|
|
377
|
+
return result.rowCount > 0
|
|
378
|
+
} catch (error) {
|
|
379
|
+
console.error('PipelinesService.delete error:', error)
|
|
380
|
+
throw new Error(error instanceof Error ? error.message : 'Failed to delete pipeline')
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline Service Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the PipelineService.
|
|
5
|
+
* Defines types for sales pipeline management including stage configuration,
|
|
6
|
+
* deal progression tracking, and pipeline customization.
|
|
7
|
+
*
|
|
8
|
+
* @module PipelineTypes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Type literals for select fields
|
|
12
|
+
export type PipelineType = 'sales' | 'support' | 'project' | 'custom'
|
|
13
|
+
|
|
14
|
+
// Pipeline stage interface
|
|
15
|
+
export interface PipelineStage {
|
|
16
|
+
id: string
|
|
17
|
+
name: string
|
|
18
|
+
probability: number
|
|
19
|
+
order: number
|
|
20
|
+
isClosed?: boolean
|
|
21
|
+
isWon?: boolean
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Main entity interface
|
|
25
|
+
export interface Pipeline {
|
|
26
|
+
id: string
|
|
27
|
+
teamId: string
|
|
28
|
+
name: string
|
|
29
|
+
description?: string | null
|
|
30
|
+
type?: PipelineType | null
|
|
31
|
+
isDefault?: boolean | null
|
|
32
|
+
isActive?: boolean | null
|
|
33
|
+
stages: PipelineStage[]
|
|
34
|
+
dealRottenDays?: number | null
|
|
35
|
+
createdAt: string
|
|
36
|
+
updatedAt: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// List options
|
|
40
|
+
export interface PipelineListOptions {
|
|
41
|
+
limit?: number
|
|
42
|
+
offset?: number
|
|
43
|
+
teamId?: string
|
|
44
|
+
type?: PipelineType
|
|
45
|
+
isDefault?: boolean
|
|
46
|
+
isActive?: boolean
|
|
47
|
+
orderBy?: 'name' | 'type' | 'isDefault' | 'isActive' | 'createdAt' | 'updatedAt'
|
|
48
|
+
orderDir?: 'asc' | 'desc'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// List result
|
|
52
|
+
export interface PipelineListResult {
|
|
53
|
+
pipelines: Pipeline[]
|
|
54
|
+
total: number
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Create data (required fields + teamId + optional fields)
|
|
58
|
+
export interface PipelineCreateData {
|
|
59
|
+
name: string
|
|
60
|
+
stages: PipelineStage[]
|
|
61
|
+
teamId: string
|
|
62
|
+
description?: string
|
|
63
|
+
type?: PipelineType
|
|
64
|
+
isDefault?: boolean
|
|
65
|
+
isActive?: boolean
|
|
66
|
+
dealRottenDays?: number
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Update data (all fields optional)
|
|
70
|
+
export interface PipelineUpdateData {
|
|
71
|
+
name?: string
|
|
72
|
+
description?: string | null
|
|
73
|
+
type?: PipelineType | null
|
|
74
|
+
isDefault?: boolean | null
|
|
75
|
+
isActive?: boolean | null
|
|
76
|
+
stages?: PipelineStage[]
|
|
77
|
+
dealRottenDays?: number | null
|
|
78
|
+
}
|