@open-mercato/core 0.4.5-develop-0f0e676c72 → 0.4.5-develop-e694581d9f
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/dist/generated/entities/customer_deal/index.js +4 -0
- package/dist/generated/entities/customer_deal/index.js.map +2 -2
- package/dist/generated/entities/customer_pipeline/index.js +17 -0
- package/dist/generated/entities/customer_pipeline/index.js.map +7 -0
- package/dist/generated/entities/customer_pipeline_stage/index.js +19 -0
- package/dist/generated/entities/customer_pipeline_stage/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +2 -0
- package/dist/generated/entities.ids.generated.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +4 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/customers/acl.js +2 -0
- package/dist/modules/customers/acl.js.map +2 -2
- package/dist/modules/customers/api/deals/[id]/route.js +4 -0
- package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
- package/dist/modules/customers/api/deals/route.js +12 -0
- package/dist/modules/customers/api/deals/route.js.map +2 -2
- package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -1
- package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js +69 -0
- package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +7 -0
- package/dist/modules/customers/api/pipeline-stages/route.js +275 -0
- package/dist/modules/customers/api/pipeline-stages/route.js.map +7 -0
- package/dist/modules/customers/api/pipelines/route.js +245 -0
- package/dist/modules/customers/api/pipelines/route.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/page.js +2 -0
- package/dist/modules/customers/backend/config/customers/page.js.map +2 -2
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +439 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +7 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +17 -0
- package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +7 -0
- package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -1
- package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/page.js +35 -1
- package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js +102 -74
- package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
- package/dist/modules/customers/cli.js +28 -2
- package/dist/modules/customers/cli.js.map +2 -2
- package/dist/modules/customers/commands/deals.js +34 -2
- package/dist/modules/customers/commands/deals.js.map +2 -2
- package/dist/modules/customers/commands/index.js +2 -0
- package/dist/modules/customers/commands/index.js.map +2 -2
- package/dist/modules/customers/commands/pipeline-stages.js +126 -0
- package/dist/modules/customers/commands/pipeline-stages.js.map +7 -0
- package/dist/modules/customers/commands/pipelines.js +87 -0
- package/dist/modules/customers/commands/pipelines.js.map +7 -0
- package/dist/modules/customers/components/DictionarySettings.js +0 -5
- package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
- package/dist/modules/customers/components/PipelineSettings.js +474 -0
- package/dist/modules/customers/components/PipelineSettings.js.map +7 -0
- package/dist/modules/customers/components/detail/DealForm.js +84 -12
- package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
- package/dist/modules/customers/data/entities.js +78 -0
- package/dist/modules/customers/data/entities.js.map +2 -2
- package/dist/modules/customers/data/validators.js +44 -0
- package/dist/modules/customers/data/validators.js.map +2 -2
- package/dist/modules/customers/migrations/Migration20260218191730.js +77 -0
- package/dist/modules/customers/migrations/Migration20260218191730.js.map +7 -0
- package/dist/modules/customers/setup.js +7 -3
- package/dist/modules/customers/setup.js.map +2 -2
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js +46 -44
- package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
- package/dist/modules/translations/api/context.js +10 -1
- package/dist/modules/translations/api/context.js.map +2 -2
- package/dist/modules/translations/commands/index.js +2 -0
- package/dist/modules/translations/commands/index.js.map +7 -0
- package/dist/modules/translations/commands/translations.js +160 -0
- package/dist/modules/translations/commands/translations.js.map +7 -0
- package/dist/modules/translations/index.js +1 -0
- package/dist/modules/translations/index.js.map +2 -2
- package/dist/modules/workflows/migrations/Migration20260222205305.js +14 -0
- package/dist/modules/workflows/migrations/Migration20260222205305.js.map +7 -0
- package/generated/entities/customer_deal/index.ts +2 -0
- package/generated/entities/customer_pipeline/index.ts +7 -0
- package/generated/entities/customer_pipeline_stage/index.ts +8 -0
- package/generated/entities.ids.generated.ts +2 -0
- package/generated/entity-fields-registry.ts +4 -0
- package/package.json +2 -2
- package/src/modules/customers/acl.ts +2 -0
- package/src/modules/customers/api/deals/[id]/route.ts +4 -0
- package/src/modules/customers/api/deals/route.ts +12 -0
- package/src/modules/customers/api/dictionaries/[kind]/route.ts +21 -1
- package/src/modules/customers/api/pipeline-stages/reorder/route.ts +71 -0
- package/src/modules/customers/api/pipeline-stages/route.ts +296 -0
- package/src/modules/customers/api/pipelines/route.ts +261 -0
- package/src/modules/customers/backend/config/customers/page.tsx +2 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +13 -0
- package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +512 -0
- package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
- package/src/modules/customers/backend/customers/deals/page.tsx +33 -1
- package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +119 -79
- package/src/modules/customers/cli.ts +29 -1
- package/src/modules/customers/commands/deals.ts +44 -1
- package/src/modules/customers/commands/index.ts +2 -0
- package/src/modules/customers/commands/pipeline-stages.ts +156 -0
- package/src/modules/customers/commands/pipelines.ts +105 -0
- package/src/modules/customers/components/DictionarySettings.tsx +0 -5
- package/src/modules/customers/components/PipelineSettings.tsx +570 -0
- package/src/modules/customers/components/detail/DealForm.tsx +89 -11
- package/src/modules/customers/data/entities.ts +64 -0
- package/src/modules/customers/data/validators.ts +57 -0
- package/src/modules/customers/i18n/de.json +4 -0
- package/src/modules/customers/i18n/en.json +4 -0
- package/src/modules/customers/i18n/es.json +4 -0
- package/src/modules/customers/i18n/pl.json +5 -1
- package/src/modules/customers/migrations/Migration20260218191730.ts +84 -0
- package/src/modules/customers/setup.ts +5 -1
- package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -60
- package/src/modules/translations/api/context.ts +12 -0
- package/src/modules/translations/commands/index.ts +1 -0
- package/src/modules/translations/commands/translations.ts +253 -0
- package/src/modules/translations/index.ts +1 -0
- package/src/modules/workflows/migrations/Migration20260222205305.ts +13 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/core",
|
|
3
|
-
"version": "0.4.5-develop-
|
|
3
|
+
"version": "0.4.5-develop-e694581d9f",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -207,7 +207,7 @@
|
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
209
|
"dependencies": {
|
|
210
|
-
"@open-mercato/shared": "0.4.5-develop-
|
|
210
|
+
"@open-mercato/shared": "0.4.5-develop-e694581d9f",
|
|
211
211
|
"@types/semver": "^7.5.8",
|
|
212
212
|
"@xyflow/react": "^12.6.0",
|
|
213
213
|
"ai": "^6.0.0",
|
|
@@ -8,6 +8,8 @@ export const features = [
|
|
|
8
8
|
{ id: 'customers.activities.view', title: 'View activities', module: 'customers' },
|
|
9
9
|
{ id: 'customers.activities.manage', title: 'Manage activities', module: 'customers' },
|
|
10
10
|
{ id: 'customers.settings.manage', title: 'Manage customer settings', module: 'customers' },
|
|
11
|
+
{ id: 'customers.pipelines.view', title: 'View pipelines', module: 'customers' },
|
|
12
|
+
{ id: 'customers.pipelines.manage', title: 'Manage pipelines', module: 'customers' },
|
|
11
13
|
{ id: 'customers.widgets.todos', title: 'Use customer todos widget', module: 'customers' },
|
|
12
14
|
{ id: 'customers.widgets.next-interactions', title: 'Use customer next interactions widget', module: 'customers' },
|
|
13
15
|
{ id: 'customers.widgets.new-customers', title: 'Use customer new customers widget', module: 'customers' },
|
|
@@ -212,6 +212,8 @@ export async function GET(request: Request, context: { params?: Record<string, u
|
|
|
212
212
|
description: deal.description ?? null,
|
|
213
213
|
status: deal.status ?? null,
|
|
214
214
|
pipelineStage: deal.pipelineStage ?? null,
|
|
215
|
+
pipelineId: deal.pipelineId ?? null,
|
|
216
|
+
pipelineStageId: deal.pipelineStageId ?? null,
|
|
215
217
|
valueAmount: deal.valueAmount ?? null,
|
|
216
218
|
valueCurrency: deal.valueCurrency ?? null,
|
|
217
219
|
probability: deal.probability ?? null,
|
|
@@ -241,6 +243,8 @@ const dealDetailResponseSchema = z.object({
|
|
|
241
243
|
description: z.string().nullable().optional(),
|
|
242
244
|
status: z.string().nullable().optional(),
|
|
243
245
|
pipelineStage: z.string().nullable().optional(),
|
|
246
|
+
pipelineId: z.string().uuid().nullable().optional(),
|
|
247
|
+
pipelineStageId: z.string().uuid().nullable().optional(),
|
|
244
248
|
valueAmount: z.number().nullable().optional(),
|
|
245
249
|
valueCurrency: z.string().nullable().optional(),
|
|
246
250
|
probability: z.number().nullable().optional(),
|
|
@@ -25,6 +25,8 @@ const listSchema = z
|
|
|
25
25
|
search: z.string().optional(),
|
|
26
26
|
status: z.string().optional(),
|
|
27
27
|
pipelineStage: z.string().optional(),
|
|
28
|
+
pipelineId: z.string().uuid().optional(),
|
|
29
|
+
pipelineStageId: z.string().uuid().optional(),
|
|
28
30
|
sortField: z.string().optional(),
|
|
29
31
|
sortDir: z.enum(['asc', 'desc']).optional(),
|
|
30
32
|
personEntityId: z.string().uuid().optional(),
|
|
@@ -98,6 +100,8 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
|
|
|
98
100
|
'description',
|
|
99
101
|
'status',
|
|
100
102
|
'pipeline_stage',
|
|
103
|
+
'pipeline_id',
|
|
104
|
+
'pipeline_stage_id',
|
|
101
105
|
'value_amount',
|
|
102
106
|
'value_currency',
|
|
103
107
|
'probability',
|
|
@@ -129,6 +133,12 @@ const crud = makeCrudRoute<unknown, unknown, DealListQuery>({
|
|
|
129
133
|
if (query.pipelineStage) {
|
|
130
134
|
filters.pipeline_stage = { $eq: query.pipelineStage }
|
|
131
135
|
}
|
|
136
|
+
if (query.pipelineId) {
|
|
137
|
+
filters.pipeline_id = { $eq: query.pipelineId }
|
|
138
|
+
}
|
|
139
|
+
if (query.pipelineStageId) {
|
|
140
|
+
filters.pipeline_stage_id = { $eq: query.pipelineStageId }
|
|
141
|
+
}
|
|
132
142
|
return filters
|
|
133
143
|
},
|
|
134
144
|
},
|
|
@@ -394,6 +404,8 @@ const dealListItemSchema = z
|
|
|
394
404
|
description: z.string().nullable().optional(),
|
|
395
405
|
status: z.string().nullable().optional(),
|
|
396
406
|
pipeline_stage: z.string().nullable().optional(),
|
|
407
|
+
pipeline_id: z.string().uuid().nullable().optional(),
|
|
408
|
+
pipeline_stage_id: z.string().uuid().nullable().optional(),
|
|
397
409
|
value_amount: z.number().nullable().optional(),
|
|
398
410
|
value_currency: z.string().nullable().optional(),
|
|
399
411
|
probability: z.number().nullable().optional(),
|
|
@@ -4,7 +4,8 @@ import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
|
4
4
|
import type { CommandBus } from '@open-mercato/shared/lib/commands'
|
|
5
5
|
import type { CommandExecuteResult } from '@open-mercato/shared/lib/commands/types'
|
|
6
6
|
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
7
|
-
import { CustomerDictionaryEntry } from '../../../data/entities'
|
|
7
|
+
import { CustomerDictionaryEntry, CustomerPipelineStage } from '../../../data/entities'
|
|
8
|
+
import { ensureDictionaryEntry } from '../../../commands/shared'
|
|
8
9
|
import { mapDictionaryKind, resolveDictionaryRouteContext } from '../context'
|
|
9
10
|
import { createDictionaryCacheKey, createDictionaryCacheTags, invalidateDictionaryCache, DICTIONARY_CACHE_TTL_MS } from '../cache'
|
|
10
11
|
import { z } from 'zod'
|
|
@@ -48,6 +49,25 @@ export async function GET(req: Request, ctx: { params?: { kind?: string } }) {
|
|
|
48
49
|
{ orderBy: { label: 'asc' } }
|
|
49
50
|
)
|
|
50
51
|
|
|
52
|
+
if (mappedKind === 'pipeline_stage' && organizationId) {
|
|
53
|
+
const existingNormalized = new Set(entries.map((e) => e.normalizedValue))
|
|
54
|
+
const pipelineStages = await em.find(CustomerPipelineStage, { organizationId, tenantId })
|
|
55
|
+
for (const stage of pipelineStages) {
|
|
56
|
+
if (!existingNormalized.has(stage.label.trim().toLowerCase())) {
|
|
57
|
+
const created = await ensureDictionaryEntry(em, {
|
|
58
|
+
tenantId,
|
|
59
|
+
organizationId,
|
|
60
|
+
kind: 'pipeline_stage',
|
|
61
|
+
value: stage.label,
|
|
62
|
+
})
|
|
63
|
+
if (created) {
|
|
64
|
+
entries.push(created)
|
|
65
|
+
existingNormalized.add(created.normalizedValue)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
51
71
|
const byValue = new Map<string, { entry: CustomerDictionaryEntry; isInherited: boolean; order: number }>()
|
|
52
72
|
for (const entry of entries) {
|
|
53
73
|
const normalized = entry.normalizedValue
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
6
|
+
import type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'
|
|
7
|
+
import { pipelineStageReorderSchema, type PipelineStageReorderInput } from '../../../data/validators'
|
|
8
|
+
import { withScopedPayload } from '../../utils'
|
|
9
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
10
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
11
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
12
|
+
|
|
13
|
+
export const metadata = {
|
|
14
|
+
POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(req: Request) {
|
|
18
|
+
try {
|
|
19
|
+
const container = await createRequestContainer()
|
|
20
|
+
const auth = await getAuthFromRequest(req)
|
|
21
|
+
const { translate } = await resolveTranslations()
|
|
22
|
+
if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })
|
|
23
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
24
|
+
const ctx: CommandRuntimeContext = {
|
|
25
|
+
container,
|
|
26
|
+
auth,
|
|
27
|
+
organizationScope: scope,
|
|
28
|
+
selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
|
|
29
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
30
|
+
request: req,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const body = await req.json().catch(() => ({}))
|
|
34
|
+
const scoped = withScopedPayload(body, ctx, translate)
|
|
35
|
+
|
|
36
|
+
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
37
|
+
await commandBus.execute<PipelineStageReorderInput, void>(
|
|
38
|
+
'customers.pipeline-stages.reorder',
|
|
39
|
+
{ input: pipelineStageReorderSchema.parse(scoped), ctx },
|
|
40
|
+
)
|
|
41
|
+
return NextResponse.json({ ok: true })
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err instanceof CrudHttpError) {
|
|
44
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
45
|
+
}
|
|
46
|
+
console.error('customers.pipeline-stages.reorder failed', err)
|
|
47
|
+
return NextResponse.json({ error: 'Failed to reorder pipeline stages' }, { status: 400 })
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const reorderOkResponseSchema = z.object({ ok: z.boolean() })
|
|
52
|
+
const reorderErrorSchema = z.object({ error: z.string() })
|
|
53
|
+
|
|
54
|
+
export const openApi: OpenApiRouteDoc = {
|
|
55
|
+
tag: 'Customers',
|
|
56
|
+
summary: 'Reorder pipeline stages',
|
|
57
|
+
methods: {
|
|
58
|
+
POST: {
|
|
59
|
+
summary: 'Reorder pipeline stages',
|
|
60
|
+
description: 'Updates the order of pipeline stages in bulk.',
|
|
61
|
+
requestBody: { contentType: 'application/json', schema: pipelineStageReorderSchema },
|
|
62
|
+
responses: [
|
|
63
|
+
{ status: 200, description: 'Stages reordered', schema: reorderOkResponseSchema },
|
|
64
|
+
],
|
|
65
|
+
errors: [
|
|
66
|
+
{ status: 400, description: 'Validation failed', schema: reorderErrorSchema },
|
|
67
|
+
{ status: 401, description: 'Unauthorized', schema: reorderErrorSchema },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
|
|
4
|
+
import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
|
|
5
|
+
import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
|
|
6
|
+
import type { EntityManager } from '@mikro-orm/postgresql'
|
|
7
|
+
import type { CommandRuntimeContext, CommandBus } from '@open-mercato/shared/lib/commands'
|
|
8
|
+
import { CustomerPipelineStage, CustomerDictionaryEntry } from '../../data/entities'
|
|
9
|
+
import {
|
|
10
|
+
pipelineStageCreateSchema,
|
|
11
|
+
pipelineStageUpdateSchema,
|
|
12
|
+
pipelineStageDeleteSchema,
|
|
13
|
+
type PipelineStageCreateInput,
|
|
14
|
+
type PipelineStageUpdateInput,
|
|
15
|
+
type PipelineStageDeleteInput,
|
|
16
|
+
} from '../../data/validators'
|
|
17
|
+
import { withScopedPayload } from '../utils'
|
|
18
|
+
import { ensureDictionaryEntry } from '../../commands/shared'
|
|
19
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
20
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
21
|
+
import { serializeOperationMetadata } from '@open-mercato/shared/lib/commands/operationMetadata'
|
|
22
|
+
import type { OpenApiRouteDoc } from '@open-mercato/shared/lib/openapi'
|
|
23
|
+
|
|
24
|
+
export const metadata = {
|
|
25
|
+
GET: { requireAuth: true, requireFeatures: ['customers.pipelines.view'] },
|
|
26
|
+
POST: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
27
|
+
PUT: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
28
|
+
DELETE: { requireAuth: true, requireFeatures: ['customers.pipelines.manage'] },
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function buildContext(
|
|
32
|
+
req: Request
|
|
33
|
+
): Promise<{ ctx: CommandRuntimeContext; organizationId: string | null; tenantId: string | null }> {
|
|
34
|
+
const container = await createRequestContainer()
|
|
35
|
+
const auth = await getAuthFromRequest(req)
|
|
36
|
+
const { translate } = await resolveTranslations()
|
|
37
|
+
if (!auth) throw new CrudHttpError(401, { error: translate('customers.errors.unauthorized', 'Unauthorized') })
|
|
38
|
+
const scope = await resolveOrganizationScopeForRequest({ container, auth, request: req })
|
|
39
|
+
const ctx: CommandRuntimeContext = {
|
|
40
|
+
container,
|
|
41
|
+
auth,
|
|
42
|
+
organizationScope: scope,
|
|
43
|
+
selectedOrganizationId: scope?.selectedId ?? auth.orgId ?? null,
|
|
44
|
+
organizationIds: scope?.filterIds ?? (auth.orgId ? [auth.orgId] : null),
|
|
45
|
+
request: req,
|
|
46
|
+
}
|
|
47
|
+
const organizationId = scope?.selectedId ?? auth.orgId ?? null
|
|
48
|
+
const tenantId = auth.tenantId ?? null
|
|
49
|
+
return { ctx, organizationId, tenantId }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function GET(req: Request) {
|
|
53
|
+
try {
|
|
54
|
+
const { ctx, organizationId, tenantId } = await buildContext(req)
|
|
55
|
+
if (!organizationId || !tenantId) {
|
|
56
|
+
return NextResponse.json({ error: 'Organization and tenant context required' }, { status: 400 })
|
|
57
|
+
}
|
|
58
|
+
const url = new URL(req.url)
|
|
59
|
+
const pipelineId = url.searchParams.get('pipelineId')
|
|
60
|
+
|
|
61
|
+
const em = (ctx.container.resolve('em') as EntityManager)
|
|
62
|
+
const where: Record<string, unknown> = { organizationId, tenantId }
|
|
63
|
+
if (pipelineId) where.pipelineId = pipelineId
|
|
64
|
+
|
|
65
|
+
const stages = await em.find(CustomerPipelineStage, where, { orderBy: { order: 'ASC' } })
|
|
66
|
+
|
|
67
|
+
const stageLabels = stages.map((s) => s.label.trim().toLowerCase())
|
|
68
|
+
const dictEntries = stageLabels.length
|
|
69
|
+
? await em.find(CustomerDictionaryEntry, {
|
|
70
|
+
organizationId,
|
|
71
|
+
tenantId,
|
|
72
|
+
kind: 'pipeline_stage',
|
|
73
|
+
normalizedValue: { $in: stageLabels },
|
|
74
|
+
})
|
|
75
|
+
: []
|
|
76
|
+
const dictByNormalized = new Map<string, CustomerDictionaryEntry>()
|
|
77
|
+
dictEntries.forEach((entry) => dictByNormalized.set(entry.normalizedValue, entry))
|
|
78
|
+
|
|
79
|
+
const missingStages = stages.filter((s) => !dictByNormalized.has(s.label.trim().toLowerCase()))
|
|
80
|
+
if (missingStages.length) {
|
|
81
|
+
for (const stage of missingStages) {
|
|
82
|
+
const created = await ensureDictionaryEntry(em, {
|
|
83
|
+
tenantId,
|
|
84
|
+
organizationId,
|
|
85
|
+
kind: 'pipeline_stage',
|
|
86
|
+
value: stage.label,
|
|
87
|
+
})
|
|
88
|
+
if (created) dictByNormalized.set(created.normalizedValue, created)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const items = stages.map((stage) => {
|
|
93
|
+
const dictEntry = dictByNormalized.get(stage.label.trim().toLowerCase())
|
|
94
|
+
return {
|
|
95
|
+
id: stage.id,
|
|
96
|
+
pipelineId: stage.pipelineId,
|
|
97
|
+
label: stage.label,
|
|
98
|
+
order: stage.order,
|
|
99
|
+
color: dictEntry?.color ?? null,
|
|
100
|
+
icon: dictEntry?.icon ?? null,
|
|
101
|
+
organizationId: stage.organizationId,
|
|
102
|
+
tenantId: stage.tenantId,
|
|
103
|
+
createdAt: stage.createdAt,
|
|
104
|
+
updatedAt: stage.updatedAt,
|
|
105
|
+
}
|
|
106
|
+
})
|
|
107
|
+
return NextResponse.json({ items, total: items.length })
|
|
108
|
+
} catch (err) {
|
|
109
|
+
if (err instanceof CrudHttpError) {
|
|
110
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
111
|
+
}
|
|
112
|
+
console.error('customers.pipeline-stages GET failed', err)
|
|
113
|
+
return NextResponse.json({ error: 'Failed to load pipeline stages' }, { status: 500 })
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function POST(req: Request) {
|
|
118
|
+
try {
|
|
119
|
+
const { ctx } = await buildContext(req)
|
|
120
|
+
const body = await req.json().catch(() => ({}))
|
|
121
|
+
const { translate } = await resolveTranslations()
|
|
122
|
+
const scoped = withScopedPayload(body, ctx, translate)
|
|
123
|
+
|
|
124
|
+
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
125
|
+
const { result, logEntry } = await commandBus.execute<PipelineStageCreateInput, { stageId: string }>(
|
|
126
|
+
'customers.pipeline-stages.create',
|
|
127
|
+
{ input: pipelineStageCreateSchema.parse(scoped), ctx },
|
|
128
|
+
)
|
|
129
|
+
const response = NextResponse.json({ id: result?.stageId ?? null }, { status: 201 })
|
|
130
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
131
|
+
response.headers.set(
|
|
132
|
+
'x-om-operation',
|
|
133
|
+
serializeOperationMetadata({
|
|
134
|
+
id: logEntry.id,
|
|
135
|
+
undoToken: logEntry.undoToken,
|
|
136
|
+
commandId: logEntry.commandId,
|
|
137
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
138
|
+
resourceKind: logEntry.resourceKind ?? 'customers.pipelineStage',
|
|
139
|
+
resourceId: logEntry.resourceId ?? result?.stageId ?? null,
|
|
140
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
|
|
141
|
+
})
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
return response
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err instanceof CrudHttpError) {
|
|
147
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
148
|
+
}
|
|
149
|
+
console.error('customers.pipeline-stages POST failed', err)
|
|
150
|
+
return NextResponse.json({ error: 'Failed to create pipeline stage' }, { status: 400 })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function PUT(req: Request) {
|
|
155
|
+
try {
|
|
156
|
+
const { ctx } = await buildContext(req)
|
|
157
|
+
const body = await req.json().catch(() => ({}))
|
|
158
|
+
const { translate } = await resolveTranslations()
|
|
159
|
+
const scoped = withScopedPayload(body, ctx, translate)
|
|
160
|
+
|
|
161
|
+
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
162
|
+
const { logEntry } = await commandBus.execute<PipelineStageUpdateInput, void>(
|
|
163
|
+
'customers.pipeline-stages.update',
|
|
164
|
+
{ input: pipelineStageUpdateSchema.parse(scoped), ctx },
|
|
165
|
+
)
|
|
166
|
+
const response = NextResponse.json({ ok: true })
|
|
167
|
+
if (logEntry?.undoToken && logEntry?.id && logEntry?.commandId) {
|
|
168
|
+
response.headers.set(
|
|
169
|
+
'x-om-operation',
|
|
170
|
+
serializeOperationMetadata({
|
|
171
|
+
id: logEntry.id,
|
|
172
|
+
undoToken: logEntry.undoToken,
|
|
173
|
+
commandId: logEntry.commandId,
|
|
174
|
+
actionLabel: logEntry.actionLabel ?? null,
|
|
175
|
+
resourceKind: logEntry.resourceKind ?? 'customers.pipelineStage',
|
|
176
|
+
resourceId: logEntry.resourceId ?? null,
|
|
177
|
+
executedAt: logEntry.createdAt instanceof Date ? logEntry.createdAt.toISOString() : undefined,
|
|
178
|
+
})
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
return response
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (err instanceof CrudHttpError) {
|
|
184
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
185
|
+
}
|
|
186
|
+
console.error('customers.pipeline-stages PUT failed', err)
|
|
187
|
+
return NextResponse.json({ error: 'Failed to update pipeline stage' }, { status: 400 })
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function DELETE(req: Request) {
|
|
192
|
+
try {
|
|
193
|
+
const { ctx } = await buildContext(req)
|
|
194
|
+
const body = await req.json().catch(() => ({}))
|
|
195
|
+
const { translate } = await resolveTranslations()
|
|
196
|
+
const scoped = withScopedPayload(body, ctx, translate)
|
|
197
|
+
|
|
198
|
+
const commandBus = (ctx.container.resolve('commandBus') as CommandBus)
|
|
199
|
+
await commandBus.execute<PipelineStageDeleteInput, void>(
|
|
200
|
+
'customers.pipeline-stages.delete',
|
|
201
|
+
{ input: pipelineStageDeleteSchema.parse(scoped), ctx },
|
|
202
|
+
)
|
|
203
|
+
return NextResponse.json({ ok: true })
|
|
204
|
+
} catch (err) {
|
|
205
|
+
if (err instanceof CrudHttpError) {
|
|
206
|
+
return NextResponse.json(err.body, { status: err.status })
|
|
207
|
+
}
|
|
208
|
+
console.error('customers.pipeline-stages DELETE failed', err)
|
|
209
|
+
return NextResponse.json({ error: 'Failed to delete pipeline stage' }, { status: 400 })
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const stageItemSchema = z.object({
|
|
214
|
+
id: z.string().uuid(),
|
|
215
|
+
pipelineId: z.string().uuid(),
|
|
216
|
+
label: z.string(),
|
|
217
|
+
order: z.number(),
|
|
218
|
+
color: z.string().nullable(),
|
|
219
|
+
icon: z.string().nullable(),
|
|
220
|
+
organizationId: z.string().uuid(),
|
|
221
|
+
tenantId: z.string().uuid(),
|
|
222
|
+
createdAt: z.date(),
|
|
223
|
+
updatedAt: z.date(),
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
const stageListResponseSchema = z.object({
|
|
227
|
+
items: z.array(stageItemSchema),
|
|
228
|
+
total: z.number(),
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
const stageCreateResponseSchema = z.object({
|
|
232
|
+
id: z.string().uuid().nullable(),
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
const stageOkResponseSchema = z.object({
|
|
236
|
+
ok: z.boolean(),
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const stageErrorSchema = z.object({
|
|
240
|
+
error: z.string(),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
export const openApi: OpenApiRouteDoc = {
|
|
244
|
+
tag: 'Customers',
|
|
245
|
+
summary: 'Manage pipeline stages',
|
|
246
|
+
methods: {
|
|
247
|
+
GET: {
|
|
248
|
+
summary: 'List pipeline stages',
|
|
249
|
+
description: 'Returns pipeline stages for the authenticated organization, optionally filtered by pipelineId.',
|
|
250
|
+
query: z.object({ pipelineId: z.string().uuid().optional() }),
|
|
251
|
+
responses: [
|
|
252
|
+
{ status: 200, description: 'Stage list', schema: stageListResponseSchema },
|
|
253
|
+
],
|
|
254
|
+
errors: [
|
|
255
|
+
{ status: 401, description: 'Unauthorized', schema: stageErrorSchema },
|
|
256
|
+
{ status: 400, description: 'Invalid request', schema: stageErrorSchema },
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
POST: {
|
|
260
|
+
summary: 'Create pipeline stage',
|
|
261
|
+
description: 'Creates a new pipeline stage.',
|
|
262
|
+
requestBody: { contentType: 'application/json', schema: pipelineStageCreateSchema },
|
|
263
|
+
responses: [
|
|
264
|
+
{ status: 201, description: 'Stage created', schema: stageCreateResponseSchema },
|
|
265
|
+
],
|
|
266
|
+
errors: [
|
|
267
|
+
{ status: 400, description: 'Validation failed', schema: stageErrorSchema },
|
|
268
|
+
{ status: 401, description: 'Unauthorized', schema: stageErrorSchema },
|
|
269
|
+
],
|
|
270
|
+
},
|
|
271
|
+
PUT: {
|
|
272
|
+
summary: 'Update pipeline stage',
|
|
273
|
+
description: 'Updates an existing pipeline stage.',
|
|
274
|
+
requestBody: { contentType: 'application/json', schema: pipelineStageUpdateSchema },
|
|
275
|
+
responses: [
|
|
276
|
+
{ status: 200, description: 'Stage updated', schema: stageOkResponseSchema },
|
|
277
|
+
],
|
|
278
|
+
errors: [
|
|
279
|
+
{ status: 400, description: 'Validation failed', schema: stageErrorSchema },
|
|
280
|
+
{ status: 404, description: 'Stage not found', schema: stageErrorSchema },
|
|
281
|
+
],
|
|
282
|
+
},
|
|
283
|
+
DELETE: {
|
|
284
|
+
summary: 'Delete pipeline stage',
|
|
285
|
+
description: 'Deletes a pipeline stage. Returns 409 if active deals use this stage.',
|
|
286
|
+
requestBody: { contentType: 'application/json', schema: pipelineStageDeleteSchema },
|
|
287
|
+
responses: [
|
|
288
|
+
{ status: 200, description: 'Stage deleted', schema: stageOkResponseSchema },
|
|
289
|
+
],
|
|
290
|
+
errors: [
|
|
291
|
+
{ status: 409, description: 'Stage has active deals', schema: stageErrorSchema },
|
|
292
|
+
{ status: 404, description: 'Stage not found', schema: stageErrorSchema },
|
|
293
|
+
],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
}
|