@open-mercato/core 0.6.8-develop.7101.1.bd9356828a → 0.7.1-develop.7102.1.b41f7e3e51

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 (159) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/auth/api/login.js +19 -0
  3. package/dist/modules/auth/api/login.js.map +2 -2
  4. package/dist/modules/auth/api/routeError.js +11 -0
  5. package/dist/modules/auth/api/routeError.js.map +7 -0
  6. package/dist/modules/auth/api/users/acl/route.js +23 -7
  7. package/dist/modules/auth/api/users/acl/route.js.map +2 -2
  8. package/dist/modules/business_rules/backend/rules/[id]/page.meta.js +6 -1
  9. package/dist/modules/business_rules/backend/rules/[id]/page.meta.js.map +2 -2
  10. package/dist/modules/business_rules/backend/rules/create/page.meta.js +5 -1
  11. package/dist/modules/business_rules/backend/rules/create/page.meta.js.map +2 -2
  12. package/dist/modules/business_rules/backend/sets/[id]/page.meta.js +6 -1
  13. package/dist/modules/business_rules/backend/sets/[id]/page.meta.js.map +2 -2
  14. package/dist/modules/business_rules/backend/sets/create/page.meta.js +5 -1
  15. package/dist/modules/business_rules/backend/sets/create/page.meta.js.map +2 -2
  16. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js +84 -0
  17. package/dist/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.js.map +7 -0
  18. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js +7 -81
  19. package/dist/modules/customer_accounts/backend/customer_accounts/settings/page.js.map +2 -2
  20. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js +525 -0
  21. package/dist/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.js.map +7 -0
  22. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js +731 -0
  23. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.js.map +7 -0
  24. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js +7 -727
  25. package/dist/modules/customer_accounts/backend/customer_accounts/users/[id]/page.js.map +2 -2
  26. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js +7 -521
  27. package/dist/modules/customer_accounts/backend/customer_accounts/users/page.js.map +2 -2
  28. package/dist/modules/customer_accounts/extension-points.js +1 -1
  29. package/dist/modules/customer_accounts/extension-points.js.map +2 -2
  30. package/dist/modules/customer_accounts/lib/portalUrl.js +69 -0
  31. package/dist/modules/customer_accounts/lib/portalUrl.js.map +7 -0
  32. package/dist/modules/customers/backend/customers/companies/page.js +2 -2
  33. package/dist/modules/customers/backend/customers/companies/page.js.map +2 -2
  34. package/dist/modules/customers/backend/customers/deals/page.js +2 -2
  35. package/dist/modules/customers/backend/customers/deals/page.js.map +2 -2
  36. package/dist/modules/customers/backend/customers/people/page.js +2 -2
  37. package/dist/modules/customers/backend/customers/people/page.js.map +2 -2
  38. package/dist/modules/customers/components/detail/AssignRoleDialog.js +33 -28
  39. package/dist/modules/customers/components/detail/AssignRoleDialog.js.map +2 -2
  40. package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js +3 -1
  41. package/dist/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.js.map +2 -2
  42. package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js +1 -0
  43. package/dist/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.js.map +2 -2
  44. package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js +4 -1
  45. package/dist/modules/eudr/backend/eudr/evidence-submissions/page.meta.js.map +2 -2
  46. package/dist/modules/eudr/backend/eudr/page.meta.js +2 -2
  47. package/dist/modules/eudr/backend/eudr/page.meta.js.map +2 -2
  48. package/dist/modules/eudr/backend/eudr/plots/[id]/page.js +8 -2
  49. package/dist/modules/eudr/backend/eudr/plots/[id]/page.js.map +2 -2
  50. package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js +3 -1
  51. package/dist/modules/eudr/backend/eudr/plots/[id]/page.meta.js.map +2 -2
  52. package/dist/modules/eudr/backend/eudr/plots/create/page.js +8 -2
  53. package/dist/modules/eudr/backend/eudr/plots/create/page.js.map +2 -2
  54. package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js +1 -0
  55. package/dist/modules/eudr/backend/eudr/plots/create/page.meta.js.map +2 -2
  56. package/dist/modules/eudr/backend/eudr/plots/page.meta.js +4 -1
  57. package/dist/modules/eudr/backend/eudr/plots/page.meta.js.map +2 -2
  58. package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js +3 -1
  59. package/dist/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.js.map +2 -2
  60. package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js +1 -0
  61. package/dist/modules/eudr/backend/eudr/product-mappings/create/page.meta.js.map +2 -2
  62. package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js +4 -1
  63. package/dist/modules/eudr/backend/eudr/product-mappings/page.meta.js.map +2 -2
  64. package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js +3 -1
  65. package/dist/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.js.map +2 -2
  66. package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js +1 -0
  67. package/dist/modules/eudr/backend/eudr/risk-assessments/create/page.meta.js.map +2 -2
  68. package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js +4 -1
  69. package/dist/modules/eudr/backend/eudr/risk-assessments/page.meta.js.map +2 -2
  70. package/dist/modules/eudr/backend/eudr/statements/[id]/page.js +2 -0
  71. package/dist/modules/eudr/backend/eudr/statements/[id]/page.js.map +2 -2
  72. package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js +3 -1
  73. package/dist/modules/eudr/backend/eudr/statements/[id]/page.meta.js.map +2 -2
  74. package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js +1 -0
  75. package/dist/modules/eudr/backend/eudr/statements/create/page.meta.js.map +2 -2
  76. package/dist/modules/eudr/backend/eudr/statements/page.js +30 -25
  77. package/dist/modules/eudr/backend/eudr/statements/page.js.map +2 -2
  78. package/dist/modules/eudr/backend/eudr/statements/page.meta.js +4 -1
  79. package/dist/modules/eudr/backend/eudr/statements/page.meta.js.map +2 -2
  80. package/dist/modules/eudr/commands/statements.js +5 -3
  81. package/dist/modules/eudr/commands/statements.js.map +2 -2
  82. package/dist/modules/eudr/components/crudErrorI18n.js +21 -1
  83. package/dist/modules/eudr/components/crudErrorI18n.js.map +2 -2
  84. package/dist/modules/eudr/components/formConfig.js +6 -2
  85. package/dist/modules/eudr/components/formConfig.js.map +2 -2
  86. package/dist/modules/eudr/lib/statement-lifecycle.js +9 -1
  87. package/dist/modules/eudr/lib/statement-lifecycle.js.map +2 -2
  88. package/dist/modules/sales/components/documents/LineItemDialog.js +1 -1
  89. package/dist/modules/sales/components/documents/LineItemDialog.js.map +1 -1
  90. package/dist/modules/sales/widgets/injection/document-history/widget.client.js +44 -2
  91. package/dist/modules/sales/widgets/injection/document-history/widget.client.js.map +2 -2
  92. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js +20 -16
  93. package/dist/modules/wms/components/backend/WmsLocationDetailPage.js.map +2 -2
  94. package/dist/modules/wms/components/backend/WmsLotDetailPage.js +10 -6
  95. package/dist/modules/wms/components/backend/WmsLotDetailPage.js.map +2 -2
  96. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js +11 -7
  97. package/dist/modules/wms/components/backend/WmsOperationalDashboardPage.js.map +2 -2
  98. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js +10 -6
  99. package/dist/modules/wms/components/backend/WmsSkuDetailPage.js.map +2 -2
  100. package/package.json +8 -8
  101. package/src/modules/auth/api/login.ts +23 -0
  102. package/src/modules/auth/api/routeError.ts +30 -0
  103. package/src/modules/auth/api/users/acl/route.ts +49 -9
  104. package/src/modules/business_rules/backend/rules/[id]/page.meta.ts +5 -0
  105. package/src/modules/business_rules/backend/rules/create/page.meta.ts +5 -1
  106. package/src/modules/business_rules/backend/sets/[id]/page.meta.ts +5 -0
  107. package/src/modules/business_rules/backend/sets/create/page.meta.ts +5 -1
  108. package/src/modules/customer_accounts/backend/customer_accounts/settings/CustomerAccountsSettingsPageClient.tsx +138 -0
  109. package/src/modules/customer_accounts/backend/customer_accounts/settings/page.tsx +8 -132
  110. package/src/modules/customer_accounts/backend/customer_accounts/users/PortalUsersPageClient.tsx +601 -0
  111. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/PortalUserDetailPageClient.tsx +838 -0
  112. package/src/modules/customer_accounts/backend/customer_accounts/users/[id]/page.tsx +6 -838
  113. package/src/modules/customer_accounts/backend/customer_accounts/users/page.tsx +6 -590
  114. package/src/modules/customer_accounts/extension-points.ts +1 -1
  115. package/src/modules/customer_accounts/lib/portalUrl.ts +112 -0
  116. package/src/modules/customers/backend/customers/companies/page.tsx +2 -2
  117. package/src/modules/customers/backend/customers/deals/page.tsx +2 -2
  118. package/src/modules/customers/backend/customers/people/page.tsx +2 -2
  119. package/src/modules/customers/components/detail/AssignRoleDialog.tsx +12 -7
  120. package/src/modules/eudr/backend/eudr/evidence-submissions/[id]/page.meta.ts +2 -0
  121. package/src/modules/eudr/backend/eudr/evidence-submissions/create/page.meta.ts +1 -0
  122. package/src/modules/eudr/backend/eudr/evidence-submissions/page.meta.ts +4 -1
  123. package/src/modules/eudr/backend/eudr/page.meta.ts +2 -2
  124. package/src/modules/eudr/backend/eudr/plots/[id]/page.meta.ts +2 -0
  125. package/src/modules/eudr/backend/eudr/plots/[id]/page.tsx +8 -2
  126. package/src/modules/eudr/backend/eudr/plots/create/page.meta.ts +1 -0
  127. package/src/modules/eudr/backend/eudr/plots/create/page.tsx +8 -2
  128. package/src/modules/eudr/backend/eudr/plots/page.meta.ts +4 -1
  129. package/src/modules/eudr/backend/eudr/product-mappings/[id]/page.meta.ts +2 -0
  130. package/src/modules/eudr/backend/eudr/product-mappings/create/page.meta.ts +1 -0
  131. package/src/modules/eudr/backend/eudr/product-mappings/page.meta.ts +4 -1
  132. package/src/modules/eudr/backend/eudr/risk-assessments/[id]/page.meta.ts +2 -0
  133. package/src/modules/eudr/backend/eudr/risk-assessments/create/page.meta.ts +1 -0
  134. package/src/modules/eudr/backend/eudr/risk-assessments/page.meta.ts +4 -1
  135. package/src/modules/eudr/backend/eudr/statements/[id]/page.meta.ts +2 -0
  136. package/src/modules/eudr/backend/eudr/statements/[id]/page.tsx +2 -0
  137. package/src/modules/eudr/backend/eudr/statements/create/page.meta.ts +1 -0
  138. package/src/modules/eudr/backend/eudr/statements/page.meta.ts +4 -1
  139. package/src/modules/eudr/backend/eudr/statements/page.tsx +32 -26
  140. package/src/modules/eudr/commands/statements.ts +4 -2
  141. package/src/modules/eudr/components/crudErrorI18n.ts +31 -0
  142. package/src/modules/eudr/components/formConfig.tsx +9 -1
  143. package/src/modules/eudr/i18n/de.json +3 -2
  144. package/src/modules/eudr/i18n/en.json +3 -2
  145. package/src/modules/eudr/i18n/es.json +3 -2
  146. package/src/modules/eudr/i18n/ko.json +3 -2
  147. package/src/modules/eudr/i18n/pl.json +3 -2
  148. package/src/modules/eudr/lib/statement-lifecycle.ts +8 -0
  149. package/src/modules/sales/components/documents/LineItemDialog.tsx +1 -1
  150. package/src/modules/sales/i18n/de.json +37 -0
  151. package/src/modules/sales/i18n/en.json +37 -0
  152. package/src/modules/sales/i18n/es.json +37 -0
  153. package/src/modules/sales/i18n/ko.json +37 -0
  154. package/src/modules/sales/i18n/pl.json +37 -0
  155. package/src/modules/sales/widgets/injection/document-history/widget.client.tsx +42 -2
  156. package/src/modules/wms/components/backend/WmsLocationDetailPage.tsx +30 -26
  157. package/src/modules/wms/components/backend/WmsLotDetailPage.tsx +21 -17
  158. package/src/modules/wms/components/backend/WmsOperationalDashboardPage.tsx +17 -13
  159. package/src/modules/wms/components/backend/WmsSkuDetailPage.tsx +16 -12
@@ -0,0 +1,112 @@
1
+ // Resolve the browser-visible origin on the server so the admin portal-address
2
+ // hints are already correct in the first server-rendered paint. Deriving the
3
+ // origin from `window.location.origin` during render made the server HTML and
4
+ // the hydrated client output disagree, which React reported as a hydration
5
+ // error and repaired with a visible flicker of the portal address (#5457).
6
+ //
7
+ // `resolvePortalRequestOrigin` is deliberately named apart from the shared
8
+ // `resolveRequestOrigin` in `@open-mercato/shared/lib/url`: that one takes a
9
+ // `Request` and reads the protocol off `req.url`, which a server component does
10
+ // not have — it only has `headers()`. This one also validates the untrusted host
11
+ // header before the value reaches rendered text or a link href, and infers the
12
+ // protocol when the proxy stays quiet, so the two are not interchangeable.
13
+
14
+ export type RequestHeaderReader = {
15
+ get(name: string): string | null | undefined
16
+ }
17
+
18
+ const FORWARDED_HOST_HEADER = 'x-forwarded-host'
19
+ const HOST_HEADER = 'host'
20
+ const FORWARDED_PROTO_HEADER = 'x-forwarded-proto'
21
+
22
+ // Host headers are client-controlled, so only a plain `host[:port]` or bracketed
23
+ // IPv6 literal is accepted before it reaches rendered text and link hrefs.
24
+ const HOST_PATTERN = /^(?:[a-z0-9.-]+|\[[0-9a-f:.]+\])(?::\d{1,5})?$/i
25
+ const PROTOCOL_PATTERN = /^https?$/i
26
+ // Bracketed, because HOST_PATTERN only accepts a colon-bearing host in that form.
27
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])
28
+
29
+ export const PORTAL_ORG_SLUG_PLACEHOLDER = '[org-slug]'
30
+
31
+ function firstHeaderValue(raw: string | null | undefined): string | null {
32
+ if (typeof raw !== 'string') return null
33
+ // Proxy chains append values, so the client-facing hop is the first entry.
34
+ const value = raw.split(',')[0]?.trim() ?? ''
35
+ return value.length > 0 ? value : null
36
+ }
37
+
38
+ function readHost(headers: RequestHeaderReader): string | null {
39
+ const forwarded = firstHeaderValue(headers.get(FORWARDED_HOST_HEADER))
40
+ const host = forwarded ?? firstHeaderValue(headers.get(HOST_HEADER))
41
+ if (!host) return null
42
+ return HOST_PATTERN.test(host) ? host.toLowerCase() : null
43
+ }
44
+
45
+ function readProtocol(headers: RequestHeaderReader, host: string): string {
46
+ const forwarded = firstHeaderValue(headers.get(FORWARDED_PROTO_HEADER))
47
+ if (forwarded && PROTOCOL_PATTERN.test(forwarded)) return forwarded.toLowerCase()
48
+ // An operator who configured APP_URL has named the canonical origin explicitly;
49
+ // when it points at this very host, trust it over any inference below. Without
50
+ // this, a plain-HTTP deployment behind a proxy that omits x-forwarded-proto
51
+ // would render https:// links that do not work.
52
+ const configured = configuredOrigin()
53
+ if (configured) {
54
+ try {
55
+ const parsed = new URL(configured)
56
+ if (parsed.host.toLowerCase() === host) return parsed.protocol.replace(':', '')
57
+ } catch {
58
+ // configuredOrigin() already parsed this successfully; ignore defensively.
59
+ }
60
+ }
61
+ // Otherwise a terminating proxy that omits the header is serving a public
62
+ // hostname over TLS far more often than not; only loopback hosts default to http.
63
+ const hostname = host.replace(/:\d{1,5}$/, '')
64
+ return LOOPBACK_HOSTS.has(hostname) ? 'http' : 'https'
65
+ }
66
+
67
+ function configuredOrigin(): string {
68
+ const candidates = [process.env.APP_URL, process.env.NEXT_PUBLIC_APP_URL]
69
+ for (const candidate of candidates) {
70
+ const value = candidate?.trim()
71
+ if (!value) continue
72
+ try {
73
+ const parsed = new URL(value)
74
+ if (PROTOCOL_PATTERN.test(parsed.protocol.replace(':', ''))) return parsed.origin
75
+ } catch {
76
+ // A malformed APP_URL must not break the page — fall through to the next candidate.
77
+ }
78
+ }
79
+ return ''
80
+ }
81
+
82
+ /**
83
+ * Resolve the origin the current request was made to, mirroring the value
84
+ * `window.location.origin` reports in the browser. Falls back to the configured
85
+ * app URL, then to an empty string so callers render a root-relative path that
86
+ * is identical on the server and the client.
87
+ *
88
+ * Named apart from `resolveRequestOrigin` in `@open-mercato/shared/lib/url` —
89
+ * see the note at the top of this file for why the two cannot be merged.
90
+ */
91
+ export function resolvePortalRequestOrigin(headers: RequestHeaderReader | null | undefined): string {
92
+ if (headers && typeof headers.get === 'function') {
93
+ const host = readHost(headers)
94
+ if (host) return `${readProtocol(headers, host)}://${host}`
95
+ }
96
+ return configuredOrigin()
97
+ }
98
+
99
+ function normalizeOrigin(origin: string | null | undefined): string {
100
+ if (typeof origin !== 'string') return ''
101
+ return origin.trim().replace(/\/+$/, '')
102
+ }
103
+
104
+ /** Display pattern for a tenant portal address, e.g. `https://app.example.com/[org-slug]/portal`. */
105
+ export function buildPortalUrlPattern(origin: string | null | undefined): string {
106
+ return `${normalizeOrigin(origin)}/${PORTAL_ORG_SLUG_PLACEHOLDER}/portal`
107
+ }
108
+
109
+ /** Portal entry point used by the "Open Portal" action on admin pages. */
110
+ export function buildPortalRootUrl(origin: string | null | undefined): string {
111
+ return `${normalizeOrigin(origin)}/portal`
112
+ }
@@ -829,8 +829,8 @@ export default function CustomersCompaniesPage() {
829
829
  header: def.label || def.key,
830
830
  enableSorting: true,
831
831
  meta: {
832
- columnChooserGroup: def.group?.title ?? 'Custom Fields',
833
- filterGroup: def.group?.title ?? 'Custom Fields',
832
+ columnChooserGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
833
+ filterGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
834
834
  filterType: mapCustomFieldKindToFilterType(def.kind),
835
835
  filterOptions: normalizeCustomFieldFilterOptions(def.options),
836
836
  hidden: def.listVisible === false,
@@ -687,8 +687,8 @@ export default function CustomersDealsPage() {
687
687
  accessorKey: `cf_${def.key}`,
688
688
  header: def.label || def.key,
689
689
  meta: {
690
- columnChooserGroup: def.group?.title ?? 'Custom Fields',
691
- filterGroup: def.group?.title ?? 'Custom Fields',
690
+ columnChooserGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
691
+ filterGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
692
692
  filterType: mapCustomFieldKindToFilterType(def.kind),
693
693
  filterOptions: normalizeCustomFieldFilterOptions(def.options),
694
694
  hidden: def.listVisible === false,
@@ -856,8 +856,8 @@ export default function CustomersPeoplePage() {
856
856
  header: def.label || def.key,
857
857
  enableSorting: true,
858
858
  meta: {
859
- columnChooserGroup: def.group?.title ?? 'Custom Fields',
860
- filterGroup: def.group?.title ?? 'Custom Fields',
859
+ columnChooserGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
860
+ filterGroup: def.group?.title ?? t('ui.columnChooser.customFieldsGroup', 'Custom Fields'),
861
861
  filterType: mapCustomFieldKindToFilterType(def.kind),
862
862
  filterOptions: normalizeCustomFieldFilterOptions(def.options),
863
863
  hidden: def.listVisible === false,
@@ -640,13 +640,18 @@ export function AssignRoleDialog({
640
640
  </div>
641
641
  </div>
642
642
 
643
- <DialogFooter className="shrink-0 border-t border-border/70 px-6 py-4 sm:justify-between">
644
- <p className="text-xs text-muted-foreground">
645
- {t(
646
- 'customers.roles.dialog.footerNote',
647
- 'One person per role · can be changed at any time',
648
- )}
649
- </p>
643
+ <DialogFooter
644
+ bordered={false}
645
+ className="shrink-0 border-t border-border/70 px-6 py-4"
646
+ leading={
647
+ <p className="text-xs text-muted-foreground sm:max-w-2xs">
648
+ {t(
649
+ 'customers.roles.dialog.footerNote',
650
+ 'One person per role · can be changed at any time',
651
+ )}
652
+ </p>
653
+ }
654
+ >
650
655
  <div className="flex items-center gap-2">
651
656
  <Button type="button" variant="outline" onClick={onClose}>
652
657
  {t('customers.roles.cancelAdd', 'Cancel')}
@@ -7,6 +7,8 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Evidence submissions', labelKey: 'eudr.nav.submissions', href: '/backend/eudr/evidence-submissions' },
12
+ { label: 'Edit evidence submission', labelKey: 'eudr.evidenceSubmissions.edit.title' },
11
13
  ],
12
14
  }
@@ -7,6 +7,7 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Evidence submissions', labelKey: 'eudr.nav.submissions', href: '/backend/eudr/evidence-submissions' },
11
12
  { label: 'Create', labelKey: 'eudr.evidenceSubmissions.create.title' },
12
13
  ],
@@ -8,5 +8,8 @@ export const metadata = {
8
8
  pagePriority: 10,
9
9
  pageOrder: 20,
10
10
  icon: 'files',
11
- breadcrumb: [{ label: 'Evidence submissions', labelKey: 'eudr.nav.submissions' }],
11
+ breadcrumb: [
12
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
13
+ { label: 'Evidence submissions', labelKey: 'eudr.nav.submissions' },
14
+ ],
12
15
  }
@@ -1,12 +1,12 @@
1
1
  export const metadata = {
2
2
  requireAuth: true,
3
3
  requireFeatures: ['eudr.statements.view'],
4
- pageTitle: 'Overview',
4
+ pageTitle: 'EUDR Overview',
5
5
  pageTitleKey: 'eudr.nav.overview',
6
6
  pageGroup: 'Compliance',
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  pagePriority: 10,
9
9
  pageOrder: 5,
10
10
  icon: 'gauge',
11
- breadcrumb: [{ label: 'Overview', labelKey: 'eudr.nav.overview' }],
11
+ breadcrumb: [{ label: 'EUDR Overview', labelKey: 'eudr.nav.overview' }],
12
12
  }
@@ -7,6 +7,8 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Plots', labelKey: 'eudr.nav.plots', href: '/backend/eudr/plots' },
12
+ { label: 'Edit plot', labelKey: 'eudr.plots.edit.title' },
11
13
  ],
12
14
  }
@@ -93,6 +93,8 @@ export default function EditEudrPlotPage({ params }: { params?: { id?: string }
93
93
  const [loading, setLoading] = React.useState(true)
94
94
  const [error, setError] = React.useState<string | null>(null)
95
95
  const [notFound, setNotFound] = React.useState(false)
96
+ const [geometryDraft, setGeometryDraft] = React.useState<string | null>(null)
97
+ const areaRequired = !isPolygonGeometry(geometryDraft ?? formatGeometry(record?.geometry))
96
98
 
97
99
  React.useEffect(() => {
98
100
  let cancelled = false
@@ -197,7 +199,10 @@ export default function EditEudrPlotPage({ params }: { params?: { id?: string }
197
199
  <GeometryInput
198
200
  id={id}
199
201
  value={typeof value === 'string' ? value : ''}
200
- onChange={(nextValue) => setValue(nextValue)}
202
+ onChange={(nextValue) => {
203
+ setValue(nextValue)
204
+ setGeometryDraft(nextValue)
205
+ }}
201
206
  disabled={disabled}
202
207
  areaHa={typeof values?.areaHa === 'string' || typeof values?.areaHa === 'number' ? String(values.areaHa) : ''}
203
208
  />
@@ -208,6 +213,7 @@ export default function EditEudrPlotPage({ params }: { params?: { id?: string }
208
213
  layout: 'half',
209
214
  label: translate('eudr.plots.form.areaHa'),
210
215
  type: 'custom',
216
+ required: areaRequired,
211
217
  description: translate('eudr.plots.form.areaHaHelp'),
212
218
  component: ({ id, value, setValue, values, disabled }) => {
213
219
  const polygonGeometry = isPolygonGeometry(values?.geometry)
@@ -241,7 +247,7 @@ export default function EditEudrPlotPage({ params }: { params?: { id?: string }
241
247
  />
242
248
  ),
243
249
  },
244
- ], [translate])
250
+ ], [translate, areaRequired])
245
251
 
246
252
  const groups = React.useMemo<CrudFormGroup[]>(() => [
247
253
  {
@@ -7,6 +7,7 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Plots', labelKey: 'eudr.nav.plots', href: '/backend/eudr/plots' },
11
12
  { label: 'Create', labelKey: 'eudr.plots.create.title' },
12
13
  ],
@@ -50,6 +50,8 @@ function isCompanySnapshot(value: unknown): value is CompanySnapshot {
50
50
  export default function CreateEudrPlotPage() {
51
51
  const translate = useT()
52
52
  const router = useRouter()
53
+ const [geometryDraft, setGeometryDraft] = React.useState('')
54
+ const areaRequired = !isPolygonGeometry(geometryDraft)
53
55
 
54
56
  const fields = React.useMemo<CrudField[]>(() => [
55
57
  {
@@ -113,7 +115,10 @@ export default function CreateEudrPlotPage() {
113
115
  <GeometryInput
114
116
  id={id}
115
117
  value={typeof value === 'string' ? value : ''}
116
- onChange={(nextValue) => setValue(nextValue)}
118
+ onChange={(nextValue) => {
119
+ setValue(nextValue)
120
+ setGeometryDraft(nextValue)
121
+ }}
117
122
  disabled={disabled}
118
123
  areaHa={typeof values?.areaHa === 'string' || typeof values?.areaHa === 'number' ? String(values.areaHa) : ''}
119
124
  />
@@ -124,6 +129,7 @@ export default function CreateEudrPlotPage() {
124
129
  layout: 'half',
125
130
  label: translate('eudr.plots.form.areaHa'),
126
131
  type: 'custom',
132
+ required: areaRequired,
127
133
  description: translate('eudr.plots.form.areaHaHelp'),
128
134
  component: ({ id, value, setValue, values, disabled }) => {
129
135
  const polygonGeometry = isPolygonGeometry(values?.geometry)
@@ -158,7 +164,7 @@ export default function CreateEudrPlotPage() {
158
164
  />
159
165
  ),
160
166
  },
161
- ], [translate])
167
+ ], [translate, areaRequired])
162
168
 
163
169
  const groups = React.useMemo<CrudFormGroup[]>(() => [
164
170
  {
@@ -8,5 +8,8 @@ export const metadata = {
8
8
  pagePriority: 10,
9
9
  pageOrder: 20,
10
10
  icon: 'map',
11
- breadcrumb: [{ label: 'Plots', labelKey: 'eudr.nav.plots' }],
11
+ breadcrumb: [
12
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
13
+ { label: 'Plots', labelKey: 'eudr.nav.plots' },
14
+ ],
12
15
  }
@@ -7,6 +7,8 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Product mappings', labelKey: 'eudr.nav.mappings', href: '/backend/eudr/product-mappings' },
12
+ { label: 'Edit product mapping', labelKey: 'eudr.productMappings.edit.title' },
11
13
  ],
12
14
  }
@@ -7,6 +7,7 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Product mappings', labelKey: 'eudr.nav.mappings', href: '/backend/eudr/product-mappings' },
11
12
  { label: 'Create', labelKey: 'eudr.productMappings.create.title' },
12
13
  ],
@@ -8,5 +8,8 @@ export const metadata = {
8
8
  pagePriority: 10,
9
9
  pageOrder: 10,
10
10
  icon: 'package',
11
- breadcrumb: [{ label: 'Product mappings', labelKey: 'eudr.nav.mappings' }],
11
+ breadcrumb: [
12
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
13
+ { label: 'Product mappings', labelKey: 'eudr.nav.mappings' },
14
+ ],
12
15
  }
@@ -7,6 +7,8 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Risk assessments', labelKey: 'eudr.nav.riskAssessments', href: '/backend/eudr/risk-assessments' },
12
+ { label: 'Edit risk assessment', labelKey: 'eudr.riskAssessments.edit.title' },
11
13
  ],
12
14
  }
@@ -7,6 +7,7 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Risk assessments', labelKey: 'eudr.nav.riskAssessments', href: '/backend/eudr/risk-assessments' },
11
12
  { label: 'Create', labelKey: 'eudr.riskAssessments.create.title' },
12
13
  ],
@@ -8,5 +8,8 @@ export const metadata = {
8
8
  pagePriority: 10,
9
9
  pageOrder: 40,
10
10
  icon: 'shield-alert',
11
- breadcrumb: [{ label: 'Risk assessments', labelKey: 'eudr.nav.riskAssessments' }],
11
+ breadcrumb: [
12
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
13
+ { label: 'Risk assessments', labelKey: 'eudr.nav.riskAssessments' },
14
+ ],
12
15
  }
@@ -7,6 +7,8 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Statements', labelKey: 'eudr.nav.statements', href: '/backend/eudr/statements' },
12
+ { label: 'Edit statement', labelKey: 'eudr.statements.edit.title' },
11
13
  ],
12
14
  }
@@ -29,6 +29,7 @@ import {
29
29
  type ReferencedStatementValue,
30
30
  translateEudrCrudError,
31
31
  } from '../../../../components/formConfig'
32
+ import { canDeleteStatement } from '../../../../lib/statement-lifecycle'
32
33
  import { StatementLifecycleBar } from '../../../../components/StatementLifecycleBar'
33
34
  import { StatementReadinessChecklist } from '../../../../components/StatementReadinessChecklist'
34
35
  import { StatementRiskSection, type StatementLatestRisk } from '../../../../components/StatementRiskSection'
@@ -644,6 +645,7 @@ export default function EditEudrStatementPage({ params }: { params?: { id?: stri
644
645
  backHref="/backend/eudr/statements"
645
646
  cancelHref="/backend/eudr/statements"
646
647
  deleteRedirect="/backend/eudr/statements"
648
+ deleteVisible={canDeleteStatement(record.status)}
647
649
  submitLabel={translate('eudr.statements.form.submitUpdate')}
648
650
  fields={fields}
649
651
  groups={groups}
@@ -7,6 +7,7 @@ export const metadata = {
7
7
  pageGroupKey: 'eudr.nav.group',
8
8
  navHidden: true,
9
9
  breadcrumb: [
10
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
10
11
  { label: 'Statements', labelKey: 'eudr.nav.statements', href: '/backend/eudr/statements' },
11
12
  { label: 'Create', labelKey: 'eudr.statements.create.title' },
12
13
  ],
@@ -8,5 +8,8 @@ export const metadata = {
8
8
  pagePriority: 10,
9
9
  pageOrder: 30,
10
10
  icon: 'file-text',
11
- breadcrumb: [{ label: 'Statements', labelKey: 'eudr.nav.statements' }],
11
+ breadcrumb: [
12
+ { label: 'EUDR', labelKey: 'eudr.nav.module', href: '/backend/eudr' },
13
+ { label: 'Statements', labelKey: 'eudr.nav.statements' },
14
+ ],
12
15
  }
@@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'
6
6
  import { Page, PageBody } from '@open-mercato/ui/backend/Page'
7
7
  import { DataTable } from '@open-mercato/ui/backend/DataTable'
8
8
  import { ListEmptyState } from '@open-mercato/ui/backend/filters/ListEmptyState'
9
- import { RowActions } from '@open-mercato/ui/backend/RowActions'
9
+ import { RowActions, type RowActionItem } from '@open-mercato/ui/backend/RowActions'
10
10
  import type { FilterDef, FilterValues } from '@open-mercato/ui/backend/FilterBar'
11
11
  import { apiCall, withScopedApiRequestHeaders } from '@open-mercato/ui/backend/utils/apiCall'
12
12
  import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimisticLock'
@@ -22,7 +22,8 @@ import type { LegacyColumnDef as ColumnDef } from '@tanstack/react-table/legacy'
22
22
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
23
23
  import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
24
24
  import type { EudrActivityType, EudrCommodity, EudrRiskConclusion, EudrRiskTier, EudrStatementStatus } from '../../../data/validators'
25
- import { commodityOptions, statementStatusOptions, statusBadgeVariant } from '../../../components/formConfig'
25
+ import { commodityOptions, statementStatusOptions, statusBadgeVariant, translateEudrErrorMessage } from '../../../components/formConfig'
26
+ import { canDeleteStatement } from '../../../lib/statement-lifecycle'
26
27
  import {
27
28
  riskConclusionBadgeVariant,
28
29
  riskTierBadgeVariant,
@@ -189,7 +190,11 @@ export default function EudrStatementsPage() {
189
190
  refreshRows()
190
191
  } catch (error) {
191
192
  if (surfaceRecordConflict(error, translate, { onRefresh: refreshRows })) return
192
- flash(translate('eudr.statements.list.deleteError'), 'error')
193
+ flash(
194
+ translateEudrErrorMessage(error, translate, translate('eudr.statements.list.deleteError')),
195
+ 'error',
196
+ )
197
+ refreshRows()
193
198
  }
194
199
  }, [confirm, mutationContextId, refreshRows, retryLastMutation, runMutation, translate])
195
200
 
@@ -312,30 +317,31 @@ export default function EudrStatementsPage() {
312
317
  </Link>
313
318
  </Button>
314
319
  )}
315
- rowActions={(row) => (
316
- <RowActions
317
- items={[
318
- {
319
- id: 'edit',
320
- label: translate('eudr.statements.list.actions.edit'),
321
- href: `/backend/eudr/statements/${row.id}`,
322
- },
323
- {
324
- id: 'duplicate',
325
- label: translate('eudr.statements.duplicateAction'),
326
- href: `/backend/eudr/statements/create?duplicateFrom=${encodeURIComponent(row.id)}`,
327
- },
328
- {
329
- id: 'delete',
330
- label: translate('eudr.statements.list.actions.delete'),
331
- destructive: true,
332
- onSelect: () => {
333
- void handleDelete(row)
334
- },
320
+ rowActions={(row) => {
321
+ const items: RowActionItem[] = [
322
+ {
323
+ id: 'edit',
324
+ label: translate('eudr.statements.list.actions.edit'),
325
+ href: `/backend/eudr/statements/${row.id}`,
326
+ },
327
+ {
328
+ id: 'duplicate',
329
+ label: translate('eudr.statements.duplicateAction'),
330
+ href: `/backend/eudr/statements/create?duplicateFrom=${encodeURIComponent(row.id)}`,
331
+ },
332
+ ]
333
+ if (canDeleteStatement(row.status)) {
334
+ items.push({
335
+ id: 'delete',
336
+ label: translate('eudr.statements.list.actions.delete'),
337
+ destructive: true,
338
+ onSelect: () => {
339
+ void handleDelete(row)
335
340
  },
336
- ]}
337
- />
338
- )}
341
+ })
342
+ }
343
+ return <RowActions items={items} />
344
+ }}
339
345
  onRowClick={(row) => router.push(`/backend/eudr/statements/${row.id}`)}
340
346
  rowClickActionIds={['edit']}
341
347
  emptyState={(
@@ -43,9 +43,11 @@ import {
43
43
  } from '../data/validators'
44
44
  import {
45
45
  EUDR_AMEND_GUARDED_FIELDS,
46
+ canDeleteStatement,
46
47
  canTransition,
47
48
  evaluateSubmissionGate,
48
49
  isAmendWindowOpen,
50
+ isStatementReadOnly,
49
51
  type GateAssessmentView,
50
52
  type GateSubmissionView,
51
53
  } from '../lib/statement-lifecycle'
@@ -495,7 +497,7 @@ async function validateStatementUpdate(
495
497
  }
496
498
 
497
499
  const hasBaseChanges = hasStatementFieldChanges(record, parsed)
498
- if (record.status === 'archived' && (hasBaseChanges || Object.keys(custom).length > 0)) {
500
+ if (isStatementReadOnly(record.status) && (hasBaseChanges || Object.keys(custom).length > 0)) {
499
501
  throw new CrudHttpError(400, { error: 'eudr.errors.archivedReadOnly' })
500
502
  }
501
503
 
@@ -841,7 +843,7 @@ const deleteStatementCommand: CommandHandler<{ body?: Record<string, unknown>; q
841
843
  if (!record) throw new CrudHttpError(404, { error: 'EUDR due diligence statement not found' })
842
844
  ensureTenantScope(ctx, record.tenantId)
843
845
  ensureOrganizationScope(ctx, record.organizationId)
844
- if (record.status === 'archived') {
846
+ if (isStatementStatus(record.status) && !canDeleteStatement(record.status)) {
845
847
  throw new CrudHttpError(400, { error: 'eudr.errors.archivedReadOnly' })
846
848
  }
847
849
  if (record.status === 'available') {
@@ -15,6 +15,37 @@ function translateEudrErrorToken(token: string, translate: Translator): string {
15
15
  return translated === trimmed ? token : translated
16
16
  }
17
17
 
18
+ function readEudrErrorToken(err: unknown): string | null {
19
+ if (err == null || typeof err !== 'object') return null
20
+ const candidates = [
21
+ (err as { error?: unknown }).error,
22
+ (err as { message?: unknown }).message,
23
+ ]
24
+ for (const candidate of candidates) {
25
+ if (typeof candidate !== 'string') continue
26
+ const trimmed = candidate.trim()
27
+ if (EUDR_ERROR_KEY_PATTERN.test(trimmed)) return trimmed
28
+ }
29
+ return null
30
+ }
31
+
32
+ /**
33
+ * Resolves the user-facing message for a rejected EUDR mutation, preferring the
34
+ * server's `eudr.errors.*` token over the caller's generic copy so the reason
35
+ * survives to the user. Falls back to `fallbackMessage` when the rejection
36
+ * carries no known token (network errors, 5xx, unmapped tokens).
37
+ */
38
+ export function translateEudrErrorMessage(
39
+ err: unknown,
40
+ translate: Translator,
41
+ fallbackMessage: string,
42
+ ): string {
43
+ const token = readEudrErrorToken(err)
44
+ if (!token) return fallbackMessage
45
+ const translated = translate(token)
46
+ return translated === token ? fallbackMessage : translated
47
+ }
48
+
18
49
  export function translateEudrCrudError(err: unknown, translate: Translator): unknown {
19
50
  if (!(err instanceof Error)) return err
20
51
  const rawMessage = typeof err.message === 'string' ? err.message : ''
@@ -247,8 +247,16 @@ export function LookupSelectField<Snapshot extends Record<string, unknown>>({
247
247
  }
248
248
  }, [loadSelectedOption, selectedOption, selectedValue, translate])
249
249
 
250
+ // A snapshot is a denormalization captured when the user picks a record, so resolving the
251
+ // value an edit form was hydrated with must not re-emit it. Doing so overwrites the stored
252
+ // snapshot with a freshly fetched one, which makes an untouched CrudForm look dirty and
253
+ // raises the "You have unsaved changes" prompt on the way out.
254
+ const hydratedValueRef = React.useRef<string | null>(selectedValue)
255
+
250
256
  React.useEffect(() => {
251
257
  if (!onSnapshot || !selectedOption || selectedOption.unavailable) return
258
+ if (hydratedValueRef.current === selectedOption.value) return
259
+ hydratedValueRef.current = null
252
260
  onSnapshot(selectedOption.snapshot ?? null)
253
261
  }, [onSnapshot, selectedOption])
254
262
 
@@ -592,7 +600,7 @@ export function parseGeolocationInput(raw: string, translate: Translator): Recor
592
600
  return parsed
593
601
  }
594
602
 
595
- export { translateEudrCrudError } from './crudErrorI18n'
603
+ export { translateEudrCrudError, translateEudrErrorMessage } from './crudErrorI18n'
596
604
 
597
605
  let referencedStatementRowKeySeq = 0
598
606
 
@@ -239,7 +239,7 @@
239
239
  "eudr.lifecycle.amendWindowClosed": "Änderungsfenster geschlossen",
240
240
  "eudr.lifecycle.amendWindowRemaining": "Verbleibendes Änderungsfenster: {hours} Std. {minutes} Min.",
241
241
  "eudr.lifecycle.confirmArchive": "Erklärung archivieren?",
242
- "eudr.lifecycle.confirmArchiveBody": "Die Erklärung wird archiviert.",
242
+ "eudr.lifecycle.confirmArchiveBody": "Die Erklärung wird archiviert. Archivierte Erklärungen sind schreibgeschützt und können nicht bearbeitet, wiederhergestellt oder gelöscht werden.",
243
243
  "eudr.lifecycle.confirmMarkAvailable": "Erklärung als verfügbar markieren?",
244
244
  "eudr.lifecycle.confirmMarkAvailableBody": "Referenzdetails sind erforderlich, bevor die Erklärung verfügbar wird.",
245
245
  "eudr.lifecycle.confirmReturnToDraft": "Erklärung in Entwurf zurücksetzen?",
@@ -313,7 +313,8 @@
313
313
  "eudr.mitigation_actions.update": "EUDR-Minderungsmaßnahme aktualisieren",
314
314
  "eudr.nav.group": "Compliance",
315
315
  "eudr.nav.mappings": "Produktzuordnungen",
316
- "eudr.nav.overview": "Übersicht",
316
+ "eudr.nav.module": "EUDR",
317
+ "eudr.nav.overview": "EUDR-Übersicht",
317
318
  "eudr.nav.plots": "Parzellen",
318
319
  "eudr.nav.riskAssessments": "Risikobewertungen",
319
320
  "eudr.nav.statements": "Erklärungen",