83 |
84 |
}
89 | value={filterValue}
90 | onClear={() => onClear()}
91 | onValueChange={onSearchChange}
92 | />
93 |
94 |
95 |
96 | }
98 | variant="flat"
99 | >
100 | Columns
101 |
102 |
103 |
111 | {columnsTable.map((column) => (
112 |
113 | {capitalize(column.name)}
114 |
115 | ))}
116 |
117 |
118 | }>
119 | Crear Nueva
120 |
121 |
122 |
123 |
124 |
125 | Total {brands.length} marcas
126 |
127 |
128 |
139 |
140 |
141 | );
142 | }, [
143 | filterValue,
144 | onSearchChange,
145 | visibleColumns,
146 | setVisibleColumns,
147 | columnsTable,
148 | brands.length,
149 | onRowsPerPageChange,
150 | onClear,
151 | ]);
152 |
153 | const bottomContent = useMemo(() => {
154 | return (
155 | (initialCategory);
31 |
32 | const { deleteCategory } = useQueryCategories();
33 | const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
34 |
35 | const {
36 | categories,
37 | columnsTable,
38 | filteredItems,
39 | filterValue,
40 | headerColumns,
41 | isLoading,
42 | onClear,
43 | onRowsPerPageChange,
44 | onSearchChange,
45 | page,
46 | pages,
47 | selectedKeys,
48 | setPage,
49 | setSelectedKeys,
50 | setSortDescriptor,
51 | setVisibleColumns,
52 | sortDescriptor,
53 | sortedItems,
54 | visibleColumns,
55 | } = useTableCategory();
56 |
57 | const renderCell = useCallback(
58 | (category: Category, columnKey: Key) => {
59 | const cellValue = category[columnKey as keyof Category];
60 |
61 | switch (columnKey) {
62 | case 'nombre':
63 | return category.nombre;
64 | case 'descripcion':
65 | return category.descripcion;
66 | case 'actions':
67 | return (
68 |
69 |
70 |
71 |
74 |
75 |
76 | console.log('Ver', category.id)}>
77 | ver
78 |
79 | {
81 | if (!category.id) return;
82 |
83 | setCategory({
84 | id: category.id,
85 | nombre: category.nombre,
86 | descripcion: category.descripcion,
87 | });
88 |
89 | onOpen();
90 | }}
91 | >
92 | Editar
93 |
94 | category.id && deleteCategory.mutate(category.id)}
96 | >
97 | Eliminar
98 |
99 |
100 |
101 |
102 | );
103 | default:
104 | return cellValue;
105 | }
106 | },
107 | [deleteCategory, onOpen, setCategory],
108 | );
109 |
110 | const topContent = useMemo(() => {
111 | return (
112 | <>
113 |
114 |
115 |
}
120 | value={filterValue}
121 | onClear={() => onClear()}
122 | onValueChange={onSearchChange}
123 | />
124 |
125 |
126 |
127 | }
129 | variant="flat"
130 | >
131 | Columns
132 |
133 |
134 |
142 | {columnsTable.map((column) => (
143 |
144 | {capitalize(column.name)}
145 |
146 | ))}
147 |
148 |
149 | }>
150 | Crear Nueva
151 |
152 | {isOpen && (
153 |
160 | )}
161 |
162 |
163 |
164 |
165 | Total {categories.length} categorias
166 |
167 |
168 |
179 |
180 |
181 | >
182 | );
183 | }, [
184 | categories.length,
185 | category,
186 | columnsTable,
187 | filterValue,
188 | isOpen,
189 | onClear,
190 | onClose,
191 | onOpen,
192 | onOpenChange,
193 | onRowsPerPageChange,
194 | onSearchChange,
195 | setVisibleColumns,
196 | visibleColumns,
197 | ]);
198 |
199 | const bottomContent = useMemo(() => {
200 | return (
201 |
202 |
203 | {selectedKeys === 'all'
204 | ? 'Todas las categorias seleccionadas'
205 | : `${selectedKeys.size} de ${filteredItems.length} seleccionados`}
206 |
207 |
217 |
218 | );
219 | }, [selectedKeys, filteredItems.length, page, pages, setPage]);
220 |
221 | if (isLoading) return ;
222 |
223 | return (
224 |
242 |
243 | {(column) => (
244 |
249 | {column.name}
250 |
251 | )}
252 |
253 |
254 | {(item) => (
255 |
256 | {(columnKey) => {renderCell(item, columnKey)}}
257 |
258 | )}
259 |
260 |
261 | );
262 | };
263 |
--------------------------------------------------------------------------------
/src/modules/products/components/TableProduct.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Dropdown,
4 | DropdownItem,
5 | DropdownMenu,
6 | DropdownTrigger,
7 | Input,
8 | Pagination,
9 | Table,
10 | TableBody,
11 | TableCell,
12 | TableColumn,
13 | TableHeader,
14 | TableRow,
15 | useDisclosure,
16 | } from '@nextui-org/react';
17 |
18 | import { IoAdd, IoChevronDown, IoEllipsisVertical, IoSearch } from 'react-icons/io5';
19 |
20 | import { ModalProduct } from '@/products/components';
21 | import { useQueryProducts, useTableProducts } from '@/products/hooks';
22 | import { Product } from '@/products/interfaces';
23 | import { ProductForm } from '@/products/schemas';
24 | import { Loader } from '@/shared/components';
25 | import { initialProduct } from '@/shared/constants';
26 | import { capitalize } from '@/shared/utils';
27 | import { Key, useCallback, useMemo, useState } from 'react';
28 |
29 | export const TableProduct = () => {
30 | const {
31 | products,
32 | columnsTable,
33 | filteredItems,
34 | filterValue,
35 | headerColumns,
36 | isLoading,
37 | onClear,
38 | onRowsPerPageChange,
39 | onSearchChange,
40 | page,
41 | pages,
42 | selectedKeys,
43 | setPage,
44 | setSelectedKeys,
45 | setSortDescriptor,
46 | setVisibleColumns,
47 | sortDescriptor,
48 | sortedItems,
49 | visibleColumns,
50 | } = useTableProducts();
51 |
52 | const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
53 |
54 | const [product, setProduct] = useState(initialProduct);
55 |
56 | const { deleteProduct } = useQueryProducts();
57 |
58 | const renderCell = useCallback(
59 | (product: Product, columnKey: Key) => {
60 | const cellValue = product[columnKey as keyof Product];
61 |
62 | switch (columnKey) {
63 | case 'nombre':
64 | return product.nombre;
65 | case 'descripcion':
66 | return product.descripcion;
67 | case 'fecha_vencimiento':
68 | return product.fecha_vencimiento?.toLocaleDateString();
69 | case 'categoria':
70 | return product.categoria.nombre;
71 | case 'marca':
72 | return product.marca.nombre;
73 | case 'unidad':
74 | return product.unidad.nombre; // Asumiendo que el valor nd
75 | case 'actions':
76 | return (
77 |
78 |
79 |
80 |
83 |
84 |
85 | console.log('Ver', product.id)}>
86 | ver
87 |
88 | {
90 | if (!product.id) return;
91 |
92 | setProduct({
93 | id: product.id,
94 | nombre: product.nombre,
95 | codigo: product.codigo,
96 | descripcion: product.descripcion,
97 | precio: product.precio,
98 | stock: product.stock,
99 | stock_minimo: product.stock_minimo,
100 | categoria_id: product.categoria_id,
101 | marca_id: product.marca_id,
102 | unidad_id: product.unidad_id,
103 | });
104 |
105 | onOpen();
106 | }}
107 | >
108 | Editar
109 |
110 | product.id && deleteProduct.mutate(product.id)}
112 | >
113 | Eliminar
114 |
115 |
116 |
117 |
118 | );
119 | default:
120 | return cellValue?.toString();
121 | }
122 | },
123 | [deleteProduct, onOpen, setProduct],
124 | );
125 |
126 | const topContent = useMemo(() => {
127 | return (
128 |
129 |
130 |
}
135 | value={filterValue}
136 | onClear={() => onClear()}
137 | onValueChange={onSearchChange}
138 | />
139 |
140 |
141 |
142 | }
144 | variant="flat"
145 | >
146 | Columns
147 |
148 |
149 |
157 | {columnsTable.map((column) => (
158 |
159 | {capitalize(column.name)}
160 |
161 | ))}
162 |
163 |
164 | }>
165 | Crear Nueva
166 |
167 | {isOpen && (
168 |
175 | )}
176 |
177 |
178 |
179 |
180 | Total {products.length} productos
181 |
182 |
183 |
194 |
195 |
196 | );
197 | }, [
198 | filterValue,
199 | onSearchChange,
200 | visibleColumns,
201 | setVisibleColumns,
202 | columnsTable,
203 | onOpen,
204 | isOpen,
205 | onOpenChange,
206 | product,
207 | onClose,
208 | products.length,
209 | onRowsPerPageChange,
210 | onClear,
211 | ]);
212 |
213 | const bottomContent = useMemo(() => {
214 | return (
215 |
216 |
217 | {selectedKeys === 'all'
218 | ? 'Todos las productos seleccionados'
219 | : `${selectedKeys.size} de ${filteredItems.length} seleccionados`}
220 |
221 |
231 |
232 | );
233 | }, [selectedKeys, filteredItems.length, page, pages, setPage]);
234 |
235 | if (isLoading) return ;
236 |
237 | return (
238 |
256 |
257 | {(column) => (
258 |
263 | {column.name}
264 |
265 | )}
266 |
267 |
268 | {(item) => (
269 |
270 | {(columnKey) => {
271 | return {renderCell(item, columnKey)};
272 | }}
273 |
274 | )}
275 |
276 |
277 | );
278 | };
279 |
--------------------------------------------------------------------------------
/src/modules/products/components/ModalProduct.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Input,
4 | Modal,
5 | ModalBody,
6 | ModalContent,
7 | ModalFooter,
8 | ModalHeader,
9 | Select,
10 | SelectItem,
11 | } from '@nextui-org/react';
12 |
13 | import { useQueryBrands } from '@/brand/hooks';
14 | import { useQueryCategories } from '@/categories/hooks';
15 | import { useQueryProducts } from '@/products/hooks';
16 | import { ProductForm, ProductSchema } from '@/products/schemas';
17 | import { initialProduct } from '@/shared/constants';
18 | import { useQueryUnits } from '@/units/hooks';
19 | import { zodResolver } from '@hookform/resolvers/zod';
20 | import { useCallback, useEffect } from 'react';
21 | import { Controller, SubmitHandler, useForm } from 'react-hook-form';
22 |
23 | export const ModalProduct = ({
24 | product,
25 | isOpen,
26 | setProduct,
27 | onClose,
28 | onOpenChange,
29 | }: Props) => {
30 | const { getUnits } = useQueryUnits();
31 | const { getBrands } = useQueryBrands();
32 | const { getCategories } = useQueryCategories();
33 | const { postProduct, updateProduct } = useQueryProducts();
34 |
35 | const { data: units = [] } = getUnits;
36 | const { data: brands = [] } = getBrands;
37 | const { data: categories = [] } = getCategories;
38 |
39 | const {
40 | control,
41 | reset,
42 | handleSubmit,
43 | formState: { errors },
44 | } = useForm({
45 | defaultValues: initialProduct,
46 | resolver: zodResolver(ProductSchema),
47 | });
48 |
49 | useEffect(() => {
50 | if (product && product.id) {
51 | reset({
52 | id: product.id,
53 | nombre: product.nombre,
54 | descripcion: product.descripcion,
55 | codigo: product.codigo,
56 | precio: product.precio,
57 | stock: product.stock,
58 | stock_minimo: product.stock_minimo,
59 | categoria_id: product.categoria_id,
60 | marca_id: product.marca_id,
61 | unidad_id: product.unidad_id,
62 | });
63 | } else {
64 | reset();
65 | }
66 | }, [product, reset]);
67 |
68 | const formSubmit: SubmitHandler = useCallback(
69 | async (data) => {
70 | if (data.id) {
71 | updateProduct.mutate({ id: data.id, product: data });
72 | } else {
73 | postProduct.mutate(data);
74 | }
75 | reset();
76 | onClose();
77 | setProduct(initialProduct);
78 | },
79 | [onClose, postProduct, reset, setProduct, updateProduct],
80 | );
81 |
82 | return (
83 | {
89 | setProduct(initialProduct);
90 | }}
91 | classNames={{
92 | backdrop: 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20',
93 | }}
94 | >
95 |
96 | {() => (
97 |
365 | )}
366 |
367 |
368 | );
369 | };
370 |
371 | interface Props {
372 | isOpen: boolean;
373 | onOpenChange: () => void;
374 | product: ProductForm;
375 | onClose: () => void;
376 | setProduct: (product: ProductForm) => void;
377 | }
378 |
--------------------------------------------------------------------------------
/src/modules/app/layouts/AppLayout.tsx:
--------------------------------------------------------------------------------
1 | import { routes } from '@/shared/routes';
2 | import { NavLink, Outlet, useLocation } from 'react-router-dom';
3 | import { Toaster } from 'sonner';
4 |
5 | export const AppLayout = () => {
6 | const { pathname } = useLocation();
7 |
8 | return (
9 |
10 |
112 |
113 |
243 |
244 |
245 |
254 |
255 |
256 |
257 | );
258 | };
259 |
--------------------------------------------------------------------------------