@zauru-sdk/components 2.0.77 → 2.0.79

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.79](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.78...v2.0.79) (2024-12-17)
7
+
8
+ **Note:** Version bump only for package @zauru-sdk/components
9
+
10
+
11
+
12
+
13
+
14
+ ## [2.0.78](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.77...v2.0.78) (2024-12-17)
15
+
16
+ **Note:** Version bump only for package @zauru-sdk/components
17
+
18
+
19
+
20
+
21
+
6
22
  ## [2.0.77](https://github.com/intuitiva/zauru-typescript-sdk/compare/v2.0.76...v2.0.77) (2024-12-13)
7
23
 
8
24
  **Note:** Version bump only for package @zauru-sdk/components
@@ -23,6 +23,8 @@ type Props = {
23
23
  withoutBg?: boolean;
24
24
  orientation?: "horizontal" | "vertical";
25
25
  maxRows?: number;
26
+ confirmDelete?: boolean;
27
+ addRowButtonHandler?: (tableData: RowDataType[], setTableData: (data: RowDataType[]) => void) => void;
26
28
  };
27
29
  /**
28
30
  *
@@ -1,7 +1,7 @@
1
1
  type FooterProps = {
2
2
  href: string;
3
3
  showConnection?: boolean;
4
- selectedColor: "purple" | "pink" | "indigo" | "cyan" | "slate" | "green" | "red" | "sky";
4
+ selectedColor: "purple" | "pink" | "indigo" | "cyan" | "slate" | "green" | "red" | "sky" | "yellow";
5
5
  version?: string;
6
6
  };
7
7
  export declare const Footer: ({ href, selectedColor, showConnection, version, }: FooterProps) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ import { SelectFieldOption } from "@zauru-sdk/types";
2
+ import React from "react";
3
+ type Props = {
4
+ id?: string;
5
+ name: string;
6
+ title?: string;
7
+ options: SelectFieldOption[];
8
+ defaultValue?: string | number;
9
+ disabled?: boolean;
10
+ required?: boolean;
11
+ onChange?: (value: string | number, event: React.ChangeEvent<HTMLInputElement>) => void;
12
+ helpText?: string;
13
+ hint?: string;
14
+ className?: string;
15
+ orientation?: "horizontal" | "vertical";
16
+ };
17
+ export declare const RadioButton: (props: Props) => import("react/jsx-runtime").JSX.Element;
18
+ export {};
@@ -11,3 +11,4 @@ export * from "./TextField/index.js";
11
11
  export * from "./TimePicker/index.js";
12
12
  export * from "./YesNo/index.js";
13
13
  export * from "./ReactZodForm/index.js";
14
+ export * from "./RadioButton/index.js";
@@ -1,5 +1,6 @@
1
- export declare const ValidateEmployeeAccess: ({ children, permissionVariableName, showIfNoPermission, }: {
1
+ export declare const ValidateEmployeeAccess: ({ children, permissionVariableName, showIfNoPermission, noPermissionComponent, }: {
2
2
  children: React.ReactNode;
3
3
  permissionVariableName: string;
4
4
  showIfNoPermission?: boolean;
5
+ noPermissionComponent?: React.ReactNode;
5
6
  }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,24 @@
1
+ type Item = {
2
+ name: string;
3
+ code: string;
4
+ unitPrice: number;
5
+ stock: number;
6
+ currencyPrefix: string;
7
+ imageUrl: string;
8
+ };
9
+ type ItemCategory = {
10
+ name: string;
11
+ items: Item[];
12
+ };
13
+ type ItemModalProps = {
14
+ itemList: ItemCategory[];
15
+ onClose: (selectedItem: Item | null) => void;
16
+ config?: {
17
+ itemSize: {
18
+ width: string;
19
+ height: string;
20
+ };
21
+ };
22
+ };
23
+ export declare const createItemModal: (itemList: ItemCategory[], config?: ItemModalProps["config"]) => Promise<Item | null>;
24
+ export {};
@@ -1 +1,2 @@
1
1
  export * from "./Modal.js";
2
+ export * from "./ItemModal.js";
@@ -1,5 +1,5 @@
1
- import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  export const BodyContainer = (props) => {
3
3
  const { children } = props;
4
- return (_jsx(_Fragment, { children: _jsx("body", { className: "flex flex-col min-h-screen m-0", children: children }) }));
4
+ return _jsx("body", { className: "flex flex-col min-h-screen m-0", children: children });
5
5
  };
@@ -68,7 +68,7 @@ const GenericDynamicTableErrorComponent = ({ name }) => {
68
68
  />
69
69
  */
70
70
  export const GenericDynamicTable = (props) => {
71
- 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;
71
+ 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, confirmDelete = true, addRowButtonHandler, } = props;
72
72
  try {
73
73
  const [tableData, setTableData] = useState(defaultValue);
74
74
  const [deletedData, setDeletedData] = useState([]);
@@ -181,14 +181,19 @@ export const GenericDynamicTable = (props) => {
181
181
  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) => {
182
182
  event.preventDefault();
183
183
  event.stopPropagation();
184
- createModal({
185
- title: "¿Está seguro que quiere eliminar este registro?",
186
- description: "Una vez eliminada la información no podrá ser recuperada.",
187
- }).then((response) => {
188
- if (response === "OK") {
189
- removeRow(rowData.id);
190
- }
191
- });
184
+ if (confirmDelete) {
185
+ createModal({
186
+ title: "¿Está seguro que quiere eliminar este registro?",
187
+ description: "Una vez eliminada la información no podrá ser recuperada.",
188
+ }).then((response) => {
189
+ if (response === "OK") {
190
+ removeRow(rowData.id);
191
+ }
192
+ });
193
+ }
194
+ else {
195
+ removeRow(rowData.id);
196
+ }
192
197
  }, type: "button", children: _jsx(TrashSvg, {}) }) }) }));
193
198
  const renderRows = () => {
194
199
  let mapeable = filteredTableData.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
@@ -242,7 +247,12 @@ export const GenericDynamicTable = (props) => {
242
247
  .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) => {
243
248
  event.preventDefault();
244
249
  event.stopPropagation();
245
- addRow();
250
+ if (addRowButtonHandler) {
251
+ addRowButtonHandler(tableData, setTableData);
252
+ }
253
+ else {
254
+ addRow();
255
+ }
246
256
  }, type: "button", children: "+" })) }) }) })), footerRow && (_jsx("tfoot", { className: "border-t-2 border-black", children: _jsxs("tr", { children: [columns.map((column, index) => {
247
257
  const footerCell = footerRow.find((fc) => fc.name === column.name);
248
258
  return (_jsx("td", { colSpan: orientation === "vertical" ? 2 : 1, className: `align-middle ${footerCell?.className || ""}`, children: footerCell ? footerCell.content : _jsx(_Fragment, {}) }, index));
@@ -9,8 +9,9 @@ const COLORS = {
9
9
  green: "bg-green-500",
10
10
  red: "bg-red-500",
11
11
  sky: "bg-sky-500",
12
+ yellow: "bg-yellow-500",
12
13
  };
13
14
  export const Footer = ({ href, selectedColor, showConnection = false, version = "2.0.5", }) => {
14
15
  const color = COLORS[selectedColor];
15
- return (_jsx("footer", { className: `inset-x-0 bottom-0 px-2 py-[20px] ${color}`, children: _jsxs("div", { className: "px-4 mx-auto flex flex-wrap items-center justify-center", children: [_jsxs("p", { className: "text-white text-[1.2rem]", children: [`Creado en `, " ", _jsx("a", { href: href, children: "Zauru" }), " ", `con ❤️ ${new Date().getFullYear()} v.${version}`] }), showConnection && (_jsx("div", { className: "ml-5", children: _jsx(ConnectionState, {}) }))] }) }));
16
+ return (_jsx("footer", { className: `inset-x-0 bottom-0 px-2 py-[20px] ${color}`, children: _jsxs("div", { className: "px-4 mx-auto flex flex-wrap items-center justify-center", children: [_jsxs("p", { className: "text-white text-[1.2rem]", children: [`Creado en `, " ", _jsx("a", { href: href, children: "Zauru" }), " ", `con ❤️ v.${version}`] }), showConnection && (_jsx("div", { className: "ml-5", children: _jsx(ConnectionState, {}) }))] }) }));
16
17
  };
@@ -0,0 +1,26 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { useFormContext } from "react-hook-form";
4
+ export const RadioButton = (props) => {
5
+ const { id, name, title, options, defaultValue, disabled = false, required = false, onChange, helpText, hint, className = "", orientation = "vertical", } = props;
6
+ const [selectedValue, setSelectedValue] = useState(defaultValue);
7
+ const { register: tempRegister, formState: { errors }, setValue: setOnFormValue, } = useFormContext() || { formState: {} };
8
+ const error = errors ? errors[name] : undefined;
9
+ const color = error ? "red" : "gray";
10
+ const register = tempRegister ? tempRegister(name, { required }) : undefined; // Solo usar register si está disponible
11
+ const handleChange = (event) => {
12
+ const newValue = event.target.value;
13
+ setSelectedValue(newValue);
14
+ if (register) {
15
+ register.onChange(event);
16
+ }
17
+ if (setOnFormValue) {
18
+ setOnFormValue(name, newValue);
19
+ }
20
+ onChange && onChange(newValue, event);
21
+ };
22
+ const containerClass = orientation === "horizontal"
23
+ ? "flex items-center gap-4 flex-wrap" // Estilo para disposición horizontal
24
+ : ""; // Vertical no necesita clase especial
25
+ return (_jsxs("div", { className: `col-span-6 sm:col-span-3 ${className}`, children: [title && (_jsxs("label", { htmlFor: id ?? name, className: `block mb-1 text-sm font-medium text-${color}-700 dark:text-${color}-500`, children: [title, required && _jsx("span", { className: "text-red-500", children: "*" })] })), _jsx("div", { className: containerClass, children: options.map((option) => (_jsxs("div", { className: `flex items-center mb-2 ${disabled ? "opacity-50 pointer-events-none" : ""}`, children: [_jsx("input", { type: "radio", id: `${name}-${option.value}`, value: option.value, disabled: disabled, checked: selectedValue === option.value, className: `h-4 w-4 text-indigo-600 border-${color}-300 focus:ring-indigo-500`, ...(register ?? {}), onChange: handleChange }), _jsx("label", { htmlFor: `${name}-${option.value}`, className: `ml-2 block text-sm font-medium text-${color}-900 dark:text-${color}-500`, children: option.label })] }, option.value))) }), helpText && (_jsx("p", { className: `mt-2 italic text-sm text-${color}-500 dark:text-${color}-400`, children: helpText })), error && (_jsxs("p", { className: `mt-2 text-sm text-${color}-600 dark:text-${color}-500`, children: [_jsx("span", { className: "font-medium", children: "Oops!" }), " ", error.message?.toString()] })), !error && hint && (_jsx("p", { className: `mt-2 italic text-sm text-${color}-500 dark:text-${color}-400`, children: hint }))] }));
26
+ };
@@ -154,12 +154,8 @@ export const SelectField = (props) => {
154
154
  }
155
155
  else if (e.key === "Backspace" && (value || valueMulti.length > 0)) {
156
156
  e.preventDefault();
157
- if (!isMulti) {
158
- handleClear();
159
- }
160
- else {
161
- setInputValue("");
162
- }
157
+ handleClear();
158
+ setInputValue("");
163
159
  setFilteredOptions(options);
164
160
  setIsOpen(true);
165
161
  }
@@ -11,3 +11,4 @@ export * from "./TextField/index.js";
11
11
  export * from "./TimePicker/index.js";
12
12
  export * from "./YesNo/index.js";
13
13
  export * from "./ReactZodForm/index.js";
14
+ export * from "./RadioButton/index.js";
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useGetEmployeeProfile, useGetSessionAttribute, } from "@zauru-sdk/hooks";
3
3
  import { Link } from "@remix-run/react";
4
- export const ValidateEmployeeAccess = ({ children, permissionVariableName, showIfNoPermission = false, }) => {
4
+ export const ValidateEmployeeAccess = ({ children, permissionVariableName, showIfNoPermission = false, noPermissionComponent, }) => {
5
5
  const { data: employee } = useGetEmployeeProfile();
6
6
  const variable_string = useGetSessionAttribute(permissionVariableName, "sessionVariable");
7
7
  const variable = variable_string?.split(",");
@@ -9,5 +9,8 @@ export const ValidateEmployeeAccess = ({ children, permissionVariableName, showI
9
9
  if (showIfNoPermission && !hasPermission) {
10
10
  return (_jsxs("div", { className: "bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4", children: [_jsx("img", { src: "/logo.png", alt: "Zauru Logo", className: "mb-8 h-20" }), _jsx("h1", { className: "text-5xl font-extrabold text-red-500 mb-6", children: "\u00A1Acceso Denegado!" }), _jsx("div", { className: "w-full max-w-2xl", children: _jsx("p", { className: "text-2xl text-gray-300 mb-8 text-center", children: "Lo sentimos, no tienes permiso para acceder a esta p\u00E1gina." }) }), _jsx(Link, { to: "/", className: "bg-blue-600 text-white py-3 px-8 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300 transform hover:scale-105", children: "Regresar al inicio" }), _jsx("div", { className: "mt-12 text-gray-500", children: _jsx("p", { children: "Si crees que esto es un error, por favor contacta a soporte." }) })] }));
11
11
  }
12
+ if (noPermissionComponent && !hasPermission) {
13
+ return _jsx(_Fragment, { children: noPermissionComponent });
14
+ }
12
15
  return _jsx(_Fragment, { children: hasPermission ? children : null });
13
16
  };
@@ -2,13 +2,19 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { isRouteErrorResponse, Links, Meta, Scripts, useRouteError, Link, } from "@remix-run/react";
3
3
  import { useState } from "react";
4
4
  export const ErrorLayout = ({ from, error: parentError, }) => {
5
- const error = useRouteError();
6
- const [showDetails, setShowDetails] = useState(!!parentError);
7
- return (_jsxs("html", { lang: "es", className: "bg-gray-900 text-white", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "\u00A1Ups! Algo sali\u00F3 mal" }), _jsx(Meta, {}), _jsx(Links, {})] }), _jsxs("body", { className: "min-h-screen flex flex-col items-center justify-center p-4", children: [_jsx("img", { src: "/logo.png", alt: "Zauru Logo", className: "mb-8 h-20" }), _jsx("h1", { className: "text-5xl font-extrabold text-red-500 mb-6", children: "\u00A1Ups!" }), _jsxs("div", { className: "w-full max-w-2xl flex flex-col items-center", children: [_jsx("p", { className: "text-2xl text-gray-300 mb-8 text-center", children: isRouteErrorResponse(error)
8
- ? `Error ${error.status}: ${error.statusText}`
9
- : error instanceof Error
10
- ? error.message
11
- : "Ha ocurrido un error inesperado" }), from && (_jsxs("p", { className: "text-lg text-gray-400 mb-4 text-center", children: ["Error lanzado desde: ", from] })), parentError && (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("button", { onClick: () => setShowDetails(!showDetails), className: "text-blue-400 hover:text-blue-300 transition duration-300", children: showDetails ? "Ocultar detalles" : "Ver más detalles" }), showDetails && (_jsx("p", { className: "mt-2 text-gray-400 text-sm", children: parentError instanceof Error
12
- ? parentError.message
13
- : String(parentError) }))] }))] }), _jsx(Link, { to: "/", className: "bg-blue-600 text-white py-3 px-8 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300 transform hover:scale-105", children: "Regresar al inicio" }), _jsx("div", { className: "mt-12 text-gray-500 text-center", children: _jsx("p", { children: "Si el problema persiste, por favor contacta a soporte." }) }), _jsx(Scripts, {})] })] }));
5
+ try {
6
+ const error = useRouteError();
7
+ const [showDetails, setShowDetails] = useState(!!parentError);
8
+ return (_jsxs("html", { lang: "es", className: "bg-gray-900 text-white", children: [_jsxs("head", { children: [_jsx("meta", { charSet: "utf-8" }), _jsx("meta", { name: "viewport", content: "width=device-width, initial-scale=1" }), _jsx("title", { children: "\u00A1Ups! Algo sali\u00F3 mal" }), _jsx(Meta, {}), _jsx(Links, {})] }), _jsxs("body", { className: "min-h-screen flex flex-col items-center justify-center p-4", children: [_jsx("img", { src: "/logo.png", alt: "Zauru Logo", className: "mb-8 h-20" }), _jsx("h1", { className: "text-5xl font-extrabold text-red-500 mb-6", children: "\u00A1Ups!" }), _jsxs("div", { className: "w-full max-w-2xl flex flex-col items-center", children: [_jsx("p", { className: "text-2xl text-gray-300 mb-8 text-center", children: isRouteErrorResponse(error)
9
+ ? `Error ${error.status}: ${error.statusText}`
10
+ : error instanceof Error
11
+ ? error.message
12
+ : "Ha ocurrido un error inesperado" }), from && (_jsxs("p", { className: "text-lg text-gray-400 mb-4 text-center", children: ["Error lanzado desde: ", from] })), parentError && (_jsxs("div", { className: "mb-4 text-center", children: [_jsx("button", { onClick: () => setShowDetails(!showDetails), className: "text-blue-400 hover:text-blue-300 transition duration-300", children: showDetails ? "Ocultar detalles" : "Ver más detalles" }), showDetails && (_jsx("p", { className: "mt-2 text-gray-400 text-sm", children: parentError instanceof Error
13
+ ? parentError.message
14
+ : String(parentError) }))] }))] }), _jsx(Link, { to: "/", className: "bg-blue-600 text-white py-3 px-8 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300 transform hover:scale-105", children: "Regresar al inicio" }), _jsx("div", { className: "mt-12 text-gray-500 text-center", children: _jsx("p", { children: "Si el problema persiste, por favor contacta a soporte." }) }), _jsx(Scripts, {})] })] }));
15
+ }
16
+ catch (error) {
17
+ console.error(error);
18
+ return (_jsxs("html", { lang: "es", className: "bg-gray-900 text-white", children: [_jsx("head", { children: _jsx("title", { children: "\u00A1Ups! Algo sali\u00F3 mal" }) }), _jsxs("body", { children: ["Error al renderizar el layout de error ", error] })] }));
19
+ }
14
20
  };
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { createRoot } from "react-dom/client";
4
+ const ItemSelectionModal = ({ itemList, onClose, config, }) => {
5
+ const defaultConfig = {
6
+ itemSize: {
7
+ width: config?.itemSize?.width || "150px",
8
+ height: config?.itemSize?.height || "200px",
9
+ },
10
+ };
11
+ const [searchTerm, setSearchTerm] = useState("");
12
+ const [expandedCategories, setExpandedCategories] = useState({});
13
+ const toggleCategory = (categoryName) => {
14
+ setExpandedCategories((prev) => ({
15
+ ...prev,
16
+ [categoryName]: !prev[categoryName],
17
+ }));
18
+ };
19
+ const handleItemClick = (item) => {
20
+ onClose(item);
21
+ };
22
+ const filteredList = itemList
23
+ .map((category) => ({
24
+ ...category,
25
+ items: category.items.filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase())),
26
+ }))
27
+ .filter((category) => category.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
28
+ category.items.length > 0);
29
+ return (_jsx("div", { className: "fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50", children: _jsxs("div", { className: "bg-white p-4 rounded-lg shadow-lg w-11/12 max-w-4xl h-5/6 overflow-y-auto", children: [_jsxs("div", { className: "flex flex-col mb-4", children: [_jsxs("div", { className: "flex justify-between items-center mb-4", children: [_jsx("h2", { className: "text-2xl font-bold", children: "Seleccionar un \u00CDtem" }), _jsx("button", { className: "text-gray-500 hover:text-gray-800", onClick: () => onClose(null), children: "\u00D7" })] }), _jsx("input", { type: "text", placeholder: "Buscar por nombre o categor\u00EDa...", value: searchTerm, onChange: (e) => setSearchTerm(e.target.value), className: "p-2 border rounded-lg w-full" })] }), filteredList.length === 0 ? (_jsx("p", { className: "text-gray-500 text-center", children: "No se encontraron resultados." })) : (filteredList.map((category) => (_jsxs("div", { className: "mb-4", children: [_jsxs("h3", { className: "text-lg font-semibold mb-2 cursor-pointer flex justify-between items-center hover:text-blue-600", onClick: () => toggleCategory(category.name), children: [_jsxs("span", { children: [category.name, " ", _jsxs("span", { className: "text-gray-500", children: ["(", category.items.length, ")"] })] }), _jsx("span", { className: `transform transition-transform duration-200 ${expandedCategories[category.name] ? "rotate-90" : ""}`, children: "\u25B6" })] }), expandedCategories[category.name] && (_jsx("div", { className: "grid gap-5", style: {
30
+ gridTemplateColumns: `repeat(auto-fit, minmax(${defaultConfig.itemSize.width}, 1fr))`,
31
+ }, children: category.items.map((item) => (_jsxs("div", { className: "border rounded-lg shadow-lg hover:shadow-xl cursor-pointer relative", onClick: () => handleItemClick(item), style: {
32
+ backgroundImage: `url(${item.imageUrl})`,
33
+ backgroundSize: "cover",
34
+ backgroundPosition: "center",
35
+ width: defaultConfig.itemSize.width,
36
+ height: defaultConfig.itemSize.height,
37
+ }, children: [_jsx("div", { className: "bg-white bg-opacity-80 p-2 rounded absolute top-0 left-0 right-0", children: _jsx("h4", { className: "font-bold text-sm text-center", children: item.name }) }), _jsx("div", { className: "absolute bottom-0 left-0 bg-white bg-opacity-80 p-2 rounded", children: _jsxs("span", { className: "text-xs", children: ["Stock: ", item.stock] }) }), _jsx("div", { className: "absolute bottom-0 right-0 bg-white bg-opacity-80 p-2 rounded", children: _jsxs("span", { className: "text-xs", children: [item.currencyPrefix, item.unitPrice] }) })] }, item.code))) }))] }, category.name))))] }) }));
38
+ };
39
+ // Función para crear el modal
40
+ export const createItemModal = (itemList, config) => {
41
+ return new Promise((resolve) => {
42
+ const handleClose = (selectedItem) => {
43
+ resolve(selectedItem);
44
+ removeModal();
45
+ };
46
+ const removeModal = () => {
47
+ document.body.removeChild(modalWrapper);
48
+ };
49
+ const modalWrapper = document.createElement("div");
50
+ document.body.appendChild(modalWrapper);
51
+ const root = createRoot(modalWrapper);
52
+ root.render(_jsx(ItemSelectionModal, { itemList: itemList, onClose: handleClose, config: config }));
53
+ });
54
+ };
@@ -1 +1,2 @@
1
1
  export * from "./Modal.js";
2
+ export * from "./ItemModal.js";
@@ -11,25 +11,13 @@ const NavItem = ({ name, link, icon, color, specialColor, childrens = [], }) =>
11
11
  export const NavBar = ({ title, loggedIn, items, selectedColor, version, }) => {
12
12
  const color = COLORS[selectedColor];
13
13
  const [NavBarOpen, setNavBarOpen] = useState(false);
14
- const [isDarkMode, setIsDarkMode] = useState(false);
15
14
  const [currentVersion, setCurrentVersion] = useState(version);
16
15
  const navigate = useNavigate();
17
- useEffect(() => {
18
- if (isDarkMode) {
19
- document.documentElement.classList.add("dark");
20
- }
21
- else {
22
- document.documentElement.classList.remove("dark");
23
- }
24
- }, [isDarkMode]);
25
16
  useEffect(() => {
26
17
  if (version !== currentVersion) {
27
18
  setCurrentVersion(version);
28
19
  }
29
20
  }, [version, currentVersion]);
30
- const toggleDarkMode = () => {
31
- setIsDarkMode(!isDarkMode);
32
- };
33
21
  const refreshPage = () => {
34
22
  navigate(0);
35
23
  };
@@ -43,7 +31,6 @@ export const NavBar = ({ title, loggedIn, items, selectedColor, version, }) => {
43
31
  }) }));
44
32
  const options = (_jsxs(_Fragment, { children: [_jsx("ul", { className: "w-full lg:flex lg:items-center", children: renderNavItems(items.filter((item) => item.loggedIn === loggedIn)) }), _jsx("ul", { className: "sm:flex sm:flex-col lg:flex-row ml-auto", children: loggedIn && (_jsx(OptionsDropDownButton, { color: color, options: [
45
33
  _jsx(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`, to: "/logout", prefetch: "none", children: _jsxs("div", { className: "mx-auto pt-2", children: [_jsx(LogoutDropDownSvgIcon, {}), _jsx("span", { children: "Cerrar sesi\u00F3n" })] }) }),
46
- _jsx("button", { className: `block w-full text-left px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white`, onClick: toggleDarkMode, children: _jsxs("div", { className: "mx-auto pt-2", children: [isDarkMode ? "🌞" : "🌙", _jsx("span", { children: isDarkMode ? "Modo Claro" : "Modo Oscuro" })] }) }),
47
34
  ] })) })] }));
48
35
  return (_jsx("nav", { className: `py-3 ${color.bg600}`, children: _jsxs("div", { className: "flex items-center justify-between ml-5 mr-5", children: [_jsxs("div", { className: "flex justify-between items-center w-full lg:w-auto", children: [_jsx(Link, { className: "text-sm font-bold leading-relaxed inline-block mr-4 py-2 whitespace-nowrap uppercase text-white", to: "/home", prefetch: "none", children: _jsxs(_Fragment, { children: [_jsx("div", { className: "inline-block mr-2 mb-2 align-middle", children: _jsx("img", { className: "w-auto h-7", src: "/logo.png", alt: "logo-zauru" }) }), title] }) }), version !== currentVersion && (_jsx("button", { className: `ml-2 px-2 py-1 text-xs text-white ${color.bg700} rounded-full hover:${color.bg900} transition-colors duration-200`, onClick: refreshPage, children: "\uD83D\uDD04 Actualizar versi\u00F3n" })), _jsx("button", { className: `rounded lg:hidden focus:outline-none focus:ring focus:${color.ring600} focus:ring-opacity-50`, "aria-label": "Toggle mobile menu", type: "button", onClick: () => setNavBarOpen(!NavBarOpen), children: _jsx(MenuAlt4Svg, { open: NavBarOpen }) })] }), _jsx("div", { className: `lg:hidden fixed top-0 left-0 z-50 w-64 h-full ${color.bg700} dark:bg-gray-900 shadow-lg transform ${NavBarOpen ? "translate-x-0" : "-translate-x-full"} transition-transform duration-300 ease-in-out overflow-y-auto`, children: _jsx("div", { className: "p-4", children: options }) }), _jsx("div", { className: "hidden lg:flex lg:items-center w-full lg:w-auto", children: options })] }) }));
49
36
  };
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
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" } }) }) })] }) }));
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-200 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
5
  };
6
6
  export default LoadingWindow;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zauru-sdk/components",
3
- "version": "2.0.77",
3
+ "version": "2.0.79",
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.77",
37
- "@zauru-sdk/hooks": "^2.0.77",
36
+ "@zauru-sdk/common": "^2.0.78",
37
+ "@zauru-sdk/hooks": "^2.0.78",
38
38
  "@zauru-sdk/icons": "^2.0.0",
39
- "@zauru-sdk/types": "^2.0.70",
40
- "@zauru-sdk/utils": "^2.0.77",
39
+ "@zauru-sdk/types": "^2.0.78",
40
+ "@zauru-sdk/utils": "^2.0.79",
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": "127c74c34eccffe40b39ad76957297833a8fc04c"
52
+ "gitHead": "4055b172e4a8813f86234a8038303d759740cd6f"
53
53
  }
@@ -6,9 +6,5 @@ type Props = {
6
6
 
7
7
  export const BodyContainer = (props: Props) => {
8
8
  const { children } = props;
9
- return (
10
- <>
11
- <body className="flex flex-col min-h-screen m-0">{children}</body>
12
- </>
13
- );
9
+ return <body className="flex flex-col min-h-screen m-0">{children}</body>;
14
10
  };
@@ -40,6 +40,11 @@ type Props = {
40
40
  withoutBg?: boolean;
41
41
  orientation?: "horizontal" | "vertical";
42
42
  maxRows?: number;
43
+ confirmDelete?: boolean;
44
+ addRowButtonHandler?: (
45
+ tableData: RowDataType[],
46
+ setTableData: (data: RowDataType[]) => void
47
+ ) => void;
43
48
  };
44
49
 
45
50
  const GenericDynamicTableErrorComponent = ({ name }: { name: string }) => {
@@ -127,6 +132,8 @@ export const GenericDynamicTable = (props: Props) => {
127
132
  withoutBg = false,
128
133
  orientation = "horizontal",
129
134
  maxRows,
135
+ confirmDelete = true,
136
+ addRowButtonHandler,
130
137
  } = props;
131
138
 
132
139
  try {
@@ -346,15 +353,19 @@ export const GenericDynamicTable = (props: Props) => {
346
353
  ) => {
347
354
  event.preventDefault();
348
355
  event.stopPropagation();
349
- createModal({
350
- title: "¿Está seguro que quiere eliminar este registro?",
351
- description:
352
- "Una vez eliminada la información no podrá ser recuperada.",
353
- }).then((response) => {
354
- if (response === "OK") {
355
- removeRow(rowData.id);
356
- }
357
- });
356
+ if (confirmDelete) {
357
+ createModal({
358
+ title: "¿Está seguro que quiere eliminar este registro?",
359
+ description:
360
+ "Una vez eliminada la información no podrá ser recuperada.",
361
+ }).then((response) => {
362
+ if (response === "OK") {
363
+ removeRow(rowData.id);
364
+ }
365
+ });
366
+ } else {
367
+ removeRow(rowData.id);
368
+ }
358
369
  }}
359
370
  type="button"
360
371
  >
@@ -474,7 +485,11 @@ export const GenericDynamicTable = (props: Props) => {
474
485
  ) => {
475
486
  event.preventDefault();
476
487
  event.stopPropagation();
477
- addRow();
488
+ if (addRowButtonHandler) {
489
+ addRowButtonHandler(tableData, setTableData);
490
+ } else {
491
+ addRow();
492
+ }
478
493
  }}
479
494
  type="button"
480
495
  >
@@ -11,7 +11,8 @@ type FooterProps = {
11
11
  | "slate"
12
12
  | "green"
13
13
  | "red"
14
- | "sky";
14
+ | "sky"
15
+ | "yellow";
15
16
  version?: string;
16
17
  };
17
18
 
@@ -24,6 +25,7 @@ const COLORS = {
24
25
  green: "bg-green-500",
25
26
  red: "bg-red-500",
26
27
  sky: "bg-sky-500",
28
+ yellow: "bg-yellow-500",
27
29
  };
28
30
 
29
31
  export const Footer = ({
@@ -38,8 +40,7 @@ export const Footer = ({
38
40
  <footer className={`inset-x-0 bottom-0 px-2 py-[20px] ${color}`}>
39
41
  <div className="px-4 mx-auto flex flex-wrap items-center justify-center">
40
42
  <p className="text-white text-[1.2rem]">
41
- {`Creado en `} <a href={href}>Zauru</a>{" "}
42
- {`con ❤️ ${new Date().getFullYear()} v.${version}`}
43
+ {`Creado en `} <a href={href}>Zauru</a> {`con ❤️ v.${version}`}
43
44
  </p>
44
45
  {showConnection && (
45
46
  <div className="ml-5">
@@ -0,0 +1,130 @@
1
+ import { SelectFieldOption } from "@zauru-sdk/types";
2
+ import React, { useState } from "react";
3
+ import { useFormContext } from "react-hook-form";
4
+
5
+ type Props = {
6
+ id?: string;
7
+ name: string;
8
+ title?: string;
9
+ options: SelectFieldOption[];
10
+ defaultValue?: string | number;
11
+ disabled?: boolean;
12
+ required?: boolean;
13
+ onChange?: (
14
+ value: string | number,
15
+ event: React.ChangeEvent<HTMLInputElement>
16
+ ) => void;
17
+ helpText?: string;
18
+ hint?: string;
19
+ className?: string;
20
+ orientation?: "horizontal" | "vertical";
21
+ };
22
+
23
+ export const RadioButton = (props: Props) => {
24
+ const {
25
+ id,
26
+ name,
27
+ title,
28
+ options,
29
+ defaultValue,
30
+ disabled = false,
31
+ required = false,
32
+ onChange,
33
+ helpText,
34
+ hint,
35
+ className = "",
36
+ orientation = "vertical",
37
+ } = props;
38
+
39
+ const [selectedValue, setSelectedValue] = useState(defaultValue);
40
+ const {
41
+ register: tempRegister,
42
+ formState: { errors },
43
+ setValue: setOnFormValue,
44
+ } = useFormContext() || { formState: {} };
45
+
46
+ const error = errors ? errors[name] : undefined;
47
+ const color = error ? "red" : "gray";
48
+
49
+ const register = tempRegister ? tempRegister(name, { required }) : undefined; // Solo usar register si está disponible
50
+
51
+ const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
52
+ const newValue = event.target.value;
53
+ setSelectedValue(newValue);
54
+
55
+ if (register) {
56
+ register.onChange(event);
57
+ }
58
+
59
+ if (setOnFormValue) {
60
+ setOnFormValue(name, newValue);
61
+ }
62
+
63
+ onChange && onChange(newValue, event);
64
+ };
65
+
66
+ const containerClass =
67
+ orientation === "horizontal"
68
+ ? "flex items-center gap-4 flex-wrap" // Estilo para disposición horizontal
69
+ : ""; // Vertical no necesita clase especial
70
+
71
+ return (
72
+ <div className={`col-span-6 sm:col-span-3 ${className}`}>
73
+ {title && (
74
+ <label
75
+ htmlFor={id ?? name}
76
+ className={`block mb-1 text-sm font-medium text-${color}-700 dark:text-${color}-500`}
77
+ >
78
+ {title}
79
+ {required && <span className="text-red-500">*</span>}
80
+ </label>
81
+ )}
82
+ <div className={containerClass}>
83
+ {options.map((option) => (
84
+ <div
85
+ key={option.value}
86
+ className={`flex items-center mb-2 ${
87
+ disabled ? "opacity-50 pointer-events-none" : ""
88
+ }`}
89
+ >
90
+ <input
91
+ type="radio"
92
+ id={`${name}-${option.value}`}
93
+ value={option.value}
94
+ disabled={disabled}
95
+ checked={selectedValue === option.value}
96
+ className={`h-4 w-4 text-indigo-600 border-${color}-300 focus:ring-indigo-500`}
97
+ {...(register ?? {})}
98
+ onChange={handleChange}
99
+ />
100
+ <label
101
+ htmlFor={`${name}-${option.value}`}
102
+ className={`ml-2 block text-sm font-medium text-${color}-900 dark:text-${color}-500`}
103
+ >
104
+ {option.label}
105
+ </label>
106
+ </div>
107
+ ))}
108
+ </div>
109
+ {helpText && (
110
+ <p
111
+ className={`mt-2 italic text-sm text-${color}-500 dark:text-${color}-400`}
112
+ >
113
+ {helpText}
114
+ </p>
115
+ )}
116
+ {error && (
117
+ <p className={`mt-2 text-sm text-${color}-600 dark:text-${color}-500`}>
118
+ <span className="font-medium">Oops!</span> {error.message?.toString()}
119
+ </p>
120
+ )}
121
+ {!error && hint && (
122
+ <p
123
+ className={`mt-2 italic text-sm text-${color}-500 dark:text-${color}-400`}
124
+ >
125
+ {hint}
126
+ </p>
127
+ )}
128
+ </div>
129
+ );
130
+ };
@@ -226,11 +226,8 @@ export const SelectField = (props: Props) => {
226
226
  handleOptionClick(filteredOptions[highlightedIndex]);
227
227
  } else if (e.key === "Backspace" && (value || valueMulti.length > 0)) {
228
228
  e.preventDefault();
229
- if (!isMulti) {
230
- handleClear();
231
- } else {
232
- setInputValue("");
233
- }
229
+ handleClear();
230
+ setInputValue("");
234
231
  setFilteredOptions(options);
235
232
  setIsOpen(true);
236
233
  }
package/src/Form/index.ts CHANGED
@@ -11,3 +11,4 @@ export * from "./TextField/index.js";
11
11
  export * from "./TimePicker/index.js";
12
12
  export * from "./YesNo/index.js";
13
13
  export * from "./ReactZodForm/index.js";
14
+ export * from "./RadioButton/index.js";
@@ -8,10 +8,12 @@ export const ValidateEmployeeAccess = ({
8
8
  children,
9
9
  permissionVariableName,
10
10
  showIfNoPermission = false,
11
+ noPermissionComponent,
11
12
  }: {
12
13
  children: React.ReactNode;
13
14
  permissionVariableName: string;
14
15
  showIfNoPermission?: boolean;
16
+ noPermissionComponent?: React.ReactNode;
15
17
  }) => {
16
18
  const { data: employee } = useGetEmployeeProfile();
17
19
  const variable_string = useGetSessionAttribute(
@@ -47,5 +49,9 @@ export const ValidateEmployeeAccess = ({
47
49
  );
48
50
  }
49
51
 
52
+ if (noPermissionComponent && !hasPermission) {
53
+ return <>{noPermissionComponent}</>;
54
+ }
55
+
50
56
  return <>{hasPermission ? children : null}</>;
51
57
  };
@@ -15,63 +15,75 @@ export const ErrorLayout = ({
15
15
  from?: string;
16
16
  error?: Error;
17
17
  }) => {
18
- const error = useRouteError();
19
- const [showDetails, setShowDetails] = useState(!!parentError);
18
+ try {
19
+ const error = useRouteError();
20
+ const [showDetails, setShowDetails] = useState(!!parentError);
20
21
 
21
- return (
22
- <html lang="es" className="bg-gray-900 text-white">
23
- <head>
24
- <meta charSet="utf-8" />
25
- <meta name="viewport" content="width=device-width, initial-scale=1" />
26
- <title>¡Ups! Algo salió mal</title>
27
- <Meta />
28
- <Links />
29
- </head>
30
- <body className="min-h-screen flex flex-col items-center justify-center p-4">
31
- <img src="/logo.png" alt="Zauru Logo" className="mb-8 h-20" />
32
- <h1 className="text-5xl font-extrabold text-red-500 mb-6">¡Ups!</h1>
33
- <div className="w-full max-w-2xl flex flex-col items-center">
34
- <p className="text-2xl text-gray-300 mb-8 text-center">
35
- {isRouteErrorResponse(error)
36
- ? `Error ${error.status}: ${error.statusText}`
37
- : error instanceof Error
38
- ? error.message
39
- : "Ha ocurrido un error inesperado"}
40
- </p>
41
- {from && (
42
- <p className="text-lg text-gray-400 mb-4 text-center">
43
- Error lanzado desde: {from}
22
+ return (
23
+ <html lang="es" className="bg-gray-900 text-white">
24
+ <head>
25
+ <meta charSet="utf-8" />
26
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
27
+ <title>¡Ups! Algo salió mal</title>
28
+ <Meta />
29
+ <Links />
30
+ </head>
31
+ <body className="min-h-screen flex flex-col items-center justify-center p-4">
32
+ <img src="/logo.png" alt="Zauru Logo" className="mb-8 h-20" />
33
+ <h1 className="text-5xl font-extrabold text-red-500 mb-6">¡Ups!</h1>
34
+ <div className="w-full max-w-2xl flex flex-col items-center">
35
+ <p className="text-2xl text-gray-300 mb-8 text-center">
36
+ {isRouteErrorResponse(error)
37
+ ? `Error ${error.status}: ${error.statusText}`
38
+ : error instanceof Error
39
+ ? error.message
40
+ : "Ha ocurrido un error inesperado"}
44
41
  </p>
45
- )}
46
- {parentError && (
47
- <div className="mb-4 text-center">
48
- <button
49
- onClick={() => setShowDetails(!showDetails)}
50
- className="text-blue-400 hover:text-blue-300 transition duration-300"
51
- >
52
- {showDetails ? "Ocultar detalles" : "Ver más detalles"}
53
- </button>
54
- {showDetails && (
55
- <p className="mt-2 text-gray-400 text-sm">
56
- {parentError instanceof Error
57
- ? parentError.message
58
- : String(parentError)}
59
- </p>
60
- )}
61
- </div>
62
- )}
63
- </div>
64
- <Link
65
- to="/"
66
- className="bg-blue-600 text-white py-3 px-8 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300 transform hover:scale-105"
67
- >
68
- Regresar al inicio
69
- </Link>
70
- <div className="mt-12 text-gray-500 text-center">
71
- <p>Si el problema persiste, por favor contacta a soporte.</p>
72
- </div>
73
- <Scripts />
74
- </body>
75
- </html>
76
- );
42
+ {from && (
43
+ <p className="text-lg text-gray-400 mb-4 text-center">
44
+ Error lanzado desde: {from}
45
+ </p>
46
+ )}
47
+ {parentError && (
48
+ <div className="mb-4 text-center">
49
+ <button
50
+ onClick={() => setShowDetails(!showDetails)}
51
+ className="text-blue-400 hover:text-blue-300 transition duration-300"
52
+ >
53
+ {showDetails ? "Ocultar detalles" : "Ver más detalles"}
54
+ </button>
55
+ {showDetails && (
56
+ <p className="mt-2 text-gray-400 text-sm">
57
+ {parentError instanceof Error
58
+ ? parentError.message
59
+ : String(parentError)}
60
+ </p>
61
+ )}
62
+ </div>
63
+ )}
64
+ </div>
65
+ <Link
66
+ to="/"
67
+ className="bg-blue-600 text-white py-3 px-8 rounded-full text-lg font-semibold hover:bg-blue-700 transition duration-300 transform hover:scale-105"
68
+ >
69
+ Regresar al inicio
70
+ </Link>
71
+ <div className="mt-12 text-gray-500 text-center">
72
+ <p>Si el problema persiste, por favor contacta a soporte.</p>
73
+ </div>
74
+ <Scripts />
75
+ </body>
76
+ </html>
77
+ );
78
+ } catch (error: any) {
79
+ console.error(error);
80
+ return (
81
+ <html lang="es" className="bg-gray-900 text-white">
82
+ <head>
83
+ <title>¡Ups! Algo salió mal</title>
84
+ </head>
85
+ <body>Error al renderizar el layout de error {error}</body>
86
+ </html>
87
+ );
88
+ }
77
89
  };
@@ -0,0 +1,186 @@
1
+ import React, { useState } from "react";
2
+ import { createRoot } from "react-dom/client";
3
+
4
+ // Tipos de datos
5
+ type Item = {
6
+ name: string;
7
+ code: string;
8
+ unitPrice: number;
9
+ stock: number;
10
+ currencyPrefix: string;
11
+ imageUrl: string;
12
+ };
13
+
14
+ type ItemCategory = { name: string; items: Item[] };
15
+
16
+ type ItemModalProps = {
17
+ itemList: ItemCategory[];
18
+ onClose: (selectedItem: Item | null) => void;
19
+ config?: { itemSize: { width: string; height: string } }; // Tamaño configurable
20
+ };
21
+
22
+ const ItemSelectionModal: React.FC<ItemModalProps> = ({
23
+ itemList,
24
+ onClose,
25
+ config,
26
+ }) => {
27
+ const defaultConfig = {
28
+ itemSize: {
29
+ width: config?.itemSize?.width || "150px",
30
+ height: config?.itemSize?.height || "200px",
31
+ },
32
+ };
33
+
34
+ const [searchTerm, setSearchTerm] = useState("");
35
+ const [expandedCategories, setExpandedCategories] = useState<{
36
+ [key: string]: boolean;
37
+ }>({});
38
+
39
+ const toggleCategory = (categoryName: string) => {
40
+ setExpandedCategories((prev) => ({
41
+ ...prev,
42
+ [categoryName]: !prev[categoryName],
43
+ }));
44
+ };
45
+
46
+ const handleItemClick = (item: Item) => {
47
+ onClose(item);
48
+ };
49
+
50
+ const filteredList = itemList
51
+ .map((category) => ({
52
+ ...category,
53
+ items: category.items.filter((item) =>
54
+ item.name.toLowerCase().includes(searchTerm.toLowerCase())
55
+ ),
56
+ }))
57
+ .filter(
58
+ (category) =>
59
+ category.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
60
+ category.items.length > 0
61
+ );
62
+
63
+ return (
64
+ <div className="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50">
65
+ <div className="bg-white p-4 rounded-lg shadow-lg w-11/12 max-w-4xl h-5/6 overflow-y-auto">
66
+ {/* Header */}
67
+ <div className="flex flex-col mb-4">
68
+ <div className="flex justify-between items-center mb-4">
69
+ <h2 className="text-2xl font-bold">Seleccionar un Ítem</h2>
70
+ <button
71
+ className="text-gray-500 hover:text-gray-800"
72
+ onClick={() => onClose(null)}
73
+ >
74
+ &times;
75
+ </button>
76
+ </div>
77
+ {/* Barra de búsqueda */}
78
+ <input
79
+ type="text"
80
+ placeholder="Buscar por nombre o categoría..."
81
+ value={searchTerm}
82
+ onChange={(e) => setSearchTerm(e.target.value)}
83
+ className="p-2 border rounded-lg w-full"
84
+ />
85
+ </div>
86
+
87
+ {/* Lista de categorías e ítems */}
88
+ {filteredList.length === 0 ? (
89
+ <p className="text-gray-500 text-center">
90
+ No se encontraron resultados.
91
+ </p>
92
+ ) : (
93
+ filteredList.map((category) => (
94
+ <div key={category.name} className="mb-4">
95
+ <h3
96
+ className="text-lg font-semibold mb-2 cursor-pointer flex justify-between items-center hover:text-blue-600"
97
+ onClick={() => toggleCategory(category.name)}
98
+ >
99
+ <span>
100
+ {category.name}{" "}
101
+ <span className="text-gray-500">
102
+ ({category.items.length})
103
+ </span>
104
+ </span>
105
+ <span
106
+ className={`transform transition-transform duration-200 ${
107
+ expandedCategories[category.name] ? "rotate-90" : ""
108
+ }`}
109
+ >
110
+
111
+ </span>
112
+ </h3>
113
+ {expandedCategories[category.name] && (
114
+ <div
115
+ className="grid gap-5"
116
+ style={{
117
+ gridTemplateColumns: `repeat(auto-fit, minmax(${defaultConfig.itemSize.width}, 1fr))`,
118
+ }}
119
+ >
120
+ {category.items.map((item) => (
121
+ <div
122
+ key={item.code}
123
+ className="border rounded-lg shadow-lg hover:shadow-xl cursor-pointer relative"
124
+ onClick={() => handleItemClick(item)}
125
+ style={{
126
+ backgroundImage: `url(${item.imageUrl})`,
127
+ backgroundSize: "cover",
128
+ backgroundPosition: "center",
129
+ width: defaultConfig.itemSize.width,
130
+ height: defaultConfig.itemSize.height,
131
+ }}
132
+ >
133
+ <div className="bg-white bg-opacity-80 p-2 rounded absolute top-0 left-0 right-0">
134
+ <h4 className="font-bold text-sm text-center">
135
+ {item.name}
136
+ </h4>
137
+ </div>
138
+ <div className="absolute bottom-0 left-0 bg-white bg-opacity-80 p-2 rounded">
139
+ <span className="text-xs">Stock: {item.stock}</span>
140
+ </div>
141
+ <div className="absolute bottom-0 right-0 bg-white bg-opacity-80 p-2 rounded">
142
+ <span className="text-xs">
143
+ {item.currencyPrefix}
144
+ {item.unitPrice}
145
+ </span>
146
+ </div>
147
+ </div>
148
+ ))}
149
+ </div>
150
+ )}
151
+ </div>
152
+ ))
153
+ )}
154
+ </div>
155
+ </div>
156
+ );
157
+ };
158
+
159
+ // Función para crear el modal
160
+ export const createItemModal = (
161
+ itemList: ItemCategory[],
162
+ config?: ItemModalProps["config"]
163
+ ): Promise<Item | null> => {
164
+ return new Promise((resolve) => {
165
+ const handleClose = (selectedItem: Item | null) => {
166
+ resolve(selectedItem);
167
+ removeModal();
168
+ };
169
+
170
+ const removeModal = () => {
171
+ document.body.removeChild(modalWrapper);
172
+ };
173
+
174
+ const modalWrapper = document.createElement("div");
175
+ document.body.appendChild(modalWrapper);
176
+
177
+ const root = createRoot(modalWrapper);
178
+ root.render(
179
+ <ItemSelectionModal
180
+ itemList={itemList}
181
+ onClose={handleClose}
182
+ config={config}
183
+ />
184
+ );
185
+ });
186
+ };
@@ -1 +1,2 @@
1
1
  export * from "./Modal.js";
2
+ export * from "./ItemModal.js";
@@ -98,28 +98,15 @@ export const NavBar = ({
98
98
  }: NavBarProps) => {
99
99
  const color: ColorInterface = COLORS[selectedColor];
100
100
  const [NavBarOpen, setNavBarOpen] = useState(false);
101
- const [isDarkMode, setIsDarkMode] = useState(false);
102
101
  const [currentVersion, setCurrentVersion] = useState(version);
103
102
  const navigate = useNavigate();
104
103
 
105
- useEffect(() => {
106
- if (isDarkMode) {
107
- document.documentElement.classList.add("dark");
108
- } else {
109
- document.documentElement.classList.remove("dark");
110
- }
111
- }, [isDarkMode]);
112
-
113
104
  useEffect(() => {
114
105
  if (version !== currentVersion) {
115
106
  setCurrentVersion(version);
116
107
  }
117
108
  }, [version, currentVersion]);
118
109
 
119
- const toggleDarkMode = () => {
120
- setIsDarkMode(!isDarkMode);
121
- };
122
-
123
110
  const refreshPage = () => {
124
111
  navigate(0);
125
112
  };
@@ -167,15 +154,6 @@ export const NavBar = ({
167
154
  <span>Cerrar sesión</span>
168
155
  </div>
169
156
  </Link>,
170
- <button
171
- className={`block w-full text-left px-4 py-3 text-sm text-gray-600 capitalize transition-colors duration-200 transform dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 dark:hover:text-white`}
172
- onClick={toggleDarkMode}
173
- >
174
- <div className="mx-auto pt-2">
175
- {isDarkMode ? "🌞" : "🌙"}
176
- <span>{isDarkMode ? "Modo Claro" : "Modo Oscuro"}</span>
177
- </div>
178
- </button>,
179
157
  ]}
180
158
  />
181
159
  )}
@@ -12,11 +12,11 @@ interface LoadingWindowProps {
12
12
  }
13
13
 
14
14
  export const LoadingWindow: React.FC<LoadingWindowProps> = ({
15
- loadingItems,
15
+ loadingItems = [],
16
16
  description = "Por favor, espere mientras se carga la página.",
17
17
  }) => {
18
18
  return (
19
- <div className="min-h-screen bg-gray-100 flex flex-col justify-center items-center">
19
+ <div className="min-h-screen bg-gray-200 flex flex-col justify-center items-center">
20
20
  <motion.div
21
21
  initial={{ opacity: 0, y: -20 }}
22
22
  animate={{ opacity: 1, y: 0 }}