@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
|
@@ -20,6 +20,8 @@ export type DealFormBaseValues = {
|
|
|
20
20
|
title: string
|
|
21
21
|
status?: string | null
|
|
22
22
|
pipelineStage?: string | null
|
|
23
|
+
pipelineId?: string | null
|
|
24
|
+
pipelineStageId?: string | null
|
|
23
25
|
valueAmount?: number | null
|
|
24
26
|
valueCurrency?: string | null
|
|
25
27
|
probability?: number | null
|
|
@@ -88,6 +90,8 @@ const schema = z.object({
|
|
|
88
90
|
.trim()
|
|
89
91
|
.max(100, 'customers.people.detail.deals.pipelineTooLong')
|
|
90
92
|
.optional(),
|
|
93
|
+
pipelineId: z.string().uuid().optional(),
|
|
94
|
+
pipelineStageId: z.string().uuid().optional(),
|
|
91
95
|
valueAmount: z
|
|
92
96
|
.preprocess((value) => {
|
|
93
97
|
if (value === '' || value === null || value === undefined) return undefined
|
|
@@ -441,7 +445,6 @@ export function DealForm({
|
|
|
441
445
|
|
|
442
446
|
const dictionaryLabels = React.useMemo(() => ({
|
|
443
447
|
status: createDictionarySelectLabels('deal-statuses', translate),
|
|
444
|
-
pipeline: createDictionarySelectLabels('pipeline-stages', translate),
|
|
445
448
|
}), [translate])
|
|
446
449
|
|
|
447
450
|
const resolvedCurrencyError = React.useMemo(() => {
|
|
@@ -577,6 +580,51 @@ export function DealForm({
|
|
|
577
580
|
const disabled = pending || isSubmitting
|
|
578
581
|
const canDelete = mode === 'edit' && typeof onDelete === 'function'
|
|
579
582
|
|
|
583
|
+
type PipelineOption = { id: string; name: string; isDefault: boolean }
|
|
584
|
+
type PipelineStageOption = { id: string; label: string; order: number }
|
|
585
|
+
|
|
586
|
+
const [pipelines, setPipelines] = React.useState<PipelineOption[]>([])
|
|
587
|
+
const [pipelineStages, setPipelineStages] = React.useState<PipelineStageOption[]>([])
|
|
588
|
+
|
|
589
|
+
const loadStagesForPipeline = React.useCallback(async (pipelineId: string) => {
|
|
590
|
+
if (!pipelineId) {
|
|
591
|
+
setPipelineStages([])
|
|
592
|
+
return
|
|
593
|
+
}
|
|
594
|
+
try {
|
|
595
|
+
const call = await apiCall<{ items: PipelineStageOption[] }>(`/api/customers/pipeline-stages?pipelineId=${encodeURIComponent(pipelineId)}`)
|
|
596
|
+
if (call.ok && call.result?.items) {
|
|
597
|
+
const sorted = [...call.result.items].sort((a, b) => a.order - b.order)
|
|
598
|
+
setPipelineStages(sorted)
|
|
599
|
+
}
|
|
600
|
+
} catch {
|
|
601
|
+
setPipelineStages([])
|
|
602
|
+
}
|
|
603
|
+
}, [])
|
|
604
|
+
|
|
605
|
+
React.useEffect(() => {
|
|
606
|
+
let cancelled = false
|
|
607
|
+
;(async () => {
|
|
608
|
+
try {
|
|
609
|
+
const call = await apiCall<{ items: PipelineOption[] }>('/api/customers/pipelines')
|
|
610
|
+
if (cancelled) return
|
|
611
|
+
if (call.ok && call.result?.items) {
|
|
612
|
+
setPipelines(call.result.items)
|
|
613
|
+
}
|
|
614
|
+
} catch {
|
|
615
|
+
// ignore
|
|
616
|
+
}
|
|
617
|
+
})().catch(() => {})
|
|
618
|
+
return () => { cancelled = true }
|
|
619
|
+
}, [])
|
|
620
|
+
|
|
621
|
+
React.useEffect(() => {
|
|
622
|
+
const pid = initialValues?.pipelineId
|
|
623
|
+
if (typeof pid === 'string' && pid.length) {
|
|
624
|
+
loadStagesForPipeline(pid).catch(() => {})
|
|
625
|
+
}
|
|
626
|
+
}, [initialValues?.pipelineId, loadStagesForPipeline])
|
|
627
|
+
|
|
580
628
|
const baseFields = React.useMemo<CrudField[]>(() => [
|
|
581
629
|
{
|
|
582
630
|
id: 'title',
|
|
@@ -600,18 +648,44 @@ export function DealForm({
|
|
|
600
648
|
),
|
|
601
649
|
} as CrudField,
|
|
602
650
|
{
|
|
603
|
-
id: '
|
|
651
|
+
id: 'pipelineId',
|
|
652
|
+
label: t('customers.people.detail.deals.fields.pipeline', 'Pipeline'),
|
|
653
|
+
type: 'custom',
|
|
654
|
+
layout: 'half',
|
|
655
|
+
component: ({ value, setValue }) => (
|
|
656
|
+
<select
|
|
657
|
+
className="w-full rounded border px-2 py-1.5 text-sm"
|
|
658
|
+
value={typeof value === 'string' ? value : ''}
|
|
659
|
+
onChange={(e) => {
|
|
660
|
+
setValue(e.target.value)
|
|
661
|
+
loadStagesForPipeline(e.target.value).catch(() => {})
|
|
662
|
+
}}
|
|
663
|
+
disabled={disabled}
|
|
664
|
+
>
|
|
665
|
+
<option value="">{t('customers.deals.form.pipeline.placeholder', 'Select pipeline…')}</option>
|
|
666
|
+
{pipelines.map((p) => (
|
|
667
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
668
|
+
))}
|
|
669
|
+
</select>
|
|
670
|
+
),
|
|
671
|
+
} as CrudField,
|
|
672
|
+
{
|
|
673
|
+
id: 'pipelineStageId',
|
|
604
674
|
label: t('customers.people.detail.deals.fields.pipelineStage', 'Pipeline stage'),
|
|
605
675
|
type: 'custom',
|
|
606
676
|
layout: 'half',
|
|
607
677
|
component: ({ value, setValue }) => (
|
|
608
|
-
<
|
|
609
|
-
|
|
610
|
-
value={typeof value === 'string' ? value :
|
|
611
|
-
onChange={(
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
678
|
+
<select
|
|
679
|
+
className="w-full rounded border px-2 py-1.5 text-sm"
|
|
680
|
+
value={typeof value === 'string' ? value : ''}
|
|
681
|
+
onChange={(e) => setValue(e.target.value)}
|
|
682
|
+
disabled={disabled || !pipelineStages.length}
|
|
683
|
+
>
|
|
684
|
+
<option value="">{t('customers.deals.form.pipelineStage.placeholder', 'Select stage…')}</option>
|
|
685
|
+
{pipelineStages.map((s) => (
|
|
686
|
+
<option key={s.id} value={s.id}>{s.label}</option>
|
|
687
|
+
))}
|
|
688
|
+
</select>
|
|
615
689
|
),
|
|
616
690
|
} as CrudField,
|
|
617
691
|
{
|
|
@@ -703,14 +777,14 @@ export function DealForm({
|
|
|
703
777
|
/>
|
|
704
778
|
),
|
|
705
779
|
} as CrudField,
|
|
706
|
-
], [currencyDictionaryLabels, fetchCurrencyOptions, resolvedCurrencyError,
|
|
780
|
+
], [currencyDictionaryLabels, fetchCurrencyOptions, resolvedCurrencyError, pipelines, pipelineStages, loadStagesForPipeline, dictionaryLabels.status, disabled, fetchCompaniesByIds, fetchPeopleByIds, searchCompanies, searchPeople, t])
|
|
707
781
|
|
|
708
782
|
const groups = React.useMemo<CrudFormGroup[]>(() => [
|
|
709
783
|
{
|
|
710
784
|
id: 'details',
|
|
711
785
|
title: t('customers.people.detail.deals.form.details', 'Deal details'),
|
|
712
786
|
column: 1,
|
|
713
|
-
fields: ['title', 'status', '
|
|
787
|
+
fields: ['title', 'status', 'pipelineId', 'pipelineStageId', 'valueAmount', 'valueCurrency', 'probability', 'expectedCloseAt', 'description'],
|
|
714
788
|
},
|
|
715
789
|
{
|
|
716
790
|
id: 'associations',
|
|
@@ -756,6 +830,8 @@ export function DealForm({
|
|
|
756
830
|
title: initialValues?.title ?? '',
|
|
757
831
|
status: initialValues?.status ?? '',
|
|
758
832
|
pipelineStage: initialValues?.pipelineStage ?? '',
|
|
833
|
+
pipelineId: initialValues?.pipelineId ?? (typeof (initialValues as Record<string, unknown>)?.pipeline_id === 'string' ? (initialValues as Record<string, unknown>).pipeline_id as string : ''),
|
|
834
|
+
pipelineStageId: initialValues?.pipelineStageId ?? (typeof (initialValues as Record<string, unknown>)?.pipeline_stage_id === 'string' ? (initialValues as Record<string, unknown>).pipeline_stage_id as string : ''),
|
|
759
835
|
valueAmount: normalizeNumber(initialValues?.valueAmount ?? null),
|
|
760
836
|
valueCurrency: normalizeCurrency(initialValues?.valueCurrency ?? null),
|
|
761
837
|
probability: normalizeNumber(initialValues?.probability ?? null),
|
|
@@ -790,6 +866,8 @@ export function DealForm({
|
|
|
790
866
|
title: parsed.data.title,
|
|
791
867
|
status: parsed.data.status || undefined,
|
|
792
868
|
pipelineStage: parsed.data.pipelineStage || undefined,
|
|
869
|
+
pipelineId: parsed.data.pipelineId || undefined,
|
|
870
|
+
pipelineStageId: parsed.data.pipelineStageId || undefined,
|
|
793
871
|
valueAmount:
|
|
794
872
|
typeof parsed.data.valueAmount === 'number' ? parsed.data.valueAmount : undefined,
|
|
795
873
|
valueCurrency: parsed.data.valueCurrency || undefined,
|
|
@@ -278,6 +278,12 @@ export class CustomerDeal {
|
|
|
278
278
|
@Property({ name: 'pipeline_stage', type: 'text', nullable: true })
|
|
279
279
|
pipelineStage?: string | null
|
|
280
280
|
|
|
281
|
+
@Property({ name: 'pipeline_id', type: 'uuid', nullable: true })
|
|
282
|
+
pipelineId?: string | null
|
|
283
|
+
|
|
284
|
+
@Property({ name: 'pipeline_stage_id', type: 'uuid', nullable: true })
|
|
285
|
+
pipelineStageId?: string | null
|
|
286
|
+
|
|
281
287
|
@Property({ name: 'value_amount', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
|
282
288
|
valueAmount?: string | null
|
|
283
289
|
|
|
@@ -646,6 +652,64 @@ export class CustomerDictionaryEntry {
|
|
|
646
652
|
updatedAt: Date = new Date()
|
|
647
653
|
}
|
|
648
654
|
|
|
655
|
+
@Entity({ tableName: 'customer_pipelines' })
|
|
656
|
+
@Index({ name: 'customer_pipelines_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
|
|
657
|
+
export class CustomerPipeline {
|
|
658
|
+
[OptionalProps]?: 'isDefault' | 'createdAt' | 'updatedAt'
|
|
659
|
+
|
|
660
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
661
|
+
id!: string
|
|
662
|
+
|
|
663
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
664
|
+
organizationId!: string
|
|
665
|
+
|
|
666
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
667
|
+
tenantId!: string
|
|
668
|
+
|
|
669
|
+
@Property({ type: 'text' })
|
|
670
|
+
name!: string
|
|
671
|
+
|
|
672
|
+
@Property({ name: 'is_default', type: 'boolean', default: false })
|
|
673
|
+
isDefault: boolean = false
|
|
674
|
+
|
|
675
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
676
|
+
createdAt: Date = new Date()
|
|
677
|
+
|
|
678
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
679
|
+
updatedAt: Date = new Date()
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
@Entity({ tableName: 'customer_pipeline_stages' })
|
|
683
|
+
@Index({ name: 'customer_pipeline_stages_pipeline_position_idx', properties: ['pipelineId', 'order'] })
|
|
684
|
+
@Index({ name: 'customer_pipeline_stages_org_tenant_idx', properties: ['organizationId', 'tenantId'] })
|
|
685
|
+
export class CustomerPipelineStage {
|
|
686
|
+
[OptionalProps]?: 'order' | 'createdAt' | 'updatedAt'
|
|
687
|
+
|
|
688
|
+
@PrimaryKey({ type: 'uuid', defaultRaw: 'gen_random_uuid()' })
|
|
689
|
+
id!: string
|
|
690
|
+
|
|
691
|
+
@Property({ name: 'organization_id', type: 'uuid' })
|
|
692
|
+
organizationId!: string
|
|
693
|
+
|
|
694
|
+
@Property({ name: 'tenant_id', type: 'uuid' })
|
|
695
|
+
tenantId!: string
|
|
696
|
+
|
|
697
|
+
@Property({ name: 'pipeline_id', type: 'uuid' })
|
|
698
|
+
pipelineId!: string
|
|
699
|
+
|
|
700
|
+
@Property({ name: 'name', type: 'text' })
|
|
701
|
+
label!: string
|
|
702
|
+
|
|
703
|
+
@Property({ name: 'position', type: 'int', default: 0 })
|
|
704
|
+
order: number = 0
|
|
705
|
+
|
|
706
|
+
@Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
|
|
707
|
+
createdAt: Date = new Date()
|
|
708
|
+
|
|
709
|
+
@Property({ name: 'updated_at', type: Date, onUpdate: () => new Date() })
|
|
710
|
+
updatedAt: Date = new Date()
|
|
711
|
+
}
|
|
712
|
+
|
|
649
713
|
@Entity({ tableName: 'customer_todo_links' })
|
|
650
714
|
@Index({ name: 'customer_todo_links_entity_idx', properties: ['entity'] })
|
|
651
715
|
@Index({ name: 'customer_todo_links_entity_created_idx', properties: ['entity', 'createdAt'] })
|
|
@@ -105,6 +105,8 @@ export const dealCreateSchema = scopedSchema.extend({
|
|
|
105
105
|
description: z.string().max(4000).optional(),
|
|
106
106
|
status: z.string().max(50).optional(),
|
|
107
107
|
pipelineStage: z.string().max(100).optional(),
|
|
108
|
+
pipelineId: uuid().optional(),
|
|
109
|
+
pipelineStageId: uuid().optional(),
|
|
108
110
|
valueAmount: z.coerce.number().min(0).optional(),
|
|
109
111
|
valueCurrency: z.string().min(3).max(3).optional(),
|
|
110
112
|
probability: z.number().min(0).max(100).optional(),
|
|
@@ -313,3 +315,58 @@ export type TodoLinkCreateInput = z.infer<typeof todoLinkCreateSchema>
|
|
|
313
315
|
export type TodoLinkWithTodoCreateInput = z.infer<typeof todoLinkWithTodoCreateSchema>
|
|
314
316
|
export type CustomerSettingsUpsertInput = z.infer<typeof customerSettingsUpsertSchema>
|
|
315
317
|
export type CustomerAddressFormatInput = z.infer<typeof customerAddressFormatSchema>
|
|
318
|
+
|
|
319
|
+
// --- Pipeline schemas ---
|
|
320
|
+
|
|
321
|
+
export const pipelineCreateSchema = scopedSchema.extend({
|
|
322
|
+
name: z.string().trim().min(1).max(200),
|
|
323
|
+
isDefault: z.boolean().optional(),
|
|
324
|
+
})
|
|
325
|
+
|
|
326
|
+
export const pipelineUpdateSchema = z.object({
|
|
327
|
+
id: uuid(),
|
|
328
|
+
name: z.string().trim().min(1).max(200).optional(),
|
|
329
|
+
isDefault: z.boolean().optional(),
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
export const pipelineDeleteSchema = z.object({
|
|
333
|
+
id: uuid(),
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
export type PipelineCreateInput = z.infer<typeof pipelineCreateSchema>
|
|
337
|
+
export type PipelineUpdateInput = z.infer<typeof pipelineUpdateSchema>
|
|
338
|
+
export type PipelineDeleteInput = z.infer<typeof pipelineDeleteSchema>
|
|
339
|
+
|
|
340
|
+
// --- Pipeline Stage schemas ---
|
|
341
|
+
|
|
342
|
+
export const pipelineStageCreateSchema = scopedSchema.extend({
|
|
343
|
+
pipelineId: uuid(),
|
|
344
|
+
label: z.string().trim().min(1).max(200),
|
|
345
|
+
order: z.number().int().min(0).optional(),
|
|
346
|
+
color: z.string().trim().max(20).optional(),
|
|
347
|
+
icon: z.string().trim().max(100).optional(),
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
export const pipelineStageUpdateSchema = z.object({
|
|
351
|
+
id: uuid(),
|
|
352
|
+
label: z.string().trim().min(1).max(200).optional(),
|
|
353
|
+
order: z.number().int().min(0).optional(),
|
|
354
|
+
color: z.string().trim().max(20).optional(),
|
|
355
|
+
icon: z.string().trim().max(100).optional(),
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
export const pipelineStageDeleteSchema = z.object({
|
|
359
|
+
id: uuid(),
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
export const pipelineStageReorderSchema = scopedSchema.extend({
|
|
363
|
+
stages: z.array(z.object({
|
|
364
|
+
id: uuid(),
|
|
365
|
+
order: z.number().int().min(0),
|
|
366
|
+
})).min(1),
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
export type PipelineStageCreateInput = z.infer<typeof pipelineStageCreateSchema>
|
|
370
|
+
export type PipelineStageUpdateInput = z.infer<typeof pipelineStageUpdateSchema>
|
|
371
|
+
export type PipelineStageDeleteInput = z.infer<typeof pipelineStageDeleteSchema>
|
|
372
|
+
export type PipelineStageReorderInput = z.infer<typeof pipelineStageReorderSchema>
|
|
@@ -393,6 +393,7 @@
|
|
|
393
393
|
"customers.deals.list.columns.companies": "Unternehmen",
|
|
394
394
|
"customers.deals.list.columns.expectedClose": "Voraussichtlicher Abschluss",
|
|
395
395
|
"customers.deals.list.columns.people": "Personen",
|
|
396
|
+
"customers.deals.list.columns.pipeline": "Pipeline",
|
|
396
397
|
"customers.deals.list.columns.pipelineStage": "Pipeline-Stufe",
|
|
397
398
|
"customers.deals.list.columns.probability": "Wahrscheinlichkeit",
|
|
398
399
|
"customers.deals.list.columns.status": "Status",
|
|
@@ -421,9 +422,11 @@
|
|
|
421
422
|
"customers.deals.pipeline.emptyLane": "Keine Deals in dieser Phase.",
|
|
422
423
|
"customers.deals.pipeline.limitNotice": "Es werden die ersten {count} Deals angezeigt. Verwende Filter, um mehr zu sehen.",
|
|
423
424
|
"customers.deals.pipeline.loadError": "Deals konnten nicht geladen werden.",
|
|
425
|
+
"customers.deals.pipeline.manageStages": "Etappen verwalten",
|
|
424
426
|
"customers.deals.pipeline.moveError": "Pipeline-Phase konnte nicht aktualisiert werden.",
|
|
425
427
|
"customers.deals.pipeline.moveSuccess": "Deal aktualisiert.",
|
|
426
428
|
"customers.deals.pipeline.noExpectedClose": "Kein Datum",
|
|
429
|
+
"customers.deals.pipeline.noPipeline": "Keine Pipeline ausgewählt. Erstelle eine Pipeline in den Einstellungen.",
|
|
427
430
|
"customers.deals.pipeline.noProbability": "k. A.",
|
|
428
431
|
"customers.deals.pipeline.noStages": "Definiere Pipeline-Phasen, um mit dem Tracking zu beginnen.",
|
|
429
432
|
"customers.deals.pipeline.sort.createdAt": "Erstellt (neueste zuerst)",
|
|
@@ -431,6 +434,7 @@
|
|
|
431
434
|
"customers.deals.pipeline.sort.label": "Sortieren nach",
|
|
432
435
|
"customers.deals.pipeline.sort.probability": "Wahrscheinlichkeit (absteigend)",
|
|
433
436
|
"customers.deals.pipeline.subtitle": "Verfolge Deals nach Pipeline-Phase und ziehe sie zwischen den Spalten, um den Fortschritt zu aktualisieren.",
|
|
437
|
+
"customers.deals.pipeline.switch.label": "Pipeline",
|
|
434
438
|
"customers.deals.pipeline.title": "Sales-Pipeline",
|
|
435
439
|
"customers.deals.pipeline.unassigned": "Keine Phase",
|
|
436
440
|
"customers.deals.pipeline.unassignedDisabled": "Verschieben nach \"Keine Phase\" wird nicht unterstützt.",
|
|
@@ -393,6 +393,7 @@
|
|
|
393
393
|
"customers.deals.list.columns.companies": "Companies",
|
|
394
394
|
"customers.deals.list.columns.expectedClose": "Expected close",
|
|
395
395
|
"customers.deals.list.columns.people": "People",
|
|
396
|
+
"customers.deals.list.columns.pipeline": "Pipeline",
|
|
396
397
|
"customers.deals.list.columns.pipelineStage": "Pipeline stage",
|
|
397
398
|
"customers.deals.list.columns.probability": "Probability",
|
|
398
399
|
"customers.deals.list.columns.status": "Status",
|
|
@@ -421,9 +422,11 @@
|
|
|
421
422
|
"customers.deals.pipeline.emptyLane": "No deals in this stage yet.",
|
|
422
423
|
"customers.deals.pipeline.limitNotice": "Showing the first {count} deals. Refine your filters to see more.",
|
|
423
424
|
"customers.deals.pipeline.loadError": "Failed to load deals.",
|
|
425
|
+
"customers.deals.pipeline.manageStages": "Manage stages",
|
|
424
426
|
"customers.deals.pipeline.moveError": "Failed to update deal stage.",
|
|
425
427
|
"customers.deals.pipeline.moveSuccess": "Deal updated.",
|
|
426
428
|
"customers.deals.pipeline.noExpectedClose": "No date",
|
|
429
|
+
"customers.deals.pipeline.noPipeline": "No pipeline selected. Create a pipeline in settings.",
|
|
427
430
|
"customers.deals.pipeline.noProbability": "N/A",
|
|
428
431
|
"customers.deals.pipeline.noStages": "Define pipeline stages to start tracking deals.",
|
|
429
432
|
"customers.deals.pipeline.sort.createdAt": "Created (newest first)",
|
|
@@ -431,6 +434,7 @@
|
|
|
431
434
|
"customers.deals.pipeline.sort.label": "Sort by",
|
|
432
435
|
"customers.deals.pipeline.sort.probability": "Probability (high to low)",
|
|
433
436
|
"customers.deals.pipeline.subtitle": "Track deals by pipeline stage and drag them between lanes to update progress.",
|
|
437
|
+
"customers.deals.pipeline.switch.label": "Pipeline",
|
|
434
438
|
"customers.deals.pipeline.title": "Sales Pipeline",
|
|
435
439
|
"customers.deals.pipeline.unassigned": "No stage",
|
|
436
440
|
"customers.deals.pipeline.unassignedDisabled": "Moving to \"No stage\" is not supported.",
|
|
@@ -393,6 +393,7 @@
|
|
|
393
393
|
"customers.deals.list.columns.companies": "Empresas",
|
|
394
394
|
"customers.deals.list.columns.expectedClose": "Cierre esperado",
|
|
395
395
|
"customers.deals.list.columns.people": "Personas",
|
|
396
|
+
"customers.deals.list.columns.pipeline": "Embudo de ventas",
|
|
396
397
|
"customers.deals.list.columns.pipelineStage": "Etapa del embudo",
|
|
397
398
|
"customers.deals.list.columns.probability": "Probabilidad",
|
|
398
399
|
"customers.deals.list.columns.status": "Estado",
|
|
@@ -421,9 +422,11 @@
|
|
|
421
422
|
"customers.deals.pipeline.emptyLane": "No hay oportunidades en esta etapa.",
|
|
422
423
|
"customers.deals.pipeline.limitNotice": "Se muestran las primeras {count} oportunidades. Ajusta los filtros para ver más.",
|
|
423
424
|
"customers.deals.pipeline.loadError": "No se pudieron cargar las oportunidades.",
|
|
425
|
+
"customers.deals.pipeline.manageStages": "Gestionar etapas",
|
|
424
426
|
"customers.deals.pipeline.moveError": "No se pudo actualizar la etapa del pipeline.",
|
|
425
427
|
"customers.deals.pipeline.moveSuccess": "Oportunidad actualizada.",
|
|
426
428
|
"customers.deals.pipeline.noExpectedClose": "Sin fecha",
|
|
429
|
+
"customers.deals.pipeline.noPipeline": "No se seleccionó un pipeline. Crea uno en la configuración.",
|
|
427
430
|
"customers.deals.pipeline.noProbability": "N/D",
|
|
428
431
|
"customers.deals.pipeline.noStages": "Define las etapas del pipeline para comenzar a seguir las oportunidades.",
|
|
429
432
|
"customers.deals.pipeline.sort.createdAt": "Creada (más recientes primero)",
|
|
@@ -431,6 +434,7 @@
|
|
|
431
434
|
"customers.deals.pipeline.sort.label": "Ordenar por",
|
|
432
435
|
"customers.deals.pipeline.sort.probability": "Probabilidad (de mayor a menor)",
|
|
433
436
|
"customers.deals.pipeline.subtitle": "Sigue las oportunidades por etapa del pipeline y arrástralas entre columnas para actualizar su progreso.",
|
|
437
|
+
"customers.deals.pipeline.switch.label": "Pipeline",
|
|
434
438
|
"customers.deals.pipeline.title": "Pipeline de ventas",
|
|
435
439
|
"customers.deals.pipeline.unassigned": "Sin etapa",
|
|
436
440
|
"customers.deals.pipeline.unassignedDisabled": "Mover a \"Sin etapa\" no está disponible.",
|
|
@@ -381,7 +381,7 @@
|
|
|
381
381
|
"customers.deals.form.people.loading": "Wyszukiwanie osób…",
|
|
382
382
|
"customers.deals.form.people.noResults": "Brak osób spełniających kryteria.",
|
|
383
383
|
"customers.deals.form.people.searchPlaceholder": "Szukaj osób…",
|
|
384
|
-
"customers.deals.form.pipeline.placeholder": "Wybierz
|
|
384
|
+
"customers.deals.form.pipeline.placeholder": "Wybierz lejek…",
|
|
385
385
|
"customers.deals.form.status.placeholder": "Wybierz status szansy",
|
|
386
386
|
"customers.deals.list.actions.delete": "Usuń",
|
|
387
387
|
"customers.deals.list.actions.deleting": "Usuwanie…",
|
|
@@ -393,6 +393,7 @@
|
|
|
393
393
|
"customers.deals.list.columns.companies": "Firmy",
|
|
394
394
|
"customers.deals.list.columns.expectedClose": "Planowana finalizacja",
|
|
395
395
|
"customers.deals.list.columns.people": "Osoby",
|
|
396
|
+
"customers.deals.list.columns.pipeline": "Lejek sprzedażowy",
|
|
396
397
|
"customers.deals.list.columns.pipelineStage": "Etap lejka",
|
|
397
398
|
"customers.deals.list.columns.probability": "Prawdopodobieństwo",
|
|
398
399
|
"customers.deals.list.columns.status": "Status",
|
|
@@ -421,9 +422,11 @@
|
|
|
421
422
|
"customers.deals.pipeline.emptyLane": "Brak szans na tym etapie.",
|
|
422
423
|
"customers.deals.pipeline.limitNotice": "Wyświetlamy pierwsze {count} szans. Zawęź filtr, aby zobaczyć więcej.",
|
|
423
424
|
"customers.deals.pipeline.loadError": "Nie udało się wczytać szans.",
|
|
425
|
+
"customers.deals.pipeline.manageStages": "Zarządzaj etapami",
|
|
424
426
|
"customers.deals.pipeline.moveError": "Nie udało się zaktualizować etapu szansy.",
|
|
425
427
|
"customers.deals.pipeline.moveSuccess": "Zapisano etap szansy.",
|
|
426
428
|
"customers.deals.pipeline.noExpectedClose": "Brak daty",
|
|
429
|
+
"customers.deals.pipeline.noPipeline": "Nie wybrano lejka. Utwórz lejek w ustawieniach.",
|
|
427
430
|
"customers.deals.pipeline.noProbability": "brak",
|
|
428
431
|
"customers.deals.pipeline.noStages": "Zdefiniuj etapy lejka sprzedażowego, aby zacząć śledzić szanse.",
|
|
429
432
|
"customers.deals.pipeline.sort.createdAt": "Utworzone (najnowsze na górze)",
|
|
@@ -431,6 +434,7 @@
|
|
|
431
434
|
"customers.deals.pipeline.sort.label": "Sortuj według",
|
|
432
435
|
"customers.deals.pipeline.sort.probability": "Prawdopodobieństwo (malejąco)",
|
|
433
436
|
"customers.deals.pipeline.subtitle": "Monitoruj szanse według etapu lejka i przeciągaj je między kolumnami, aby aktualizować postęp.",
|
|
437
|
+
"customers.deals.pipeline.switch.label": "Lejek",
|
|
434
438
|
"customers.deals.pipeline.title": "Lejek sprzedaży",
|
|
435
439
|
"customers.deals.pipeline.unassigned": "Brak etapu",
|
|
436
440
|
"customers.deals.pipeline.unassignedDisabled": "Przenoszenie do sekcji \"Brak etapu\" nie jest obsługiwane.",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
export class Migration20260218191730 extends Migration {
|
|
4
|
+
|
|
5
|
+
override async up(): Promise<void> {
|
|
6
|
+
this.addSql(`create table if not exists "customer_pipelines" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "name" text not null, "is_default" boolean not null default false, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "customer_pipelines_pkey" primary key ("id"));`);
|
|
7
|
+
this.addSql(`create index if not exists "customer_pipelines_org_tenant_idx" on "customer_pipelines" ("organization_id", "tenant_id");`);
|
|
8
|
+
|
|
9
|
+
this.addSql(`create table if not exists "customer_pipeline_stages" ("id" uuid not null default gen_random_uuid(), "organization_id" uuid not null, "tenant_id" uuid not null, "pipeline_id" uuid not null, "name" text not null, "position" int not null default 0, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "customer_pipeline_stages_pkey" primary key ("id"));`);
|
|
10
|
+
|
|
11
|
+
// Rename columns from duplicate migration if they exist (label→name, stage_order→position)
|
|
12
|
+
this.addSql(`DO $$ BEGIN ALTER TABLE "customer_pipeline_stages" RENAME COLUMN "label" TO "name"; EXCEPTION WHEN undefined_column THEN NULL; END $$;`);
|
|
13
|
+
this.addSql(`DO $$ BEGIN ALTER TABLE "customer_pipeline_stages" RENAME COLUMN "stage_order" TO "position"; EXCEPTION WHEN undefined_column THEN NULL; END $$;`);
|
|
14
|
+
|
|
15
|
+
this.addSql(`create index if not exists "customer_pipeline_stages_pipeline_position_idx" on "customer_pipeline_stages" ("pipeline_id", "position");`);
|
|
16
|
+
this.addSql(`create index if not exists "customer_pipeline_stages_org_tenant_idx" on "customer_pipeline_stages" ("organization_id", "tenant_id");`);
|
|
17
|
+
|
|
18
|
+
this.addSql(`DO $$ BEGIN ALTER TABLE "customer_deals" ADD COLUMN "pipeline_id" uuid null; EXCEPTION WHEN duplicate_column THEN NULL; END $$;`);
|
|
19
|
+
this.addSql(`DO $$ BEGIN ALTER TABLE "customer_deals" ADD COLUMN "pipeline_stage_id" uuid null; EXCEPTION WHEN duplicate_column THEN NULL; END $$;`);
|
|
20
|
+
|
|
21
|
+
// Data migration: backfill existing deals from legacy pipeline_stage (text) → pipeline_id + pipeline_stage_id
|
|
22
|
+
this.addSql(`
|
|
23
|
+
DO $$
|
|
24
|
+
DECLARE
|
|
25
|
+
r RECORD;
|
|
26
|
+
v_pipeline_id UUID;
|
|
27
|
+
v_stage_id UUID;
|
|
28
|
+
v_pos INT;
|
|
29
|
+
stage_values TEXT[] := ARRAY[
|
|
30
|
+
'opportunity','marketing_qualified_lead','sales_qualified_lead',
|
|
31
|
+
'offering','negotiations','win','loose','stalled'
|
|
32
|
+
];
|
|
33
|
+
stage_labels TEXT[] := ARRAY[
|
|
34
|
+
'Opportunity','Marketing Qualified Lead','Sales Qualified Lead',
|
|
35
|
+
'Offering','Negotiations','Win','Loose','Stalled'
|
|
36
|
+
];
|
|
37
|
+
BEGIN
|
|
38
|
+
FOR r IN (
|
|
39
|
+
SELECT DISTINCT organization_id, tenant_id FROM customer_deals
|
|
40
|
+
WHERE pipeline_stage IS NOT NULL AND pipeline_stage <> '' AND pipeline_id IS NULL
|
|
41
|
+
) LOOP
|
|
42
|
+
SELECT id INTO v_pipeline_id FROM customer_pipelines
|
|
43
|
+
WHERE organization_id = r.organization_id AND tenant_id = r.tenant_id AND is_default = true LIMIT 1;
|
|
44
|
+
|
|
45
|
+
IF v_pipeline_id IS NULL THEN
|
|
46
|
+
INSERT INTO customer_pipelines (id, organization_id, tenant_id, name, is_default, created_at, updated_at)
|
|
47
|
+
VALUES (gen_random_uuid(), r.organization_id, r.tenant_id, 'Default Pipeline', true, now(), now())
|
|
48
|
+
RETURNING id INTO v_pipeline_id;
|
|
49
|
+
END IF;
|
|
50
|
+
|
|
51
|
+
IF NOT EXISTS (SELECT 1 FROM customer_pipeline_stages WHERE pipeline_id = v_pipeline_id) THEN
|
|
52
|
+
FOR v_pos IN 1..array_length(stage_labels, 1) LOOP
|
|
53
|
+
INSERT INTO customer_pipeline_stages (id, organization_id, tenant_id, pipeline_id, name, position, created_at, updated_at)
|
|
54
|
+
VALUES (gen_random_uuid(), r.organization_id, r.tenant_id, v_pipeline_id, stage_labels[v_pos], v_pos - 1, now(), now());
|
|
55
|
+
END LOOP;
|
|
56
|
+
END IF;
|
|
57
|
+
|
|
58
|
+
FOR v_pos IN 1..array_length(stage_values, 1) LOOP
|
|
59
|
+
SELECT id INTO v_stage_id FROM customer_pipeline_stages
|
|
60
|
+
WHERE pipeline_id = v_pipeline_id AND name = stage_labels[v_pos] LIMIT 1;
|
|
61
|
+
IF v_stage_id IS NOT NULL THEN
|
|
62
|
+
UPDATE customer_deals
|
|
63
|
+
SET pipeline_id = v_pipeline_id, pipeline_stage_id = v_stage_id
|
|
64
|
+
WHERE organization_id = r.organization_id AND tenant_id = r.tenant_id
|
|
65
|
+
AND pipeline_id IS NULL
|
|
66
|
+
AND pipeline_stage IN (stage_values[v_pos], stage_labels[v_pos]);
|
|
67
|
+
END IF;
|
|
68
|
+
END LOOP;
|
|
69
|
+
|
|
70
|
+
-- deals with unknown stage value: assign to pipeline, stage stays NULL
|
|
71
|
+
UPDATE customer_deals SET pipeline_id = v_pipeline_id
|
|
72
|
+
WHERE organization_id = r.organization_id AND tenant_id = r.tenant_id AND pipeline_id IS NULL;
|
|
73
|
+
END LOOP;
|
|
74
|
+
END $$;
|
|
75
|
+
`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
override async down(): Promise<void> {
|
|
79
|
+
this.addSql(`alter table "customer_deals" drop column "pipeline_id", drop column "pipeline_stage_id";`);
|
|
80
|
+
this.addSql(`drop table if exists "customer_pipeline_stages";`);
|
|
81
|
+
this.addSql(`drop table if exists "customer_pipelines";`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { ModuleSetupConfig } from '@open-mercato/shared/modules/setup'
|
|
2
|
-
import { seedCustomerDictionaries, seedCurrencyDictionary, seedCustomerExamples } from './cli'
|
|
2
|
+
import { seedCustomerDictionaries, seedCurrencyDictionary, seedCustomerExamples, seedDefaultPipeline } from './cli'
|
|
3
3
|
|
|
4
4
|
export const setup: ModuleSetupConfig = {
|
|
5
5
|
seedDefaults: async (ctx) => {
|
|
6
6
|
const scope = { tenantId: ctx.tenantId, organizationId: ctx.organizationId }
|
|
7
7
|
await seedCustomerDictionaries(ctx.em, scope)
|
|
8
8
|
await seedCurrencyDictionary(ctx.em, scope)
|
|
9
|
+
await seedDefaultPipeline(ctx.em, scope)
|
|
9
10
|
},
|
|
10
11
|
|
|
11
12
|
seedExamples: async (ctx) => {
|
|
@@ -22,6 +23,8 @@ export const setup: ModuleSetupConfig = {
|
|
|
22
23
|
'customers.companies.manage',
|
|
23
24
|
'customers.deals.view',
|
|
24
25
|
'customers.deals.manage',
|
|
26
|
+
'customers.pipelines.view',
|
|
27
|
+
'customers.pipelines.manage',
|
|
25
28
|
],
|
|
26
29
|
employee: [
|
|
27
30
|
'customers.*',
|
|
@@ -29,6 +32,7 @@ export const setup: ModuleSetupConfig = {
|
|
|
29
32
|
'customers.people.manage',
|
|
30
33
|
'customers.companies.view',
|
|
31
34
|
'customers.companies.manage',
|
|
35
|
+
'customers.pipelines.view',
|
|
32
36
|
],
|
|
33
37
|
},
|
|
34
38
|
}
|