@open-mercato/core 0.6.8-develop.7082.1.f2a845b020 → 0.6.8-develop.7090.1.4dbca7b350

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 (62) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/modules/customers/components/DealsKpiStrip.js +7 -6
  3. package/dist/modules/customers/components/DealsKpiStrip.js.map +2 -2
  4. package/dist/modules/customers/components/detail/CompanyKpiBar.js +5 -4
  5. package/dist/modules/customers/components/detail/CompanyKpiBar.js.map +2 -2
  6. package/dist/modules/customers/components/detail/DealsSection.js +6 -5
  7. package/dist/modules/customers/components/detail/DealsSection.js.map +2 -2
  8. package/dist/modules/customers/components/detail/utils.js +5 -5
  9. package/dist/modules/customers/components/detail/utils.js.map +2 -2
  10. package/dist/modules/sales/backend/sales/documents/[id]/page.js +10 -8
  11. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  12. package/dist/modules/sales/components/PriceWithCurrency.js +7 -5
  13. package/dist/modules/sales/components/PriceWithCurrency.js.map +2 -2
  14. package/dist/modules/sales/components/documents/AdjustmentsSection.js +8 -6
  15. package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
  16. package/dist/modules/sales/components/documents/ItemsSection.js +14 -7
  17. package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
  18. package/dist/modules/sales/components/documents/LineItemDialog.js +8 -7
  19. package/dist/modules/sales/components/documents/LineItemDialog.js.map +2 -2
  20. package/dist/modules/sales/components/documents/PaymentsSection.js +9 -8
  21. package/dist/modules/sales/components/documents/PaymentsSection.js.map +2 -2
  22. package/dist/modules/sales/components/documents/ReturnsSection.js +8 -6
  23. package/dist/modules/sales/components/documents/ReturnsSection.js.map +2 -2
  24. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js +5 -4
  25. package/dist/modules/sales/components/documents/SalesOrderDraftLines.js.map +2 -2
  26. package/dist/modules/sales/components/documents/ShipmentDialog.js +5 -4
  27. package/dist/modules/sales/components/documents/ShipmentDialog.js.map +2 -2
  28. package/dist/modules/sales/components/documents/ShipmentsSection.js +8 -6
  29. package/dist/modules/sales/components/documents/ShipmentsSection.js.map +2 -2
  30. package/dist/modules/sales/components/documents/lineItemUtils.js +2 -2
  31. package/dist/modules/sales/components/documents/lineItemUtils.js.map +2 -2
  32. package/dist/modules/sales/data/entities.js +8 -2
  33. package/dist/modules/sales/data/entities.js.map +2 -2
  34. package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js +32 -0
  35. package/dist/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.js.map +7 -0
  36. package/dist/modules/staff/backend/staff/timesheets/page.js +29 -18
  37. package/dist/modules/staff/backend/staff/timesheets/page.js.map +2 -2
  38. package/package.json +7 -7
  39. package/src/modules/auth/i18n/de.json +3 -0
  40. package/src/modules/auth/i18n/en.json +3 -0
  41. package/src/modules/auth/i18n/es.json +3 -0
  42. package/src/modules/auth/i18n/ko.json +3 -0
  43. package/src/modules/auth/i18n/pl.json +3 -0
  44. package/src/modules/customers/components/DealsKpiStrip.tsx +7 -7
  45. package/src/modules/customers/components/detail/CompanyKpiBar.tsx +5 -4
  46. package/src/modules/customers/components/detail/DealsSection.tsx +11 -5
  47. package/src/modules/customers/components/detail/utils.ts +15 -5
  48. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +14 -8
  49. package/src/modules/sales/components/PriceWithCurrency.tsx +17 -5
  50. package/src/modules/sales/components/documents/AdjustmentsSection.tsx +6 -5
  51. package/src/modules/sales/components/documents/ItemsSection.tsx +8 -1
  52. package/src/modules/sales/components/documents/LineItemDialog.tsx +9 -8
  53. package/src/modules/sales/components/documents/PaymentsSection.tsx +9 -8
  54. package/src/modules/sales/components/documents/ReturnsSection.tsx +6 -5
  55. package/src/modules/sales/components/documents/SalesOrderDraftLines.tsx +5 -4
  56. package/src/modules/sales/components/documents/ShipmentDialog.tsx +5 -4
  57. package/src/modules/sales/components/documents/ShipmentsSection.tsx +6 -5
  58. package/src/modules/sales/components/documents/lineItemUtils.ts +7 -2
  59. package/src/modules/sales/data/entities.ts +6 -0
  60. package/src/modules/sales/migrations/.snapshot-open-mercato.json +60 -0
  61. package/src/modules/sales/migrations/Migration20260821120000_sales_notes_addresses_context_idx.ts +87 -0
  62. package/src/modules/staff/backend/staff/timesheets/page.tsx +12 -1
@@ -2,7 +2,7 @@
2
2
 
3
3
  import * as React from 'react'
4
4
  import { EyeOff } from 'lucide-react'
5
- import { useT } from '@open-mercato/shared/lib/i18n/context'
5
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
6
6
  import { KpiCard, type KpiTrend } from '@open-mercato/ui/backend/charts/KpiCard'
7
7
  import { Button } from '@open-mercato/ui/primitives/button'
8
8
  import { IconButton } from '@open-mercato/ui/primitives/icon-button'
@@ -67,6 +67,7 @@ type CompanyKpiBarProps = {
67
67
 
68
68
  export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
69
69
  const t = useT()
70
+ const locale = useLocale()
70
71
 
71
72
  const activeDeals = React.useMemo(() => getActiveDeals(data.deals), [data.deals])
72
73
  const activeDealsValue = React.useMemo(
@@ -126,7 +127,7 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
126
127
  title: t('customers.companies.dashboard.kpi.activeDeals', 'ACTIVE DEALS'),
127
128
  value: activeDealsValue,
128
129
  trend: dealTrend,
129
- formatValue: (v: number) => formatCurrency(v, dealCurrency),
130
+ formatValue: (v: number) => formatCurrency(v, dealCurrency, locale),
130
131
  comparisonLabel: `${data.kpis?.activeDealsCount ?? activeDeals.length} ${(data.kpis?.activeDealsCount ?? activeDeals.length) === 1 ? 'pipeline' : 'pipelines'}`,
131
132
  },
132
133
  {
@@ -140,7 +141,7 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
140
141
  id: 'ltv',
141
142
  title: t('customers.companies.dashboard.kpi.ltv', 'CUSTOMER VALUE (LTV)'),
142
143
  value: ltvValue,
143
- formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency) : undefined,
144
+ formatValue: ltvValue !== null ? (v: number) => formatCurrency(v, dealCurrency, locale) : undefined,
144
145
  comparisonLabel: ltvValue !== null
145
146
  ? t('customers.companies.dashboard.kpi.wonDeals', 'won deals total')
146
147
  : t('customers.companies.dashboard.kpi.noWonDeals', 'No won deals'),
@@ -158,7 +159,7 @@ export function CompanyKpiBar({ data }: CompanyKpiBarProps) {
158
159
  ? `${data.kpis?.completedDealsCount ?? data.deals.filter((d) => isWonDealStatus(d.status)).length} ${t('customers.companies.dashboard.kpi.completedDeals', 'completed deals')}`
159
160
  : t('customers.companies.dashboard.kpi.noInteractions', 'No interactions yet'),
160
161
  },
161
- ], [t, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])
162
+ ], [t, locale, activeDealsValue, dealTrend, dealCurrency, activeDeals.length, activityTrend, ltvValue, clientTenureYears, data.deals, data.interactions.length, data.kpis])
162
163
 
163
164
  const visibleTiles = kpiTiles.filter((tile) => !hiddenTiles.has(tile.id))
164
165
 
@@ -12,7 +12,7 @@ import { LinkEntityDialog } from '../linking/LinkEntityDialog'
12
12
  import { createDealLinkAdapter } from '../linking/adapters/dealAdapter'
13
13
  import { LoadingMessage, TabEmptyState } from '@open-mercato/ui/backend/detail'
14
14
  import { useOrganizationScopeVersion } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
- import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
16
16
  import { hasMoreFromPage } from '@open-mercato/shared/lib/pagination/load-more'
17
17
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
18
18
  import { E } from '#generated/entities.ids.generated'
@@ -275,9 +275,14 @@ function normalizeDeal(deal: Partial<DealSummary> & { id: string; title?: string
275
275
  }
276
276
  }
277
277
 
278
- function formatValueLabel(amount: number | null, currency: string | null, emptyLabel: string): string {
278
+ function formatValueLabel(
279
+ amount: number | null,
280
+ currency: string | null,
281
+ emptyLabel: string,
282
+ locale?: string,
283
+ ): string {
279
284
  if (typeof amount === 'number') {
280
- const formatter = new Intl.NumberFormat(undefined, {
285
+ const formatter = new Intl.NumberFormat(locale, {
281
286
  style: currency ? 'currency' : 'decimal',
282
287
  currency: currency ?? undefined,
283
288
  maximumFractionDigits: 2,
@@ -317,6 +322,7 @@ export function DealsSection({
317
322
  runGuardedMutation,
318
323
  }: DealsSectionProps) {
319
324
  const tHook = useT()
325
+ const locale = useLocale()
320
326
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
321
327
  const fallbackTranslator = React.useMemo<Translator>(() => createTranslatorWithFallback(tHook), [tHook])
322
328
  const t: Translator = React.useMemo(() => translator ?? fallbackTranslator, [translator, fallbackTranslator])
@@ -963,8 +969,8 @@ export function DealsSection({
963
969
  ) : null}
964
970
  <div className="space-y-4">
965
971
  {sortedDeals.map((deal) => {
966
- const valueLabel = formatValueLabel(deal.valueAmount, deal.valueCurrency ?? null, emptyLabel)
967
- const expectedLabel = deal.expectedCloseAt ? formatDate(deal.expectedCloseAt) ?? emptyLabel : emptyLabel
972
+ const valueLabel = formatValueLabel(deal.valueAmount, deal.valueCurrency ?? null, emptyLabel, locale)
973
+ const expectedLabel = deal.expectedCloseAt ? formatDate(deal.expectedCloseAt, locale) ?? emptyLabel : emptyLabel
968
974
  const probabilityLabel =
969
975
  typeof deal.probability === 'number' ? `${deal.probability}%` : emptyLabel
970
976
  const isUnlinkPending = pendingAction?.kind === 'remove' && pendingAction.id === deal.id
@@ -5,11 +5,16 @@ import type { CustomerDictionaryKind } from '../../lib/dictionaries'
5
5
  import { CUSTOMER_INTERACTION_TASK_SOURCE, CUSTOMER_INTERACTION_TASK_TYPE } from '../../lib/interactionCompatibility'
6
6
 
7
7
 
8
- export function formatDate(value?: string | null): string | null {
8
+ /**
9
+ * Pass `locale` to format in the application locale (`useLocale()` in client components).
10
+ * Omitting it keeps the runtime default, which varies per machine and is therefore not
11
+ * assertable in tests.
12
+ */
13
+ export function formatDate(value?: string | null, locale?: string): string | null {
9
14
  if (!value) return null
10
15
  const date = new Date(value)
11
16
  if (Number.isNaN(date.getTime())) return null
12
- return date.toLocaleDateString()
17
+ return date.toLocaleDateString(locale)
13
18
  }
14
19
 
15
20
  export function formatTemplate(template: string, params?: Record<string, string | number>): string {
@@ -148,15 +153,20 @@ export function createDictionarySelectLabels(
148
153
  }
149
154
  }
150
155
 
151
- export function formatCurrency(amount: number, currency?: string | null): string {
156
+ /**
157
+ * Pass `locale` to format in the application locale (`useLocale()` in client components).
158
+ * Omitting it keeps the runtime default, which varies per machine and is therefore not
159
+ * assertable in tests.
160
+ */
161
+ export function formatCurrency(amount: number, currency?: string | null, locale?: string): string {
152
162
  try {
153
- return new Intl.NumberFormat(undefined, {
163
+ return new Intl.NumberFormat(locale, {
154
164
  style: 'currency',
155
165
  currency: currency || 'PLN',
156
166
  maximumFractionDigits: 0,
157
167
  }).format(amount)
158
168
  } catch {
159
- return `${amount.toLocaleString()} ${currency || 'PLN'}`
169
+ return `${amount.toLocaleString(locale)} ${currency || 'PLN'}`
160
170
  }
161
171
  }
162
172
 
@@ -38,7 +38,7 @@ import { buildOptimisticLockHeader } from '@open-mercato/ui/backend/utils/optimi
38
38
  import { surfaceRecordConflict } from '@open-mercato/ui/backend/conflicts'
39
39
  import { collectCustomFieldValues } from '@open-mercato/ui/backend/utils/customFieldValues'
40
40
  import { mapCrudServerErrorToFormErrors } from '@open-mercato/ui/backend/utils/serverErrors'
41
- import { useT } from '@open-mercato/shared/lib/i18n/context'
41
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
42
42
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
43
43
  import { cn } from '@open-mercato/shared/lib/utils'
44
44
  import { ContactEmailDisplay } from '@open-mercato/core/modules/sales/components/ContactEmailDisplay'
@@ -79,13 +79,17 @@ import { createLogger } from '@open-mercato/shared/lib/logger'
79
79
 
80
80
  const logger = createLogger('sales')
81
81
 
82
- function formatMessageAmount(amount: number | null | undefined, currency: string | null | undefined): string | null {
82
+ function formatMessageAmount(
83
+ amount: number | null | undefined,
84
+ currency: string | null | undefined,
85
+ locale?: string
86
+ ): string | null {
83
87
  if (typeof amount !== 'number' || !Number.isFinite(amount)) return null
84
- if (!currency) return amount.toLocaleString()
88
+ if (!currency) return amount.toLocaleString(locale)
85
89
  try {
86
- return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(amount)
90
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(amount)
87
91
  } catch {
88
- return `${amount.toLocaleString()} ${currency}`
92
+ return `${amount.toLocaleString(locale)} ${currency}`
89
93
  }
90
94
  }
91
95
 
@@ -1900,6 +1904,7 @@ export default function SalesDocumentDetailPage({
1900
1904
  includeAmountInMessageMetadata?: boolean
1901
1905
  }) {
1902
1906
  const t = useT()
1907
+ const locale = useLocale()
1903
1908
  const { enabled: channelsEnabled } = useSalesChannelsEnabled()
1904
1909
  const router = useRouter()
1905
1910
  const searchParams = useSearchParams()
@@ -2807,7 +2812,7 @@ export default function SalesDocumentDetailPage({
2807
2812
  : null
2808
2813
  const contactEmail = resolveCustomerEmail(customerSnapshot) ?? metadataEmail ?? record?.contactEmail ?? null
2809
2814
  const statusDisplay = record?.status ? statusDictionaryMap[record.status] ?? null : null
2810
- const previewAmount = formatMessageAmount(record?.grandTotalGrossAmount ?? null, record?.currencyCode ?? null)
2815
+ const previewAmount = formatMessageAmount(record?.grandTotalGrossAmount ?? null, record?.currencyCode ?? null, locale)
2811
2816
  const messagePreviewMetadata: Record<string, string> = {}
2812
2817
  if (includeAmountInMessageMetadata && previewAmount) {
2813
2818
  messagePreviewMetadata[t('sales.documents.detail.totals.grandTotalGross')] = previewAmount
@@ -3867,7 +3872,7 @@ export default function SalesDocumentDetailPage({
3867
3872
  renderDisplay: (params) => {
3868
3873
  const { value, emptyLabel } = params
3869
3874
  if (value && value.length) {
3870
- return <span className="text-sm text-muted-foreground">{new Date(value).toLocaleDateString()}</span>
3875
+ return <span className="text-sm text-muted-foreground">{new Date(value).toLocaleDateString(locale)}</span>
3871
3876
  }
3872
3877
  return <span className="text-sm text-muted-foreground">{emptyLabel}</span>
3873
3878
  },
@@ -3944,6 +3949,7 @@ export default function SalesDocumentDetailPage({
3944
3949
  shippingMethodLoading,
3945
3950
  shippingMethodOptions,
3946
3951
  t,
3952
+ locale,
3947
3953
  kind,
3948
3954
  saveShortcutLabel,
3949
3955
  ])
@@ -4717,7 +4723,7 @@ export default function SalesDocumentDetailPage({
4717
4723
  renderDisplay={({ value, emptyLabel }) =>
4718
4724
  value && value.length ? (
4719
4725
  <span className="text-sm text-muted-foreground">
4720
- {new Date(value).toLocaleDateString()}
4726
+ {new Date(value).toLocaleDateString(locale)}
4721
4727
  </span>
4722
4728
  ) : (
4723
4729
  <span className="text-sm text-muted-foreground">{emptyLabel}</span>
@@ -1,19 +1,26 @@
1
1
  "use client"
2
2
 
3
3
  import * as React from 'react'
4
+ import { useOptionalLocale } from '@open-mercato/shared/lib/i18n/context'
4
5
  import { cn } from '@open-mercato/shared/lib/utils'
5
6
 
7
+ /**
8
+ * Pass `locale` to format in the application locale (`useLocale()` in client components).
9
+ * Omitting it keeps the runtime default, which varies per machine and is therefore not
10
+ * assertable in tests.
11
+ */
6
12
  export function formatPriceWithCurrency(
7
13
  amount: number | string | null | undefined,
8
14
  currency: string | null | undefined,
9
- fallback = '—'
15
+ fallback = '—',
16
+ locale?: string
10
17
  ): string {
11
18
  if (amount === null || amount === undefined) return fallback
12
19
  const parsed = typeof amount === 'string' ? Number(amount) : amount
13
20
  if (Number.isNaN(parsed)) return fallback
14
21
  if (currency) {
15
22
  try {
16
- return new Intl.NumberFormat(undefined, {
23
+ return new Intl.NumberFormat(locale, {
17
24
  style: 'currency',
18
25
  currency,
19
26
  minimumFractionDigits: 2,
@@ -23,7 +30,7 @@ export function formatPriceWithCurrency(
23
30
  // fall through to plain number formatting
24
31
  }
25
32
  }
26
- return parsed.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
33
+ return parsed.toLocaleString(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 })
27
34
  }
28
35
 
29
36
  type PriceWithCurrencyProps = {
@@ -34,9 +41,14 @@ type PriceWithCurrencyProps = {
34
41
  }
35
42
 
36
43
  export function PriceWithCurrency({ amount, currency, fallback = '—', className }: PriceWithCurrencyProps) {
44
+ // `useOptionalLocale` rather than `useLocale`: this component is deep-importable and had no i18n
45
+ // dependency at all before it started formatting in the app locale, so making it throw outside
46
+ // `I18nProvider` would narrow its mounting contract. Without a provider it falls back to the
47
+ // runtime default, which is exactly the behaviour it had before.
48
+ const locale = useOptionalLocale()
37
49
  const label = React.useMemo(
38
- () => formatPriceWithCurrency(amount, currency, fallback),
39
- [amount, currency, fallback]
50
+ () => formatPriceWithCurrency(amount, currency, fallback, locale),
51
+ [amount, currency, fallback, locale]
40
52
  )
41
53
  return <span className={cn('font-mono text-sm text-foreground', className)}>{label}</span>
42
54
  }
@@ -23,7 +23,7 @@ import type { SectionAction } from '@open-mercato/core/modules/customers/compone
23
23
  import type { SalesAdjustmentKind } from '../../data/entities'
24
24
  import { PriceWithCurrency } from '../PriceWithCurrency'
25
25
  import { AdjustmentDialog, type AdjustmentRowData, type AdjustmentSubmitPayload } from './AdjustmentDialog'
26
- import { useT } from '@open-mercato/shared/lib/i18n/context'
26
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
27
27
  import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
28
28
  import { extractCustomFieldValues } from './customFieldHelpers'
29
29
  import { createLogger } from '@open-mercato/shared/lib/logger'
@@ -60,9 +60,9 @@ function normalizeNumber(value: unknown, fallback = 0): number {
60
60
  return fallback
61
61
  }
62
62
 
63
- function formatPercent(value: number | null | undefined): string {
63
+ export function formatPercent(value: number | null | undefined, locale?: string): string {
64
64
  if (value === null || value === undefined) return '—'
65
- const formatter = new Intl.NumberFormat(undefined, { style: 'percent', maximumFractionDigits: 2 })
65
+ const formatter = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 2 })
66
66
  return formatter.format(value / 100)
67
67
  }
68
68
 
@@ -77,6 +77,7 @@ export function SalesDocumentAdjustmentsSection({
77
77
  onRowsChange,
78
78
  }: SalesDocumentAdjustmentsSectionProps) {
79
79
  const t = useT()
80
+ const locale = useLocale()
80
81
  const { organizationId, tenantId } = useOrganizationScopeDetail()
81
82
  const resolvedOrganizationId = orgFromProps ?? organizationId ?? null
82
83
  const resolvedTenantId = tenantFromProps ?? tenantId ?? null
@@ -433,7 +434,7 @@ export function SalesDocumentAdjustmentsSection({
433
434
  {
434
435
  id: 'rate',
435
436
  header: t('sales.documents.adjustments.rate', 'Rate'),
436
- cell: ({ row }) => <span className="font-mono text-sm text-muted-foreground">{formatPercent(row.original.rate)}</span>,
437
+ cell: ({ row }) => <span className="font-mono text-sm text-muted-foreground">{formatPercent(row.original.rate, locale)}</span>,
437
438
  },
438
439
  {
439
440
  id: 'position',
@@ -441,7 +442,7 @@ export function SalesDocumentAdjustmentsSection({
441
442
  cell: ({ row }) => <span className="text-sm text-muted-foreground">{row.original.position}</span>,
442
443
  },
443
444
  ],
444
- [currencyCode, resolveKindLabel, t]
445
+ [currencyCode, locale, resolveKindLabel, t]
445
446
  )
446
447
 
447
448
  const showLoadingState = loading && rows.length === 0
@@ -16,7 +16,7 @@ import {
16
16
  createDictionaryMap,
17
17
  normalizeDictionaryEntries,
18
18
  } from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
19
- import { useT } from "@open-mercato/shared/lib/i18n/context";
19
+ import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
20
20
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
21
21
  import { useConfirmDialog } from "@open-mercato/ui/backend/confirm-dialog";
22
22
  import { emitSalesDocumentTotalsRefresh } from "@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents";
@@ -158,6 +158,7 @@ export function SalesDocumentItemsSection({
158
158
  onItemsChange,
159
159
  }: SalesDocumentItemsSectionProps) {
160
160
  const t = useT();
161
+ const locale = useLocale();
161
162
  const { organizationId, tenantId } = useOrganizationScopeDetail();
162
163
  const { confirm, ConfirmDialogElement } = useConfirmDialog();
163
164
  const resolvedOrganizationId = orgFromProps ?? organizationId ?? null;
@@ -827,6 +828,7 @@ export function SalesDocumentItemsSection({
827
828
  {formatMoney(
828
829
  item.unitPriceGross,
829
830
  item.currencyCode ?? currencyCode ?? undefined,
831
+ locale,
830
832
  )}{" "}
831
833
  <span className="text-xs text-muted-foreground">
832
834
  {t("sales.documents.items.table.gross", "gross")}
@@ -836,6 +838,7 @@ export function SalesDocumentItemsSection({
836
838
  {formatMoney(
837
839
  item.unitPriceNet,
838
840
  item.currencyCode ?? currencyCode ?? undefined,
841
+ locale,
839
842
  )}{" "}
840
843
  {t("sales.documents.items.table.net", "net")}
841
844
  </span>
@@ -850,6 +853,7 @@ export function SalesDocumentItemsSection({
850
853
  item.currencyCode ??
851
854
  currencyCode ??
852
855
  undefined,
856
+ locale,
853
857
  ),
854
858
  unit: unitPriceReference.referenceUnitCode,
855
859
  },
@@ -873,6 +877,7 @@ export function SalesDocumentItemsSection({
873
877
  item.currencyCode ??
874
878
  currencyCode ??
875
879
  undefined,
880
+ locale,
876
881
  ),
877
882
  },
878
883
  )}
@@ -903,6 +908,7 @@ export function SalesDocumentItemsSection({
903
908
  {formatMoney(
904
909
  item.totalGross,
905
910
  item.currencyCode ?? currencyCode ?? undefined,
911
+ locale,
906
912
  )}{" "}
907
913
  <span className="text-xs font-normal text-muted-foreground">
908
914
  {t("sales.documents.items.table.gross", "gross")}
@@ -912,6 +918,7 @@ export function SalesDocumentItemsSection({
912
918
  {formatMoney(
913
919
  item.totalNet,
914
920
  item.currencyCode ?? currencyCode ?? undefined,
921
+ locale,
915
922
  )}{" "}
916
923
  {t("sales.documents.items.table.net", "net")}
917
924
  </span>
@@ -46,7 +46,7 @@ import {
46
46
  renderDictionaryColor,
47
47
  } from "@open-mercato/core/modules/dictionaries/components/dictionaryAppearance";
48
48
  import { E } from "#generated/entities.ids.generated";
49
- import { useT } from "@open-mercato/shared/lib/i18n/context";
49
+ import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
50
50
  import { useOrganizationScopeDetail } from "@open-mercato/shared/lib/frontend/useOrganizationScope";
51
51
  import { formatMoney, normalizeNumber } from "./lineItemUtils";
52
52
  import type { SalesLineRecord } from "./lineItemTypes";
@@ -489,6 +489,7 @@ export function LineItemDialog({
489
489
  onDraftSaved,
490
490
  }: SalesLineDialogProps) {
491
491
  const t = useT();
492
+ const locale = useLocale();
492
493
  const scope = useOrganizationScopeDetail();
493
494
  const resolvedOrganizationId = organizationId ?? scope.organizationId ?? null;
494
495
  const resolvedTenantId = tenantId ?? scope.tenantId ?? null;
@@ -1044,10 +1045,10 @@ export function LineItemDialog({
1044
1045
  displayMode === "including-tax" &&
1045
1046
  amountGross !== null &&
1046
1047
  currency
1047
- ? formatMoney(amountGross, currency)
1048
+ ? formatMoney(amountGross, currency, locale)
1048
1049
  : null,
1049
1050
  displayMode === "excluding-tax" && amountNet !== null && currency
1050
- ? formatMoney(amountNet, currency)
1051
+ ? formatMoney(amountNet, currency, locale)
1051
1052
  : null,
1052
1053
  displayMode
1053
1054
  ? displayMode === "including-tax"
@@ -1063,9 +1064,9 @@ export function LineItemDialog({
1063
1064
  labelParts.length > 0
1064
1065
  ? labelParts.join(" • ")
1065
1066
  : amountGross !== null && currency
1066
- ? formatMoney(amountGross, currency)
1067
+ ? formatMoney(amountGross, currency, locale)
1067
1068
  : amountNet !== null && currency
1068
- ? formatMoney(amountNet, currency)
1069
+ ? formatMoney(amountNet, currency, locale)
1069
1070
  : id;
1070
1071
  return {
1071
1072
  id,
@@ -2059,7 +2060,7 @@ export function LineItemDialog({
2059
2060
  ? t("sales.documents.items.priceNet", "Net")
2060
2061
  : t("sales.documents.items.priceGross", "Gross");
2061
2062
  const lockedAmountLabel = Number.isFinite(lockedAmount)
2062
- ? `${formatMoney(lockedAmount, lockedCurrency)} — ${lockedModeLabel}`
2063
+ ? `${formatMoney(lockedAmount, lockedCurrency, locale)} — ${lockedModeLabel}`
2063
2064
  : lockedModeLabel;
2064
2065
  const lockedPriceDetail =
2065
2066
  selectedPrice?.priceKindTitle ??
@@ -2260,11 +2261,11 @@ export function LineItemDialog({
2260
2261
  "sales.documents.items.priceBasisTemplate",
2261
2262
  "Catalog price basis: {{baseAmount}} / {{baseUnit}}. Converted for {{unit}}: {{baseAmount}} × {{factor}} = {{convertedAmount}}.",
2262
2263
  {
2263
- baseAmount: formatMoney(selectedBaseAmount as number, selectedCurrency),
2264
+ baseAmount: formatMoney(selectedBaseAmount as number, selectedCurrency, locale),
2264
2265
  baseUnit: baseUnitCode,
2265
2266
  unit: quantityUnitCode,
2266
2267
  factor: unitFactor,
2267
- convertedAmount: formatMoney(convertedAmount, selectedCurrency),
2268
+ convertedAmount: formatMoney(convertedAmount, selectedCurrency, locale),
2268
2269
  },
2269
2270
  )}
2270
2271
  </p>
@@ -13,7 +13,7 @@ import { RowActions } from '@open-mercato/ui/backend/RowActions'
13
13
  import { Button } from '@open-mercato/ui/primitives/button'
14
14
  import { flash } from '@open-mercato/ui/backend/FlashMessages'
15
15
  import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
16
- import { useT } from '@open-mercato/shared/lib/i18n/context'
16
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
17
17
  import { emitSalesDocumentTotalsRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
18
18
  import { extensionPoints } from '@open-mercato/core/modules/sales/extension-points'
19
19
  import { PaymentDialog, type PaymentFormData, type PaymentTotals } from './PaymentDialog'
@@ -60,10 +60,10 @@ function normalizeNumber(value: unknown): number {
60
60
  return 0
61
61
  }
62
62
 
63
- function formatMoney(value: number, currency: string | null | undefined): string {
63
+ function formatMoney(value: number, currency: string | null | undefined, locale?: string): string {
64
64
  if (!currency || currency.trim().length !== 3) return value.toFixed(2)
65
65
  try {
66
- return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format(value)
66
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value)
67
67
  } catch {
68
68
  return `${currency.toUpperCase()} ${value.toFixed(2)}`
69
69
  }
@@ -80,6 +80,7 @@ export function SalesDocumentPaymentsSection({
80
80
  onPaymentsChange,
81
81
  }: SalesDocumentPaymentsSectionProps) {
82
82
  const t = useT()
83
+ const locale = useLocale()
83
84
  const { organizationId, tenantId } = useOrganizationScopeDetail()
84
85
  const resolvedOrganizationId = orgFromProps ?? organizationId ?? null
85
86
  const resolvedTenantId = tenantFromProps ?? tenantId ?? null
@@ -287,24 +288,24 @@ export function SalesDocumentPaymentsSection({
287
288
  {
288
289
  accessorKey: 'amount',
289
290
  header: t('sales.documents.payments.amount', 'Amount'),
290
- cell: ({ row }) => formatMoney(row.original.amount, row.original.currencyCode ?? currencyCode),
291
+ cell: ({ row }) => formatMoney(row.original.amount, row.original.currencyCode ?? currencyCode, locale),
291
292
  },
292
293
  {
293
294
  accessorKey: 'receivedAt',
294
295
  header: t('sales.documents.payments.receivedAt', 'Received'),
295
296
  cell: ({ row }) =>
296
297
  row.original.receivedAt
297
- ? new Date(row.original.receivedAt).toLocaleDateString()
298
+ ? new Date(row.original.receivedAt).toLocaleDateString(locale)
298
299
  : '—',
299
300
  },
300
301
  {
301
302
  accessorKey: 'createdAt',
302
303
  header: t('sales.documents.payments.createdAt', 'Created'),
303
304
  cell: ({ row }) =>
304
- row.original.createdAt ? new Date(row.original.createdAt).toLocaleString() : '—',
305
+ row.original.createdAt ? new Date(row.original.createdAt).toLocaleString(locale) : '—',
305
306
  meta: {
306
307
  tooltipContent: (row: PaymentRow) =>
307
- row.createdAt ? new Date(row.createdAt).toLocaleString() : undefined,
308
+ row.createdAt ? new Date(row.createdAt).toLocaleString(locale) : undefined,
308
309
  },
309
310
  },
310
311
  {
@@ -327,7 +328,7 @@ export function SalesDocumentPaymentsSection({
327
328
  },
328
329
  },
329
330
  ],
330
- [currencyCode, deleteActionLabel, editActionLabel, handleDelete, openEditPayment, t]
331
+ [currencyCode, deleteActionLabel, editActionLabel, handleDelete, locale, openEditPayment, t]
331
332
  )
332
333
 
333
334
  if (loading) {
@@ -12,7 +12,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
12
12
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
13
13
  import { useConfirmDialog } from '@open-mercato/ui/backend/confirm-dialog'
14
14
  import { useOrganizationScopeDetail } from '@open-mercato/shared/lib/frontend/useOrganizationScope'
15
- import { useT } from '@open-mercato/shared/lib/i18n/context'
15
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
16
16
  import {
17
17
  emitSalesDocumentTotalsRefresh,
18
18
  subscribeSalesDocumentTotalsRefresh,
@@ -41,15 +41,16 @@ type SalesReturnsSectionProps = {
41
41
  documentUpdatedAt?: string | null
42
42
  }
43
43
 
44
- function formatDisplayDate(value: string | null | undefined): string | null {
44
+ export function formatDisplayDate(value: string | null | undefined, locale?: string): string | null {
45
45
  if (!value) return null
46
46
  const date = new Date(value)
47
47
  if (Number.isNaN(date.getTime())) return null
48
- return new Intl.DateTimeFormat(undefined, { dateStyle: 'medium' }).format(date)
48
+ return new Intl.DateTimeFormat(locale, { dateStyle: 'medium' }).format(date)
49
49
  }
50
50
 
51
51
  export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }: SalesReturnsSectionProps) {
52
52
  const t = useT()
53
+ const locale = useLocale()
53
54
  const { organizationId, tenantId } = useOrganizationScopeDetail()
54
55
  const { confirm, ConfirmDialogElement } = useConfirmDialog()
55
56
  const [returns, setReturns] = React.useState<ReturnRow[]>([])
@@ -328,10 +329,10 @@ export function SalesReturnsSection({ orderId, currencyCode, documentUpdatedAt }
328
329
  </div>
329
330
  </div>
330
331
  <div className="whitespace-nowrap text-right text-sm text-muted-foreground">
331
- {formatDisplayDate(ret.returnedAt) ?? t('sales.returns.notSet', 'Not set')}
332
+ {formatDisplayDate(ret.returnedAt, locale) ?? t('sales.returns.notSet', 'Not set')}
332
333
  </div>
333
334
  <div className="whitespace-nowrap text-right text-sm font-medium">
334
- {formatMoney(ret.total, currencyCode ?? null)}
335
+ {formatMoney(ret.total, currencyCode ?? null, locale)}
335
336
  </div>
336
337
  <div className="flex justify-end">
337
338
  <RowActions
@@ -7,7 +7,7 @@ import { DataTable } from '@open-mercato/ui/backend/DataTable'
7
7
  import { RowActions } from '@open-mercato/ui/backend/RowActions'
8
8
  import { EmptyState } from '@open-mercato/ui/primitives/empty-state'
9
9
  import { Button } from '@open-mercato/ui/primitives/button'
10
- import { useT } from '@open-mercato/shared/lib/i18n/context'
10
+ import { useLocale, useT } from '@open-mercato/shared/lib/i18n/context'
11
11
  import { LineItemDialog } from './LineItemDialog'
12
12
  import type { SalesLineRecord } from './lineItemTypes'
13
13
  import { formatMoney, normalizeNumber } from './lineItemUtils'
@@ -93,6 +93,7 @@ export function SalesOrderDraftLines({
93
93
  onChange,
94
94
  }: SalesOrderDraftLinesProps) {
95
95
  const t = useT()
96
+ const locale = useLocale()
96
97
  const [dialogOpen, setDialogOpen] = React.useState(false)
97
98
  const [editing, setEditing] = React.useState<SalesOrderLineDraft | null>(null)
98
99
 
@@ -112,14 +113,14 @@ export function SalesOrderDraftLines({
112
113
  {
113
114
  id: 'unitPrice',
114
115
  header: t('sales.documents.items.table.unit', 'Unit price'),
115
- cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined),
116
+ cell: ({ row }) => formatMoney(row.original.record.unitPriceGross, row.original.record.currencyCode ?? currencyCode ?? undefined, locale),
116
117
  },
117
118
  {
118
119
  id: 'total',
119
120
  header: t('sales.documents.items.table.total', 'Total'),
120
- cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined),
121
+ cell: ({ row }) => formatMoney(row.original.record.totalGross, row.original.record.currencyCode ?? currencyCode ?? undefined, locale),
121
122
  },
122
- ], [currencyCode, t])
123
+ ], [currencyCode, locale, t])
123
124
 
124
125
  const openCreate = React.useCallback(() => {
125
126
  setEditing(null)
@@ -20,7 +20,7 @@ import { flash } from '@open-mercato/ui/backend/FlashMessages'
20
20
  import { cn } from '@open-mercato/shared/lib/utils'
21
21
  import { E } from '#generated/entities.ids.generated'
22
22
  import { emitSalesDocumentTotalsRefresh } from '@open-mercato/core/modules/sales/lib/frontend/documentTotalsEvents'
23
- import { useT } from '@open-mercato/shared/lib/i18n/context'
23
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
24
24
  import { formatMoney, normalizeNumber } from './lineItemUtils'
25
25
  import type { OrderLine, ShipmentRow } from './shipmentTypes'
26
26
  import { formatAddressString, type AddressFormatStrategy, type AddressValue } from '@open-mercato/core/modules/customers/utils/addressFormat'
@@ -290,6 +290,7 @@ export function ShipmentDialog({
290
290
  onAddComment,
291
291
  }: ShipmentDialogProps) {
292
292
  const t = useT()
293
+ const locale = useLocale()
293
294
  const [formResetKey, setFormResetKey] = React.useState(0)
294
295
  const [shippingMethods, setShippingMethods] = React.useState<ShippingMethodOption[]>([])
295
296
  const [shippingMethodLoading, setShippingMethodLoading] = React.useState(false)
@@ -540,21 +541,21 @@ export function ShipmentDialog({
540
541
  if (option.avgPrice !== null) {
541
542
  parts.push(
542
543
  t('sales.documents.shipments.shippingMethodAvg', 'Avg {{price}}', {
543
- price: formatMoney(option.avgPrice, currency ?? null),
544
+ price: formatMoney(option.avgPrice, currency ?? null, locale),
544
545
  }),
545
546
  )
546
547
  }
547
548
  if (option.minPrice !== null) {
548
549
  parts.push(
549
550
  t('sales.documents.shipments.shippingMethodMin', 'Min {{price}}', {
550
- price: formatMoney(option.minPrice, currency ?? null),
551
+ price: formatMoney(option.minPrice, currency ?? null, locale),
551
552
  }),
552
553
  )
553
554
  }
554
555
  if (parts.length === 0) return undefined
555
556
  return parts.join(' · ')
556
557
  },
557
- [currencyCode, t],
558
+ [currencyCode, locale, t],
558
559
  )
559
560
 
560
561
  const mapShippingMethod = React.useCallback(