@grafosoft/excel-validator 1.0.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/README.md ADDED
@@ -0,0 +1,357 @@
1
+ # excel-validator
2
+
3
+ Componente React para validar archivos Excel (`.xlsx` / `.xls`) con reglas declarativas (`jsonValidator`) y catálogos remotos (`fetch` / `string-empty`).
4
+
5
+ ## Requisitos
6
+
7
+ - Node.js 20+
8
+ - Next.js 16+
9
+ - React 19+
10
+ - Proyecto con Tailwind CSS (el componente usa clases utility)
11
+ - Proveedor de HeroUI y `next-themes` en el árbol de la app
12
+
13
+ ## Instalación
14
+
15
+ ```bash
16
+ npm install excel-validator @heroui/react next-themes xlsx react-icons framer-motion
17
+ ```
18
+
19
+ ## Uso básico
20
+
21
+ ### 1) Configura providers globales
22
+
23
+ ```tsx
24
+ "use client";
25
+
26
+ import { HeroUIProvider } from "@heroui/react";
27
+ import { ThemeProvider as NextThemesProvider } from "next-themes";
28
+
29
+ export function Providers({ children }: { children: React.ReactNode }) {
30
+ return (
31
+ <HeroUIProvider>
32
+ <NextThemesProvider attribute="class" defaultTheme="light">
33
+ {children}
34
+ </NextThemesProvider>
35
+ </HeroUIProvider>
36
+ );
37
+ }
38
+ ```
39
+
40
+ ### 2) Renderiza `ExcelLayout`
41
+
42
+ ```tsx
43
+ "use client";
44
+
45
+ import { ExcelLayout } from "excel-validator";
46
+ import type { SettingsProps } from "excel-validator";
47
+
48
+ export default function Page() {
49
+ const initData: SettingsProps = {
50
+ apikey: "794340c3-4956-4763-b8ab-145da3510100",
51
+ companyId: "1",
52
+ environment: "https://lab.globho.com/",
53
+ title: "Catálogo Cuental",
54
+ type: "items",
55
+ };
56
+
57
+ const jsonValidator = [
58
+ "int",
59
+ {
60
+ type: "fetch",
61
+ url: "api/v1/codesystem/item-groups",
62
+ prop: "name",
63
+ },
64
+ "string(255)",
65
+ "$string(20)",
66
+ "$string(20)",
67
+ "decimal",
68
+ "decimal",
69
+ "decimal",
70
+ "string(255)",
71
+ "bool",
72
+ ];
73
+
74
+ return <ExcelLayout initData={initData} jsonValidator={jsonValidator} />;
75
+ }
76
+ ```
77
+
78
+ ## API
79
+
80
+ ### `SettingsProps`
81
+
82
+ ```ts
83
+ type SettingsProps = {
84
+ apikey: string;
85
+ companyId: string;
86
+ environment: string;
87
+ title: string;
88
+ type: string;
89
+ };
90
+ ```
91
+
92
+ ### Props de `ExcelLayout`
93
+
94
+ ```ts
95
+ type ExcelLayoutProps = {
96
+ initData: SettingsProps;
97
+ jsonValidator: (string | { type: string; url: string; prop: string })[];
98
+ isBalances?: boolean;
99
+ };
100
+ ```
101
+
102
+ ## Formato de reglas (`jsonValidator`)
103
+
104
+ - `"int"`: valida entero
105
+ - `"decimal"`: valida decimal
106
+ - `"bool"`: valida booleano
107
+ - `"string(n)"`: valida texto con longitud máxima `n`
108
+ - `"string(yyyy-mm-dd)"`: valida formato fecha
109
+ - Regla opcional: prefijo `$`, por ejemplo `"$string(20)"`
110
+ - `{ type: "fetch", url, prop }`: valida contra catálogo remoto
111
+ - `{ type: "string-empty", url, prop, validation }`: permite vacío o valida con consulta remota
112
+
113
+ ## Configuraciones de ejemplo (tomadas de `page.tsx`)
114
+
115
+ ### PLAN DE CUENTAS
116
+
117
+ ```ts
118
+ const jsonValidator = [
119
+ "int",
120
+ {
121
+ type: "string-empty",
122
+ url: "api/v1/codesystem/accounts",
123
+ prop: "code",
124
+ validation: "string(20)",
125
+ },
126
+ "string(255)",
127
+ "$string(30)",
128
+ "$string(10)",
129
+ "$string(30)",
130
+ "decimal",
131
+ ];
132
+ ```
133
+
134
+ ### SALDOS INICIALES
135
+
136
+ ```ts
137
+ const jsonValidator = [
138
+ {
139
+ type: "fetch",
140
+ url: "api/v1/codesystem/accounts",
141
+ prop: "code",
142
+ },
143
+ {
144
+ type: "fetch",
145
+ url: "api/v1/codesystem/contacts",
146
+ prop: "code",
147
+ },
148
+ "decimal",
149
+ "decimal",
150
+ "$string(255)",
151
+ "$int",
152
+ ];
153
+ ```
154
+
155
+ ### CATÁLOGO GLOBHO
156
+
157
+ ```ts
158
+ const jsonValidator = [
159
+ "int",
160
+ "string(255)",
161
+ {
162
+ type: "fetch",
163
+ url: "api/v1/codesystem/item-groups",
164
+ prop: "id",
165
+ },
166
+ "$string(20)",
167
+ "$string(20)",
168
+ "$string(500)",
169
+ "bool",
170
+ "decimal",
171
+ "$string(20)",
172
+ {
173
+ type: "fetch",
174
+ url: "api/v1/valueset/units",
175
+ prop: "code",
176
+ },
177
+ {
178
+ type: "fetch",
179
+ url: "api/v1/valueset/pharmaceuticalForms",
180
+ prop: "id",
181
+ },
182
+ "decimal",
183
+ "decimal",
184
+ {
185
+ type: "fetch",
186
+ url: "api/v1/valueset/taxes",
187
+ prop: "code",
188
+ },
189
+ "$string(255)",
190
+ "$string(100)",
191
+ "$string(100)",
192
+ "bool",
193
+ "int",
194
+ "$string(20)",
195
+ "decimal",
196
+ "decimal",
197
+ ];
198
+ ```
199
+
200
+ ### CATÁLOGO CUENTAL
201
+
202
+ ```ts
203
+ const jsonValidator = [
204
+ "int",
205
+ {
206
+ type: "fetch",
207
+ url: "api/v1/codesystem/item-groups",
208
+ prop: "name",
209
+ },
210
+ "string(255)",
211
+ "$string(20)",
212
+ "$string(20)",
213
+ "decimal",
214
+ "decimal",
215
+ "decimal",
216
+ "string(255)",
217
+ "bool",
218
+ ];
219
+ ```
220
+
221
+ ### TERCEROS
222
+
223
+ ```ts
224
+ const jsonValidator = [
225
+ "int",
226
+ {
227
+ type: "fetch",
228
+ url: "api/v1/valueset/contactTypes",
229
+ prop: "id",
230
+ },
231
+ {
232
+ type: "fetch",
233
+ url: "api/v1/valueset/contactPersons",
234
+ prop: "id",
235
+ },
236
+ {
237
+ type: "fetch",
238
+ url: "api/v1/valueset/identificationTypes",
239
+ prop: "id",
240
+ },
241
+ {
242
+ type: "string-empty",
243
+ url: "api/v1/codesystem/contacts",
244
+ prop: "code",
245
+ validation: "string(20)",
246
+ },
247
+ "$string(255)",
248
+ "$string(50)",
249
+ "$string(50)",
250
+ "$string(50)",
251
+ "$string(50)",
252
+ "$string(100)",
253
+ "$string(100)",
254
+ "$string(100)",
255
+ "$string(500)",
256
+ {
257
+ type: "fetch",
258
+ url: "api/v1/valueset/cities",
259
+ prop: "code",
260
+ },
261
+ "$string(100)",
262
+ {
263
+ type: "fetch",
264
+ url: "api/v1/valueset/contactRegimes",
265
+ prop: "id",
266
+ },
267
+ "$string(100)",
268
+ "$string(100)",
269
+ "bool",
270
+ "int",
271
+ "$string(100)",
272
+ "$string(100)",
273
+ "$string(100)",
274
+ "$string(yyyy-mm-dd)",
275
+ "$string(100)",
276
+ {
277
+ type: "fetch",
278
+ url: "api/v1/valueset/activities",
279
+ prop: "id",
280
+ },
281
+ {
282
+ type: "fetch",
283
+ url: "api/v1/valueset/contactResponsibilities",
284
+ prop: "id",
285
+ },
286
+ "$string(6)",
287
+ "int",
288
+ {
289
+ type: "fetch",
290
+ url: "api/v1/valueset/taxTypes",
291
+ prop: "id",
292
+ },
293
+ ];
294
+ ```
295
+
296
+ ### PACIENTES
297
+
298
+ ```ts
299
+ const jsonValidator = [
300
+ "int",
301
+ "string(20)",
302
+ {
303
+ type: "fetch",
304
+ url: "api/v1/valueset/identificationTypes",
305
+ prop: "id",
306
+ },
307
+ "string(50)",
308
+ "$string(50)",
309
+ "string(50)",
310
+ "$string(50)",
311
+ "string(yyyy-mm-dd)",
312
+ {
313
+ type: "fetch",
314
+ url: "api/v1/valueset/genderGroups",
315
+ prop: "id",
316
+ },
317
+ {
318
+ type: "fetch",
319
+ url: "api/v1/valueset/genderIdentities",
320
+ prop: "id",
321
+ },
322
+ "$string(100)",
323
+ "$string(100)",
324
+ "$string(100)",
325
+ {
326
+ type: "fetch",
327
+ url: "api/v1/valueset/cities",
328
+ prop: "code",
329
+ },
330
+ {
331
+ type: "fetch",
332
+ url: "api/v1/valueset/patientRegimes",
333
+ prop: "code",
334
+ },
335
+ {
336
+ type: "fetch",
337
+ url: "api/v1/valueset/patientStatus",
338
+ prop: "id",
339
+ },
340
+ ];
341
+ ```
342
+
343
+ ## Reglas de ejecución para `initData`
344
+
345
+ - `environment` debe ser una URL base válida (ejemplo: `https://lab.globho.com/`).
346
+ - El componente normaliza automáticamente la barra final `/`.
347
+ - Las reglas `fetch` y `string-empty` construyen URL final con:
348
+ - `apikey`
349
+ - `companyId`
350
+
351
+ ## Publicación del paquete
352
+
353
+ ```bash
354
+ npm run build:lib
355
+ npm pack --dry-run
356
+ npm publish --access public
357
+ ```
@@ -0,0 +1,5 @@
1
+ import { ExcelMatrix } from "../types/excelTypes";
2
+ export declare const BalanceTotals: ({ headers, rows, }: {
3
+ headers: string[];
4
+ rows: ExcelMatrix;
5
+ }) => import("react").JSX.Element;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BalanceTotals = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const balances_1 = require("../utils/balances");
6
+ const tb_1 = require("react-icons/tb");
7
+ const BalanceTotals = ({ headers, rows, }) => {
8
+ const totalDebit = (0, balances_1.sumColumn)(rows, headers, "ValorDebito");
9
+ const totalCredit = (0, balances_1.sumColumn)(rows, headers, "ValorCredito");
10
+ const difference = Math.abs(totalDebit - totalCredit);
11
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "mt-4 flex flex-col items-end gap-2 pt-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm text-default-500 font-medium uppercase tracking-wide text-right", children: "Total D\u00E9bito:" }), (0, jsx_runtime_1.jsxs)("span", { className: "text-xl font-semibold text-default-700 w-55 text-right", children: ["COP ", (0, balances_1.formatNumber)(totalDebit)] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 border-b border-default-200 pb-3", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm text-default-500 font-medium uppercase tracking-wide text-right", children: "Total Cr\u00E9dito:" }), (0, jsx_runtime_1.jsxs)("span", { className: "text-xl font-semibold text-default-700 w-55 text-right", children: ["COP ", (0, balances_1.formatNumber)(totalCredit), " "] })] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 pt-1", children: [(0, jsx_runtime_1.jsx)("span", { className: "text-sm text-default-500 font-medium uppercase tracking-wide text-right", children: "Diferencia:" }), (0, jsx_runtime_1.jsxs)("div", { className: `flex justify-end gap-2 items-center text-xl font-semibold w-55 text-right ${difference === 0 ? "text-success-600" : "text-danger-600"}`, children: [difference !== 0 && (0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { color: "red", size: 20 }), " COP", " ", (0, balances_1.formatNumber)(difference)] })] })] }));
12
+ };
13
+ exports.BalanceTotals = BalanceTotals;
@@ -0,0 +1,2 @@
1
+ import { ExcelFileProps } from "../types/excelTypes";
2
+ export declare const ExcelFile: ({ initData, fileContent, matrix, error, onFileSelected, onReset, validationErrors, onSelectError, hasBalanceDifference, }: ExcelFileProps) => import("react").JSX.Element;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExcelFile = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ const react_2 = require("react");
7
+ const tb_1 = require("react-icons/tb");
8
+ const ExcelValidator_1 = require("./ExcelValidator");
9
+ const ExcelFile = ({ initData, fileContent, matrix, error, onFileSelected, onReset, validationErrors, onSelectError, hasBalanceDifference = false, }) => {
10
+ var _a;
11
+ const [isVisible, setIsVisible] = (0, react_2.useState)(false);
12
+ const inputRef = (0, react_2.useRef)(null);
13
+ const handleReset = () => {
14
+ setIsVisible(false);
15
+ onReset();
16
+ if (inputRef.current) {
17
+ inputRef.current.value = "";
18
+ }
19
+ };
20
+ const bytesFormat = (bytes) => {
21
+ if (typeof bytes !== "number" || bytes < 0) {
22
+ throw new Error("Debes pasar un número entero positivo");
23
+ }
24
+ const KB = 1024;
25
+ const MB = KB * 1024;
26
+ if (bytes >= MB) {
27
+ return (bytes / MB).toFixed(2) + " MB";
28
+ }
29
+ return (bytes / KB).toFixed(2) + " KB";
30
+ };
31
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "h-full bg-content1 p-4 border-r border-default-200", children: [(0, jsx_runtime_1.jsxs)("h2", { className: "text-xl font-semibold text-default-700", children: ["Importa ", initData.title] }), error ? ((0, jsx_runtime_1.jsx)(react_1.Alert, { color: "danger", title: error, className: "mt-3", radius: "sm", hideIconWrapper: true })) : isVisible ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.Alert, { radius: "sm", hideIconWrapper: true, hideIcon: true, isVisible: isVisible, className: "mt-4", variant: "bordered", onClose: handleReset, children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2 text-default-700", children: [(0, jsx_runtime_1.jsx)(tb_1.TbFile, { size: 28 }), " ", (0, jsx_runtime_1.jsxs)("div", { className: "flex flex-col", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm", children: fileContent === null || fileContent === void 0 ? void 0 : fileContent.name }), (0, jsx_runtime_1.jsx)("small", { className: "text-default-500", children: bytesFormat((_a = fileContent === null || fileContent === void 0 ? void 0 : fileContent.size) !== null && _a !== void 0 ? _a : 0) })] })] }) }), (0, jsx_runtime_1.jsx)(ExcelValidator_1.ExcelValidator, { initData: initData, fileContent: fileContent, matrix: matrix, validationErrors: validationErrors, onSelectError: onSelectError, hasBalanceDifference: hasBalanceDifference })] })) : ((0, jsx_runtime_1.jsx)("label", { className: "mt-4 flex cursor-pointer items-center justify-center rounded-lg border border-dashed border-default-300 px-4 py-8 text-sm font-medium text-default-600 transition hover:border-primary hover:text-primary", htmlFor: "excel-upload", children: (0, jsx_runtime_1.jsxs)("div", { className: "text-center flex flex-col items-center gap-2", children: [(0, jsx_runtime_1.jsx)(tb_1.TbUpload, { size: 28 }), (0, jsx_runtime_1.jsx)("p", { className: "font-semibold text-lg", children: "Seleccionar plantilla" }), (0, jsx_runtime_1.jsx)("small", { className: "text-default-500", children: "Adjunta un archivo .xlsx o .xls para validar y previsualizar." })] }) })), (0, jsx_runtime_1.jsx)("input", { id: "excel-upload", ref: inputRef, className: "hidden", type: "file", accept: ".xlsx,.xls", onChange: (event) => {
32
+ void onFileSelected(event);
33
+ setIsVisible(true);
34
+ } })] }));
35
+ };
36
+ exports.ExcelFile = ExcelFile;
@@ -0,0 +1,12 @@
1
+ import { SettingsProps } from "../types/SettingsProps";
2
+ type ExcelLayoutProps = {
3
+ initData: SettingsProps;
4
+ jsonValidator: (string | {
5
+ type: string;
6
+ url: string;
7
+ prop: string;
8
+ })[];
9
+ isBalances?: boolean;
10
+ };
11
+ export declare function ExcelLayout({ initData, jsonValidator, isBalances, }: ExcelLayoutProps): import("react").JSX.Element;
12
+ export {};
@@ -0,0 +1,509 @@
1
+ "use client";
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ exports.ExcelLayout = ExcelLayout;
38
+ const jsx_runtime_1 = require("react/jsx-runtime");
39
+ const react_1 = require("react");
40
+ const XLSX = __importStar(require("xlsx"));
41
+ const ExcelFile_1 = require("./ExcelFile");
42
+ const ExcelPreview_1 = require("./ExcelPreview");
43
+ const ThemeSwitcher_1 = require("./ThemeSwitcher");
44
+ const balances_1 = require("../utils/balances");
45
+ const updateJsonValidatorFetchUrls = (validator, initData) => {
46
+ const environment = initData.environment.endsWith("/")
47
+ ? initData.environment
48
+ : `${initData.environment}/`;
49
+ return validator.map((item) => {
50
+ if (typeof item === "object") {
51
+ const normalizedType = item.type.trim().replace(/^\$/, "").toLowerCase();
52
+ if (normalizedType !== "fetch" && normalizedType !== "string-empty") {
53
+ return item;
54
+ }
55
+ const cleanUrl = item.url.startsWith("/") ? item.url.slice(1) : item.url;
56
+ return Object.assign(Object.assign({}, item), { url: `${environment}${cleanUrl}?apikey=${initData.apikey}&companyId=${initData.companyId}` });
57
+ }
58
+ return item;
59
+ });
60
+ };
61
+ function ExcelLayout({ initData, jsonValidator, isBalances = false, }) {
62
+ const resolvedJsonValidator = (0, react_1.useMemo)(() => updateJsonValidatorFetchUrls(jsonValidator, initData), [jsonValidator, initData]);
63
+ const isSkippedRule = (item) => {
64
+ if (typeof item === "string") {
65
+ const normalizedRule = item.trim().replace(/^\$/, "").toLowerCase();
66
+ if (normalizedRule === "int" ||
67
+ normalizedRule === "decimal" ||
68
+ normalizedRule === "bool") {
69
+ return false;
70
+ }
71
+ if (/^string\([^()]+\)$/.test(normalizedRule)) {
72
+ return false;
73
+ }
74
+ return item.trim().startsWith("$");
75
+ }
76
+ const normalizedType = item.type.trim().replace(/^\$/, "").toLowerCase();
77
+ if (normalizedType === "fetch" || normalizedType === "string-empty") {
78
+ return false;
79
+ }
80
+ return item.type.trim().startsWith("$");
81
+ };
82
+ const rowsPerPage = 30;
83
+ const [fileContent, setFileContent] = (0, react_1.useState)(undefined);
84
+ const [matrix, setMatrix] = (0, react_1.useState)([]);
85
+ const [error, setError] = (0, react_1.useState)(null);
86
+ const [currentPage, setCurrentPage] = (0, react_1.useState)(1);
87
+ const [focusTarget, setFocusTarget] = (0, react_1.useState)(null);
88
+ const [fetchCatalogsByUrl, setFetchCatalogsByUrl] = (0, react_1.useState)({});
89
+ const [stringEmptyResultsByQuery, setStringEmptyResultsByQuery] = (0, react_1.useState)({});
90
+ (0, react_1.useEffect)(() => {
91
+ let isCancelled = false;
92
+ const fetchRules = resolvedJsonValidator.filter((item) => typeof item === "object" &&
93
+ !isSkippedRule(item) &&
94
+ item.type.trim().replace(/^\$/, "").toLowerCase() === "fetch");
95
+ const uniqueFetchUrls = Array.from(new Set(fetchRules.map((rule) => rule.url)));
96
+ if (uniqueFetchUrls.length === 0) {
97
+ return;
98
+ }
99
+ const loadCatalogs = async () => {
100
+ const entries = await Promise.all(uniqueFetchUrls.map(async (url) => {
101
+ try {
102
+ const response = await fetch(url);
103
+ if (!response.ok) {
104
+ return [url, []];
105
+ }
106
+ const data = (await response.json());
107
+ const catalog = Array.isArray(data)
108
+ ? data
109
+ : [];
110
+ return [url, catalog];
111
+ }
112
+ catch (_a) {
113
+ return [url, []];
114
+ }
115
+ }));
116
+ if (!isCancelled) {
117
+ setFetchCatalogsByUrl(Object.fromEntries(entries));
118
+ }
119
+ };
120
+ void loadCatalogs();
121
+ return () => {
122
+ isCancelled = true;
123
+ };
124
+ }, [resolvedJsonValidator]);
125
+ (0, react_1.useEffect)(() => {
126
+ let isCancelled = false;
127
+ const stringEmptyRules = resolvedJsonValidator
128
+ .map((item, index) => ({ item, index }))
129
+ .filter((entry) => typeof entry.item === "object" &&
130
+ entry.item.type.trim().replace(/^\$/, "").toLowerCase() ===
131
+ "string-empty");
132
+ if (stringEmptyRules.length === 0 || matrix.length <= 1) {
133
+ return;
134
+ }
135
+ const filteredRows = matrix
136
+ .slice(1)
137
+ .filter((row) => { var _a, _b; return ((_b = (_a = row[0]) === null || _a === void 0 ? void 0 : _a.toString().trim()) !== null && _b !== void 0 ? _b : "") !== ""; });
138
+ const queryUrls = new Set();
139
+ stringEmptyRules.forEach(({ item, index }) => {
140
+ filteredRows.forEach((row) => {
141
+ var _a;
142
+ const cellValue = String((_a = row[index]) !== null && _a !== void 0 ? _a : "").trim();
143
+ if (cellValue !== "") {
144
+ queryUrls.add(`${item.url}&${encodeURIComponent(item.prop)}=${encodeURIComponent(cellValue)}`);
145
+ }
146
+ });
147
+ });
148
+ if (queryUrls.size === 0) {
149
+ return;
150
+ }
151
+ const loadStringEmptyResults = async () => {
152
+ const entries = await Promise.all([...queryUrls].map(async (queryUrl) => {
153
+ try {
154
+ const response = await fetch(queryUrl);
155
+ if (!response.ok) {
156
+ return [queryUrl, []];
157
+ }
158
+ const data = (await response.json());
159
+ const results = Array.isArray(data)
160
+ ? data
161
+ : [];
162
+ return [queryUrl, results];
163
+ }
164
+ catch (_a) {
165
+ return [queryUrl, []];
166
+ }
167
+ }));
168
+ if (!isCancelled) {
169
+ setStringEmptyResultsByQuery((prev) => (Object.assign(Object.assign({}, prev), Object.fromEntries(entries))));
170
+ }
171
+ };
172
+ void loadStringEmptyResults();
173
+ return () => {
174
+ isCancelled = true;
175
+ };
176
+ }, [resolvedJsonValidator, matrix]);
177
+ const validationErrors = (0, react_1.useMemo)(() => {
178
+ if (matrix.length <= 1 || resolvedJsonValidator.length === 0) {
179
+ return [];
180
+ }
181
+ const parseRule = (rawRule) => rawRule.trim().toLowerCase();
182
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
183
+ const buildDatePattern = (format) => {
184
+ const tokenRegex = /(yyyy|yy|mm|dd|m|d)/g;
185
+ const tokens = [...format.matchAll(tokenRegex)];
186
+ if (tokens.length === 0) {
187
+ return null;
188
+ }
189
+ let pattern = "^";
190
+ let cursor = 0;
191
+ const tokenOrder = [];
192
+ tokens.forEach((tokenMatch) => {
193
+ var _a;
194
+ const token = tokenMatch[0];
195
+ const tokenStart = (_a = tokenMatch.index) !== null && _a !== void 0 ? _a : 0;
196
+ const literal = format.slice(cursor, tokenStart);
197
+ pattern += escapeRegExp(literal);
198
+ if (token === "yyyy") {
199
+ pattern += "(\\d{4})";
200
+ }
201
+ else if (token === "yy") {
202
+ pattern += "(\\d{2})";
203
+ }
204
+ else if (token === "mm") {
205
+ pattern += "(0[1-9]|1[0-2])";
206
+ }
207
+ else if (token === "m") {
208
+ pattern += "([1-9]|1[0-2])";
209
+ }
210
+ else if (token === "dd") {
211
+ pattern += "(0[1-9]|[12]\\d|3[01])";
212
+ }
213
+ else {
214
+ pattern += "([1-9]|[12]\\d|3[01])";
215
+ }
216
+ tokenOrder.push(token);
217
+ cursor = tokenStart + token.length;
218
+ });
219
+ pattern += `${escapeRegExp(format.slice(cursor))}$`;
220
+ return {
221
+ tokenOrder,
222
+ regex: new RegExp(pattern),
223
+ };
224
+ };
225
+ const validateDateByFormat = (value, format) => {
226
+ const pattern = buildDatePattern(format);
227
+ if (!pattern) {
228
+ return {
229
+ valid: false,
230
+ message: `Formato de fecha no soportado: ${format}`,
231
+ };
232
+ }
233
+ const matched = value.match(pattern.regex);
234
+ if (!matched) {
235
+ return {
236
+ valid: false,
237
+ message: `Debe cumplir el formato de fecha ${format}`,
238
+ };
239
+ }
240
+ let day = null;
241
+ let month = null;
242
+ let year = null;
243
+ pattern.tokenOrder.forEach((token, index) => {
244
+ const currentValue = matched[index + 1];
245
+ if (!currentValue) {
246
+ return;
247
+ }
248
+ if (token === "dd" || token === "d") {
249
+ day = Number(currentValue);
250
+ return;
251
+ }
252
+ if (token === "mm" || token === "m") {
253
+ month = Number(currentValue);
254
+ return;
255
+ }
256
+ if (token === "yyyy") {
257
+ year = Number(currentValue);
258
+ return;
259
+ }
260
+ year = 2000 + Number(currentValue);
261
+ });
262
+ if (day === null || month === null || year === null) {
263
+ return {
264
+ valid: false,
265
+ message: `El formato de fecha ${format} debe incluir día, mes y año`,
266
+ };
267
+ }
268
+ const candidate = new Date(year, month - 1, day);
269
+ const isValidDate = candidate.getFullYear() === year &&
270
+ candidate.getMonth() === month - 1 &&
271
+ candidate.getDate() === day;
272
+ return {
273
+ valid: isValidDate,
274
+ message: isValidDate ? "" : "La fecha no es válida",
275
+ };
276
+ };
277
+ const validateByRule = (value, rawRule) => {
278
+ const isOptional = rawRule.trim().startsWith("$");
279
+ const rule = parseRule(rawRule.replace(/^\$/, ""));
280
+ const trimmedValue = value.trim();
281
+ if (rule === "int") {
282
+ if (trimmedValue === "") {
283
+ return isOptional
284
+ ? { valid: true, message: "" }
285
+ : { valid: false, message: "Valor vacío" };
286
+ }
287
+ const isValid = /^-?\d+$/.test(trimmedValue);
288
+ return {
289
+ valid: isValid,
290
+ message: isValid ? "" : "Debe ser un número entero",
291
+ };
292
+ }
293
+ if (rule === "decimal") {
294
+ if (trimmedValue === "") {
295
+ return isOptional
296
+ ? { valid: true, message: "" }
297
+ : { valid: false, message: "Valor vacío" };
298
+ }
299
+ if (trimmedValue.includes(",")) {
300
+ return {
301
+ valid: false,
302
+ message: "Debes quitar la coma (,) para que sea un número decimal válido",
303
+ };
304
+ }
305
+ const isValid = /^-?\d+(\.\d+)?$/.test(trimmedValue);
306
+ return {
307
+ valid: isValid,
308
+ message: isValid ? "" : "Debe ser un número decimal",
309
+ };
310
+ }
311
+ if (rule === "bool") {
312
+ if (trimmedValue === "") {
313
+ return isOptional
314
+ ? { valid: true, message: "" }
315
+ : { valid: false, message: "Valor vacío" };
316
+ }
317
+ const isValid = trimmedValue === "SI" || trimmedValue === "NO";
318
+ return {
319
+ valid: isValid,
320
+ message: isValid ? "" : "Debe ser SI o NO",
321
+ };
322
+ }
323
+ const stringRuleMatch = rule.match(/^string\(([^()]+)\)$/);
324
+ if (stringRuleMatch) {
325
+ if (trimmedValue === "") {
326
+ return isOptional
327
+ ? { valid: true, message: "" }
328
+ : { valid: false, message: "Valor vacío" };
329
+ }
330
+ const stringRuleArg = stringRuleMatch[1].trim();
331
+ if (/^\d+$/.test(stringRuleArg)) {
332
+ const maxLength = Number(stringRuleArg);
333
+ const isWithinLength = trimmedValue.length <= maxLength;
334
+ return {
335
+ valid: isWithinLength,
336
+ message: isWithinLength
337
+ ? ""
338
+ : `Longitud máxima permitida: ${maxLength} caracteres`,
339
+ };
340
+ }
341
+ return validateDateByFormat(trimmedValue, stringRuleArg);
342
+ }
343
+ return { valid: true, message: "" };
344
+ };
345
+ const filteredRows = matrix
346
+ .slice(1)
347
+ .filter((row) => { var _a, _b; return ((_b = (_a = row[0]) === null || _a === void 0 ? void 0 : _a.toString().trim()) !== null && _b !== void 0 ? _b : "") !== ""; });
348
+ const detectedErrors = [];
349
+ filteredRows.forEach((row, filteredRowIndex) => {
350
+ resolvedJsonValidator.forEach((rule, columnIndex) => {
351
+ var _a, _b, _c;
352
+ if (isSkippedRule(rule)) {
353
+ return;
354
+ }
355
+ const value = String((_a = row[columnIndex]) !== null && _a !== void 0 ? _a : "");
356
+ const result = (() => {
357
+ var _a;
358
+ if (typeof rule === "string") {
359
+ return validateByRule(value, rule);
360
+ }
361
+ const normalizedType = rule.type
362
+ .trim()
363
+ .replace(/^\$/, "")
364
+ .toLowerCase();
365
+ const isOptional = rule.type.trim().startsWith("$");
366
+ if (normalizedType === "fetch") {
367
+ const trimmedValue = value.trim();
368
+ if (trimmedValue === "") {
369
+ return isOptional
370
+ ? { valid: true, message: "" }
371
+ : { valid: false, message: "Valor vacío" };
372
+ }
373
+ const catalog = (_a = fetchCatalogsByUrl[rule.url]) !== null && _a !== void 0 ? _a : [];
374
+ const hasMatch = catalog.some((catalogItem) => {
375
+ const propValue = catalogItem[rule.prop];
376
+ return String(propValue !== null && propValue !== void 0 ? propValue : "").trim() === trimmedValue;
377
+ });
378
+ return {
379
+ valid: hasMatch,
380
+ message: hasMatch
381
+ ? ""
382
+ : `El valor '${trimmedValue}' no existe en base de datos`,
383
+ };
384
+ }
385
+ if (normalizedType === "string-empty") {
386
+ const trimmedValue = value.trim();
387
+ if (trimmedValue === "") {
388
+ return isOptional
389
+ ? { valid: true, message: "" }
390
+ : { valid: false, message: "Valor vacío" };
391
+ }
392
+ if (rule.validation) {
393
+ const validationResult = validateByRule(trimmedValue, rule.validation);
394
+ if (!validationResult.valid) {
395
+ return validationResult;
396
+ }
397
+ }
398
+ const queryKey = `${rule.url}&${encodeURIComponent(rule.prop)}=${encodeURIComponent(trimmedValue)}`;
399
+ const queryResult = stringEmptyResultsByQuery[queryKey];
400
+ if (queryResult === undefined) {
401
+ return { valid: true, message: "" };
402
+ }
403
+ const alreadyExists = queryResult.length > 0;
404
+ return {
405
+ valid: !alreadyExists,
406
+ message: alreadyExists
407
+ ? `El valor '${trimmedValue}' ya existe`
408
+ : "",
409
+ };
410
+ }
411
+ return { valid: true, message: "" };
412
+ })();
413
+ if (!result.valid) {
414
+ const headers = (_b = matrix[0]) !== null && _b !== void 0 ? _b : [];
415
+ const columnName = ((_c = headers[columnIndex]) === null || _c === void 0 ? void 0 : _c.trim()) || `Columna ${columnIndex + 1}`;
416
+ detectedErrors.push({
417
+ id: `${filteredRowIndex}-${columnIndex}`,
418
+ row: filteredRowIndex + 1,
419
+ column: columnIndex + 1,
420
+ columnName,
421
+ message: result.message,
422
+ value,
423
+ filteredRowIndex,
424
+ });
425
+ }
426
+ });
427
+ });
428
+ return detectedErrors;
429
+ }, [
430
+ matrix,
431
+ resolvedJsonValidator,
432
+ fetchCatalogsByUrl,
433
+ stringEmptyResultsByQuery,
434
+ ]);
435
+ const hasBalanceDifference = (0, react_1.useMemo)(() => {
436
+ var _a;
437
+ if (!isBalances || matrix.length <= 1) {
438
+ return false;
439
+ }
440
+ const headers = (_a = matrix[0]) !== null && _a !== void 0 ? _a : [];
441
+ const dataRows = matrix
442
+ .slice(1)
443
+ .filter((row) => { var _a, _b; return ((_b = (_a = row[0]) === null || _a === void 0 ? void 0 : _a.toString().trim()) !== null && _b !== void 0 ? _b : "") !== ""; });
444
+ const totalDebit = (0, balances_1.sumColumn)(dataRows, headers, "ValorDebito");
445
+ const totalCredit = (0, balances_1.sumColumn)(dataRows, headers, "ValorCredito");
446
+ return Math.abs(totalDebit - totalCredit) !== 0;
447
+ }, [isBalances, matrix]);
448
+ const handleFileSelected = async (event) => {
449
+ var _a;
450
+ const file = (_a = event.target.files) === null || _a === void 0 ? void 0 : _a[0];
451
+ if (!file) {
452
+ return;
453
+ }
454
+ try {
455
+ setError(null);
456
+ setCurrentPage(1);
457
+ setFocusTarget(null);
458
+ const fileBuffer = await file.arrayBuffer();
459
+ const workbook = XLSX.read(fileBuffer, { type: "array" });
460
+ const firstSheetName = workbook.SheetNames[0];
461
+ if (!firstSheetName) {
462
+ setFileContent(file);
463
+ setMatrix([]);
464
+ setError("El archivo no contiene hojas para procesar.");
465
+ return;
466
+ }
467
+ const worksheet = workbook.Sheets[firstSheetName];
468
+ const rawRows = XLSX.utils.sheet_to_json(worksheet, {
469
+ header: 1,
470
+ raw: false,
471
+ defval: "",
472
+ });
473
+ const normalizedRows = rawRows.map((row) => row.map((cell) => String(cell !== null && cell !== void 0 ? cell : "")));
474
+ const recordsCount = Math.max(normalizedRows.length - 1, 0);
475
+ if (recordsCount > 3000) {
476
+ setFileContent(file);
477
+ setMatrix([]);
478
+ setError("El archivo supera el máximo permitido de 3000 registros.");
479
+ return;
480
+ }
481
+ setFileContent(file);
482
+ setMatrix(normalizedRows);
483
+ }
484
+ catch (_b) {
485
+ setFileContent(undefined);
486
+ setMatrix([]);
487
+ setCurrentPage(1);
488
+ setFocusTarget(null);
489
+ setError("No fue posible leer el archivo. Verifica que sea un Excel válido.");
490
+ }
491
+ };
492
+ const handleSelectError = (selectedError) => {
493
+ const targetPage = Math.floor(selectedError.filteredRowIndex / rowsPerPage) + 1;
494
+ setCurrentPage(targetPage);
495
+ setFocusTarget({
496
+ filteredRowIndex: selectedError.filteredRowIndex,
497
+ columnIndex: selectedError.column - 1,
498
+ token: Date.now(),
499
+ });
500
+ };
501
+ const handleReset = () => {
502
+ setFileContent(undefined);
503
+ setMatrix([]);
504
+ setError(null);
505
+ setCurrentPage(1);
506
+ setFocusTarget(null);
507
+ };
508
+ return ((0, jsx_runtime_1.jsxs)("main", { className: "grid min-h-screen grid-cols-1 md:grid-cols-4", children: [(0, jsx_runtime_1.jsxs)("div", { className: "space-y-4 md:col-span-1", children: [(0, jsx_runtime_1.jsx)(ThemeSwitcher_1.ThemeSwitcher, {}), (0, jsx_runtime_1.jsx)(ExcelFile_1.ExcelFile, { initData: initData, fileContent: fileContent, matrix: matrix, error: error, onFileSelected: handleFileSelected, onReset: handleReset, validationErrors: validationErrors, onSelectError: handleSelectError, hasBalanceDifference: hasBalanceDifference })] }), (0, jsx_runtime_1.jsx)("div", { className: "md:col-span-3", children: (0, jsx_runtime_1.jsx)(ExcelPreview_1.ExcelPreview, { initData: initData, fileContent: fileContent, matrix: matrix, error: error, validationErrors: validationErrors, focusTarget: focusTarget, currentPage: currentPage, onPageChange: setCurrentPage, isBalances: isBalances }) })] }));
509
+ }
@@ -0,0 +1,2 @@
1
+ import { ExcelPreviewProps } from "../types/excelTypes";
2
+ export declare const ExcelPreview: ({ initData, fileContent, matrix, error, validationErrors, focusTarget, currentPage, onPageChange, isBalances, }: ExcelPreviewProps) => import("react").JSX.Element;
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExcelPreview = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ const react_2 = require("react");
7
+ const pi_1 = require("react-icons/pi");
8
+ const tb_1 = require("react-icons/tb");
9
+ const BalanceTotals_1 = require("./BalanceTotals");
10
+ const ExcelPreview = ({ initData, fileContent, matrix, error, validationErrors, focusTarget, currentPage, onPageChange, isBalances = false, }) => {
11
+ var _a;
12
+ const rowsPerPage = 30;
13
+ const cellRefs = (0, react_2.useRef)({});
14
+ const maxColumns = (0, react_2.useMemo)(() => matrix.reduce((max, row) => Math.max(max, row.length), 0), [matrix]);
15
+ const tableHeaders = (_a = matrix[0]) !== null && _a !== void 0 ? _a : [];
16
+ const allDataRows = matrix
17
+ .slice(1)
18
+ .filter((row) => { var _a, _b; return ((_b = (_a = row[0]) === null || _a === void 0 ? void 0 : _a.toString().trim()) !== null && _b !== void 0 ? _b : "") !== ""; });
19
+ const totalRecords = allDataRows.length;
20
+ const totalPages = Math.max(1, Math.ceil(totalRecords / rowsPerPage));
21
+ const effectivePage = Math.min(currentPage, totalPages);
22
+ const startIndex = (effectivePage - 1) * rowsPerPage;
23
+ const endIndex = startIndex + rowsPerPage;
24
+ const dataRows = allDataRows.slice(startIndex, endIndex);
25
+ const errorMap = (0, react_2.useMemo)(() => {
26
+ const map = new Set();
27
+ validationErrors.forEach((item) => {
28
+ map.add(`${item.filteredRowIndex}-${item.column - 1}`);
29
+ });
30
+ return map;
31
+ }, [validationErrors]);
32
+ (0, react_2.useEffect)(() => {
33
+ if (!focusTarget) {
34
+ return;
35
+ }
36
+ const targetPage = Math.floor(focusTarget.filteredRowIndex / rowsPerPage) + 1;
37
+ if (targetPage !== effectivePage) {
38
+ return;
39
+ }
40
+ const cellKey = `${focusTarget.filteredRowIndex}-${focusTarget.columnIndex}`;
41
+ const targetCell = cellRefs.current[cellKey];
42
+ if (!targetCell) {
43
+ return;
44
+ }
45
+ requestAnimationFrame(() => {
46
+ targetCell.scrollIntoView({
47
+ behavior: "smooth",
48
+ block: "center",
49
+ inline: "center",
50
+ });
51
+ targetCell.focus();
52
+ });
53
+ }, [focusTarget, effectivePage]);
54
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "h-full bg-gray-50 dark:bg-content1 border-l border-default-200 px-4 py-2", children: [!fileContent ? ((0, jsx_runtime_1.jsx)(react_1.Alert, { color: "primary", title: `Al importar ${initData.title} verás una previsualización de su contenido.`, className: "my-3", radius: "sm", hideIconWrapper: true, variant: "faded" })) : null, fileContent && matrix.length === 0 && !error ? ((0, jsx_runtime_1.jsx)(react_1.Alert, { color: "danger", title: `No se encontraron datos en el archivo seleccionado.`, className: "my-3", radius: "sm", hideIconWrapper: true, variant: "faded" })) : null, fileContent && matrix.length > 0 ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: "mt-4 max-h-[80vh] max-w-full overflow-x-auto overflow-y-auto ", children: (0, jsx_runtime_1.jsxs)("table", { className: "min-w-max w-full text-sm", children: [(0, jsx_runtime_1.jsx)("thead", { className: "sticky top-0 bg-default-100", children: (0, jsx_runtime_1.jsx)("tr", { children: Array.from({ length: maxColumns }).map((_, columnIndex) => ((0, jsx_runtime_1.jsx)("th", { className: "border-x border-t border-default-200 p-3 font-semibold text-default-700", children: tableHeaders[columnIndex] ||
55
+ `Columna ${columnIndex + 1}` }, `header-${columnIndex}`))) }) }), (0, jsx_runtime_1.jsx)("tbody", { className: "bg-content1", children: dataRows.map((row, rowIndex) => ((0, jsx_runtime_1.jsx)("tr", { children: Array.from({ length: maxColumns }).map((_, columnIndex) => (() => {
56
+ var _a, _b;
57
+ const absoluteRowIndex = startIndex + rowIndex;
58
+ const key = `${absoluteRowIndex}-${columnIndex}`;
59
+ const hasError = errorMap.has(key);
60
+ const isFocusedTarget = (focusTarget === null || focusTarget === void 0 ? void 0 : focusTarget.filteredRowIndex) === absoluteRowIndex &&
61
+ (focusTarget === null || focusTarget === void 0 ? void 0 : focusTarget.columnIndex) === columnIndex;
62
+ return ((0, jsx_runtime_1.jsx)("td", { ref: (element) => {
63
+ cellRefs.current[key] = element;
64
+ }, tabIndex: -1, className: `border border-default-200 p-3 align-top whitespace-nowrap outline-none transition-all duration-200 ease-in-out text-default-700
65
+ ${isFocusedTarget ? "ring-[1.7px] ring-danger ring-inset" : ""}`, children: hasError ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between", children: [(0, jsx_runtime_1.jsx)("p", { children: (_a = row[columnIndex]) !== null && _a !== void 0 ? _a : "" }), (0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { color: "red", size: 20 })] })) : ((0, jsx_runtime_1.jsx)("p", { children: (_b = row[columnIndex]) !== null && _b !== void 0 ? _b : "" })) }, `cell-${rowIndex}-${columnIndex}`));
66
+ })()) }, `row-${rowIndex}`))) })] }) }), totalRecords > rowsPerPage ? ((0, jsx_runtime_1.jsxs)("div", { className: "mt-4 flex items-center justify-between gap-3", children: [(0, jsx_runtime_1.jsxs)("p", { className: "text-sm text-default-600", children: ["Mostrando ", startIndex + 1, "-", Math.min(endIndex, totalRecords), " de", " ", totalRecords, " registros"] }), (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", className: "text-sm border border-default-300 disabled:border-default-200 disabled:bg-default-100 disabled:text-default-500", variant: "bordered", disabled: effectivePage === 1, onPress: () => onPageChange(Math.max(1, effectivePage - 1)), isIconOnly: true, children: (0, jsx_runtime_1.jsx)(pi_1.PiCaretLeftBold, { size: 16 }) }), (0, jsx_runtime_1.jsxs)("span", { className: "text-sm text-default-700 mx-1", children: ["Pagina ", effectivePage, " de ", totalPages] }), (0, jsx_runtime_1.jsx)(react_1.Button, { size: "sm", className: "text-sm border border-default-300 disabled:border-default-200 disabled:bg-default-100 disabled:text-default-500", variant: "bordered", disabled: effectivePage === totalPages, onPress: () => onPageChange(Math.min(totalPages, effectivePage + 1)), isIconOnly: true, children: (0, jsx_runtime_1.jsx)(pi_1.PiCaretRightBold, { size: 16 }) })] })] })) : null, isBalances ? ((0, jsx_runtime_1.jsx)(BalanceTotals_1.BalanceTotals, { headers: tableHeaders, rows: allDataRows })) : null] })) : null] }));
67
+ };
68
+ exports.ExcelPreview = ExcelPreview;
@@ -0,0 +1,2 @@
1
+ import { ExcelValidatorProps } from "../types/excelTypes";
2
+ export declare const ExcelValidator: ({ initData, fileContent, matrix, validationErrors, onSelectError, hasBalanceDifference, }: ExcelValidatorProps) => import("react").JSX.Element | null;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExcelValidator = void 0;
4
+ const jsx_runtime_1 = require("react/jsx-runtime");
5
+ const react_1 = require("@heroui/react");
6
+ const react_2 = require("react");
7
+ const tb_1 = require("react-icons/tb");
8
+ const ExcelValidator = ({ initData, fileContent, matrix, validationErrors, onSelectError, hasBalanceDifference = false, }) => {
9
+ const [selectedErrorId, setSelectedErrorId] = (0, react_2.useState)(null);
10
+ const [isUploadingTemplate, setIsUploadingTemplate] = (0, react_2.useState)(false);
11
+ const [uploadMessage, setUploadMessage] = (0, react_2.useState)(null);
12
+ if (!fileContent) {
13
+ return null;
14
+ }
15
+ const handleTXTFormat = async () => {
16
+ setIsUploadingTemplate(true);
17
+ setUploadMessage(null);
18
+ const dataRows = matrix
19
+ .slice(1)
20
+ .filter((row) => { var _a, _b; return ((_b = (_a = row[0]) === null || _a === void 0 ? void 0 : _a.toString().trim()) !== null && _b !== void 0 ? _b : "") !== ""; });
21
+ const maxColumns = matrix.reduce((max, row) => Math.max(max, row.length), 0);
22
+ const lines = dataRows.map((row) => {
23
+ const padded = Array.from({ length: maxColumns }, (_, i) => row[i] !== undefined && row[i] !== null ? String(row[i]) : "");
24
+ return padded.join("|");
25
+ });
26
+ const content = lines.join("\n");
27
+ const base64 = btoa(String.fromCharCode(...new TextEncoder().encode(content)));
28
+ try {
29
+ const response = await fetch(`http://lab.globho.com/api/v1/plains/upload?companyId=&apikey=`, {
30
+ method: "POST",
31
+ headers: {
32
+ "Content-Type": "application/json",
33
+ },
34
+ body: JSON.stringify({
35
+ content: base64,
36
+ extension: "txt",
37
+ name: `Plantilla ${initData.title}`,
38
+ type: initData.type,
39
+ }),
40
+ });
41
+ if (!response.ok) {
42
+ throw new Error("No se pudo enviar la plantilla");
43
+ }
44
+ setUploadMessage({
45
+ type: "success",
46
+ text: "La plantilla se envió correctamente.",
47
+ });
48
+ }
49
+ catch (_a) {
50
+ setUploadMessage({
51
+ type: "error",
52
+ text: "Ocurrió un error al enviar la plantilla.",
53
+ });
54
+ }
55
+ finally {
56
+ setIsUploadingTemplate(false);
57
+ setTimeout(() => {
58
+ setUploadMessage(null);
59
+ }, 3000);
60
+ }
61
+ };
62
+ return ((0, jsx_runtime_1.jsxs)("section", { className: "mt-10 bg-content1 max-h-[calc(100vh-11.5rem)] overflow-y-auto pr-1", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex mb-3 justify-between items-center", children: [(0, jsx_runtime_1.jsx)("h2", { className: "text-xl font-semibold text-default-700", children: "Validaciones" }), (0, jsx_runtime_1.jsx)(react_1.Chip, { color: validationErrors.length > 0 ? "danger" : "success", variant: "solid", className: "text-white", startContent: validationErrors.length > 0 ? ((0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { size: 20 })) : ((0, jsx_runtime_1.jsx)(tb_1.TbCheck, { size: 20 })), children: (0, jsx_runtime_1.jsxs)("p", { className: "pl-1", children: [validationErrors.length, " ", validationErrors.length === 1
63
+ ? "error encontrado"
64
+ : "errores encontrados"] }) })] }), hasBalanceDifference ? ((0, jsx_runtime_1.jsx)(react_1.Alert, { color: "danger", title: "Hay diferencias en los saldos iniciales", variant: "faded", hideIcon: true, startContent: (0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { size: 30 }), className: "mb-3" })) : null, validationErrors.length === 0 && !hasBalanceDifference ? ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(react_1.Alert, { color: "success", title: "Sin errores de validaci\u00F3n.", variant: "faded", hideIcon: true, startContent: (0, jsx_runtime_1.jsx)(tb_1.TbCircleCheck, { size: 30 }) }), (0, jsx_runtime_1.jsx)(react_1.Button, { color: "success", className: "mt-4 w-full", variant: "flat", endContent: (0, jsx_runtime_1.jsx)(tb_1.TbUpload, { size: 20 }), radius: "sm", isLoading: isUploadingTemplate, onPress: handleTXTFormat, children: "Enviar Plantilla" }), uploadMessage ? ((0, jsx_runtime_1.jsx)("p", { className: `mt-2 text-sm ${uploadMessage.type === "success"
65
+ ? "text-success-600"
66
+ : "text-danger-600"}`, children: uploadMessage.text })) : null] })) : ((0, jsx_runtime_1.jsx)(react_1.Accordion, { selectionMode: "multiple", showDivider: false, children: Object.entries(validationErrors.reduce((groups, error) => {
67
+ const key = error.columnName;
68
+ if (!groups[key])
69
+ groups[key] = [];
70
+ groups[key].push(error);
71
+ return groups;
72
+ }, {})).map(([columnName, errors]) => ((0, jsx_runtime_1.jsx)(react_1.AccordionItem, { textValue: columnName, title: (0, jsx_runtime_1.jsxs)("div", { className: "flex justify-between items-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-default-700", children: `Columna ${columnName}` }), (0, jsx_runtime_1.jsx)(react_1.Chip, { color: "danger", variant: "faded", size: "sm", startContent: (0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { size: 18 }), children: (0, jsx_runtime_1.jsx)("p", { className: "p-1 text-default-600", children: errors.length }) })] }), children: errors.map((error) => ((0, jsx_runtime_1.jsx)("button", { className: "w-full text-left mt-2", onClick: () => {
73
+ setSelectedErrorId(error.id);
74
+ onSelectError(error);
75
+ }, children: (0, jsx_runtime_1.jsx)(react_1.Alert, { color: "danger", title: error.message, hideIcon: true, startContent: (0, jsx_runtime_1.jsx)(tb_1.TbAlertCircle, { size: 30 }), classNames: {
76
+ title: `font-semibold text-default-700 ${selectedErrorId === error.id ? "text-red-600" : ""}`,
77
+ description: "text-default-500",
78
+ }, description: `Fila #${error.row}`, variant: "bordered", radius: "sm", className: `border-1 border-default-300 cursor-pointer transition-all duration-200 ease-in-out hover:scale-[0.98] ${selectedErrorId === error.id
79
+ ? "bg-red-50 border-red-300"
80
+ : " data-[state=open]:bg-red-50"}` }) }, error.id))) }, columnName))) }))] }));
81
+ };
82
+ exports.ExcelValidator = ExcelValidator;
@@ -0,0 +1 @@
1
+ export declare function ThemeSwitcher(): import("react").JSX.Element | null;
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ThemeSwitcher = ThemeSwitcher;
5
+ const jsx_runtime_1 = require("react/jsx-runtime");
6
+ const next_themes_1 = require("next-themes");
7
+ const react_1 = require("react");
8
+ function ThemeSwitcher() {
9
+ const [mounted, setMounted] = (0, react_1.useState)(false);
10
+ const { theme, setTheme } = (0, next_themes_1.useTheme)();
11
+ (0, react_1.useEffect)(() => {
12
+ setMounted(true);
13
+ }, []);
14
+ if (!mounted)
15
+ return null;
16
+ return ((0, jsx_runtime_1.jsxs)("div", { className: "bg-content1 dark:text-white", children: ["The current theme is: ", theme, (0, jsx_runtime_1.jsx)("button", { onClick: () => setTheme("light"), children: "Light Mode" }), (0, jsx_runtime_1.jsx)("button", { onClick: () => setTheme("dark"), children: "Dark Mode" })] }));
17
+ }
@@ -0,0 +1,2 @@
1
+ export { ExcelLayout } from "./components/ExcelLayout";
2
+ export type { SettingsProps } from "./types/SettingsProps";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ExcelLayout = void 0;
4
+ var ExcelLayout_1 = require("./components/ExcelLayout");
5
+ Object.defineProperty(exports, "ExcelLayout", { enumerable: true, get: function () { return ExcelLayout_1.ExcelLayout; } });
@@ -0,0 +1,7 @@
1
+ export interface SettingsProps {
2
+ apikey: string;
3
+ companyId: string;
4
+ environment: string;
5
+ title: string;
6
+ type: string;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,47 @@
1
+ import { ChangeEvent } from "react";
2
+ import { SettingsProps } from "./SettingsProps";
3
+ export type ExcelMatrix = string[][];
4
+ export type ValidationError = {
5
+ id: string;
6
+ row: number;
7
+ column: number;
8
+ columnName: string;
9
+ message: string;
10
+ value: string;
11
+ filteredRowIndex: number;
12
+ };
13
+ export type FocusTarget = {
14
+ filteredRowIndex: number;
15
+ columnIndex: number;
16
+ token: number;
17
+ } | null;
18
+ export type ExcelFileProps = {
19
+ initData: SettingsProps;
20
+ fileContent: File | undefined;
21
+ matrix: ExcelMatrix;
22
+ error: string | null;
23
+ onFileSelected: (event: ChangeEvent<HTMLInputElement>) => Promise<void>;
24
+ onReset: () => void;
25
+ validationErrors: ValidationError[];
26
+ onSelectError: (error: ValidationError) => void;
27
+ hasBalanceDifference?: boolean;
28
+ };
29
+ export type ExcelValidatorProps = {
30
+ initData: SettingsProps;
31
+ fileContent: File | undefined;
32
+ matrix: ExcelMatrix;
33
+ validationErrors: ValidationError[];
34
+ onSelectError: (error: ValidationError) => void;
35
+ hasBalanceDifference?: boolean;
36
+ };
37
+ export type ExcelPreviewProps = {
38
+ initData: SettingsProps;
39
+ fileContent: File | undefined;
40
+ matrix: ExcelMatrix;
41
+ error: string | null;
42
+ validationErrors: ValidationError[];
43
+ focusTarget: FocusTarget;
44
+ currentPage: number;
45
+ onPageChange: (page: number) => void;
46
+ isBalances?: boolean;
47
+ };
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,4 @@
1
+ import { ExcelMatrix } from "../types/excelTypes";
2
+ export declare const parseDecimal: (value: string) => number;
3
+ export declare const sumColumn: (rows: ExcelMatrix, headers: string[], colName: string) => number;
4
+ export declare const formatNumber: (n: number) => string;
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatNumber = exports.sumColumn = exports.parseDecimal = void 0;
4
+ const parseDecimal = (value) => {
5
+ const n = parseFloat(value.trim().replace(",", "."));
6
+ return isNaN(n) ? 0 : n;
7
+ };
8
+ exports.parseDecimal = parseDecimal;
9
+ const sumColumn = (rows, headers, colName) => {
10
+ const idx = headers.findIndex((h) => h.trim().toLowerCase() === colName.toLowerCase());
11
+ if (idx === -1)
12
+ return 0;
13
+ return rows.reduce((acc, row) => { var _a; return acc + (0, exports.parseDecimal)((_a = row[idx]) !== null && _a !== void 0 ? _a : ""); }, 0);
14
+ };
15
+ exports.sumColumn = sumColumn;
16
+ const formatNumber = (n) => n.toLocaleString("es-CO", {
17
+ minimumFractionDigits: 2,
18
+ maximumFractionDigits: 2,
19
+ });
20
+ exports.formatNumber = formatNumber;
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "@grafosoft/excel-validator",
3
+ "version": "1.0.0",
4
+ "description": "React component to validate Excel files against configurable JSON rules.",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md"
17
+ ],
18
+ "private": false,
19
+ "scripts": {
20
+ "clean": "rm -rf dist",
21
+ "dev": "next dev",
22
+ "start": "next start",
23
+ "build:app": "next build",
24
+ "watch": "tsc --watch",
25
+ "build": "npm run build:lib",
26
+ "build:lib": "npm run clean && tsc -p tsconfig.build.json",
27
+ "lint": "eslint",
28
+ "prepublishOnly": "npm run build:lib"
29
+ },
30
+ "dependencies": {
31
+ "@heroui/react": "2.8.10",
32
+ "framer-motion": "12.38.0",
33
+ "next": "16.2.6",
34
+ "next-themes": "^0.4.6",
35
+ "react": "19.2.4",
36
+ "react-dom": "19.2.4",
37
+ "react-icons": "5.0.1",
38
+ "xlsx": "^0.18.5"
39
+ },
40
+ "peerDependencies": {
41
+ "next": ">=16",
42
+ "react": ">=19",
43
+ "react-dom": ">=19"
44
+ },
45
+ "devDependencies": {
46
+ "@tailwindcss/postcss": "^4",
47
+ "@types/node": "^20",
48
+ "@types/react": "^19",
49
+ "@types/react-dom": "^19",
50
+ "eslint": "^9",
51
+ "eslint-config-next": "16.2.6",
52
+ "tailwindcss": "^4",
53
+ "typescript": "^5"
54
+ }
55
+ }