@ethisyscore/core-utils 1.94.0 → 1.95.0
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/dist/chunk-2F5FENXB.js +84 -0
- package/dist/chunk-2F5FENXB.js.map +1 -0
- package/dist/chunk-3LIIUXBE.js +14 -0
- package/dist/chunk-3LIIUXBE.js.map +1 -0
- package/dist/chunk-G2XGZH7P.cjs +16 -0
- package/dist/chunk-G2XGZH7P.cjs.map +1 -0
- package/dist/chunk-GAZLFZEL.js +105 -0
- package/dist/chunk-GAZLFZEL.js.map +1 -0
- package/dist/chunk-LT7I4JVK.cjs +121 -0
- package/dist/chunk-LT7I4JVK.cjs.map +1 -0
- package/dist/chunk-OEDAX6WN.js +121 -0
- package/dist/chunk-OEDAX6WN.js.map +1 -0
- package/dist/chunk-P5FSVRZO.cjs +127 -0
- package/dist/chunk-P5FSVRZO.cjs.map +1 -0
- package/dist/chunk-RLPXYKSM.cjs +23 -0
- package/dist/chunk-RLPXYKSM.cjs.map +1 -0
- package/dist/chunk-TN6DXBVD.js +252 -0
- package/dist/chunk-TN6DXBVD.js.map +1 -0
- package/dist/chunk-YNBKRMK3.cjs +290 -0
- package/dist/chunk-YNBKRMK3.cjs.map +1 -0
- package/dist/chunk-ZQCGDT7D.js +19 -0
- package/dist/chunk-ZQCGDT7D.js.map +1 -0
- package/dist/chunk-ZR7TABGT.cjs +94 -0
- package/dist/chunk-ZR7TABGT.cjs.map +1 -0
- package/dist/date/index.cjs +210 -397
- package/dist/date/index.cjs.map +1 -1
- package/dist/date/index.js +2 -349
- package/dist/date/index.js.map +1 -1
- package/dist/date/org-format.cjs +13 -151
- package/dist/date/org-format.cjs.map +1 -1
- package/dist/date/org-format.js +3 -141
- package/dist/date/org-format.js.map +1 -1
- package/dist/index.cjs +286 -600
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -539
- package/dist/index.js.map +1 -1
- package/dist/list/index.cjs +7 -11
- package/dist/list/index.cjs.map +1 -1
- package/dist/list/index.js +1 -12
- package/dist/list/index.js.map +1 -1
- package/dist/money/index.cjs +39 -51
- package/dist/money/index.cjs.map +1 -1
- package/dist/money/index.d.cts +129 -8
- package/dist/money/index.d.ts +129 -8
- package/dist/money/index.js +1 -49
- package/dist/money/index.js.map +1 -1
- package/dist/number/index.cjs +15 -18
- package/dist/number/index.cjs.map +1 -1
- package/dist/number/index.js +1 -17
- package/dist/number/index.js.map +1 -1
- package/dist/patch/index.cjs +22 -121
- package/dist/patch/index.cjs.map +1 -1
- package/dist/patch/index.js +1 -119
- package/dist/patch/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// src/money/orgCurrency.ts
|
|
2
|
+
var MONEY_FALLBACK_CURRENCY = "GBP";
|
|
3
|
+
var orgReportingCurrencyRef = null;
|
|
4
|
+
function setOrgReportingCurrency(currency) {
|
|
5
|
+
orgReportingCurrencyRef = currency;
|
|
6
|
+
}
|
|
7
|
+
function getOrgReportingCurrency() {
|
|
8
|
+
return orgReportingCurrencyRef;
|
|
9
|
+
}
|
|
10
|
+
function resolveOrgReportingCurrencyCode() {
|
|
11
|
+
return orgReportingCurrencyRef?.code ?? MONEY_FALLBACK_CURRENCY;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/money/format.ts
|
|
15
|
+
var MONEY_FALLBACK_LOCALE = "en-GB";
|
|
16
|
+
function formatMoney(amount, currency) {
|
|
17
|
+
const options = typeof currency === "string" ? { currencyCode: currency } : currency ?? {};
|
|
18
|
+
const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;
|
|
19
|
+
const orgCurrency = currencyCode === void 0 ? getOrgReportingCurrency() : null;
|
|
20
|
+
const effectiveCode = currencyCode ?? orgCurrency?.code ?? MONEY_FALLBACK_CURRENCY;
|
|
21
|
+
const effectiveDecimalPlaces = decimalPlaces !== void 0 ? decimalPlaces : orgCurrency?.decimalPlaces ?? null;
|
|
22
|
+
const normalizedCode = effectiveCode.toUpperCase();
|
|
23
|
+
try {
|
|
24
|
+
const formatter = new Intl.NumberFormat(locale, {
|
|
25
|
+
style: "currency",
|
|
26
|
+
currency: normalizedCode,
|
|
27
|
+
// Narrow symbol so a clear currency context renders "$100", not "US$100".
|
|
28
|
+
currencyDisplay: "narrowSymbol",
|
|
29
|
+
...effectiveDecimalPlaces != null ? { minimumFractionDigits: effectiveDecimalPlaces, maximumFractionDigits: effectiveDecimalPlaces } : {}
|
|
30
|
+
});
|
|
31
|
+
return formatter.format(amount);
|
|
32
|
+
} catch {
|
|
33
|
+
const fractionDigits = effectiveDecimalPlaces ?? 2;
|
|
34
|
+
const prefix = symbol ?? normalizedCode;
|
|
35
|
+
const sign = amount < 0 ? "-" : "";
|
|
36
|
+
return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function formatCompactMoney(amount, options) {
|
|
40
|
+
const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;
|
|
41
|
+
const sym = getCurrencySymbol(currencyCode, locale, symbol);
|
|
42
|
+
const sign = amount < 0 ? "-" : "";
|
|
43
|
+
const abs = Math.abs(amount);
|
|
44
|
+
if (abs >= 1e6) {
|
|
45
|
+
return `${sign}${sym}${(abs / 1e6).toFixed(1)}m`;
|
|
46
|
+
}
|
|
47
|
+
if (abs >= 1e3) {
|
|
48
|
+
return `${sign}${sym}${(abs / 1e3).toFixed(0)}k`;
|
|
49
|
+
}
|
|
50
|
+
return `${sign}${sym}${abs.toFixed(0)}`;
|
|
51
|
+
}
|
|
52
|
+
function getCurrencySymbol(currencyCode, locale = MONEY_FALLBACK_LOCALE, symbol) {
|
|
53
|
+
const normalizedCode = currencyCode.toUpperCase();
|
|
54
|
+
try {
|
|
55
|
+
const parts = new Intl.NumberFormat(locale, {
|
|
56
|
+
style: "currency",
|
|
57
|
+
currency: normalizedCode,
|
|
58
|
+
currencyDisplay: "narrowSymbol"
|
|
59
|
+
}).formatToParts(0);
|
|
60
|
+
return parts.find((part) => part.type === "currency")?.value ?? symbol ?? normalizedCode;
|
|
61
|
+
} catch {
|
|
62
|
+
return symbol ?? normalizedCode;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// src/money/parse.ts
|
|
67
|
+
var UNICODE_MINUS = /−/g;
|
|
68
|
+
var NON_NUMERIC_CHARS = /[^0-9.-]/g;
|
|
69
|
+
function parseMoney(value) {
|
|
70
|
+
if (value === null || value === void 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const trimmed = value.trim();
|
|
74
|
+
if (trimmed === "" || trimmed === "N/A") {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
const cleaned = trimmed.replace(UNICODE_MINUS, "-").replace(NON_NUMERIC_CHARS, "");
|
|
78
|
+
const parsed = Number.parseFloat(cleaned);
|
|
79
|
+
return Number.isNaN(parsed) ? null : parsed;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export { MONEY_FALLBACK_CURRENCY, MONEY_FALLBACK_LOCALE, formatCompactMoney, formatMoney, getCurrencySymbol, getOrgReportingCurrency, parseMoney, resolveOrgReportingCurrencyCode, setOrgReportingCurrency };
|
|
83
|
+
//# sourceMappingURL=chunk-2F5FENXB.js.map
|
|
84
|
+
//# sourceMappingURL=chunk-2F5FENXB.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/money/orgCurrency.ts","../src/money/format.ts","../src/money/parse.ts"],"names":[],"mappings":"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","file":"chunk-2F5FENXB.js","sourcesContent":["/**\n * Organisation-driven reporting-currency resolution.\n *\n * Plugin (and host) surfaces render reporting-currency amounts in whatever the\n * organisation has configured as its reporting currency, without threading a\n * currency code through every call site. This module holds the module-level\n * reporting-currency ref, its setter, and the readers with a deterministic\n * fallback constant. It is pure — no React, no host access — so it lives in\n * core-utils behind the `@ethisyscore/core-utils/money` sub-path, mirroring the\n * date org-format seam (`@ethisyscore/core-utils/date/org-format`).\n *\n * The ref is stamped by the surface sync hook — the SDK\n * `useReportingCurrencySync` reads the host\n * `settings:get-organisation-reporting-currency` tool and calls\n * `setOrgReportingCurrency`. When no reporting currency is stamped (a\n * standalone/mock run, an old host, or a failed fetch) resolution falls back to\n * the deterministic UK default `GBP`, NOT the OS locale, so output is\n * machine-stable regardless of the host. This is the exact fail-open contract of\n * the date seam's UK `dd/MM/yyyy` fallback.\n *\n * Deciding WHICH currency an amount is in (its own currency vs the org's\n * reporting currency) and CONVERTING between currencies stay host-aware concerns\n * of the plugin-ui `useCurrency` hook. This seam only answers \"what is the org's\n * reporting currency\" for the pure {@link formatMoney} default path.\n */\n\n/**\n * Deterministic fallback reporting currency when no org currency is stamped.\n * Matches the date seam's UK fallback (`ORG_DATE_FALLBACK_FORMAT`) so money and\n * dates degrade consistently, and so output does not drift with the OS locale of\n * whatever host renders it.\n */\nexport const MONEY_FALLBACK_CURRENCY = \"GBP\";\n\n/** The organisation's reporting currency as stamped by the surface sync hook. */\nexport interface OrgReportingCurrency {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) drives `Intl` currency formatting. */\n code: string;\n /**\n * Host decimal-places override for the currency, or null to use the currency's\n * ISO default (GBP -> 2, JPY -> 0). Applied by {@link formatMoney} only when the\n * caller supplies no explicit decimalPlaces.\n */\n decimalPlaces: number | null;\n}\n\n// ---------------------------------------------------------------------------\n// Module-level reporting-currency ref\n// ---------------------------------------------------------------------------\n\nlet orgReportingCurrencyRef: OrgReportingCurrency | null = null;\n\n/**\n * Stamps the module-level reporting-currency ref. Called by the surface sync hook\n * each time the organisation's reporting currency resolves. Pass `null` to clear\n * it and fall back to the deterministic {@link MONEY_FALLBACK_CURRENCY}.\n */\nexport function setOrgReportingCurrency(currency: OrgReportingCurrency | null): void {\n orgReportingCurrencyRef = currency;\n}\n\n/** The org's stamped reporting currency, or null to fall back to the UK default. */\nexport function getOrgReportingCurrency(): OrgReportingCurrency | null {\n return orgReportingCurrencyRef;\n}\n\n/**\n * The org's effective reporting-currency code: the stamped code when present, else\n * the deterministic {@link MONEY_FALLBACK_CURRENCY} (`GBP`). Never null, so callers\n * always have a well-formed code to format with.\n */\nexport function resolveOrgReportingCurrencyCode(): string {\n return orgReportingCurrencyRef?.code ?? MONEY_FALLBACK_CURRENCY;\n}\n","/**\n * Framework-agnostic money DISPLAY formatting.\n *\n * The platform stores amounts as a plain number plus a currency (an ISO 4217 code,\n * optionally a symbol and a decimal-places override from the host `Currency` row).\n * This module turns that into a localized string via `Intl.NumberFormat`, with a\n * deterministic fallback so output is machine-stable regardless of the host OS\n * locale - the same principle as the date org-format seam.\n *\n * It is pure - no React, no host access - so it lives in core-utils behind the\n * `@ethisyscore/core-utils/money` sub-path. Resolving WHICH currency to render in\n * (the amount's own currency, or the org's reporting currency) and CONVERTING\n * between currencies are host-aware concerns handled by the plugin-ui `useCurrency`\n * hook, which formats through this module.\n *\n * When no currency is given, `formatMoney` falls back to the organisation's\n * reporting currency via the {@link ./orgCurrency} seam (the same module-ref +\n * fail-open-to-`GBP` mechanism as the date org-format seam), so a plugin can render\n * reporting-currency amounts without threading a code or hardcoding one.\n */\n\nimport {\n getOrgReportingCurrency,\n MONEY_FALLBACK_CURRENCY,\n} from \"./orgCurrency\";\n\n/**\n * Deterministic fallback locale. Matches the date seam's UK fallback so money and\n * dates render consistently when no explicit locale is supplied, and so output does\n * not drift with the OS locale of whatever host renders it.\n */\nexport const MONEY_FALLBACK_LOCALE = \"en-GB\";\n\n/** Options for {@link formatMoney}. */\nexport interface FormatMoneyOptions {\n /**\n * ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - drives `Intl` currency formatting.\n * Optional: when omitted, `formatMoney` resolves the organisation's reporting\n * currency (see {@link ./orgCurrency}), falling back to `GBP` when none is stamped.\n */\n currencyCode?: string;\n /**\n * Fraction digits to show. When null/undefined, `Intl`'s per-currency default is\n * used (GBP -> 2, JPY -> 0). Pass a number to force a currency's decimal-places\n * override from the host `Currency` row.\n */\n decimalPlaces?: number | null;\n /** BCP-47 locale; defaults to {@link MONEY_FALLBACK_LOCALE} for stable output. */\n locale?: string;\n /**\n * Symbol to use only in the fallback path when `Intl` cannot format the currency\n * code (e.g. a non-ISO custom code). Ignored on the happy path, where `Intl`\n * supplies the symbol.\n */\n symbol?: string | null;\n}\n\n/**\n * Formats a monetary amount. The currency is resolved in this order:\n *\n * - `formatMoney(amount, \"EUR\")` — a positional ISO code, with the currency's\n * natural decimals.\n * - `formatMoney(amount, { currencyCode, decimalPlaces, locale, symbol })` — the\n * options form; any of `currencyCode`/`decimalPlaces` may be omitted.\n * - `formatMoney(amount)` (or an options object without `currencyCode`) — the\n * organisation's reporting currency stamped via the {@link ./orgCurrency} seam,\n * including that currency's decimal-places override when the caller gave none.\n * When no org currency is stamped it falls back to {@link MONEY_FALLBACK_CURRENCY}\n * (`GBP`), the same deterministic fail-open as the date org-format seam.\n *\n * On an unknown/invalid currency code (which makes `Intl.NumberFormat` throw) it\n * falls back to a symbol/code prefix plus the fixed-decimal amount, so a bad code\n * degrades to a readable string rather than throwing on a render path.\n */\nexport function formatMoney(amount: number, currency?: string | FormatMoneyOptions): string {\n const options: FormatMoneyOptions = typeof currency === \"string\" ? { currencyCode: currency } : (currency ?? {});\n const { currencyCode, decimalPlaces, locale = MONEY_FALLBACK_LOCALE, symbol } = options;\n\n // No explicit code (undefined arg, or an options object without currencyCode)\n // resolves the org's reporting currency, falling back to GBP when none is stamped.\n const orgCurrency = currencyCode === undefined ? getOrgReportingCurrency() : null;\n const effectiveCode = currencyCode ?? orgCurrency?.code ?? MONEY_FALLBACK_CURRENCY;\n // The org currency's decimal-places override applies ONLY when the caller omitted\n // decimalPlaces entirely (undefined). An explicit `null` means \"use Intl's\n // per-currency default\" and must NOT pick up the org override, so distinguish\n // undefined from null rather than coalescing both with `??`.\n const effectiveDecimalPlaces =\n decimalPlaces !== undefined ? decimalPlaces : (orgCurrency?.decimalPlaces ?? null);\n\n // Intl.NumberFormat requires a well-formed (uppercase) ISO 4217 code; a lowercase\n // code (e.g. \"usd\") throws and forces the fallback, so normalise up front.\n const normalizedCode = effectiveCode.toUpperCase();\n\n try {\n const formatter = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n // Narrow symbol so a clear currency context renders \"$100\", not \"US$100\".\n currencyDisplay: \"narrowSymbol\",\n ...(effectiveDecimalPlaces != null\n ? { minimumFractionDigits: effectiveDecimalPlaces, maximumFractionDigits: effectiveDecimalPlaces }\n : {}),\n });\n return formatter.format(amount);\n } catch {\n const fractionDigits = effectiveDecimalPlaces ?? 2;\n const prefix = symbol ?? normalizedCode;\n // Keep the minus sign before the symbol/code prefix (\"-£10.00\", not \"£-10.00\").\n const sign = amount < 0 ? \"-\" : \"\";\n return `${sign}${prefix}${Math.abs(amount).toFixed(fractionDigits)}`;\n }\n}\n\n/** Options for {@link formatCompactMoney}. */\nexport interface FormatCompactMoneyOptions {\n /** ISO 4217 code (e.g. `\"GBP\"`, `\"EUR\"`) - resolves the leading symbol. */\n currencyCode: string;\n /**\n * Fallback symbol used ONLY when `Intl` cannot resolve `currencyCode` (a non-ISO\n * custom code). For a valid ISO code `Intl`'s own symbol always wins - this option\n * does not override it. Mirrors {@link FormatMoneyOptions.symbol} and the\n * {@link getCurrencySymbol} fallback contract.\n */\n symbol?: string | null;\n /** BCP-47 locale used only to resolve the symbol; defaults to {@link MONEY_FALLBACK_LOCALE}. */\n locale?: string;\n}\n\n/**\n * Formats an amount as an abbreviated currency string for compact display, such\n * as chart axis ticks where space is tight - e.g. `\"£1.3m\"`, `\"£500k\"`, `\"£99\"`.\n *\n * Thresholds on the absolute value: >= 1,000,000 renders in millions with a\n * lowercase `m` and one decimal place; >= 1,000 renders in thousands with a\n * lowercase `k` and no decimals; otherwise the whole amount with no decimals.\n * A leading minus is kept before the symbol (e.g. `\"-£1.3m\"`).\n *\n * Unlike {@link formatMoney} this is NOT org-locale driven beyond resolving the\n * leading symbol via {@link getCurrencySymbol}; the number itself is formatted\n * with fixed abbreviations so axis labels stay short and machine-stable.\n */\nexport function formatCompactMoney(amount: number, options: FormatCompactMoneyOptions): string {\n const { currencyCode, symbol, locale = MONEY_FALLBACK_LOCALE } = options;\n\n const sym = getCurrencySymbol(currencyCode, locale, symbol);\n const sign = amount < 0 ? \"-\" : \"\";\n const abs = Math.abs(amount);\n\n if (abs >= 1_000_000) {\n return `${sign}${sym}${(abs / 1_000_000).toFixed(1)}m`;\n }\n if (abs >= 1_000) {\n return `${sign}${sym}${(abs / 1_000).toFixed(0)}k`;\n }\n return `${sign}${sym}${abs.toFixed(0)}`;\n}\n\n/**\n * Resolves the currency symbol for a code in a locale (e.g. `\"GBP\"` -> `\"£\"`),\n * for use in input adornments and labels. Falls back to the supplied `symbol`, then\n * the code itself, when `Intl` cannot resolve it.\n */\nexport function getCurrencySymbol(\n currencyCode: string,\n locale: string = MONEY_FALLBACK_LOCALE,\n symbol?: string | null,\n): string {\n // Intl needs an uppercase ISO code; a lowercase one throws (see formatMoney).\n const normalizedCode = currencyCode.toUpperCase();\n\n try {\n const parts = new Intl.NumberFormat(locale, {\n style: \"currency\",\n currency: normalizedCode,\n currencyDisplay: \"narrowSymbol\",\n }).formatToParts(0);\n\n return parts.find((part) => part.type === \"currency\")?.value ?? symbol ?? normalizedCode;\n } catch {\n return symbol ?? normalizedCode;\n }\n}\n","/**\n * Framework-agnostic money PARSING — the inverse of {@link formatMoney} for form\n * inputs and editable amount fields.\n *\n * A user (or a round-tripped display value) types an amount in the en-GB /\n * dot-decimal display shape — a currency symbol or ISO code prefix, comma grouping\n * separators, stray whitespace, and a dot decimal point (\"£1,234.56\", \"GBP 1,234.56\").\n * This turns that back into a plain number, or null when there is no meaningful value\n * to parse.\n *\n * It is deliberately lenient about the symbol, currency-code prefix and grouping so\n * it tolerates the display forms {@link formatMoney} produces in the en-GB fallback\n * locale, but it is NOT a locale-universal inverse of {@link formatMoney}. Only the\n * dot-decimal shape is understood: a comma is always treated as a grouping separator\n * and dropped, so comma-decimal locales (de-DE `1.234,56`) are OUT OF SCOPE and would\n * mis-parse. The platform stores and edits amounts in the dot-decimal numeric form.\n *\n * Ported from the per-plugin `parseCurrency` helper so consuming plugins drop the\n * local copy.\n */\n\n/**\n * The Unicode minus sign (U+2212) that `Intl.NumberFormat` emits for negatives.\n * It is not the ASCII hyphen-minus, so `parseFloat` would not treat it as a sign;\n * normalise it to `-` before stripping.\n */\nconst UNICODE_MINUS = /−/g;\n\n/**\n * After normalising the minus, everything that is NOT a digit, dot or minus is\n * stripped: currency symbols (£ $ € ¥ ₹ and any other), alpha ISO-code prefixes\n * (the \"GBP \" prefix, and the invalid-code fallback prefix like \"ZZZZ\"), comma\n * grouping separators, and all whitespace (including the non-breaking / narrow\n * no-break spaces `Intl` inserts).\n */\nconst NON_NUMERIC_CHARS = /[^0-9.-]/g;\n\n/**\n * Parses a formatted currency string back to a number. Returns null for\n * null/undefined, an empty/whitespace-only string, or the literal `\"N/A\"`\n * placeholder; normalises the Unicode minus, strips every non-numeric character\n * (currency symbols, alpha currency-code prefixes, grouping commas and whitespace),\n * then `parseFloat`s the remainder, returning null when the result is not a number.\n *\n * Parses only the en-GB / dot-decimal display shape (comma thousands, dot decimal);\n * see the module docs — it is NOT a locale-universal inverse of {@link formatMoney}.\n * A leading minus is preserved, so `\"-£10.00\"` parses to `-10`. Garbage that contains\n * no leading number (`\"abc\"`) yields null.\n *\n * @example\n * parseMoney(\"£1,234.56\") // 1234.56\n * parseMoney(\"GBP 1,234.56\") // 1234.56\n * parseMoney(\"−£10.00\") // -10 (Unicode minus U+2212)\n * parseMoney(\"ZZZZ10.00\") // 10 (invalid-code fallback prefix)\n * parseMoney(\"N/A\") // null\n * parseMoney(\"\") // null\n * parseMoney(null) // null\n */\nexport function parseMoney(value: string | null | undefined): number | null {\n if (value === null || value === undefined) {\n return null;\n }\n\n const trimmed = value.trim();\n if (trimmed === \"\" || trimmed === \"N/A\") {\n return null;\n }\n\n const cleaned = trimmed.replace(UNICODE_MINUS, \"-\").replace(NON_NUMERIC_CHARS, \"\");\n const parsed = Number.parseFloat(cleaned);\n\n return Number.isNaN(parsed) ? null : parsed;\n}\n"]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/list/index.ts
|
|
2
|
+
function unwrapList(response) {
|
|
3
|
+
if (response == null) {
|
|
4
|
+
return [];
|
|
5
|
+
}
|
|
6
|
+
if (Array.isArray(response)) {
|
|
7
|
+
return response;
|
|
8
|
+
}
|
|
9
|
+
return response.items ?? response.rows ?? [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export { unwrapList };
|
|
13
|
+
//# sourceMappingURL=chunk-3LIIUXBE.js.map
|
|
14
|
+
//# sourceMappingURL=chunk-3LIIUXBE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/list/index.ts"],"names":[],"mappings":";AAYO,SAAS,WAAc,QAAA,EAAmD;AAC/E,EAAA,IAAI,YAAY,IAAA,EAAM;AACpB,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC3B,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA,IAAQ,EAAC;AAC7C","file":"chunk-3LIIUXBE.js","sourcesContent":["/**\n * List-response helpers shared by the EthisysCore monolith and plugins.\n *\n * List-returning tools (MCP tools, REST endpoints) return their rows either as a\n * bare array or wrapped in an `{ items }` / `{ rows }` envelope. {@link unwrapList}\n * normalises either shape to a flat array so callers never branch on it.\n */\n\n/** A list response: a bare array or an `{ items }` / `{ rows }` envelope. */\nexport type ListEnvelope<T> = T[] | { items?: T[]; rows?: T[] };\n\n/** Normalise a list response (bare array or `{ items }` / `{ rows }`) to a flat array. */\nexport function unwrapList<T>(response: ListEnvelope<T> | null | undefined): T[] {\n if (response == null) {\n return [];\n }\n if (Array.isArray(response)) {\n return response;\n }\n return response.items ?? response.rows ?? [];\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/list/index.ts
|
|
4
|
+
function unwrapList(response) {
|
|
5
|
+
if (response == null) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
if (Array.isArray(response)) {
|
|
9
|
+
return response;
|
|
10
|
+
}
|
|
11
|
+
return response.items ?? response.rows ?? [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
exports.unwrapList = unwrapList;
|
|
15
|
+
//# sourceMappingURL=chunk-G2XGZH7P.cjs.map
|
|
16
|
+
//# sourceMappingURL=chunk-G2XGZH7P.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/list/index.ts"],"names":[],"mappings":";;;AAYO,SAAS,WAAc,QAAA,EAAmD;AAC/E,EAAA,IAAI,YAAY,IAAA,EAAM;AACpB,IAAA,OAAO,EAAC;AAAA,EACV;AACA,EAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAQ,CAAA,EAAG;AAC3B,IAAA,OAAO,QAAA;AAAA,EACT;AACA,EAAA,OAAO,QAAA,CAAS,KAAA,IAAS,QAAA,CAAS,IAAA,IAAQ,EAAC;AAC7C","file":"chunk-G2XGZH7P.cjs","sourcesContent":["/**\n * List-response helpers shared by the EthisysCore monolith and plugins.\n *\n * List-returning tools (MCP tools, REST endpoints) return their rows either as a\n * bare array or wrapped in an `{ items }` / `{ rows }` envelope. {@link unwrapList}\n * normalises either shape to a flat array so callers never branch on it.\n */\n\n/** A list response: a bare array or an `{ items }` / `{ rows }` envelope. */\nexport type ListEnvelope<T> = T[] | { items?: T[]; rows?: T[] };\n\n/** Normalise a list response (bare array or `{ items }` / `{ rows }`) to a flat array. */\nexport function unwrapList<T>(response: ListEnvelope<T> | null | undefined): T[] {\n if (response == null) {\n return [];\n }\n if (Array.isArray(response)) {\n return response;\n }\n return response.items ?? response.rows ?? [];\n}\n"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { DATE_FORMAT, MS_PER_HOUR, MS_PER_MINUTE, MS_PER_DAY } from './chunk-TN6DXBVD.js';
|
|
2
|
+
import { format, isValid, subDays, subYears, subMonths } from 'date-fns';
|
|
3
|
+
|
|
4
|
+
// src/date/duration.ts
|
|
5
|
+
var timespanToMilliseconds = (timespan) => {
|
|
6
|
+
if (!timespan) return 0;
|
|
7
|
+
const [h = "0", m = "0", s = "0"] = timespan.split(":");
|
|
8
|
+
const hours = parseInt(h || "0", 10);
|
|
9
|
+
const minutes = parseInt(m || "0", 10);
|
|
10
|
+
const seconds = parseInt(s || "0", 10);
|
|
11
|
+
return (isNaN(hours) ? 0 : hours) * 60 * 60 * 1e3 + (isNaN(minutes) ? 0 : minutes) * 60 * 1e3 + (isNaN(seconds) ? 0 : seconds) * 1e3;
|
|
12
|
+
};
|
|
13
|
+
var millisecondsToHours = (ms) => ms / MS_PER_HOUR;
|
|
14
|
+
var hoursToMilliseconds = (hours) => hours * MS_PER_HOUR;
|
|
15
|
+
var formatDuration = (ms) => {
|
|
16
|
+
if (ms == null || Number.isNaN(ms) || ms < 0) return "\u2014";
|
|
17
|
+
if (ms < MS_PER_MINUTE) return "0m";
|
|
18
|
+
const days = Math.floor(ms / MS_PER_DAY);
|
|
19
|
+
const hours = Math.floor(ms % MS_PER_DAY / MS_PER_HOUR);
|
|
20
|
+
const minutes = Math.floor(ms % MS_PER_HOUR / MS_PER_MINUTE);
|
|
21
|
+
const parts = [];
|
|
22
|
+
if (days) parts.push(`${days}d`);
|
|
23
|
+
if (hours) parts.push(`${hours}h`);
|
|
24
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
25
|
+
return parts.join(" ");
|
|
26
|
+
};
|
|
27
|
+
function toHHmm(value, fallback) {
|
|
28
|
+
if (!value || value.length < 5) return fallback;
|
|
29
|
+
return value.slice(0, 5);
|
|
30
|
+
}
|
|
31
|
+
function toHHmmss(value) {
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
if (!trimmed) return null;
|
|
34
|
+
return trimmed.length === 5 ? `${trimmed}:00` : trimmed;
|
|
35
|
+
}
|
|
36
|
+
function dateOnlyToIsoUtc(value) {
|
|
37
|
+
if (!value) return void 0;
|
|
38
|
+
const parsed = value.includes("T") ? new Date(value) : /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
39
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
|
|
40
|
+
}
|
|
41
|
+
function nowLocalDateTimeInputValue() {
|
|
42
|
+
return format(/* @__PURE__ */ new Date(), "yyyy-MM-dd'T'HH:mm");
|
|
43
|
+
}
|
|
44
|
+
function localDateTimeInputToIsoUtc(value) {
|
|
45
|
+
if (!value) return void 0;
|
|
46
|
+
const parsed = new Date(value);
|
|
47
|
+
if (!isValid(parsed)) return void 0;
|
|
48
|
+
return parsed.toISOString();
|
|
49
|
+
}
|
|
50
|
+
function ensureUtcIso(value) {
|
|
51
|
+
return /[Zz]|[+-]\d{2}:?\d{2}$/.test(value) ? value : value + "Z";
|
|
52
|
+
}
|
|
53
|
+
function isoUtcToLocalDateTimeInput(value, fallback = "") {
|
|
54
|
+
if (!value) return fallback;
|
|
55
|
+
const parsed = new Date(ensureUtcIso(value));
|
|
56
|
+
if (!isValid(parsed)) return fallback;
|
|
57
|
+
return format(parsed, "yyyy-MM-dd'T'HH:mm");
|
|
58
|
+
}
|
|
59
|
+
var getDateRange = (timePeriod, dateFormat = DATE_FORMAT) => {
|
|
60
|
+
const today = /* @__PURE__ */ new Date();
|
|
61
|
+
switch (timePeriod) {
|
|
62
|
+
case "month":
|
|
63
|
+
return { startDate: format(subMonths(today, 1), dateFormat), endDate: format(today, dateFormat) };
|
|
64
|
+
case "quarter":
|
|
65
|
+
return { startDate: format(subMonths(today, 3), dateFormat), endDate: format(today, dateFormat) };
|
|
66
|
+
case "year":
|
|
67
|
+
return { startDate: format(subYears(today, 1), dateFormat), endDate: format(today, dateFormat) };
|
|
68
|
+
case "week":
|
|
69
|
+
default:
|
|
70
|
+
return { startDate: format(subDays(today, 7), dateFormat), endDate: format(today, dateFormat) };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
function computeDateRange(days) {
|
|
74
|
+
const to = (/* @__PURE__ */ new Date()).toISOString();
|
|
75
|
+
const from = new Date(Date.now() - days * 864e5).toISOString();
|
|
76
|
+
return { from, to };
|
|
77
|
+
}
|
|
78
|
+
function currentMonthRangeUtc(today = /* @__PURE__ */ new Date()) {
|
|
79
|
+
const year = today.getUTCFullYear();
|
|
80
|
+
const month = today.getUTCMonth();
|
|
81
|
+
return {
|
|
82
|
+
fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),
|
|
83
|
+
beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString()
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function getCurrentOffsetMinutes(iana) {
|
|
87
|
+
try {
|
|
88
|
+
const parts = new Intl.DateTimeFormat("en", {
|
|
89
|
+
timeZone: iana,
|
|
90
|
+
timeZoneName: "longOffset"
|
|
91
|
+
}).formatToParts(/* @__PURE__ */ new Date());
|
|
92
|
+
const label = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
|
|
93
|
+
if (label === "GMT" || label === "UTC") return 0;
|
|
94
|
+
const m = label.match(/GMT([+-])(\d{2}):(\d{2})/);
|
|
95
|
+
if (!m) return null;
|
|
96
|
+
const sign = m[1] === "+" ? 1 : -1;
|
|
97
|
+
return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { computeDateRange, currentMonthRangeUtc, dateOnlyToIsoUtc, ensureUtcIso, formatDuration, getCurrentOffsetMinutes, getDateRange, hoursToMilliseconds, isoUtcToLocalDateTimeInput, localDateTimeInputToIsoUtc, millisecondsToHours, nowLocalDateTimeInputValue, timespanToMilliseconds, toHHmm, toHHmmss };
|
|
104
|
+
//# sourceMappingURL=chunk-GAZLFZEL.js.map
|
|
105
|
+
//# sourceMappingURL=chunk-GAZLFZEL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/date/duration.ts","../src/date/wire.ts","../src/date/range.ts"],"names":["format"],"mappings":"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","file":"chunk-GAZLFZEL.js","sourcesContent":["/**\n * Pure duration / timespan helpers — no external dependencies.\n *\n * Cover the two duration shapes that flow across the EthisysCore wire: a\n * `\"HH:mm:ss\"` timespan string (.NET `TimeSpan`) and a millisecond count.\n */\nimport { MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE } from \"./constants\";\n\n/** Converts a `\"HH:mm:ss\"` timespan string to milliseconds. */\nexport const timespanToMilliseconds = (timespan: string): number => {\n if (!timespan) return 0;\n\n const [h = \"0\", m = \"0\", s = \"0\"] = timespan.split(\":\");\n const hours = parseInt(h || \"0\", 10);\n const minutes = parseInt(m || \"0\", 10);\n const seconds = parseInt(s || \"0\", 10);\n\n return (\n (isNaN(hours) ? 0 : hours) * 60 * 60 * 1000 +\n (isNaN(minutes) ? 0 : minutes) * 60 * 1000 +\n (isNaN(seconds) ? 0 : seconds) * 1000\n );\n};\n\n/** Converts milliseconds to hours (as a float). */\nexport const millisecondsToHours = (ms: number): number => ms / MS_PER_HOUR;\n\n/** Converts hours to milliseconds. */\nexport const hoursToMilliseconds = (hours: number): number => hours * MS_PER_HOUR;\n\n/**\n * The one human-readable duration formatter. Takes a MILLISECOND count and renders\n * the largest non-zero units as `Xd Xh Xm` (e.g. `4h 15m`, `2d 3h`, `45m`). Zero is\n * a real value (`0m`); a nullish/NaN/negative input is missing data and returns the\n * em-dash. Minute granularity — seconds are not shown.\n *\n * Feed non-millisecond inputs through the converters in this module:\n * `formatDuration(timespanToMilliseconds(\"04:15:00\"))` // \"HH:mm:ss\" TimeSpan\n * `formatDuration(hoursToMilliseconds(2.5))` // fractional hours\n */\nexport const formatDuration = (ms: number | null | undefined): string => {\n if (ms == null || Number.isNaN(ms) || ms < 0) return \"—\";\n if (ms < MS_PER_MINUTE) return \"0m\";\n\n const days = Math.floor(ms / MS_PER_DAY);\n const hours = Math.floor((ms % MS_PER_DAY) / MS_PER_HOUR);\n const minutes = Math.floor((ms % MS_PER_HOUR) / MS_PER_MINUTE);\n\n const parts: string[] = [];\n if (days) parts.push(`${days}d`);\n if (hours) parts.push(`${hours}h`);\n if (minutes) parts.push(`${minutes}m`);\n return parts.join(\" \");\n};\n\n/**\n * Trims an API-supplied `\"HH:mm:ss\"` down to the `\"HH:mm\"` string used by\n * MUI `TimePicker`-backed forms. Returns the supplied fallback when the value\n * is missing or shorter than five characters.\n */\nexport function toHHmm(value: string | null | undefined, fallback: string): string {\n if (!value || value.length < 5) return fallback;\n return value.slice(0, 5);\n}\n\n/**\n * Serialises a non-empty `\"HH:mm\"` / `\"HH:mm:ss\"` value back to the canonical\n * `\"HH:mm:ss\"` API format. Returns `null` for empty input so callers can block\n * the save rather than silently persisting midnight — an empty TimePicker\n * (cleared via keyboard) is an unsaved edit, not a legitimate `00:00:00` value.\n */\nexport function toHHmmss(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n return trimmed.length === 5 ? `${trimmed}:00` : trimmed;\n}\n","import { format, isValid } from \"date-fns\";\n\n// ---------------------------------------------------------------------------\n// Wire-boundary helpers\n// ---------------------------------------------------------------------------\n//\n// HTML's `<input type=\"date\">` and `<input type=\"datetime-local\">` emit naive\n// local strings with NO timezone suffix. Sending those raw to a .NET backend\n// that deserialises into `DateTimeOffset` parses them using the BE process's\n// local offset (UTC on most container hosts), silently misinterpreting the\n// user's wall-clock as UTC and shifting the stored timestamp by the user's\n// offset (e.g. losing the first hour of the local day during BST). These\n// helpers bridge cleanly between the input shapes and full ISO-8601 UTC.\n//\n// All are pure and timezone-aware via the runtime `Date` object.\n\n/**\n * Normalises a date-only value from `<input type=\"date\">` (`yyyy-MM-dd`) into a\n * canonical UTC ISO-8601 timestamp at midnight UTC, e.g.\n * `2026-07-14` → `2026-07-14T00:00:00.000Z`. Use at the API-payload boundary\n * for fields the BE types as `DateTimeOffset` — a bare date-only string risks\n * ambiguous/failed deserialisation. A value that is already a full ISO\n * timestamp is passed through unchanged. Empty/invalid → `undefined` so callers\n * omit the field (BE clears it).\n */\nexport function dateOnlyToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = value.includes(\"T\") ? new Date(value) : new Date(`${value}T00:00:00Z`);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n}\n\n/**\n * Returns the current local wall-clock as a `yyyy-MM-ddTHH:mm` string — the\n * format an `<input type=\"datetime-local\">` element expects. Minute precision;\n * seconds and timezone deliberately omitted.\n */\nexport function nowLocalDateTimeInputValue(): string {\n return format(new Date(), \"yyyy-MM-dd'T'HH:mm\");\n}\n\n/**\n * Converts a naive local-time string from `<input type=\"datetime-local\">`\n * (shape `yyyy-MM-ddTHH:mm` or `yyyy-MM-ddTHH:mm:ss`) into a full ISO-8601 UTC\n * string with `Z` suffix that .NET `DateTimeOffset` cannot ambiguously\n * interpret. Empty / undefined inputs pass through as `undefined`.\n */\nexport function localDateTimeInputToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = new Date(value);\n if (!isValid(parsed)) return undefined;\n return parsed.toISOString();\n}\n\n/**\n * Ensures an ISO-8601 string is treated as UTC by appending a `Z` suffix when\n * no explicit offset or UTC indicator is present. The backend may emit\n * timestamps like `2026-05-26T06:21:00` (no Z), which JS would otherwise parse\n * as local time.\n */\nexport function ensureUtcIso(value: string): string {\n return /[Zz]|[+-]\\d{2}:?\\d{2}$/.test(value) ? value : value + \"Z\";\n}\n\n/**\n * Converts an ISO-8601 string (with or without explicit offset) into the naive\n * local-time shape `yyyy-MM-ddTHH:mm` expected by `<input type=\"datetime-local\">`.\n * Returns the supplied fallback (default empty string) when the input is\n * missing or unparseable. When `value` carries no offset or `Z` suffix it is\n * treated as UTC before converting to local time, so the edit form displays the\n * correct time regardless of the user's timezone.\n */\nexport function isoUtcToLocalDateTimeInput(value: string | null | undefined, fallback = \"\"): string {\n if (!value) return fallback;\n const parsed = new Date(ensureUtcIso(value));\n if (!isValid(parsed)) return fallback;\n return format(parsed, \"yyyy-MM-dd'T'HH:mm\");\n}\n","import { format, subDays, subMonths, subYears } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\n\nexport interface DateRange {\n startDate: string;\n endDate: string;\n}\n\n/**\n * Returns a `{ startDate, endDate }` date range for a named relative period,\n * formatted with `dateFormat` (defaults to the ISO date-only format). Unknown\n * periods fall back to the last week.\n * @param timePeriod - one of `week` | `month` | `quarter` | `year`\n * @param dateFormat - output format for both bounds (defaults to `DATE_FORMAT`)\n */\nexport const getDateRange = (timePeriod: string, dateFormat: string = DATE_FORMAT): DateRange => {\n const today = new Date();\n switch (timePeriod) {\n case \"month\":\n return { startDate: format(subMonths(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"quarter\":\n return { startDate: format(subMonths(today, 3), dateFormat), endDate: format(today, dateFormat) };\n case \"year\":\n return { startDate: format(subYears(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"week\":\n default:\n return { startDate: format(subDays(today, 7), dateFormat), endDate: format(today, dateFormat) };\n }\n};\n\n/** Computes an ISO timestamp range from a number of days back to now. */\nexport function computeDateRange(days: number): { from: string; to: string } {\n const to = new Date().toISOString();\n const from = new Date(Date.now() - days * 86_400_000).toISOString();\n return { from, to };\n}\n\n/**\n * Returns the current calendar month as a half-open UTC range `[fromUtc, beforeUtc)`,\n * suitable for \"this month\" count filters. `fromUtc` is the first instant of the\n * month; `beforeUtc` is the first instant of the next month (exclusive).\n */\nexport function currentMonthRangeUtc(today: Date = new Date()): { fromUtc: string; beforeUtc: string } {\n const year = today.getUTCFullYear();\n const month = today.getUTCMonth();\n return {\n fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),\n beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString(),\n };\n}\n\n/**\n * Computes the current UTC-offset minutes for a given IANA zone name using the\n * runtime's `Intl` implementation. Returns `null` when the zone is unknown.\n *\n * Used by timezone auto-populate fallbacks so a browser reporting an IANA zone\n * that isn't in the backend seed (e.g. `Europe/London` during BST) can still\n * match against a seeded zone sharing the same current offset.\n */\nexport function getCurrentOffsetMinutes(iana: string): number | null {\n try {\n const parts = new Intl.DateTimeFormat(\"en\", {\n timeZone: iana,\n timeZoneName: \"longOffset\",\n }).formatToParts(new Date());\n const label = parts.find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n if (label === \"GMT\" || label === \"UTC\") return 0;\n const m = label.match(/GMT([+-])(\\d{2}):(\\d{2})/);\n if (!m) return null;\n const sign = m[1] === \"+\" ? 1 : -1;\n return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));\n } catch {\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkYNBKRMK3_cjs = require('./chunk-YNBKRMK3.cjs');
|
|
4
|
+
var dateFns = require('date-fns');
|
|
5
|
+
|
|
6
|
+
// src/date/duration.ts
|
|
7
|
+
var timespanToMilliseconds = (timespan) => {
|
|
8
|
+
if (!timespan) return 0;
|
|
9
|
+
const [h = "0", m = "0", s = "0"] = timespan.split(":");
|
|
10
|
+
const hours = parseInt(h || "0", 10);
|
|
11
|
+
const minutes = parseInt(m || "0", 10);
|
|
12
|
+
const seconds = parseInt(s || "0", 10);
|
|
13
|
+
return (isNaN(hours) ? 0 : hours) * 60 * 60 * 1e3 + (isNaN(minutes) ? 0 : minutes) * 60 * 1e3 + (isNaN(seconds) ? 0 : seconds) * 1e3;
|
|
14
|
+
};
|
|
15
|
+
var millisecondsToHours = (ms) => ms / chunkYNBKRMK3_cjs.MS_PER_HOUR;
|
|
16
|
+
var hoursToMilliseconds = (hours) => hours * chunkYNBKRMK3_cjs.MS_PER_HOUR;
|
|
17
|
+
var formatDuration = (ms) => {
|
|
18
|
+
if (ms == null || Number.isNaN(ms) || ms < 0) return "\u2014";
|
|
19
|
+
if (ms < chunkYNBKRMK3_cjs.MS_PER_MINUTE) return "0m";
|
|
20
|
+
const days = Math.floor(ms / chunkYNBKRMK3_cjs.MS_PER_DAY);
|
|
21
|
+
const hours = Math.floor(ms % chunkYNBKRMK3_cjs.MS_PER_DAY / chunkYNBKRMK3_cjs.MS_PER_HOUR);
|
|
22
|
+
const minutes = Math.floor(ms % chunkYNBKRMK3_cjs.MS_PER_HOUR / chunkYNBKRMK3_cjs.MS_PER_MINUTE);
|
|
23
|
+
const parts = [];
|
|
24
|
+
if (days) parts.push(`${days}d`);
|
|
25
|
+
if (hours) parts.push(`${hours}h`);
|
|
26
|
+
if (minutes) parts.push(`${minutes}m`);
|
|
27
|
+
return parts.join(" ");
|
|
28
|
+
};
|
|
29
|
+
function toHHmm(value, fallback) {
|
|
30
|
+
if (!value || value.length < 5) return fallback;
|
|
31
|
+
return value.slice(0, 5);
|
|
32
|
+
}
|
|
33
|
+
function toHHmmss(value) {
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
if (!trimmed) return null;
|
|
36
|
+
return trimmed.length === 5 ? `${trimmed}:00` : trimmed;
|
|
37
|
+
}
|
|
38
|
+
function dateOnlyToIsoUtc(value) {
|
|
39
|
+
if (!value) return void 0;
|
|
40
|
+
const parsed = value.includes("T") ? new Date(value) : /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
|
|
41
|
+
return Number.isNaN(parsed.getTime()) ? void 0 : parsed.toISOString();
|
|
42
|
+
}
|
|
43
|
+
function nowLocalDateTimeInputValue() {
|
|
44
|
+
return dateFns.format(/* @__PURE__ */ new Date(), "yyyy-MM-dd'T'HH:mm");
|
|
45
|
+
}
|
|
46
|
+
function localDateTimeInputToIsoUtc(value) {
|
|
47
|
+
if (!value) return void 0;
|
|
48
|
+
const parsed = new Date(value);
|
|
49
|
+
if (!dateFns.isValid(parsed)) return void 0;
|
|
50
|
+
return parsed.toISOString();
|
|
51
|
+
}
|
|
52
|
+
function ensureUtcIso(value) {
|
|
53
|
+
return /[Zz]|[+-]\d{2}:?\d{2}$/.test(value) ? value : value + "Z";
|
|
54
|
+
}
|
|
55
|
+
function isoUtcToLocalDateTimeInput(value, fallback = "") {
|
|
56
|
+
if (!value) return fallback;
|
|
57
|
+
const parsed = new Date(ensureUtcIso(value));
|
|
58
|
+
if (!dateFns.isValid(parsed)) return fallback;
|
|
59
|
+
return dateFns.format(parsed, "yyyy-MM-dd'T'HH:mm");
|
|
60
|
+
}
|
|
61
|
+
var getDateRange = (timePeriod, dateFormat = chunkYNBKRMK3_cjs.DATE_FORMAT) => {
|
|
62
|
+
const today = /* @__PURE__ */ new Date();
|
|
63
|
+
switch (timePeriod) {
|
|
64
|
+
case "month":
|
|
65
|
+
return { startDate: dateFns.format(dateFns.subMonths(today, 1), dateFormat), endDate: dateFns.format(today, dateFormat) };
|
|
66
|
+
case "quarter":
|
|
67
|
+
return { startDate: dateFns.format(dateFns.subMonths(today, 3), dateFormat), endDate: dateFns.format(today, dateFormat) };
|
|
68
|
+
case "year":
|
|
69
|
+
return { startDate: dateFns.format(dateFns.subYears(today, 1), dateFormat), endDate: dateFns.format(today, dateFormat) };
|
|
70
|
+
case "week":
|
|
71
|
+
default:
|
|
72
|
+
return { startDate: dateFns.format(dateFns.subDays(today, 7), dateFormat), endDate: dateFns.format(today, dateFormat) };
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
function computeDateRange(days) {
|
|
76
|
+
const to = (/* @__PURE__ */ new Date()).toISOString();
|
|
77
|
+
const from = new Date(Date.now() - days * 864e5).toISOString();
|
|
78
|
+
return { from, to };
|
|
79
|
+
}
|
|
80
|
+
function currentMonthRangeUtc(today = /* @__PURE__ */ new Date()) {
|
|
81
|
+
const year = today.getUTCFullYear();
|
|
82
|
+
const month = today.getUTCMonth();
|
|
83
|
+
return {
|
|
84
|
+
fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),
|
|
85
|
+
beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString()
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function getCurrentOffsetMinutes(iana) {
|
|
89
|
+
try {
|
|
90
|
+
const parts = new Intl.DateTimeFormat("en", {
|
|
91
|
+
timeZone: iana,
|
|
92
|
+
timeZoneName: "longOffset"
|
|
93
|
+
}).formatToParts(/* @__PURE__ */ new Date());
|
|
94
|
+
const label = parts.find((p) => p.type === "timeZoneName")?.value ?? "";
|
|
95
|
+
if (label === "GMT" || label === "UTC") return 0;
|
|
96
|
+
const m = label.match(/GMT([+-])(\d{2}):(\d{2})/);
|
|
97
|
+
if (!m) return null;
|
|
98
|
+
const sign = m[1] === "+" ? 1 : -1;
|
|
99
|
+
return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
exports.computeDateRange = computeDateRange;
|
|
106
|
+
exports.currentMonthRangeUtc = currentMonthRangeUtc;
|
|
107
|
+
exports.dateOnlyToIsoUtc = dateOnlyToIsoUtc;
|
|
108
|
+
exports.ensureUtcIso = ensureUtcIso;
|
|
109
|
+
exports.formatDuration = formatDuration;
|
|
110
|
+
exports.getCurrentOffsetMinutes = getCurrentOffsetMinutes;
|
|
111
|
+
exports.getDateRange = getDateRange;
|
|
112
|
+
exports.hoursToMilliseconds = hoursToMilliseconds;
|
|
113
|
+
exports.isoUtcToLocalDateTimeInput = isoUtcToLocalDateTimeInput;
|
|
114
|
+
exports.localDateTimeInputToIsoUtc = localDateTimeInputToIsoUtc;
|
|
115
|
+
exports.millisecondsToHours = millisecondsToHours;
|
|
116
|
+
exports.nowLocalDateTimeInputValue = nowLocalDateTimeInputValue;
|
|
117
|
+
exports.timespanToMilliseconds = timespanToMilliseconds;
|
|
118
|
+
exports.toHHmm = toHHmm;
|
|
119
|
+
exports.toHHmmss = toHHmmss;
|
|
120
|
+
//# sourceMappingURL=chunk-LT7I4JVK.cjs.map
|
|
121
|
+
//# sourceMappingURL=chunk-LT7I4JVK.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/date/duration.ts","../src/date/wire.ts","../src/date/range.ts"],"names":["MS_PER_HOUR","MS_PER_MINUTE","MS_PER_DAY","format","isValid","DATE_FORMAT","subMonths","subYears","subDays"],"mappings":";;;;;;AASO,IAAM,sBAAA,GAAyB,CAAC,QAAA,KAA6B;AAClE,EAAA,IAAI,CAAC,UAAU,OAAO,CAAA;AAEtB,EAAA,MAAM,CAAC,CAAA,GAAI,GAAA,EAAK,CAAA,GAAI,GAAA,EAAK,IAAI,GAAG,CAAA,GAAI,QAAA,CAAS,KAAA,CAAM,GAAG,CAAA;AACtD,EAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,CAAA,IAAK,GAAA,EAAK,EAAE,CAAA;AACnC,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,CAAA,IAAK,GAAA,EAAK,EAAE,CAAA;AACrC,EAAA,MAAM,OAAA,GAAU,QAAA,CAAS,CAAA,IAAK,GAAA,EAAK,EAAE,CAAA;AAErC,EAAA,OAAA,CACG,MAAM,KAAK,CAAA,GAAI,IAAI,KAAA,IAAS,EAAA,GAAK,KAAK,GAAA,GAAA,CACtC,KAAA,CAAM,OAAO,CAAA,GAAI,CAAA,GAAI,WAAW,EAAA,GAAK,GAAA,GAAA,CACrC,MAAM,OAAO,CAAA,GAAI,IAAI,OAAA,IAAW,GAAA;AAErC;AAGO,IAAM,mBAAA,GAAsB,CAAC,EAAA,KAAuB,EAAA,GAAKA;AAGzD,IAAM,mBAAA,GAAsB,CAAC,KAAA,KAA0B,KAAA,GAAQA;AAY/D,IAAM,cAAA,GAAiB,CAAC,EAAA,KAA0C;AACvE,EAAA,IAAI,EAAA,IAAM,QAAQ,MAAA,CAAO,KAAA,CAAM,EAAE,CAAA,IAAK,EAAA,GAAK,GAAG,OAAO,QAAA;AACrD,EAAA,IAAI,EAAA,GAAKC,iCAAe,OAAO,IAAA;AAE/B,EAAA,MAAM,IAAA,GAAO,IAAA,CAAK,KAAA,CAAM,EAAA,GAAKC,4BAAU,CAAA;AACvC,EAAA,MAAM,KAAA,GAAQ,IAAA,CAAK,KAAA,CAAO,EAAA,GAAKA,+BAAcF,6BAAW,CAAA;AACxD,EAAA,MAAM,OAAA,GAAU,IAAA,CAAK,KAAA,CAAO,EAAA,GAAKA,gCAAeC,+BAAa,CAAA;AAE7D,EAAA,MAAM,QAAkB,EAAC;AACzB,EAAA,IAAI,IAAA,EAAM,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,IAAI,CAAA,CAAA,CAAG,CAAA;AAC/B,EAAA,IAAI,KAAA,EAAO,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,CAAA,CAAG,CAAA;AACjC,EAAA,IAAI,OAAA,EAAS,KAAA,CAAM,IAAA,CAAK,CAAA,EAAG,OAAO,CAAA,CAAA,CAAG,CAAA;AACrC,EAAA,OAAO,KAAA,CAAM,KAAK,GAAG,CAAA;AACvB;AAOO,SAAS,MAAA,CAAO,OAAkC,QAAA,EAA0B;AACjF,EAAA,IAAI,CAAC,KAAA,IAAS,KAAA,CAAM,MAAA,GAAS,GAAG,OAAO,QAAA;AACvC,EAAA,OAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA;AACzB;AAQO,SAAS,SAAS,KAAA,EAA8B;AACrD,EAAA,MAAM,OAAA,GAAU,MAAM,IAAA,EAAK;AAC3B,EAAA,IAAI,CAAC,SAAS,OAAO,IAAA;AACrB,EAAA,OAAO,OAAA,CAAQ,MAAA,KAAW,CAAA,GAAI,CAAA,EAAG,OAAO,CAAA,GAAA,CAAA,GAAQ,OAAA;AAClD;AClDO,SAAS,iBAAiB,KAAA,EAAsD;AACrF,EAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,EAAA,MAAM,MAAA,GAAS,KAAA,CAAM,QAAA,CAAS,GAAG,CAAA,GAAI,IAAI,IAAA,CAAK,KAAK,CAAA,mBAAI,IAAI,IAAA,CAAK,CAAA,EAAG,KAAK,CAAA,UAAA,CAAY,CAAA;AACpF,EAAA,OAAO,MAAA,CAAO,MAAM,MAAA,CAAO,OAAA,EAAS,CAAA,GAAI,MAAA,GAAY,OAAO,WAAA,EAAY;AACzE;AAOO,SAAS,0BAAA,GAAqC;AACnD,EAAA,OAAOE,cAAA,iBAAO,IAAI,IAAA,EAAK,EAAG,oBAAoB,CAAA;AAChD;AAQO,SAAS,2BAA2B,KAAA,EAAsD;AAC/F,EAAA,IAAI,CAAC,OAAO,OAAO,MAAA;AACnB,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,KAAK,CAAA;AAC7B,EAAA,IAAI,CAACC,eAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,MAAA;AAC7B,EAAA,OAAO,OAAO,WAAA,EAAY;AAC5B;AAQO,SAAS,aAAa,KAAA,EAAuB;AAClD,EAAA,OAAO,wBAAA,CAAyB,IAAA,CAAK,KAAK,CAAA,GAAI,QAAQ,KAAA,GAAQ,GAAA;AAChE;AAUO,SAAS,0BAAA,CAA2B,KAAA,EAAkC,QAAA,GAAW,EAAA,EAAY;AAClG,EAAA,IAAI,CAAC,OAAO,OAAO,QAAA;AACnB,EAAA,MAAM,MAAA,GAAS,IAAI,IAAA,CAAK,YAAA,CAAa,KAAK,CAAC,CAAA;AAC3C,EAAA,IAAI,CAACA,eAAA,CAAQ,MAAM,CAAA,EAAG,OAAO,QAAA;AAC7B,EAAA,OAAOD,cAAA,CAAO,QAAQ,oBAAoB,CAAA;AAC5C;AC5DO,IAAM,YAAA,GAAe,CAAC,UAAA,EAAoB,UAAA,GAAqBE,6BAAA,KAA2B;AAC/F,EAAA,MAAM,KAAA,uBAAY,IAAA,EAAK;AACvB,EAAA,QAAQ,UAAA;AAAY,IAClB,KAAK,OAAA;AACH,MAAA,OAAO,EAAE,SAAA,EAAWF,cAAAA,CAAOG,iBAAA,CAAU,KAAA,EAAO,CAAC,CAAA,EAAG,UAAU,CAAA,EAAG,OAAA,EAASH,cAAAA,CAAO,KAAA,EAAO,UAAU,CAAA,EAAE;AAAA,IAClG,KAAK,SAAA;AACH,MAAA,OAAO,EAAE,SAAA,EAAWA,cAAAA,CAAOG,iBAAA,CAAU,KAAA,EAAO,CAAC,CAAA,EAAG,UAAU,CAAA,EAAG,OAAA,EAASH,cAAAA,CAAO,KAAA,EAAO,UAAU,CAAA,EAAE;AAAA,IAClG,KAAK,MAAA;AACH,MAAA,OAAO,EAAE,SAAA,EAAWA,cAAAA,CAAOI,gBAAA,CAAS,KAAA,EAAO,CAAC,CAAA,EAAG,UAAU,CAAA,EAAG,OAAA,EAASJ,cAAAA,CAAO,KAAA,EAAO,UAAU,CAAA,EAAE;AAAA,IACjG,KAAK,MAAA;AAAA,IACL;AACE,MAAA,OAAO,EAAE,SAAA,EAAWA,cAAAA,CAAOK,eAAA,CAAQ,KAAA,EAAO,CAAC,CAAA,EAAG,UAAU,CAAA,EAAG,OAAA,EAASL,cAAAA,CAAO,KAAA,EAAO,UAAU,CAAA,EAAE;AAAA;AAEpG;AAGO,SAAS,iBAAiB,IAAA,EAA4C;AAC3E,EAAA,MAAM,EAAA,GAAA,iBAAK,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAClC,EAAA,MAAM,IAAA,GAAO,IAAI,IAAA,CAAK,IAAA,CAAK,KAAI,GAAI,IAAA,GAAO,KAAU,CAAA,CAAE,WAAA,EAAY;AAClE,EAAA,OAAO,EAAE,MAAM,EAAA,EAAG;AACpB;AAOO,SAAS,oBAAA,CAAqB,KAAA,mBAAc,IAAI,IAAA,EAAK,EAA2C;AACrG,EAAA,MAAM,IAAA,GAAO,MAAM,cAAA,EAAe;AAClC,EAAA,MAAM,KAAA,GAAQ,MAAM,WAAA,EAAY;AAChC,EAAA,OAAO;AAAA,IACL,OAAA,EAAS,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,MAAM,KAAA,EAAO,CAAC,CAAC,CAAA,CAAE,WAAA,EAAY;AAAA,IACxD,SAAA,EAAW,IAAI,IAAA,CAAK,IAAA,CAAK,GAAA,CAAI,IAAA,EAAM,KAAA,GAAQ,CAAA,EAAG,CAAC,CAAC,CAAA,CAAE,WAAA;AAAY,GAChE;AACF;AAUO,SAAS,wBAAwB,IAAA,EAA6B;AACnE,EAAA,IAAI;AACF,IAAA,MAAM,KAAA,GAAQ,IAAI,IAAA,CAAK,cAAA,CAAe,IAAA,EAAM;AAAA,MAC1C,QAAA,EAAU,IAAA;AAAA,MACV,YAAA,EAAc;AAAA,KACf,CAAA,CAAE,aAAA,iBAAc,IAAI,MAAM,CAAA;AAC3B,IAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,IAAA,KAAS,cAAc,CAAA,EAAG,KAAA,IAAS,EAAA;AACrE,IAAA,IAAI,KAAA,KAAU,KAAA,IAAS,KAAA,KAAU,KAAA,EAAO,OAAO,CAAA;AAC/C,IAAA,MAAM,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,0BAA0B,CAAA;AAChD,IAAA,IAAI,CAAC,GAAG,OAAO,IAAA;AACf,IAAA,MAAM,IAAA,GAAO,CAAA,CAAE,CAAC,CAAA,KAAM,MAAM,CAAA,GAAI,CAAA,CAAA;AAChC,IAAA,OAAO,IAAA,IAAQ,QAAA,CAAS,CAAA,CAAE,CAAC,CAAA,EAAG,EAAE,CAAA,GAAI,EAAA,GAAK,QAAA,CAAS,CAAA,CAAE,CAAC,CAAA,EAAG,EAAE,CAAA,CAAA;AAAA,EAC5D,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF","file":"chunk-LT7I4JVK.cjs","sourcesContent":["/**\n * Pure duration / timespan helpers — no external dependencies.\n *\n * Cover the two duration shapes that flow across the EthisysCore wire: a\n * `\"HH:mm:ss\"` timespan string (.NET `TimeSpan`) and a millisecond count.\n */\nimport { MS_PER_DAY, MS_PER_HOUR, MS_PER_MINUTE } from \"./constants\";\n\n/** Converts a `\"HH:mm:ss\"` timespan string to milliseconds. */\nexport const timespanToMilliseconds = (timespan: string): number => {\n if (!timespan) return 0;\n\n const [h = \"0\", m = \"0\", s = \"0\"] = timespan.split(\":\");\n const hours = parseInt(h || \"0\", 10);\n const minutes = parseInt(m || \"0\", 10);\n const seconds = parseInt(s || \"0\", 10);\n\n return (\n (isNaN(hours) ? 0 : hours) * 60 * 60 * 1000 +\n (isNaN(minutes) ? 0 : minutes) * 60 * 1000 +\n (isNaN(seconds) ? 0 : seconds) * 1000\n );\n};\n\n/** Converts milliseconds to hours (as a float). */\nexport const millisecondsToHours = (ms: number): number => ms / MS_PER_HOUR;\n\n/** Converts hours to milliseconds. */\nexport const hoursToMilliseconds = (hours: number): number => hours * MS_PER_HOUR;\n\n/**\n * The one human-readable duration formatter. Takes a MILLISECOND count and renders\n * the largest non-zero units as `Xd Xh Xm` (e.g. `4h 15m`, `2d 3h`, `45m`). Zero is\n * a real value (`0m`); a nullish/NaN/negative input is missing data and returns the\n * em-dash. Minute granularity — seconds are not shown.\n *\n * Feed non-millisecond inputs through the converters in this module:\n * `formatDuration(timespanToMilliseconds(\"04:15:00\"))` // \"HH:mm:ss\" TimeSpan\n * `formatDuration(hoursToMilliseconds(2.5))` // fractional hours\n */\nexport const formatDuration = (ms: number | null | undefined): string => {\n if (ms == null || Number.isNaN(ms) || ms < 0) return \"—\";\n if (ms < MS_PER_MINUTE) return \"0m\";\n\n const days = Math.floor(ms / MS_PER_DAY);\n const hours = Math.floor((ms % MS_PER_DAY) / MS_PER_HOUR);\n const minutes = Math.floor((ms % MS_PER_HOUR) / MS_PER_MINUTE);\n\n const parts: string[] = [];\n if (days) parts.push(`${days}d`);\n if (hours) parts.push(`${hours}h`);\n if (minutes) parts.push(`${minutes}m`);\n return parts.join(\" \");\n};\n\n/**\n * Trims an API-supplied `\"HH:mm:ss\"` down to the `\"HH:mm\"` string used by\n * MUI `TimePicker`-backed forms. Returns the supplied fallback when the value\n * is missing or shorter than five characters.\n */\nexport function toHHmm(value: string | null | undefined, fallback: string): string {\n if (!value || value.length < 5) return fallback;\n return value.slice(0, 5);\n}\n\n/**\n * Serialises a non-empty `\"HH:mm\"` / `\"HH:mm:ss\"` value back to the canonical\n * `\"HH:mm:ss\"` API format. Returns `null` for empty input so callers can block\n * the save rather than silently persisting midnight — an empty TimePicker\n * (cleared via keyboard) is an unsaved edit, not a legitimate `00:00:00` value.\n */\nexport function toHHmmss(value: string): string | null {\n const trimmed = value.trim();\n if (!trimmed) return null;\n return trimmed.length === 5 ? `${trimmed}:00` : trimmed;\n}\n","import { format, isValid } from \"date-fns\";\n\n// ---------------------------------------------------------------------------\n// Wire-boundary helpers\n// ---------------------------------------------------------------------------\n//\n// HTML's `<input type=\"date\">` and `<input type=\"datetime-local\">` emit naive\n// local strings with NO timezone suffix. Sending those raw to a .NET backend\n// that deserialises into `DateTimeOffset` parses them using the BE process's\n// local offset (UTC on most container hosts), silently misinterpreting the\n// user's wall-clock as UTC and shifting the stored timestamp by the user's\n// offset (e.g. losing the first hour of the local day during BST). These\n// helpers bridge cleanly between the input shapes and full ISO-8601 UTC.\n//\n// All are pure and timezone-aware via the runtime `Date` object.\n\n/**\n * Normalises a date-only value from `<input type=\"date\">` (`yyyy-MM-dd`) into a\n * canonical UTC ISO-8601 timestamp at midnight UTC, e.g.\n * `2026-07-14` → `2026-07-14T00:00:00.000Z`. Use at the API-payload boundary\n * for fields the BE types as `DateTimeOffset` — a bare date-only string risks\n * ambiguous/failed deserialisation. A value that is already a full ISO\n * timestamp is passed through unchanged. Empty/invalid → `undefined` so callers\n * omit the field (BE clears it).\n */\nexport function dateOnlyToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = value.includes(\"T\") ? new Date(value) : new Date(`${value}T00:00:00Z`);\n return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString();\n}\n\n/**\n * Returns the current local wall-clock as a `yyyy-MM-ddTHH:mm` string — the\n * format an `<input type=\"datetime-local\">` element expects. Minute precision;\n * seconds and timezone deliberately omitted.\n */\nexport function nowLocalDateTimeInputValue(): string {\n return format(new Date(), \"yyyy-MM-dd'T'HH:mm\");\n}\n\n/**\n * Converts a naive local-time string from `<input type=\"datetime-local\">`\n * (shape `yyyy-MM-ddTHH:mm` or `yyyy-MM-ddTHH:mm:ss`) into a full ISO-8601 UTC\n * string with `Z` suffix that .NET `DateTimeOffset` cannot ambiguously\n * interpret. Empty / undefined inputs pass through as `undefined`.\n */\nexport function localDateTimeInputToIsoUtc(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n const parsed = new Date(value);\n if (!isValid(parsed)) return undefined;\n return parsed.toISOString();\n}\n\n/**\n * Ensures an ISO-8601 string is treated as UTC by appending a `Z` suffix when\n * no explicit offset or UTC indicator is present. The backend may emit\n * timestamps like `2026-05-26T06:21:00` (no Z), which JS would otherwise parse\n * as local time.\n */\nexport function ensureUtcIso(value: string): string {\n return /[Zz]|[+-]\\d{2}:?\\d{2}$/.test(value) ? value : value + \"Z\";\n}\n\n/**\n * Converts an ISO-8601 string (with or without explicit offset) into the naive\n * local-time shape `yyyy-MM-ddTHH:mm` expected by `<input type=\"datetime-local\">`.\n * Returns the supplied fallback (default empty string) when the input is\n * missing or unparseable. When `value` carries no offset or `Z` suffix it is\n * treated as UTC before converting to local time, so the edit form displays the\n * correct time regardless of the user's timezone.\n */\nexport function isoUtcToLocalDateTimeInput(value: string | null | undefined, fallback = \"\"): string {\n if (!value) return fallback;\n const parsed = new Date(ensureUtcIso(value));\n if (!isValid(parsed)) return fallback;\n return format(parsed, \"yyyy-MM-dd'T'HH:mm\");\n}\n","import { format, subDays, subMonths, subYears } from \"date-fns\";\n\nimport { DATE_FORMAT } from \"./constants\";\n\nexport interface DateRange {\n startDate: string;\n endDate: string;\n}\n\n/**\n * Returns a `{ startDate, endDate }` date range for a named relative period,\n * formatted with `dateFormat` (defaults to the ISO date-only format). Unknown\n * periods fall back to the last week.\n * @param timePeriod - one of `week` | `month` | `quarter` | `year`\n * @param dateFormat - output format for both bounds (defaults to `DATE_FORMAT`)\n */\nexport const getDateRange = (timePeriod: string, dateFormat: string = DATE_FORMAT): DateRange => {\n const today = new Date();\n switch (timePeriod) {\n case \"month\":\n return { startDate: format(subMonths(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"quarter\":\n return { startDate: format(subMonths(today, 3), dateFormat), endDate: format(today, dateFormat) };\n case \"year\":\n return { startDate: format(subYears(today, 1), dateFormat), endDate: format(today, dateFormat) };\n case \"week\":\n default:\n return { startDate: format(subDays(today, 7), dateFormat), endDate: format(today, dateFormat) };\n }\n};\n\n/** Computes an ISO timestamp range from a number of days back to now. */\nexport function computeDateRange(days: number): { from: string; to: string } {\n const to = new Date().toISOString();\n const from = new Date(Date.now() - days * 86_400_000).toISOString();\n return { from, to };\n}\n\n/**\n * Returns the current calendar month as a half-open UTC range `[fromUtc, beforeUtc)`,\n * suitable for \"this month\" count filters. `fromUtc` is the first instant of the\n * month; `beforeUtc` is the first instant of the next month (exclusive).\n */\nexport function currentMonthRangeUtc(today: Date = new Date()): { fromUtc: string; beforeUtc: string } {\n const year = today.getUTCFullYear();\n const month = today.getUTCMonth();\n return {\n fromUtc: new Date(Date.UTC(year, month, 1)).toISOString(),\n beforeUtc: new Date(Date.UTC(year, month + 1, 1)).toISOString(),\n };\n}\n\n/**\n * Computes the current UTC-offset minutes for a given IANA zone name using the\n * runtime's `Intl` implementation. Returns `null` when the zone is unknown.\n *\n * Used by timezone auto-populate fallbacks so a browser reporting an IANA zone\n * that isn't in the backend seed (e.g. `Europe/London` during BST) can still\n * match against a seeded zone sharing the same current offset.\n */\nexport function getCurrentOffsetMinutes(iana: string): number | null {\n try {\n const parts = new Intl.DateTimeFormat(\"en\", {\n timeZone: iana,\n timeZoneName: \"longOffset\",\n }).formatToParts(new Date());\n const label = parts.find((p) => p.type === \"timeZoneName\")?.value ?? \"\";\n if (label === \"GMT\" || label === \"UTC\") return 0;\n const m = label.match(/GMT([+-])(\\d{2}):(\\d{2})/);\n if (!m) return null;\n const sign = m[1] === \"+\" ? 1 : -1;\n return sign * (parseInt(m[2], 10) * 60 + parseInt(m[3], 10));\n } catch {\n return null;\n }\n}\n"]}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { create, apply } from 'mutative';
|
|
2
|
+
|
|
3
|
+
// src/patch/index.ts
|
|
4
|
+
function generateJsonPatch(original, modified) {
|
|
5
|
+
if (!isSameType(original, modified)) {
|
|
6
|
+
if (original === modified) return [];
|
|
7
|
+
return [{ op: "replace", path: "", value: modified }];
|
|
8
|
+
}
|
|
9
|
+
const [, patches] = create(
|
|
10
|
+
original,
|
|
11
|
+
(draft) => {
|
|
12
|
+
applyChanges(draft, modified);
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
enablePatches: {
|
|
16
|
+
pathAsArray: false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
);
|
|
20
|
+
return patches.map(
|
|
21
|
+
(patch) => ({
|
|
22
|
+
op: patch.op,
|
|
23
|
+
path: patch.path,
|
|
24
|
+
// explicitly check for value existence to satisfy strict null checks if needed
|
|
25
|
+
...patch.op !== "remove" ? { value: patch.value } : {}
|
|
26
|
+
})
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
function applyChanges(draft, modified) {
|
|
30
|
+
if (isDeepEqual(draft, modified)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (Array.isArray(draft) && Array.isArray(modified)) {
|
|
34
|
+
const draftArray = draft;
|
|
35
|
+
draftArray.length = 0;
|
|
36
|
+
draftArray.push(...modified);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (isPlainObject(draft) && isPlainObject(modified)) {
|
|
40
|
+
const draftObj = draft;
|
|
41
|
+
const modifiedObj = modified;
|
|
42
|
+
for (const key of Object.keys(draftObj)) {
|
|
43
|
+
if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {
|
|
44
|
+
delete draftObj[key];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const key of Object.keys(modifiedObj)) {
|
|
48
|
+
if (!isDeepEqual(draftObj[key], modifiedObj[key])) {
|
|
49
|
+
draftObj[key] = modifiedObj[key];
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function isDeepEqual(a, b) {
|
|
56
|
+
if (a === b) return true;
|
|
57
|
+
if (a === null || a === void 0 || b === null || b === void 0) {
|
|
58
|
+
return a === b;
|
|
59
|
+
}
|
|
60
|
+
if (typeof a !== typeof b) return false;
|
|
61
|
+
if (a instanceof Date && b instanceof Date) {
|
|
62
|
+
return a.getTime() === b.getTime();
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
65
|
+
if (a.length !== b.length) return false;
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
if (!isDeepEqual(a[i], b[i])) return false;
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
72
|
+
const objA = a;
|
|
73
|
+
const objB = b;
|
|
74
|
+
const keysA = Object.keys(objA);
|
|
75
|
+
const keysB = Object.keys(objB);
|
|
76
|
+
if (keysA.length !== keysB.length) return false;
|
|
77
|
+
for (const key of keysA) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
|
|
79
|
+
if (!isDeepEqual(objA[key], objB[key])) return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
function isPlainObject(value) {
|
|
86
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date);
|
|
87
|
+
}
|
|
88
|
+
function isSameType(a, b) {
|
|
89
|
+
if (Array.isArray(a)) return Array.isArray(b);
|
|
90
|
+
if (Array.isArray(b)) return false;
|
|
91
|
+
if (a instanceof Date) return b instanceof Date;
|
|
92
|
+
if (a && typeof a === "object" && b && typeof b === "object") return true;
|
|
93
|
+
return typeof a === typeof b;
|
|
94
|
+
}
|
|
95
|
+
function applyJsonPatch(original, patches) {
|
|
96
|
+
const rootReplacement = patches.find((patch) => patch.path === "" && patch.op === "replace");
|
|
97
|
+
if (rootReplacement) {
|
|
98
|
+
return rootReplacement.value;
|
|
99
|
+
}
|
|
100
|
+
const nonRootPatches = patches.filter((patch) => patch.path !== "");
|
|
101
|
+
if (nonRootPatches.length === 0) {
|
|
102
|
+
return original;
|
|
103
|
+
}
|
|
104
|
+
return apply(original, nonRootPatches);
|
|
105
|
+
}
|
|
106
|
+
function isNullOrEmpty(patches) {
|
|
107
|
+
return !patches || patches.length === 0;
|
|
108
|
+
}
|
|
109
|
+
function hasPatches(patches) {
|
|
110
|
+
return patches.length > 0;
|
|
111
|
+
}
|
|
112
|
+
function deepClone(obj) {
|
|
113
|
+
if (typeof structuredClone === "function") {
|
|
114
|
+
return structuredClone(obj);
|
|
115
|
+
}
|
|
116
|
+
return JSON.parse(JSON.stringify(obj));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export { applyJsonPatch, deepClone, generateJsonPatch, hasPatches, isNullOrEmpty };
|
|
120
|
+
//# sourceMappingURL=chunk-OEDAX6WN.js.map
|
|
121
|
+
//# sourceMappingURL=chunk-OEDAX6WN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/patch/index.ts"],"names":[],"mappings":"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","file":"chunk-OEDAX6WN.js","sourcesContent":["/**\n * RFC 6902 JSON Patch helpers shared by the EthisysCore monolith and plugins.\n *\n * The wire shape (`JsonPatchOperation`) matches the backend SDK's\n * `EthisysCore.Plugin.Sdk.JsonPatchOperation` record, so a patch produced here by\n * {@link generateJsonPatch} is applied by `IEntityPatchService` on the server.\n */\nimport { apply, create } from \"mutative\";\n\n/** JSON Patch operation type. */\nexport type PatchOperation = \"add\" | \"remove\" | \"replace\";\n\n/**\n * A single JSON Patch operation (RFC 6902 compatible; mutative's format with a string path).\n */\nexport interface JsonPatchOperation {\n /** The operation to be performed. */\n op: PatchOperation;\n /** JSON Pointer path (RFC 6902 format). */\n path: string;\n /** The value for add/replace operations. */\n value?: unknown;\n /** Source pointer for move/copy operations (backend `From`); unused by the generate/apply helpers. */\n from?: string;\n}\n\n/** Array of JSON Patch operations. */\nexport type JsonPatch = JsonPatchOperation[];\n\n/**\n * Generates JSON Patch operations (RFC 6902).\n * Strictly typed, handles Date objects, and ignores key order.\n */\nexport function generateJsonPatch<T>(original: T, modified: T): JsonPatch {\n // If types are completely different (e.g. Array vs Object), strict replacement.\n if (!isSameType(original, modified)) {\n // If both are strictly equal (e.g. both null), returns empty\n if (original === modified) return [];\n // If different, replace root\n return [{ op: \"replace\", path: \"\", value: modified }];\n }\n\n const [, patches] = create(\n original,\n (draft) => {\n // We can safely cast draft to unknown here because we control the recursion\n applyChanges(draft, modified);\n },\n {\n enablePatches: {\n pathAsArray: false,\n },\n },\n );\n\n return patches.map(\n (patch): JsonPatchOperation => ({\n op: patch.op as \"add\" | \"remove\" | \"replace\",\n path: patch.path as string,\n // explicitly check for value existence to satisfy strict null checks if needed\n ...(patch.op !== \"remove\" ? { value: patch.value } : {}),\n }),\n );\n}\n\n/**\n * Recursively applies changes to the Mutative draft.\n * Uses 'unknown' and Type Guards instead of 'any'.\n */\nfunction applyChanges(draft: unknown, modified: unknown): void {\n // This handles Primitives, Dates, and identical References early\n if (isDeepEqual(draft, modified)) {\n return;\n }\n\n // We explicitly check isArray to safely access .length and .push\n if (Array.isArray(draft) && Array.isArray(modified)) {\n const draftArray = draft as Array<unknown>;\n // Clear and Replace approach (safer for DTOs than diffing indices)\n draftArray.length = 0;\n draftArray.push(...modified);\n return;\n }\n\n // We use type guards to ensure we can treat them as dictionaries\n if (isPlainObject(draft) && isPlainObject(modified)) {\n const draftObj = draft;\n const modifiedObj = modified;\n\n // A. Remove keys that don't exist in modified\n for (const key of Object.keys(draftObj)) {\n if (!Object.prototype.hasOwnProperty.call(modifiedObj, key)) {\n delete draftObj[key];\n }\n }\n\n // B. Add or update top-level keys. We deliberately do NOT recurse into nested objects: the backend\n // patch applier (IEntityPatchService) supports only top-level property operations, so a changed\n // nested object is replaced wholesale (path \"/key\"), never diffed into \"/key/child\" paths that the\n // server would reject.\n for (const key of Object.keys(modifiedObj)) {\n if (!isDeepEqual(draftObj[key], modifiedObj[key])) {\n draftObj[key] = modifiedObj[key];\n }\n }\n return;\n }\n}\n\n/**\n * Handles: Primitives, Arrays, Dates, Objects (order-independent), null/undefined\n */\nfunction isDeepEqual(a: unknown, b: unknown): boolean {\n // Strict Reference & Primitive Check\n if (a === b) return true;\n\n // Null/Undefined Checks\n if (a === null || a === undefined || b === null || b === undefined) {\n return a === b;\n }\n\n // Type Mismatch\n if (typeof a !== typeof b) return false;\n\n // Date Objects\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() === b.getTime();\n }\n\n // Arrays\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!isDeepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n\n // Objects\n if (typeof a === \"object\" && typeof b === \"object\") {\n // Treat as Record<string, unknown> safely\n const objA = a as Record<string, unknown>;\n const objB = b as Record<string, unknown>;\n\n const keysA = Object.keys(objA);\n const keysB = Object.keys(objB);\n\n if (keysA.length !== keysB.length) return false;\n\n for (const key of keysA) {\n if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;\n if (!isDeepEqual(objA[key], objB[key])) return false;\n }\n return true;\n }\n\n return false;\n}\n\n/**\n * Type Guard: Checks if value is a plain object (not null, array, or Date)\n * Allows safe casting to Record<string, unknown>\n */\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n !Array.isArray(value) &&\n !(value instanceof Date)\n );\n}\n\n/**\n * Helper: Checks if two values share a high-level type category\n */\nfunction isSameType(a: unknown, b: unknown): boolean {\n if (Array.isArray(a)) return Array.isArray(b);\n if (Array.isArray(b)) return false;\n if (a instanceof Date) return b instanceof Date;\n if (a && typeof a === \"object\" && b && typeof b === \"object\") return true;\n return typeof a === typeof b;\n}\n\n/**\n * Applies JSON Patch operations to an object using Mutative.\n * @param original - The original object\n * @param patches - Array of JSON Patch operations to apply\n * @returns The patched object (immutable)\n */\nexport function applyJsonPatch<T>(original: T, patches: JsonPatch): T {\n // Check for root replacement first - if found, it should be the only operation\n const rootReplacement = patches.find((patch) => patch.path === \"\" && patch.op === \"replace\");\n if (rootReplacement) {\n return rootReplacement.value as T;\n }\n\n // Filter out any root operations that might interfere and apply the rest\n const nonRootPatches = patches.filter((patch) => patch.path !== \"\");\n if (nonRootPatches.length === 0) {\n return original;\n }\n\n // Mutative's apply function expects patches in RFC 6902 format, which is what we have\n return apply(original as Record<string, unknown>, nonRootPatches) as T;\n}\n\n/**\n * Checks if a patch array is empty (null, undefined, or empty array).\n * @param patches - Array of patch operations or null/undefined\n * @returns True if patches is null, undefined, or empty array\n */\nexport function isNullOrEmpty(patches: JsonPatch | null | undefined): boolean {\n return !patches || patches.length === 0;\n}\n\n/**\n * Checks if a patch array has any operations.\n * @param patches - Array of patch operations\n * @returns True if patches array has operations, false if empty\n */\nexport function hasPatches(patches: JsonPatch): boolean {\n return patches.length > 0;\n}\n\n/**\n * Creates a deep clone of an object.\n * Useful for creating the \"modified\" version before generating patches.\n * @param obj - Object to clone\n * @returns Deep cloned object\n */\nexport function deepClone<T>(obj: T): T {\n // structuredClone preserves Dates (and undefined); fall back to JSON clone on legacy runtimes.\n if (typeof structuredClone === \"function\") {\n return structuredClone(obj);\n }\n return JSON.parse(JSON.stringify(obj));\n}\n"]}
|