├── .env.template ├── .npmrc ├── src ├── modules │ ├── app │ │ ├── pages │ │ │ ├── index.ts │ │ │ └── AppPage.tsx │ │ └── layouts │ │ │ ├── index.ts │ │ │ └── AppLayout.tsx │ ├── auth │ │ ├── layouts │ │ │ ├── index.ts │ │ │ └── AuthLayout.tsx │ │ └── pages │ │ │ ├── index.ts │ │ │ ├── LoginPage.tsx │ │ │ └── RegisterPage.tsx │ ├── brand │ │ ├── pages │ │ │ ├── index.ts │ │ │ └── BrandPage.tsx │ │ ├── components │ │ │ ├── index.ts │ │ │ └── TableBrand.tsx │ │ ├── services │ │ │ ├── index.ts │ │ │ └── brands.service.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── brand.interface.ts │ │ └── hooks │ │ │ ├── index.ts │ │ │ ├── useQueryBrand.ts │ │ │ └── useTableBrand.ts │ ├── shared │ │ ├── components │ │ │ ├── index.ts │ │ │ └── Loader.tsx │ │ ├── utils │ │ │ ├── index.ts │ │ │ └── capitalize.ts │ │ ├── providers │ │ │ ├── index.ts │ │ │ └── Providers.tsx │ │ ├── constants │ │ │ ├── index.ts │ │ │ └── initial-states.ts │ │ ├── config │ │ │ ├── env.config.ts │ │ │ ├── index.ts │ │ │ └── axios.config.ts │ │ └── routes │ │ │ └── index.ts │ ├── units │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useQueryUnit.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ └── units.service.ts │ │ └── interfaces │ │ │ ├── index.ts │ │ │ └── unit.interface.ts │ ├── categories │ │ ├── pages │ │ │ ├── index.ts │ │ │ └── CategoryPage.tsx │ │ ├── schemas │ │ │ ├── index.ts │ │ │ └── category.schema.ts │ │ ├── services │ │ │ ├── index.ts │ │ │ └── categories.service.ts │ │ ├── interfaces │ │ │ ├── index.ts │ │ │ └── category.interface.ts │ │ ├── components │ │ │ ├── index.ts │ │ │ ├── CustomModal.tsx │ │ │ └── TableCategory.tsx │ │ └── hooks │ │ │ ├── index.ts │ │ │ ├── useQueryCategory.ts │ │ │ └── useTableCategory.ts │ └── products │ │ ├── page │ │ ├── index.ts │ │ └── ProductsPage.tsx │ │ ├── schemas │ │ ├── index.ts │ │ └── ProductSchema.ts │ │ ├── services │ │ ├── index.ts │ │ └── product.service.ts │ │ ├── interfaces │ │ ├── index.ts │ │ └── product.interface.ts │ │ ├── components │ │ ├── index.ts │ │ ├── TableProduct.tsx │ │ └── ModalProduct.tsx │ │ └── hooks │ │ ├── index.ts │ │ ├── useQueryProduct.ts │ │ └── useTableProduct.ts ├── vite-env.d.ts ├── main.tsx ├── index.css ├── router.tsx └── assets │ └── react.svg ├── .prettierrc ├── postcss.config.js ├── tsconfig.node.json ├── .gitignore ├── tailwind.config.js ├── index.html ├── .eslintrc.cjs ├── vite.config.ts ├── tsconfig.json ├── public └── vite.svg ├── package.json └── README.md /.env.template: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL= -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | public-hoist-pattern[]=*@nextui-org/* -------------------------------------------------------------------------------- /src/modules/app/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppPage'; 2 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/modules/app/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AppLayout'; 2 | -------------------------------------------------------------------------------- /src/modules/auth/layouts/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AuthLayout'; 2 | -------------------------------------------------------------------------------- /src/modules/brand/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BrandPage'; 2 | -------------------------------------------------------------------------------- /src/modules/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Loader'; 2 | -------------------------------------------------------------------------------- /src/modules/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './capitalize'; 2 | -------------------------------------------------------------------------------- /src/modules/units/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useQueryUnit'; 2 | -------------------------------------------------------------------------------- /src/modules/brand/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './TableBrand'; 2 | -------------------------------------------------------------------------------- /src/modules/brand/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './brands.service'; 2 | -------------------------------------------------------------------------------- /src/modules/categories/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CategoryPage'; 2 | -------------------------------------------------------------------------------- /src/modules/products/page/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProductsPage'; 2 | -------------------------------------------------------------------------------- /src/modules/shared/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Providers'; 2 | -------------------------------------------------------------------------------- /src/modules/units/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './units.service'; 2 | -------------------------------------------------------------------------------- /src/modules/brand/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './brand.interface'; 2 | -------------------------------------------------------------------------------- /src/modules/categories/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './category.schema'; 2 | -------------------------------------------------------------------------------- /src/modules/products/schemas/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ProductSchema'; 2 | -------------------------------------------------------------------------------- /src/modules/products/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './product.service'; 2 | -------------------------------------------------------------------------------- /src/modules/shared/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './initial-states'; 2 | -------------------------------------------------------------------------------- /src/modules/units/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './unit.interface'; 2 | -------------------------------------------------------------------------------- /src/modules/categories/services/index.ts: -------------------------------------------------------------------------------- 1 | export * from './categories.service'; 2 | -------------------------------------------------------------------------------- /src/modules/products/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './product.interface'; 2 | -------------------------------------------------------------------------------- /src/modules/categories/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './category.interface'; 2 | -------------------------------------------------------------------------------- /src/modules/auth/pages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './LoginPage'; 2 | export * from './RegisterPage'; 3 | -------------------------------------------------------------------------------- /src/modules/shared/config/env.config.ts: -------------------------------------------------------------------------------- 1 | export const { VITE_BASE_URL: BASE_URL } = import.meta.env; 2 | -------------------------------------------------------------------------------- /src/modules/app/pages/AppPage.tsx: -------------------------------------------------------------------------------- 1 | export const AppPage = () => { 2 | return
AppPage
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/modules/brand/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useQueryBrand'; 2 | export * from './useTableBrand'; 3 | -------------------------------------------------------------------------------- /src/modules/shared/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './axios.config'; 2 | export * from './env.config'; 3 | -------------------------------------------------------------------------------- /src/modules/products/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ModalProduct'; 2 | export * from './TableProduct'; 3 | -------------------------------------------------------------------------------- /src/modules/products/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useQueryProduct'; 2 | export * from './useTableProduct'; 3 | -------------------------------------------------------------------------------- /src/modules/auth/pages/LoginPage.tsx: -------------------------------------------------------------------------------- 1 | export const LoginPage = () => { 2 | return
LoginPage
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/modules/categories/components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CustomModal'; 2 | export * from './TableCategory'; 3 | -------------------------------------------------------------------------------- /src/modules/categories/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './useQueryCategory'; 2 | export * from './useTableCategory'; 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-tailwindcss"], 3 | "singleQuote": true, 4 | "printWidth": 90 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /src/modules/auth/layouts/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | export const AuthLayout = () => { 2 | return
AuthLayout
; 3 | }; 4 | -------------------------------------------------------------------------------- /src/modules/shared/utils/capitalize.ts: -------------------------------------------------------------------------------- 1 | export const capitalize = (str: string) => { 2 | return str.charAt(0).toUpperCase() + str.slice(1); 3 | }; 4 | -------------------------------------------------------------------------------- /src/modules/auth/pages/RegisterPage.tsx: -------------------------------------------------------------------------------- 1 | export const RegisterPage = () => { 2 | return ( 3 |
4 |

RegisterPage

5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/shared/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | export const Loader = () => { 2 | return ( 3 |
4 |
5 |
6 | ); 7 | }; 8 | -------------------------------------------------------------------------------- /src/modules/brand/interfaces/brand.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Brand { 2 | id?: number; 3 | nombre: string; 4 | descripcion: string; 5 | created_at?: string; 6 | updated_at?: string; 7 | deleted_at?: string; 8 | actions?: string; 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/brand/pages/BrandPage.tsx: -------------------------------------------------------------------------------- 1 | import { TableBrand } from '@/brand/components'; 2 | 3 | export const BrandPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/shared/config/axios.config.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { BASE_URL } from './env.config'; 3 | 4 | export const http = axios.create({ 5 | baseURL: BASE_URL, 6 | headers: { 7 | 'Content-Type': 'application/json', 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /src/modules/products/page/ProductsPage.tsx: -------------------------------------------------------------------------------- 1 | import { TableProduct } from '@/products/components'; 2 | 3 | export const ProductsPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /src/modules/categories/pages/CategoryPage.tsx: -------------------------------------------------------------------------------- 1 | import { TableCategory } from '@/categories/components'; 2 | 3 | export const CategoryPage = () => { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true, 8 | "strict": true 9 | }, 10 | "include": ["vite.config.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /src/modules/units/interfaces/unit.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Unit { 2 | id?: number; 3 | nombre: string; 4 | estado: Estado; 5 | created_at?: Date; 6 | updated_at?: Date; 7 | actions?: string; 8 | } 9 | 10 | export enum Estado { 11 | Activo = 'activo', 12 | Inactivo = 'inactivo', 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/units/hooks/useQueryUnit.ts: -------------------------------------------------------------------------------- 1 | import { UnitsService } from '@/units/services'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | 4 | export const useQueryUnits = () => { 5 | const getUnits = useQuery({ 6 | queryKey: ['units'], 7 | queryFn: UnitsService.getUnits, 8 | }); 9 | 10 | return { getUnits }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/brand/hooks/useQueryBrand.ts: -------------------------------------------------------------------------------- 1 | import { BrandsService } from '@/brand/services'; 2 | import { useQuery } from '@tanstack/react-query'; 3 | 4 | export const useQueryBrands = () => { 5 | const getBrands = useQuery({ 6 | queryKey: ['brands'], 7 | queryFn: BrandsService.getBrands, 8 | }); 9 | 10 | return { getBrands }; 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/categories/interfaces/category.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Category { 2 | id?: number; 3 | nombre: string; 4 | descripcion: string; 5 | created_at?: string; 6 | updated_at?: string; 7 | actions?: string; 8 | } 9 | 10 | // TODO: Verificar estructura de respuesta a las mutaciones 11 | export interface CategoryResponse { 12 | mensaje: string; 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | todo.md 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | pnpm-debug.log* 10 | lerna-debug.log* 11 | 12 | node_modules 13 | dist 14 | dist-ssr 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | .idea 21 | .DS_Store 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | -------------------------------------------------------------------------------- /src/main.tsx: -------------------------------------------------------------------------------- 1 | import ReactDOM from 'react-dom/client'; 2 | 3 | import { Providers } from '@/shared/providers'; 4 | import { RouterProvider } from 'react-router-dom'; 5 | import { AppRouter } from './router'; 6 | 7 | import './index.css'; 8 | 9 | ReactDOM.createRoot(document.getElementById('root')!).render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | 3 | import { nextui } from '@nextui-org/react'; 4 | 5 | export default { 6 | content: [ 7 | './index.html', 8 | './src/**/*.{ts,tsx}', 9 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}', 10 | ], 11 | theme: { 12 | extend: {}, 13 | }, 14 | darkMode: 'class', 15 | plugins: [nextui()], 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/categories/schemas/category.schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const CategorySchema = z.object({ 4 | id: z.number().optional(), 5 | nombre: z.string().min(3, 'El nombre debe tener al menos 3 caracteres'), 6 | descripcion: z 7 | .string() 8 | .min(3, 'La descripción debe tener al menos 3 caracteres'), 9 | }); 10 | 11 | export type CategoryForm = z.infer; 12 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Punto de Venta 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/modules/shared/constants/initial-states.ts: -------------------------------------------------------------------------------- 1 | import { CategoryForm } from '@/categories/schemas'; 2 | import { ProductForm } from '@/products/schemas'; 3 | 4 | export const initialProduct: ProductForm = { 5 | nombre: '', 6 | codigo: '', 7 | precio: 0, 8 | stock: 0, 9 | stock_minimo: 0, 10 | categoria_id: 0, 11 | marca_id: 0, 12 | unidad_id: 0, 13 | descripcion: '', 14 | }; 15 | 16 | export const initialCategory: CategoryForm = { 17 | nombre: '', 18 | descripcion: '', 19 | }; 20 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | ], 9 | ignorePatterns: ['dist', '.eslintrc.cjs'], 10 | parser: '@typescript-eslint/parser', 11 | plugins: ['react-refresh'], 12 | rules: { 13 | 'react-refresh/only-export-components': [ 14 | 'warn', 15 | { allowConstantExport: true }, 16 | ], 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/modules/units/services/units.service.ts: -------------------------------------------------------------------------------- 1 | import { http } from '@/shared/config'; 2 | import { isAxiosError } from 'axios'; 3 | import { Unit } from '@/units/interfaces'; 4 | 5 | export class UnitsService { 6 | static getUnits = async (): Promise => { 7 | try { 8 | const { data } = await http('/unidades'); 9 | return data; 10 | } catch (error) { 11 | if (isAxiosError(error)) { 12 | throw new Error(error.response?.data.message); 13 | } 14 | throw new Error('Ocurrió un error al obtener las unidades'); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/brand/services/brands.service.ts: -------------------------------------------------------------------------------- 1 | import { Brand } from '@/brand/interfaces'; 2 | import { http } from '@/shared/config'; 3 | import { isAxiosError } from 'axios'; 4 | 5 | export class BrandsService { 6 | static getBrands = async (): Promise => { 7 | try { 8 | const { data } = await http('/marcas'); 9 | return data; 10 | } catch (error) { 11 | if (isAxiosError(error)) { 12 | throw new Error(error.response?.data.message); 13 | } 14 | throw new Error('Ocurrió un error al obtener las marcas'); 15 | } 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /src/modules/shared/providers/Providers.tsx: -------------------------------------------------------------------------------- 1 | import { NextUIProvider } from '@nextui-org/react'; 2 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 3 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; 4 | import { PropsWithChildren } from 'react'; 5 | 6 | const queryClient = new QueryClient(); 7 | 8 | export const Providers = ({ children }: PropsWithChildren) => { 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .spinner { 6 | width: 56px; 7 | height: 56px; 8 | display: grid; 9 | border: 4px solid #0000; 10 | border-radius: 50%; 11 | border-right-color: #004dff; 12 | animation: spinner-a4dj62 1s infinite linear; 13 | } 14 | 15 | .spinner::before, 16 | .spinner::after { 17 | content: ""; 18 | grid-area: 1/1; 19 | margin: 2px; 20 | border: inherit; 21 | border-radius: 50%; 22 | animation: spinner-a4dj62 2s infinite; 23 | } 24 | 25 | .spinner::after { 26 | margin: 8px; 27 | animation-duration: 3s; 28 | } 29 | 30 | @keyframes spinner-a4dj62 { 31 | 100% { 32 | transform: rotate(1turn); 33 | } 34 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import react from '@vitejs/plugin-react-swc'; 2 | import path from 'path'; 3 | import { defineConfig } from 'vite'; 4 | 5 | export default defineConfig({ 6 | plugins: [react()], 7 | resolve: { 8 | alias: { 9 | '@/app': path.resolve(__dirname, './src/modules/app'), 10 | '@/auth': path.resolve(__dirname, './src/modules/auth'), 11 | '@/brand': path.resolve(__dirname, './src/modules/brand'), 12 | '@/categories': path.resolve(__dirname, './src/modules/categories'), 13 | '@/products': path.resolve(__dirname, './src/modules/products'), 14 | '@/shared': path.resolve(__dirname, './src/modules/shared'), 15 | '@/units': path.resolve(__dirname, './src/modules/units'), 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/modules/products/interfaces/product.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Product { 2 | id?: number; 3 | nombre: string; 4 | codigo: string; 5 | descripcion?: string; 6 | precio: number; 7 | stock: number; 8 | stock_minimo: number; 9 | categoria_id: number; 10 | marca_id: number; 11 | unidad_id: number; 12 | imagen?: string; 13 | created_at?: string; 14 | updated_at?: string; 15 | fecha_vencimiento?: Date; 16 | estado: State; 17 | categoria: Category; 18 | marca: Category; 19 | unidad: Category; 20 | actions?: string; 21 | } 22 | export interface ProductResponse { 23 | mensaje: string; 24 | } 25 | 26 | interface Category { 27 | id: number; 28 | nombre: string; 29 | } 30 | 31 | enum State { 32 | Activo = 'activo', 33 | Inactivo = 'inactivo', 34 | } 35 | -------------------------------------------------------------------------------- /src/router.tsx: -------------------------------------------------------------------------------- 1 | import { AppLayout } from '@/app/layouts'; 2 | import { AuthLayout } from '@/auth/layouts'; 3 | import { LoginPage, RegisterPage } from '@/auth/pages'; 4 | import { routes } from '@/shared/routes'; 5 | import { createBrowserRouter } from 'react-router-dom'; 6 | 7 | export const AppRouter = createBrowserRouter([ 8 | { 9 | path: '/', 10 | element: , 11 | children: [ 12 | ...routes.map(({ element: Element, path }) => ({ 13 | element: , 14 | path: path, 15 | })), 16 | ], 17 | }, 18 | { 19 | path: '/auth', 20 | element: , 21 | children: [ 22 | { 23 | index: true, 24 | path: 'login', 25 | element: , 26 | }, 27 | { 28 | path: 'register', 29 | element: , 30 | }, 31 | ], 32 | }, 33 | ]); 34 | -------------------------------------------------------------------------------- /src/modules/shared/routes/index.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from '@/app/pages'; 2 | import { BrandPage } from '@/brand/pages'; 3 | import { CategoryPage } from '@/categories/pages'; 4 | import { ProductsPage } from '@/products/page'; 5 | import { CiBoxes } from 'react-icons/ci'; 6 | import { HiOutlineHome } from 'react-icons/hi'; 7 | import { MdOutlineCategory } from 'react-icons/md'; 8 | import { TbBrandUbuntu } from 'react-icons/tb'; 9 | 10 | export const routes = [ 11 | { 12 | path: '/', 13 | name: 'Inicio', 14 | icon: HiOutlineHome, 15 | element: AppPage, 16 | }, 17 | { 18 | path: '/categorias', 19 | name: 'Categorias', 20 | icon: MdOutlineCategory, 21 | element: CategoryPage, 22 | }, 23 | { 24 | path: '/productos', 25 | name: 'Productos', 26 | icon: CiBoxes, 27 | element: ProductsPage, 28 | }, 29 | { 30 | path: '/marcas', 31 | name: 'Marcas', 32 | icon: TbBrandUbuntu, 33 | element: BrandPage, 34 | }, 35 | ]; 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "paths": { 9 | "@/auth/*": ["./src/modules/auth/*"], 10 | "@/app/*": ["./src/modules/app/*"], 11 | "@/brand/*": ["./src/modules/brand/*"], 12 | "@/categories/*": ["./src/modules/categories/*"], 13 | "@/products/*": ["./src/modules/products/*"], 14 | "@/shared/*": ["./src/modules/shared/*"], 15 | "@/units/*": ["./src/modules/units/*"] 16 | }, 17 | /* Bundler mode */ 18 | "moduleResolution": "bundler", 19 | "allowImportingTsExtensions": true, 20 | "resolveJsonModule": true, 21 | "isolatedModules": true, 22 | "noEmit": true, 23 | "jsx": "react-jsx", 24 | /* Linting */ 25 | "strict": true, 26 | "noUnusedLocals": true, 27 | "noUnusedParameters": true, 28 | "noFallthroughCasesInSwitch": true 29 | }, 30 | "include": ["src"], 31 | "references": [ 32 | { 33 | "path": "./tsconfig.node.json" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/modules/products/schemas/ProductSchema.ts: -------------------------------------------------------------------------------- 1 | import { z } from 'zod'; 2 | 3 | export const ProductSchema = z.object({ 4 | id: z.number().optional(), 5 | nombre: z.string().min(3, 'El nombre del producto debe tener al menos 3 caracteres'), 6 | descripcion: z.string().optional(), 7 | codigo: z.string().min(3, 'El código del producto debe tener al menos 1 caracter'), 8 | precio: z.coerce 9 | .number() 10 | .positive('Debe ser mayor a 0') 11 | .min(1, 'El precio del producto debe ser mayor a 0'), 12 | stock: z.coerce 13 | .number() 14 | .int('Debe ser un número entero') 15 | .positive('Debe ser mayor a 0') 16 | .min(1, 'El stock mínimo del producto debe ser mayor a 0'), 17 | stock_minimo: z.coerce 18 | .number() 19 | .int('Debe ser un número entero') 20 | .positive('Debe ser mayor a 0') 21 | .min(1, 'El stock mínimo es requerida'), 22 | categoria_id: z.coerce.number().min(1, 'La categoria es requerida'), 23 | marca_id: z.coerce.number().min(1, 'La marca es requerida'), 24 | unidad_id: z.coerce.number().min(1, 'La unidad es requerida'), 25 | }); 26 | 27 | export type ProductForm = z.infer; 28 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pos", 3 | "private": true, 4 | "version": "0.0.1", 5 | "type": "module", 6 | "author": { 7 | "name": "Maycol Rodriguez", 8 | "email": "maycol.rodriguez.ma@gmail.com" 9 | }, 10 | "scripts": { 11 | "dev": "vite", 12 | "build": "tsc && vite build", 13 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 14 | "preview": "vite preview", 15 | "format": "prettier --write ." 16 | }, 17 | "dependencies": { 18 | "@hookform/resolvers": "3.3.4", 19 | "@nextui-org/react": "2.2.10", 20 | "@tanstack/react-query": "5.29.2", 21 | "@tanstack/react-query-devtools": "5.29.2", 22 | "axios": "1.6.8", 23 | "clsx": "2.1.0", 24 | "framer-motion": "11.0.28", 25 | "react": "18.2.0", 26 | "react-dom": "18.2.0", 27 | "react-hook-form": "7.51.3", 28 | "react-icons": "5.0.1", 29 | "react-router-dom": "6.22.3", 30 | "sonner": "1.4.41", 31 | "zod": "3.22.4", 32 | "zustand": "4.5.2" 33 | }, 34 | "devDependencies": { 35 | "@types/node": "20.12.5", 36 | "@types/react": "18.2.66", 37 | "@types/react-dom": "18.2.22", 38 | "@typescript-eslint/eslint-plugin": "7.2.0", 39 | "@typescript-eslint/parser": "7.2.0", 40 | "@vitejs/plugin-react-swc": "3.5.0", 41 | "autoprefixer": "10.4.19", 42 | "eslint": "8.57.0", 43 | "eslint-plugin-react-hooks": "4.6.0", 44 | "eslint-plugin-react-refresh": "0.4.6", 45 | "postcss": "8.4.38", 46 | "prettier": "3.2.5", 47 | "prettier-plugin-tailwindcss": "0.5.13", 48 | "tailwindcss": "3.4.3", 49 | "typescript": "5.4.5", 50 | "vite": "5.2.8" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/modules/products/services/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Product, ProductResponse } from '@/products/interfaces'; 2 | import { ProductForm } from '@/products/schemas'; 3 | import { http } from '@/shared/config'; 4 | import { isAxiosError } from 'axios'; 5 | 6 | export class ProductsService { 7 | static getProducts = async (): Promise => { 8 | try { 9 | const { data } = await http('/productos'); 10 | return data; 11 | } catch (error) { 12 | if (isAxiosError(error)) { 13 | throw new Error(error.response?.data.message); 14 | } 15 | throw new Error('Ocurrió un error al obtener las categorías'); 16 | } 17 | }; 18 | 19 | static createProduct = async (product: ProductForm): Promise => { 20 | try { 21 | const { data } = await http.post('/productos', product); 22 | return data.mensaje; 23 | } catch (error) { 24 | if (isAxiosError(error)) { 25 | throw new Error(error.response?.data.message); 26 | } 27 | throw new Error('Ocurrió un error al crear el producto'); 28 | } 29 | }; 30 | 31 | static deleteProduct = async (id: number): Promise => { 32 | try { 33 | const { data } = await http.delete(`/productos/${id}`); 34 | return data.mensaje; 35 | } catch (error) { 36 | if (isAxiosError(error)) { 37 | throw new Error(error.response?.data.message); 38 | } 39 | throw new Error('Ocurrió un error al eliminar el producto'); 40 | } 41 | } 42 | 43 | static updateProduct = async (id: number, product: ProductForm): Promise => { 44 | try { 45 | const { data } = await http.put(`/productos/${id}`, product); 46 | return data.mensaje; 47 | } catch (error) { 48 | if (isAxiosError(error)) { 49 | throw new Error(error.response?.data.message); 50 | } 51 | throw new Error('Ocurrió un error al actualizar el producto'); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/modules/categories/services/categories.service.ts: -------------------------------------------------------------------------------- 1 | import { Category, CategoryResponse } from '@/categories/interfaces'; 2 | import { CategoryForm } from '@/categories/schemas'; 3 | import { http } from '@/shared/config'; 4 | import { isAxiosError } from 'axios'; 5 | 6 | export class CategoriesService { 7 | static getCategories = async (): Promise => { 8 | try { 9 | const { data } = await http('/categorias'); 10 | return data; 11 | } catch (error) { 12 | if (isAxiosError(error)) { 13 | throw new Error(error.response?.data.message); 14 | } 15 | throw new Error('Ocurrió un error al obtener las categorías'); 16 | } 17 | }; 18 | 19 | static createCategory = async (category: CategoryForm): Promise => { 20 | try { 21 | const { data } = await http.post( 22 | '/categorias', 23 | category, 24 | ); 25 | return data.mensaje; 26 | } catch (error) { 27 | if (isAxiosError(error)) { 28 | throw new Error(error.response?.data.message); 29 | } 30 | throw new Error('Ocurrió un error al crear la categoría'); 31 | } 32 | }; 33 | 34 | static deleteCategory = async (id: number): Promise => { 35 | try { 36 | const { data } = await http.delete(`/categorias/${id}`); 37 | return data.mensaje; 38 | } catch (error) { 39 | if (isAxiosError(error)) { 40 | throw new Error(error.response?.data.message); 41 | } 42 | throw new Error('Ocurrió un error al eliminar la categoría'); 43 | } 44 | }; 45 | 46 | static updateCategory = async ( 47 | id: number, 48 | category: CategoryForm, 49 | ): Promise => { 50 | try { 51 | const { data } = await http.put( 52 | `/categorias/${id}`, 53 | category, 54 | ); 55 | return data.mensaje; 56 | } catch (error) { 57 | if (isAxiosError(error)) { 58 | throw new Error(error.response?.data.message); 59 | } 60 | throw new Error('Ocurrió un error al actualizar la categoría'); 61 | } 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/modules/products/hooks/useQueryProduct.ts: -------------------------------------------------------------------------------- 1 | import { ProductForm } from '@/products/schemas'; 2 | import { ProductsService } from '@/products/services'; 3 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 4 | import { toast } from 'sonner'; 5 | 6 | export const useQueryProducts = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | const getProducts = useQuery({ 10 | queryKey: ['products'], 11 | queryFn: ProductsService.getProducts, 12 | }); 13 | 14 | const postProduct = useMutation({ 15 | mutationFn: (product: ProductForm) => 16 | ProductsService.createProduct(product), 17 | onMutate: async () => { 18 | toast.loading('Creando producto...'); 19 | }, 20 | onSuccess: (data) => { 21 | queryClient.invalidateQueries({ queryKey: ['products'] }); 22 | toast.success(data); 23 | }, 24 | onError: (error) => { 25 | toast.error(error.message); 26 | }, 27 | onSettled: () => { 28 | toast.dismiss(); 29 | }, 30 | }); 31 | 32 | const deleteProduct = useMutation({ 33 | mutationFn: (id: number) => ProductsService.deleteProduct(id), 34 | onMutate: async () => { 35 | toast.loading('Eliminando producto...'); 36 | }, 37 | onSuccess: (data) => { 38 | queryClient.invalidateQueries({ queryKey: ['products'] }); 39 | toast.success(data); 40 | }, 41 | onError: (error) => { 42 | toast.error(error.message); 43 | }, 44 | onSettled: () => { 45 | toast.dismiss(); 46 | }, 47 | }); 48 | 49 | const updateProduct = useMutation({ 50 | mutationFn: (data: { id: number; product: ProductForm }) => 51 | ProductsService.updateProduct(data.id, data.product), 52 | onMutate: async () => { 53 | toast.loading('Actualizando producto...'); 54 | }, 55 | onSuccess: (data) => { 56 | queryClient.invalidateQueries({ queryKey: ['products'] }); 57 | toast.success(data); 58 | }, 59 | onError: (error) => { 60 | toast.error(error.message); 61 | }, 62 | onSettled: () => { 63 | toast.dismiss(); 64 | }, 65 | }); 66 | 67 | return { getProducts, postProduct, deleteProduct, updateProduct }; 68 | }; 69 | -------------------------------------------------------------------------------- /src/modules/categories/hooks/useQueryCategory.ts: -------------------------------------------------------------------------------- 1 | import { CategoryForm } from '@/categories/schemas'; 2 | import { CategoriesService } from '@/categories/services'; 3 | import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; 4 | import { toast } from 'sonner'; 5 | 6 | export const useQueryCategories = () => { 7 | const queryClient = useQueryClient(); 8 | 9 | const getCategories = useQuery({ 10 | queryKey: ['categories'], 11 | queryFn: CategoriesService.getCategories, 12 | }); 13 | 14 | const postCategory = useMutation({ 15 | mutationFn: (categoryForm: CategoryForm) => 16 | CategoriesService.createCategory(categoryForm), 17 | onMutate: async () => { 18 | toast.loading('Creando categoria...'); 19 | }, 20 | onSuccess: (data) => { 21 | queryClient.invalidateQueries({ queryKey: ['categories'] }); 22 | toast.success(data); 23 | }, 24 | onError: (error) => { 25 | console.log(error); 26 | toast.error(error.message); 27 | }, 28 | onSettled: () => { 29 | toast.dismiss(); 30 | }, 31 | }); 32 | 33 | const deleteCategory = useMutation({ 34 | mutationFn: (id: number) => CategoriesService.deleteCategory(id), 35 | onMutate: async () => { 36 | toast.loading('Eliminando categoria...'); 37 | }, 38 | onSuccess: (data) => { 39 | queryClient.invalidateQueries({ queryKey: ['categories'] }); 40 | toast.success(data); 41 | }, 42 | onError: (error) => { 43 | toast.error(error.message); 44 | }, 45 | onSettled: () => { 46 | toast.dismiss(); 47 | }, 48 | }); 49 | 50 | const updateCategory = useMutation({ 51 | mutationFn: (data: { id: number; category: CategoryForm }) => 52 | CategoriesService.updateCategory(data.id, data.category), 53 | onMutate: async () => { 54 | toast.loading('Actualizando categoria...'); 55 | }, 56 | onSuccess: (data) => { 57 | queryClient.invalidateQueries({ queryKey: ['categories'] }); 58 | toast.success(data); 59 | }, 60 | onError: (error) => { 61 | toast.error(error.message); 62 | }, 63 | onSettled: () => { 64 | toast.dismiss(); 65 | }, 66 | }); 67 | 68 | return { getCategories, postCategory, deleteCategory, updateCategory }; 69 | }; 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sistema de Punto de Venta (POS) - React POS 2 | 3 | Este proyecto consiste en un sistema de punto de venta (POS) diseñado específicamente para uso interno. Se ha elegido React como la tecnología principal para su desarrollo debido a su flexibilidad, eficiencia y amplio ecosistema de herramientas y librerías. La decisión de no usar Next.js se basa en la naturaleza interna de la aplicación, donde la renderización del lado del servidor y las funcionalidades relacionadas con SEO no son críticas. 4 | 5 | ## Objetivo 6 | 7 | El objetivo principal de este sistema es proporcionar una solución robusta, ágil y fácil de usar para la gestión de ventas, inventario y clientes dentro de una organización. Busca optimizar los procesos internos, mejorar la experiencia de los usuarios y proporcionar datos valiosos para la toma de decisiones. 8 | 9 | ## Tecnologías y Librerías 10 | 11 | El proyecto está construido utilizando las siguientes tecnologías y librerías: 12 | 13 | - **React**: Para la construcción de la interfaz de usuario. 14 | - **TypeScript**: Para la tipificación estática y la mejora de la calidad del código. 15 | - **ESLint**: Para el linting y formateo del código. 16 | - **NextUI**: Como librería de componentes. 17 | - **Vite**: Como herramienta de construcción y desarrollo local. 18 | - **React Router Dom**: Para la gestión de rutas. 19 | - **TanStack Query**: Para la gestión de consultas. 20 | - **Axios**: Para las solicitudes HTTP. 21 | - **Sonner**: Para notificaciones. 22 | - **Zustand**: Para el manejo del estado global. 23 | - **React Hook Form**: Para la gestión de formularios. 24 | - **React Icons**: Para iconos. 25 | - **TailwindCSS**: Para el diseño y estilos. 26 | - **Framer Motion**: Para animaciones. 27 | - Además, se utilizan otras librerías para validación de formularios, manejo de consultas, y más, detalladas en el archivo `package.json` del proyecto. 28 | 29 | ## Metodología 30 | 31 | El desarrollo del proyecto sigue una metodología ágil, centrada en la entrega continua de valor, con iteraciones cortas y feedback frecuente. Se enfatiza la colaboración, la adaptabilidad y la mejora continua. 32 | 33 | ## Contribuciones 34 | 35 | Las contribuciones son bienvenidas. Si estás interesado en contribuir al proyecto, por favor revisa las siguientes directrices: 36 | 37 | 1. **Reporte de Bugs**: Si encuentras un error, por favor crea un issue detallando el problema. 38 | 2. **Sugerencias de Mejoras**: Si tienes ideas para mejorar el sistema, siéntete libre de abrir un issue para discutirlas. 39 | 3. **Pull Requests**: Si deseas contribuir directamente con código, asegúrate de seguir las convenciones de código establecidas y de realizar pruebas adecuadas. 40 | 41 | ## Formato de Contribuciones 42 | 43 | Para mantener la calidad y coherencia del código, por favor sigue estas pautas al contribuir: 44 | 45 | - **Estilo de Código**: Asegúrate de seguir el estilo de código definido por Prettier y ESLint. Puedes verificar tu código ejecutando `pnpm run lint` y formatearlo con `pnpm run format`. 46 | - **Documentación**: Actualiza la documentación relevante si estás añadiendo o cambiando funcionalidades. 47 | 48 | ## Levantar el Proyecto en Local 49 | 50 | Para levantar el proyecto en tu entorno local, sigue estos pasos: 51 | 52 | 1. Clona el repositorio a tu máquina local. 53 | 2. Instala las dependencias del proyecto ejecutando `pnpm install` en la raíz del directorio del proyecto. 54 | 3. Inicia el servidor de desarrollo ejecutando `pnpm run dev`. Esto abrirá el proyecto en tu navegador predeterminado. 55 | 4. Para construir el proyecto para producción, puedes ejecutar `pnpm run build`. 56 | 57 | Si encuentras algún problema al levantar el proyecto, no dudes en crear un issue en el repositorio. 58 | 59 | ## Autor 60 | 61 | - Maycol Rodriguez - [maycol.rodriguez.ma@gmail.com](mailto:maycol.rodriguez.ma@gmail.com) 62 | -------------------------------------------------------------------------------- /src/modules/brand/hooks/useTableBrand.ts: -------------------------------------------------------------------------------- 1 | import { useQueryBrands } from '@/brand/hooks'; 2 | import { Brand } from '@/brand/interfaces'; 3 | import { Selection, SortDescriptor } from '@nextui-org/react'; 4 | import { ChangeEvent, useCallback, useMemo, useState } from 'react'; 5 | 6 | export const columnsTable = [ 7 | { name: 'ID', uid: 'id', sortable: true }, 8 | { name: 'NOMBRE', uid: 'nombre', sortable: true }, 9 | { name: 'DESCRIPCION', uid: 'descripcion', sortable: true }, 10 | { name: 'ACTIONS', uid: 'actions' }, 11 | ]; 12 | 13 | const INITIAL_VISIBLE_COLUMNS = ['id', 'nombre', 'descripcion', 'actions']; 14 | 15 | export const useTableBrand = () => { 16 | const { getBrands } = useQueryBrands(); 17 | const { data: brands = [], isLoading } = getBrands; 18 | 19 | const [filterValue, setFilterValue] = useState(''); 20 | const [selectedKeys, setSelectedKeys] = useState(new Set([])); 21 | const [rowsPerPage, setRowsPerPage] = useState(5); 22 | const [page, setPage] = useState(1); 23 | 24 | const hasSearchFilter = Boolean(filterValue); 25 | 26 | const [visibleColumns, setVisibleColumns] = useState( 27 | new Set(INITIAL_VISIBLE_COLUMNS), 28 | ); 29 | 30 | const [sortDescriptor, setSortDescriptor] = useState({ 31 | column: 'data', 32 | direction: 'ascending', 33 | }); 34 | 35 | const headerColumns = useMemo(() => { 36 | if (visibleColumns === 'all') return columnsTable; 37 | return columnsTable.filter((column) => 38 | Array.from(visibleColumns).includes(column.uid), 39 | ); 40 | }, [visibleColumns]); 41 | 42 | const filteredItems = useMemo(() => { 43 | let filteredBrands = [...brands]; 44 | if (hasSearchFilter) { 45 | filteredBrands = filteredBrands.filter((brand) => 46 | brand.nombre.toLowerCase().includes(filterValue.toLowerCase()), 47 | ); 48 | } 49 | return filteredBrands; 50 | }, [brands, hasSearchFilter, filterValue]); 51 | 52 | const pages = Math.ceil(filteredItems.length / rowsPerPage); 53 | 54 | const items = useMemo(() => { 55 | const start = (page - 1) * rowsPerPage; 56 | const end = start + rowsPerPage; 57 | return filteredItems.slice(start, end); 58 | }, [page, filteredItems, rowsPerPage]); 59 | 60 | const sortedItems = useMemo(() => { 61 | return [...items].sort((a: Brand, b: Brand) => { 62 | const first = a[sortDescriptor.column as keyof Brand] as number; 63 | const second = b[sortDescriptor.column as keyof Brand] as number; 64 | const cmp = first < second ? -1 : first > second ? 1 : 0; 65 | return sortDescriptor.direction === 'descending' ? -cmp : cmp; 66 | }); 67 | }, [sortDescriptor, items]); 68 | 69 | const onRowsPerPageChange = useCallback( 70 | (e: ChangeEvent) => { 71 | if (!e.target.value) return; 72 | setRowsPerPage(Number(e.target.value)); 73 | setPage(1); 74 | }, 75 | [], 76 | ); 77 | 78 | const onSearchChange = useCallback((value?: string) => { 79 | if (value) { 80 | setFilterValue(value); 81 | setPage(1); 82 | } else { 83 | setFilterValue(''); 84 | } 85 | }, []); 86 | 87 | const onClear = useCallback(() => { 88 | setFilterValue(''); 89 | setPage(1); 90 | }, []); 91 | 92 | return { 93 | brands, 94 | isLoading, 95 | visibleColumns, 96 | onRowsPerPageChange, 97 | onSearchChange, 98 | onClear, 99 | selectedKeys, 100 | setSelectedKeys, 101 | headerColumns, 102 | sortDescriptor, 103 | setSortDescriptor, 104 | page, 105 | setPage, 106 | pages, 107 | filterValue, 108 | setVisibleColumns, 109 | filteredItems, 110 | sortedItems, 111 | columnsTable, 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /src/modules/categories/hooks/useTableCategory.ts: -------------------------------------------------------------------------------- 1 | import { useQueryCategories } from '@/categories/hooks'; 2 | import { Category } from '@/categories/interfaces'; 3 | import { Selection, SortDescriptor } from '@nextui-org/react'; 4 | import { ChangeEvent, useCallback, useMemo, useState } from 'react'; 5 | 6 | export const columnsTable = [ 7 | { name: 'ID', uid: 'id', sortable: true }, 8 | { name: 'NOMBRE', uid: 'nombre', sortable: true }, 9 | { name: 'DESCRIPCION', uid: 'descripcion', sortable: true }, 10 | { name: 'ACTIONS', uid: 'actions' }, 11 | ]; 12 | 13 | const INITIAL_VISIBLE_COLUMNS = ['id', 'nombre', 'descripcion', 'actions']; 14 | 15 | export const useTableCategory = () => { 16 | const { getCategories } = useQueryCategories(); 17 | const { data: categories = [], isLoading } = getCategories; 18 | 19 | const [filterValue, setFilterValue] = useState(''); 20 | const [selectedKeys, setSelectedKeys] = useState(new Set([])); 21 | const [rowsPerPage, setRowsPerPage] = useState(5); 22 | const [page, setPage] = useState(1); 23 | 24 | const hasSearchFilter = Boolean(filterValue); 25 | 26 | const [visibleColumns, setVisibleColumns] = useState( 27 | new Set(INITIAL_VISIBLE_COLUMNS), 28 | ); 29 | 30 | const [sortDescriptor, setSortDescriptor] = useState({ 31 | column: 'data', 32 | direction: 'ascending', 33 | }); 34 | 35 | const headerColumns = useMemo(() => { 36 | if (visibleColumns === 'all') return columnsTable; 37 | return columnsTable.filter((column) => 38 | Array.from(visibleColumns).includes(column.uid), 39 | ); 40 | }, [visibleColumns]); 41 | 42 | const filteredItems = useMemo(() => { 43 | let filteredCategories = [...categories]; 44 | if (hasSearchFilter) { 45 | filteredCategories = filteredCategories.filter((category) => 46 | category.nombre.toLowerCase().includes(filterValue.toLowerCase()), 47 | ); 48 | } 49 | return filteredCategories; 50 | }, [categories, hasSearchFilter, filterValue]); 51 | 52 | const pages = Math.ceil(filteredItems.length / rowsPerPage); 53 | 54 | const items = useMemo(() => { 55 | const start = (page - 1) * rowsPerPage; 56 | const end = start + rowsPerPage; 57 | return filteredItems.slice(start, end); 58 | }, [page, filteredItems, rowsPerPage]); 59 | 60 | const sortedItems = useMemo(() => { 61 | return [...items].sort((a: Category, b: Category) => { 62 | const first = a[sortDescriptor.column as keyof Category] as number; 63 | const second = b[sortDescriptor.column as keyof Category] as number; 64 | const cmp = first < second ? -1 : first > second ? 1 : 0; 65 | return sortDescriptor.direction === 'descending' ? -cmp : cmp; 66 | }); 67 | }, [sortDescriptor, items]); 68 | 69 | const onRowsPerPageChange = useCallback( 70 | (e: ChangeEvent) => { 71 | if (!e.target.value) return; 72 | setRowsPerPage(Number(e.target.value)); 73 | setPage(1); 74 | }, 75 | [], 76 | ); 77 | 78 | const onSearchChange = useCallback((value?: string) => { 79 | if (value) { 80 | setFilterValue(value); 81 | setPage(1); 82 | } else { 83 | setFilterValue(''); 84 | } 85 | }, []); 86 | 87 | const onClear = useCallback(() => { 88 | setFilterValue(''); 89 | setPage(1); 90 | }, []); 91 | 92 | return { 93 | categories, 94 | isLoading, 95 | visibleColumns, 96 | onRowsPerPageChange, 97 | onSearchChange, 98 | onClear, 99 | selectedKeys, 100 | setSelectedKeys, 101 | headerColumns, 102 | sortDescriptor, 103 | setSortDescriptor, 104 | page, 105 | setPage, 106 | pages, 107 | filterValue, 108 | setVisibleColumns, 109 | filteredItems, 110 | sortedItems, 111 | columnsTable, 112 | }; 113 | }; 114 | -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/modules/categories/components/CustomModal.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Input, 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalFooter, 8 | ModalHeader, 9 | } from '@nextui-org/react'; 10 | 11 | import { useQueryCategories } from '@/categories/hooks'; 12 | import { CategoryForm, CategorySchema } from '@/categories/schemas'; 13 | import { initialCategory } from '@/shared/constants'; 14 | import { zodResolver } from '@hookform/resolvers/zod'; 15 | import { useCallback, useEffect } from 'react'; 16 | import { Controller, SubmitHandler, useForm } from 'react-hook-form'; 17 | 18 | export const CustomModal = ({ 19 | category, 20 | isOpen, 21 | setCategory, 22 | onClose, 23 | onOpenChange, 24 | }: Props) => { 25 | const { postCategory, updateCategory } = useQueryCategories(); 26 | 27 | const { 28 | control, 29 | reset, 30 | handleSubmit, 31 | formState: { errors }, 32 | } = useForm({ 33 | resolver: zodResolver(CategorySchema), 34 | defaultValues: initialCategory, 35 | }); 36 | 37 | useEffect(() => { 38 | if (category && category.id) { 39 | reset({ 40 | id: category.id, 41 | nombre: category.nombre, 42 | descripcion: category.descripcion, 43 | }); 44 | } else { 45 | reset(initialCategory); 46 | } 47 | }, [category, reset]); 48 | 49 | const formSubmit: SubmitHandler = useCallback( 50 | async (data) => { 51 | if (data.id) { 52 | updateCategory.mutate({ id: data.id, category: data }); 53 | } else { 54 | postCategory.mutate(data); 55 | } 56 | reset(); 57 | onClose(); 58 | setCategory(initialCategory); 59 | }, 60 | [onClose, postCategory, reset, setCategory, updateCategory], 61 | ); 62 | 63 | return ( 64 | { 69 | setCategory(initialCategory); 70 | }} 71 | classNames={{ 72 | backdrop: 73 | 'bg-gradient-to-t from-zinc-900 to-zinc-900/10 backdrop-opacity-20', 74 | }} 75 | > 76 | 77 | {() => ( 78 |
82 | 83 | {category.id ? 'Editar Categoria' : 'Crear Categoria'} 84 | 85 | 86 | ( 90 | 101 | )} 102 | /> 103 | 104 | ( 108 | 118 | )} 119 | /> 120 | 121 | 122 | 125 | 126 |
127 | )} 128 |
129 |
130 | ); 131 | }; 132 | 133 | interface Props { 134 | isOpen: boolean; 135 | onOpenChange: () => void; 136 | category: CategoryForm; 137 | onClose: () => void; 138 | setCategory: (category: CategoryForm) => void; 139 | } 140 | -------------------------------------------------------------------------------- /src/modules/products/hooks/useTableProduct.ts: -------------------------------------------------------------------------------- 1 | import { useQueryProducts } from '@/products/hooks'; 2 | import { Product } from '@/products/interfaces'; 3 | import { Selection, SortDescriptor } from '@nextui-org/react'; 4 | import { ChangeEvent, useCallback, useMemo, useState } from 'react'; 5 | 6 | export const columnsTable = [ 7 | { name: 'ID', uid: 'id', sortable: true }, 8 | { name: 'CODIGO', uid: 'codigo', sortable: true }, 9 | { name: 'PRECIO', uid: 'precio', sortable: true }, 10 | { name: 'NOMBRE', uid: 'nombre', sortable: true }, 11 | { name: 'STOCK', uid: 'stock', sortable: true }, 12 | { name: 'STOCK MINIMO', uid: 'stock_minimo', sortable: true }, 13 | { name: 'DESCRIPCION', uid: 'descripcion', sortable: true }, 14 | { name: 'CATEGORIA', uid: 'categoria', sortable: true }, 15 | { name: 'MARCA', uid: 'marca', sortable: true }, 16 | { name: 'UNIDAD', uid: 'unidad', sortable: true }, 17 | { name: 'ACTIONS', uid: 'actions' }, 18 | ]; 19 | 20 | const INITIAL_VISIBLE_COLUMNS = [ 21 | 'id', 22 | 'codigo', 23 | 'precio', 24 | 'nombre', 25 | 'stock', 26 | 'stock_minimo', 27 | 'descripcion', 28 | 'categoria', 29 | 'marca', 30 | 'unidad', 31 | 'actions', 32 | ]; 33 | 34 | export const useTableProducts = () => { 35 | const { getProducts } = useQueryProducts(); 36 | const { data: products = [], isLoading } = getProducts; 37 | 38 | const [filterValue, setFilterValue] = useState(''); 39 | const [selectedKeys, setSelectedKeys] = useState(new Set([])); 40 | const [rowsPerPage, setRowsPerPage] = useState(5); 41 | const [page, setPage] = useState(1); 42 | 43 | const hasSearchFilter = Boolean(filterValue); 44 | 45 | const [visibleColumns, setVisibleColumns] = useState( 46 | new Set(INITIAL_VISIBLE_COLUMNS), 47 | ); 48 | 49 | const [sortDescriptor, setSortDescriptor] = useState({ 50 | column: 'data', 51 | direction: 'ascending', 52 | }); 53 | 54 | const headerColumns = useMemo(() => { 55 | if (visibleColumns === 'all') return columnsTable; 56 | return columnsTable.filter((column) => 57 | Array.from(visibleColumns).includes(column.uid), 58 | ); 59 | }, [visibleColumns]); 60 | 61 | const filteredItems = useMemo(() => { 62 | let filteredProducts = [...products]; 63 | if (hasSearchFilter) { 64 | filteredProducts = filteredProducts.filter((product) => 65 | product.nombre.toLowerCase().includes(filterValue.toLowerCase()), 66 | ); 67 | } 68 | return filteredProducts; 69 | }, [products, hasSearchFilter, filterValue]); 70 | 71 | const pages = Math.ceil(filteredItems.length / rowsPerPage); 72 | 73 | const items = useMemo(() => { 74 | const start = (page - 1) * rowsPerPage; 75 | const end = start + rowsPerPage; 76 | return filteredItems.slice(start, end); 77 | }, [page, filteredItems, rowsPerPage]); 78 | 79 | const sortedItems = useMemo(() => { 80 | const getAttributeValue = (product: Product) => { 81 | switch (sortDescriptor.column) { 82 | case 'categoria': 83 | return product.categoria?.nombre; 84 | case 'marca': 85 | return product.marca?.nombre; 86 | case 'unidad': 87 | return product.unidad?.nombre; 88 | default: 89 | return product[sortDescriptor.column as keyof Product]; 90 | } 91 | }; 92 | 93 | return [...items].sort((a: Product, b: Product) => { 94 | const first = getAttributeValue(a); 95 | const second = getAttributeValue(b); 96 | let cmp = 0; 97 | 98 | if (first === undefined || second === undefined) { 99 | return first === undefined ? 1 : -1; 100 | } 101 | 102 | if (typeof first === 'string' && typeof second === 'string') { 103 | cmp = first.localeCompare(second); // Usamos localeCompare para la comparación de cadenas 104 | } else { 105 | cmp = first < second ? -1 : first > second ? 1 : 0; 106 | } 107 | 108 | return sortDescriptor.direction === 'descending' ? -cmp : cmp; 109 | }); 110 | }, [sortDescriptor, items]); 111 | 112 | const onRowsPerPageChange = useCallback( 113 | (e: ChangeEvent) => { 114 | if (!e.target.value) return; 115 | setRowsPerPage(Number(e.target.value)); 116 | setPage(1); 117 | }, 118 | [], 119 | ); 120 | 121 | const onSearchChange = useCallback((value?: string) => { 122 | if (value) { 123 | setFilterValue(value); 124 | setPage(1); 125 | } else { 126 | setFilterValue(''); 127 | } 128 | }, []); 129 | 130 | const onClear = useCallback(() => { 131 | setFilterValue(''); 132 | setPage(1); 133 | }, []); 134 | 135 | return { 136 | products, 137 | isLoading, 138 | visibleColumns, 139 | onRowsPerPageChange, 140 | onSearchChange, 141 | onClear, 142 | selectedKeys, 143 | setSelectedKeys, 144 | headerColumns, 145 | sortDescriptor, 146 | setSortDescriptor, 147 | page, 148 | setPage, 149 | pages, 150 | filterValue, 151 | setVisibleColumns, 152 | filteredItems, 153 | sortedItems, 154 | columnsTable, 155 | }; 156 | }; 157 | -------------------------------------------------------------------------------- /src/modules/brand/components/TableBrand.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 | } from '@nextui-org/react'; 16 | 17 | import { IoAdd, IoChevronDown, IoEllipsisVertical, IoSearch } from 'react-icons/io5'; 18 | 19 | import { useTableBrand } from '@/brand/hooks'; 20 | import { Brand } from '@/brand/interfaces'; 21 | import { Loader } from '@/shared/components'; 22 | import { capitalize } from '@/shared/utils'; 23 | import { Key, useCallback, useMemo } from 'react'; 24 | 25 | export const TableBrand = () => { 26 | const { 27 | brands, 28 | columnsTable, 29 | filteredItems, 30 | filterValue, 31 | headerColumns, 32 | isLoading, 33 | onClear, 34 | onRowsPerPageChange, 35 | onSearchChange, 36 | page, 37 | pages, 38 | selectedKeys, 39 | setPage, 40 | setSelectedKeys, 41 | setSortDescriptor, 42 | setVisibleColumns, 43 | sortDescriptor, 44 | sortedItems, 45 | visibleColumns, 46 | } = useTableBrand(); 47 | 48 | const renderCell = useCallback((brand: Brand, columnKey: Key) => { 49 | const cellValue = brand[columnKey as keyof Brand]; 50 | 51 | switch (columnKey) { 52 | case 'nombre': 53 | return {brand.nombre}; 54 | case 'descripcion': 55 | return brand.descripcion; 56 | case 'actions': 57 | return ( 58 |
59 | 60 | 61 | 64 | 65 | 66 | console.log('Ver', brand.id)}> 67 | ver 68 | 69 | Editar 70 | Eliminar 71 | 72 | 73 |
74 | ); 75 | default: 76 | return cellValue; 77 | } 78 | }, []); 79 | 80 | const topContent = useMemo(() => { 81 | return ( 82 |
83 |
84 | } 89 | value={filterValue} 90 | onClear={() => onClear()} 91 | onValueChange={onSearchChange} 92 | /> 93 |
94 | 95 | 96 | 102 | 103 | 111 | {columnsTable.map((column) => ( 112 | 113 | {capitalize(column.name)} 114 | 115 | ))} 116 | 117 | 118 | 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 |
156 | 157 | {selectedKeys === 'all' 158 | ? 'Todos las marcas seleccionados' 159 | : `${selectedKeys.size} de ${filteredItems.length} seleccionados`} 160 | 161 | 170 |
171 | ); 172 | }, [selectedKeys, filteredItems.length, page, pages, setPage]); 173 | 174 | if (isLoading) return ; 175 | 176 | return ( 177 | 195 | 196 | {(column) => ( 197 | 202 | {column.name} 203 | 204 | )} 205 | 206 | 207 | {(item) => ( 208 | 209 | {(columnKey) => { 210 | return {renderCell(item, columnKey)}; 211 | }} 212 | 213 | )} 214 | 215 |
216 | ); 217 | }; 218 | -------------------------------------------------------------------------------- /src/modules/categories/components/TableCategory.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 { CustomModal } from '@/categories/components'; 21 | import { useQueryCategories, useTableCategory } from '@/categories/hooks'; 22 | import { Category } from '@/categories/interfaces'; 23 | import { CategoryForm } from '@/categories/schemas'; 24 | import { Loader } from '@/shared/components'; 25 | import { initialCategory } from '@/shared/constants'; 26 | import { capitalize } from '@/shared/utils'; 27 | import { Key, useCallback, useMemo, useState } from 'react'; 28 | 29 | export const TableCategory = () => { 30 | const [category, setCategory] = useState(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 | 133 | 134 | 142 | {columnsTable.map((column) => ( 143 | 144 | {capitalize(column.name)} 145 | 146 | ))} 147 | 148 | 149 | 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 | 148 | 149 | 157 | {columnsTable.map((column) => ( 158 | 159 | {capitalize(column.name)} 160 | 161 | ))} 162 | 163 | 164 | 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 |
101 | 102 | {product.id ? 'Editar Producto' : 'Crear Producto'} 103 | 104 | 105 | ( 109 | 122 | )} 123 | /> 124 | 125 | ( 129 | 141 | )} 142 | /> 143 | 144 |
145 | ( 149 | 162 | )} 163 | /> 164 | 165 | ( 169 | 182 | )} 183 | /> 184 |
185 | 186 |
187 | ( 191 | 204 | )} 205 | /> 206 | 207 | ( 211 | 249 | )} 250 | /> 251 |
252 | 253 |
254 | ( 258 | 294 | )} 295 | /> 296 | 297 | ( 301 | 337 | )} 338 | /> 339 |
340 | 341 | ( 345 | 356 | )} 357 | /> 358 |
359 | 360 | 363 | 364 |
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 | --------------------------------------------------------------------------------