@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
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
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";
|
|
@@ -32,18 +33,19 @@ const REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({
|
|
|
32
33
|
i18nKey: `catalog.products.unitPrice.options.${code}`,
|
|
33
34
|
fallback: REFERENCE_UNIT_DISPLAY[code] ?? code
|
|
34
35
|
}));
|
|
35
|
-
function normalizeDecimalInput(value) {
|
|
36
|
-
|
|
36
|
+
function normalizeDecimalInput(value, locale) {
|
|
37
|
+
const parsed = parseLocaleNumber(value, locale);
|
|
38
|
+
return parsed === null ? value : String(parsed);
|
|
37
39
|
}
|
|
38
|
-
function toPositiveNumber(value) {
|
|
40
|
+
function toPositiveNumber(value, locale) {
|
|
39
41
|
if (typeof value === "number") {
|
|
40
42
|
return Number.isFinite(value) && value > 0 ? value : null;
|
|
41
43
|
}
|
|
42
44
|
if (typeof value !== "string") return null;
|
|
43
45
|
const normalized = toTrimmedOrNull(value);
|
|
44
46
|
if (!normalized) return null;
|
|
45
|
-
const numeric =
|
|
46
|
-
return
|
|
47
|
+
const numeric = parseLocaleNumber(normalized, locale);
|
|
48
|
+
return numeric !== null && numeric > 0 ? numeric : null;
|
|
47
49
|
}
|
|
48
50
|
function toSortValue(value) {
|
|
49
51
|
const numeric = Number(value);
|
|
@@ -54,7 +56,7 @@ function formatPreviewNumber(value) {
|
|
|
54
56
|
const rounded = Math.round(value * 1e6) / 1e6;
|
|
55
57
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toString();
|
|
56
58
|
}
|
|
57
|
-
function normalizeConversions(value) {
|
|
59
|
+
function normalizeConversions(value, locale) {
|
|
58
60
|
if (!Array.isArray(value)) return [];
|
|
59
61
|
const normalized = value.map((entry) => {
|
|
60
62
|
if (!entry || typeof entry !== "object") return null;
|
|
@@ -62,7 +64,7 @@ function normalizeConversions(value) {
|
|
|
62
64
|
return {
|
|
63
65
|
id: toTrimmedOrNull(row.id) ?? null,
|
|
64
66
|
unitCode: toTrimmedOrNull(row.unitCode) ?? "",
|
|
65
|
-
toBaseFactor: toTrimmedOrNull(row.toBaseFactor) ? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor)) : "",
|
|
67
|
+
toBaseFactor: toTrimmedOrNull(row.toBaseFactor) ? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor), locale) : "",
|
|
66
68
|
sortOrder: toTrimmedOrNull(row.sortOrder) ?? "",
|
|
67
69
|
isActive: row.isActive !== false
|
|
68
70
|
};
|
|
@@ -97,13 +99,14 @@ function ProductUomSection({
|
|
|
97
99
|
embedded = false
|
|
98
100
|
}) {
|
|
99
101
|
const t = useT();
|
|
102
|
+
const locale = useLocale();
|
|
100
103
|
const { enabled: unitPriceDisplayEnabled } = useUnitPriceDisplayEnabled();
|
|
101
104
|
const [unitOptions, setUnitOptions] = React.useState([]);
|
|
102
105
|
const [loadingUnits, setLoadingUnits] = React.useState(false);
|
|
103
106
|
const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);
|
|
104
107
|
const conversions = React.useMemo(
|
|
105
|
-
() => normalizeConversions(values.unitConversions),
|
|
106
|
-
[values.unitConversions]
|
|
108
|
+
() => normalizeConversions(values.unitConversions, locale),
|
|
109
|
+
[values.unitConversions, locale]
|
|
107
110
|
);
|
|
108
111
|
React.useEffect(() => {
|
|
109
112
|
let cancelled = false;
|
|
@@ -191,11 +194,11 @@ function ProductUomSection({
|
|
|
191
194
|
const defaultUnit = toTrimmedOrNull(values.defaultUnit) ?? "";
|
|
192
195
|
const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? "";
|
|
193
196
|
const defaultSalesQuantityRaw = toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? "1";
|
|
194
|
-
const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw);
|
|
197
|
+
const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw, locale);
|
|
195
198
|
const unitPriceEnabled = Boolean(values.unitPriceEnabled);
|
|
196
199
|
const unitPriceReferenceUnit = toTrimmedOrNull(values.unitPriceReferenceUnit) ?? "";
|
|
197
200
|
const unitPriceBaseQuantityRaw = toTrimmedOrNull(values.unitPriceBaseQuantity) ?? "";
|
|
198
|
-
const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw);
|
|
201
|
+
const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw, locale);
|
|
199
202
|
const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;
|
|
200
203
|
const salesUnitLabel = findUnitLabel(defaultSalesUnit || defaultUnit) ?? defaultSalesUnit ?? defaultUnit;
|
|
201
204
|
const defaultSalesFactor = React.useMemo(() => {
|
|
@@ -204,13 +207,13 @@ function ProductUomSection({
|
|
|
204
207
|
if (!defaultUnitKey || !defaultSalesKey) return null;
|
|
205
208
|
if (defaultSalesKey === defaultUnitKey) return 1;
|
|
206
209
|
const row = conversions.find(
|
|
207
|
-
(entry) => entry.isActive && entry.unitCode.toLowerCase() === defaultSalesKey && toPositiveNumber(entry.toBaseFactor) !== null
|
|
210
|
+
(entry) => entry.isActive && entry.unitCode.toLowerCase() === defaultSalesKey && toPositiveNumber(entry.toBaseFactor, locale) !== null
|
|
208
211
|
);
|
|
209
|
-
return row ? toPositiveNumber(row.toBaseFactor) : null;
|
|
210
|
-
}, [conversions, defaultSalesUnit, defaultUnit]);
|
|
211
|
-
const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity);
|
|
212
|
+
return row ? toPositiveNumber(row.toBaseFactor, locale) : null;
|
|
213
|
+
}, [conversions, defaultSalesUnit, defaultUnit, locale]);
|
|
214
|
+
const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity, locale);
|
|
212
215
|
const defaultSalesQuantityNormalized = defaultSalesQuantityNumber && defaultSalesFactor ? defaultSalesQuantityNumber * defaultSalesFactor : null;
|
|
213
|
-
const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity);
|
|
216
|
+
const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity, locale);
|
|
214
217
|
const validConversions = conversions.filter(
|
|
215
218
|
(entry) => toTrimmedOrNull(entry.unitCode) && toTrimmedOrNull(entry.toBaseFactor)
|
|
216
219
|
);
|
|
@@ -288,7 +291,7 @@ function ProductUomSection({
|
|
|
288
291
|
value: defaultSalesQuantity,
|
|
289
292
|
onChange: (event) => setValue(
|
|
290
293
|
"defaultSalesUnitQuantity",
|
|
291
|
-
normalizeDecimalInput(event.target.value)
|
|
294
|
+
normalizeDecimalInput(event.target.value, locale)
|
|
292
295
|
),
|
|
293
296
|
placeholder: "1"
|
|
294
297
|
}
|
|
@@ -418,7 +421,7 @@ function ProductUomSection({
|
|
|
418
421
|
value: unitPriceBaseQuantity,
|
|
419
422
|
onChange: (event) => setValue(
|
|
420
423
|
"unitPriceBaseQuantity",
|
|
421
|
-
normalizeDecimalInput(event.target.value)
|
|
424
|
+
normalizeDecimalInput(event.target.value, locale)
|
|
422
425
|
),
|
|
423
426
|
placeholder: "1"
|
|
424
427
|
}
|
|
@@ -458,7 +461,7 @@ function ProductUomSection({
|
|
|
458
461
|
"catalog.products.uom.emptyConversions",
|
|
459
462
|
"No conversions configured yet."
|
|
460
463
|
) }) : /* @__PURE__ */ jsx("div", { className: "space-y-2", children: conversions.map((entry, index) => {
|
|
461
|
-
const conversionFactor = toPositiveNumber(entry.toBaseFactor);
|
|
464
|
+
const conversionFactor = toPositiveNumber(entry.toBaseFactor, locale);
|
|
462
465
|
const conversionPreviewText = entry.unitCode && conversionFactor !== null ? t(
|
|
463
466
|
"catalog.products.uom.conversionPreview",
|
|
464
467
|
"1 {{fromUnit}} = {{factor}} {{baseUnit}}",
|
|
@@ -501,7 +504,7 @@ function ProductUomSection({
|
|
|
501
504
|
inputMode: "decimal",
|
|
502
505
|
value: entry.toBaseFactor,
|
|
503
506
|
onChange: (event) => updateConversion(index, {
|
|
504
|
-
toBaseFactor: normalizeDecimalInput(event.target.value)
|
|
507
|
+
toBaseFactor: normalizeDecimalInput(event.target.value, locale)
|
|
505
508
|
}),
|
|
506
509
|
placeholder: "1"
|
|
507
510
|
}
|
|
@@ -593,6 +596,8 @@ function ProductUomSection({
|
|
|
593
596
|
);
|
|
594
597
|
}
|
|
595
598
|
export {
|
|
596
|
-
ProductUomSection
|
|
599
|
+
ProductUomSection,
|
|
600
|
+
normalizeDecimalInput,
|
|
601
|
+
toPositiveNumber
|
|
597
602
|
};
|
|
598
603
|
//# sourceMappingURL=ProductUomSection.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/catalog/components/products/ProductUomSection.tsx"],
|
|
4
|
-
"sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\nimport { useT } from \"@open-mercato/shared/lib/i18n/context\";\nimport { apiCall } from \"@open-mercato/ui/backend/utils/apiCall\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { Checkbox } from \"@open-mercato/ui/primitives/checkbox\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport { Label } from \"@open-mercato/ui/primitives/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { Spinner } from \"@open-mercato/ui/primitives/spinner\";\nimport { ArrowDown, ArrowUp, Plus, Trash2 } from \"lucide-react\";\nimport type {\n ProductFormValues,\n ProductUnitConversionDraft,\n} from \"./productForm\";\nimport { createProductUnitConversionDraft } from \"./productForm\";\nimport { REFERENCE_UNIT_CODES } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { toTrimmedOrNull } from \"./productFormUtils\";\nimport { useUnitPriceDisplayEnabled } from \"./hooks/useUnitPriceDisplayEnabled\";\n\ntype UnitDictionaryEntry = {\n id?: string;\n value?: string;\n label?: string;\n};\n\ntype UnitDictionaryResponse = {\n entries?: UnitDictionaryEntry[];\n};\n\ntype UnitOption = {\n value: string;\n label: string;\n};\n\ntype ProductUomSectionProps = {\n values: ProductFormValues;\n errors: Record<string, string>;\n setValue: (id: string, value: unknown) => void;\n embedded?: boolean;\n};\n\nconst REFERENCE_UNIT_DISPLAY: Record<string, string> = {\n kg: \"1 kg\", l: \"1 l\", m2: \"1 m\\u00B2\", m3: \"1 m\\u00B3\", pc: \"1 pc\",\n};\nconst REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({\n value: code,\n i18nKey: `catalog.products.unitPrice.options.${code}` as const,\n fallback: REFERENCE_UNIT_DISPLAY[code] ?? code,\n}));\n\nfunction normalizeDecimalInput(value: string): string {\n return value.replace(/,/g, \".\");\n}\n\nfunction toPositiveNumber(value: unknown): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) && value > 0 ? value : null;\n }\n if (typeof value !== \"string\") return null;\n const normalized = toTrimmedOrNull(value);\n if (!normalized) return null;\n const numeric = Number(normalized.replace(\",\", \".\"));\n return Number.isFinite(numeric) && numeric > 0 ? numeric : null;\n}\n\nfunction toSortValue(value: string): number {\n const numeric = Number(value);\n return Number.isFinite(numeric) ? numeric : Number.MAX_SAFE_INTEGER;\n}\n\nfunction formatPreviewNumber(value: number): string {\n if (!Number.isFinite(value)) return \"0\";\n const rounded = Math.round(value * 1_000_000) / 1_000_000;\n return Number.isInteger(rounded) ? String(rounded) : rounded.toString();\n}\n\nfunction normalizeConversions(value: unknown): ProductUnitConversionDraft[] {\n if (!Array.isArray(value)) return [];\n const normalized = value\n .map((entry) => {\n if (!entry || typeof entry !== \"object\") return null;\n const row = entry as ProductUnitConversionDraft;\n return {\n id: toTrimmedOrNull(row.id) ?? null,\n unitCode: toTrimmedOrNull(row.unitCode) ?? \"\",\n toBaseFactor: toTrimmedOrNull(row.toBaseFactor)\n ? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor) as string)\n : \"\",\n sortOrder: toTrimmedOrNull(row.sortOrder) ?? \"\",\n isActive: row.isActive !== false,\n } satisfies ProductUnitConversionDraft;\n })\n .filter((entry): entry is ProductUnitConversionDraft => Boolean(entry));\n normalized.sort((left, right) => {\n const leftOrder = toSortValue(left.sortOrder);\n const rightOrder = toSortValue(right.sortOrder);\n if (leftOrder === rightOrder) return left.unitCode.localeCompare(right.unitCode);\n return leftOrder - rightOrder;\n });\n return normalized.map((entry, index) => ({\n ...entry,\n sortOrder: String((index + 1) * 10),\n }));\n}\n\nfunction buildUnitOptions(\n entries: UnitDictionaryEntry[] | undefined,\n): UnitOption[] {\n const list = Array.isArray(entries) ? entries : [];\n const options = list\n .map((entry) => {\n const value = toTrimmedOrNull(entry.value);\n if (!value) return null;\n return {\n value,\n label: toTrimmedOrNull(entry.label) ?? value,\n } satisfies UnitOption;\n })\n .filter((entry): entry is UnitOption => Boolean(entry));\n return options.sort((left, right) => left.label.localeCompare(right.label));\n}\n\nexport function ProductUomSection({\n values,\n errors,\n setValue,\n embedded = false,\n}: ProductUomSectionProps) {\n const t = useT();\n const { enabled: unitPriceDisplayEnabled } = useUnitPriceDisplayEnabled();\n const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);\n const [loadingUnits, setLoadingUnits] = React.useState(false);\n const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);\n const conversions = React.useMemo(\n () => normalizeConversions(values.unitConversions),\n [values.unitConversions],\n );\n\n React.useEffect(() => {\n let cancelled = false;\n async function loadUnits() {\n setLoadingUnits(true);\n setErrorLoadingUnits(false);\n try {\n const response = await apiCall<UnitDictionaryResponse>(\n \"/api/catalog/dictionaries/unit\",\n undefined,\n { fallback: { entries: [] } },\n );\n if (cancelled) return;\n setUnitOptions(buildUnitOptions(response.result?.entries));\n } catch {\n if (!cancelled) {\n setUnitOptions([]);\n setErrorLoadingUnits(true);\n }\n } finally {\n if (!cancelled) setLoadingUnits(false);\n }\n }\n void loadUnits();\n return () => {\n cancelled = true;\n };\n }, []);\n\n const findUnitLabel = React.useCallback(\n (value: string | null | undefined) => {\n const code = toTrimmedOrNull(value);\n if (!code) return null;\n const option = unitOptions.find((entry) => entry.value === code);\n return option?.label ?? code;\n },\n [unitOptions],\n );\n\n const setConversions = React.useCallback(\n (next: ProductUnitConversionDraft[]) => {\n const normalized = next.map((entry, index) => ({\n ...entry,\n sortOrder: String((index + 1) * 10),\n }));\n setValue(\"unitConversions\", normalized);\n },\n [setValue],\n );\n\n const addConversion = React.useCallback(() => {\n const next = [\n ...conversions,\n createProductUnitConversionDraft({\n sortOrder: String((conversions.length + 1) * 10),\n }),\n ];\n setConversions(next);\n }, [conversions, setConversions]);\n\n const updateConversion = React.useCallback(\n (index: number, patch: Partial<ProductUnitConversionDraft>) => {\n const next = conversions.map((entry, rowIndex) =>\n rowIndex === index ? { ...entry, ...patch } : entry,\n );\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const removeConversion = React.useCallback(\n (index: number) => {\n const next = conversions.filter((_entry, rowIndex) => rowIndex !== index);\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const moveConversion = React.useCallback(\n (index: number, direction: \"up\" | \"down\") => {\n const targetIndex = direction === \"up\" ? index - 1 : index + 1;\n if (targetIndex < 0 || targetIndex >= conversions.length) return;\n const next = [...conversions];\n const source = next[index];\n next[index] = next[targetIndex];\n next[targetIndex] = source;\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const defaultUnit = toTrimmedOrNull(values.defaultUnit) ?? \"\";\n const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? \"\";\n const defaultSalesQuantityRaw =\n toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? \"1\";\n const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw);\n const unitPriceEnabled = Boolean(values.unitPriceEnabled);\n const unitPriceReferenceUnit =\n toTrimmedOrNull(values.unitPriceReferenceUnit) ?? \"\";\n const unitPriceBaseQuantityRaw =\n toTrimmedOrNull(values.unitPriceBaseQuantity) ?? \"\";\n const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw);\n\n const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;\n const salesUnitLabel =\n findUnitLabel(defaultSalesUnit || defaultUnit) ??\n defaultSalesUnit ??\n defaultUnit;\n\n const defaultSalesFactor = React.useMemo(() => {\n const defaultUnitKey = defaultUnit.toLowerCase();\n const defaultSalesKey = (defaultSalesUnit || defaultUnit).toLowerCase();\n if (!defaultUnitKey || !defaultSalesKey) return null;\n if (defaultSalesKey === defaultUnitKey) return 1;\n const row = conversions.find(\n (entry) =>\n entry.isActive &&\n entry.unitCode.toLowerCase() === defaultSalesKey &&\n toPositiveNumber(entry.toBaseFactor) !== null,\n );\n return row ? toPositiveNumber(row.toBaseFactor) : null;\n }, [conversions, defaultSalesUnit, defaultUnit]);\n\n const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity);\n const defaultSalesQuantityNormalized =\n defaultSalesQuantityNumber && defaultSalesFactor\n ? defaultSalesQuantityNumber * defaultSalesFactor\n : null;\n const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity);\n\n const validConversions = conversions.filter(\n (entry) =>\n toTrimmedOrNull(entry.unitCode) && toTrimmedOrNull(entry.toBaseFactor),\n );\n const conversionPreviewItems = validConversions\n .slice(0, 3)\n .map((entry) => {\n const label = findUnitLabel(entry.unitCode) ?? entry.unitCode;\n const baseLabel = findUnitLabel(defaultUnit) ?? defaultUnit;\n const factor = toTrimmedOrNull(entry.toBaseFactor) ?? \"1\";\n return `1 ${label} = ${factor} ${baseLabel || t(\"catalog.products.uom.baseUnit\", \"base unit\")}`;\n });\n const conversionPreview =\n validConversions.length > 3\n ? `${conversionPreviewItems.join(\" \u2022 \")} (+${validConversions.length - 3})`\n : conversionPreviewItems.join(\" \u2022 \");\n\n return (\n <div\n className={\n embedded ? \"space-y-5\" : \"space-y-5 rounded-lg border bg-card p-4\"\n }\n >\n <div className=\"space-y-1\">\n <h3 className=\"text-sm font-semibold\">\n {t(\"catalog.products.uom.title\", \"Units of measure\")}\n </h3>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.description\",\n \"Set base unit, sales unit, and packaging conversions.\",\n )}\n </p>\n </div>\n\n {loadingUnits ? (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Spinner className=\"h-3 w-3\" />\n {t(\"catalog.products.uom.loadingUnits\", \"Loading units...\")}\n </div>\n ) : null}\n\n {errorLoadingUnits ? (\n <p className=\"text-xs text-destructive\">\n {t(\n \"catalog.products.uom.errors.loadUnits\",\n \"Failed to load units. Please try refreshing the page.\",\n )}\n </p>\n ) : null}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-base-unit\">{t(\"catalog.products.uom.baseUnit\", \"Base unit\")}</Label>\n <Select\n value={defaultUnit || undefined}\n onValueChange={(value) => setValue(\"defaultUnit\", value || null)}\n disabled={loadingUnits}\n >\n <SelectTrigger id=\"catalog-product-uom-base-unit\">\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {errors.defaultUnit ? (\n <p className=\"text-xs text-destructive\">{errors.defaultUnit}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-sales-unit\">\n {t(\"catalog.products.uom.defaultSalesUnit\", \"Default sales unit\")}\n </Label>\n <Select\n value={defaultSalesUnit || undefined}\n onValueChange={(value) => setValue(\"defaultSalesUnit\", value || null)}\n disabled={loadingUnits}\n >\n <SelectTrigger id=\"catalog-product-uom-sales-unit\">\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {errors.defaultSalesUnit ? (\n <p className=\"text-xs text-destructive\">{errors.defaultSalesUnit}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"catalog-product-uom-default-sales-quantity\">\n {t(\n \"catalog.products.uom.defaultSalesQuantityLabel\",\n \"Default line quantity (in sales unit)\",\n )}\n </Label>\n <Input\n id=\"catalog-product-uom-default-sales-quantity\"\n type=\"text\"\n inputMode=\"decimal\"\n value={defaultSalesQuantity}\n onChange={(event) =>\n setValue(\n \"defaultSalesUnitQuantity\",\n normalizeDecimalInput(event.target.value),\n )\n }\n placeholder=\"1\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.defaultSalesQuantityHint\",\n \"Used to prefill quantity in quote/order lines. Value is interpreted in Default sales unit.\",\n )}\n </p>\n {defaultSalesQuantityNumber && salesUnitLabel ? (\n <p className=\"text-xs text-muted-foreground\">\n {defaultSalesQuantityNormalized && baseUnitLabel\n ? t(\n \"catalog.products.uom.defaultSalesQuantityPreviewWithNormalization\",\n \"Default line: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).\",\n {\n quantity: formatPreviewNumber(defaultSalesQuantityNumber),\n salesUnit: salesUnitLabel,\n normalized: formatPreviewNumber(\n defaultSalesQuantityNormalized,\n ),\n baseUnit: baseUnitLabel,\n },\n )\n : t(\n \"catalog.products.uom.defaultSalesQuantityPreview\",\n \"Default line: {{quantity}} {{salesUnit}}.\",\n {\n quantity: formatPreviewNumber(defaultSalesQuantityNumber),\n salesUnit: salesUnitLabel,\n },\n )}\n </p>\n ) : null}\n {errors.defaultSalesUnitQuantity ? (\n <p className=\"text-xs text-destructive\">\n {errors.defaultSalesUnitQuantity}\n </p>\n ) : null}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-rounding-mode\">\n {t(\"catalog.products.uom.roundingMode\", \"Rounding mode\")}\n </Label>\n <Select\n value={values.uomRoundingMode ?? \"half_up\"}\n onValueChange={(value) => setValue(\"uomRoundingMode\", value)}\n >\n <SelectTrigger id=\"catalog-product-uom-rounding-mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"half_up\">\n {t(\"catalog.products.uom.roundingModeHalfUp\", \"Half up (default)\")}\n </SelectItem>\n <SelectItem value=\"down\">\n {t(\"catalog.products.uom.roundingModeDown\", \"Round down\")}\n </SelectItem>\n <SelectItem value=\"up\">\n {t(\"catalog.products.uom.roundingModeUp\", \"Round up\")}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-rounding-scale\">\n {t(\"catalog.products.uom.roundingScale\", \"Rounding scale (decimal places)\")}\n </Label>\n <Input\n id=\"catalog-product-uom-rounding-scale\"\n type=\"number\"\n min={0}\n max={6}\n value={values.uomRoundingScale ?? 4}\n onChange={(event) => {\n const parsed = Number.parseInt(event.target.value, 10);\n setValue(\"uomRoundingScale\", Number.isFinite(parsed) ? Math.max(0, Math.min(6, parsed)) : 4);\n }}\n />\n </div>\n </div>\n\n {unitPriceDisplayEnabled ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"catalog-product-unit-price-enabled\"\n checked={unitPriceEnabled}\n onCheckedChange={(checked) =>\n setValue(\"unitPriceEnabled\", checked === true)\n }\n />\n <Label\n htmlFor=\"catalog-product-unit-price-enabled\"\n className=\"text-sm\"\n >\n {t(\n \"catalog.products.unitPrice.enable\",\n \"Enable EU unit price display\",\n )}\n </Label>\n </div>\n\n {unitPriceEnabled ? (\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-reference-unit\">\n {t(\n \"catalog.products.unitPrice.referenceUnit\",\n \"Reference unit\",\n )}\n </Label>\n <Select\n value={unitPriceReferenceUnit || undefined}\n onValueChange={(value) => setValue(\"unitPriceReferenceUnit\", value || null)}\n >\n <SelectTrigger id=\"catalog-product-uom-reference-unit\">\n <SelectValue\n placeholder={t(\n \"catalog.products.unitPrice.selectReferenceUnit\",\n \"Select reference unit\",\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {REFERENCE_UNIT_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {t(option.i18nKey, option.fallback)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-unit-price-base-quantity\">\n {t(\n \"catalog.products.unitPrice.baseQuantity\",\n \"Reference quantity (in base unit)\",\n )}\n </Label>\n <Input\n id=\"catalog-product-uom-unit-price-base-quantity\"\n type=\"text\"\n inputMode=\"decimal\"\n value={unitPriceBaseQuantity}\n onChange={(event) =>\n setValue(\n \"unitPriceBaseQuantity\",\n normalizeDecimalInput(event.target.value),\n )\n }\n placeholder=\"1\"\n />\n </div>\n </div>\n ) : null}\n {unitPriceEnabled ? (\n <p className=\"text-xs text-muted-foreground\">\n {unitPriceReferenceUnit && unitPriceBaseQuantityNumber\n ? t(\n \"catalog.products.unitPrice.hintWithPreview\",\n \"Show calculated price per {{quantity}} {{unit}}. For most products use 1 (for example: 1 kg, 1 l, 1 m\u00B2).\",\n {\n quantity: formatPreviewNumber(unitPriceBaseQuantityNumber),\n unit: unitPriceReferenceUnit,\n },\n )\n : t(\n \"catalog.products.unitPrice.hint\",\n \"Show calculated price per selected reference unit. In most cases set quantity to 1.\",\n )}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <Label className=\"text-sm\">\n {t(\"catalog.products.uom.conversions\", \"Product conversions\")}\n </Label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={addConversion}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {t(\"catalog.products.uom.addConversion\", \"Add conversion\")}\n </Button>\n </div>\n\n {conversions.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.emptyConversions\",\n \"No conversions configured yet.\",\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {conversions.map((entry, index) => {\n const conversionFactor = toPositiveNumber(entry.toBaseFactor);\n const conversionPreviewText =\n entry.unitCode && conversionFactor !== null\n ? t(\n \"catalog.products.uom.conversionPreview\",\n \"1 {{fromUnit}} = {{factor}} {{baseUnit}}\",\n {\n fromUnit: findUnitLabel(entry.unitCode) ?? entry.unitCode,\n factor: formatPreviewNumber(conversionFactor),\n baseUnit:\n findUnitLabel(defaultUnit) ??\n defaultUnit ??\n t(\"catalog.products.uom.baseUnit\", \"base unit\"),\n },\n )\n : null;\n\n return (\n <div\n key={entry.id ?? `uom-conversion-${index}`}\n className=\"grid gap-3 rounded-md border p-3 md:grid-cols-12\"\n >\n <div className=\"space-y-1 md:col-span-4\">\n <Label htmlFor={`catalog-product-uom-conversion-unit-${index}`} className=\"text-xs text-muted-foreground\">\n {t(\"catalog.products.uom.conversionUnit\", \"Sales unit\")}\n </Label>\n <Select\n value={entry.unitCode || undefined}\n onValueChange={(value) => updateConversion(index, { unitCode: value ?? '' })}\n disabled={loadingUnits}\n >\n <SelectTrigger id={`catalog-product-uom-conversion-unit-${index}`}>\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1 md:col-span-3\">\n <Label htmlFor={`catalog-product-uom-conversion-factor-${index}`} className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.toBaseFactor\",\n \"Base units per 1 sales unit\",\n )}\n </Label>\n <Input\n id={`catalog-product-uom-conversion-factor-${index}`}\n type=\"text\"\n inputMode=\"decimal\"\n value={entry.toBaseFactor}\n onChange={(event) =>\n updateConversion(index, {\n toBaseFactor: normalizeDecimalInput(event.target.value),\n })\n }\n placeholder=\"1\"\n />\n </div>\n\n <div className=\"flex items-end gap-2 md:col-span-3\">\n <div className=\"inline-flex h-9 rounded-md border\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 rounded-none border-r\"\n onClick={() => moveConversion(index, \"up\")}\n disabled={index === 0}\n aria-label={t(\n \"catalog.products.uom.moveUp\",\n \"Move conversion up\",\n )}\n >\n <ArrowUp className=\"h-4 w-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 rounded-none\"\n onClick={() => moveConversion(index, \"down\")}\n disabled={index === conversions.length - 1}\n aria-label={t(\n \"catalog.products.uom.moveDown\",\n \"Move conversion down\",\n )}\n >\n <ArrowDown className=\"h-4 w-4\" />\n </Button>\n </div>\n <label\n htmlFor={`catalog-product-uom-conversion-active-${index}`}\n className=\"flex items-center gap-2 px-1 text-xs text-muted-foreground\"\n >\n <Checkbox\n id={`catalog-product-uom-conversion-active-${index}`}\n checked={entry.isActive}\n onCheckedChange={(checked) =>\n updateConversion(index, { isActive: checked === true })\n }\n />\n {t(\"catalog.products.uom.active\", \"Active\")}\n </label>\n </div>\n\n <div className=\"flex items-end justify-end md:col-span-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"text-destructive\"\n onClick={() => removeConversion(index)}\n aria-label={t(\n \"catalog.products.uom.removeConversion\",\n \"Remove conversion\",\n )}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {conversionPreviewText && (\n <p className=\"text-xs text-muted-foreground md:col-span-12\">\n {conversionPreviewText}\n </p>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {conversions.length > 1 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.conversionOrderHint\",\n \"Use arrows to reorder conversion priority.\",\n )}\n </p>\n ) : null}\n\n {conversionPreview ? (\n <p className=\"text-xs text-muted-foreground\">{conversionPreview}</p>\n ) : null}\n </div>\n </div>\n );\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["\"use client\";\n\nimport * as React from \"react\";\nimport { useT, useLocale } from \"@open-mercato/shared/lib/i18n/context\";\nimport { parseLocaleNumber } from \"@open-mercato/shared/lib/number\";\nimport { apiCall } from \"@open-mercato/ui/backend/utils/apiCall\";\nimport { Button } from \"@open-mercato/ui/primitives/button\";\nimport { Checkbox } from \"@open-mercato/ui/primitives/checkbox\";\nimport { Input } from \"@open-mercato/ui/primitives/input\";\nimport { Label } from \"@open-mercato/ui/primitives/label\";\nimport {\n Select,\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from \"@open-mercato/ui/primitives/select\";\nimport { Spinner } from \"@open-mercato/ui/primitives/spinner\";\nimport { ArrowDown, ArrowUp, Plus, Trash2 } from \"lucide-react\";\nimport type {\n ProductFormValues,\n ProductUnitConversionDraft,\n} from \"./productForm\";\nimport { createProductUnitConversionDraft } from \"./productForm\";\nimport { REFERENCE_UNIT_CODES } from \"@open-mercato/shared/lib/units/unitCodes\";\nimport { toTrimmedOrNull } from \"./productFormUtils\";\nimport { useUnitPriceDisplayEnabled } from \"./hooks/useUnitPriceDisplayEnabled\";\n\ntype UnitDictionaryEntry = {\n id?: string;\n value?: string;\n label?: string;\n};\n\ntype UnitDictionaryResponse = {\n entries?: UnitDictionaryEntry[];\n};\n\ntype UnitOption = {\n value: string;\n label: string;\n};\n\ntype ProductUomSectionProps = {\n values: ProductFormValues;\n errors: Record<string, string>;\n setValue: (id: string, value: unknown) => void;\n embedded?: boolean;\n};\n\nconst REFERENCE_UNIT_DISPLAY: Record<string, string> = {\n kg: \"1 kg\", l: \"1 l\", m2: \"1 m\\u00B2\", m3: \"1 m\\u00B3\", pc: \"1 pc\",\n};\nconst REFERENCE_UNIT_OPTIONS = REFERENCE_UNIT_CODES.map((code) => ({\n value: code,\n i18nKey: `catalog.products.unitPrice.options.${code}` as const,\n fallback: REFERENCE_UNIT_DISPLAY[code] ?? code,\n}));\n\n// Keeps the field submit-ready (dot-decimal, no grouping characters) while the user is\n// still typing. Falls back to the raw value when it isn't parseable yet (empty, a lone\n// sign) so the field stays editable. Delegates to `parseLocaleNumber` instead of a\n// hard-coded `,` -> `.` swap, which broke on grouped input such as `1 234,56` (#5828).\nexport function normalizeDecimalInput(value: string, locale?: string): string {\n const parsed = parseLocaleNumber(value, locale);\n return parsed === null ? value : String(parsed);\n}\n\nexport function toPositiveNumber(value: unknown, locale?: string): number | null {\n if (typeof value === \"number\") {\n return Number.isFinite(value) && value > 0 ? value : null;\n }\n if (typeof value !== \"string\") return null;\n const normalized = toTrimmedOrNull(value);\n if (!normalized) return null;\n const numeric = parseLocaleNumber(normalized, locale);\n return numeric !== null && numeric > 0 ? numeric : null;\n}\n\nfunction toSortValue(value: string): number {\n const numeric = Number(value);\n return Number.isFinite(numeric) ? numeric : Number.MAX_SAFE_INTEGER;\n}\n\nfunction formatPreviewNumber(value: number): string {\n if (!Number.isFinite(value)) return \"0\";\n const rounded = Math.round(value * 1_000_000) / 1_000_000;\n return Number.isInteger(rounded) ? String(rounded) : rounded.toString();\n}\n\nfunction normalizeConversions(\n value: unknown,\n locale?: string,\n): ProductUnitConversionDraft[] {\n if (!Array.isArray(value)) return [];\n const normalized = value\n .map((entry) => {\n if (!entry || typeof entry !== \"object\") return null;\n const row = entry as ProductUnitConversionDraft;\n return {\n id: toTrimmedOrNull(row.id) ?? null,\n unitCode: toTrimmedOrNull(row.unitCode) ?? \"\",\n toBaseFactor: toTrimmedOrNull(row.toBaseFactor)\n ? normalizeDecimalInput(toTrimmedOrNull(row.toBaseFactor) as string, locale)\n : \"\",\n sortOrder: toTrimmedOrNull(row.sortOrder) ?? \"\",\n isActive: row.isActive !== false,\n } satisfies ProductUnitConversionDraft;\n })\n .filter((entry): entry is ProductUnitConversionDraft => Boolean(entry));\n normalized.sort((left, right) => {\n const leftOrder = toSortValue(left.sortOrder);\n const rightOrder = toSortValue(right.sortOrder);\n if (leftOrder === rightOrder) return left.unitCode.localeCompare(right.unitCode);\n return leftOrder - rightOrder;\n });\n return normalized.map((entry, index) => ({\n ...entry,\n sortOrder: String((index + 1) * 10),\n }));\n}\n\nfunction buildUnitOptions(\n entries: UnitDictionaryEntry[] | undefined,\n): UnitOption[] {\n const list = Array.isArray(entries) ? entries : [];\n const options = list\n .map((entry) => {\n const value = toTrimmedOrNull(entry.value);\n if (!value) return null;\n return {\n value,\n label: toTrimmedOrNull(entry.label) ?? value,\n } satisfies UnitOption;\n })\n .filter((entry): entry is UnitOption => Boolean(entry));\n return options.sort((left, right) => left.label.localeCompare(right.label));\n}\n\nexport function ProductUomSection({\n values,\n errors,\n setValue,\n embedded = false,\n}: ProductUomSectionProps) {\n const t = useT();\n const locale = useLocale();\n const { enabled: unitPriceDisplayEnabled } = useUnitPriceDisplayEnabled();\n const [unitOptions, setUnitOptions] = React.useState<UnitOption[]>([]);\n const [loadingUnits, setLoadingUnits] = React.useState(false);\n const [errorLoadingUnits, setErrorLoadingUnits] = React.useState(false);\n const conversions = React.useMemo(\n () => normalizeConversions(values.unitConversions, locale),\n [values.unitConversions, locale],\n );\n\n React.useEffect(() => {\n let cancelled = false;\n async function loadUnits() {\n setLoadingUnits(true);\n setErrorLoadingUnits(false);\n try {\n const response = await apiCall<UnitDictionaryResponse>(\n \"/api/catalog/dictionaries/unit\",\n undefined,\n { fallback: { entries: [] } },\n );\n if (cancelled) return;\n setUnitOptions(buildUnitOptions(response.result?.entries));\n } catch {\n if (!cancelled) {\n setUnitOptions([]);\n setErrorLoadingUnits(true);\n }\n } finally {\n if (!cancelled) setLoadingUnits(false);\n }\n }\n void loadUnits();\n return () => {\n cancelled = true;\n };\n }, []);\n\n const findUnitLabel = React.useCallback(\n (value: string | null | undefined) => {\n const code = toTrimmedOrNull(value);\n if (!code) return null;\n const option = unitOptions.find((entry) => entry.value === code);\n return option?.label ?? code;\n },\n [unitOptions],\n );\n\n const setConversions = React.useCallback(\n (next: ProductUnitConversionDraft[]) => {\n const normalized = next.map((entry, index) => ({\n ...entry,\n sortOrder: String((index + 1) * 10),\n }));\n setValue(\"unitConversions\", normalized);\n },\n [setValue],\n );\n\n const addConversion = React.useCallback(() => {\n const next = [\n ...conversions,\n createProductUnitConversionDraft({\n sortOrder: String((conversions.length + 1) * 10),\n }),\n ];\n setConversions(next);\n }, [conversions, setConversions]);\n\n const updateConversion = React.useCallback(\n (index: number, patch: Partial<ProductUnitConversionDraft>) => {\n const next = conversions.map((entry, rowIndex) =>\n rowIndex === index ? { ...entry, ...patch } : entry,\n );\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const removeConversion = React.useCallback(\n (index: number) => {\n const next = conversions.filter((_entry, rowIndex) => rowIndex !== index);\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const moveConversion = React.useCallback(\n (index: number, direction: \"up\" | \"down\") => {\n const targetIndex = direction === \"up\" ? index - 1 : index + 1;\n if (targetIndex < 0 || targetIndex >= conversions.length) return;\n const next = [...conversions];\n const source = next[index];\n next[index] = next[targetIndex];\n next[targetIndex] = source;\n setConversions(next);\n },\n [conversions, setConversions],\n );\n\n const defaultUnit = toTrimmedOrNull(values.defaultUnit) ?? \"\";\n const defaultSalesUnit = toTrimmedOrNull(values.defaultSalesUnit) ?? \"\";\n const defaultSalesQuantityRaw =\n toTrimmedOrNull(values.defaultSalesUnitQuantity) ?? \"1\";\n const defaultSalesQuantity = normalizeDecimalInput(defaultSalesQuantityRaw, locale);\n const unitPriceEnabled = Boolean(values.unitPriceEnabled);\n const unitPriceReferenceUnit =\n toTrimmedOrNull(values.unitPriceReferenceUnit) ?? \"\";\n const unitPriceBaseQuantityRaw =\n toTrimmedOrNull(values.unitPriceBaseQuantity) ?? \"\";\n const unitPriceBaseQuantity = normalizeDecimalInput(unitPriceBaseQuantityRaw, locale);\n\n const baseUnitLabel = findUnitLabel(defaultUnit) ?? defaultUnit;\n const salesUnitLabel =\n findUnitLabel(defaultSalesUnit || defaultUnit) ??\n defaultSalesUnit ??\n defaultUnit;\n\n const defaultSalesFactor = React.useMemo(() => {\n const defaultUnitKey = defaultUnit.toLowerCase();\n const defaultSalesKey = (defaultSalesUnit || defaultUnit).toLowerCase();\n if (!defaultUnitKey || !defaultSalesKey) return null;\n if (defaultSalesKey === defaultUnitKey) return 1;\n const row = conversions.find(\n (entry) =>\n entry.isActive &&\n entry.unitCode.toLowerCase() === defaultSalesKey &&\n toPositiveNumber(entry.toBaseFactor, locale) !== null,\n );\n return row ? toPositiveNumber(row.toBaseFactor, locale) : null;\n }, [conversions, defaultSalesUnit, defaultUnit, locale]);\n\n const defaultSalesQuantityNumber = toPositiveNumber(defaultSalesQuantity, locale);\n const defaultSalesQuantityNormalized =\n defaultSalesQuantityNumber && defaultSalesFactor\n ? defaultSalesQuantityNumber * defaultSalesFactor\n : null;\n const unitPriceBaseQuantityNumber = toPositiveNumber(unitPriceBaseQuantity, locale);\n\n const validConversions = conversions.filter(\n (entry) =>\n toTrimmedOrNull(entry.unitCode) && toTrimmedOrNull(entry.toBaseFactor),\n );\n const conversionPreviewItems = validConversions\n .slice(0, 3)\n .map((entry) => {\n const label = findUnitLabel(entry.unitCode) ?? entry.unitCode;\n const baseLabel = findUnitLabel(defaultUnit) ?? defaultUnit;\n const factor = toTrimmedOrNull(entry.toBaseFactor) ?? \"1\";\n return `1 ${label} = ${factor} ${baseLabel || t(\"catalog.products.uom.baseUnit\", \"base unit\")}`;\n });\n const conversionPreview =\n validConversions.length > 3\n ? `${conversionPreviewItems.join(\" \u2022 \")} (+${validConversions.length - 3})`\n : conversionPreviewItems.join(\" \u2022 \");\n\n return (\n <div\n className={\n embedded ? \"space-y-5\" : \"space-y-5 rounded-lg border bg-card p-4\"\n }\n >\n <div className=\"space-y-1\">\n <h3 className=\"text-sm font-semibold\">\n {t(\"catalog.products.uom.title\", \"Units of measure\")}\n </h3>\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.description\",\n \"Set base unit, sales unit, and packaging conversions.\",\n )}\n </p>\n </div>\n\n {loadingUnits ? (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground\">\n <Spinner className=\"h-3 w-3\" />\n {t(\"catalog.products.uom.loadingUnits\", \"Loading units...\")}\n </div>\n ) : null}\n\n {errorLoadingUnits ? (\n <p className=\"text-xs text-destructive\">\n {t(\n \"catalog.products.uom.errors.loadUnits\",\n \"Failed to load units. Please try refreshing the page.\",\n )}\n </p>\n ) : null}\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-base-unit\">{t(\"catalog.products.uom.baseUnit\", \"Base unit\")}</Label>\n <Select\n value={defaultUnit || undefined}\n onValueChange={(value) => setValue(\"defaultUnit\", value || null)}\n disabled={loadingUnits}\n >\n <SelectTrigger id=\"catalog-product-uom-base-unit\">\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {errors.defaultUnit ? (\n <p className=\"text-xs text-destructive\">{errors.defaultUnit}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-sales-unit\">\n {t(\"catalog.products.uom.defaultSalesUnit\", \"Default sales unit\")}\n </Label>\n <Select\n value={defaultSalesUnit || undefined}\n onValueChange={(value) => setValue(\"defaultSalesUnit\", value || null)}\n disabled={loadingUnits}\n >\n <SelectTrigger id=\"catalog-product-uom-sales-unit\">\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n {errors.defaultSalesUnit ? (\n <p className=\"text-xs text-destructive\">{errors.defaultSalesUnit}</p>\n ) : null}\n </div>\n\n <div className=\"space-y-2 md:col-span-2\">\n <Label htmlFor=\"catalog-product-uom-default-sales-quantity\">\n {t(\n \"catalog.products.uom.defaultSalesQuantityLabel\",\n \"Default line quantity (in sales unit)\",\n )}\n </Label>\n <Input\n id=\"catalog-product-uom-default-sales-quantity\"\n type=\"text\"\n inputMode=\"decimal\"\n value={defaultSalesQuantity}\n onChange={(event) =>\n setValue(\n \"defaultSalesUnitQuantity\",\n normalizeDecimalInput(event.target.value, locale),\n )\n }\n placeholder=\"1\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.defaultSalesQuantityHint\",\n \"Used to prefill quantity in quote/order lines. Value is interpreted in Default sales unit.\",\n )}\n </p>\n {defaultSalesQuantityNumber && salesUnitLabel ? (\n <p className=\"text-xs text-muted-foreground\">\n {defaultSalesQuantityNormalized && baseUnitLabel\n ? t(\n \"catalog.products.uom.defaultSalesQuantityPreviewWithNormalization\",\n \"Default line: {{quantity}} {{salesUnit}} (= {{normalized}} {{baseUnit}}).\",\n {\n quantity: formatPreviewNumber(defaultSalesQuantityNumber),\n salesUnit: salesUnitLabel,\n normalized: formatPreviewNumber(\n defaultSalesQuantityNormalized,\n ),\n baseUnit: baseUnitLabel,\n },\n )\n : t(\n \"catalog.products.uom.defaultSalesQuantityPreview\",\n \"Default line: {{quantity}} {{salesUnit}}.\",\n {\n quantity: formatPreviewNumber(defaultSalesQuantityNumber),\n salesUnit: salesUnitLabel,\n },\n )}\n </p>\n ) : null}\n {errors.defaultSalesUnitQuantity ? (\n <p className=\"text-xs text-destructive\">\n {errors.defaultSalesUnitQuantity}\n </p>\n ) : null}\n </div>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-rounding-mode\">\n {t(\"catalog.products.uom.roundingMode\", \"Rounding mode\")}\n </Label>\n <Select\n value={values.uomRoundingMode ?? \"half_up\"}\n onValueChange={(value) => setValue(\"uomRoundingMode\", value)}\n >\n <SelectTrigger id=\"catalog-product-uom-rounding-mode\">\n <SelectValue />\n </SelectTrigger>\n <SelectContent>\n <SelectItem value=\"half_up\">\n {t(\"catalog.products.uom.roundingModeHalfUp\", \"Half up (default)\")}\n </SelectItem>\n <SelectItem value=\"down\">\n {t(\"catalog.products.uom.roundingModeDown\", \"Round down\")}\n </SelectItem>\n <SelectItem value=\"up\">\n {t(\"catalog.products.uom.roundingModeUp\", \"Round up\")}\n </SelectItem>\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-rounding-scale\">\n {t(\"catalog.products.uom.roundingScale\", \"Rounding scale (decimal places)\")}\n </Label>\n <Input\n id=\"catalog-product-uom-rounding-scale\"\n type=\"number\"\n min={0}\n max={6}\n value={values.uomRoundingScale ?? 4}\n onChange={(event) => {\n const parsed = Number.parseInt(event.target.value, 10);\n setValue(\"uomRoundingScale\", Number.isFinite(parsed) ? Math.max(0, Math.min(6, parsed)) : 4);\n }}\n />\n </div>\n </div>\n\n {unitPriceDisplayEnabled ? (\n <div className=\"space-y-3\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"catalog-product-unit-price-enabled\"\n checked={unitPriceEnabled}\n onCheckedChange={(checked) =>\n setValue(\"unitPriceEnabled\", checked === true)\n }\n />\n <Label\n htmlFor=\"catalog-product-unit-price-enabled\"\n className=\"text-sm\"\n >\n {t(\n \"catalog.products.unitPrice.enable\",\n \"Enable EU unit price display\",\n )}\n </Label>\n </div>\n\n {unitPriceEnabled ? (\n <div className=\"grid gap-3 md:grid-cols-2\">\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-reference-unit\">\n {t(\n \"catalog.products.unitPrice.referenceUnit\",\n \"Reference unit\",\n )}\n </Label>\n <Select\n value={unitPriceReferenceUnit || undefined}\n onValueChange={(value) => setValue(\"unitPriceReferenceUnit\", value || null)}\n >\n <SelectTrigger id=\"catalog-product-uom-reference-unit\">\n <SelectValue\n placeholder={t(\n \"catalog.products.unitPrice.selectReferenceUnit\",\n \"Select reference unit\",\n )}\n />\n </SelectTrigger>\n <SelectContent>\n {REFERENCE_UNIT_OPTIONS.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {t(option.i18nKey, option.fallback)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n <div className=\"space-y-2\">\n <Label htmlFor=\"catalog-product-uom-unit-price-base-quantity\">\n {t(\n \"catalog.products.unitPrice.baseQuantity\",\n \"Reference quantity (in base unit)\",\n )}\n </Label>\n <Input\n id=\"catalog-product-uom-unit-price-base-quantity\"\n type=\"text\"\n inputMode=\"decimal\"\n value={unitPriceBaseQuantity}\n onChange={(event) =>\n setValue(\n \"unitPriceBaseQuantity\",\n normalizeDecimalInput(event.target.value, locale),\n )\n }\n placeholder=\"1\"\n />\n </div>\n </div>\n ) : null}\n {unitPriceEnabled ? (\n <p className=\"text-xs text-muted-foreground\">\n {unitPriceReferenceUnit && unitPriceBaseQuantityNumber\n ? t(\n \"catalog.products.unitPrice.hintWithPreview\",\n \"Show calculated price per {{quantity}} {{unit}}. For most products use 1 (for example: 1 kg, 1 l, 1 m\u00B2).\",\n {\n quantity: formatPreviewNumber(unitPriceBaseQuantityNumber),\n unit: unitPriceReferenceUnit,\n },\n )\n : t(\n \"catalog.products.unitPrice.hint\",\n \"Show calculated price per selected reference unit. In most cases set quantity to 1.\",\n )}\n </p>\n ) : null}\n </div>\n ) : null}\n\n <div className=\"space-y-3\">\n <div className=\"flex items-center justify-between gap-2\">\n <Label className=\"text-sm\">\n {t(\"catalog.products.uom.conversions\", \"Product conversions\")}\n </Label>\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={addConversion}\n >\n <Plus className=\"mr-1.5 h-4 w-4\" />\n {t(\"catalog.products.uom.addConversion\", \"Add conversion\")}\n </Button>\n </div>\n\n {conversions.length === 0 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.emptyConversions\",\n \"No conversions configured yet.\",\n )}\n </p>\n ) : (\n <div className=\"space-y-2\">\n {conversions.map((entry, index) => {\n const conversionFactor = toPositiveNumber(entry.toBaseFactor, locale);\n const conversionPreviewText =\n entry.unitCode && conversionFactor !== null\n ? t(\n \"catalog.products.uom.conversionPreview\",\n \"1 {{fromUnit}} = {{factor}} {{baseUnit}}\",\n {\n fromUnit: findUnitLabel(entry.unitCode) ?? entry.unitCode,\n factor: formatPreviewNumber(conversionFactor),\n baseUnit:\n findUnitLabel(defaultUnit) ??\n defaultUnit ??\n t(\"catalog.products.uom.baseUnit\", \"base unit\"),\n },\n )\n : null;\n\n return (\n <div\n key={entry.id ?? `uom-conversion-${index}`}\n className=\"grid gap-3 rounded-md border p-3 md:grid-cols-12\"\n >\n <div className=\"space-y-1 md:col-span-4\">\n <Label htmlFor={`catalog-product-uom-conversion-unit-${index}`} className=\"text-xs text-muted-foreground\">\n {t(\"catalog.products.uom.conversionUnit\", \"Sales unit\")}\n </Label>\n <Select\n value={entry.unitCode || undefined}\n onValueChange={(value) => updateConversion(index, { unitCode: value ?? '' })}\n disabled={loadingUnits}\n >\n <SelectTrigger id={`catalog-product-uom-conversion-unit-${index}`}>\n <SelectValue placeholder={t(\"catalog.products.uom.selectUnit\", \"Select unit\")} />\n </SelectTrigger>\n <SelectContent>\n {unitOptions.map((option) => (\n <SelectItem key={option.value} value={option.value}>\n {option.label}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </div>\n\n <div className=\"space-y-1 md:col-span-3\">\n <Label htmlFor={`catalog-product-uom-conversion-factor-${index}`} className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.toBaseFactor\",\n \"Base units per 1 sales unit\",\n )}\n </Label>\n <Input\n id={`catalog-product-uom-conversion-factor-${index}`}\n type=\"text\"\n inputMode=\"decimal\"\n value={entry.toBaseFactor}\n onChange={(event) =>\n updateConversion(index, {\n toBaseFactor: normalizeDecimalInput(event.target.value, locale),\n })\n }\n placeholder=\"1\"\n />\n </div>\n\n <div className=\"flex items-end gap-2 md:col-span-3\">\n <div className=\"inline-flex h-9 rounded-md border\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 rounded-none border-r\"\n onClick={() => moveConversion(index, \"up\")}\n disabled={index === 0}\n aria-label={t(\n \"catalog.products.uom.moveUp\",\n \"Move conversion up\",\n )}\n >\n <ArrowUp className=\"h-4 w-4\" />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"h-9 w-9 rounded-none\"\n onClick={() => moveConversion(index, \"down\")}\n disabled={index === conversions.length - 1}\n aria-label={t(\n \"catalog.products.uom.moveDown\",\n \"Move conversion down\",\n )}\n >\n <ArrowDown className=\"h-4 w-4\" />\n </Button>\n </div>\n <label\n htmlFor={`catalog-product-uom-conversion-active-${index}`}\n className=\"flex items-center gap-2 px-1 text-xs text-muted-foreground\"\n >\n <Checkbox\n id={`catalog-product-uom-conversion-active-${index}`}\n checked={entry.isActive}\n onCheckedChange={(checked) =>\n updateConversion(index, { isActive: checked === true })\n }\n />\n {t(\"catalog.products.uom.active\", \"Active\")}\n </label>\n </div>\n\n <div className=\"flex items-end justify-end md:col-span-2\">\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon\"\n className=\"text-destructive\"\n onClick={() => removeConversion(index)}\n aria-label={t(\n \"catalog.products.uom.removeConversion\",\n \"Remove conversion\",\n )}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n\n {conversionPreviewText && (\n <p className=\"text-xs text-muted-foreground md:col-span-12\">\n {conversionPreviewText}\n </p>\n )}\n </div>\n );\n })}\n </div>\n )}\n\n {conversions.length > 1 ? (\n <p className=\"text-xs text-muted-foreground\">\n {t(\n \"catalog.products.uom.conversionOrderHint\",\n \"Use arrows to reorder conversion priority.\",\n )}\n </p>\n ) : null}\n\n {conversionPreview ? (\n <p className=\"text-xs text-muted-foreground\">{conversionPreview}</p>\n ) : null}\n </div>\n </div>\n );\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
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 { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
9
8
|
import { ActivitiesDayStrip } from "./ActivitiesDayStrip.js";
|
|
10
9
|
import { ActivitiesAddNewMenu } from "./ActivitiesAddNewMenu.js";
|
|
11
10
|
import { isOpenInteractionStatus } from "../../lib/interactionStatus.js";
|
|
11
|
+
import { isSameDay, toLocalZonedDate } from "../../lib/localDay.js";
|
|
12
12
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
13
13
|
const logger = createLogger("customers");
|
|
14
14
|
const TYPE_ICONS = {
|
|
@@ -17,24 +17,11 @@ const TYPE_ICONS = {
|
|
|
17
17
|
meeting: Users,
|
|
18
18
|
note: StickyNote
|
|
19
19
|
};
|
|
20
|
-
const USER_TIMEZONE = (() => {
|
|
21
|
-
try {
|
|
22
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
23
|
-
} catch {
|
|
24
|
-
return "UTC";
|
|
25
|
-
}
|
|
26
|
-
})();
|
|
27
|
-
function toLocalZonedDate(value) {
|
|
28
|
-
return toZonedTime(value, USER_TIMEZONE);
|
|
29
|
-
}
|
|
30
20
|
function startOfDay(date) {
|
|
31
21
|
const next = new Date(date);
|
|
32
22
|
next.setHours(0, 0, 0, 0);
|
|
33
23
|
return next;
|
|
34
24
|
}
|
|
35
|
-
function isSameDay(a, b) {
|
|
36
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
37
|
-
}
|
|
38
25
|
function isOverdue(activity, now) {
|
|
39
26
|
const scheduled = activity.scheduledAt ?? activity.occurredAt;
|
|
40
27
|
if (!scheduled) return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../src/modules/customers/components/detail/ActivitiesCard.tsx"],
|
|
4
|
-
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'\nimport { toZonedTime } from 'date-fns-tz'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ActivitiesDayStrip } from './ActivitiesDayStrip'\nimport { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'\nimport type { InteractionSummary } from './types'\nimport { isOpenInteractionStatus } from '../../lib/interactionStatus'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesCardProps {\n entityId: string\n /**\n * Initial planned activities (from the parent route's `plannedActivitiesPreview`).\n * Used as the seed value before the broader `/api/customers/interactions` fetch\n * resolves, and as the fallback when the fetch fails. The card always prefers\n * its own fetched window (issue #1809 \u2014 fixes E1 status alignment and E2 type\n * coverage by sourcing from the same endpoint as the day strip rather than the\n * 5-item server preview that excluded most types in practice).\n */\n plannedActivities: InteractionSummary[]\n refreshKey?: number\n onAddNew: (kind: ActivityKind) => void\n onEditActivity?: (activity: InteractionSummary) => void\n /**\n * Optional company name for the parent entity. When the planned activity has no `dealTitle`,\n * the row subtitle falls back to \"{type} \u00B7 {company}\" to mirror Figma 784:809.\n */\n entityCompanyName?: string | null\n}\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\nconst USER_TIMEZONE = (() => {\n try {\n return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'\n } catch {\n return 'UTC'\n }\n})()\n\n// Project a UTC instant to the user's local timezone before extracting day/month/year\n// for \"same day\" comparisons (issue #1809 \u2014 E3 timezone drift).\nfunction toLocalZonedDate(value: string | Date): Date {\n return toZonedTime(value, USER_TIMEZONE)\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction isSameDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()\n}\n\nfunction isOverdue(activity: InteractionSummary, now: Date): boolean {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n return date.getTime() < now.getTime() && isOpenInteractionStatus(activity.status)\n}\n\n// Visible window for the day-strip + activity list. Mirrors `VISIBLE_DAYS = 5`\n// in ActivitiesDayStrip with extra padding so navigation forward/back doesn't\n// race the fetch.\nconst FETCH_WINDOW_DAYS = 31\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction formatRelativeDay(date: Date, t: TranslateFn): string {\n const now = new Date()\n const today = startOfDay(now)\n const target = startOfDay(date)\n const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))\n if (diff === 0) return t('customers.timeline.date.today', 'today')\n if (diff === 1) return t('customers.timeline.date.tomorrow', 'tomorrow')\n if (diff === -1) return t('customers.timeline.date.yesterday', 'yesterday')\n return target.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n}\n\nfunction formatDuration(minutes: number, t: TranslateFn): string {\n if (minutes >= 60) {\n const hours = Math.round((minutes / 60) * 10) / 10\n return t('customers.activities.calendar.hoursShort', '{hours}h', { hours })\n }\n return t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes })\n}\n\nexport function ActivitiesCard({\n entityId,\n plannedActivities,\n refreshKey = 0,\n onAddNew,\n onEditActivity,\n entityCompanyName,\n}: ActivitiesCardProps) {\n const t = useT()\n const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))\n // Fetch the same broader window as the day strip via the canonical interactions\n // endpoint. This single source of truth aligns the day-strip count with the\n // visible event list (issue #1809 \u2014 E1) and surfaces all interaction types\n // (issue #1809 \u2014 E2: the previous reliance on the server-side 5-item preview\n // produced \"Person view shows only Calls\" because the limit happened to drop\n // every non-call entry from the prefix-window).\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[] | null>(null)\n\n React.useEffect(() => {\n if (!entityId) {\n setFetchedEvents(null)\n return\n }\n const controller = new AbortController()\n const today = startOfDay(new Date())\n const fromDate = new Date(today)\n fromDate.setDate(today.getDate() - FETCH_WINDOW_DAYS)\n const toDate = new Date(today)\n toDate.setDate(today.getDate() + FETCH_WINDOW_DAYS)\n toDate.setHours(23, 59, 59, 999)\n const params = new URLSearchParams({\n entityId,\n from: fromDate.toISOString(),\n to: toDate.toISOString(),\n // Server caps at 100 (interactions querySchema). 100 is well above what\n // an active CRM record accumulates in a 31-day window of meetings/calls,\n // and the day strip + list naturally degrade to truncation if exceeded.\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name !== 'AbortError') {\n logger.warn('failed to load interactions', { component: 'ActivitiesCard', err })\n setFetchedEvents(null)\n }\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey])\n\n // Prefer the broader fetch when it has resolved; fall back to the seed prop\n // (route-supplied preview) only while the fetch is in flight or after a\n // hard failure. This guarantees that the rare prop-only render path keeps\n // backwards-compat with existing unit tests while live UI uses the broader fetch.\n const effectiveEvents: InteractionSummary[] = fetchedEvents ?? plannedActivities\n\n const eventsForSelectedDay = React.useMemo(() => {\n const items = effectiveEvents.filter((activity) => {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n // Compare in the user's local timezone so a 23:30 local activity stays\n // on its local-day chip instead of bleeding into the next UTC day\n // (issue #1809 \u2014 E3).\n return isSameDay(toLocalZonedDate(scheduled), selectedDate)\n })\n return items.sort((left, right) => {\n const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()\n const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()\n return leftTime - rightTime\n })\n }, [effectiveEvents, selectedDate])\n\n const overdueCount = React.useMemo(() => {\n const now = new Date()\n return effectiveEvents.filter((activity) => isOverdue(activity, now)).length\n }, [effectiveEvents])\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"size-4 text-foreground\" />\n <h3 className=\"text-sm font-semibold leading-none text-foreground\">\n {t('customers.activities.card.title', 'Activities')}\n </h3>\n {overdueCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg px-1.5 py-0.5 text-xs font-medium text-status-error-text\">\n <CalendarClock className=\"size-3\" />\n {t('customers.activities.card.overdue', '{count} overdue', { count: overdueCount })}\n </span>\n ) : null}\n </div>\n <ActivitiesAddNewMenu onSelect={onAddNew} />\n </div>\n\n <ActivitiesDayStrip\n entityId={entityId}\n selectedDate={selectedDate}\n onSelectDate={setSelectedDate}\n refreshKey={refreshKey}\n events={fetchedEvents ?? undefined}\n />\n\n {eventsForSelectedDay.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border\" />\n <ul className=\"flex flex-col\">\n {eventsForSelectedDay.map((activity) => (\n <PlannedEventRow\n key={activity.id}\n activity={activity}\n onClick={onEditActivity}\n entityCompanyName={entityCompanyName ?? null}\n t={t}\n />\n ))}\n </ul>\n </>\n ) : (\n <>\n <div className=\"h-px w-full bg-border\" />\n <p className=\"px-1 py-2 text-xs text-muted-foreground\">\n {t('customers.activities.card.empty', 'Nothing scheduled for this day.')}\n </p>\n </>\n )}\n </div>\n )\n}\n\ninterface PlannedEventRowProps {\n activity: InteractionSummary\n onClick?: (activity: InteractionSummary) => void\n entityCompanyName: string | null\n t: TranslateFn\n}\n\nfunction PlannedEventRow({ activity, onClick, entityCompanyName, t }: PlannedEventRowProps) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const date = new Date(dateStr)\n const validDate = !Number.isNaN(date.getTime())\n const Icon = TYPE_ICONS[activity.interactionType] ?? Users\n const duration = typeof activity.duration === 'number' && activity.duration > 0 ? activity.duration : null\n const overdue = validDate && date.getTime() < Date.now() && isOpenInteractionStatus(activity.status)\n const typeLabel = labelForType(activity.interactionType, t)\n const subtitleSuffix = activity.dealTitle ?? entityCompanyName ?? null\n const subtitle = subtitleSuffix ? `${typeLabel} \u00B7 ${subtitleSuffix}` : typeLabel\n const interactive = !!onClick\n\n return (\n <li>\n <button\n type=\"button\"\n onClick={interactive ? () => onClick?.(activity) : undefined}\n disabled={!interactive}\n className={cn(\n 'flex w-full items-start gap-[9px] pt-[8px] text-left transition-colors',\n interactive ? 'cursor-pointer rounded-md hover:bg-accent/30 px-1' : 'px-1',\n )}\n >\n <div className=\"flex h-[44px] w-[43px] shrink-0 flex-col gap-[2px] pt-[2px]\">\n <span className=\"text-xs font-semibold leading-none text-foreground\">\n {validDate ? formatTime(date) : ''}\n </span>\n <span className=\"text-[10px] leading-none font-normal text-muted-foreground\">\n {validDate ? formatRelativeDay(date, t) : ''}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center justify-center rounded-full bg-muted border-4 border-background size-7\">\n <Icon className=\"size-4 text-muted-foreground\" />\n </div>\n <div className=\"min-w-0 flex flex-1 flex-col gap-[4px]\">\n <span className=\"text-sm leading-5 tracking-[-0.084px] text-foreground\">\n {activity.title ?? activity.body ?? labelForType(activity.interactionType, t)}\n </span>\n {duration ? (\n <span className={cn(\n 'inline-flex w-fit items-center gap-[2px] rounded-full pl-[4px] pr-[8px] py-[2px] text-xs font-medium leading-[16px]',\n overdue\n ? 'bg-status-error-bg text-status-error-text'\n : 'bg-status-warning-bg text-status-warning-text',\n )}>\n <Clock className=\"size-4\" />\n {formatDuration(duration, t)}\n </span>\n ) : null}\n <span className=\"text-[11px] font-normal text-muted-foreground\">{subtitle}</span>\n </div>\n </button>\n </li>\n )\n}\n\nfunction labelForType(type: string, t: TranslateFn): string {\n const map: Record<string, [string, string]> = {\n meeting: ['customers.timeline.filter.meeting', 'Meeting'],\n call: ['customers.timeline.filter.call', 'Call'],\n email: ['customers.timeline.filter.email', 'Email'],\n note: ['customers.timeline.filter.note', 'Note'],\n task: ['customers.timeline.filter.task', 'Task'],\n }\n const entry = map[type]\n return entry ? t(entry[0], entry[1]) : type\n}\n\nexport default ActivitiesCard\n"],
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { Calendar, CalendarClock, Clock, Mail, Phone, StickyNote, Users } from 'lucide-react'\nimport { cn } from '@open-mercato/shared/lib/utils'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport type { TranslateFn } from '@open-mercato/shared/lib/i18n/context'\nimport { readApiResultOrThrow } from '@open-mercato/ui/backend/utils/apiCall'\nimport { ActivitiesDayStrip } from './ActivitiesDayStrip'\nimport { ActivitiesAddNewMenu, type ActivityKind } from './ActivitiesAddNewMenu'\nimport type { InteractionSummary } from './types'\nimport { isOpenInteractionStatus } from '../../lib/interactionStatus'\nimport { isSameDay, toLocalZonedDate } from '../../lib/localDay'\nimport { createLogger } from '@open-mercato/shared/lib/logger'\n\nconst logger = createLogger('customers')\n\ninterface ActivitiesCardProps {\n entityId: string\n /**\n * Initial planned activities (from the parent route's `plannedActivitiesPreview`).\n * Used as the seed value before the broader `/api/customers/interactions` fetch\n * resolves, and as the fallback when the fetch fails. The card always prefers\n * its own fetched window (issue #1809 \u2014 fixes E1 status alignment and E2 type\n * coverage by sourcing from the same endpoint as the day strip rather than the\n * 5-item server preview that excluded most types in practice).\n */\n plannedActivities: InteractionSummary[]\n refreshKey?: number\n onAddNew: (kind: ActivityKind) => void\n onEditActivity?: (activity: InteractionSummary) => void\n /**\n * Optional company name for the parent entity. When the planned activity has no `dealTitle`,\n * the row subtitle falls back to \"{type} \u00B7 {company}\" to mirror Figma 784:809.\n */\n entityCompanyName?: string | null\n}\n\nconst TYPE_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {\n call: Phone,\n email: Mail,\n meeting: Users,\n note: StickyNote,\n}\n\nfunction startOfDay(date: Date): Date {\n const next = new Date(date)\n next.setHours(0, 0, 0, 0)\n return next\n}\n\nfunction isOverdue(activity: InteractionSummary, now: Date): boolean {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n return date.getTime() < now.getTime() && isOpenInteractionStatus(activity.status)\n}\n\n// Visible window for the day-strip + activity list. Mirrors `VISIBLE_DAYS = 5`\n// in ActivitiesDayStrip with extra padding so navigation forward/back doesn't\n// race the fetch.\nconst FETCH_WINDOW_DAYS = 31\n\nfunction formatTime(date: Date): string {\n return date.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' })\n}\n\nfunction formatRelativeDay(date: Date, t: TranslateFn): string {\n const now = new Date()\n const today = startOfDay(now)\n const target = startOfDay(date)\n const diff = Math.round((target.getTime() - today.getTime()) / (1000 * 60 * 60 * 24))\n if (diff === 0) return t('customers.timeline.date.today', 'today')\n if (diff === 1) return t('customers.timeline.date.tomorrow', 'tomorrow')\n if (diff === -1) return t('customers.timeline.date.yesterday', 'yesterday')\n return target.toLocaleDateString(undefined, { day: 'numeric', month: 'short' })\n}\n\nfunction formatDuration(minutes: number, t: TranslateFn): string {\n if (minutes >= 60) {\n const hours = Math.round((minutes / 60) * 10) / 10\n return t('customers.activities.calendar.hoursShort', '{hours}h', { hours })\n }\n return t('customers.activities.calendar.minutesShort', '{minutes}m', { minutes })\n}\n\nexport function ActivitiesCard({\n entityId,\n plannedActivities,\n refreshKey = 0,\n onAddNew,\n onEditActivity,\n entityCompanyName,\n}: ActivitiesCardProps) {\n const t = useT()\n const [selectedDate, setSelectedDate] = React.useState<Date>(() => startOfDay(new Date()))\n // Fetch the same broader window as the day strip via the canonical interactions\n // endpoint. This single source of truth aligns the day-strip count with the\n // visible event list (issue #1809 \u2014 E1) and surfaces all interaction types\n // (issue #1809 \u2014 E2: the previous reliance on the server-side 5-item preview\n // produced \"Person view shows only Calls\" because the limit happened to drop\n // every non-call entry from the prefix-window).\n const [fetchedEvents, setFetchedEvents] = React.useState<InteractionSummary[] | null>(null)\n\n React.useEffect(() => {\n if (!entityId) {\n setFetchedEvents(null)\n return\n }\n const controller = new AbortController()\n const today = startOfDay(new Date())\n const fromDate = new Date(today)\n fromDate.setDate(today.getDate() - FETCH_WINDOW_DAYS)\n const toDate = new Date(today)\n toDate.setDate(today.getDate() + FETCH_WINDOW_DAYS)\n toDate.setHours(23, 59, 59, 999)\n const params = new URLSearchParams({\n entityId,\n from: fromDate.toISOString(),\n to: toDate.toISOString(),\n // Server caps at 100 (interactions querySchema). 100 is well above what\n // an active CRM record accumulates in a 31-day window of meetings/calls,\n // and the day strip + list naturally degrade to truncation if exceeded.\n limit: '100',\n sortField: 'scheduledAt',\n sortDir: 'asc',\n excludeInteractionType: 'task',\n })\n void (async () => {\n try {\n const payload = await readApiResultOrThrow<{ items?: InteractionSummary[] }>(\n `/api/customers/interactions?${params.toString()}`,\n { signal: controller.signal },\n )\n setFetchedEvents(Array.isArray(payload?.items) ? payload.items : [])\n } catch (err) {\n if ((err as { name?: string } | null)?.name !== 'AbortError') {\n logger.warn('failed to load interactions', { component: 'ActivitiesCard', err })\n setFetchedEvents(null)\n }\n }\n })()\n return () => controller.abort()\n }, [entityId, refreshKey])\n\n // Prefer the broader fetch when it has resolved; fall back to the seed prop\n // (route-supplied preview) only while the fetch is in flight or after a\n // hard failure. This guarantees that the rare prop-only render path keeps\n // backwards-compat with existing unit tests while live UI uses the broader fetch.\n const effectiveEvents: InteractionSummary[] = fetchedEvents ?? plannedActivities\n\n const eventsForSelectedDay = React.useMemo(() => {\n const items = effectiveEvents.filter((activity) => {\n const scheduled = activity.scheduledAt ?? activity.occurredAt\n if (!scheduled) return false\n const date = new Date(scheduled)\n if (Number.isNaN(date.getTime())) return false\n // Compare in the user's local timezone so a 23:30 local activity stays\n // on its local-day chip instead of bleeding into the next UTC day\n // (issue #1809 \u2014 E3).\n return isSameDay(toLocalZonedDate(scheduled), selectedDate)\n })\n return items.sort((left, right) => {\n const leftTime = new Date(left.scheduledAt ?? left.occurredAt ?? left.createdAt).getTime()\n const rightTime = new Date(right.scheduledAt ?? right.occurredAt ?? right.createdAt).getTime()\n return leftTime - rightTime\n })\n }, [effectiveEvents, selectedDate])\n\n const overdueCount = React.useMemo(() => {\n const now = new Date()\n return effectiveEvents.filter((activity) => isOverdue(activity, now)).length\n }, [effectiveEvents])\n\n return (\n <div className=\"flex flex-col gap-3 rounded-lg border border-border bg-card pt-4 pb-4 px-4\">\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Calendar className=\"size-4 text-foreground\" />\n <h3 className=\"text-sm font-semibold leading-none text-foreground\">\n {t('customers.activities.card.title', 'Activities')}\n </h3>\n {overdueCount > 0 ? (\n <span className=\"inline-flex items-center gap-1 rounded-full bg-status-error-bg px-1.5 py-0.5 text-xs font-medium text-status-error-text\">\n <CalendarClock className=\"size-3\" />\n {t('customers.activities.card.overdue', '{count} overdue', { count: overdueCount })}\n </span>\n ) : null}\n </div>\n <ActivitiesAddNewMenu onSelect={onAddNew} />\n </div>\n\n <ActivitiesDayStrip\n entityId={entityId}\n selectedDate={selectedDate}\n onSelectDate={setSelectedDate}\n refreshKey={refreshKey}\n events={fetchedEvents ?? undefined}\n />\n\n {eventsForSelectedDay.length > 0 ? (\n <>\n <div className=\"h-px w-full bg-border\" />\n <ul className=\"flex flex-col\">\n {eventsForSelectedDay.map((activity) => (\n <PlannedEventRow\n key={activity.id}\n activity={activity}\n onClick={onEditActivity}\n entityCompanyName={entityCompanyName ?? null}\n t={t}\n />\n ))}\n </ul>\n </>\n ) : (\n <>\n <div className=\"h-px w-full bg-border\" />\n <p className=\"px-1 py-2 text-xs text-muted-foreground\">\n {t('customers.activities.card.empty', 'Nothing scheduled for this day.')}\n </p>\n </>\n )}\n </div>\n )\n}\n\ninterface PlannedEventRowProps {\n activity: InteractionSummary\n onClick?: (activity: InteractionSummary) => void\n entityCompanyName: string | null\n t: TranslateFn\n}\n\nfunction PlannedEventRow({ activity, onClick, entityCompanyName, t }: PlannedEventRowProps) {\n const dateStr = activity.scheduledAt ?? activity.occurredAt ?? activity.createdAt\n const date = new Date(dateStr)\n const validDate = !Number.isNaN(date.getTime())\n const Icon = TYPE_ICONS[activity.interactionType] ?? Users\n const duration = typeof activity.duration === 'number' && activity.duration > 0 ? activity.duration : null\n const overdue = validDate && date.getTime() < Date.now() && isOpenInteractionStatus(activity.status)\n const typeLabel = labelForType(activity.interactionType, t)\n const subtitleSuffix = activity.dealTitle ?? entityCompanyName ?? null\n const subtitle = subtitleSuffix ? `${typeLabel} \u00B7 ${subtitleSuffix}` : typeLabel\n const interactive = !!onClick\n\n return (\n <li>\n <button\n type=\"button\"\n onClick={interactive ? () => onClick?.(activity) : undefined}\n disabled={!interactive}\n className={cn(\n 'flex w-full items-start gap-[9px] pt-[8px] text-left transition-colors',\n interactive ? 'cursor-pointer rounded-md hover:bg-accent/30 px-1' : 'px-1',\n )}\n >\n <div className=\"flex h-[44px] w-[43px] shrink-0 flex-col gap-[2px] pt-[2px]\">\n <span className=\"text-xs font-semibold leading-none text-foreground\">\n {validDate ? formatTime(date) : ''}\n </span>\n <span className=\"text-[10px] leading-none font-normal text-muted-foreground\">\n {validDate ? formatRelativeDay(date, t) : ''}\n </span>\n </div>\n <div className=\"flex shrink-0 items-center justify-center rounded-full bg-muted border-4 border-background size-7\">\n <Icon className=\"size-4 text-muted-foreground\" />\n </div>\n <div className=\"min-w-0 flex flex-1 flex-col gap-[4px]\">\n <span className=\"text-sm leading-5 tracking-[-0.084px] text-foreground\">\n {activity.title ?? activity.body ?? labelForType(activity.interactionType, t)}\n </span>\n {duration ? (\n <span className={cn(\n 'inline-flex w-fit items-center gap-[2px] rounded-full pl-[4px] pr-[8px] py-[2px] text-xs font-medium leading-[16px]',\n overdue\n ? 'bg-status-error-bg text-status-error-text'\n : 'bg-status-warning-bg text-status-warning-text',\n )}>\n <Clock className=\"size-4\" />\n {formatDuration(duration, t)}\n </span>\n ) : null}\n <span className=\"text-[11px] font-normal text-muted-foreground\">{subtitle}</span>\n </div>\n </button>\n </li>\n )\n}\n\nfunction labelForType(type: string, t: TranslateFn): string {\n const map: Record<string, [string, string]> = {\n meeting: ['customers.timeline.filter.meeting', 'Meeting'],\n call: ['customers.timeline.filter.call', 'Call'],\n email: ['customers.timeline.filter.email', 'Email'],\n note: ['customers.timeline.filter.note', 'Note'],\n task: ['customers.timeline.filter.task', 'Task'],\n }\n const entry = map[type]\n return entry ? t(entry[0], entry[1]) : type\n}\n\nexport default ActivitiesCard\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -2,22 +2,12 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
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 { readApiResultOrThrow } from "@open-mercato/ui/backend/utils/apiCall";
|
|
8
|
+
import { isSameDay, toLocalZonedDate } from "../../lib/localDay.js";
|
|
9
9
|
import { createLogger } from "@open-mercato/shared/lib/logger";
|
|
10
10
|
const logger = createLogger("customers");
|
|
11
|
-
const USER_TIMEZONE = (() => {
|
|
12
|
-
try {
|
|
13
|
-
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
|
|
14
|
-
} catch {
|
|
15
|
-
return "UTC";
|
|
16
|
-
}
|
|
17
|
-
})();
|
|
18
|
-
function toLocalZonedDate(value) {
|
|
19
|
-
return toZonedTime(value, USER_TIMEZONE);
|
|
20
|
-
}
|
|
21
11
|
const VISIBLE_DAYS = 5;
|
|
22
12
|
const BUSYNESS_SLOTS = 10;
|
|
23
13
|
const SLOT_START_HOUR = 7;
|
|
@@ -55,9 +45,6 @@ function endOfDay(date) {
|
|
|
55
45
|
next.setHours(23, 59, 59, 999);
|
|
56
46
|
return next;
|
|
57
47
|
}
|
|
58
|
-
function isSameDay(a, b) {
|
|
59
|
-
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
60
|
-
}
|
|
61
48
|
function isWeekend(date) {
|
|
62
49
|
const day = date.getDay();
|
|
63
50
|
return day === 0 || day === 6;
|