@eztrak/shared 0.7.0 → 0.7.3

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/README.md CHANGED
@@ -8,13 +8,38 @@ Shared utilities, hooks, and UI components for Eztrak applications.
8
8
  npm install @eztrak/shared
9
9
  ```
10
10
 
11
- Peer dependencies (hooks + `handleApiError` + components):
11
+ Peer dependencies:
12
12
 
13
13
  ```bash
14
- npm install react react-dom react-router-dom react-hot-toast sweetalert2
14
+ npm install react react-dom react-router-dom react-hot-toast sweetalert2 react-icons framer-motion
15
15
  ```
16
16
 
17
- Your app must render `<Toaster />` from react-hot-toast (e.g. in your root layout).
17
+ Your app must render `<Toaster />` from `react-hot-toast` (e.g. in your root layout).
18
+
19
+ Import loader styles once if you use the `Loader` component:
20
+
21
+ ```ts
22
+ import "@eztrak/shared/components/loader.css";
23
+ ```
24
+
25
+ `CustomCellEditor` does **not** require `loader.css` — it uses an inline SVG spinner for the save button.
26
+
27
+ ## Development
28
+
29
+ From the monorepo root:
30
+
31
+ ```bash
32
+ npm run build:shared
33
+ npm run storybook
34
+ ```
35
+
36
+ From `packages/shared`:
37
+
38
+ ```bash
39
+ npm run build
40
+ npm run storybook
41
+ npm run build:storybook
42
+ ```
18
43
 
19
44
  ## Usage
20
45
 
@@ -171,6 +196,77 @@ import {
171
196
 
172
197
  `ResetColumnsButton` shows a SweetAlert2 confirmation before calling `onReset`.
173
198
 
199
+ ### Components — CustomCellEditor
200
+
201
+ AG Grid cell editor with inline save/cancel buttons, keyboard shortcuts (Enter to save, Escape to cancel), and toast feedback. Implements the AG Grid `ICellEditor` ref API (`getValue`, `isCancelBeforeStart`, `isCancelAfterEnd`).
202
+
203
+ Your app owns the API call via `onSave` — the package does not use RTK Query or app-specific endpoints.
204
+
205
+ For narrow columns, set `cellEditorPopup: true` on the column definition so the editor (and save/cancel buttons) is not clipped.
206
+
207
+ ```tsx
208
+ import { CustomCellEditor } from "@eztrak/shared/components";
209
+ import { ELITE_API } from "../constants/apiConstants";
210
+ import { createCellEditorOnSave } from "../utils/cellEditorOnSave";
211
+
212
+ // In column defs:
213
+ {
214
+ field: "name",
215
+ editable: true,
216
+ cellEditor: CustomCellEditor,
217
+ cellEditorPopup: true,
218
+ cellEditorSelector: (params) => ({
219
+ component: CustomCellEditor,
220
+ params: {
221
+ value: params.value,
222
+ name: "name",
223
+ entityId: params.data.id,
224
+ entityName: "Package",
225
+ inputType: "text",
226
+ onSave: createCellEditorOnSave([ELITE_API.PACKAGE]),
227
+ dropdownOptions: [], // when inputType is "dropdown"
228
+ },
229
+ }),
230
+ }
231
+ ```
232
+
233
+ Example `createCellEditorOnSave` helper (elite-pilot pattern):
234
+
235
+ ```js
236
+ import { ELITE_API } from "../constants/apiConstants";
237
+ import { store } from "../redux/store";
238
+ import { generalApi } from "../redux/api/generalApi";
239
+
240
+ export function createCellEditorOnSave(invalidate = []) {
241
+ return async (payload) => {
242
+ const result = await store.dispatch(
243
+ generalApi.endpoints.updateRecord.initiate({
244
+ endpoint: ELITE_API.SAVE_CELL_EDIT,
245
+ data: payload,
246
+ invalidate: invalidate.map((endpoint) => ({
247
+ type: "General",
248
+ id: endpoint,
249
+ })),
250
+ })
251
+ );
252
+ if (result.error) {
253
+ throw new Error(result.error?.data?.message || "Failed to update record");
254
+ }
255
+ };
256
+ }
257
+ ```
258
+
259
+ #### Input types
260
+
261
+ | `inputType` | Behavior |
262
+ | --- | --- |
263
+ | `text` | Standard text input |
264
+ | `number` | Numeric input (filters non-numeric characters); `propertyValue` is sent as a `number` |
265
+ | `date` | Date picker (`YYYY-MM-DD` via `toDateInputValue`) |
266
+ | `dropdown` | Select from `dropdownOptions` |
267
+
268
+ Save is disabled when the value is unchanged (compares as strings so `42` and `"42"` are treated as equal). Pressing Enter while a save is in progress does not submit twice.
269
+
174
270
  ### Components — ResetFiltersButton
175
271
 
176
272
  Clears all URL search params (`react-router-dom`) in one click. Self-contained — no app `Button` needed.
@@ -196,6 +292,104 @@ import { ResetFiltersButton } from "@eztrak/shared/components";
196
292
  | `onReset` | `() => void` | — | Called after search params are cleared |
197
293
  | `disabled` | `boolean` | `false` | Disables the button |
198
294
 
295
+ ### Components — SearchInput
296
+
297
+ URL-synced search field with optional debounced live search. Requires a React Router context (`useSearchParams`).
298
+
299
+ ```tsx
300
+ import { SearchInput } from "@eztrak/shared/components";
301
+
302
+ // Submit on Enter or search icon click (updates ?query= in the URL)
303
+ <SearchInput placeholder="Search items..." />
304
+ ```
305
+
306
+ #### Live search (debounced)
307
+
308
+ ```tsx
309
+ <SearchInput
310
+ placeholder="Search..."
311
+ liveSearch
312
+ debounceDelay={500}
313
+ />
314
+ ```
315
+
316
+ #### Custom URL param name
317
+
318
+ ```tsx
319
+ <SearchInput defaultParam="search" placeholder="Find a user..." />
320
+ ```
321
+
322
+ #### Custom styling and icon
323
+
324
+ ```tsx
325
+ <SearchInput
326
+ searchWrapperClass="border border-gray-200 shadow-sm"
327
+ searchClassName="w-full"
328
+ searchIconClassName="text-blue-500"
329
+ showIcon
330
+ />
331
+ ```
332
+
333
+ #### Hide icon / custom icon
334
+
335
+ ```tsx
336
+ import { HiOutlineSearch } from "react-icons/hi";
337
+
338
+ <SearchInput showIcon={false} />
339
+
340
+ <SearchInput customIcon={<HiOutlineSearch className="h-5 w-5" />} />
341
+ ```
342
+
343
+ `SearchInput` reads and writes a single query param (default `query`). Clearing the input removes that param from the URL. The input stays in sync when the URL changes externally (e.g. browser back/forward or `ResetFiltersButton`).
344
+
345
+ | Prop | Type | Default | Description |
346
+ | --- | --- | --- | --- |
347
+ | `placeholder` | `string` | `"Search..."` | Input placeholder |
348
+ | `defaultParam` | `string` | `"query"` | URL search param key |
349
+ | `defaultValue` | `string` | — | Initial value when the param is absent |
350
+ | `liveSearch` | `boolean` | `false` | Update URL on input change (debounced) |
351
+ | `debounceDelay` | `number` | `500` | Debounce delay in ms when `liveSearch` is true |
352
+ | `showIcon` | `boolean` | `true` | Show the submit/search icon button |
353
+ | `customIcon` | `ReactNode \| null` | `null` | Replace the default search icon |
354
+ | `name` | `string` | `"search"` | Form input `name` (used on submit) |
355
+ | `type` | `string` | `"search"` | Input `type` attribute |
356
+ | `searchWrapperClass` | `string` | — | Classes for the form wrapper |
357
+ | `searchClassName` | `string` | — | Classes for the input |
358
+ | `searchBtn` | `string` | — | Classes for the icon button |
359
+ | `searchIconClassName` | `string` | — | Classes for the default search icon |
360
+
361
+ Also accepts standard `<input>` HTML attributes (except `name`, `type`, `value`, `onChange`, and `defaultValue`, which are managed internally).
362
+
363
+ ### Components — Loader
364
+
365
+ Spinner for full-page or inline loading states. Requires `loader.css`:
366
+
367
+ ```tsx
368
+ import { Loader } from "@eztrak/shared/components";
369
+ import "@eztrak/shared/components/loader.css";
370
+
371
+ <Loader size="36px" color="#F54C00" />
372
+ ```
373
+
374
+ | Prop | Type | Default | Description |
375
+ | --- | --- | --- | --- |
376
+ | `size` | `string` | `"50px"` | Spinner diameter |
377
+ | `color` | `string` | `"#F54C00"` | Spinner accent color |
378
+ | `speed` | `string` | `"1s"` | Animation duration |
379
+ | `className` | `string` | — | Extra class on the spinner element |
380
+
381
+ ### Components — Modal
382
+
383
+ Animated modal dialog (uses `framer-motion`).
384
+
385
+ ```tsx
386
+ import { Modal } from "@eztrak/shared/components";
387
+
388
+ <Modal isOpen={open} onClose={() => setOpen(false)} title="Confirm">
389
+ <p>Modal content</p>
390
+ </Modal>
391
+ ```
392
+
199
393
  ### Components — EztrakTabs
200
394
 
201
395
  Import styles once in your app entry or layout:
@@ -315,6 +509,10 @@ import {
315
509
  useGridHeight,
316
510
  EztrakTabs,
317
511
  CustomPagination,
512
+ SearchInput,
513
+ CustomCellEditor,
514
+ Loader,
515
+ Modal,
318
516
  } from "@eztrak/shared";
319
517
  ```
320
518
 
@@ -323,12 +521,14 @@ import {
323
521
  Run the component playground locally (dev-only, not published to npm):
324
522
 
325
523
  ```bash
524
+ # From monorepo root
525
+ npm run storybook
526
+
527
+ # From packages/shared
326
528
  npm run storybook
327
- # or from the monorepo root:
328
- npm run storybook -w @eztrak/shared
329
529
  ```
330
530
 
331
- Opens on [http://localhost:6006](http://localhost:6006) with stories for `EztrakTabs`, `CustomPagination`, and `ResetColumnsButton`.
531
+ Opens on [http://localhost:6006](http://localhost:6006) with stories for `EztrakTabs`, `CustomPagination`, `ResetColumnsButton`, `CustomCellEditor`, and `Loader`.
332
532
 
333
533
  ## Exports
334
534
 
@@ -337,8 +537,9 @@ Opens on [http://localhost:6006](http://localhost:6006) with stories for `Eztrak
337
537
  | `@eztrak/shared` | Main entry — re-exports utils, hooks, and components |
338
538
  | `@eztrak/shared/utils` | `cn`, `handleApiError`, `confirmationAlert`, date/format helpers, API error helpers, form field helpers |
339
539
  | `@eztrak/shared/hooks` | `usePaginationUrlSync`, `useGridHeight` |
340
- | `@eztrak/shared/components` | `EztrakTabs`, `CustomPagination`, `ResetColumnsButton`, `TableLayoutToolbarControls`, `ResetFiltersButton`, and related types |
540
+ | `@eztrak/shared/components` | `EztrakTabs`, `CustomPagination`, `CustomCellEditor`, `SearchInput`, `ResetFiltersButton`, `Modal`, `Loader`, `TableLayoutToolbarControls`, `ResetColumnsButton`, and related types |
341
541
  | `@eztrak/shared/components/tabs.css` | Default tab styles (CSS variables) |
542
+ | `@eztrak/shared/components/loader.css` | Loader spinner styles |
342
543
 
343
544
  ### EztrakTabs props
344
545
 
@@ -375,12 +576,30 @@ Keyboard: Arrow keys move between tabs; Home/End jump to first/last enabled tab.
375
576
 
376
577
  `CustomPagination` writes `page` and `perPage` to the URL via `react-router-dom`. Your page should read those params, fetch data, and pass the result back as `paginationData`.
377
578
 
579
+ ### CustomCellEditor props
580
+
581
+ | Prop | Type | Default | Description |
582
+ | --- | --- | --- | --- |
583
+ | `value` | `string \| number \| null` | required | Initial cell value (AG Grid passes via `cellEditorParams`) |
584
+ | `name` | `string` | required | Property name sent in save payload as `propertyName` |
585
+ | `stopEditing` | `() => void` | required | AG Grid callback to close the editor |
586
+ | `entityId` | `string \| number` | required | Entity id sent in save payload |
587
+ | `entityName` | `string` | `"Record"` | Entity type name sent in save payload |
588
+ | `inputType` | `CellEditorInputType` | `"text"` | `text`, `number`, `date`, or `dropdown` |
589
+ | `onSave` | `(payload: CellEditPayload) => Promise<void>` | required | Async save handler; throw on failure to show error toast |
590
+ | `dropdownOptions` | `DropdownOption[]` | `[]` | Options when `inputType` is `dropdown` |
591
+
592
+ `CellEditPayload` shape: `{ propertyName, propertyValue, entityId, entityName }`. For `inputType: "number"`, `propertyValue` is a `number` (via `parseFloat`).
593
+
378
594
  ## Requirements
379
595
 
380
596
  - Node.js 18+
381
597
  - React 18+ (for hooks and components)
382
- - `react-router-dom` (required for `CustomPagination` and `usePaginationUrlSync`)
383
- - `sweetalert2` (required for `ResetColumnsButton` and `confirmationAlert`)
598
+ - `react-router-dom` — `CustomPagination`, `SearchInput`, `ResetFiltersButton`, and `usePaginationUrlSync` (optional peer; install if you use these)
599
+ - `react-hot-toast` — toast feedback in `CustomCellEditor` and `handleApiError`
600
+ - `sweetalert2` — `ResetColumnsButton` and `confirmationAlert`
601
+ - `react-icons` — `CustomCellEditor`, `SearchInput`, and `Modal`
602
+ - `framer-motion` — `Modal`
384
603
  - Works with any bundler that supports the [Node.js `exports` field](https://nodejs.org/api/packages.html#exports)
385
604
 
386
605
  ## License
@@ -0,0 +1 @@
1
+ //# sourceMappingURL=chunk-GED6IT3S.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -1,3 +1,10 @@
1
+ // src/utils/cn.ts
2
+ import clsx from "clsx";
3
+ import { twMerge } from "tailwind-merge";
4
+ function cn(...inputs) {
5
+ return twMerge(clsx(inputs));
6
+ }
7
+
1
8
  // src/utils/helpers.ts
2
9
  var truncateText = (text, maxLength = 20) => {
3
10
  if (text === null || text === void 0) return "";
@@ -196,7 +203,76 @@ var getFieldsByInputType = (fields = [], key) => {
196
203
  return fields.filter(({ inputType = "" }) => inputType === key);
197
204
  };
198
205
 
206
+ // src/utils/handleApiError.ts
207
+ import toast from "react-hot-toast";
208
+ import Swal from "sweetalert2";
209
+ function formatValidationErrorsHtml(errors) {
210
+ const items = Object.keys(errors).flatMap(
211
+ (field) => (errors[field] || []).map((msg) => `<li>${msg}</li>`)
212
+ );
213
+ return `
214
+ <div style="text-align:left">
215
+ <p>Please review and correct the following:</p>
216
+ <ul style="margin:8px 0 0 16px; list-style:disc;">
217
+ ${items.join("")}
218
+ </ul>
219
+ </div>
220
+ `;
221
+ }
222
+ function showValidationAlert(html) {
223
+ void Swal.fire({
224
+ title: "Validation errors",
225
+ html,
226
+ icon: "error",
227
+ confirmButtonText: "OK",
228
+ showCancelButton: false,
229
+ confirmButtonColor: "#FF7335"
230
+ });
231
+ }
232
+ function handleApiError(error, { showAlert = false, fallbackMessage } = {}) {
233
+ if (!error) return;
234
+ const status = error.status ?? error.originalStatus;
235
+ const data = error.data;
236
+ if (status === 200) {
237
+ return;
238
+ }
239
+ if (data?.errors && typeof data.errors === "object") {
240
+ const errors = data.errors;
241
+ if (showAlert) {
242
+ showValidationAlert(formatValidationErrorsHtml(errors));
243
+ } else {
244
+ Object.keys(errors).forEach((field) => {
245
+ (errors[field] || []).forEach((errorMessage) => {
246
+ toast.error(errorMessage);
247
+ });
248
+ });
249
+ }
250
+ return;
251
+ }
252
+ if (status === 404) {
253
+ return;
254
+ }
255
+ const message = fallbackMessage || data?.message || data?.title || error.error || "An error occurred. Please try again.";
256
+ toast.error(message);
257
+ }
258
+ function confirmationAlert(onConfirm, options = {}) {
259
+ void Swal.fire({
260
+ title: options.title ?? "Are you sure?",
261
+ text: options.text,
262
+ icon: options.icon ?? "warning",
263
+ showCancelButton: true,
264
+ confirmButtonText: options.confirmButtonText ?? "Yes",
265
+ cancelButtonText: options.cancelButtonText ?? "Cancel",
266
+ confirmButtonColor: "#FF7335"
267
+ }).then((result) => {
268
+ if (result.isConfirmed) {
269
+ onConfirm();
270
+ }
271
+ });
272
+ }
273
+
199
274
  export {
275
+ cn,
200
276
  truncateText,
201
277
  truncateFileName,
202
278
  getApiError,
@@ -218,6 +294,9 @@ export {
218
294
  deserializeJson,
219
295
  formatDateTime,
220
296
  removeByPropertyValues,
221
- getFieldsByInputType
297
+ getFieldsByInputType,
298
+ formatValidationErrorsHtml,
299
+ handleApiError,
300
+ confirmationAlert
222
301
  };
223
- //# sourceMappingURL=chunk-GIBPOQGL.mjs.map
302
+ //# sourceMappingURL=chunk-GVIHLI67.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/utils/cn.ts","../src/utils/helpers.ts","../src/utils/handleApiError.ts"],"sourcesContent":["import clsx from \"clsx\";\r\nimport { twMerge } from \"tailwind-merge\";\r\n\r\nexport function cn(...inputs: Parameters<typeof clsx>) {\r\n return twMerge(clsx(inputs));\r\n}\r\n","export const truncateText = (\r\n text: string | number | null | undefined,\r\n maxLength = 20\r\n): string => {\r\n if (text === null || text === undefined) return \"\";\r\n const value = String(text);\r\n if (value.length <= maxLength) {\r\n return value;\r\n }\r\n return value.slice(0, maxLength - 3) + \"...\";\r\n};\r\n\r\nexport const truncateFileName = (\r\n fileName: string | null | undefined,\r\n maxLength = 20\r\n): string => {\r\n if (!fileName) return \"\";\r\n if (fileName.length <= maxLength) {\r\n return fileName;\r\n }\r\n const extension = fileName.slice(fileName.lastIndexOf(\".\"));\r\n const truncatedName =\r\n fileName.slice(0, maxLength - extension.length - 3) + \"...\";\r\n return truncatedName + extension;\r\n};\r\n\r\nexport function getApiError(apiError: unknown): string {\r\n if (!apiError) return \"An unknown error occurred\";\r\n\r\n if (typeof apiError === \"string\") {\r\n return apiError;\r\n }\r\n\r\n const error = apiError as {\r\n response?: { data?: Record<string, unknown> };\r\n message?: string;\r\n };\r\n\r\n if (error.response?.data) {\r\n const responseData = error.response.data;\r\n\r\n if (typeof responseData === \"string\") {\r\n return responseData;\r\n }\r\n\r\n const errors = responseData.errors;\r\n if (errors && typeof errors === \"object\") {\r\n for (const key in errors) {\r\n const messages = (errors as Record<string, unknown>)[key];\r\n if (Array.isArray(messages) && messages.length > 0) {\r\n return String(messages[0]);\r\n }\r\n }\r\n }\r\n\r\n if (responseData.message) {\r\n return String(responseData.message);\r\n }\r\n\r\n if (responseData.title) {\r\n return String(responseData.title);\r\n }\r\n }\r\n\r\n if (error.message) {\r\n return error.message;\r\n }\r\n\r\n return \"An unknown error occurred\";\r\n}\r\n\r\nexport const normalizeApiListItems = (responseData: unknown): unknown[] => {\r\n if (responseData == null) return [];\r\n if (Array.isArray(responseData)) return responseData;\r\n\r\n const record = responseData as Record<string, unknown>;\r\n const data = record.data;\r\n\r\n if (\r\n data &&\r\n typeof data === \"object\" &&\r\n !Array.isArray(data) &&\r\n Array.isArray((data as { items?: unknown[] }).items)\r\n ) {\r\n return (data as { items: unknown[] }).items;\r\n }\r\n\r\n const returnModel = record.returnModel as { items?: unknown[] } | undefined;\r\n if (Array.isArray(returnModel?.items)) return returnModel.items;\r\n if (Array.isArray(data)) return data;\r\n if (typeof data === \"object\" && data !== null) {\r\n return [data];\r\n }\r\n return [];\r\n};\r\n\r\nexport function capitalizeText(name: string | null | undefined): string {\r\n if (!name) return \"\";\r\n return name\r\n .split(\" \")\r\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())\r\n .join(\" \");\r\n}\r\n\r\nexport function toLowerCaseText(text: string | null | undefined): string {\r\n if (!text) return \"\";\r\n return text.toLowerCase();\r\n}\r\n\r\nexport const formatDate = (dateString: string | null | undefined): string => {\r\n if (dateString === null || dateString === undefined) return \"-\";\r\n const date = new Date(dateString);\r\n\r\n if (isNaN(date.getTime())) {\r\n return dateString;\r\n }\r\n\r\n const month = (date.getMonth() + 1).toString().padStart(2, \"0\");\r\n const day = date.getDate().toString().padStart(2, \"0\");\r\n const year = date.getFullYear().toString().slice(-2);\r\n return `${month}/${day}/${year}`;\r\n};\r\n\r\nexport const isEmpty = (obj: object): boolean => {\r\n return Object.keys(obj).length === 0;\r\n};\r\n\r\nexport const toDateInputValue = (date: string | Date | null | undefined): string => {\r\n if (!date) return \"\";\r\n\r\n let d: Date;\r\n\r\n if (typeof date === \"string\") {\r\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(date)) {\r\n return date;\r\n }\r\n d = new Date(date);\r\n } else {\r\n d = new Date(date);\r\n }\r\n\r\n if (isNaN(d.getTime())) return \"\";\r\n\r\n const localDate = new Date(d.getTime() - d.getTimezoneOffset() * 60000);\r\n const year = localDate.getFullYear();\r\n const month = (localDate.getMonth() + 1).toString().padStart(2, \"0\");\r\n const day = localDate.getDate().toString().padStart(2, \"0\");\r\n\r\n return `${year}-${month}-${day}`;\r\n};\r\n\r\nexport function convertCommaDataToArray(data: string | null | undefined): string[] {\r\n if (!data || data.trim() === \"\") {\r\n return [];\r\n }\r\n return data.split(\",\").map((item) => item.trim());\r\n}\r\n\r\nexport function checkIfExistsInArray<T>(array: T[], searchValue: T): boolean {\r\n return array.includes(searchValue);\r\n}\r\n\r\nexport const currencyFormatter = (value: number | null | undefined): string => {\r\n return value?.toLocaleString(\"en-US\", {\r\n style: \"currency\",\r\n currency: \"USD\",\r\n }) ?? \"\";\r\n};\r\nexport const formatNumber = (number: number | string | null | undefined): string => {\r\n if (isNaN(Number(number)) || number === null || number === undefined) {\r\n return \"0\";\r\n }\r\n\r\n const formattedNumber = Number(number)\r\n .toString()\r\n .replace(/\\B(?=(\\d{3})+(?!\\d))/g, \",\");\r\n\r\n return formattedNumber;\r\n};\r\n\r\nexport const parseCurrencyValueToNumber = (value: unknown): number => {\r\n if (value == null || value === \"\") return 0;\r\n\r\n const raw =\r\n typeof value === \"object\" && value !== null && \"value\" in value\r\n ? (value as { value: unknown }).value\r\n : value;\r\n\r\n const stringValue = raw == null || raw === \"\" ? \"\" : String(raw);\r\n const numericValue = stringValue.replace(/[$,]/g, \"\");\r\n return parseFloat(numericValue) || 0;\r\n};\r\n\r\nexport function flattenTableData<T extends Record<string, unknown>>(\r\n data: T[] | null | undefined,\r\n columns: { field?: string }[] | null | undefined\r\n): T[] {\r\n if (!Array.isArray(data)) return [];\r\n return data.map((row) => {\r\n const flatRow = { ...row };\r\n (columns ?? []).forEach((col) => {\r\n const key = col.field;\r\n if (!key) return;\r\n if (key.includes(\".\")) {\r\n const value = key.split(\".\").reduce<unknown>(\r\n (acc, part) => (acc as Record<string, unknown> | undefined)?.[part],\r\n row\r\n );\r\n (flatRow as Record<string, unknown>)[key] = value ?? \"\";\r\n }\r\n });\r\n return flatRow;\r\n });\r\n}\r\n\r\nexport const getErrorMessages = (error: {\r\n data?: {\r\n errors?: Record<string, string[]>;\r\n title?: string;\r\n error?: { name?: string[] };\r\n };\r\n} | null | undefined): string => {\r\n if (error?.data?.errors) {\r\n return Object.values(error.data.errors).flat().join(\"\\n\");\r\n }\r\n return (\r\n error?.data?.title ??\r\n error?.data?.error?.name?.[0] ??\r\n \"Failed to save\"\r\n );\r\n};\r\n\r\nexport const getFieldsByCategory = <T extends { category?: string }>(\r\n fields: T[] = [],\r\n key: string\r\n): T[] => {\r\n return fields?.filter(({ category = \"\" }) => category === key);\r\n};\r\n\r\nexport const getFieldsByName = <T extends { name?: string }>(\r\n fields: T[] = [],\r\n key: string\r\n): T[] => {\r\n return fields.filter(({ name = \"\" }) => name === key);\r\n};\r\n\r\nexport const deserializeJson = <T = unknown>(jsonString: string): T | null => {\r\n try {\r\n return JSON.parse(jsonString) as T;\r\n } catch (error) {\r\n console.error(\"Error parsing JSON:\", error);\r\n return null;\r\n }\r\n};\r\n\r\nexport const formatDateTime = (dateString: string | null | undefined): string => {\r\n if (!dateString) return \"Invalid Date\";\r\n const date = new Date(dateString);\r\n if (isNaN(date.getTime())) return \"Invalid Date\";\r\n const options: Intl.DateTimeFormatOptions = {\r\n year: \"numeric\",\r\n month: \"numeric\",\r\n day: \"numeric\",\r\n hour: \"numeric\",\r\n minute: \"numeric\",\r\n hour12: true,\r\n };\r\n return date.toLocaleString(\"en-US\", options);\r\n};\r\n\r\nexport const removeByPropertyValues = <T extends Record<string, unknown>>(\r\n items: T[] = [],\r\n property: keyof T & string,\r\n values: unknown[] = [],\r\n { caseInsensitive = false }: { caseInsensitive?: boolean } = {}\r\n): T[] => {\r\n if (!Array.isArray(items) || !property) return Array.isArray(items) ? items : [];\r\n const normalize = (v: unknown) =>\r\n caseInsensitive && typeof v === \"string\" ? v.toLowerCase() : v;\r\n const valuesArr = Array.isArray(values) ? values : [values];\r\n const toRemove = new Set(valuesArr.map(normalize));\r\n return items.filter((item) => !toRemove.has(normalize(item?.[property])));\r\n};\r\n\r\nexport const getFieldsByInputType = <T extends { inputType?: string }>(\r\n fields: T[] = [],\r\n key: string\r\n): T[] => {\r\n return fields.filter(({ inputType = \"\" }) => inputType === key);\r\n};\r\n","import toast from \"react-hot-toast\";\r\nimport Swal from \"sweetalert2\";\r\n\r\nexport type RtkQueryError = {\r\n status?: number | string;\r\n originalStatus?: number;\r\n data?: {\r\n errors?: Record<string, string[]>;\r\n message?: string;\r\n title?: string;\r\n };\r\n error?: string;\r\n};\r\n\r\nexport interface HandleApiErrorOptions {\r\n showAlert?: boolean;\r\n fallbackMessage?: string;\r\n}\r\n\r\nexport function formatValidationErrorsHtml(\r\n errors: Record<string, string[]>\r\n): string {\r\n const items = Object.keys(errors).flatMap((field) =>\r\n (errors[field] || []).map((msg) => `<li>${msg}</li>`)\r\n );\r\n return `\r\n <div style=\"text-align:left\">\r\n <p>Please review and correct the following:</p>\r\n <ul style=\"margin:8px 0 0 16px; list-style:disc;\">\r\n ${items.join(\"\")}\r\n </ul>\r\n </div>\r\n `;\r\n}\r\n\r\nfunction showValidationAlert(html: string): void {\r\n void Swal.fire({\r\n title: \"Validation errors\",\r\n html,\r\n icon: \"error\",\r\n confirmButtonText: \"OK\",\r\n showCancelButton: false,\r\n confirmButtonColor: \"#FF7335\",\r\n });\r\n}\r\n\r\n/** Handles RTK Query errors with built-in toast.error or SweetAlert (when showAlert is true). */\r\nexport function handleApiError(\r\n error: RtkQueryError | null | undefined,\r\n { showAlert = false, fallbackMessage }: HandleApiErrorOptions = {}\r\n): void {\r\n if (!error) return;\r\n\r\n const status = error.status ?? error.originalStatus;\r\n const data = error.data;\r\n\r\n if (status === 200) {\r\n return;\r\n }\r\n\r\n if (data?.errors && typeof data.errors === \"object\") {\r\n const errors = data.errors;\r\n\r\n if (showAlert) {\r\n showValidationAlert(formatValidationErrorsHtml(errors));\r\n } else {\r\n Object.keys(errors).forEach((field) => {\r\n (errors[field] || []).forEach((errorMessage) => {\r\n toast.error(errorMessage);\r\n });\r\n });\r\n }\r\n return;\r\n }\r\n\r\n if (status === 404) {\r\n return;\r\n }\r\n\r\n const message =\r\n fallbackMessage ||\r\n data?.message ||\r\n data?.title ||\r\n error.error ||\r\n \"An error occurred. Please try again.\";\r\n\r\n toast.error(message);\r\n}\r\n\r\nexport interface ConfirmationAlertOptions {\r\n title?: string;\r\n text?: string;\r\n icon?: \"warning\" | \"info\" | \"question\" | \"error\" | \"success\";\r\n confirmButtonText?: string;\r\n cancelButtonText?: string;\r\n}\r\n\r\nexport function confirmationAlert(\r\n onConfirm: () => void,\r\n options: ConfirmationAlertOptions = {}\r\n): void {\r\n void Swal.fire({\r\n title: options.title ?? \"Are you sure?\",\r\n text: options.text,\r\n icon: options.icon ?? \"warning\",\r\n showCancelButton: true,\r\n confirmButtonText: options.confirmButtonText ?? \"Yes\",\r\n cancelButtonText: options.cancelButtonText ?? \"Cancel\",\r\n confirmButtonColor: \"#FF7335\",\r\n }).then((result) => {\r\n if (result.isConfirmed) {\r\n onConfirm();\r\n }\r\n });\r\n}\r\n"],"mappings":"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","names":[]}