@caffeinebounce/ui 0.62.2
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/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/FinancialStatementTable-BNUrdTU4.d.cts +249 -0
- package/dist/FinancialStatementTable-BNUrdTU4.d.ts +249 -0
- package/dist/NewsletterSignup-BPreFi5W.d.cts +37 -0
- package/dist/NewsletterSignup-BPreFi5W.d.ts +37 -0
- package/dist/TestimonialsSection-DtPu2OeR.d.ts +736 -0
- package/dist/TestimonialsSection-z-yotRfv.d.cts +736 -0
- package/dist/UnifiedTableControls-CG299y9h.d.ts +1233 -0
- package/dist/UnifiedTableControls-CIjRpNhy.d.cts +1233 -0
- package/dist/blog.d.cts +99 -0
- package/dist/blog.d.ts +99 -0
- package/dist/blog.js +292 -0
- package/dist/blog.mjs +279 -0
- package/dist/button-BHnoRUkF.d.cts +34 -0
- package/dist/button-BHnoRUkF.d.ts +34 -0
- package/dist/charts.d.cts +80 -0
- package/dist/charts.d.ts +80 -0
- package/dist/charts.js +19 -0
- package/dist/charts.mjs +2 -0
- package/dist/chunk-33JR5SYE.js +44 -0
- package/dist/chunk-3NDB4YPC.js +247 -0
- package/dist/chunk-5RW4PYZ6.js +1504 -0
- package/dist/chunk-5THF4YP6.mjs +155 -0
- package/dist/chunk-6BUBNZPG.mjs +164 -0
- package/dist/chunk-6DVWMJ3H.mjs +7029 -0
- package/dist/chunk-6TGV2DNP.js +120 -0
- package/dist/chunk-72JGQ7IJ.mjs +1498 -0
- package/dist/chunk-7DHKQODW.mjs +2932 -0
- package/dist/chunk-7IKHZKAC.js +160 -0
- package/dist/chunk-ADIDI7AJ.js +11 -0
- package/dist/chunk-BFWX5GJZ.mjs +940 -0
- package/dist/chunk-BMFQG6RA.mjs +5537 -0
- package/dist/chunk-C4UT5V3P.js +65 -0
- package/dist/chunk-CC4KUIKC.js +2004 -0
- package/dist/chunk-D3QXPL47.mjs +1814 -0
- package/dist/chunk-DQWHYBV2.js +36 -0
- package/dist/chunk-E42B4GSB.js +993 -0
- package/dist/chunk-ESZ7CBA2.js +1007 -0
- package/dist/chunk-EYJJQVV4.js +111 -0
- package/dist/chunk-FRRS2YZH.mjs +129 -0
- package/dist/chunk-G5TQWWQA.mjs +37 -0
- package/dist/chunk-GAIQJCAZ.js +5612 -0
- package/dist/chunk-GMBGWOCR.mjs +48 -0
- package/dist/chunk-GUSTCU2W.mjs +105 -0
- package/dist/chunk-HBLKVQIQ.mjs +23 -0
- package/dist/chunk-HMAMMC72.js +26 -0
- package/dist/chunk-IMKLN273.mjs +9 -0
- package/dist/chunk-J6OLBSZR.js +250 -0
- package/dist/chunk-JAE45W2T.js +28 -0
- package/dist/chunk-JORVSG52.js +606 -0
- package/dist/chunk-KB7UXIQZ.mjs +984 -0
- package/dist/chunk-KGGQ5NZV.js +186 -0
- package/dist/chunk-KHCC5UKW.mjs +22 -0
- package/dist/chunk-KRRIRS6A.mjs +1945 -0
- package/dist/chunk-LQCVRWVF.js +48 -0
- package/dist/chunk-MHZXUOGN.mjs +168 -0
- package/dist/chunk-MPZFPU4X.js +349 -0
- package/dist/chunk-MVYRNJA6.js +74 -0
- package/dist/chunk-N6AFNFZR.mjs +565 -0
- package/dist/chunk-OFQLWSUR.mjs +214 -0
- package/dist/chunk-OLAAXLXY.mjs +219 -0
- package/dist/chunk-OZZWDIOY.js +1859 -0
- package/dist/chunk-PZRI43EZ.js +111 -0
- package/dist/chunk-Q256HRYD.js +176 -0
- package/dist/chunk-Q52ZEL62.mjs +223 -0
- package/dist/chunk-R26Y7LGJ.js +142 -0
- package/dist/chunk-RBUYW6QQ.js +39 -0
- package/dist/chunk-RVEFPVZF.mjs +43 -0
- package/dist/chunk-SORWLKOK.js +96 -0
- package/dist/chunk-STEP6C4C.mjs +33 -0
- package/dist/chunk-T5R6HUDG.mjs +22 -0
- package/dist/chunk-T6ZBA6VJ.js +7109 -0
- package/dist/chunk-UM7WMXIU.js +2995 -0
- package/dist/chunk-VBD4Q5LL.mjs +117 -0
- package/dist/chunk-WOITB4JL.js +195 -0
- package/dist/chunk-YKZEKJMZ.mjs +71 -0
- package/dist/chunk-YL7RZRHM.mjs +103 -0
- package/dist/chunk-YSLD2HLP.mjs +346 -0
- package/dist/chunk-ZFLZA7UP.mjs +140 -0
- package/dist/chunk-ZKUIUE7W.mjs +26 -0
- package/dist/chunk-ZOA7B32D.js +248 -0
- package/dist/data-table.d.cts +46 -0
- package/dist/data-table.d.ts +46 -0
- package/dist/data-table.js +270 -0
- package/dist/data-table.mjs +13 -0
- package/dist/editor.d.cts +89 -0
- package/dist/editor.d.ts +89 -0
- package/dist/editor.js +10 -0
- package/dist/editor.mjs +1 -0
- package/dist/forms.d.cts +142 -0
- package/dist/forms.d.ts +142 -0
- package/dist/forms.js +24 -0
- package/dist/forms.mjs +11 -0
- package/dist/hooks-8ORd7T1A.d.cts +751 -0
- package/dist/hooks-B2jU1GVT.d.ts +751 -0
- package/dist/hooks.d.cts +6 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +99 -0
- package/dist/hooks.mjs +10 -0
- package/dist/index.d.cts +3141 -0
- package/dist/index.d.ts +3141 -0
- package/dist/index.js +7332 -0
- package/dist/index.mjs +5876 -0
- package/dist/layouts-DPfDDF53.d.cts +1001 -0
- package/dist/layouts-DPfDDF53.d.ts +1001 -0
- package/dist/layouts.d.cts +4 -0
- package/dist/layouts.d.ts +4 -0
- package/dist/layouts.js +100 -0
- package/dist/layouts.mjs +11 -0
- package/dist/marketing-3d.d.cts +51 -0
- package/dist/marketing-3d.d.ts +51 -0
- package/dist/marketing-3d.js +553 -0
- package/dist/marketing-3d.mjs +526 -0
- package/dist/marketing.d.cts +583 -0
- package/dist/marketing.d.ts +583 -0
- package/dist/marketing.js +299 -0
- package/dist/marketing.mjs +10 -0
- package/dist/media.d.cts +29 -0
- package/dist/media.d.ts +29 -0
- package/dist/media.js +189 -0
- package/dist/media.mjs +182 -0
- package/dist/navigation.d.cts +591 -0
- package/dist/navigation.d.ts +591 -0
- package/dist/navigation.js +108 -0
- package/dist/navigation.mjs +15 -0
- package/dist/portal.d.cts +58 -0
- package/dist/portal.d.ts +58 -0
- package/dist/portal.js +403 -0
- package/dist/portal.mjs +395 -0
- package/dist/primitives.d.cts +508 -0
- package/dist/primitives.d.ts +508 -0
- package/dist/primitives.js +295 -0
- package/dist/primitives.mjs +14 -0
- package/dist/settings.d.cts +216 -0
- package/dist/settings.d.ts +216 -0
- package/dist/settings.js +34 -0
- package/dist/settings.mjs +9 -0
- package/dist/shared-Dqn36QHZ.d.cts +4 -0
- package/dist/shared-Dqn36QHZ.d.ts +4 -0
- package/dist/styles.css +1213 -0
- package/package.json +189 -0
- package/styles.css +1 -0
|
@@ -0,0 +1,1007 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chunkUM7WMXIU_js = require('./chunk-UM7WMXIU.js');
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var dateFns = require('date-fns');
|
|
6
|
+
var sonner = require('sonner');
|
|
7
|
+
|
|
8
|
+
var DEFAULT_MAX_ERRORS = 20;
|
|
9
|
+
var DEFAULT_STORAGE_KEY = "session_errors";
|
|
10
|
+
var listenersInitialized = false;
|
|
11
|
+
var currentStorageKey = DEFAULT_STORAGE_KEY;
|
|
12
|
+
var currentMaxErrors = DEFAULT_MAX_ERRORS;
|
|
13
|
+
function addError(error) {
|
|
14
|
+
if (typeof window === "undefined") return;
|
|
15
|
+
try {
|
|
16
|
+
const stored = sessionStorage.getItem(currentStorageKey);
|
|
17
|
+
const errors = stored ? JSON.parse(stored) : [];
|
|
18
|
+
const truncatedError = {
|
|
19
|
+
...error,
|
|
20
|
+
stack: error.stack?.slice(0, 2e3)
|
|
21
|
+
};
|
|
22
|
+
errors.push(truncatedError);
|
|
23
|
+
if (errors.length > currentMaxErrors) {
|
|
24
|
+
errors.shift();
|
|
25
|
+
}
|
|
26
|
+
sessionStorage.setItem(currentStorageKey, JSON.stringify(errors));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (e instanceof DOMException && (e.name === "QuotaExceededError" || e.code === 22)) {
|
|
29
|
+
console.warn(
|
|
30
|
+
"[useSessionErrors] sessionStorage quota exceeded. Recent errors may not be tracked."
|
|
31
|
+
);
|
|
32
|
+
} else {
|
|
33
|
+
console.warn("[useSessionErrors] Failed to store error:", e);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function initializeErrorListeners() {
|
|
38
|
+
if (listenersInitialized || typeof window === "undefined") return;
|
|
39
|
+
listenersInitialized = true;
|
|
40
|
+
const handleError = (event) => {
|
|
41
|
+
addError({
|
|
42
|
+
message: event.message || "Unknown error",
|
|
43
|
+
stack: event.error?.stack,
|
|
44
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
45
|
+
type: "error"
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
const handleRejection = (event) => {
|
|
49
|
+
const reason = event.reason;
|
|
50
|
+
addError({
|
|
51
|
+
message: reason instanceof Error ? reason.message : String(reason) || "Unhandled promise rejection",
|
|
52
|
+
stack: reason instanceof Error ? reason.stack : void 0,
|
|
53
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54
|
+
type: "unhandledrejection"
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener("error", handleError);
|
|
58
|
+
window.addEventListener("unhandledrejection", handleRejection);
|
|
59
|
+
}
|
|
60
|
+
function useSessionErrors(options = {}) {
|
|
61
|
+
const { maxErrors = DEFAULT_MAX_ERRORS, storageKey = DEFAULT_STORAGE_KEY } = options;
|
|
62
|
+
if (!listenersInitialized) {
|
|
63
|
+
currentMaxErrors = maxErrors;
|
|
64
|
+
currentStorageKey = storageKey;
|
|
65
|
+
} else if (process.env.NODE_ENV !== "production" && (maxErrors !== currentMaxErrors || storageKey !== currentStorageKey)) {
|
|
66
|
+
console.warn(
|
|
67
|
+
`[useSessionErrors] Hook called with different options than initial configuration. Global listeners use: { maxErrors: ${currentMaxErrors}, storageKey: "${currentStorageKey}" }. Received: { maxErrors: ${maxErrors}, storageKey: "${storageKey}" }. Only the first configuration is used for global error listeners.`
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
react.useEffect(() => {
|
|
71
|
+
initializeErrorListeners();
|
|
72
|
+
}, []);
|
|
73
|
+
const getErrors = react.useCallback(() => {
|
|
74
|
+
if (typeof window === "undefined") return [];
|
|
75
|
+
try {
|
|
76
|
+
const stored = sessionStorage.getItem(storageKey);
|
|
77
|
+
return stored ? JSON.parse(stored) : [];
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}, [storageKey]);
|
|
82
|
+
const clearErrors = react.useCallback(() => {
|
|
83
|
+
if (typeof window === "undefined") return;
|
|
84
|
+
try {
|
|
85
|
+
sessionStorage.removeItem(storageKey);
|
|
86
|
+
} catch {
|
|
87
|
+
}
|
|
88
|
+
}, [storageKey]);
|
|
89
|
+
return { getErrors, clearErrors };
|
|
90
|
+
}
|
|
91
|
+
function useComparisonData({
|
|
92
|
+
useDataHook,
|
|
93
|
+
currentParams,
|
|
94
|
+
comparisonRanges,
|
|
95
|
+
comparisonMode,
|
|
96
|
+
comparisonPeriods,
|
|
97
|
+
toEntries,
|
|
98
|
+
getPeriodKey
|
|
99
|
+
}) {
|
|
100
|
+
const { data: response, isLoading } = useDataHook(currentParams);
|
|
101
|
+
const shouldFetchComparison = comparisonMode !== "none";
|
|
102
|
+
const comparisonEnabled = [0, 1, 2].map(
|
|
103
|
+
(index) => shouldFetchComparison && !!comparisonRanges[index]
|
|
104
|
+
);
|
|
105
|
+
const { data: compResp0 } = useDataHook({
|
|
106
|
+
...currentParams,
|
|
107
|
+
periodStart: comparisonRanges[0]?.start ?? "",
|
|
108
|
+
periodEnd: comparisonRanges[0]?.end ?? "",
|
|
109
|
+
enabled: comparisonEnabled[0],
|
|
110
|
+
keepPrevious: true
|
|
111
|
+
});
|
|
112
|
+
const { data: compResp1 } = useDataHook({
|
|
113
|
+
...currentParams,
|
|
114
|
+
periodStart: comparisonRanges[1]?.start ?? "",
|
|
115
|
+
periodEnd: comparisonRanges[1]?.end ?? "",
|
|
116
|
+
enabled: comparisonEnabled[1],
|
|
117
|
+
keepPrevious: true
|
|
118
|
+
});
|
|
119
|
+
const { data: compResp2 } = useDataHook({
|
|
120
|
+
...currentParams,
|
|
121
|
+
periodStart: comparisonRanges[2]?.start ?? "",
|
|
122
|
+
periodEnd: comparisonRanges[2]?.end ?? "",
|
|
123
|
+
enabled: comparisonEnabled[2],
|
|
124
|
+
keepPrevious: true
|
|
125
|
+
});
|
|
126
|
+
const rawData = response?.data ?? [];
|
|
127
|
+
const entries = react.useMemo(() => {
|
|
128
|
+
const current = toEntries(rawData);
|
|
129
|
+
if (comparisonMode === "none") return current;
|
|
130
|
+
const compDataSets = [compResp0, compResp1, compResp2].filter(Boolean);
|
|
131
|
+
const compEntries = compDataSets.flatMap(
|
|
132
|
+
(result) => toEntries(result?.data ?? [])
|
|
133
|
+
);
|
|
134
|
+
return [...compEntries, ...current];
|
|
135
|
+
}, [rawData, comparisonMode, compResp0, compResp1, compResp2, toEntries]);
|
|
136
|
+
const currentPeriodCount = react.useMemo(
|
|
137
|
+
() => new Set(rawData.map(getPeriodKey)).size,
|
|
138
|
+
[rawData, getPeriodKey]
|
|
139
|
+
);
|
|
140
|
+
const currentPeriodKeys = react.useMemo(
|
|
141
|
+
() => Array.from(new Set(rawData.map(getPeriodKey))).sort(
|
|
142
|
+
(a, b) => b.localeCompare(a)
|
|
143
|
+
),
|
|
144
|
+
[rawData, getPeriodKey]
|
|
145
|
+
);
|
|
146
|
+
const totalVisiblePeriodCount = react.useMemo(
|
|
147
|
+
() => new Set(entries.map((entry) => entry.period_first_day)).size,
|
|
148
|
+
[entries]
|
|
149
|
+
);
|
|
150
|
+
const showVariance = totalVisiblePeriodCount === 2 && !(comparisonMode === "previous" && comparisonPeriods > 1);
|
|
151
|
+
return {
|
|
152
|
+
response,
|
|
153
|
+
isLoading,
|
|
154
|
+
rawData,
|
|
155
|
+
entries,
|
|
156
|
+
currentPeriodCount,
|
|
157
|
+
currentPeriodKeys,
|
|
158
|
+
totalVisiblePeriodCount,
|
|
159
|
+
showVariance,
|
|
160
|
+
classes: response?.classes ?? [],
|
|
161
|
+
departments: response?.departments ?? []
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function fallbackCopy(text) {
|
|
165
|
+
try {
|
|
166
|
+
const textarea = document.createElement("textarea");
|
|
167
|
+
textarea.value = text;
|
|
168
|
+
textarea.style.position = "fixed";
|
|
169
|
+
textarea.style.left = "-9999px";
|
|
170
|
+
textarea.style.top = "-9999px";
|
|
171
|
+
document.body.appendChild(textarea);
|
|
172
|
+
textarea.select();
|
|
173
|
+
const ok = document.execCommand("copy");
|
|
174
|
+
document.body.removeChild(textarea);
|
|
175
|
+
return ok;
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
function useCopyToClipboard({
|
|
181
|
+
timeout = 2e3
|
|
182
|
+
} = {}) {
|
|
183
|
+
const [isCopied, setIsCopied] = react.useState(false);
|
|
184
|
+
const [copiedId, setCopiedId] = react.useState(null);
|
|
185
|
+
const copyToClipboard = react.useCallback(
|
|
186
|
+
(value, id) => {
|
|
187
|
+
if (typeof window === "undefined" || !value) return;
|
|
188
|
+
const onSuccess = () => {
|
|
189
|
+
setIsCopied(true);
|
|
190
|
+
if (id) setCopiedId(id);
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
setIsCopied(false);
|
|
193
|
+
if (id) setCopiedId(null);
|
|
194
|
+
}, timeout);
|
|
195
|
+
};
|
|
196
|
+
if (navigator.clipboard?.writeText) {
|
|
197
|
+
navigator.clipboard.writeText(value).then(onSuccess).catch(() => {
|
|
198
|
+
fallbackCopy(value) && onSuccess();
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
fallbackCopy(value) && onSuccess();
|
|
203
|
+
},
|
|
204
|
+
[timeout]
|
|
205
|
+
);
|
|
206
|
+
return { isCopied, copiedId, copyToClipboard };
|
|
207
|
+
}
|
|
208
|
+
function useDebounce(value, delay = 500) {
|
|
209
|
+
const [debouncedValue, setDebouncedValue] = react.useState(value);
|
|
210
|
+
react.useEffect(() => {
|
|
211
|
+
const timer = setTimeout(() => {
|
|
212
|
+
setDebouncedValue(value);
|
|
213
|
+
}, delay);
|
|
214
|
+
return () => {
|
|
215
|
+
clearTimeout(timer);
|
|
216
|
+
};
|
|
217
|
+
}, [value, delay]);
|
|
218
|
+
return debouncedValue;
|
|
219
|
+
}
|
|
220
|
+
function getDefaultLtmRange() {
|
|
221
|
+
const now = /* @__PURE__ */ new Date();
|
|
222
|
+
const year = now.getFullYear();
|
|
223
|
+
const month = now.getMonth();
|
|
224
|
+
const start = new Date(year - 1, month, 1);
|
|
225
|
+
const end = new Date(year, month + 1, 0);
|
|
226
|
+
return {
|
|
227
|
+
start: start.toISOString().slice(0, 10),
|
|
228
|
+
end: end.toISOString().slice(0, 10)
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function useFinancialStatementState({
|
|
232
|
+
initialGranularity = "ltm",
|
|
233
|
+
initialComparisonMode = "none",
|
|
234
|
+
initialComparisonPeriods = 1
|
|
235
|
+
} = {}) {
|
|
236
|
+
const defaultRange = react.useMemo(() => getDefaultLtmRange(), []);
|
|
237
|
+
const [granularity, setGranularity] = react.useState(initialGranularity);
|
|
238
|
+
const [periodStart, setPeriodStart] = react.useState(defaultRange.start);
|
|
239
|
+
const [periodEnd, setPeriodEnd] = react.useState(defaultRange.end);
|
|
240
|
+
const [timeUnit, setTimeUnit] = react.useState("month");
|
|
241
|
+
const [comparisonMode, setComparisonMode] = react.useState(
|
|
242
|
+
initialComparisonMode
|
|
243
|
+
);
|
|
244
|
+
const [comparisonPeriods, setComparisonPeriods] = react.useState(
|
|
245
|
+
initialComparisonPeriods
|
|
246
|
+
);
|
|
247
|
+
const [comparisonCustomStart, setComparisonCustomStart] = react.useState();
|
|
248
|
+
const [comparisonCustomEnd, setComparisonCustomEnd] = react.useState();
|
|
249
|
+
const [selectedClass, setSelectedClass] = react.useState("");
|
|
250
|
+
const [selectedDepartment, setSelectedDepartment] = react.useState("");
|
|
251
|
+
const [mobileFiltersOpen, setMobileFiltersOpen] = react.useState(false);
|
|
252
|
+
const [hideZeroRows, setHideZeroRows] = react.useState(true);
|
|
253
|
+
const [showRowTotals, setShowRowTotals] = react.useState(false);
|
|
254
|
+
const handlePeriodChange = react.useCallback(
|
|
255
|
+
(params) => {
|
|
256
|
+
setGranularity(params.granularity);
|
|
257
|
+
setPeriodStart(params.periodStart);
|
|
258
|
+
setPeriodEnd(params.periodEnd);
|
|
259
|
+
setTimeUnit(params.timeUnit);
|
|
260
|
+
},
|
|
261
|
+
[]
|
|
262
|
+
);
|
|
263
|
+
const handleComparisonChange = react.useCallback(
|
|
264
|
+
(params) => {
|
|
265
|
+
if (granularity === "ltm") {
|
|
266
|
+
const ltmDefault = params.mode === "previous" && params.periods === 11;
|
|
267
|
+
if (!ltmDefault) {
|
|
268
|
+
const end = dateFns.endOfMonth(dateFns.parseISO(periodEnd));
|
|
269
|
+
const start = dateFns.startOfMonth(end);
|
|
270
|
+
setGranularity("month");
|
|
271
|
+
setTimeUnit("month");
|
|
272
|
+
setPeriodStart(start.toISOString().slice(0, 10));
|
|
273
|
+
setPeriodEnd(end.toISOString().slice(0, 10));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
setComparisonMode(params.mode);
|
|
277
|
+
setComparisonPeriods(params.periods);
|
|
278
|
+
setComparisonCustomStart(params.customStart);
|
|
279
|
+
setComparisonCustomEnd(params.customEnd);
|
|
280
|
+
},
|
|
281
|
+
[granularity, periodEnd]
|
|
282
|
+
);
|
|
283
|
+
const priorRange = react.useMemo(() => {
|
|
284
|
+
const start = /* @__PURE__ */ new Date(`${periodStart}T00:00:00`);
|
|
285
|
+
const end = /* @__PURE__ */ new Date(`${periodEnd}T00:00:00`);
|
|
286
|
+
const durationMs = end.getTime() - start.getTime();
|
|
287
|
+
const priorEnd = new Date(start.getTime() - 1);
|
|
288
|
+
const priorStart = new Date(priorEnd.getTime() - durationMs);
|
|
289
|
+
return {
|
|
290
|
+
start: priorStart.toISOString().slice(0, 10),
|
|
291
|
+
end: priorEnd.toISOString().slice(0, 10)
|
|
292
|
+
};
|
|
293
|
+
}, [periodStart, periodEnd]);
|
|
294
|
+
const comparisonRanges = react.useMemo(() => {
|
|
295
|
+
if (comparisonMode === "none") return [];
|
|
296
|
+
if (comparisonMode === "custom" && comparisonCustomStart && comparisonCustomEnd) {
|
|
297
|
+
return [{ start: comparisonCustomStart, end: comparisonCustomEnd }];
|
|
298
|
+
}
|
|
299
|
+
const start = /* @__PURE__ */ new Date(`${periodStart}T00:00:00`);
|
|
300
|
+
const end = /* @__PURE__ */ new Date(`${periodEnd}T00:00:00`);
|
|
301
|
+
const durationMs = end.getTime() - start.getTime();
|
|
302
|
+
const ranges = [];
|
|
303
|
+
if (comparisonMode === "previous") {
|
|
304
|
+
const dayCount = Math.round(durationMs / 864e5) + 1;
|
|
305
|
+
const cEnd = new Date(start);
|
|
306
|
+
cEnd.setDate(cEnd.getDate() - 1);
|
|
307
|
+
const cStart = new Date(cEnd);
|
|
308
|
+
cStart.setDate(cStart.getDate() - (dayCount * comparisonPeriods - 1));
|
|
309
|
+
return [
|
|
310
|
+
{
|
|
311
|
+
start: cStart.toISOString().slice(0, 10),
|
|
312
|
+
end: cEnd.toISOString().slice(0, 10)
|
|
313
|
+
}
|
|
314
|
+
];
|
|
315
|
+
}
|
|
316
|
+
for (let i = 1; i <= comparisonPeriods; i++) {
|
|
317
|
+
if (comparisonMode === "same-period-last-year") {
|
|
318
|
+
const cStart = new Date(start);
|
|
319
|
+
cStart.setFullYear(cStart.getFullYear() - i);
|
|
320
|
+
const cEnd = new Date(end);
|
|
321
|
+
cEnd.setFullYear(cEnd.getFullYear() - i);
|
|
322
|
+
ranges.push({
|
|
323
|
+
start: cStart.toISOString().slice(0, 10),
|
|
324
|
+
end: cEnd.toISOString().slice(0, 10)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return ranges;
|
|
329
|
+
}, [
|
|
330
|
+
comparisonMode,
|
|
331
|
+
comparisonPeriods,
|
|
332
|
+
comparisonCustomStart,
|
|
333
|
+
comparisonCustomEnd,
|
|
334
|
+
periodStart,
|
|
335
|
+
periodEnd
|
|
336
|
+
]);
|
|
337
|
+
const formatPeriodLabel = react.useCallback((periodKey) => {
|
|
338
|
+
if (!periodKey) return "\u2014";
|
|
339
|
+
return dateFns.format(dateFns.parseISO(periodKey), "MMM yy");
|
|
340
|
+
}, []);
|
|
341
|
+
const activeFilterCount = (selectedClass ? 1 : 0) + (selectedDepartment ? 1 : 0) + (comparisonMode !== "none" ? 1 : 0);
|
|
342
|
+
const periodChipLabel = granularity === "ltm" ? "LTM" : granularity === "ytd" ? "YTD" : granularity === "month" ? "Month" : granularity === "quarter" ? "Quarter" : granularity === "year" ? "Year" : "Custom";
|
|
343
|
+
const comparisonChipLabel = granularity === "ltm" ? "vs Previous 11" : comparisonMode === "none" ? "No comparison" : comparisonMode === "previous" ? `vs Previous ${comparisonPeriods}` : comparisonMode === "same-period-last-year" ? `vs Last year (${comparisonPeriods})` : "Custom comparison";
|
|
344
|
+
const resetFilters = react.useCallback(() => {
|
|
345
|
+
setSelectedClass("");
|
|
346
|
+
setSelectedDepartment("");
|
|
347
|
+
setComparisonMode("none");
|
|
348
|
+
setComparisonPeriods(1);
|
|
349
|
+
setComparisonCustomStart(void 0);
|
|
350
|
+
setComparisonCustomEnd(void 0);
|
|
351
|
+
}, []);
|
|
352
|
+
return {
|
|
353
|
+
granularity,
|
|
354
|
+
periodStart,
|
|
355
|
+
periodEnd,
|
|
356
|
+
timeUnit,
|
|
357
|
+
selectedClass,
|
|
358
|
+
selectedDepartment,
|
|
359
|
+
comparisonMode,
|
|
360
|
+
comparisonPeriods,
|
|
361
|
+
comparisonCustomStart,
|
|
362
|
+
comparisonCustomEnd,
|
|
363
|
+
mobileFiltersOpen,
|
|
364
|
+
hideZeroRows,
|
|
365
|
+
showRowTotals,
|
|
366
|
+
priorRange,
|
|
367
|
+
comparisonRanges,
|
|
368
|
+
periodChipLabel,
|
|
369
|
+
comparisonChipLabel,
|
|
370
|
+
activeFilterCount,
|
|
371
|
+
formatPeriodLabel,
|
|
372
|
+
setSelectedClass,
|
|
373
|
+
setSelectedDepartment,
|
|
374
|
+
setMobileFiltersOpen,
|
|
375
|
+
setHideZeroRows,
|
|
376
|
+
setShowRowTotals,
|
|
377
|
+
handlePeriodChange,
|
|
378
|
+
handleComparisonChange,
|
|
379
|
+
resetFilters
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function matchesShortcut(event, shortcut) {
|
|
383
|
+
if (shortcut.enabled === false) return false;
|
|
384
|
+
const ctrlMatch = shortcut.ctrl ? event.ctrlKey : !event.ctrlKey;
|
|
385
|
+
const shiftMatch = shortcut.shift ? event.shiftKey : !event.shiftKey;
|
|
386
|
+
const altMatch = shortcut.alt ? event.altKey : !event.altKey;
|
|
387
|
+
const metaMatch = shortcut.meta ? event.metaKey : !event.metaKey;
|
|
388
|
+
const keyMatch = event.key === shortcut.key;
|
|
389
|
+
return ctrlMatch && shiftMatch && altMatch && metaMatch && keyMatch;
|
|
390
|
+
}
|
|
391
|
+
function useKeyboardShortcut(shortcut, callback, options = {}) {
|
|
392
|
+
const {
|
|
393
|
+
enabled = true,
|
|
394
|
+
target = typeof document !== "undefined" ? document : null,
|
|
395
|
+
capture = false,
|
|
396
|
+
preventDefault = false,
|
|
397
|
+
stopPropagation = false
|
|
398
|
+
} = options;
|
|
399
|
+
const callbackRef = react.useRef(callback);
|
|
400
|
+
react.useEffect(() => {
|
|
401
|
+
callbackRef.current = callback;
|
|
402
|
+
}, [callback]);
|
|
403
|
+
react.useEffect(() => {
|
|
404
|
+
if (typeof window === "undefined") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (!enabled || !target) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
const handler = (event) => {
|
|
411
|
+
const keyboardEvent = event;
|
|
412
|
+
const target2 = keyboardEvent.target;
|
|
413
|
+
if (target2.tagName === "INPUT" || target2.tagName === "TEXTAREA" || target2.isContentEditable) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (matchesShortcut(keyboardEvent, shortcut)) {
|
|
417
|
+
if (preventDefault) {
|
|
418
|
+
keyboardEvent.preventDefault();
|
|
419
|
+
}
|
|
420
|
+
if (stopPropagation) {
|
|
421
|
+
keyboardEvent.stopPropagation();
|
|
422
|
+
}
|
|
423
|
+
callbackRef.current(keyboardEvent);
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
target.addEventListener("keydown", handler, capture);
|
|
427
|
+
return () => {
|
|
428
|
+
target.removeEventListener("keydown", handler, capture);
|
|
429
|
+
};
|
|
430
|
+
}, [shortcut, enabled, target, capture, preventDefault, stopPropagation]);
|
|
431
|
+
}
|
|
432
|
+
function formatShortcut(shortcut, usePlatformSymbols = false) {
|
|
433
|
+
const parts = [];
|
|
434
|
+
const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.userAgent);
|
|
435
|
+
if (shortcut.ctrl) {
|
|
436
|
+
parts.push(usePlatformSymbols && isMac ? "\u2303" : "Ctrl");
|
|
437
|
+
}
|
|
438
|
+
if (shortcut.alt) {
|
|
439
|
+
parts.push(usePlatformSymbols && isMac ? "\u2325" : "Alt");
|
|
440
|
+
}
|
|
441
|
+
if (shortcut.shift) {
|
|
442
|
+
parts.push(usePlatformSymbols && isMac ? "\u21E7" : "Shift");
|
|
443
|
+
}
|
|
444
|
+
if (shortcut.meta) {
|
|
445
|
+
parts.push(usePlatformSymbols && isMac ? "\u2318" : "Meta");
|
|
446
|
+
}
|
|
447
|
+
const keyDisplay = shortcut.key.length === 1 ? shortcut.key.toUpperCase() : shortcut.key;
|
|
448
|
+
parts.push(keyDisplay);
|
|
449
|
+
return parts.join(usePlatformSymbols ? "" : "+");
|
|
450
|
+
}
|
|
451
|
+
function useLocalStorage(key, initialValue) {
|
|
452
|
+
const readValue = react.useCallback(() => {
|
|
453
|
+
if (typeof window === "undefined" || !key) {
|
|
454
|
+
return initialValue;
|
|
455
|
+
}
|
|
456
|
+
try {
|
|
457
|
+
const item = window.localStorage.getItem(key);
|
|
458
|
+
return item ? JSON.parse(item) : initialValue;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
console.warn(`Error reading localStorage key "${key}":`, error);
|
|
461
|
+
return initialValue;
|
|
462
|
+
}
|
|
463
|
+
}, [initialValue, key]);
|
|
464
|
+
const [storedValue, setStoredValue] = react.useState(initialValue);
|
|
465
|
+
const [isLoaded, setIsLoaded] = react.useState(false);
|
|
466
|
+
react.useEffect(() => {
|
|
467
|
+
setStoredValue(readValue());
|
|
468
|
+
setIsLoaded(true);
|
|
469
|
+
}, [readValue]);
|
|
470
|
+
const setValue = react.useCallback(
|
|
471
|
+
(value) => {
|
|
472
|
+
try {
|
|
473
|
+
setStoredValue((prevStoredValue) => {
|
|
474
|
+
const valueToStore = value instanceof Function ? value(prevStoredValue) : value;
|
|
475
|
+
if (typeof window !== "undefined" && key) {
|
|
476
|
+
window.localStorage.setItem(key, JSON.stringify(valueToStore));
|
|
477
|
+
window.dispatchEvent(
|
|
478
|
+
new StorageEvent("storage", {
|
|
479
|
+
key,
|
|
480
|
+
newValue: JSON.stringify(valueToStore),
|
|
481
|
+
url: window.location.href
|
|
482
|
+
})
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
return valueToStore;
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
console.warn(`Error setting localStorage key "${key}":`, error);
|
|
489
|
+
}
|
|
490
|
+
},
|
|
491
|
+
[key]
|
|
492
|
+
);
|
|
493
|
+
const removeValue = react.useCallback(() => {
|
|
494
|
+
try {
|
|
495
|
+
if (typeof window !== "undefined" && key) {
|
|
496
|
+
window.localStorage.removeItem(key);
|
|
497
|
+
window.dispatchEvent(
|
|
498
|
+
new StorageEvent("storage", {
|
|
499
|
+
key,
|
|
500
|
+
newValue: null
|
|
501
|
+
})
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
setStoredValue(initialValue);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
console.warn(`Error removing localStorage key "${key}":`, error);
|
|
507
|
+
}
|
|
508
|
+
}, [initialValue, key]);
|
|
509
|
+
react.useEffect(() => {
|
|
510
|
+
if (!key) return;
|
|
511
|
+
const handleStorageChange = (event) => {
|
|
512
|
+
if (event.key === key && event.newValue !== null) {
|
|
513
|
+
try {
|
|
514
|
+
setStoredValue(JSON.parse(event.newValue));
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
} else if (event.key === key && event.newValue === null) {
|
|
518
|
+
setStoredValue(initialValue);
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
window.addEventListener("storage", handleStorageChange);
|
|
522
|
+
return () => window.removeEventListener("storage", handleStorageChange);
|
|
523
|
+
}, [initialValue, key]);
|
|
524
|
+
return [storedValue, setValue, removeValue, isLoaded];
|
|
525
|
+
}
|
|
526
|
+
function useMediaQuery(query) {
|
|
527
|
+
const [matches, setMatches] = react.useState(false);
|
|
528
|
+
react.useEffect(() => {
|
|
529
|
+
if (typeof window === "undefined") {
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
const mediaQuery = window.matchMedia(query);
|
|
533
|
+
setMatches(mediaQuery.matches);
|
|
534
|
+
const handleChange = (event) => {
|
|
535
|
+
setMatches(event.matches);
|
|
536
|
+
};
|
|
537
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
538
|
+
return () => {
|
|
539
|
+
mediaQuery.removeEventListener("change", handleChange);
|
|
540
|
+
};
|
|
541
|
+
}, [query]);
|
|
542
|
+
return matches;
|
|
543
|
+
}
|
|
544
|
+
var breakpoints = {
|
|
545
|
+
sm: "(min-width: 640px)",
|
|
546
|
+
md: "(min-width: 768px)",
|
|
547
|
+
lg: "(min-width: 1024px)",
|
|
548
|
+
xl: "(min-width: 1280px)",
|
|
549
|
+
"2xl": "(min-width: 1536px)"
|
|
550
|
+
};
|
|
551
|
+
function useBreakpoints() {
|
|
552
|
+
const isSm = useMediaQuery(breakpoints.sm);
|
|
553
|
+
const isMd = useMediaQuery(breakpoints.md);
|
|
554
|
+
const isLg = useMediaQuery(breakpoints.lg);
|
|
555
|
+
const isXl = useMediaQuery(breakpoints.xl);
|
|
556
|
+
const is2xl = useMediaQuery(breakpoints["2xl"]);
|
|
557
|
+
return {
|
|
558
|
+
/** Below 640px */
|
|
559
|
+
isMobile: !isSm,
|
|
560
|
+
/** 640px - 767px */
|
|
561
|
+
isSm: isSm && !isMd,
|
|
562
|
+
/** 768px - 1023px */
|
|
563
|
+
isTablet: isMd && !isLg,
|
|
564
|
+
/** 768px+ */
|
|
565
|
+
isMd,
|
|
566
|
+
/** 1024px+ */
|
|
567
|
+
isDesktop: isLg,
|
|
568
|
+
/** 1024px - 1279px */
|
|
569
|
+
isLg: isLg && !isXl,
|
|
570
|
+
/** 1280px+ */
|
|
571
|
+
isXl,
|
|
572
|
+
/** 1536px+ */
|
|
573
|
+
is2xl
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
function useStatementExport({
|
|
577
|
+
entries,
|
|
578
|
+
config,
|
|
579
|
+
timeUnit,
|
|
580
|
+
subtotalRules,
|
|
581
|
+
hideZeroRows,
|
|
582
|
+
periodStart,
|
|
583
|
+
periodEnd,
|
|
584
|
+
filenamePrefix,
|
|
585
|
+
worksheetName
|
|
586
|
+
}) {
|
|
587
|
+
return react.useCallback(
|
|
588
|
+
(format2) => {
|
|
589
|
+
const { rows: flat, periodKeys } = chunkUM7WMXIU_js.flattenStatementForExport(
|
|
590
|
+
entries,
|
|
591
|
+
config,
|
|
592
|
+
timeUnit,
|
|
593
|
+
subtotalRules,
|
|
594
|
+
hideZeroRows
|
|
595
|
+
);
|
|
596
|
+
const headers = ["Account", ...periodKeys, "Total"];
|
|
597
|
+
const dataRows = flat.map((row) => [
|
|
598
|
+
row.account,
|
|
599
|
+
...periodKeys.map((periodKey) => String(row[periodKey] ?? 0)),
|
|
600
|
+
String(row.total ?? 0)
|
|
601
|
+
]);
|
|
602
|
+
const filename = `${filenamePrefix}-${periodStart}-to-${periodEnd}`;
|
|
603
|
+
if (format2 === "csv") {
|
|
604
|
+
const escapeCsv = (value) => `"${value.replace(/"/g, '""')}"`;
|
|
605
|
+
const csv = [
|
|
606
|
+
headers.map(escapeCsv).join(","),
|
|
607
|
+
...dataRows.map((row) => row.map(escapeCsv).join(","))
|
|
608
|
+
].join("\n");
|
|
609
|
+
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
|
|
610
|
+
const url = URL.createObjectURL(blob);
|
|
611
|
+
const anchor = document.createElement("a");
|
|
612
|
+
anchor.href = url;
|
|
613
|
+
anchor.download = `${filename}.csv`;
|
|
614
|
+
anchor.click();
|
|
615
|
+
URL.revokeObjectURL(url);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const rowStart = 2;
|
|
619
|
+
const firstValueColumn = 1;
|
|
620
|
+
const lastValueColumn = periodKeys.length;
|
|
621
|
+
const totalColumn = headers.length - 1;
|
|
622
|
+
const rows = [
|
|
623
|
+
headers.map((header, index) => ({
|
|
624
|
+
value: header,
|
|
625
|
+
style: index === totalColumn ? "totalLabel" : "header"
|
|
626
|
+
})),
|
|
627
|
+
...flat.map((row, rowIndex) => {
|
|
628
|
+
const excelRow = rowStart + rowIndex;
|
|
629
|
+
const totalFormula = `SUM(${chunkUM7WMXIU_js.excelColumnName(firstValueColumn)}${excelRow}:${chunkUM7WMXIU_js.excelColumnName(lastValueColumn)}${excelRow})`;
|
|
630
|
+
const periodCells = periodKeys.map(
|
|
631
|
+
(periodKey) => ({
|
|
632
|
+
value: Number(row[periodKey] ?? 0),
|
|
633
|
+
style: "currency",
|
|
634
|
+
type: "Number"
|
|
635
|
+
})
|
|
636
|
+
);
|
|
637
|
+
return [
|
|
638
|
+
{ value: row.account, style: "text" },
|
|
639
|
+
...periodCells,
|
|
640
|
+
{
|
|
641
|
+
formula: totalFormula,
|
|
642
|
+
value: Number(row.total ?? 0),
|
|
643
|
+
style: "totalNumber",
|
|
644
|
+
type: "Number"
|
|
645
|
+
}
|
|
646
|
+
];
|
|
647
|
+
})
|
|
648
|
+
];
|
|
649
|
+
const xml = chunkUM7WMXIU_js.buildSpreadsheetWorkbookXml({
|
|
650
|
+
worksheetName,
|
|
651
|
+
rows
|
|
652
|
+
});
|
|
653
|
+
chunkUM7WMXIU_js.downloadExcelXml(xml, filename);
|
|
654
|
+
},
|
|
655
|
+
[
|
|
656
|
+
config,
|
|
657
|
+
entries,
|
|
658
|
+
filenamePrefix,
|
|
659
|
+
hideZeroRows,
|
|
660
|
+
periodEnd,
|
|
661
|
+
periodStart,
|
|
662
|
+
subtotalRules,
|
|
663
|
+
timeUnit,
|
|
664
|
+
worksheetName
|
|
665
|
+
]
|
|
666
|
+
);
|
|
667
|
+
}
|
|
668
|
+
function useStatementMobileMode({
|
|
669
|
+
isMobile,
|
|
670
|
+
entries,
|
|
671
|
+
currentPeriodKeys,
|
|
672
|
+
comparisonMode
|
|
673
|
+
}) {
|
|
674
|
+
const normalizedPeriodKeys = react.useMemo(
|
|
675
|
+
() => Array.from(new Set(currentPeriodKeys)).sort((a, b) => b.localeCompare(a)),
|
|
676
|
+
[currentPeriodKeys]
|
|
677
|
+
);
|
|
678
|
+
const [activePeriodIndex, setActivePeriodIndex] = react.useState(0);
|
|
679
|
+
const [compareEnabled, setCompareEnabled] = react.useState(false);
|
|
680
|
+
react.useEffect(() => {
|
|
681
|
+
setActivePeriodIndex(
|
|
682
|
+
(index) => normalizedPeriodKeys.length === 0 ? 0 : Math.min(index, normalizedPeriodKeys.length - 1)
|
|
683
|
+
);
|
|
684
|
+
}, [normalizedPeriodKeys.length]);
|
|
685
|
+
const activePeriod = normalizedPeriodKeys[activePeriodIndex] ?? normalizedPeriodKeys[0];
|
|
686
|
+
const comparisonPeriodKeys = react.useMemo(
|
|
687
|
+
() => Array.from(new Set(entries.map((entry) => entry.period_first_day))).filter((period) => !normalizedPeriodKeys.includes(period)).sort((a, b) => b.localeCompare(a)),
|
|
688
|
+
[entries, normalizedPeriodKeys]
|
|
689
|
+
);
|
|
690
|
+
const hasComparisonCandidate = comparisonMode === "none" ? normalizedPeriodKeys.length > 1 : comparisonPeriodKeys.length > 0;
|
|
691
|
+
react.useEffect(() => {
|
|
692
|
+
if (!hasComparisonCandidate) setCompareEnabled(false);
|
|
693
|
+
}, [hasComparisonCandidate]);
|
|
694
|
+
const mobileComparisonPeriod = react.useMemo(() => {
|
|
695
|
+
if (!compareEnabled || !activePeriod) return void 0;
|
|
696
|
+
if (comparisonMode !== "none") return comparisonPeriodKeys[0];
|
|
697
|
+
const activeIndex = normalizedPeriodKeys.indexOf(activePeriod);
|
|
698
|
+
return activeIndex >= 0 ? normalizedPeriodKeys[activeIndex + 1] : void 0;
|
|
699
|
+
}, [
|
|
700
|
+
compareEnabled,
|
|
701
|
+
activePeriod,
|
|
702
|
+
comparisonMode,
|
|
703
|
+
comparisonPeriodKeys,
|
|
704
|
+
normalizedPeriodKeys
|
|
705
|
+
]);
|
|
706
|
+
const mobileVisiblePeriods = react.useMemo(() => {
|
|
707
|
+
if (!isMobile || !activePeriod) return null;
|
|
708
|
+
return mobileComparisonPeriod ? [activePeriod, mobileComparisonPeriod] : [activePeriod];
|
|
709
|
+
}, [isMobile, activePeriod, mobileComparisonPeriod]);
|
|
710
|
+
const mobileEntries = react.useMemo(() => {
|
|
711
|
+
if (!mobileVisiblePeriods) return entries;
|
|
712
|
+
const visible = new Set(mobileVisiblePeriods);
|
|
713
|
+
return entries.filter((entry) => visible.has(entry.period_first_day));
|
|
714
|
+
}, [entries, mobileVisiblePeriods]);
|
|
715
|
+
return {
|
|
716
|
+
activePeriodIndex,
|
|
717
|
+
setActivePeriodIndex,
|
|
718
|
+
compareEnabled,
|
|
719
|
+
setCompareEnabled,
|
|
720
|
+
mobileEntries,
|
|
721
|
+
periodLabel: activePeriod ?? "",
|
|
722
|
+
compareLabel: mobileComparisonPeriod,
|
|
723
|
+
hasPrev: activePeriodIndex > 0,
|
|
724
|
+
hasNext: normalizedPeriodKeys.length > 0 && activePeriodIndex < normalizedPeriodKeys.length - 1,
|
|
725
|
+
hasComparisonCandidate,
|
|
726
|
+
activePeriod
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
function toast(options) {
|
|
730
|
+
const { title, description, variant, duration } = options;
|
|
731
|
+
const message = title || description || "";
|
|
732
|
+
const toastOptions = {
|
|
733
|
+
description: title ? description : void 0,
|
|
734
|
+
duration
|
|
735
|
+
};
|
|
736
|
+
if (variant === "destructive") {
|
|
737
|
+
return sonner.toast.error(message, toastOptions);
|
|
738
|
+
}
|
|
739
|
+
return sonner.toast(message, toastOptions);
|
|
740
|
+
}
|
|
741
|
+
function useToast() {
|
|
742
|
+
return { toast };
|
|
743
|
+
}
|
|
744
|
+
function createDataRestorationHandler(form, defaultValues, onComplete) {
|
|
745
|
+
return (data) => {
|
|
746
|
+
const dataWithPossibleValues = data;
|
|
747
|
+
const { values: _ignoredValues, ...cleanData } = dataWithPossibleValues;
|
|
748
|
+
const actualData = Object.keys(cleanData).length > 0 ? cleanData : data;
|
|
749
|
+
const newValues = { ...defaultValues };
|
|
750
|
+
const keys = Object.keys(defaultValues);
|
|
751
|
+
for (const key of keys) {
|
|
752
|
+
const restoredValue = actualData[key];
|
|
753
|
+
if (restoredValue !== void 0 && restoredValue !== null) {
|
|
754
|
+
newValues[key] = restoredValue;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
requestAnimationFrame(() => {
|
|
758
|
+
for (const key of keys) {
|
|
759
|
+
form.setFieldValue(key, newValues[key]);
|
|
760
|
+
}
|
|
761
|
+
setTimeout(() => {
|
|
762
|
+
for (const key of keys) {
|
|
763
|
+
form.validateField(key, "change");
|
|
764
|
+
}
|
|
765
|
+
onComplete?.();
|
|
766
|
+
}, 100);
|
|
767
|
+
});
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
function extractZodErrors(zodResult, fieldFilter) {
|
|
771
|
+
if (zodResult.success || !zodResult.error) {
|
|
772
|
+
return [];
|
|
773
|
+
}
|
|
774
|
+
const issues = zodResult.error.issues;
|
|
775
|
+
if (!fieldFilter) {
|
|
776
|
+
return issues.map((issue) => issue.message);
|
|
777
|
+
}
|
|
778
|
+
return issues.filter((issue) => {
|
|
779
|
+
const fieldName = issue.path[0];
|
|
780
|
+
return typeof fieldName === "string" && fieldFilter.includes(fieldName);
|
|
781
|
+
}).map((issue) => issue.message);
|
|
782
|
+
}
|
|
783
|
+
function createStepHasFieldErrors(steps, formFieldMeta) {
|
|
784
|
+
return (stepIndex) => {
|
|
785
|
+
const fields = steps[stepIndex]?.fields ?? [];
|
|
786
|
+
for (const fieldName of fields) {
|
|
787
|
+
const meta = formFieldMeta[fieldName];
|
|
788
|
+
if (meta?.errors && meta.errors.length > 0) {
|
|
789
|
+
return true;
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return false;
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
function createFormResetHandler(form, defaultValues, resetWizardState, clearSubmitError) {
|
|
796
|
+
return () => {
|
|
797
|
+
form.reset();
|
|
798
|
+
const keys = Object.keys(defaultValues);
|
|
799
|
+
for (const key of keys) {
|
|
800
|
+
form.setFieldValue(key, defaultValues[key]);
|
|
801
|
+
}
|
|
802
|
+
resetWizardState();
|
|
803
|
+
clearSubmitError?.();
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
function createFormSubmitHandler(form, markAsSubmitted, options) {
|
|
807
|
+
const {
|
|
808
|
+
validateStep,
|
|
809
|
+
totalSteps,
|
|
810
|
+
errorLogger,
|
|
811
|
+
componentName,
|
|
812
|
+
getStepStatuses,
|
|
813
|
+
showValidationToast
|
|
814
|
+
} = options;
|
|
815
|
+
return () => {
|
|
816
|
+
markAsSubmitted();
|
|
817
|
+
form.validateAllFields("change");
|
|
818
|
+
const allValid = Array.from({ length: totalSteps }, (_, i) => i).every(
|
|
819
|
+
(index) => validateStep(index)
|
|
820
|
+
);
|
|
821
|
+
if (!allValid) {
|
|
822
|
+
showValidationToast?.(
|
|
823
|
+
"Please fix the errors in the highlighted sections."
|
|
824
|
+
);
|
|
825
|
+
errorLogger?.logError(new Error(`${componentName} validation failed`), {
|
|
826
|
+
component: componentName,
|
|
827
|
+
action: "submit",
|
|
828
|
+
metadata: { stepStatuses: getStepStatuses?.() }
|
|
829
|
+
});
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
return form.handleSubmit();
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
function useWizardForm(options, currentValues) {
|
|
836
|
+
const {
|
|
837
|
+
steps,
|
|
838
|
+
validateStep,
|
|
839
|
+
getStepErrors,
|
|
840
|
+
stepHasFieldErrors,
|
|
841
|
+
initialStep = 0
|
|
842
|
+
} = options;
|
|
843
|
+
const [currentStep, setCurrentStep] = react.useState(initialStep);
|
|
844
|
+
const [highestStepReached, setHighestStepReached] = react.useState(initialStep);
|
|
845
|
+
const [stepsWithErrorsShown, setStepsWithErrorsShown] = react.useState(
|
|
846
|
+
/* @__PURE__ */ new Set()
|
|
847
|
+
);
|
|
848
|
+
const [hasAttemptedFinalSubmit, setHasAttemptedFinalSubmit] = react.useState(false);
|
|
849
|
+
const totalSteps = steps.length;
|
|
850
|
+
const isFirstStep = currentStep === 0;
|
|
851
|
+
const isLastStep = currentStep === totalSteps - 1;
|
|
852
|
+
const stepStatuses = react.useMemo(() => {
|
|
853
|
+
return steps.map((step, index) => {
|
|
854
|
+
const isCurrentStepIndex = index === currentStep;
|
|
855
|
+
const isValid = validateStep(index, currentValues);
|
|
856
|
+
step.optional ?? false;
|
|
857
|
+
const shouldShowErrors = hasAttemptedFinalSubmit || stepsWithErrorsShown.has(index);
|
|
858
|
+
const hasFieldErrors = shouldShowErrors && stepHasFieldErrors?.(index);
|
|
859
|
+
const hasMovedPastThisStep = index < highestStepReached;
|
|
860
|
+
const isFutureStep = index > highestStepReached;
|
|
861
|
+
if (shouldShowErrors && (!isValid || hasFieldErrors)) {
|
|
862
|
+
return "error";
|
|
863
|
+
}
|
|
864
|
+
if (isFutureStep && !hasAttemptedFinalSubmit) {
|
|
865
|
+
return "not-started";
|
|
866
|
+
}
|
|
867
|
+
if (isValid) {
|
|
868
|
+
return "complete";
|
|
869
|
+
}
|
|
870
|
+
if (hasMovedPastThisStep && !isCurrentStepIndex) {
|
|
871
|
+
return "incomplete";
|
|
872
|
+
}
|
|
873
|
+
return "not-started";
|
|
874
|
+
});
|
|
875
|
+
}, [
|
|
876
|
+
steps,
|
|
877
|
+
currentStep,
|
|
878
|
+
highestStepReached,
|
|
879
|
+
hasAttemptedFinalSubmit,
|
|
880
|
+
stepsWithErrorsShown,
|
|
881
|
+
validateStep,
|
|
882
|
+
stepHasFieldErrors,
|
|
883
|
+
currentValues
|
|
884
|
+
]);
|
|
885
|
+
const hasIncompleteOrErrorSteps = react.useMemo(() => {
|
|
886
|
+
return stepStatuses.some(
|
|
887
|
+
(status) => status === "incomplete" || status === "error"
|
|
888
|
+
);
|
|
889
|
+
}, [stepStatuses]);
|
|
890
|
+
const isCurrentStepValid = react.useMemo(() => {
|
|
891
|
+
return validateStep(currentStep, currentValues);
|
|
892
|
+
}, [validateStep, currentStep, currentValues]);
|
|
893
|
+
const handleNext = react.useCallback(() => {
|
|
894
|
+
setStepsWithErrorsShown((prev) => new Set(prev).add(currentStep));
|
|
895
|
+
const nextStep = currentStep + 1;
|
|
896
|
+
if (nextStep < totalSteps) {
|
|
897
|
+
setCurrentStep(nextStep);
|
|
898
|
+
setHighestStepReached((prev) => Math.max(prev, nextStep));
|
|
899
|
+
}
|
|
900
|
+
}, [currentStep, totalSteps]);
|
|
901
|
+
const handleBack = react.useCallback(() => {
|
|
902
|
+
if (!isFirstStep) {
|
|
903
|
+
const prevStep = currentStep - 1;
|
|
904
|
+
setCurrentStep(prevStep);
|
|
905
|
+
}
|
|
906
|
+
}, [currentStep, isFirstStep]);
|
|
907
|
+
const handleStepClick = react.useCallback(
|
|
908
|
+
(stepIndex) => {
|
|
909
|
+
if (stepIndex !== currentStep) {
|
|
910
|
+
setStepsWithErrorsShown((prev) => new Set(prev).add(currentStep));
|
|
911
|
+
}
|
|
912
|
+
setCurrentStep(stepIndex);
|
|
913
|
+
if (stepIndex > highestStepReached) {
|
|
914
|
+
setHighestStepReached(stepIndex);
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
[currentStep, highestStepReached]
|
|
918
|
+
);
|
|
919
|
+
const markAsSubmitted = react.useCallback(() => {
|
|
920
|
+
setHasAttemptedFinalSubmit(true);
|
|
921
|
+
}, []);
|
|
922
|
+
const resetWizardState = react.useCallback(() => {
|
|
923
|
+
setCurrentStep(initialStep);
|
|
924
|
+
setHighestStepReached(initialStep);
|
|
925
|
+
setStepsWithErrorsShown(/* @__PURE__ */ new Set());
|
|
926
|
+
setHasAttemptedFinalSubmit(false);
|
|
927
|
+
}, [initialStep]);
|
|
928
|
+
const shouldShowErrorsForStep = react.useCallback(
|
|
929
|
+
(stepIndex) => {
|
|
930
|
+
return hasAttemptedFinalSubmit || stepsWithErrorsShown.has(stepIndex);
|
|
931
|
+
},
|
|
932
|
+
[hasAttemptedFinalSubmit, stepsWithErrorsShown]
|
|
933
|
+
);
|
|
934
|
+
const getStepStatus = react.useCallback(
|
|
935
|
+
(stepIndex) => stepStatuses[stepIndex],
|
|
936
|
+
[stepStatuses]
|
|
937
|
+
);
|
|
938
|
+
const getStepTooltip = react.useCallback(
|
|
939
|
+
(stepIndex, status) => {
|
|
940
|
+
if (status !== "error" && status !== "incomplete") return null;
|
|
941
|
+
if (getStepErrors) {
|
|
942
|
+
const stepErrors = getStepErrors(stepIndex, currentValues);
|
|
943
|
+
if (stepErrors.length > 0) {
|
|
944
|
+
return `${stepErrors.length} issue${stepErrors.length > 1 ? "s" : ""}:
|
|
945
|
+
\u2022 ${stepErrors.join("\n\u2022 ")}`;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return null;
|
|
949
|
+
},
|
|
950
|
+
[getStepErrors, currentValues]
|
|
951
|
+
);
|
|
952
|
+
const getStepFields = react.useCallback(
|
|
953
|
+
(stepIndex) => {
|
|
954
|
+
return steps[stepIndex]?.fields ?? [];
|
|
955
|
+
},
|
|
956
|
+
[steps]
|
|
957
|
+
);
|
|
958
|
+
return {
|
|
959
|
+
// Navigation state
|
|
960
|
+
currentStep,
|
|
961
|
+
totalSteps,
|
|
962
|
+
isFirstStep,
|
|
963
|
+
isLastStep,
|
|
964
|
+
highestStepReached,
|
|
965
|
+
// Validation state
|
|
966
|
+
stepsWithErrorsShown,
|
|
967
|
+
hasAttemptedFinalSubmit,
|
|
968
|
+
stepStatuses,
|
|
969
|
+
hasIncompleteOrErrorSteps,
|
|
970
|
+
isCurrentStepValid,
|
|
971
|
+
// Navigation handlers
|
|
972
|
+
handleNext,
|
|
973
|
+
handleBack,
|
|
974
|
+
handleStepClick,
|
|
975
|
+
// Submit handlers
|
|
976
|
+
markAsSubmitted,
|
|
977
|
+
resetWizardState,
|
|
978
|
+
// Utility functions
|
|
979
|
+
shouldShowErrorsForStep,
|
|
980
|
+
getStepStatus,
|
|
981
|
+
getStepTooltip,
|
|
982
|
+
getStepFields
|
|
983
|
+
};
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
exports.breakpoints = breakpoints;
|
|
987
|
+
exports.createDataRestorationHandler = createDataRestorationHandler;
|
|
988
|
+
exports.createFormResetHandler = createFormResetHandler;
|
|
989
|
+
exports.createFormSubmitHandler = createFormSubmitHandler;
|
|
990
|
+
exports.createStepHasFieldErrors = createStepHasFieldErrors;
|
|
991
|
+
exports.extractZodErrors = extractZodErrors;
|
|
992
|
+
exports.formatShortcut = formatShortcut;
|
|
993
|
+
exports.getDefaultLtmRange = getDefaultLtmRange;
|
|
994
|
+
exports.matchesShortcut = matchesShortcut;
|
|
995
|
+
exports.useBreakpoints = useBreakpoints;
|
|
996
|
+
exports.useComparisonData = useComparisonData;
|
|
997
|
+
exports.useCopyToClipboard = useCopyToClipboard;
|
|
998
|
+
exports.useDebounce = useDebounce;
|
|
999
|
+
exports.useFinancialStatementState = useFinancialStatementState;
|
|
1000
|
+
exports.useKeyboardShortcut = useKeyboardShortcut;
|
|
1001
|
+
exports.useLocalStorage = useLocalStorage;
|
|
1002
|
+
exports.useMediaQuery = useMediaQuery;
|
|
1003
|
+
exports.useSessionErrors = useSessionErrors;
|
|
1004
|
+
exports.useStatementExport = useStatementExport;
|
|
1005
|
+
exports.useStatementMobileMode = useStatementMobileMode;
|
|
1006
|
+
exports.useToast = useToast;
|
|
1007
|
+
exports.useWizardForm = useWizardForm;
|