@open-mercato/core 0.6.7-develop.6726.1.983ae8a07e → 0.6.7-develop.6749.1.6b54c56dfe

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 (191) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/generated/entities/resources_resource/index.js +2 -0
  3. package/dist/generated/entities/resources_resource/index.js.map +2 -2
  4. package/dist/generated/entity-fields-registry.js +1 -0
  5. package/dist/generated/entity-fields-registry.js.map +2 -2
  6. package/dist/helpers/integration/salesFixtures.js +31 -2
  7. package/dist/helpers/integration/salesFixtures.js.map +2 -2
  8. package/dist/helpers/integration/salesUi.js +5 -0
  9. package/dist/helpers/integration/salesUi.js.map +2 -2
  10. package/dist/modules/auth/api/profile/route.js +8 -2
  11. package/dist/modules/auth/api/profile/route.js.map +2 -2
  12. package/dist/modules/auth/backend/auth/profile/page.js.map +2 -2
  13. package/dist/modules/auth/backend/profile/change-password/page.js.map +2 -2
  14. package/dist/modules/communication_channels/data/enrichers.js +4 -4
  15. package/dist/modules/communication_channels/data/enrichers.js.map +2 -2
  16. package/dist/modules/customers/analytics.js +1 -0
  17. package/dist/modules/customers/analytics.js.map +2 -2
  18. package/dist/modules/customers/components/detail/DealForm.js +2 -1
  19. package/dist/modules/customers/components/detail/DealForm.js.map +2 -2
  20. package/dist/modules/customers/components/detail/DealsSection.js +1 -1
  21. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  22. package/dist/modules/customers/data/validators.js +3 -1
  23. package/dist/modules/customers/data/validators.js.map +2 -2
  24. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js +52 -2
  25. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/config.js.map +2 -2
  26. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js +42 -26
  27. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.js.map +2 -2
  28. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js +2 -2
  29. package/dist/modules/dashboards/widgets/dashboard/pipeline-summary/widget.js.map +2 -2
  30. package/dist/modules/data_sync/lib/queue-policy.js +15 -0
  31. package/dist/modules/data_sync/lib/queue-policy.js.map +7 -0
  32. package/dist/modules/data_sync/lib/queue.js +16 -1
  33. package/dist/modules/data_sync/lib/queue.js.map +2 -2
  34. package/dist/modules/data_sync/lib/start-run.js +2 -1
  35. package/dist/modules/data_sync/lib/start-run.js.map +2 -2
  36. package/dist/modules/data_sync/lib/sync-engine.js +35 -2
  37. package/dist/modules/data_sync/lib/sync-engine.js.map +2 -2
  38. package/dist/modules/data_sync/lib/sync-run-service.js +70 -2
  39. package/dist/modules/data_sync/lib/sync-run-service.js.map +2 -2
  40. package/dist/modules/data_sync/workers/sync-export.js +9 -2
  41. package/dist/modules/data_sync/workers/sync-export.js.map +2 -2
  42. package/dist/modules/data_sync/workers/sync-import.js +9 -2
  43. package/dist/modules/data_sync/workers/sync-import.js.map +2 -2
  44. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js +34 -12
  45. package/dist/modules/dictionaries/api/[dictionaryId]/entries/route.js.map +2 -2
  46. package/dist/modules/dictionaries/api/openapi.js +15 -2
  47. package/dist/modules/dictionaries/api/openapi.js.map +2 -2
  48. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js +30 -29
  49. package/dist/modules/dictionaries/components/hooks/useDictionaryEntries.js.map +2 -2
  50. package/dist/modules/dictionaries/data/validators.js +7 -0
  51. package/dist/modules/dictionaries/data/validators.js.map +2 -2
  52. package/dist/modules/dictionaries/lib/clientEntries.js +4 -3
  53. package/dist/modules/dictionaries/lib/clientEntries.js.map +2 -2
  54. package/dist/modules/dictionaries/lib/fetchAllEntries.js +43 -0
  55. package/dist/modules/dictionaries/lib/fetchAllEntries.js.map +7 -0
  56. package/dist/modules/entities/api/definitions.js +51 -3
  57. package/dist/modules/entities/api/definitions.js.map +2 -2
  58. package/dist/modules/entities/api/entities.js +21 -2
  59. package/dist/modules/entities/api/entities.js.map +2 -2
  60. package/dist/modules/entities/api/records.js +3 -24
  61. package/dist/modules/entities/api/records.js.map +2 -2
  62. package/dist/modules/entities/api/sidebar-entities.js +34 -12
  63. package/dist/modules/entities/api/sidebar-entities.js.map +2 -2
  64. package/dist/modules/entities/lib/entityAcl.js +46 -0
  65. package/dist/modules/entities/lib/entityAcl.js.map +2 -2
  66. package/dist/modules/messages/api/route.js +2 -5
  67. package/dist/modules/messages/api/route.js.map +2 -2
  68. package/dist/modules/messages/lib/participantScope.js +15 -0
  69. package/dist/modules/messages/lib/participantScope.js.map +7 -0
  70. package/dist/modules/notifications/api/route.js +3 -1
  71. package/dist/modules/notifications/api/route.js.map +2 -2
  72. package/dist/modules/notifications/api/unread-count/route.js +25 -6
  73. package/dist/modules/notifications/api/unread-count/route.js.map +2 -2
  74. package/dist/modules/notifications/lib/notificationScope.js +26 -0
  75. package/dist/modules/notifications/lib/notificationScope.js.map +7 -0
  76. package/dist/modules/notifications/lib/notificationService.js +7 -3
  77. package/dist/modules/notifications/lib/notificationService.js.map +2 -2
  78. package/dist/modules/notifications/lib/routeHelpers.js +16 -2
  79. package/dist/modules/notifications/lib/routeHelpers.js.map +2 -2
  80. package/dist/modules/planner/components/unavailabilityReasons.js +3 -3
  81. package/dist/modules/planner/components/unavailabilityReasons.js.map +2 -2
  82. package/dist/modules/progress/lib/progressService.js +2 -0
  83. package/dist/modules/progress/lib/progressService.js.map +2 -2
  84. package/dist/modules/progress/lib/progressServiceImpl.js +322 -104
  85. package/dist/modules/progress/lib/progressServiceImpl.js.map +2 -2
  86. package/dist/modules/resources/api/resources.js +2 -0
  87. package/dist/modules/resources/api/resources.js.map +2 -2
  88. package/dist/modules/resources/backend/resources/resources/[id]/page.js +3 -2
  89. package/dist/modules/resources/backend/resources/resources/[id]/page.js.map +2 -2
  90. package/dist/modules/resources/commands/resources.js +9 -0
  91. package/dist/modules/resources/commands/resources.js.map +2 -2
  92. package/dist/modules/resources/components/detail/dictionaries.js +3 -3
  93. package/dist/modules/resources/components/detail/dictionaries.js.map +2 -2
  94. package/dist/modules/resources/data/entities.js +3 -0
  95. package/dist/modules/resources/data/entities.js.map +2 -2
  96. package/dist/modules/resources/data/validators.js +6 -2
  97. package/dist/modules/resources/data/validators.js.map +2 -2
  98. package/dist/modules/resources/lib/seeds.js +16 -9
  99. package/dist/modules/resources/lib/seeds.js.map +2 -2
  100. package/dist/modules/resources/migrations/Migration20260608231000.js +13 -0
  101. package/dist/modules/resources/migrations/Migration20260608231000.js.map +7 -0
  102. package/dist/modules/sales/backend/sales/documents/create/page.js +11 -14
  103. package/dist/modules/sales/backend/sales/documents/create/page.js.map +2 -2
  104. package/dist/modules/sales/commands/documents.js +8 -0
  105. package/dist/modules/sales/commands/documents.js.map +2 -2
  106. package/dist/modules/sales/components/documents/ItemsSection.js +18 -9
  107. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  108. package/dist/modules/sales/components/documents/LineItemDialog.js +12 -5
  109. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  110. package/dist/modules/sales/components/documents/SalesDocumentForm.js +48 -2
  111. package/dist/modules/sales/components/documents/SalesDocumentForm.js.map +2 -2
  112. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +167 -0
  113. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +7 -0
  114. package/dist/modules/sales/data/validators.js +5 -1
  115. package/dist/modules/sales/data/validators.js.map +2 -2
  116. package/dist/modules/staff/components/detail/dictionaries.js +3 -3
  117. package/dist/modules/staff/components/detail/dictionaries.js.map +2 -2
  118. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js +11 -4
  119. package/dist/modules/workflows/widgets/injection/order-approval/widget.client.js.map +2 -2
  120. package/generated/entities/resources_resource/index.ts +1 -0
  121. package/generated/entity-fields-registry.ts +1 -0
  122. package/package.json +7 -7
  123. package/src/helpers/integration/salesFixtures.ts +34 -2
  124. package/src/helpers/integration/salesUi.ts +8 -0
  125. package/src/modules/auth/api/profile/route.ts +8 -2
  126. package/src/modules/auth/backend/auth/profile/page.tsx +1 -0
  127. package/src/modules/auth/backend/profile/change-password/page.tsx +1 -0
  128. package/src/modules/communication_channels/data/enrichers.ts +6 -26
  129. package/src/modules/customers/analytics.ts +1 -0
  130. package/src/modules/customers/components/detail/DealForm.tsx +5 -1
  131. package/src/modules/customers/components/detail/DealsSection.tsx +1 -1
  132. package/src/modules/customers/data/validators.ts +5 -1
  133. package/src/modules/dashboards/i18n/de.json +3 -0
  134. package/src/modules/dashboards/i18n/en.json +3 -0
  135. package/src/modules/dashboards/i18n/es.json +3 -0
  136. package/src/modules/dashboards/i18n/pl.json +3 -0
  137. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/config.ts +86 -0
  138. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.client.tsx +33 -18
  139. package/src/modules/dashboards/widgets/dashboard/pipeline-summary/widget.ts +2 -2
  140. package/src/modules/data_sync/lib/adapter.ts +18 -0
  141. package/src/modules/data_sync/lib/queue-policy.ts +30 -0
  142. package/src/modules/data_sync/lib/queue.ts +19 -1
  143. package/src/modules/data_sync/lib/start-run.ts +2 -1
  144. package/src/modules/data_sync/lib/sync-engine.ts +46 -0
  145. package/src/modules/data_sync/lib/sync-run-service.ts +82 -1
  146. package/src/modules/data_sync/workers/sync-export.ts +8 -1
  147. package/src/modules/data_sync/workers/sync-import.ts +8 -1
  148. package/src/modules/dictionaries/api/[dictionaryId]/entries/route.ts +39 -11
  149. package/src/modules/dictionaries/api/openapi.ts +19 -0
  150. package/src/modules/dictionaries/components/hooks/useDictionaryEntries.ts +35 -36
  151. package/src/modules/dictionaries/data/validators.ts +14 -0
  152. package/src/modules/dictionaries/lib/clientEntries.ts +4 -3
  153. package/src/modules/dictionaries/lib/fetchAllEntries.ts +72 -0
  154. package/src/modules/entities/api/definitions.ts +51 -3
  155. package/src/modules/entities/api/entities.ts +30 -2
  156. package/src/modules/entities/api/records.ts +3 -27
  157. package/src/modules/entities/api/sidebar-entities.ts +43 -14
  158. package/src/modules/entities/lib/entityAcl.ts +58 -0
  159. package/src/modules/messages/api/route.ts +4 -5
  160. package/src/modules/messages/lib/participantScope.ts +64 -0
  161. package/src/modules/notifications/api/route.ts +2 -0
  162. package/src/modules/notifications/api/unread-count/route.ts +42 -5
  163. package/src/modules/notifications/lib/notificationScope.ts +40 -0
  164. package/src/modules/notifications/lib/notificationService.ts +5 -0
  165. package/src/modules/notifications/lib/routeHelpers.ts +19 -2
  166. package/src/modules/planner/components/unavailabilityReasons.ts +3 -3
  167. package/src/modules/progress/AGENTS.md +11 -1
  168. package/src/modules/progress/lib/progressService.ts +1 -0
  169. package/src/modules/progress/lib/progressServiceImpl.ts +385 -117
  170. package/src/modules/resources/api/resources.ts +2 -0
  171. package/src/modules/resources/backend/resources/resources/[id]/page.tsx +9 -3
  172. package/src/modules/resources/commands/resources.ts +10 -0
  173. package/src/modules/resources/components/detail/dictionaries.ts +3 -3
  174. package/src/modules/resources/data/entities.ts +3 -0
  175. package/src/modules/resources/data/validators.ts +5 -0
  176. package/src/modules/resources/lib/seeds.ts +16 -9
  177. package/src/modules/resources/migrations/.snapshot-open-mercato.json +17 -1
  178. package/src/modules/resources/migrations/Migration20260608231000.ts +13 -0
  179. package/src/modules/sales/backend/sales/documents/create/page.tsx +2 -4
  180. package/src/modules/sales/commands/documents.ts +8 -0
  181. package/src/modules/sales/components/documents/ItemsSection.tsx +19 -10
  182. package/src/modules/sales/components/documents/LineItemDialog.tsx +13 -5
  183. package/src/modules/sales/components/documents/SalesDocumentForm.tsx +48 -6
  184. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +198 -0
  185. package/src/modules/sales/data/validators.ts +7 -1
  186. package/src/modules/sales/i18n/de.json +2 -0
  187. package/src/modules/sales/i18n/en.json +2 -0
  188. package/src/modules/sales/i18n/es.json +2 -0
  189. package/src/modules/sales/i18n/pl.json +2 -0
  190. package/src/modules/staff/components/detail/dictionaries.ts +3 -3
  191. package/src/modules/workflows/widgets/injection/order-approval/widget.client.tsx +12 -5
@@ -86,6 +86,7 @@ const crud = makeCrudRoute({
86
86
  'appearance_color',
87
87
  F.is_active,
88
88
  'availability_rule_set_id',
89
+ 'custom_fieldset_code',
89
90
  F.created_at,
90
91
  F.updated_at,
91
92
  ],
@@ -242,6 +243,7 @@ const resourceListItemSchema = z.object({
242
243
  appearance_color: z.string().nullable().optional(),
243
244
  is_active: z.boolean().nullable().optional(),
244
245
  availability_rule_set_id: z.string().uuid().nullable().optional(),
246
+ custom_fieldset_code: z.string().nullable().optional(),
245
247
  created_at: z.string().nullable().optional(),
246
248
  updated_at: z.string().nullable().optional(),
247
249
  tags: z.array(resourceTagListItemSchema).optional(),
@@ -59,6 +59,8 @@ type ResourceRecord = {
59
59
  is_active?: boolean
60
60
  availabilityRuleSetId?: string | null
61
61
  availability_rule_set_id?: string | null
62
+ customFieldsetCode?: string | null
63
+ custom_fieldset_code?: string | null
62
64
  } & Record<string, unknown>
63
65
 
64
66
  type ResourceResponse = {
@@ -85,6 +87,7 @@ function normalizeResourceRecord(record: ResourceRecord): ResourceRecord {
85
87
  appearanceIcon: record.appearanceIcon ?? record.appearance_icon ?? null,
86
88
  appearanceColor: record.appearanceColor ?? record.appearance_color ?? null,
87
89
  isActive: record.isActive ?? record.is_active ?? true,
90
+ customFieldsetCode: record.customFieldsetCode ?? record.custom_fieldset_code ?? null,
88
91
  }
89
92
  }
90
93
 
@@ -547,9 +550,12 @@ export default function ResourcesResourceDetailPage({ params }: { params?: { id?
547
550
  : typeof resource.updated_at === 'string'
548
551
  ? resource.updated_at
549
552
  : null,
550
- customFieldsetCode: resource.resourceTypeId
551
- ? resolveFieldsetCode(resource.resourceTypeId)
552
- : RESOURCES_RESOURCE_FIELDSET_DEFAULT,
553
+ customFieldsetCode:
554
+ typeof resource.customFieldsetCode === 'string' && resource.customFieldsetCode.trim().length
555
+ ? resource.customFieldsetCode.trim()
556
+ : resource.resourceTypeId
557
+ ? resolveFieldsetCode(resource.resourceTypeId)
558
+ : RESOURCES_RESOURCE_FIELDSET_DEFAULT,
553
559
  ...customValues,
554
560
  })
555
561
  }
@@ -50,6 +50,7 @@ type ResourceSnapshot = {
50
50
  appearanceColor: string | null
51
51
  isActive: boolean
52
52
  availabilityRuleSetId: string | null
53
+ customFieldsetCode: string | null
53
54
  tags: string[]
54
55
  deletedAt: string | null
55
56
  customFields?: CustomFieldSnapshot | null
@@ -154,6 +155,7 @@ async function loadResourceSnapshot(em: EntityManager, id: string): Promise<Reso
154
155
  appearanceColor: resource.appearanceColor ?? null,
155
156
  isActive: resource.isActive,
156
157
  availabilityRuleSetId: resource.availabilityRuleSetId ?? null,
158
+ customFieldsetCode: resource.customFieldsetCode ?? null,
157
159
  tags,
158
160
  deletedAt: resource.deletedAt ? resource.deletedAt.toISOString() : null,
159
161
  }
@@ -235,6 +237,7 @@ const createResourceCommand: CommandHandler<ResourcesResourceCreateInput, { reso
235
237
  appearanceColor: parsed.appearanceColor ?? null,
236
238
  isActive: parsed.isActive ?? true,
237
239
  availabilityRuleSetId: parsed.availabilityRuleSetId ?? null,
240
+ customFieldsetCode: parsed.customFieldsetCode ?? null,
238
241
  createdAt: now,
239
242
  updatedAt: now,
240
243
  })
@@ -358,6 +361,7 @@ const createResourceCommand: CommandHandler<ResourcesResourceCreateInput, { reso
358
361
  appearanceColor: after.appearanceColor ?? null,
359
362
  isActive: after.isActive,
360
363
  availabilityRuleSetId: after.availabilityRuleSetId ?? null,
364
+ customFieldsetCode: after.customFieldsetCode ?? null,
361
365
  deletedAt: null,
362
366
  createdAt: new Date(),
363
367
  updatedAt: new Date(),
@@ -376,6 +380,7 @@ const createResourceCommand: CommandHandler<ResourcesResourceCreateInput, { reso
376
380
  record.appearanceColor = after.appearanceColor ?? null
377
381
  record.isActive = after.isActive
378
382
  record.availabilityRuleSetId = after.availabilityRuleSetId ?? null
383
+ record.customFieldsetCode = after.customFieldsetCode ?? null
379
384
  record.deletedAt = null
380
385
  record.updatedAt = new Date()
381
386
  }
@@ -471,6 +476,7 @@ const updateResourceCommand: CommandHandler<ResourcesResourceUpdateInput, { reso
471
476
  if (parsed.appearanceIcon !== undefined) record.appearanceIcon = parsed.appearanceIcon ?? null
472
477
  if (parsed.appearanceColor !== undefined) record.appearanceColor = parsed.appearanceColor ?? null
473
478
  if (parsed.availabilityRuleSetId !== undefined) record.availabilityRuleSetId = parsed.availabilityRuleSetId ?? null
479
+ if (parsed.customFieldsetCode !== undefined) record.customFieldsetCode = parsed.customFieldsetCode ?? null
474
480
  record.updatedAt = new Date()
475
481
  if (parsed.isActive !== undefined) record.isActive = parsed.isActive
476
482
  },
@@ -531,6 +537,7 @@ const updateResourceCommand: CommandHandler<ResourcesResourceUpdateInput, { reso
531
537
  'appearanceColor',
532
538
  'isActive',
533
539
  'availabilityRuleSetId',
540
+ 'customFieldsetCode',
534
541
  'deletedAt',
535
542
  ])
536
543
  if (before.tags.join(',') !== after.tags.join(',')) {
@@ -583,6 +590,7 @@ const updateResourceCommand: CommandHandler<ResourcesResourceUpdateInput, { reso
583
590
  record.appearanceColor = before.appearanceColor ?? null
584
591
  record.isActive = before.isActive
585
592
  record.availabilityRuleSetId = before.availabilityRuleSetId ?? null
593
+ record.customFieldsetCode = before.customFieldsetCode ?? null
586
594
  record.deletedAt = before.deletedAt ? new Date(before.deletedAt) : null
587
595
  record.updatedAt = new Date()
588
596
  },
@@ -716,6 +724,7 @@ const deleteResourceCommand: CommandHandler<{ id?: string }, { resourceId: strin
716
724
  appearanceColor: before.appearanceColor ?? null,
717
725
  isActive: before.isActive,
718
726
  availabilityRuleSetId: before.availabilityRuleSetId ?? null,
727
+ customFieldsetCode: before.customFieldsetCode ?? null,
719
728
  deletedAt: null,
720
729
  createdAt: new Date(),
721
730
  updatedAt: new Date(),
@@ -734,6 +743,7 @@ const deleteResourceCommand: CommandHandler<{ id?: string }, { resourceId: strin
734
743
  record.appearanceColor = before.appearanceColor ?? null
735
744
  record.isActive = before.isActive
736
745
  record.availabilityRuleSetId = before.availabilityRuleSetId ?? null
746
+ record.customFieldsetCode = before.customFieldsetCode ?? null
737
747
  record.deletedAt = null
738
748
  record.updatedAt = new Date()
739
749
  }
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { apiCall, apiCallOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
4
+ import { fetchAllDictionaryEntries } from '@open-mercato/core/modules/dictionaries/lib/fetchAllEntries'
4
5
 
5
6
  export type DictionaryEntryOption = {
6
7
  value: string
@@ -52,10 +53,9 @@ export async function loadResourceDictionary(
52
53
  const name = 'Resource activity types'
53
54
  const dictionary = await ensureDictionary(key, name)
54
55
  if (!dictionary) return { dictionary: null, entries: [] }
55
- const entriesCall = await apiCall<{ items?: Record<string, unknown>[] }>(`/api/dictionaries/${dictionary.id}/entries`)
56
+ const entriesCall = await fetchAllDictionaryEntries(dictionary.id)
56
57
  if (!entriesCall.ok) return { dictionary, entries: [] }
57
- const items = Array.isArray(entriesCall.result?.items) ? entriesCall.result?.items ?? [] : []
58
- const entries = items
58
+ const entries = entriesCall.items
59
59
  .map((entry) => {
60
60
  if (!entry || typeof entry !== 'object') return null
61
61
  const record = entry as Record<string, unknown>
@@ -83,6 +83,9 @@ export class ResourcesResource {
83
83
  @Property({ name: 'availability_rule_set_id', type: 'uuid', nullable: true })
84
84
  availabilityRuleSetId?: string | null
85
85
 
86
+ @Property({ name: 'custom_fieldset_code', type: 'text', nullable: true })
87
+ customFieldsetCode?: string | null
88
+
86
89
  @Property({ name: 'created_at', type: Date, onCreate: () => new Date() })
87
90
  createdAt: Date = new Date()
88
91
 
@@ -1,4 +1,7 @@
1
1
  import { z } from 'zod'
2
+ import { fieldsetCodeRegex } from '@open-mercato/shared/modules/entities/validators'
3
+
4
+ const customFieldsetCodeSchema = z.string().trim().regex(fieldsetCodeRegex).max(100)
2
5
 
3
6
  const scopedCreateFields = {
4
7
  tenantId: z.string().uuid(),
@@ -52,6 +55,7 @@ export const resourcesResourceCreateSchema = z.object({
52
55
  .nullable(),
53
56
  isActive: z.boolean().optional(),
54
57
  availabilityRuleSetId: z.string().uuid().optional().nullable(),
58
+ customFieldsetCode: customFieldsetCodeSchema.optional().nullable(),
55
59
  })
56
60
 
57
61
  export const resourcesResourceUpdateSchema = z.object({
@@ -71,6 +75,7 @@ export const resourcesResourceUpdateSchema = z.object({
71
75
  .nullable(),
72
76
  isActive: z.boolean().optional(),
73
77
  availabilityRuleSetId: z.string().uuid().optional().nullable(),
78
+ customFieldsetCode: customFieldsetCodeSchema.optional().nullable(),
74
79
  })
75
80
 
76
81
  export const resourcesResourceTagCreateSchema = z.object({
@@ -707,25 +707,32 @@ export async function seedResourcesResourceExamples(
707
707
  const resourceByName = new Map(existingResources.map((resource) => [resource.name.toLowerCase(), resource]))
708
708
  const resourceByKey = new Map<string, ResourcesResource>()
709
709
  for (const seed of RESOURCE_SEEDS) {
710
+ const seedType = seed.typeKey ? typeByKey.get(seed.typeKey) ?? null : null
711
+ const seedFieldsetCode = resolveResourcesResourceFieldsetCode(seedType?.name ?? seed.name)
710
712
  const existing = resourceByName.get(seed.name.toLowerCase())
711
713
  if (existing) {
712
- if (!existing.resourceTypeId && seed.typeKey) {
713
- const typeId = typeByKey.get(seed.typeKey)?.id ?? null
714
- if (typeId) {
715
- existing.resourceTypeId = typeId
716
- existing.updatedAt = now
717
- em.persist(existing)
718
- }
714
+ let touched = false
715
+ if (!existing.resourceTypeId && seedType?.id) {
716
+ existing.resourceTypeId = seedType.id
717
+ touched = true
718
+ }
719
+ if (!existing.customFieldsetCode) {
720
+ existing.customFieldsetCode = seedFieldsetCode
721
+ touched = true
722
+ }
723
+ if (touched) {
724
+ existing.updatedAt = now
725
+ em.persist(existing)
719
726
  }
720
727
  resourceByKey.set(seed.key, existing)
721
728
  continue
722
729
  }
723
- const resourceTypeId = seed.typeKey ? typeByKey.get(seed.typeKey)?.id ?? null : null
724
730
  const resource = em.create(ResourcesResource, {
725
731
  tenantId: scope.tenantId,
726
732
  organizationId: scope.organizationId,
727
733
  name: seed.name,
728
- resourceTypeId,
734
+ resourceTypeId: seedType?.id ?? null,
735
+ customFieldsetCode: seedFieldsetCode,
729
736
  isActive: true,
730
737
  createdAt: now,
731
738
  updatedAt: now,
@@ -1118,6 +1118,22 @@
1118
1118
  "enumItems": [],
1119
1119
  "mappedType": "uuid"
1120
1120
  },
1121
+ "custom_fieldset_code": {
1122
+ "name": "custom_fieldset_code",
1123
+ "type": "text",
1124
+ "unsigned": false,
1125
+ "autoincrement": false,
1126
+ "primary": false,
1127
+ "nullable": true,
1128
+ "unique": false,
1129
+ "length": null,
1130
+ "precision": null,
1131
+ "scale": null,
1132
+ "default": null,
1133
+ "comment": null,
1134
+ "enumItems": [],
1135
+ "mappedType": "text"
1136
+ },
1121
1137
  "capacity": {
1122
1138
  "name": "capacity",
1123
1139
  "type": "int",
@@ -1389,4 +1405,4 @@
1389
1405
  ],
1390
1406
  "views": [],
1391
1407
  "nativeEnums": {}
1392
- }
1408
+ }
@@ -0,0 +1,13 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+
3
+ export class Migration20260608231000 extends Migration {
4
+
5
+ override async up(): Promise<void> {
6
+ this.addSql(`alter table "resources_resources" add column "custom_fieldset_code" text null;`);
7
+ }
8
+
9
+ override async down(): Promise<void> {
10
+ this.addSql(`alter table "resources_resources" drop column "custom_fieldset_code";`);
11
+ }
12
+
13
+ }
@@ -58,7 +58,7 @@ export default function CreateSalesDocumentPage() {
58
58
  } catch { /* ignore */ }
59
59
 
60
60
  // Auto-add line items from the inbox draft
61
- const lineItems = Array.isArray(inboxDraft.payload.lineItems)
61
+ const lineItems = kind === 'quote' && Array.isArray(inboxDraft.payload.lineItems)
62
62
  ? (inboxDraft.payload.lineItems as Record<string, unknown>[])
63
63
  : []
64
64
  const lineEndpoint = kind === 'order' ? '/api/sales/order-lines' : '/api/sales/quote-lines'
@@ -123,9 +123,7 @@ export default function CreateSalesDocumentPage() {
123
123
  initialKind={initialKind}
124
124
  inboxPreFill={inboxPreFill}
125
125
  />
126
- <p className="mt-4 text-sm text-muted-foreground">
127
- {t('sales.documents.form.nextStep', 'After creation you will add items, prices, and fulfillment details.')}
128
- </p>
126
+
129
127
  </PageBody>
130
128
  </Page>
131
129
  )
@@ -7066,6 +7066,14 @@ const orderLineDeleteCommand: CommandHandler<
7066
7066
  "Document not found or inaccessible.",
7067
7067
  ));
7068
7068
  }
7069
+ if (filtered.length === 0) {
7070
+ throw new CrudHttpError(409, {
7071
+ error: translate(
7072
+ "sales.documents.items.errorDeleteLast",
7073
+ "An order must contain at least one line item.",
7074
+ ),
7075
+ });
7076
+ }
7069
7077
  const sourceInputs = filtered.map((line, index) => ({
7070
7078
  ...mapOrderLineEntityToSnapshot(line),
7071
7079
  statusEntryId: line.statusEntryId ?? null,
@@ -833,6 +833,7 @@ export function SalesDocumentItemsSection({
833
833
  size="icon"
834
834
  variant="ghost"
835
835
  className="h-8 w-8"
836
+ aria-label={t('ui.actions.edit', 'Edit')}
836
837
  onClick={(event) => {
837
838
  event.stopPropagation();
838
839
  handleEdit(item);
@@ -840,17 +841,25 @@ export function SalesDocumentItemsSection({
840
841
  >
841
842
  <Pencil className="h-4 w-4" />
842
843
  </Button>
843
- <Button
844
- size="icon"
845
- variant="ghost"
846
- className="h-8 w-8 text-destructive"
847
- onClick={(event) => {
848
- event.stopPropagation();
849
- void handleDelete(item);
850
- }}
844
+ <span
845
+ title={kind === 'order' && items.length === 1
846
+ ? t('sales.documents.items.errorDeleteLast', 'An order must contain at least one line item.')
847
+ : undefined}
851
848
  >
852
- <Trash2 className="h-4 w-4" />
853
- </Button>
849
+ <Button
850
+ size="icon"
851
+ variant="ghost"
852
+ className="h-8 w-8 text-destructive"
853
+ aria-label={t('ui.actions.delete', 'Delete')}
854
+ disabled={kind === 'order' && items.length === 1}
855
+ onClick={(event) => {
856
+ event.stopPropagation();
857
+ void handleDelete(item);
858
+ }}
859
+ >
860
+ <Trash2 className="h-4 w-4" />
861
+ </Button>
862
+ </span>
854
863
  </div>
855
864
  </td>
856
865
  </tr>
@@ -272,7 +272,7 @@ type SnapshotEntity = {
272
272
  type SalesLineDialogProps = {
273
273
  open: boolean;
274
274
  kind: "order" | "quote";
275
- documentId: string;
275
+ documentId?: string;
276
276
  currencyCode: string | null | undefined;
277
277
  documentUpdatedAt?: string | null;
278
278
  organizationId: string | null;
@@ -281,6 +281,7 @@ type SalesLineDialogProps = {
281
281
  shippedQuantity?: number;
282
282
  onOpenChange: (open: boolean) => void;
283
283
  onSaved?: () => Promise<void> | void;
284
+ onDraftSaved?: (payload: Record<string, unknown>, lineId: string | null) => Promise<void> | void;
284
285
  };
285
286
 
286
287
  const defaultForm = (currencyCode?: string | null): LineFormState => ({
@@ -476,6 +477,7 @@ export function LineItemDialog({
476
477
  shippedQuantity = 0,
477
478
  onOpenChange,
478
479
  onSaved,
480
+ onDraftSaved,
479
481
  }: SalesLineDialogProps) {
480
482
  const t = useT();
481
483
  const scope = useOrganizationScopeDetail();
@@ -1273,7 +1275,7 @@ export function LineItemDialog({
1273
1275
  const resolvedOrg = resolvedOrganizationId;
1274
1276
  const resolvedTenant = resolvedTenantId;
1275
1277
 
1276
- if (!resolvedOrg || !resolvedTenant || !resolvedDocumentId) {
1278
+ if (!onDraftSaved && (!resolvedOrg || !resolvedTenant || !resolvedDocumentId)) {
1277
1279
  throw createCrudFormError(
1278
1280
  t(
1279
1281
  "sales.documents.items.errorScope",
@@ -1466,9 +1468,9 @@ export function LineItemDialog({
1466
1468
  };
1467
1469
 
1468
1470
  const payload: Record<string, unknown> = {
1469
- [documentKey]: String(resolvedDocumentId),
1470
- organizationId: String(resolvedOrg),
1471
- tenantId: String(resolvedTenant),
1471
+ ...(resolvedDocumentId ? { [documentKey]: resolvedDocumentId } : {}),
1472
+ ...(resolvedOrg ? { organizationId: resolvedOrg } : {}),
1473
+ ...(resolvedTenant ? { tenantId: resolvedTenant } : {}),
1472
1474
  productId: isCustomLine
1473
1475
  ? undefined
1474
1476
  : values.productId
@@ -1508,6 +1510,11 @@ export function LineItemDialog({
1508
1510
  if (resolvedName) payload.name = resolvedName;
1509
1511
 
1510
1512
  try {
1513
+ if (onDraftSaved) {
1514
+ await onDraftSaved(payload, editingId);
1515
+ closeDialog();
1516
+ return;
1517
+ }
1511
1518
  const action = editingId ? updateCrud : createCrud;
1512
1519
  const result = await withScopedApiRequestHeaders(
1513
1520
  buildOptimisticLockHeader(documentUpdatedAt),
@@ -1545,6 +1552,7 @@ export function LineItemDialog({
1545
1552
  documentKey,
1546
1553
  documentUpdatedAt,
1547
1554
  editingId,
1555
+ onDraftSaved,
1548
1556
  priceOptions,
1549
1557
  productOption,
1550
1558
  resourcePath,
@@ -59,6 +59,7 @@ import {
59
59
  import { AddressEditor, type AddressEditorDraft } from '@open-mercato/core/modules/customers/components/AddressEditor'
60
60
  import { useSalesChannelsEnabled } from '../useSalesChannelsEnabled'
61
61
  import { createLogger } from '@open-mercato/shared/lib/logger'
62
+ import { SalesOrderDraftLines, createSalesOrderLineDraft, type SalesOrderLineDraft } from './SalesOrderDraftLines'
62
63
 
63
64
  const logger = createLogger('sales')
64
65
 
@@ -102,6 +103,7 @@ export type SalesDocumentFormValues = {
102
103
  shippingAddressDraft?: AddressDraft
103
104
  billingAddressDraft?: AddressDraft
104
105
  comments?: string | null
106
+ lines?: SalesOrderLineDraft[]
105
107
  } & Record<string, unknown>
106
108
 
107
109
  type InboxPreFill = {
@@ -805,6 +807,7 @@ function CustomerGroupComponent({ values, setValue, t, customers, setCustomers,
805
807
 
806
808
  export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind, inboxPreFill }: SalesDocumentFormProps) {
807
809
  const t = useT()
810
+ const { organizationId, tenantId } = useOrganizationScopeDetail()
808
811
  const { enabled: channelsEnabled } = useSalesChannelsEnabled()
809
812
  const [customers, setCustomers] = React.useState<CustomerOption[]>([])
810
813
  const [customerLoading, setCustomerLoading] = React.useState(false)
@@ -1305,14 +1308,19 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1305
1308
  type: 'textarea',
1306
1309
  },
1307
1310
  {
1308
- id: 'infoNote',
1311
+ id: 'lines',
1309
1312
  label: '',
1310
1313
  type: 'custom',
1311
- component: () => (
1312
- <div className="rounded-lg border bg-muted/50 p-3 text-sm text-muted-foreground">
1313
- {t('sales.documents.form.nextStep', 'After creation you will add items, prices, and fulfillment details.')}
1314
- </div>
1315
- ),
1314
+ component: ({ value, error, values, setValue }) => values?.documentKind === 'order' ? (
1315
+ <SalesOrderDraftLines
1316
+ currencyCode={typeof values.currencyCode === 'string' ? values.currencyCode : defaultCurrency}
1317
+ organizationId={organizationId ?? null}
1318
+ tenantId={tenantId ?? null}
1319
+ lines={Array.isArray(value) ? value as SalesOrderLineDraft[] : []}
1320
+ error={error}
1321
+ onChange={(lines) => setValue(lines)}
1322
+ />
1323
+ ) : null,
1316
1324
  },
1317
1325
  ], [
1318
1326
  addressFormat,
@@ -1321,6 +1329,9 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1321
1329
  addressesLoading,
1322
1330
  channelsEnabled,
1323
1331
  currencyLabels,
1332
+ defaultCurrency,
1333
+ organizationId,
1334
+ tenantId,
1324
1335
  fetchCurrencyOptions,
1325
1336
  loadAddresses,
1326
1337
  loadChannels,
@@ -1340,6 +1351,7 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1340
1351
  },
1341
1352
  { id: 'channels-comments', title: '', column: 1, fields: channelsEnabled ? ['channelId', 'comments'] : ['comments'] },
1342
1353
  { id: 'currency', title: '', column: 2, fields: ['currencyCode'] },
1354
+ { id: 'lines', title: '', column: 1, fields: ['lines'] },
1343
1355
  { id: 'shipping', title: '', column: 2, fields: ['shippingAddressSection'] },
1344
1356
  { id: 'billing', title: '', column: 2, fields: ['billingAddressSection'] },
1345
1357
  { id: 'custom', title: t('sales.documents.form.customFields', 'Custom fields'), column: 2, kind: 'customFields' },
@@ -1361,6 +1373,21 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1361
1373
  channelId: inboxPreFill?.channelId || undefined,
1362
1374
  customerEntityId: inboxPreFill?.customerEntityId || undefined,
1363
1375
  comments: inboxPreFill?.comments || undefined,
1376
+ lines: (inboxPreFill?.lineItems ?? []).map((item, index) => {
1377
+ const unitPrice = Number(item.unitPrice ?? item.unitPriceGross ?? item.unitPriceNet ?? 0)
1378
+ const quantity = Number(item.quantity ?? 1)
1379
+ return createSalesOrderLineDraft({
1380
+ kind: item.kind || (item.productId ? 'product' : 'service'),
1381
+ productId: typeof item.productId === 'string' ? item.productId : undefined,
1382
+ productVariantId: typeof item.productVariantId === 'string' ? item.productVariantId : undefined,
1383
+ name: item.productName || item.name || `Line ${index + 1}`,
1384
+ quantity: Number.isFinite(quantity) ? quantity : 1,
1385
+ currencyCode: inboxPreFill?.currencyCode || defaultCurrency,
1386
+ unitPriceNet: Number.isFinite(unitPrice) ? unitPrice : 0,
1387
+ unitPriceGross: Number.isFinite(unitPrice) ? unitPrice : 0,
1388
+ metadata: item.productId ? undefined : { customLine: true, lineMode: 'custom' },
1389
+ })
1390
+ }),
1364
1391
  useCustomShipping: false,
1365
1392
  useCustomBilling: false,
1366
1393
  sameAsShipping: true,
@@ -1398,6 +1425,21 @@ export function SalesDocumentForm({ onCreated, isSubmitting = false, initialKind
1398
1425
  }
1399
1426
  if (documentKind === 'order') {
1400
1427
  payload.orderNumber = documentNumber
1428
+ const lines = Array.isArray(base.lines) ? base.lines : []
1429
+ if (lines.length === 0) {
1430
+ const message = t('sales.orders.linesRequired', 'Add at least one line item before creating the order.')
1431
+ throw createCrudFormError(message, { lines: message })
1432
+ }
1433
+ payload.lines = lines.map((line) => {
1434
+ const {
1435
+ orderId: _orderId,
1436
+ quoteId: _quoteId,
1437
+ organizationId: _organizationId,
1438
+ tenantId: _tenantId,
1439
+ ...linePayload
1440
+ } = line.payload
1441
+ return { ...linePayload, currencyCode: linePayload.currencyCode || payload.currencyCode }
1442
+ })
1401
1443
  } else {
1402
1444
  payload.quoteNumber = documentNumber
1403
1445
  }