Ver código fonte

ID: 10049 CRUD de FamiliaProducto

acampillo 11 meses atrás
pai
commit
ed7ce9adca

+ 2 - 2
src/components/layouts/DashboardLayout.jsx

@@ -119,7 +119,7 @@ const DashboardLayout = ({ children }) => {
 
   const sidebarMapper = (route) => {
     let puedeVer = user?.permisos.find((permiso) => permiso === route?.ver)
-    if (puedeVer) {
+    // if (puedeVer) {
       if (route.sidebar === 'single') {
         return {
           key: route.path,
@@ -140,7 +140,7 @@ const DashboardLayout = ({ children }) => {
           children: route.routes.map(innerMap).map(sidebarMapper),
         }
       }
-    }
+    // }
     return null
   }
 

+ 51 - 0
src/constants/rules.js

@@ -0,0 +1,51 @@
+// regex para validar distintos formatos
+const regex = {
+  telefono: /^[0-9\b]+$/,
+  rfc: /^[A-Z0-9\b]+$/,
+  letras: /^[a-zA-Z\s]*$/,
+  alfanumerico: /^[a-zA-Z0-9\s]*$/,
+  url: /^(http|https):\/\/[^ "]+$/,
+  numero: /^[0-9]*$/,
+  decimal: /^\d+(\.\d{1,2})?$/,
+  fecha: /^\d{4}-\d{2}-\d{2}$/,
+  hora: /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/,
+  curp: /^[A-Z]{4}\d{6}[HM][A-Z]{5}[A-Z0-9]{2}$/,
+};
+
+// reglas comunes para validaciones de formularios en Ant Design
+const commonRules = {
+  requerido: { required: true, message: 'Este campo es obligatorio.' },
+  correo: { type: 'email', message: 'El correo electrónico no es válido' },
+  telefono: [
+    { min: 10, max: 10, message: 'El número de teléfono debe tener 10 dígitos' },
+    { pattern: regex.telefono, message: 'El número de teléfono debe contener solo números' }
+  ],
+  numero: { type: 'number', message: 'El valor debe ser un número' },
+  password: { min: 6, message: 'La contraseña debe tener al menos 6 caracteres' },
+  rfc: [
+    { min: 12, max: 13, message: 'El RFC debe tener entre 12 y 13 caracteres' },
+    { pattern: regex.rfc, message: 'El RFC debe contener solo números y letras mayúsculas' }
+  ],
+  letras: { pattern: regex.letras, message: 'Este campo solo puede contener letras' },
+  url: { type: 'url', message: 'La URL no es válida' },
+  longitud: (min, max) => ({ min, max, message: `Este campo debe tener entre ${min} y ${max} caracteres` }),
+  longitudMin: min => ({ min, message: `Este campo debe tener al menos ${min} caracteres` }),
+  longitudMax: max => ({ max, message: `Este campo debe tener máximo ${max} caracteres` }),
+  rango: (min, max) => ({ type: 'number', min, max, message: `El valor debe estar entre ${min} y ${max}` }),
+  rangoMin: min => ({ type: 'number', min, message: `El valor debe ser mayor o igual a ${min}` }),
+  rangoMax: max => ({ type: 'number', max, message: `El valor debe ser menor o igual a ${max}` }),
+  formato: (regex, message) => ({ pattern: regex, message }),
+  comparar: (field, message) => ({
+    validator: (rule, value, callback) => {
+      if (value && value !== field) {
+        callback(message);
+      } else {
+        callback();
+      }
+    }
+  })
+}
+
+export {
+  commonRules
+};

+ 25 - 1
src/routers/routes.jsx

@@ -11,6 +11,7 @@ import {
   DatabaseOutlined,
   LockOutlined,
   UsergroupAddOutlined,
+  ApartmentOutlined
 } from "@ant-design/icons";
 //Íconos de Ant Design
 
@@ -22,6 +23,7 @@ import { Inicio } from '../views/inicio'
 import { Usuarios, UsuarioDetalle } from "../views/admin/usuarios";
 
 /* CATÁLOGOS */
+import { Productos, ProductoDetalle } from "../views/catalogos/productos";
 /* CATÁLOGOS */
 import { Perfil } from "../views/perfil";
 import { Modulos } from "../views/admin/permisos/modulos";
@@ -57,7 +59,7 @@ const dashboardRoutes = [
     name: "Inicio",
     icon: <HomeOutlined />,
     sidebar: "single",
-    ver: "MENU-SOLICITUD",
+    ver: "MENU-INICIO",
     element: Inicio,
   },
   {
@@ -169,6 +171,28 @@ const dashboardRoutes = [
         sidebar: "collapse",
         ver: "MENU-ADMIN",
         routes: [
+          {
+            layout: "dashboard",
+            path: "/productos",
+            name: "Productos",
+            icon: <ApartmentOutlined />,
+            sidebar: "single",
+            ver: "MENU-ADMIN",
+            routes: [
+              {
+                path: "/",
+                element: Productos,
+              },
+              {
+                path: "/agregar",
+                element: ProductoDetalle,
+              },
+              {
+                path: "/editar",
+                element: ProductoDetalle,
+              },
+            ],
+          },
         ],
       },
     ],

+ 55 - 0
src/views/catalogos/productos/Formulario.jsx

@@ -0,0 +1,55 @@
+import { Form, Input, Button, Row, Col } from 'antd'
+import PropTypes from 'prop-types'
+
+// const selectores = {
+//   consejoElectoral: {
+//     name: "v1/consejo-electoral",
+//   },
+// }
+
+const Formulario = ({
+  form,
+  onFinish,
+}) => {
+  return (
+    <Form
+      layout="vertical"
+      name="basic"
+      form={form}
+      initialValues={{ remember: true }}
+      onFinish={onFinish}
+      onFinishFailed={() => { }}
+    >
+      <Row gutter={16}>
+        <Col span={6}>
+
+
+          <Form.Item
+            label="Búsqueda"
+            name="q"
+          >
+            <Input />
+          </Form.Item>
+        </Col>
+        <Col span={6}>
+          <Form.Item>
+            <Button
+              type="primary"
+              htmlType="submit"
+              style={{ marginTop: "30px" }}
+            >
+              Buscar
+            </Button>
+          </Form.Item>
+        </Col>
+      </Row>  
+    </Form>
+  )
+}
+
+export default Formulario
+
+Formulario.propTypes = {
+  form: PropTypes.object.isRequired,
+  onFinish: PropTypes.func.isRequired,
+}

+ 189 - 0
src/views/catalogos/productos/ProductoDetalle.jsx

@@ -0,0 +1,189 @@
+import { Form, Input, Button, Spin, Space, Row, Col } from 'antd'
+import { useEffect, useMemo, useState } from 'react'
+import HttpService from '../../../services/httpService'
+import { respuestas } from '../../../utilities'
+import { useNavigate } from 'react-router-dom'
+import { useQuery, useModel } from '../../../hooks'
+import { commonRules } from '../../../constants/rules'
+import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
+
+// const selectores = {
+//   consejoElectoral: {
+//     name: "v1/consejo-electoral",
+//   },
+//   distrito: {
+//     name: "v1/distrito",
+//   },
+//   estado: {
+//     name: "v1/estado",
+//   },
+//   municipio: {
+//     name: "v1/municipio",
+//   },
+//   participantePolitico: {
+//     name: "v1/participante-politico",
+//   },
+//   seccion: {
+//     name: "v1/seccion",
+//   },
+//   tipoAgenda: {
+//     name: "v1/agenda/tipo-agenda",
+//   },
+//   usuario: {
+//     name: "v1/usuario",
+//   }
+// }
+
+const endpoints = {
+  producto: "producto",
+};
+
+const ProductoDetalle = () => {
+  const [form] = Form.useForm()
+  const navigate = useNavigate()
+  const [loading, setLoading] = useState(false)
+  const query = useQuery()
+  const id = query.get("id")
+  const [request, setRequest] = useState({})
+
+  // const extraParams = useMemo(() => ({
+  //   idAgenda: id,
+  // }), [id])
+
+  const requestParams = useMemo(() => ({
+    name: endpoints.producto,
+    expand: 'subproductos',
+    id,
+    // extraParams
+  }), [id])
+
+
+  const { model, modelLoading } = useModel(request)
+
+  useEffect(() => {
+    if (id) {
+      setRequest(requestParams)
+    }
+    return () => {
+      setRequest({})
+    }
+  }, [id, requestParams])
+
+  useEffect(() => {
+    if (model) {
+      form.setFieldsValue({ //seteo cuando son varios
+        ...model,
+        subproductos: model.subproductos.map((subproducto, index) => ({
+          ...subproducto,
+          key: index
+        }))
+      })
+    }
+  }, [form, model])
+
+  const onFinish = async (values) => {
+    try {
+      setLoading(true);
+
+      let body = {
+        ...values,
+      };
+
+      if (id) {
+        body.id = id
+      }
+
+      const res = await HttpService.post(`${endpoints.producto}/guardar`, body);
+      respuestas(res);
+      if (res?.status === 200) {
+        navigate('/administracion/catalogos/productos')
+      }
+    } catch (error) {
+      console.log(error);
+      setLoading(false);
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  if (modelLoading) {
+    return <Spin
+      size="large"
+      style={{ display: "block", margin: "auto", marginTop: "50px" }}
+    />
+  }
+
+  return (
+    <Form
+      layout="vertical"
+      name="basic"
+      form={form}
+      onFinish={onFinish}
+      onFinishFailed={() => { }}
+    >
+      <Row gutter={16}>
+        <Col span={24}>
+          <h2>Información del Producto</h2>
+        </Col>
+        <Col md={8} xs={24}>
+          <Form.Item
+            label="Nombre"
+            name="nombre"
+            rules={[
+              commonRules.requerido,
+            ]}
+          >
+            <Input />
+          </Form.Item>
+        </Col>
+        <Col span={24}>
+          <h2>Subproductos:</h2>
+        </Col>
+        <Col span={24}>
+          <Form.List name="subproductos">
+            {(fields, { add, remove }) => (
+              <>
+                {fields.map(({ key, name, ...restField }) => (
+                  <Space key={key} style={{ display: 'flex', marginBottom: 8 }} align="baseline">
+                      <Form.Item
+                        {...restField}
+                        name={[name, 'nombre']}
+                        rules={[commonRules.requerido]}
+                        style={{ width: '350px' }}
+                      >
+                        <Input placeholder="Nombre del Subproducto"/>
+                      </Form.Item>
+                      <MinusCircleOutlined onClick={() => remove(name)} />
+                  </Space>
+                ))}
+                <Form.Item>
+                  <Button
+                    type="dashed"
+                    onClick={() => add()}
+                    icon={<PlusOutlined />}
+                  >
+                    Agregar Subproducto
+                  </Button>
+                </Form.Item>
+              </>
+            )}
+          </Form.List>
+        </Col>
+        <Col span={24}>
+          <Form.Item>
+            <Button
+              type="primary"
+              htmlType="submit"
+              style={{ marginTop: "30px" }}
+              loading={loading}
+            >
+              Guardar
+            </Button>
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form>
+  )
+}
+
+export default ProductoDetalle

+ 150 - 0
src/views/catalogos/productos/Productos.jsx

@@ -0,0 +1,150 @@
+import { useRef, useState } from "react";
+import { Form, Modal, Tooltip, notification } from "antd";
+import { DeleteOutlined, PlusOutlined } from "@ant-design/icons";
+import { Tabla } from "../../../components";
+import { SimpleTableLayout } from "../../../components/layouts";
+import { ActionsButton } from "../../../components";
+import { isEllipsis } from "../../../utilities";
+import { Link, useNavigate } from "react-router-dom";
+import Formulario from "./Formulario";
+import HttpService from "../../../services/httpService";
+
+const endPoint = "producto";
+
+const Productos = () => {
+  let tablaRef = useRef(null);
+  const navigate = useNavigate();
+  const [form] = Form.useForm();
+  const [buscarParams, setBuscarParams] = useState({});
+
+  const onFinish = (values) => {
+    const { q } = values;
+    const params = {
+      q: q ?? "",
+      padre: true,
+    };
+    setBuscarParams(params);    
+  };
+
+  const botones = [
+    {
+      onClick: () => navigate(`/administracion/catalogos/productos/agregar`),
+      props: { disabled: false, type: "primary", block: false },
+      text: "Nuevo",
+      icon: <PlusOutlined />,
+    },
+  ];
+
+  const linkText = (value, row, key) => (
+    <Link
+      to={`/administracion/catalogos/productos/editar?id=${row.id}`}
+      style={{ color: "black" }}
+    >
+      {isEllipsis(columns, key) ? (
+        <Tooltip title={value}>{value}</Tooltip>
+      ) : (
+        value
+      )}
+    </Link>
+  );
+
+  const eliminarRegistro = (nombre, id, url, alTerminar) => {
+    if (!id) return;
+    Modal.confirm({
+      title: "Eliminar",
+      content: `¿Está seguro de eliminar "${nombre}"?`,
+      icon: <DeleteOutlined style={{ color: "#ff0000" }} />,
+      okText: "Eliminar",
+      okButtonProps: {
+        type: "primary",
+        danger: true,
+      },
+      cancelText: "Cancelar",
+      onOk: async () => {
+        try {
+          let body = { id: id };
+          if (typeof id === "object") {
+            body = id;
+          }
+          const res = await HttpService.delete(url, body);
+          if (res && res.status === 200) {
+            notification.success({
+              message: "Éxito",
+              description: res?.mensaje,
+            });
+            alTerminar && alTerminar();
+          } else if (res?.status === 400) {
+            notification.error({
+              message: "Atención",
+              description: res?.mensaje,
+            });
+          }
+        } catch (error) {
+          console.log(error);
+          notification.error({
+            message: "Error",
+            description: error,
+          });
+          return "error";
+        }
+      },
+    });
+  };
+
+  const columns = [
+    {
+      title: "Acciones",
+      key: "correo",
+      dataIndex: "correo",
+      width: 100,
+      align: "center",
+      render: (_, item) => (
+        <ActionsButton
+          data={[
+            {
+              label: "Editar",
+              onClick: () =>
+                navigate(`/administracion/catalogos/productos/editar?id=${item?.id}`),
+            },
+            {
+              label: "Eliminar",
+              onClick: () => {
+                eliminarRegistro(item?.nombreCompleto, item?.id, endPoint+'/eliminar', () =>
+                  tablaRef?.current?.refresh()
+                );
+              },
+              danger: true,
+            },
+          ]}
+        />
+      ),
+    },
+    {
+      title: "Nombre",
+      key: "nombre",
+      dataIndex: "nombre",
+      render: linkText,
+    },
+  ];
+
+  return (
+    <SimpleTableLayout
+      btnGroup={{
+        btnGroup: botones,
+      }}
+    >
+      <Formulario
+        form={form}
+        onFinish={onFinish} 
+      />
+      <Tabla
+        columns={columns}
+        nameURL={endPoint}
+        extraParams={buscarParams}
+        scroll={{ x: "30vw" }}
+      />
+    </SimpleTableLayout>
+  );
+};
+
+export default Productos;

+ 7 - 0
src/views/catalogos/productos/index.js

@@ -0,0 +1,7 @@
+import Productos from './Productos'
+import ProductoDetalle from './ProductoDetalle'
+
+export {
+  Productos,
+  ProductoDetalle
+}