@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.
Files changed (113) hide show
  1. package/dist/generated/entities/customer_deal/index.js +4 -0
  2. package/dist/generated/entities/customer_deal/index.js.map +2 -2
  3. package/dist/generated/entities/customer_pipeline/index.js +17 -0
  4. package/dist/generated/entities/customer_pipeline/index.js.map +7 -0
  5. package/dist/generated/entities/customer_pipeline_stage/index.js +19 -0
  6. package/dist/generated/entities/customer_pipeline_stage/index.js.map +7 -0
  7. package/dist/generated/entities.ids.generated.js +2 -0
  8. package/dist/generated/entities.ids.generated.js.map +2 -2
  9. package/dist/generated/entity-fields-registry.js +4 -0
  10. package/dist/generated/entity-fields-registry.js.map +2 -2
  11. package/dist/modules/customers/acl.js +2 -0
  12. package/dist/modules/customers/acl.js.map +2 -2
  13. package/dist/modules/customers/api/deals/[id]/route.js +4 -0
  14. package/dist/modules/customers/api/deals/[id]/route.js.map +2 -2
  15. package/dist/modules/customers/api/deals/route.js +12 -0
  16. package/dist/modules/customers/api/deals/route.js.map +2 -2
  17. package/dist/modules/customers/api/dictionaries/[kind]/route.js +20 -1
  18. package/dist/modules/customers/api/dictionaries/[kind]/route.js.map +2 -2
  19. package/dist/modules/customers/api/pipeline-stages/reorder/route.js +69 -0
  20. package/dist/modules/customers/api/pipeline-stages/reorder/route.js.map +7 -0
  21. package/dist/modules/customers/api/pipeline-stages/route.js +275 -0
  22. package/dist/modules/customers/api/pipeline-stages/route.js.map +7 -0
  23. package/dist/modules/customers/api/pipelines/route.js +245 -0
  24. package/dist/modules/customers/api/pipelines/route.js.map +7 -0
  25. package/dist/modules/customers/backend/config/customers/page.js +2 -0
  26. package/dist/modules/customers/backend/config/customers/page.js.map +2 -2
  27. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js +439 -0
  28. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.js.map +7 -0
  29. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js +17 -0
  30. package/dist/modules/customers/backend/config/customers/pipeline-stages/page.meta.js.map +7 -0
  31. package/dist/modules/customers/backend/customers/deals/[id]/page.js +19 -1
  32. package/dist/modules/customers/backend/customers/deals/[id]/page.js.map +2 -2
  33. package/dist/modules/customers/backend/customers/deals/page.js +35 -1
  34. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  35. package/dist/modules/customers/backend/customers/deals/pipeline/page.js +102 -74
  36. package/dist/modules/customers/backend/customers/deals/pipeline/page.js.map +2 -2
  37. package/dist/modules/customers/cli.js +28 -2
  38. package/dist/modules/customers/cli.js.map +2 -2
  39. package/dist/modules/customers/commands/deals.js +34 -2
  40. package/dist/modules/customers/commands/deals.js.map +2 -2
  41. package/dist/modules/customers/commands/index.js +2 -0
  42. package/dist/modules/customers/commands/index.js.map +2 -2
  43. package/dist/modules/customers/commands/pipeline-stages.js +126 -0
  44. package/dist/modules/customers/commands/pipeline-stages.js.map +7 -0
  45. package/dist/modules/customers/commands/pipelines.js +87 -0
  46. package/dist/modules/customers/commands/pipelines.js.map +7 -0
  47. package/dist/modules/customers/components/DictionarySettings.js +0 -5
  48. package/dist/modules/customers/components/DictionarySettings.js.map +2 -2
  49. package/dist/modules/customers/components/PipelineSettings.js +474 -0
  50. package/dist/modules/customers/components/PipelineSettings.js.map +7 -0
  51. package/dist/modules/customers/components/detail/DealForm.js +84 -12
  52. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  53. package/dist/modules/customers/data/entities.js +78 -0
  54. package/dist/modules/customers/data/entities.js.map +2 -2
  55. package/dist/modules/customers/data/validators.js +44 -0
  56. package/dist/modules/customers/data/validators.js.map +2 -2
  57. package/dist/modules/customers/migrations/Migration20260218191730.js +77 -0
  58. package/dist/modules/customers/migrations/Migration20260218191730.js.map +7 -0
  59. package/dist/modules/customers/setup.js +7 -3
  60. package/dist/modules/customers/setup.js.map +2 -2
  61. package/dist/modules/translations/api/[entityType]/[entityId]/route.js +46 -44
  62. package/dist/modules/translations/api/[entityType]/[entityId]/route.js.map +2 -2
  63. package/dist/modules/translations/api/context.js +10 -1
  64. package/dist/modules/translations/api/context.js.map +2 -2
  65. package/dist/modules/translations/commands/index.js +2 -0
  66. package/dist/modules/translations/commands/index.js.map +7 -0
  67. package/dist/modules/translations/commands/translations.js +160 -0
  68. package/dist/modules/translations/commands/translations.js.map +7 -0
  69. package/dist/modules/translations/index.js +1 -0
  70. package/dist/modules/translations/index.js.map +2 -2
  71. package/dist/modules/workflows/migrations/Migration20260222205305.js +14 -0
  72. package/dist/modules/workflows/migrations/Migration20260222205305.js.map +7 -0
  73. package/generated/entities/customer_deal/index.ts +2 -0
  74. package/generated/entities/customer_pipeline/index.ts +7 -0
  75. package/generated/entities/customer_pipeline_stage/index.ts +8 -0
  76. package/generated/entities.ids.generated.ts +2 -0
  77. package/generated/entity-fields-registry.ts +4 -0
  78. package/package.json +2 -2
  79. package/src/modules/customers/acl.ts +2 -0
  80. package/src/modules/customers/api/deals/[id]/route.ts +4 -0
  81. package/src/modules/customers/api/deals/route.ts +12 -0
  82. package/src/modules/customers/api/dictionaries/[kind]/route.ts +21 -1
  83. package/src/modules/customers/api/pipeline-stages/reorder/route.ts +71 -0
  84. package/src/modules/customers/api/pipeline-stages/route.ts +296 -0
  85. package/src/modules/customers/api/pipelines/route.ts +261 -0
  86. package/src/modules/customers/backend/config/customers/page.tsx +2 -0
  87. package/src/modules/customers/backend/config/customers/pipeline-stages/page.meta.ts +13 -0
  88. package/src/modules/customers/backend/config/customers/pipeline-stages/page.tsx +512 -0
  89. package/src/modules/customers/backend/customers/deals/[id]/page.tsx +21 -1
  90. package/src/modules/customers/backend/customers/deals/page.tsx +33 -1
  91. package/src/modules/customers/backend/customers/deals/pipeline/page.tsx +119 -79
  92. package/src/modules/customers/cli.ts +29 -1
  93. package/src/modules/customers/commands/deals.ts +44 -1
  94. package/src/modules/customers/commands/index.ts +2 -0
  95. package/src/modules/customers/commands/pipeline-stages.ts +156 -0
  96. package/src/modules/customers/commands/pipelines.ts +105 -0
  97. package/src/modules/customers/components/DictionarySettings.tsx +0 -5
  98. package/src/modules/customers/components/PipelineSettings.tsx +570 -0
  99. package/src/modules/customers/components/detail/DealForm.tsx +89 -11
  100. package/src/modules/customers/data/entities.ts +64 -0
  101. package/src/modules/customers/data/validators.ts +57 -0
  102. package/src/modules/customers/i18n/de.json +4 -0
  103. package/src/modules/customers/i18n/en.json +4 -0
  104. package/src/modules/customers/i18n/es.json +4 -0
  105. package/src/modules/customers/i18n/pl.json +5 -1
  106. package/src/modules/customers/migrations/Migration20260218191730.ts +84 -0
  107. package/src/modules/customers/setup.ts +5 -1
  108. package/src/modules/translations/api/[entityType]/[entityId]/route.ts +65 -60
  109. package/src/modules/translations/api/context.ts +12 -0
  110. package/src/modules/translations/commands/index.ts +1 -0
  111. package/src/modules/translations/commands/translations.ts +253 -0
  112. package/src/modules/translations/index.ts +1 -0
  113. 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: 'pipelineStage',
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
- <DictionarySelectField
609
- kind="pipeline-stages"
610
- value={typeof value === 'string' ? value : undefined}
611
- onChange={(next) => setValue(next ?? '')}
612
- labels={dictionaryLabels.pipeline}
613
- selectClassName="w-full"
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, dictionaryLabels.pipeline, dictionaryLabels.status, disabled, fetchCompaniesByIds, fetchPeopleByIds, searchCompanies, searchPeople, t])
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', 'pipelineStage', 'valueAmount', 'valueCurrency', 'probability', 'expectedCloseAt', 'description'],
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 etap lejka",
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
  }