@zauru-sdk/components 2.0.2 → 2.0.4

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/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ## [2.0.4](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.3...v2.0.4) (2024-10-03)
7
+
8
+ **Note:** Version bump only for package @zauru-sdk/components
9
+
10
+
11
+
12
+
13
+
14
+ ## [2.0.3](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.2...v2.0.3) (2024-10-03)
15
+
16
+ **Note:** Version bump only for package @zauru-sdk/components
17
+
18
+
19
+
20
+
21
+
6
22
  ## [2.0.2](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.1...v2.0.2) (2024-10-03)
7
23
 
8
24
  **Note:** Version bump only for package @zauru-sdk/components
@@ -1,12 +1,16 @@
1
1
  import React from "react";
2
2
  import { GenericDynamicTableColumn, RowDataType, SelectFieldOption } from "@zauru-sdk/types";
3
+ export type FooterColumnConfig = {
4
+ content: React.ReactNode;
5
+ className?: string;
6
+ };
3
7
  type Props = {
4
8
  name?: string;
5
9
  className?: string;
6
10
  columns: GenericDynamicTableColumn[];
7
11
  onChange?: (tableState?: any[]) => void;
8
12
  defaultValue?: RowDataType[];
9
- footerRow?: RowDataType;
13
+ footerRow?: FooterColumnConfig[];
10
14
  thCSSProperties?: React.CSSProperties;
11
15
  thElementsClassName?: string;
12
16
  editable?: boolean;
@@ -16,6 +20,58 @@ type Props = {
16
20
  defaultItemsPerPage?: number;
17
21
  itemsPerPageOptions?: number[];
18
22
  withoutBg?: boolean;
23
+ orientation?: "horizontal" | "vertical";
24
+ maxRows?: number;
19
25
  };
26
+ /**
27
+ *
28
+ * @param props
29
+ * @returns
30
+ *
31
+ * @example
32
+ *<GenericDynamicTable
33
+ name="invoice_details"
34
+ withoutBg
35
+ editable={!show}
36
+ defaultValue={
37
+ invoiceDetailsDefaultValue ?? [{ id: crypto.randomUUID() }]
38
+ }
39
+ columns={[
40
+ {
41
+ label: "Producto",
42
+ name: "item_id",
43
+ type: "selectField",
44
+ options: productOptions,
45
+ disabled: show,
46
+ onChange: (rowData, value, setTableValue) => {
47
+ const price = getProductPrice(value);
48
+ setTableValue("price", price);
49
+ },
50
+ headerClassName: "text-center font-bold",
51
+ cellClassName: "text-center",
52
+ },
53
+ {
54
+ label: "Cantidad",
55
+ name: "quantity",
56
+ type: "textField",
57
+ textFieldType: "number",
58
+ disabled: show,
59
+ onChange: (rowData, value, setTableValue) => {
60
+ const price = rowData["price"] ?? 0;
61
+ const quantity = Number(value) || 0;
62
+ setTableValue("total", price * quantity);
63
+ },
64
+ headerClassName: "text-right font-semibold",
65
+ cellClassName: "text-right",
66
+ }
67
+ ]}
68
+ footerRow={[
69
+ { content: "Total", className: "text-left font-bold" },
70
+ { content: calculateTotal(), className: "text-center" },
71
+ { content: "", className: "text-center" }
72
+ ]}
73
+ maxRows={2}
74
+ />
75
+ */
20
76
  export declare const GenericDynamicTable: (props: Props) => import("react/jsx-runtime").JSX.Element;
21
77
  export {};
@@ -41,4 +41,5 @@ export type EntityProps = {
41
41
  name?: string;
42
42
  color: ColorInterface;
43
43
  options: any[];
44
+ specialColor?: ColorInterface;
44
45
  };
@@ -0,0 +1,11 @@
1
+ import React from "react";
2
+ interface LoadingItem {
3
+ name: string;
4
+ loading: boolean;
5
+ }
6
+ interface LoadingWindowProps {
7
+ loadingItems?: LoadingItem[];
8
+ description?: string;
9
+ }
10
+ export declare const LoadingWindow: React.FC<LoadingWindowProps>;
11
+ export default LoadingWindow;
@@ -1 +1,2 @@
1
1
  export * from "./LoadingInputSkeleton.js";
2
+ export * from "./LoadingWindow.js";
@@ -5,18 +5,68 @@ import { TextField } from "../Form/TextField/index.js";
5
5
  import { CheckBox } from "../Form/Checkbox/index.js";
6
6
  import { createModal } from "../Modal/index.js";
7
7
  import { Button } from "../Buttons/index.js";
8
- import { useAppSelector } from "@zauru-sdk/redux";
9
8
  import { generateClientUUID } from "@zauru-sdk/common";
10
9
  import { LoadingInputSkeleton } from "../Skeletons/index.js";
11
10
  import { WithTooltip } from "../WithTooltip/index.js";
12
11
  import { TrashSvg } from "@zauru-sdk/icons";
13
- const GenericDynamicTableErrorComponent = ({ name, formName, }) => {
14
- const { formValidations } = useAppSelector((state) => state.formValidation);
15
- const error = formValidations[formName ?? "-1"]?.[name ?? "-1"];
16
- return error ? (_jsxs("p", { className: `mt-2 text-sm text-red-600 dark:text-red-500`, children: [_jsx("span", { className: "font-medium", children: "Oops!" }), " ", error] })) : (_jsx(_Fragment, {}));
12
+ import { useFormContext } from "react-hook-form";
13
+ const GenericDynamicTableErrorComponent = ({ name }) => {
14
+ const { formState: { errors }, } = useFormContext() || { formState: {} }; // Obtener el contexto solo si existe
15
+ const error = errors ? errors[name ?? "-1"] : undefined;
16
+ return error ? (_jsxs("p", { className: `mt-2 text-sm text-red-600 dark:text-red-500`, children: [_jsx("span", { className: "font-medium", children: "Oops!" }), " ", error?.message?.toString()] })) : (_jsx(_Fragment, {}));
17
17
  };
18
+ /**
19
+ *
20
+ * @param props
21
+ * @returns
22
+ *
23
+ * @example
24
+ *<GenericDynamicTable
25
+ name="invoice_details"
26
+ withoutBg
27
+ editable={!show}
28
+ defaultValue={
29
+ invoiceDetailsDefaultValue ?? [{ id: crypto.randomUUID() }]
30
+ }
31
+ columns={[
32
+ {
33
+ label: "Producto",
34
+ name: "item_id",
35
+ type: "selectField",
36
+ options: productOptions,
37
+ disabled: show,
38
+ onChange: (rowData, value, setTableValue) => {
39
+ const price = getProductPrice(value);
40
+ setTableValue("price", price);
41
+ },
42
+ headerClassName: "text-center font-bold",
43
+ cellClassName: "text-center",
44
+ },
45
+ {
46
+ label: "Cantidad",
47
+ name: "quantity",
48
+ type: "textField",
49
+ textFieldType: "number",
50
+ disabled: show,
51
+ onChange: (rowData, value, setTableValue) => {
52
+ const price = rowData["price"] ?? 0;
53
+ const quantity = Number(value) || 0;
54
+ setTableValue("total", price * quantity);
55
+ },
56
+ headerClassName: "text-right font-semibold",
57
+ cellClassName: "text-right",
58
+ }
59
+ ]}
60
+ footerRow={[
61
+ { content: "Total", className: "text-left font-bold" },
62
+ { content: calculateTotal(), className: "text-center" },
63
+ { content: "", className: "text-center" }
64
+ ]}
65
+ maxRows={2}
66
+ />
67
+ */
18
68
  export const GenericDynamicTable = (props) => {
19
- const { columns, onChange, className, footerRow, defaultValue = [], thCSSProperties, thElementsClassName = "", editable = true, searcheables = [], loading = false, paginated = true, defaultItemsPerPage = 10, itemsPerPageOptions = [10, 50, 100], name, withoutBg = false, } = props;
69
+ const { columns, onChange, className, footerRow, defaultValue = [], thCSSProperties, thElementsClassName = "", editable = true, searcheables = [], loading = false, paginated = true, defaultItemsPerPage = 10, itemsPerPageOptions = [10, 50, 100], name, withoutBg = false, orientation = "horizontal", maxRows, } = props;
20
70
  const [tableData, setTableData] = useState(defaultValue);
21
71
  const [deletedData, setDeletedData] = useState([]);
22
72
  const [search, setSearch] = useState("");
@@ -38,6 +88,9 @@ export const GenericDynamicTable = (props) => {
38
88
  return Math.ceil(filteredTableData.length / itemsPerPage);
39
89
  };
40
90
  const addRow = () => {
91
+ if (maxRows && tableData.length >= maxRows) {
92
+ return;
93
+ }
41
94
  const defs = {};
42
95
  columns.forEach((x) => {
43
96
  defs[`${x.name}`] =
@@ -64,76 +117,76 @@ export const GenericDynamicTable = (props) => {
64
117
  setTableData((prevData) => prevData?.filter((x) => x.id !== rowId));
65
118
  };
66
119
  const handleChange = (name, value, rowId) => {
67
- // Encontrar el índice de la fila que está cambiando
68
- const rowIndex = tableData.findIndex((x) => x.id === rowId);
69
- // Crear una copia del objeto en esa fila
70
- const updatedRow = { ...tableData[rowIndex], [name]: value };
71
- // Copiar todo el array
72
- const updatedData = [...tableData];
73
- // Reemplazar el objeto en la fila que cambió
74
- updatedData[rowIndex] = updatedRow;
75
- // Actualizar el estado con el nuevo array
76
- setTableData(updatedData);
77
- onChange && onChange(updatedData);
120
+ setTableData((prevData) => {
121
+ const updatedData = prevData.map((row) => {
122
+ if (row.id === rowId) {
123
+ return { ...row, [name]: value };
124
+ }
125
+ return row;
126
+ });
127
+ onChange && onChange(updatedData);
128
+ return updatedData;
129
+ });
130
+ };
131
+ const renderHeader = () => {
132
+ if (orientation === "horizontal") {
133
+ return (_jsxs("tr", { style: { ...thCSSProperties }, children: [columns.map((column, index) => {
134
+ const ancho = column.width ?? (editable ? 94 : 100) / (columns.length ?? 1);
135
+ return (_jsx("th", { className: `text-left align-middle p-2 ${thElementsClassName} ${column.headerClassName || ""}`, style: { width: `${ancho}%` }, children: column.label }, index));
136
+ }), editable && _jsx("th", { style: { width: "4%" } })] }));
137
+ }
138
+ else {
139
+ return null;
140
+ }
78
141
  };
79
- const renderHeader = () => (_jsxs("tr", { style: { ...thCSSProperties }, children: [columns.map((column, index) => {
80
- const ancho = column.width ?? (editable ? 94 : 100) / (columns.length ?? 1);
81
- return (_jsx("th", { className: `text-left align-middle p-2 ${thElementsClassName}`, style: { width: `${ancho}%` }, children: column.label }, index));
82
- }), editable && _jsx("th", { style: { width: "4%" } })] }));
83
142
  const renderRow = (rowData, index) => {
84
- return (_jsxs("tr", { className: index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : "", children: [columns.map((column) => {
85
- if (loading) {
86
- return (_jsx("td", { className: "align-middle p-1", children: _jsx(LoadingInputSkeleton, {}) }, `${rowData.id}-${column.name}`));
87
- }
88
- const tempVal = rowData[column.name];
89
- const defaultVal = column.type === "selectField"
90
- ? column.options?.find((x) => x.value === tempVal)
91
- : tempVal;
92
- if (column.type === "label") {
93
- return (_jsx("td", { className: "align-middle p-1", children: _jsx("div", { children: defaultVal }) }, `${rowData.id}-${column.name}`));
94
- }
95
- const FieldComponent = column.type === "textField"
96
- ? TextField
97
- : column.type === "checkbox"
98
- ? CheckBox
99
- : SelectField;
100
- const setTableValue = (columnName, newValue) => {
101
- setTableData((prevState) => {
102
- // Encontrar el índice de la fila que está cambiando
103
- const rowIndex = prevState.findIndex((x) => x.id === rowData.id);
104
- // Crear una copia del objeto en esa fila
105
- const updatedRow = {
106
- ...prevState[rowIndex],
107
- [columnName]: newValue,
108
- };
109
- // Copiar todo el array
110
- const updatedData = [...prevState];
111
- // Reemplazar el objeto en la fila que cambió
112
- updatedData[rowIndex] = updatedRow;
113
- return updatedData;
114
- });
115
- };
116
- return (_jsx("td", { className: "align-middle p-1", children: column.loadingOptions ? (_jsx(LoadingInputSkeleton, {})) : (_jsx(FieldComponent, {
117
- //name={column.name}
118
- type: column.textFieldType, integer: !!column.integer, disabled: column.disabled, isClearable: true, onChange: (value) => {
119
- const sendValue = value?.value ?? value;
120
- handleChange(column.name, sendValue, rowData.id);
121
- column.onChange &&
122
- column.onChange(rowData, sendValue, setTableValue);
123
- }, defaultValue: defaultVal, options: column.options ?? [] }, `${rowData.id}-${column.name}`)) }, `${rowData.id}-${column.name}`));
124
- }), editable && (_jsx("td", { className: "align-middle w-16", children: _jsx(WithTooltip, { text: "Eliminar", children: _jsx("button", { className: "bg-red-500 hover:bg-red-600 font-bold py-1 px-2 rounded ml-2", onClick: (event) => {
125
- event.preventDefault();
126
- event.stopPropagation();
127
- createModal({
128
- title: "¿Está seguro que quiere eliminar este registro?",
129
- description: "Una vez eliminada la información no podrá ser recuperada.",
130
- }).then((response) => {
131
- if (response === "OK") {
132
- removeRow(rowData.id);
133
- }
134
- });
135
- }, type: "button", children: _jsx(TrashSvg, {}) }) }) }))] }, rowData.id));
143
+ if (orientation === "horizontal") {
144
+ return (_jsxs("tr", { className: index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : "", children: [columns.map((column) => renderCell(rowData, column)), editable && renderDeleteButton(rowData)] }, rowData.id));
145
+ }
146
+ else {
147
+ return columns.map((column) => (_jsxs("tr", { className: index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : "", children: [_jsx("th", { className: `text-left align-middle p-2 ${thElementsClassName} ${column.headerClassName || ""}`, children: column.label }), renderCell(rowData, column), editable &&
148
+ column === columns[columns.length - 1] &&
149
+ renderDeleteButton(rowData)] }, `${rowData.id}-${column.name}`)));
150
+ }
136
151
  };
152
+ const renderCell = (rowData, column) => {
153
+ if (loading) {
154
+ return (_jsx("td", { className: `align-middle p-1 ${column.cellClassName || ""}`, children: _jsx(LoadingInputSkeleton, {}) }, `${rowData.id}-${column.name}`));
155
+ }
156
+ const tempVal = rowData[column.name];
157
+ const defaultVal = column.type === "selectField"
158
+ ? column.options?.find((x) => x.value === tempVal)
159
+ : tempVal;
160
+ if (column.type === "label") {
161
+ return (_jsx("td", { className: `align-middle p-1 ${column.cellClassName || ""}`, children: _jsx("div", { children: defaultVal }) }, `${rowData.id}-${column.name}`));
162
+ }
163
+ const FieldComponent = column.type === "textField"
164
+ ? TextField
165
+ : column.type === "checkbox"
166
+ ? CheckBox
167
+ : SelectField;
168
+ const setTableValue = (columnName, newValue) => {
169
+ handleChange(columnName, newValue, rowData.id);
170
+ };
171
+ return (_jsx("td", { className: `align-middle p-1 ${column.cellClassName || ""}`, children: column.loadingOptions ? (_jsx(LoadingInputSkeleton, {})) : (_jsx(FieldComponent, { name: `${rowData.id}-${column.name}`, type: column.textFieldType, integer: !!column.integer, disabled: column.disabled, isClearable: true, onChange: (value) => {
172
+ const sendValue = value?.value ?? value;
173
+ handleChange(column.name, sendValue, rowData.id);
174
+ column.onChange &&
175
+ column.onChange(rowData, sendValue, setTableValue);
176
+ }, defaultValue: defaultVal, options: column.options ?? [] }, `${rowData.id}-${column.name}`)) }, `${rowData.id}-${column.name}`));
177
+ };
178
+ const renderDeleteButton = (rowData) => (_jsx("td", { className: "align-middle w-16", children: _jsx(WithTooltip, { text: "Eliminar", children: _jsx("button", { className: "bg-red-500 hover:bg-red-600 font-bold py-1 px-2 rounded ml-2", onClick: (event) => {
179
+ event.preventDefault();
180
+ event.stopPropagation();
181
+ createModal({
182
+ title: "¿Está seguro que quiere eliminar este registro?",
183
+ description: "Una vez eliminada la información no podrá ser recuperada.",
184
+ }).then((response) => {
185
+ if (response === "OK") {
186
+ removeRow(rowData.id);
187
+ }
188
+ });
189
+ }, type: "button", children: _jsx(TrashSvg, {}) }) }) }));
137
190
  const renderRows = () => {
138
191
  let mapeable = filteredTableData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
139
192
  if (loading) {
@@ -183,13 +236,11 @@ export const GenericDynamicTable = (props) => {
183
236
  };
184
237
  return (_jsxs(_Fragment, { children: [name && (_jsxs(_Fragment, { children: [_jsx(GenericDynamicTableErrorComponent, { name: name }), _jsx("input", { name: name, type: "hidden", value: JSON.stringify(tableData), hidden: true }), _jsx("input", { name: `deleted_${name}`, type: "hidden", value: JSON.stringify(deletedData), hidden: true })] })), _jsxs("div", { className: `${className}`, children: [searcheables.length > 0 && (_jsx("div", { children: _jsx(TextField, { className: "mb-2", name: "search", title: `Buscar por: ${searcheables
185
238
  .map((x) => x.label)
186
- .join(", ")}`, onChange: handleChangeSearch, disabled: loading }) })), _jsxs("table", { className: "w-full", children: [_jsx("thead", { children: renderHeader() }), _jsx("tbody", { children: renderRows() }), footerRow && !editable ? (_jsx("tfoot", { className: "border-t-2 border-black", children: _jsx("tr", { children: Object.keys(footerRow ?? {})?.map((x, indx) => {
187
- return (_jsx("td", { className: "align-middle", children: footerRow[x] }, indx));
188
- }) }) })) : editable ? (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { className: "align-middle", children: _jsx("button", { className: "bg-blue-500 hover:bg-blue-600 font-bold py-2 px-4 rounded", onClick: (event) => {
239
+ .join(", ")}`, onChange: handleChangeSearch, disabled: loading }) })), _jsxs("table", { className: "w-full", children: [orientation === "horizontal" && _jsx("thead", { children: renderHeader() }), _jsx("tbody", { children: renderRows() }), editable && (_jsx("tfoot", { children: _jsx("tr", { children: _jsx("td", { colSpan: orientation === "horizontal" ? columns.length + 1 : 2, className: "align-middle", children: (!maxRows || tableData.length < maxRows) && (_jsx("button", { className: "bg-blue-500 hover:bg-blue-600 font-bold py-2 px-4 rounded", onClick: (event) => {
189
240
  event.preventDefault();
190
241
  event.stopPropagation();
191
242
  addRow();
192
- }, type: "button", children: "+" }) }) }) })) : (_jsx(_Fragment, {}))] }), paginated && totalPages() > 1 && (_jsxs("div", { className: "flex justify-between items-center mt-4", children: [_jsxs("div", { className: "flex items-center", children: [_jsx(Button, { type: "button", disabled: currentPage === 1, onClickSave: () => setCurrentPage((old) => Math.max(old - 1, 1)), children: "Anterior" }), _jsx("span", { className: "mx-2", children: `Página ${currentPage} de ${totalPages()}` }), _jsx(Button, { type: "button", disabled: currentPage === totalPages(), onClickSave: () => setCurrentPage((old) => Math.min(old + 1, totalPages())), children: "Siguiente" })] }), _jsx("div", { children: _jsx("select", { value: itemsPerPage, onChange: (e) => {
243
+ }, type: "button", children: "+" })) }) }) })), footerRow && (_jsx("tfoot", { className: "border-t-2 border-black", children: _jsx("tr", { children: footerRow.map((column, index) => (_jsx("td", { colSpan: orientation === "vertical" ? 2 : 1, className: `align-middle ${column.className || ""}`, children: column.content }, index))) }) }))] }), paginated && totalPages() > 1 && (_jsxs("div", { className: "flex justify-between items-center mt-4", children: [_jsxs("div", { className: "flex items-center", children: [_jsx(Button, { type: "button", disabled: currentPage === 1, onClickSave: () => setCurrentPage((old) => Math.max(old - 1, 1)), children: "Anterior" }), _jsx("span", { className: "mx-2", children: `Página ${currentPage} de ${totalPages()}` }), _jsx(Button, { type: "button", disabled: currentPage === totalPages(), onClickSave: () => setCurrentPage((old) => Math.min(old + 1, totalPages())), children: "Siguiente" })] }), _jsx("div", { children: _jsx("select", { value: itemsPerPage, onChange: (e) => {
193
244
  setItemsPerPage(Number(e.target.value));
194
245
  setCurrentPage(1); // resetear la página al cambiar los elementos por página
195
246
  }, children: itemsPerPageOptions.map((option) => (_jsxs("option", { value: option, children: [option, " elementos por p\u00E1gina"] }, option))) }) })] }))] })] }));
@@ -112,6 +112,11 @@ export const SelectField = (props) => {
112
112
  handleOptionClick(filteredOptions[0]);
113
113
  }
114
114
  }
115
+ else if (isTabPressed) {
116
+ if (highlightedIndex >= 0) {
117
+ handleOptionClick(filteredOptions[highlightedIndex]);
118
+ }
119
+ }
115
120
  setIsTabPressed(false);
116
121
  setIsEnterPressed(false);
117
122
  setIsSearching(false);
@@ -3,7 +3,7 @@ import { IdeaIconSVG } from "@zauru-sdk/icons";
3
3
  import { useEffect, useState } from "react";
4
4
  import { useFormContext } from "react-hook-form";
5
5
  export const TextField = (props) => {
6
- const { id, name, defaultValue = "", hidden, type = "text", onChange, onKeyDown, disabled = false, readOnly = false, min, integer = false, stopChangeEvents, style, title, helpText, className, hint, required, } = props;
6
+ const { id, name, defaultValue = "", hidden, type = "text", onChange, onKeyDown, disabled = false, readOnly = false, min, integer = false, stopChangeEvents, style, title, helpText, className = "", hint, required, } = props;
7
7
  const [showTooltip, setShowTooltip] = useState(false);
8
8
  const [value, setValue] = useState(defaultValue);
9
9
  const { register: tempRegister, formState: { errors }, setValue: setOnFormValue, } = useFormContext() || { formState: {} }; // Obtener el contexto solo si existe
@@ -3,11 +3,11 @@ import React, { useState, useEffect } from "react";
3
3
  import { DropDownArrowSvgIcon, LogoutDropDownSvgIcon, MenuAlt4Svg, OpcionButtonSvgIcon, } from "@zauru-sdk/icons";
4
4
  import { COLORS } from "./NavBar.utils.js";
5
5
  import { Link, useNavigate } from "@remix-run/react";
6
- const OptionsDropDownButton = ({ color, options, name }) => {
6
+ const OptionsDropDownButton = ({ color, options, name, specialColor, }) => {
7
7
  const [showOptionsMenu, setShowOptionsMenu] = useState(true);
8
- return (_jsx("div", { className: "nav-item ml-auto", children: _jsx("div", { className: "flex justify-center", children: _jsxs("div", { className: "relative inline-block", children: [_jsxs("button", { onClick: () => setShowOptionsMenu(!showOptionsMenu), className: `relative flex items-center p-2 text-xs text-white ${color.bg700} active:${color.bg900} border border-transparent rounded-full uppercase focus:ring-opacity-40 focus:outline-none`, children: [name ?? _jsx(OpcionButtonSvgIcon, {}), _jsx(DropDownArrowSvgIcon, {})] }), _jsx("div", { className: "absolute right-0 z-20 w-56 py-2 mt-2 overflow-hidden bg-white rounded-md shadow-xl dark:bg-gray-800", hidden: showOptionsMenu, onMouseLeave: () => setShowOptionsMenu(true), children: options.map((option, index) => (_jsx(React.Fragment, { children: option }, index))) })] }) }) }));
8
+ return (_jsxs("div", { className: `${specialColor ? specialColor.bg700 : color.bg700} container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150`, children: [_jsxs("button", { onClick: () => setShowOptionsMenu(!showOptionsMenu), className: `relative flex items-center p-2 text-xs text-white ${color.bg700} active:${color.bg900} border border-transparent rounded-full uppercase focus:ring-opacity-40 focus:outline-none`, children: [name ?? _jsx(OpcionButtonSvgIcon, {}), _jsx(DropDownArrowSvgIcon, {})] }), _jsx("div", { className: "absolute right-0 z-20 w-56 py-2 mt-2 overflow-hidden bg-white rounded-md shadow-xl dark:bg-gray-800", hidden: showOptionsMenu, onMouseLeave: () => setShowOptionsMenu(true), children: options.map((option, index) => (_jsx(React.Fragment, { children: option }, index))) })] }));
9
9
  };
10
- const NavItem = ({ name, link, icon, color, specialColor, childrens = [], }) => (_jsx("li", { className: "nav-item", children: childrens.length > 0 ? (_jsx(OptionsDropDownButton, { name: name, color: color, options: childrens.map((x, index) => (_jsx(Link, { to: x.link, className: "hover:bg-red-100", children: x.name }, index))) })) : (_jsx("div", { className: `${specialColor ? specialColor.bg700 : color.bg700} container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150 `, children: _jsx(Link, { className: "px-3 flex items-center text-xs leading-snug text-white uppercase hover:opacity-75", to: link, prefetch: "none", children: _jsxs("div", { className: "mx-auto pt-2", children: [icon, _jsx("span", { children: name })] }) }) })) }));
10
+ const NavItem = ({ name, link, icon, color, specialColor, childrens = [], }) => (_jsx("li", { className: "nav-item", children: childrens.length > 0 ? (_jsx(OptionsDropDownButton, { name: name, color: color, specialColor: specialColor, options: childrens.map((x, index) => (_jsx(Link, { to: x.link, className: `block px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-red-100 dark:hover:bg-gray-700 dark:hover:text-white`, children: x.name }, index))) })) : (_jsx("div", { className: `${specialColor ? specialColor.bg700 : color.bg700} container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150`, children: _jsx(Link, { className: "px-3 flex items-center text-xs leading-snug text-white uppercase hover:opacity-75", to: link, prefetch: "none", children: _jsxs("div", { className: "mx-auto pt-2", children: [icon, _jsx("span", { children: name })] }) }) })) }));
11
11
  export const NavBar = ({ title, loggedIn, items, selectedColor, version, }) => {
12
12
  const color = COLORS[selectedColor];
13
13
  const [NavBarOpen, setNavBarOpen] = useState(false);
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { motion } from "framer-motion";
3
+ export const LoadingWindow = ({ loadingItems, description = "Por favor, espere mientras se carga la página.", }) => {
4
+ return (_jsx("div", { className: "min-h-screen bg-gray-100 flex flex-col justify-center items-center", children: _jsxs(motion.div, { initial: { opacity: 0, y: -20 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.5 }, className: "text-center flex flex-col items-center", children: [_jsx("img", { src: "/logo.png", alt: "Zauru Logo", className: "mb-8 h-20" }), _jsx("h1", { className: "text-3xl font-bold text-gray-800 mb-4", children: "Cargando..." }), _jsx("p", { className: "text-gray-600 mb-4", children: description }), loadingItems && (_jsx("ul", { className: "text-left mb-4", children: loadingItems.map((item, index) => (_jsxs("li", { className: "flex items-center mb-2", children: [_jsx("svg", { className: `${item.loading ? "" : "hidden"} w-5 h-5 text-blue-500 mr-2 animate-spin loading-spinner`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" }) }), _jsx("svg", { className: `${item.loading ? "hidden" : ""} w-5 h-5 text-green-500 mr-2 check-mark`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M5 13l4 4L19 7" }) }), _jsx("span", { children: item.name })] }, index))) })), _jsx(motion.div, { animate: { rotate: 360 }, transition: { duration: 2, repeat: Infinity, ease: "linear" }, className: "mt-8", children: _jsx("svg", { className: "w-12 h-12 text-blue-500", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx(motion.path, { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15", animate: { rotate: 360 }, transition: { duration: 2, repeat: Infinity, ease: "linear" } }) }) })] }) }));
5
+ };
6
+ export default LoadingWindow;
@@ -1 +1,2 @@
1
1
  export * from "./LoadingInputSkeleton.js";
2
+ export * from "./LoadingWindow.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/components",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "description": "Componentes reutilizables en las WebApps de Zauru.",
5
5
  "main": "./dist/esm/index.js",
6
6
  "module": "./dist/esm/index.js",
@@ -33,11 +33,11 @@
33
33
  "@hookform/resolvers": "^3.9.0",
34
34
  "@reduxjs/toolkit": "^2.2.1",
35
35
  "@remix-run/react": "^2.8.1",
36
- "@zauru-sdk/common": "^2.0.2",
37
- "@zauru-sdk/hooks": "^2.0.2",
36
+ "@zauru-sdk/common": "^2.0.4",
37
+ "@zauru-sdk/hooks": "^2.0.4",
38
38
  "@zauru-sdk/icons": "^2.0.0",
39
- "@zauru-sdk/types": "^2.0.0",
40
- "@zauru-sdk/utils": "^2.0.2",
39
+ "@zauru-sdk/types": "^2.0.4",
40
+ "@zauru-sdk/utils": "^2.0.4",
41
41
  "framer-motion": "^11.7.0",
42
42
  "jsonwebtoken": "^9.0.2",
43
43
  "react": "^18.2.0",
@@ -49,5 +49,5 @@
49
49
  "styled-components": "^5.3.5",
50
50
  "zod": "^3.23.8"
51
51
  },
52
- "gitHead": "5caadc022d1be3bbcaf6e73de37524a6b06c806c"
52
+ "gitHead": "78f1e67e363a88974b8b74b5c0f0637fa3782ef0"
53
53
  }
@@ -9,11 +9,16 @@ import {
9
9
  RowDataType,
10
10
  SelectFieldOption,
11
11
  } from "@zauru-sdk/types";
12
- import { useAppSelector } from "@zauru-sdk/redux";
13
12
  import { generateClientUUID } from "@zauru-sdk/common";
14
13
  import { LoadingInputSkeleton } from "../Skeletons/index.js";
15
14
  import { WithTooltip } from "../WithTooltip/index.js";
16
15
  import { TrashSvg } from "@zauru-sdk/icons";
16
+ import { useFormContext } from "react-hook-form";
17
+
18
+ export type FooterColumnConfig = {
19
+ content: React.ReactNode;
20
+ className?: string;
21
+ };
17
22
 
18
23
  type Props = {
19
24
  name?: string;
@@ -21,7 +26,7 @@ type Props = {
21
26
  columns: GenericDynamicTableColumn[];
22
27
  onChange?: (tableState?: any[]) => void;
23
28
  defaultValue?: RowDataType[];
24
- footerRow?: RowDataType;
29
+ footerRow?: FooterColumnConfig[];
25
30
  thCSSProperties?: React.CSSProperties;
26
31
  thElementsClassName?: string;
27
32
  editable?: boolean;
@@ -31,27 +36,76 @@ type Props = {
31
36
  defaultItemsPerPage?: number;
32
37
  itemsPerPageOptions?: number[];
33
38
  withoutBg?: boolean;
39
+ orientation?: "horizontal" | "vertical";
40
+ maxRows?: number;
34
41
  };
35
42
 
36
- const GenericDynamicTableErrorComponent = ({
37
- name,
38
- formName,
39
- }: {
40
- name: string;
41
- formName?: string;
42
- }) => {
43
- const { formValidations } = useAppSelector((state) => state.formValidation);
44
- const error = formValidations[formName ?? "-1"]?.[name ?? "-1"];
43
+ const GenericDynamicTableErrorComponent = ({ name }: { name: string }) => {
44
+ const {
45
+ formState: { errors },
46
+ } = useFormContext() || { formState: {} }; // Obtener el contexto solo si existe
47
+ const error = errors ? errors[name ?? "-1"] : undefined;
45
48
 
46
49
  return error ? (
47
50
  <p className={`mt-2 text-sm text-red-600 dark:text-red-500`}>
48
- <span className="font-medium">Oops!</span> {error}
51
+ <span className="font-medium">Oops!</span> {error?.message?.toString()}
49
52
  </p>
50
53
  ) : (
51
54
  <></>
52
55
  );
53
56
  };
54
57
 
58
+ /**
59
+ *
60
+ * @param props
61
+ * @returns
62
+ *
63
+ * @example
64
+ *<GenericDynamicTable
65
+ name="invoice_details"
66
+ withoutBg
67
+ editable={!show}
68
+ defaultValue={
69
+ invoiceDetailsDefaultValue ?? [{ id: crypto.randomUUID() }]
70
+ }
71
+ columns={[
72
+ {
73
+ label: "Producto",
74
+ name: "item_id",
75
+ type: "selectField",
76
+ options: productOptions,
77
+ disabled: show,
78
+ onChange: (rowData, value, setTableValue) => {
79
+ const price = getProductPrice(value);
80
+ setTableValue("price", price);
81
+ },
82
+ headerClassName: "text-center font-bold",
83
+ cellClassName: "text-center",
84
+ },
85
+ {
86
+ label: "Cantidad",
87
+ name: "quantity",
88
+ type: "textField",
89
+ textFieldType: "number",
90
+ disabled: show,
91
+ onChange: (rowData, value, setTableValue) => {
92
+ const price = rowData["price"] ?? 0;
93
+ const quantity = Number(value) || 0;
94
+ setTableValue("total", price * quantity);
95
+ },
96
+ headerClassName: "text-right font-semibold",
97
+ cellClassName: "text-right",
98
+ }
99
+ ]}
100
+ footerRow={[
101
+ { content: "Total", className: "text-left font-bold" },
102
+ { content: calculateTotal(), className: "text-center" },
103
+ { content: "", className: "text-center" }
104
+ ]}
105
+ maxRows={2}
106
+ />
107
+ */
108
+
55
109
  export const GenericDynamicTable = (props: Props) => {
56
110
  const {
57
111
  columns,
@@ -69,6 +123,8 @@ export const GenericDynamicTable = (props: Props) => {
69
123
  itemsPerPageOptions = [10, 50, 100],
70
124
  name,
71
125
  withoutBg = false,
126
+ orientation = "horizontal",
127
+ maxRows,
72
128
  } = props;
73
129
 
74
130
  const [tableData, setTableData] = useState<RowDataType[]>(defaultValue);
@@ -98,6 +154,9 @@ export const GenericDynamicTable = (props: Props) => {
98
154
  };
99
155
 
100
156
  const addRow = () => {
157
+ if (maxRows && tableData.length >= maxRows) {
158
+ return;
159
+ }
101
160
  const defs: { [key: string]: any } = {};
102
161
  columns.forEach((x) => {
103
162
  defs[`${x.name}`] =
@@ -126,161 +185,176 @@ export const GenericDynamicTable = (props: Props) => {
126
185
  };
127
186
 
128
187
  const handleChange = (name: string, value: any, rowId: string) => {
129
- // Encontrar el índice de la fila que está cambiando
130
- const rowIndex = tableData.findIndex((x) => x.id === rowId);
131
-
132
- // Crear una copia del objeto en esa fila
133
- const updatedRow = { ...tableData[rowIndex], [name]: value };
134
-
135
- // Copiar todo el array
136
- const updatedData = [...tableData];
137
-
138
- // Reemplazar el objeto en la fila que cambió
139
- updatedData[rowIndex] = updatedRow;
188
+ setTableData((prevData) => {
189
+ const updatedData = prevData.map((row) => {
190
+ if (row.id === rowId) {
191
+ return { ...row, [name]: value };
192
+ }
193
+ return row;
194
+ });
195
+ onChange && onChange(updatedData);
196
+ return updatedData;
197
+ });
198
+ };
140
199
 
141
- // Actualizar el estado con el nuevo array
142
- setTableData(updatedData);
143
- onChange && onChange(updatedData);
200
+ const renderHeader = () => {
201
+ if (orientation === "horizontal") {
202
+ return (
203
+ <tr style={{ ...thCSSProperties }}>
204
+ {columns.map((column, index) => {
205
+ const ancho =
206
+ column.width ?? (editable ? 94 : 100) / (columns.length ?? 1);
207
+ return (
208
+ <th
209
+ key={index}
210
+ className={`text-left align-middle p-2 ${thElementsClassName} ${
211
+ column.headerClassName || ""
212
+ }`}
213
+ style={{ width: `${ancho}%` }}
214
+ >
215
+ {column.label}
216
+ </th>
217
+ );
218
+ })}
219
+ {editable && <th style={{ width: "4%" }}></th>}
220
+ </tr>
221
+ );
222
+ } else {
223
+ return null;
224
+ }
144
225
  };
145
226
 
146
- const renderHeader = () => (
147
- <tr style={{ ...thCSSProperties }}>
148
- {columns.map((column, index) => {
149
- const ancho =
150
- column.width ?? (editable ? 94 : 100) / (columns.length ?? 1);
151
- return (
227
+ const renderRow = (rowData: RowDataType, index: number) => {
228
+ if (orientation === "horizontal") {
229
+ return (
230
+ <tr
231
+ key={rowData.id}
232
+ className={index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : ""}
233
+ >
234
+ {columns.map((column) => renderCell(rowData, column))}
235
+ {editable && renderDeleteButton(rowData)}
236
+ </tr>
237
+ );
238
+ } else {
239
+ return columns.map((column) => (
240
+ <tr
241
+ key={`${rowData.id}-${column.name}`}
242
+ className={index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : ""}
243
+ >
152
244
  <th
153
- key={index}
154
- className={`text-left align-middle p-2 ${thElementsClassName}`}
155
- style={{ width: `${ancho}%` }}
245
+ className={`text-left align-middle p-2 ${thElementsClassName} ${
246
+ column.headerClassName || ""
247
+ }`}
156
248
  >
157
249
  {column.label}
158
250
  </th>
159
- );
160
- })}
161
- {editable && <th style={{ width: "4%" }}></th>}
162
- </tr>
163
- );
164
-
165
- const renderRow = (rowData: RowDataType, index: number) => {
166
- return (
167
- <tr
168
- key={rowData.id}
169
- className={index % 2 === 0 ? `${withoutBg ? "" : "bg-gray-200"}` : ""}
170
- >
171
- {columns.map((column) => {
172
- if (loading) {
173
- return (
174
- <td
175
- key={`${rowData.id}-${column.name}`}
176
- className="align-middle p-1"
177
- >
178
- <LoadingInputSkeleton />
179
- </td>
180
- );
181
- }
182
- const tempVal = rowData[column.name as any];
251
+ {renderCell(rowData, column)}
252
+ {editable &&
253
+ column === columns[columns.length - 1] &&
254
+ renderDeleteButton(rowData)}
255
+ </tr>
256
+ ));
257
+ }
258
+ };
183
259
 
184
- const defaultVal =
185
- column.type === "selectField"
186
- ? column.options?.find((x) => x.value === tempVal)
187
- : tempVal;
260
+ const renderCell = (
261
+ rowData: RowDataType,
262
+ column: GenericDynamicTableColumn
263
+ ) => {
264
+ if (loading) {
265
+ return (
266
+ <td
267
+ key={`${rowData.id}-${column.name}`}
268
+ className={`align-middle p-1 ${column.cellClassName || ""}`}
269
+ >
270
+ <LoadingInputSkeleton />
271
+ </td>
272
+ );
273
+ }
274
+ const tempVal = rowData[column.name as any];
188
275
 
189
- if (column.type === "label") {
190
- return (
191
- <td
192
- key={`${rowData.id}-${column.name}`}
193
- className="align-middle p-1"
194
- >
195
- <div>{defaultVal}</div>
196
- </td>
197
- );
198
- }
276
+ const defaultVal =
277
+ column.type === "selectField"
278
+ ? column.options?.find((x) => x.value === tempVal)
279
+ : tempVal;
199
280
 
200
- const FieldComponent =
201
- column.type === "textField"
202
- ? TextField
203
- : column.type === "checkbox"
204
- ? CheckBox
205
- : SelectField;
281
+ if (column.type === "label") {
282
+ return (
283
+ <td
284
+ key={`${rowData.id}-${column.name}`}
285
+ className={`align-middle p-1 ${column.cellClassName || ""}`}
286
+ >
287
+ <div>{defaultVal}</div>
288
+ </td>
289
+ );
290
+ }
206
291
 
207
- const setTableValue = (columnName: string, newValue: any) => {
208
- setTableData((prevState) => {
209
- // Encontrar el índice de la fila que está cambiando
210
- const rowIndex = prevState.findIndex((x) => x.id === rowData.id);
211
- // Crear una copia del objeto en esa fila
212
- const updatedRow = {
213
- ...prevState[rowIndex],
214
- [columnName]: newValue,
215
- };
216
- // Copiar todo el array
217
- const updatedData = [...prevState];
292
+ const FieldComponent =
293
+ column.type === "textField"
294
+ ? TextField
295
+ : column.type === "checkbox"
296
+ ? CheckBox
297
+ : SelectField;
218
298
 
219
- // Reemplazar el objeto en la fila que cambió
220
- updatedData[rowIndex] = updatedRow;
221
- return updatedData;
222
- });
223
- };
299
+ const setTableValue = (columnName: string, newValue: any) => {
300
+ handleChange(columnName, newValue, rowData.id);
301
+ };
224
302
 
225
- return (
226
- <td
227
- key={`${rowData.id}-${column.name}`}
228
- className="align-middle p-1"
229
- >
230
- {column.loadingOptions ? (
231
- <LoadingInputSkeleton />
232
- ) : (
233
- <FieldComponent
234
- key={`${rowData.id}-${column.name}`}
235
- //name={column.name}
236
- type={column.textFieldType}
237
- integer={!!column.integer}
238
- disabled={column.disabled}
239
- isClearable
240
- onChange={(value: any) => {
241
- const sendValue = value?.value ?? value;
242
- handleChange(column.name, sendValue, rowData.id);
243
- column.onChange &&
244
- column.onChange(rowData, sendValue, setTableValue);
245
- }}
246
- defaultValue={defaultVal}
247
- options={column.options ?? []}
248
- />
249
- )}
250
- </td>
251
- );
252
- })}
253
- {editable && (
254
- <td className="align-middle w-16">
255
- <WithTooltip text="Eliminar">
256
- <button
257
- className="bg-red-500 hover:bg-red-600 font-bold py-1 px-2 rounded ml-2"
258
- onClick={(
259
- event: React.MouseEvent<HTMLButtonElement, MouseEvent>
260
- ) => {
261
- event.preventDefault();
262
- event.stopPropagation();
263
- createModal({
264
- title: "¿Está seguro que quiere eliminar este registro?",
265
- description:
266
- "Una vez eliminada la información no podrá ser recuperada.",
267
- }).then((response) => {
268
- if (response === "OK") {
269
- removeRow(rowData.id);
270
- }
271
- });
272
- }}
273
- type="button"
274
- >
275
- <TrashSvg />
276
- </button>
277
- </WithTooltip>
278
- </td>
303
+ return (
304
+ <td
305
+ key={`${rowData.id}-${column.name}`}
306
+ className={`align-middle p-1 ${column.cellClassName || ""}`}
307
+ >
308
+ {column.loadingOptions ? (
309
+ <LoadingInputSkeleton />
310
+ ) : (
311
+ <FieldComponent
312
+ key={`${rowData.id}-${column.name}`}
313
+ name={`${rowData.id}-${column.name}`}
314
+ type={column.textFieldType}
315
+ integer={!!column.integer}
316
+ disabled={column.disabled}
317
+ isClearable
318
+ onChange={(value: any) => {
319
+ const sendValue = value?.value ?? value;
320
+ handleChange(column.name, sendValue, rowData.id);
321
+ column.onChange &&
322
+ column.onChange(rowData, sendValue, setTableValue);
323
+ }}
324
+ defaultValue={defaultVal}
325
+ options={column.options ?? []}
326
+ />
279
327
  )}
280
- </tr>
328
+ </td>
281
329
  );
282
330
  };
283
331
 
332
+ const renderDeleteButton = (rowData: RowDataType) => (
333
+ <td className="align-middle w-16">
334
+ <WithTooltip text="Eliminar">
335
+ <button
336
+ className="bg-red-500 hover:bg-red-600 font-bold py-1 px-2 rounded ml-2"
337
+ onClick={(event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
338
+ event.preventDefault();
339
+ event.stopPropagation();
340
+ createModal({
341
+ title: "¿Está seguro que quiere eliminar este registro?",
342
+ description:
343
+ "Una vez eliminada la información no podrá ser recuperada.",
344
+ }).then((response) => {
345
+ if (response === "OK") {
346
+ removeRow(rowData.id);
347
+ }
348
+ });
349
+ }}
350
+ type="button"
351
+ >
352
+ <TrashSvg />
353
+ </button>
354
+ </WithTooltip>
355
+ </td>
356
+ );
357
+
284
358
  const renderRows = () => {
285
359
  let mapeable = filteredTableData.slice(
286
360
  (currentPage - 1) * itemsPerPage,
@@ -372,42 +446,50 @@ export const GenericDynamicTable = (props: Props) => {
372
446
  </div>
373
447
  )}
374
448
  <table className="w-full">
375
- <thead>{renderHeader()}</thead>
449
+ {orientation === "horizontal" && <thead>{renderHeader()}</thead>}
376
450
  <tbody>{renderRows()}</tbody>
377
- {footerRow && !editable ? (
378
- <tfoot className="border-t-2 border-black">
451
+ {editable && (
452
+ <tfoot>
379
453
  <tr>
380
- {Object.keys(footerRow ?? {})?.map((x, indx) => {
381
- return (
382
- <td className="align-middle" key={indx}>
383
- {(footerRow as any)[x]}
384
- </td>
385
- );
386
- })}
454
+ <td
455
+ colSpan={
456
+ orientation === "horizontal" ? columns.length + 1 : 2
457
+ }
458
+ className="align-middle"
459
+ >
460
+ {(!maxRows || tableData.length < maxRows) && (
461
+ <button
462
+ className="bg-blue-500 hover:bg-blue-600 font-bold py-2 px-4 rounded"
463
+ onClick={(
464
+ event: React.MouseEvent<HTMLButtonElement, MouseEvent>
465
+ ) => {
466
+ event.preventDefault();
467
+ event.stopPropagation();
468
+ addRow();
469
+ }}
470
+ type="button"
471
+ >
472
+ +
473
+ </button>
474
+ )}
475
+ </td>
387
476
  </tr>
388
477
  </tfoot>
389
- ) : editable ? (
390
- <tfoot>
478
+ )}
479
+ {footerRow && (
480
+ <tfoot className="border-t-2 border-black">
391
481
  <tr>
392
- <td className="align-middle">
393
- <button
394
- className="bg-blue-500 hover:bg-blue-600 font-bold py-2 px-4 rounded"
395
- onClick={(
396
- event: React.MouseEvent<HTMLButtonElement, MouseEvent>
397
- ) => {
398
- event.preventDefault();
399
- event.stopPropagation();
400
- addRow();
401
- }}
402
- type="button"
482
+ {footerRow.map((column, index) => (
483
+ <td
484
+ key={index}
485
+ colSpan={orientation === "vertical" ? 2 : 1}
486
+ className={`align-middle ${column.className || ""}`}
403
487
  >
404
- +
405
- </button>
406
- </td>
488
+ {column.content}
489
+ </td>
490
+ ))}
407
491
  </tr>
408
492
  </tfoot>
409
- ) : (
410
- <></>
411
493
  )}
412
494
  </table>
413
495
  {paginated && totalPages() > 1 && (
@@ -178,7 +178,12 @@ export const SelectField = (props: Props) => {
178
178
  } else {
179
179
  handleOptionClick(filteredOptions[0]);
180
180
  }
181
+ } else if (isTabPressed) {
182
+ if (highlightedIndex >= 0) {
183
+ handleOptionClick(filteredOptions[highlightedIndex]);
184
+ }
181
185
  }
186
+
182
187
  setIsTabPressed(false);
183
188
  setIsEnterPressed(false);
184
189
  setIsSearching(false);
@@ -43,7 +43,7 @@ export const TextField = (props: Props) => {
43
43
  style,
44
44
  title,
45
45
  helpText,
46
- className,
46
+ className = "",
47
47
  hint,
48
48
  required,
49
49
  } = props;
@@ -15,30 +15,35 @@ import type {
15
15
  } from "./NavBar.types.js";
16
16
  import { Link, useNavigate } from "@remix-run/react";
17
17
 
18
- const OptionsDropDownButton = ({ color, options, name }: EntityProps) => {
18
+ const OptionsDropDownButton = ({
19
+ color,
20
+ options,
21
+ name,
22
+ specialColor,
23
+ }: EntityProps) => {
19
24
  const [showOptionsMenu, setShowOptionsMenu] = useState(true);
20
25
 
21
26
  return (
22
- <div className="nav-item ml-auto">
23
- <div className="flex justify-center">
24
- <div className="relative inline-block">
25
- <button
26
- onClick={() => setShowOptionsMenu(!showOptionsMenu)}
27
- className={`relative flex items-center p-2 text-xs text-white ${color.bg700} active:${color.bg900} border border-transparent rounded-full uppercase focus:ring-opacity-40 focus:outline-none`}
28
- >
29
- {name ?? <OpcionButtonSvgIcon />}
30
- <DropDownArrowSvgIcon />
31
- </button>
32
- <div
33
- className="absolute right-0 z-20 w-56 py-2 mt-2 overflow-hidden bg-white rounded-md shadow-xl dark:bg-gray-800"
34
- hidden={showOptionsMenu}
35
- onMouseLeave={() => setShowOptionsMenu(true)}
36
- >
37
- {options.map((option, index) => (
38
- <React.Fragment key={index}>{option}</React.Fragment>
39
- ))}
40
- </div>
41
- </div>
27
+ <div
28
+ className={`${
29
+ specialColor ? specialColor.bg700 : color.bg700
30
+ } container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150`}
31
+ >
32
+ <button
33
+ onClick={() => setShowOptionsMenu(!showOptionsMenu)}
34
+ className={`relative flex items-center p-2 text-xs text-white ${color.bg700} active:${color.bg900} border border-transparent rounded-full uppercase focus:ring-opacity-40 focus:outline-none`}
35
+ >
36
+ {name ?? <OpcionButtonSvgIcon />}
37
+ <DropDownArrowSvgIcon />
38
+ </button>
39
+ <div
40
+ className="absolute right-0 z-20 w-56 py-2 mt-2 overflow-hidden bg-white rounded-md shadow-xl dark:bg-gray-800"
41
+ hidden={showOptionsMenu}
42
+ onMouseLeave={() => setShowOptionsMenu(true)}
43
+ >
44
+ {options.map((option, index) => (
45
+ <React.Fragment key={index}>{option}</React.Fragment>
46
+ ))}
42
47
  </div>
43
48
  </div>
44
49
  );
@@ -57,8 +62,13 @@ const NavItem = ({
57
62
  <OptionsDropDownButton
58
63
  name={name}
59
64
  color={color}
65
+ specialColor={specialColor}
60
66
  options={childrens.map((x, index) => (
61
- <Link key={index} to={x.link} className="hover:bg-red-100">
67
+ <Link
68
+ key={index}
69
+ to={x.link}
70
+ className={`block px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-red-100 dark:hover:bg-gray-700 dark:hover:text-white`}
71
+ >
62
72
  {x.name}
63
73
  </Link>
64
74
  ))}
@@ -67,7 +77,7 @@ const NavItem = ({
67
77
  <div
68
78
  className={`${
69
79
  specialColor ? specialColor.bg700 : color.bg700
70
- } container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150 `}
80
+ } container text-white w-full sm:w-auto h-10 text-sm py-1 uppercase shadow hover:shadow-lg outline-none rounded-full focus:outline-none my-auto sm:my-0 sm:mr-1 mb-1 ease-linear transition-all duration-150`}
71
81
  >
72
82
  <Link
73
83
  className="px-3 flex items-center text-xs leading-snug text-white uppercase hover:opacity-75"
@@ -54,4 +54,5 @@ export type EntityProps = {
54
54
  name?: string;
55
55
  color: ColorInterface;
56
56
  options: any[];
57
+ specialColor?: ColorInterface;
57
58
  };
@@ -0,0 +1,94 @@
1
+ import React from "react";
2
+ import { motion } from "framer-motion";
3
+
4
+ interface LoadingItem {
5
+ name: string;
6
+ loading: boolean;
7
+ }
8
+
9
+ interface LoadingWindowProps {
10
+ loadingItems?: LoadingItem[];
11
+ description?: string;
12
+ }
13
+
14
+ export const LoadingWindow: React.FC<LoadingWindowProps> = ({
15
+ loadingItems,
16
+ description = "Por favor, espere mientras se carga la página.",
17
+ }) => {
18
+ return (
19
+ <div className="min-h-screen bg-gray-100 flex flex-col justify-center items-center">
20
+ <motion.div
21
+ initial={{ opacity: 0, y: -20 }}
22
+ animate={{ opacity: 1, y: 0 }}
23
+ transition={{ duration: 0.5 }}
24
+ className="text-center flex flex-col items-center"
25
+ >
26
+ <img src="/logo.png" alt="Zauru Logo" className="mb-8 h-20" />
27
+ <h1 className="text-3xl font-bold text-gray-800 mb-4">Cargando...</h1>
28
+ <p className="text-gray-600 mb-4">{description}</p>
29
+ {loadingItems && (
30
+ <ul className="text-left mb-4">
31
+ {loadingItems.map((item, index) => (
32
+ <li key={index} className="flex items-center mb-2">
33
+ <svg
34
+ className={`${
35
+ item.loading ? "" : "hidden"
36
+ } w-5 h-5 text-blue-500 mr-2 animate-spin loading-spinner`}
37
+ fill="none"
38
+ viewBox="0 0 24 24"
39
+ stroke="currentColor"
40
+ >
41
+ <path
42
+ strokeLinecap="round"
43
+ strokeLinejoin="round"
44
+ strokeWidth={2}
45
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
46
+ />
47
+ </svg>
48
+ <svg
49
+ className={`${
50
+ item.loading ? "hidden" : ""
51
+ } w-5 h-5 text-green-500 mr-2 check-mark`}
52
+ fill="none"
53
+ viewBox="0 0 24 24"
54
+ stroke="currentColor"
55
+ >
56
+ <path
57
+ strokeLinecap="round"
58
+ strokeLinejoin="round"
59
+ strokeWidth={2}
60
+ d="M5 13l4 4L19 7"
61
+ />
62
+ </svg>
63
+ <span>{item.name}</span>
64
+ </li>
65
+ ))}
66
+ </ul>
67
+ )}
68
+ <motion.div
69
+ animate={{ rotate: 360 }}
70
+ transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
71
+ className="mt-8"
72
+ >
73
+ <svg
74
+ className="w-12 h-12 text-blue-500"
75
+ fill="none"
76
+ viewBox="0 0 24 24"
77
+ stroke="currentColor"
78
+ >
79
+ <motion.path
80
+ strokeLinecap="round"
81
+ strokeLinejoin="round"
82
+ strokeWidth={2}
83
+ d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
84
+ animate={{ rotate: 360 }}
85
+ transition={{ duration: 2, repeat: Infinity, ease: "linear" }}
86
+ />
87
+ </svg>
88
+ </motion.div>
89
+ </motion.div>
90
+ </div>
91
+ );
92
+ };
93
+
94
+ export default LoadingWindow;
@@ -1 +1,2 @@
1
1
  export * from "./LoadingInputSkeleton.js";
2
+ export * from "./LoadingWindow.js";