@open-mercato/core 0.7.1-develop.7200.1.8ba2d93d3f → 0.7.1-develop.7202.1.a52dc6707c
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/modules/catalog/components/products/ProductUomSection.js +27 -22
- package/dist/modules/catalog/components/products/ProductUomSection.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesCard.js +1 -14
- package/dist/modules/customers/components/detail/ActivitiesCard.js.map +2 -2
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js +1 -14
- package/dist/modules/customers/components/detail/ActivitiesDayStrip.js.map +2 -2
- package/dist/modules/customers/components/detail/MiniWeekCalendar.js +1 -3
- package/dist/modules/customers/components/detail/MiniWeekCalendar.js.map +2 -2
- package/dist/modules/customers/components/detail/PlannedActivitiesSection.js +2 -1
- package/dist/modules/customers/components/detail/PlannedActivitiesSection.js.map +2 -2
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js +2 -1
- package/dist/modules/customers/components/detail/schedule/DateTimeFields.js.map +2 -2
- package/dist/modules/customers/lib/localDay.js +20 -0
- package/dist/modules/customers/lib/localDay.js.map +7 -0
- package/dist/modules/sales/components/documents/PaymentDialog.js +14 -11
- package/dist/modules/sales/components/documents/PaymentDialog.js.map +2 -2
- package/dist/modules/wms/components/backend/AdjustInventoryDialog.js +8 -6
- package/dist/modules/wms/components/backend/AdjustInventoryDialog.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/catalog/components/products/ProductUomSection.tsx +31 -21
- package/src/modules/customers/components/detail/ActivitiesCard.tsx +1 -19
- package/src/modules/customers/components/detail/ActivitiesDayStrip.tsx +1 -22
- package/src/modules/customers/components/detail/MiniWeekCalendar.tsx +1 -4
- package/src/modules/customers/components/detail/PlannedActivitiesSection.tsx +2 -1
- package/src/modules/customers/components/detail/schedule/DateTimeFields.tsx +2 -1
- package/src/modules/customers/lib/localDay.ts +36 -0
- package/src/modules/sales/components/documents/PaymentDialog.tsx +18 -11
- package/src/modules/sales/i18n/de.json +1 -0
- package/src/modules/sales/i18n/en.json +1 -0
- package/src/modules/sales/i18n/es.json +1 -0
- package/src/modules/sales/i18n/ko.json +1 -0
- package/src/modules/sales/i18n/pl.json +1 -0
- package/src/modules/wms/components/backend/AdjustInventoryDialog.tsx +6 -5
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
|
-
import { useT } from "@open-mercato/shared/lib/i18n/context";
|
|
4
|
+
import { useT, useLocale } from "@open-mercato/shared/lib/i18n/context";
|
|
5
|
+
import { parseLocaleNumber } from "@open-mercato/shared/lib/number";
|
|
5
6
|
import { apiCall } from "@open-mercato/ui/backend/utils/apiCall";
|
|
6
7
|
import { Button } from "@open-mercato/ui/primitives/button";
|
|
7
8
|
import { Checkbox } from "@open-mercato/ui/primitives/checkbox";
|
|
@@ -56,19 +57,24 @@ const REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({
|
|
|
56
57
|
fallback: REFERENCE_UNIT_DISPLAY[code] ?? code,
|
|
57
58
|
}));
|
|
58
59
|
|
|
59
|
-
|
|
60
|
-
|
|
60
|
+
// Keeps the field submit-ready (dot-decimal, no grouping characters) while the user is
|
|
61
|
+
// still typing. Falls back to the raw value when it isn't parseable yet (empty, a lone
|
|
62
|
+
// sign) so the field stays editable. Delegates to `parseLocaleNumber` instead of a
|
|
63
|
+
// hard-coded `,` -> `.` swap, which broke on grouped input such as `1 234,56` (#5828).
|
|
64
|
+
export function normalizeDecimalInput(value: string, locale?: string): string {
|
|
65
|
+
const parsed = parseLocaleNumber(value, locale);
|
|
66
|
+
return parsed === null ? value : String(parsed);
|
|
61
67
|
}
|
|
62
68
|
|
|
63
|
-
function toPositiveNumber(value: unknown): number | null {
|
|
69
|
+
export function toPositiveNumber(value: unknown, locale?: string): number | null {
|
|
64
70
|
if (typeof value === "number") {
|
|
65
71
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
66
72
|
}
|
|
67
73
|
if (typeof value !== "string") return null;
|
|
68
74
|
const normalized = toTrimmedOrNull(value);
|
|
69
75
|
if (!normalized) return null;
|
|
70
|
-
const numeric =
|
|
71
|
-
return
|
|
76
|
+
const numeric = parseLocaleNumber(normalized, locale);
|
|
77
|
+
return numeric !== null && numeric > 0 ? numeric : null;
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
function toSortValue(value: string): number {
|
|
@@ -82,7 +88,10 @@ function formatPreviewNumber(value: number): string {
|
|
|
82
88
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toString();
|
|
83
89
|
}
|
|
84
90
|
|
|
85
|
-
function normalizeConversions(
|
|
91
|
+
function normalizeConversions(
|
|
92
|
+
value: unknown,
|
|
93
|
+
locale?: string,
|
|
94
|
+
): ProductUnitConversionDraft[] {
|
|
86
95
|
if (!Array.isArray(value)) return [];
|
|
87
96
|
const normalized = value
|
|
88
97
|
.map((entry) => {
|
|
@@ -92,7 +101,7 @@ function normalizeConversions(value: unknown): ProductUnitConversionDraft[] {
|
|
|
92
101
|
id: toTrimmedOrNull(row.id) ?? null,
|
|
93
102
|
unitCode: toTrimmedOrNull(row.unitCode) ?? "",
|
|
94
103
|
toBaseFactor: toTrimmedOrNull(row.toBaseFactor)
|
|
95
|
-
? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor) as string)
|
|
104
|
+
? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor) as string, locale)
|
|
96
105
|
: "",
|
|
97
106
|
sortOrder: toTrimmedOrNull(row.sortOrder) ?? "",
|
|
98
107
|
isActive: row.isActive !== false,
|
|
@@ -135,13 +144,14 @@ export function ProductUomSection({
|
|
|
135
144
|
embedded = false,
|
|
136
145
|
}: ProductUomSectionProps) {
|
|
137
146
|
const t = useT();
|
|
147
|
+
const locale = useLocale();
|
|
138
148
|
const { enabled: unitPriceDisplayEnabled } = useUnitPriceDisplayEnabled();
|
|
139
149
|
const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);
|
|
140
150
|
const [loadingUnits, setLoadingUnits] = React.useState(false);
|
|
141
151
|
const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);
|
|
142
152
|
const conversions = React.useMemo(
|
|
143
|
-
() => normalizeConversions(values.unitConversions),
|
|
144
|
-
[values.unitConversions],
|
|
153
|
+
() => normalizeConversions(values.unitConversions, locale),
|
|
154
|
+
[values.unitConversions, locale],
|
|
145
155
|
);
|
|
146
156
|
|
|
147
157
|
React.useEffect(() => {
|
|
@@ -238,13 +248,13 @@ export function ProductUomSection({
|
|
|
238
248
|
const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? "";
|
|
239
249
|
const defaultSalesQuantityRaw =
|
|
240
250
|
toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? "1";
|
|
241
|
-
const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw);
|
|
251
|
+
const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw, locale);
|
|
242
252
|
const unitPriceEnabled = Boolean(values.unitPriceEnabled);
|
|
243
253
|
const unitPriceReferenceUnit =
|
|
244
254
|
toTrimmedOrNull(values.unitPriceReferenceUnit) ?? "";
|
|
245
255
|
const unitPriceBaseQuantityRaw =
|
|
246
256
|
toTrimmedOrNull(values.unitPriceBaseQuantity) ?? "";
|
|
247
|
-
const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw);
|
|
257
|
+
const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw, locale);
|
|
248
258
|
|
|
249
259
|
const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
|
|
250
260
|
const salesUnitLabel =
|
|
@@ -261,17 +271,17 @@ export function ProductUomSection({
|
|
|
261
271
|
(entry) =>
|
|
262
272
|
entry.isActive &&
|
|
263
273
|
entry.unitCode.toLowerCase() === defaultSalesKey &&
|
|
264
|
-
toPositiveNumber(entry.toBaseFactor) !== null,
|
|
274
|
+
toPositiveNumber(entry.toBaseFactor, locale) !== null,
|
|
265
275
|
);
|
|
266
|
-
return row ? toPositiveNumber(row.toBaseFactor) : null;
|
|
267
|
-
}, [conversions, defaultSalesUnit, defaultUnit]);
|
|
276
|
+
return row ? toPositiveNumber(row.toBaseFactor, locale) : null;
|
|
277
|
+
}, [conversions, defaultSalesUnit, defaultUnit, locale]);
|
|
268
278
|
|
|
269
|
-
const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity);
|
|
279
|
+
const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity, locale);
|
|
270
280
|
const defaultSalesQuantityNormalized =
|
|
271
281
|
defaultSalesQuantityNumber && defaultSalesFactor
|
|
272
282
|
? defaultSalesQuantityNumber * defaultSalesFactor
|
|
273
283
|
: null;
|
|
274
|
-
const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity);
|
|
284
|
+
const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity, locale);
|
|
275
285
|
|
|
276
286
|
const validConversions = conversions.filter(
|
|
277
287
|
(entry) =>
|
|
@@ -388,7 +398,7 @@ export function ProductUomSection({
|
|
|
388
398
|
onChange={(event) =>
|
|
389
399
|
setValue(
|
|
390
400
|
"defaultSalesUnitQuantity",
|
|
391
|
-
normalizeDecimalInput(event.target.value),
|
|
401
|
+
normalizeDecimalInput(event.target.value, locale),
|
|
392
402
|
)
|
|
393
403
|
}
|
|
394
404
|
placeholder="1"
|
|
@@ -541,7 +551,7 @@ export function ProductUomSection({
|
|
|
541
551
|
onChange={(event) =>
|
|
542
552
|
setValue(
|
|
543
553
|
"unitPriceBaseQuantity",
|
|
544
|
-
normalizeDecimalInput(event.target.value),
|
|
554
|
+
normalizeDecimalInput(event.target.value, locale),
|
|
545
555
|
)
|
|
546
556
|
}
|
|
547
557
|
placeholder="1"
|
|
@@ -595,7 +605,7 @@ export function ProductUomSection({
|
|
|
595
605
|
) : (
|
|
596
606
|
<div className="space-y-2">
|
|
597
607
|
{conversions.map((entry, index) => {
|
|
598
|
-
const conversionFactor = toPositiveNumber(entry.toBaseFactor);
|
|
608
|
+
const conversionFactor = toPositiveNumber(entry.toBaseFactor, locale);
|
|
599
609
|
const conversionPreviewText =
|
|
600
610
|
entry.unitCode && conversionFactor !== null
|
|
601
611
|
? t(
|
|
@@ -653,7 +663,7 @@ export function ProductUomSection({
|
|
|
653
663
|
value={entry.toBaseFactor}
|
|
654
664
|
onChange={(event) =>
|
|
655
665
|
updateConversion(index, {
|
|
656
|
-
toBaseFactor: normalizeDecimalInput(event.target.value),
|
|
666
|
+
toBaseFactor: normalizeDecimalInput(event.target.value, locale),
|
|
657
667
|
})
|
|
658
668
|
}
|
|
659
669
|
placeholder="1"
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'
|
|
5
|
-
import { toZonedTime } from 'date-fns-tz'
|
|
6
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
7
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
@@ -11,6 +10,7 @@ import { ActivitiesDayStrip } from './ActivitiesDayStrip'
|
|
|
11
10
|
import { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'
|
|
12
11
|
import type { InteractionSummary } from './types'
|
|
13
12
|
import { isOpenInteractionStatus } from '../../lib/interactionStatus'
|
|
13
|
+
import { isSameDay, toLocalZonedDate } from '../../lib/localDay'
|
|
14
14
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
15
15
|
|
|
16
16
|
const logger = createLogger('customers')
|
|
@@ -43,30 +43,12 @@ const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> =
|
|
|
43
43
|
note: StickyNote,
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const USER_TIMEZONE = (() => {
|
|
47
|
-
try {
|
|
48
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
49
|
-
} catch {
|
|
50
|
-
return 'UTC'
|
|
51
|
-
}
|
|
52
|
-
})()
|
|
53
|
-
|
|
54
|
-
// Project a UTC instant to the user's local timezone before extracting day/month/year
|
|
55
|
-
// for "same day" comparisons (issue #1809 — E3 timezone drift).
|
|
56
|
-
function toLocalZonedDate(value: string | Date): Date {
|
|
57
|
-
return toZonedTime(value, USER_TIMEZONE)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
46
|
function startOfDay(date: Date): Date {
|
|
61
47
|
const next = new Date(date)
|
|
62
48
|
next.setHours(0, 0, 0, 0)
|
|
63
49
|
return next
|
|
64
50
|
}
|
|
65
51
|
|
|
66
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
67
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
68
|
-
}
|
|
69
|
-
|
|
70
52
|
function isOverdue(activity: InteractionSummary, now: Date): boolean {
|
|
71
53
|
const scheduled = activity.scheduledAt ?? activity.occurredAt
|
|
72
54
|
if (!scheduled) return false
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from 'react'
|
|
4
4
|
import { ChevronLeft, ChevronRight } from 'lucide-react'
|
|
5
|
-
import { toZonedTime } from 'date-fns-tz'
|
|
6
5
|
import { cn } from '@open-mercato/shared/lib/utils'
|
|
7
6
|
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
8
7
|
import type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'
|
|
9
8
|
import { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'
|
|
10
9
|
import type { InteractionSummary } from './types'
|
|
10
|
+
import { isSameDay, toLocalZonedDate } from '../../lib/localDay'
|
|
11
11
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
12
12
|
|
|
13
13
|
const logger = createLogger('customers')
|
|
@@ -25,23 +25,6 @@ interface ActivitiesDayStripProps {
|
|
|
25
25
|
events?: InteractionSummary[]
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const USER_TIMEZONE = (() => {
|
|
29
|
-
try {
|
|
30
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
31
|
-
} catch {
|
|
32
|
-
return 'UTC'
|
|
33
|
-
}
|
|
34
|
-
})()
|
|
35
|
-
|
|
36
|
-
// Project a UTC ISO timestamp to the user's local timezone before comparing
|
|
37
|
-
// "same day" (issue #1809 — E3). The browser's `new Date(iso)` treats the
|
|
38
|
-
// instant correctly, but `getDate()/getMonth()/getFullYear()` reflect the
|
|
39
|
-
// user's local day, so for activities scheduled at e.g. 23:30 local on a UTC
|
|
40
|
-
// boundary the day-strip and list now agree.
|
|
41
|
-
function toLocalZonedDate(value: string | Date): Date {
|
|
42
|
-
return toZonedTime(value, USER_TIMEZONE)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
28
|
const VISIBLE_DAYS = 5
|
|
46
29
|
const BUSYNESS_SLOTS = 10
|
|
47
30
|
const SLOT_START_HOUR = 7
|
|
@@ -84,10 +67,6 @@ function endOfDay(date: Date): Date {
|
|
|
84
67
|
return next
|
|
85
68
|
}
|
|
86
69
|
|
|
87
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
88
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
89
|
-
}
|
|
90
|
-
|
|
91
70
|
function isWeekend(date: Date): boolean {
|
|
92
71
|
const day = date.getDay()
|
|
93
72
|
return day === 0 || day === 6
|
|
@@ -9,6 +9,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
9
9
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
10
10
|
import type { InteractionSummary } from './types'
|
|
11
11
|
import { loadLegacyActivitiesInRange } from './legacyActivities'
|
|
12
|
+
import { isSameDay } from '../../lib/localDay'
|
|
12
13
|
|
|
13
14
|
interface MiniWeekCalendarProps {
|
|
14
15
|
entityId: string
|
|
@@ -30,10 +31,6 @@ function getWeekDays(baseDate: Date): Date[] {
|
|
|
30
31
|
return days
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
function isSameDay(a: Date, b: Date): boolean {
|
|
34
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
35
|
-
}
|
|
36
|
-
|
|
37
34
|
function getDayLabels(t: ReturnType<typeof useT>): string[] {
|
|
38
35
|
return [
|
|
39
36
|
t('customers.calendar.day.mon', 'MON'),
|
|
@@ -7,6 +7,7 @@ import { Button } from '@open-mercato/ui/primitives/button'
|
|
|
7
7
|
import { IconButton } from '@open-mercato/ui/primitives/icon-button'
|
|
8
8
|
import { Popover, PopoverContent, PopoverTrigger } from '@open-mercato/ui/primitives/popover'
|
|
9
9
|
import type { InteractionSummary } from './types'
|
|
10
|
+
import { isSameDay } from '../../lib/localDay'
|
|
10
11
|
|
|
11
12
|
const TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
|
|
12
13
|
call: Phone,
|
|
@@ -245,7 +246,7 @@ function formatScheduledDate(isoString: string): string {
|
|
|
245
246
|
const dateStr = date.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })
|
|
246
247
|
const timeStr = date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })
|
|
247
248
|
|
|
248
|
-
if (date
|
|
249
|
+
if (isSameDay(date, tomorrow)) {
|
|
249
250
|
return `Tomorrow ${timeStr}`
|
|
250
251
|
}
|
|
251
252
|
return `${dayName}, ${dateStr} · ${timeStr}`
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from '@open-mercato/ui/primitives/select'
|
|
17
17
|
import type { ActivityType, ScheduleFieldId } from './fieldConfig'
|
|
18
18
|
import { isVisible, getFieldLabel } from './fieldConfig'
|
|
19
|
+
import { USER_TIMEZONE } from '../../../lib/localDay'
|
|
19
20
|
|
|
20
21
|
function parseIsoDate(value: string): Date | null {
|
|
21
22
|
if (!value) return null
|
|
@@ -193,7 +194,7 @@ export function DateTimeFields({
|
|
|
193
194
|
<span className="text-muted-foreground">·</span>
|
|
194
195
|
<span className="flex items-center gap-1.5">
|
|
195
196
|
<Globe className="size-3.5" />
|
|
196
|
-
{
|
|
197
|
+
{USER_TIMEZONE} (GMT{new Date().getTimezoneOffset() <= 0 ? '+' : '-'}{String(Math.abs(Math.floor(new Date().getTimezoneOffset() / 60))).padStart(1, '0')})
|
|
197
198
|
</span>
|
|
198
199
|
{showRecurrence && (
|
|
199
200
|
<>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Single source of truth for "what timezone is the reader in, and is this the same
|
|
2
|
+
// calendar day" across the customers detail components.
|
|
3
|
+
//
|
|
4
|
+
// Every activity surface in `components/detail/` answers the same two questions, and
|
|
5
|
+
// until issue #6011 each answered them with its own byte-identical copy. They are kept
|
|
6
|
+
// together here because they are one decision, not two: `isSameDay` is only meaningful
|
|
7
|
+
// once both operands have been projected into the same zone, which is what
|
|
8
|
+
// `toLocalZonedDate` does with `USER_TIMEZONE`.
|
|
9
|
+
//
|
|
10
|
+
// The zone is deliberately the reader's own runtime zone. Host/tenant-configured
|
|
11
|
+
// display timezones are planned in `.ai/specs/2026-05-18-date-locale-settings.md`
|
|
12
|
+
// (issue #1964); when that lands, threading a configured zone through the detail
|
|
13
|
+
// components is an edit to this file rather than a hunt through every component.
|
|
14
|
+
|
|
15
|
+
import { toZonedTime } from 'date-fns-tz'
|
|
16
|
+
|
|
17
|
+
export const USER_TIMEZONE = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
|
|
20
|
+
} catch {
|
|
21
|
+
return 'UTC'
|
|
22
|
+
}
|
|
23
|
+
})()
|
|
24
|
+
|
|
25
|
+
// Project a UTC instant to the user's local timezone before extracting day/month/year
|
|
26
|
+
// for "same day" comparisons (issue #1809 — E3 timezone drift). The browser's
|
|
27
|
+
// `new Date(iso)` treats the instant correctly, but `getDate()/getMonth()/getFullYear()`
|
|
28
|
+
// reflect the user's local day, so an activity scheduled at e.g. 23:30 local on a UTC
|
|
29
|
+
// boundary lands on the day the reader would call it.
|
|
30
|
+
export function toLocalZonedDate(value: string | Date): Date {
|
|
31
|
+
return toZonedTime(value, USER_TIMEZONE)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isSameDay(a: Date, b: Date): boolean {
|
|
35
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
|
|
36
|
+
}
|
|
@@ -15,7 +15,8 @@ import { useDialogKeyHandler } from '@open-mercato/ui/hooks/useDialogKeyHandler'
|
|
|
15
15
|
import { Input } from '@open-mercato/ui/primitives/input'
|
|
16
16
|
import { Spinner } from '@open-mercato/ui/primitives/spinner'
|
|
17
17
|
import { E } from '#generated/entities.ids.generated'
|
|
18
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
18
|
+
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
19
|
+
import { parseLocaleNumber } from '@open-mercato/shared/lib/number'
|
|
19
20
|
import { normalizeCustomFieldSubmitValue, extractCustomFieldValues } from './customFieldHelpers'
|
|
20
21
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
21
22
|
|
|
@@ -66,13 +67,15 @@ type PaymentDialogProps = {
|
|
|
66
67
|
onSaved?: (totals?: PaymentTotals | null) => void | Promise<void>
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return 0
|
|
70
|
+
// The amount field is a hand-rolled text input, so the raw string a user typed reaches
|
|
71
|
+
// the submit handler carrying the separator the surrounding UI displays — `110,70` under
|
|
72
|
+
// Polish (issue #5828, same defect class as #5552). A blank/absent value keeps the old
|
|
73
|
+
// "must be a positive amount" message; only genuinely unparseable input returns null.
|
|
74
|
+
const parseAmountInput = (value: unknown, locale?: string): number | null => {
|
|
75
|
+
if (typeof value === 'number') return Number.isFinite(value) ? value : null
|
|
76
|
+
if (typeof value !== 'string') return 0
|
|
77
|
+
if (!value.trim()) return 0
|
|
78
|
+
return parseLocaleNumber(value, locale)
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
export function PaymentDialog({
|
|
@@ -88,6 +91,7 @@ export function PaymentDialog({
|
|
|
88
91
|
onSaved,
|
|
89
92
|
}: PaymentDialogProps) {
|
|
90
93
|
const t = useT()
|
|
94
|
+
const locale = useLocale()
|
|
91
95
|
const dialogContentRef = React.useRef<HTMLDivElement | null>(null)
|
|
92
96
|
const [formResetKey, setFormResetKey] = React.useState(0)
|
|
93
97
|
const [paymentMethods, setPaymentMethods] = React.useState<PaymentMethodOption[]>([])
|
|
@@ -352,9 +356,8 @@ export function PaymentDialog({
|
|
|
352
356
|
return (
|
|
353
357
|
<div className="flex items-center gap-2">
|
|
354
358
|
<Input
|
|
355
|
-
type="
|
|
359
|
+
type="text"
|
|
356
360
|
inputMode="decimal"
|
|
357
|
-
step="0.01"
|
|
358
361
|
value={normalized as string | number}
|
|
359
362
|
onChange={(event) => setValue(event.target.value)}
|
|
360
363
|
placeholder="0.00"
|
|
@@ -493,10 +496,14 @@ export function PaymentDialog({
|
|
|
493
496
|
const handleSubmit = React.useCallback(
|
|
494
497
|
async (values: Record<string, unknown>) => {
|
|
495
498
|
const resolvedCurrency = currencyCode ? currencyCode.toUpperCase() : ''
|
|
496
|
-
const amountValue =
|
|
499
|
+
const amountValue = parseAmountInput(values.amount, locale)
|
|
497
500
|
if (!resolvedCurrency.trim()) {
|
|
498
501
|
throw createCrudFormError(t('sales.documents.payments.currencyRequired', 'Currency is required.'))
|
|
499
502
|
}
|
|
503
|
+
if (amountValue === null) {
|
|
504
|
+
const message = t('sales.documents.payments.amountInvalid', 'Enter the amount as a number.')
|
|
505
|
+
throw createCrudFormError(message, { amount: message })
|
|
506
|
+
}
|
|
500
507
|
if (amountValue <= 0) {
|
|
501
508
|
throw createCrudFormError(t('sales.documents.payments.amountRequired', 'Enter a positive amount.'), {
|
|
502
509
|
amount: t('sales.documents.payments.amountRequired', 'Enter a positive amount.'),
|
|
@@ -1139,6 +1139,7 @@
|
|
|
1139
1139
|
"sales.documents.list.table.unassigned": "Nicht zugewiesen",
|
|
1140
1140
|
"sales.documents.payments.add": "Zahlung hinzufügen",
|
|
1141
1141
|
"sales.documents.payments.amount": "Betrag",
|
|
1142
|
+
"sales.documents.payments.amountInvalid": "Geben Sie den Betrag als Zahl ein.",
|
|
1142
1143
|
"sales.documents.payments.amountRequired": "Geben Sie einen positiven Betrag ein.",
|
|
1143
1144
|
"sales.documents.payments.created": "Zahlung erfasst.",
|
|
1144
1145
|
"sales.documents.payments.createdAt": "Erstellt",
|
|
@@ -1139,6 +1139,7 @@
|
|
|
1139
1139
|
"sales.documents.list.table.unassigned": "Unassigned",
|
|
1140
1140
|
"sales.documents.payments.add": "Add payment",
|
|
1141
1141
|
"sales.documents.payments.amount": "Amount",
|
|
1142
|
+
"sales.documents.payments.amountInvalid": "Enter the amount as a number.",
|
|
1142
1143
|
"sales.documents.payments.amountRequired": "Enter a positive amount.",
|
|
1143
1144
|
"sales.documents.payments.created": "Payment recorded.",
|
|
1144
1145
|
"sales.documents.payments.createdAt": "Created",
|
|
@@ -1139,6 +1139,7 @@
|
|
|
1139
1139
|
"sales.documents.list.table.unassigned": "Sin asignar",
|
|
1140
1140
|
"sales.documents.payments.add": "Agregar pago",
|
|
1141
1141
|
"sales.documents.payments.amount": "Importe",
|
|
1142
|
+
"sales.documents.payments.amountInvalid": "Introduce el importe como un número.",
|
|
1142
1143
|
"sales.documents.payments.amountRequired": "Introduce un importe positivo.",
|
|
1143
1144
|
"sales.documents.payments.created": "Pago registrado.",
|
|
1144
1145
|
"sales.documents.payments.createdAt": "Creado",
|
|
@@ -1139,6 +1139,7 @@
|
|
|
1139
1139
|
"sales.documents.list.table.unassigned": "미지정",
|
|
1140
1140
|
"sales.documents.payments.add": "결제 추가",
|
|
1141
1141
|
"sales.documents.payments.amount": "금액",
|
|
1142
|
+
"sales.documents.payments.amountInvalid": "금액을 숫자로 입력하세요.",
|
|
1142
1143
|
"sales.documents.payments.amountRequired": "0보다 큰 금액을 입력하세요.",
|
|
1143
1144
|
"sales.documents.payments.created": "결제가 등록되었습니다.",
|
|
1144
1145
|
"sales.documents.payments.createdAt": "등록일",
|
|
@@ -1139,6 +1139,7 @@
|
|
|
1139
1139
|
"sales.documents.list.table.unassigned": "Nieprzypisany",
|
|
1140
1140
|
"sales.documents.payments.add": "Dodaj płatność",
|
|
1141
1141
|
"sales.documents.payments.amount": "Kwota",
|
|
1142
|
+
"sales.documents.payments.amountInvalid": "Podaj kwotę jako liczbę.",
|
|
1142
1143
|
"sales.documents.payments.amountRequired": "Wprowadź dodatnią kwotę.",
|
|
1143
1144
|
"sales.documents.payments.created": "Płatność zarejestrowana.",
|
|
1144
1145
|
"sales.documents.payments.createdAt": "Utworzono",
|
|
@@ -32,7 +32,8 @@ import {
|
|
|
32
32
|
} from '@open-mercato/ui/primitives/select'
|
|
33
33
|
import { StatusBadge } from '@open-mercato/ui/primitives/status-badge'
|
|
34
34
|
import { Textarea } from '@open-mercato/ui/primitives/textarea'
|
|
35
|
-
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
35
|
+
import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
|
|
36
|
+
import { parseLocaleNumber } from '@open-mercato/shared/lib/number'
|
|
36
37
|
import { createLogger } from '@open-mercato/shared/lib/logger'
|
|
37
38
|
import { buildInventoryMutationReferenceId } from '../../lib/inventoryMutationUi'
|
|
38
39
|
import {
|
|
@@ -91,11 +92,10 @@ const EMPTY_FORM: AdjustFormValues = {
|
|
|
91
92
|
serialNumber: '',
|
|
92
93
|
}
|
|
93
94
|
|
|
94
|
-
function parseDeltaInput(value: string): number | null {
|
|
95
|
+
export function parseDeltaInput(value: string, locale?: string): number | null {
|
|
95
96
|
const trimmed = value.trim()
|
|
96
97
|
if (!trimmed || trimmed === '-' || trimmed === '+') return null
|
|
97
|
-
|
|
98
|
-
return Number.isFinite(parsed) ? parsed : null
|
|
98
|
+
return parseLocaleNumber(trimmed, locale)
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
export function AdjustInventoryDialog({
|
|
@@ -108,6 +108,7 @@ export function AdjustInventoryDialog({
|
|
|
108
108
|
initialLotId,
|
|
109
109
|
}: AdjustInventoryDialogProps) {
|
|
110
110
|
const t = useT()
|
|
111
|
+
const locale = useLocale()
|
|
111
112
|
const queryClient = useQueryClient()
|
|
112
113
|
const formRef = React.useRef<HTMLFormElement>(null)
|
|
113
114
|
const { runMutation, retryLastMutation } = useGuardedMutation<{
|
|
@@ -690,7 +691,7 @@ export function AdjustInventoryDialog({
|
|
|
690
691
|
inputMode="decimal"
|
|
691
692
|
value={String(form.delta)}
|
|
692
693
|
onChange={(event) => {
|
|
693
|
-
const parsed = parseDeltaInput(event.target.value)
|
|
694
|
+
const parsed = parseDeltaInput(event.target.value, locale)
|
|
694
695
|
if (parsed == null) return
|
|
695
696
|
patchForm({ delta: parsed })
|
|
696
697
|
}}
|