Bladeren bron

Proyecto Base

Hello, World! (semilla)
WriestTavo 11 maanden geleden
commit
32a3fa5dfc
97 gewijzigde bestanden met toevoegingen van 7987 en 0 verwijderingen
  1. 14 0
      .env.development.local
  2. 22 0
      .env.production.local
  3. 3 0
      .eslintrc.json
  4. 42 0
      .gitignore
  5. 22 0
      index.html
  6. 47 0
      package.json
  7. BIN
      public/icon-192-maskable.png
  8. BIN
      public/icon-192.png
  9. BIN
      public/icon-512-maskable.png
  10. BIN
      public/icon-512.png
  11. BIN
      public/logo.png
  12. BIN
      public/logoCollapsed.png
  13. 37 0
      src/App.jsx
  14. 31 0
      src/Estilos.css
  15. 32 0
      src/components/ActionsButton.jsx
  16. 28 0
      src/components/AppLoading.jsx
  17. 211 0
      src/components/ArbolPermisos.jsx
  18. 72 0
      src/components/BeneficiariosSelect.jsx
  19. 61 0
      src/components/ButtonGroup.jsx
  20. 23 0
      src/components/Card.jsx
  21. 29 0
      src/components/EditorTexto.jsx
  22. 161 0
      src/components/EditorToolbar.jsx
  23. 27 0
      src/components/FormItem.jsx
  24. 125 0
      src/components/ImageUploader.jsx
  25. 35 0
      src/components/InputPass.jsx
  26. 75 0
      src/components/MediaCards.jsx
  27. 170 0
      src/components/Select.jsx
  28. 139 0
      src/components/Selector.jsx
  29. 89 0
      src/components/Tabla.jsx
  30. 128 0
      src/components/Upload.jsx
  31. 144 0
      src/components/Uploader.jsx
  32. 20 0
      src/components/ViewLoading.jsx
  33. 31 0
      src/components/index.js
  34. 34 0
      src/components/layouts/AuthLayout.jsx
  35. 10 0
      src/components/layouts/DashboardLayout.css
  36. 316 0
      src/components/layouts/DashboardLayout.jsx
  37. 45 0
      src/components/layouts/DefaultLayout.jsx
  38. 78 0
      src/components/layouts/SimpleTableLayout.jsx
  39. 11 0
      src/components/layouts/index.js
  40. 59 0
      src/constants/httpStatusCodes.js
  41. 38 0
      src/constants/index.js
  42. 31 0
      src/constants/requests.js
  43. 9 0
      src/hooks/index.js
  44. 66 0
      src/hooks/useAlert.jsx
  45. 34 0
      src/hooks/useApp.jsx
  46. 102 0
      src/hooks/useAuth.jsx
  47. 185 0
      src/hooks/useHttp.jsx
  48. 90 0
      src/hooks/useModel.jsx
  49. 86 0
      src/hooks/useModels.jsx
  50. 40 0
      src/hooks/usePagination.jsx
  51. 7 0
      src/hooks/useQuery.jsx
  52. 52 0
      src/hooks/useSortColumns.jsx
  53. 12 0
      src/main.jsx
  54. 16 0
      src/routers/AppRouting.jsx
  55. 38 0
      src/routers/PrivateRouter.jsx
  56. 20 0
      src/routers/PublicRouter.jsx
  57. 4 0
      src/routers/index.js
  58. 181 0
      src/routers/routes.jsx
  59. 200 0
      src/services/httpService.js
  60. 1 0
      src/services/index.js
  61. 24 0
      src/utilities/InformacionArchivos.jsx
  62. 4 0
      src/utilities/QuitarObjetosDuplicados.jsx
  63. 13 0
      src/utilities/QuitarSignos.jsx
  64. 31 0
      src/utilities/RenderEstatusSolicitudPrimaria.jsx
  65. 13 0
      src/utilities/ValidarPermisosVista.jsx
  66. 11 0
      src/utilities/estatusExpediente.jsx
  67. 396 0
      src/utilities/index.jsx
  68. 258 0
      src/utilities/inventarioConcentracion.jsx
  69. 4 0
      src/utilities/obtenerExtencionImagen.jsx
  70. 158 0
      src/utilities/reporteExpediente.jsx
  71. 239 0
      src/views/admin/permisos/modulos/Modulos.jsx
  72. 5 0
      src/views/admin/permisos/modulos/index.js
  73. 224 0
      src/views/admin/permisos/perfiles/FormPerfil.jsx
  74. 245 0
      src/views/admin/permisos/perfiles/PerfilDetalle.jsx
  75. 115 0
      src/views/admin/permisos/perfiles/Perfiles.jsx
  76. 7 0
      src/views/admin/permisos/perfiles/index.js
  77. 56 0
      src/views/admin/permisos/permisos/BuscarComponente.jsx
  78. 355 0
      src/views/admin/permisos/permisos/Permisos.jsx
  79. 5 0
      src/views/admin/permisos/permisos/index.js
  80. 617 0
      src/views/admin/usuarios/Formulario.jsx
  81. 57 0
      src/views/admin/usuarios/UsuarioDetalle.jsx
  82. 103 0
      src/views/admin/usuarios/Usuarios.jsx
  83. 7 0
      src/views/admin/usuarios/index.jsx
  84. 206 0
      src/views/auth/Ingresar.jsx
  85. 397 0
      src/views/auth/Recuperar.jsx
  86. 181 0
      src/views/auth/Registrar.jsx
  87. 5 0
      src/views/auth/index.js
  88. 236 0
      src/views/dashboard/DashboardChart.jsx
  89. 17 0
      src/views/dashboard/index.jsx
  90. 14 0
      src/views/error/NoAutorizado.jsx
  91. 14 0
      src/views/error/NoEncontrado.jsx
  92. 7 0
      src/views/error/index.js
  93. 92 0
      src/views/inicio/Inicio.jsx
  94. 5 0
      src/views/inicio/index.js
  95. 269 0
      src/views/perfil/Perfil.jsx
  96. 5 0
      src/views/perfil/index.js
  97. 9 0
      vite.config.js

+ 14 - 0
.env.development.local

@@ -0,0 +1,14 @@
+# Projecto   firebase | jwt
+VITE_PROJECT=jwt
+VITE_NOMBRE_PAGINA=
+VITE_API_URL=http://localhost:8080
+VITE_USR_URL=http://127.0.0.1:5173/
+VITE_API_VERSION=1.24.05.06+0
+
+VITE_API_MODULE=/v1/
+VITE_API_MODULE_PDF=/pdf/
+VITE_API_MODULE_EXCEL=/excel/
+VITE_API_MODULE_WORD=/word/
+VITE_API_MODULE_PUBLIC=/publico/
+
+VITE_API_VERSION=1.24.05.06+0

+ 22 - 0
.env.production.local

@@ -0,0 +1,22 @@
+# Projecto   firebase | jwt
+VITE_PROJECT=jwt
+VITE_NOMBRE_PAGINA=
+VITE_API_URL=http://localhost:8080
+VITE_USR_URL=http://127.0.0.1:5173/
+VITE_API_VERSION=1.24.05.06+0
+
+VITE_API_MODULE=/v1/
+VITE_API_MODULE_PDF=/pdf/
+VITE_API_MODULE_EXCEL=/excel/
+VITE_API_MODULE_WORD=/word/
+VITE_API_MODULE_PUBLIC=/publico/
+
+# Firebase
+VITE_FB_API_KEY=
+VITE_FB_AUTH_DOMAIN=
+VITE_FB_DB_URL=
+VITE_FB_PROJ_ID=
+VITE_FB_STORAGE=
+VITE_FB_SENDER_ID=
+VITE_FB_APP_ID=
+VITE_FB_MEASUREMENT_ID=

+ 3 - 0
.eslintrc.json

@@ -0,0 +1,3 @@
+{
+  "extends": "react-app"
+}

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+.DS_STORE
+node_modules
+scripts/flow/*/.flowconfig
+.flowconfig
+*~
+*.pyc
+.grunt
+_SpecRunner.html
+__benchmarks__
+dist/
+build2/
+remote-repo/
+coverage/
+.module-cache
+fixtures/dom/public/react-dom.js
+fixtures/dom/public/react.js
+test/the-files-to-test.generated.js
+*.log*
+chrome-user-data
+*.sublime-project
+*.sublime-workspace
+.idea
+*.iml
+.vscode
+*.swp
+*.swo
+
+.firebase/*
+*lock*
+
+packages/react-devtools-core/dist
+packages/react-devtools-extensions/chrome/build
+packages/react-devtools-extensions/chrome/*.crx
+packages/react-devtools-extensions/chrome/*.pem
+packages/react-devtools-extensions/firefox/build
+packages/react-devtools-extensions/firefox/*.xpi
+packages/react-devtools-extensions/firefox/*.pem
+packages/react-devtools-extensions/shared/build
+packages/react-devtools-extensions/.tempUserDataDir
+packages/react-devtools-inline/dist
+packages/react-devtools-shell/dist
+packages/react-devtools-scheduling-profiler/dist

+ 22 - 0
index.html

@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="es">
+  <head>
+    <meta charset="UTF-8" />
+    <!-- Aquí va el ícono -->
+    <!-- <link rel="icon" href="/favicon.ico"/> -->
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title></title>
+  </head>
+  <body>
+    <style>
+      #root {
+        margin: -8px
+      }
+      .ant-form-item {
+        margin-bottom: 0 !important;
+      }
+    </style>
+    <div id="root"></div>
+    <script type="module" src="/src/main.jsx"></script>
+  </body>
+</html>

+ 47 - 0
package.json

@@ -0,0 +1,47 @@
+{
+  "name": "react-vite-base",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "vite build",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@ant-design/icons": "^5.0.0",
+    "@fullcalendar/core": "^6.0.3",
+    "@fullcalendar/daygrid": "^6.0.3",
+    "@fullcalendar/interaction": "^6.0.3",
+    "@fullcalendar/react": "^6.0.4",
+    "@fullcalendar/timegrid": "^6.0.3",
+    "antd": "^5.1.5",
+    "antd-img-crop": "^4.13.0",
+    "array-move": "^4.0.0",
+    "dayjs": "^1.11.7",
+    "exceljs": "^4.3.0",
+    "file-saver": "^2.0.5",
+    "highcharts": "^11.1.0",
+    "highcharts-react-official": "^3.1.0",
+    "html-react-parser": "^5.0.7",
+    "image-to-base64": "^2.2.0",
+    "moment": "^2.29.4",
+    "prop-types": "^15.8.1",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-helmet-async": "^1.3.0",
+    "react-icons": "^4.11.0",
+    "react-number-format": "^5.2.2",
+    "react-quill": "^2.0.0",
+    "react-router-dom": "^6.7.0",
+    "react-sortable-hoc": "^2.0.0"
+  },
+  "devDependencies": {
+    "@types/react": "^18.0.26",
+    "@types/react-dom": "^18.0.9",
+    "@vitejs/plugin-react": "^3.0.0",
+    "eslint-config-react-app": "^7.0.1",
+    "standard": "^17.0.0",
+    "vite": "^4.0.0"
+  }
+}

BIN
public/icon-192-maskable.png


BIN
public/icon-192.png


BIN
public/icon-512-maskable.png


BIN
public/icon-512.png


BIN
public/logo.png


BIN
public/logoCollapsed.png


+ 37 - 0
src/App.jsx

@@ -0,0 +1,37 @@
+import React from "react";
+import { ConfigProvider } from "antd";
+import { HelmetProvider } from "react-helmet-async";
+import { AppProvider, AuthProvider, AlertProvider } from "./hooks";
+import { AppRouting } from "./routers";
+import esES from "antd/lib/locale/es_ES";
+
+function App() {
+
+
+  return (
+    <ConfigProvider
+      locale={esES}
+      theme={{
+        token: {
+          // Seed Token
+          // colorPrimary: '#00827e',
+          // colorInfo: '#813699',
+          // colorPrimaryBorderHover: '#813699',
+          // borderRadius: 5,
+        },
+      }}
+    >
+      <HelmetProvider>
+        <AppProvider>
+          <AlertProvider>
+            <AuthProvider>
+              <AppRouting />
+            </AuthProvider>
+          </AlertProvider>
+        </AppProvider>
+      </HelmetProvider>
+    </ConfigProvider>
+  );
+}
+
+export default App;

+ 31 - 0
src/Estilos.css

@@ -0,0 +1,31 @@
+.input-numerico {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 4px 11px;
+  color: rgba(0, 0, 0, 0.88);
+  font-size: 14px;
+  line-height: 1.5714285714285714;
+  list-style: none;
+  font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
+  position: relative;
+  display: inline-block;
+  width: 100%;
+  min-width: 0;
+  background-color: #ffffff;
+  background-image: none;
+  border-width: 1px;
+  border-style: solid;
+  border-color: #d9d9d9;
+  border-radius: 2px;
+  transition: all 0.2s;
+}
+
+.input-numerico:hover {
+  border-color: #298a73;
+}
+
+.input-numerico:focus {
+  border-color: #127d67;
+  box-shadow: 0 0 0 2px rgba(3, 42, 29, 0.31);
+  outline: 0;
+}

+ 32 - 0
src/components/ActionsButton.jsx

@@ -0,0 +1,32 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button, Dropdown } from "antd";
+import { MoreOutlined } from "@ant-design/icons";
+
+const ActionsButton = ({ data = [] }) => {
+  const [items, setItems] = React.useState([]);
+
+  React.useEffect(() => {
+    if (data.length < 1) return;
+    const arr = [];
+    data.forEach((i) => {
+      arr.push({ ...i });
+    });
+
+    setItems(data);
+  }, [data]);
+
+  return (
+    <Dropdown menu={{ items }} placement="bottomLeft">
+      <Button>
+        <MoreOutlined />
+      </Button>
+    </Dropdown>
+  );
+};
+
+ActionsButton.propTypes = {
+  data: PropTypes.array.isRequired,
+};
+
+export default ActionsButton;

+ 28 - 0
src/components/AppLoading.jsx

@@ -0,0 +1,28 @@
+import React from "react";
+import { Spin } from "antd";
+
+const AppLoading = (props) => {
+  const { text, loading, children: ChildComponents } = props;
+
+  return (
+    <div
+      style={{
+        display: "flex",
+        justifyContent: "center",
+        alignItems: "center",
+        height: "100vh",
+      }}
+    >
+      <Spin
+        spinning={loading}
+        size="large"
+        delay={5}
+        tip={text || "Cargando aplicación..."}
+      >
+        {loading ? <div style={{ height: "100vh" }} /> : ChildComponents}
+      </Spin>
+    </div>
+  );
+};
+
+export default AppLoading;

+ 211 - 0
src/components/ArbolPermisos.jsx

@@ -0,0 +1,211 @@
+import React, { useState, useEffect, useCallback, useMemo } from "react";
+import { Col, Row, Tooltip, Tree } from "antd";
+import { useModels, useModel, useAuth } from "../hooks";
+import { emptyRequest } from "../constants/requests";
+import Select from "./Select";
+
+const ArbolPermisos = ({
+  conPerfil = false,
+  urlModulo,
+  urlPerfil = '',
+  permisosCargados = [],
+  alMarcar
+}) => {
+
+  const { user } = useAuth();
+
+  const [ape, setApe] = useState([]) // Arbol de permisos expandido
+  const [idPerfil, setIdPerfil] = useState(null) // Estado de id parar perfil de permisos
+  const [checkedList, setCheckedList] = useState([])
+  const [listaPermisos, setListaPermisos] = useState([])
+  const [arbolPermisos, setArbolPermisos] = useState([])
+  const [perfilRequest, setPerfilRequest] = useState(emptyRequest) // Petición para perfil de permisos
+  const [modulosRequest, setModulosRequest] = useState([]) // Estado petición módulos permisos
+
+  const modulosParams = useMemo(() => ({
+    name: urlModulo,
+    limite: 100,
+    expand: 'permisos'
+  }), [urlModulo])
+
+  // Parámetros de perfil de permisos
+  const perfilesParams = useMemo(() => {
+    if (conPerfil) {
+      return {
+        name: urlPerfil,
+        id: idPerfil,
+        expand: 'permisos',
+      }
+    }
+
+    return emptyRequest
+  }, [conPerfil, idPerfil, urlPerfil])
+
+  const { models: modulos } = useModels(modulosRequest)
+
+  // Perfil de permisos
+  const { model: perfilPermiso } = useModel(perfilRequest)
+
+  const ordenarLista = useCallback((a, b) => {
+    const nombreA = a.nombre
+      ? a.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, '')
+      : a.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, '')
+    const nombreB = b.nombre
+      ? b.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, '')
+      : b.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, '')
+
+    if (nombreA < nombreB) {
+      return -1
+    }
+
+    if (nombreA > nombreB) {
+      return 1
+    }
+
+    return 0
+  }, [])
+
+  const onCheckedTree = useCallback(
+    (checkedList, info) => {
+      checkedList = checkedList.map((item) => {
+        const val = item?.replace('p-', '')
+        return val
+      })
+
+      let _checkedList = []
+      for (let i = 0, l = listaPermisos.length; i < l; i++) {
+        const _permiso = listaPermisos[i]
+        if (checkedList.includes(_permiso['id'])) {
+          _checkedList.push(_permiso['id'])
+        }
+      }
+
+      setCheckedList(_checkedList)
+
+      alMarcar && alMarcar(_checkedList);
+    },
+    [alMarcar, listaPermisos]
+  )
+
+  useEffect(() => {
+    setModulosRequest(modulosParams)
+    return () => setModulosRequest(emptyRequest)
+  }, [modulosParams])
+
+  /**
+   * useEffect para hacer petición de un perfil
+   * dependiendo de la selección
+   */
+  useEffect(() => {
+    setPerfilRequest(perfilesParams)
+    return () => setPerfilRequest({})
+  }, [perfilesParams])
+
+
+  useEffect(() => {
+    if (modulos && modulos?.length) {
+      let _arbolPermisos = []
+      let _listaPermisos = []
+      let _ape = []
+      let _modulosPermisos = modulos
+
+      _modulosPermisos.sort((a, b) => ordenarLista(a, b))
+
+      for (let i = 0, l = _modulosPermisos.length; i < l; i++) {
+        let _permisos = _modulosPermisos[i]['permisos']
+        let _modulos = []
+        for (let j = 0, k = _permisos?.length; j < k; j++) {
+          let nPermiso = {
+            title: `${_permisos[j]['nombre']} (${_permisos[j]['id']})`,
+            descripcion: `${_permisos[j]['descripcion']}`,
+            id: _permisos[j]['id'],
+            selectable: false,
+            key: `p-${_permisos[j]['id']}`,
+          }
+          _listaPermisos.push(nPermiso)
+          _modulos.push(nPermiso)
+        }
+
+        _modulos.sort((a, b) => ordenarLista(a, b))
+
+        const llave = `m-${modulos[i]['id']}`
+        _ape.push(llave)
+        if (modulos[i]?.id === 'ADMN' && user?.rol !== 'Super Administrador')
+          continue
+        _arbolPermisos.push({
+          title: modulos[i]['nombre'],
+          key: llave,
+          id: modulos[i]['id'],
+          children: _modulos,
+        })
+      }
+
+      setApe(_ape)
+      setListaPermisos(_listaPermisos)
+      setArbolPermisos(_arbolPermisos)
+    }
+  }, [modulos, ordenarLista, user?.rol])
+
+  useEffect(() => {
+    if (conPerfil) {
+      if (perfilPermiso) {
+        onCheckedTree([])
+        let permisos = []
+        for (let i = 0, l = perfilPermiso?.permisos?.length; i < l; i++) {
+          permisos.push(
+            `m-${perfilPermiso?.permisos[i]?.idModulo}`,
+            `p-${perfilPermiso?.permisos[i]?.id}`
+          )
+        }
+        onCheckedTree(permisos)
+      }
+    }
+  }, [conPerfil, onCheckedTree, perfilPermiso])
+
+  useEffect(() => {
+    setCheckedList(permisosCargados)
+  }, [permisosCargados])
+
+  return (
+    <Row gutter={[10, 10]}>
+      {conPerfil && (
+        <Col span={24}>
+          <Select
+            modelsParams={{
+              name: urlPerfil,
+              limite: 20,
+              ordenar: 'nombre-asc',
+            }}
+            labelProp="nombre"
+            valueProp="id"
+            placeholder="Seleccione un Perfíl de permisos"
+            render={(_, row) => `${row?.clave} - ${row?.nombre}`}
+            onSelect={(v) => setIdPerfil(v)}
+          />
+        </Col>
+      )}
+      <Col span={24}>
+        <Tree
+          checkable
+          defaultExpandedKeys={ape}
+          checkedKeys={checkedList?.map((i) => `p-${i}`)}
+          onCheck={onCheckedTree}
+          treeData={arbolPermisos}
+          titleRender={(item) => {
+            if (item?.descripcion !== null) {
+              return (
+                <Tooltip title={item?.descripcion}>
+                  {item?.title}
+                </Tooltip>
+              )
+            } else {
+              return item?.title
+            }
+          }}
+        />
+      </Col>
+    </Row>
+  )
+}
+
+export default ArbolPermisos

+ 72 - 0
src/components/BeneficiariosSelect.jsx

@@ -0,0 +1,72 @@
+import React from 'react';
+import PropTypes from "prop-types";
+import { Select } from 'antd';
+import { useModels } from '../hooks';
+import { agregarFaltantes } from '../utilities';
+
+const BeneficiarioSelect = ({ append, valueProp, ...props }) => {
+
+  const [request, setRequest] = React.useState({});
+  const [buscarValue, setBuscarValue] = React.useState('');
+  const [timer, setTimer] = React.useState(null);
+
+  const extraParamsMemo = React.useMemo(() => ({ buscar: buscarValue }), [buscarValue]);
+
+  const requestMemo = React.useMemo(() => ({
+    name: "beneficiario",
+    ordenar: "nombre-asc",
+    limite: 20,
+    extraParams: extraParamsMemo
+  }), [extraParamsMemo]);
+
+  const {
+    models,
+    modelsLoading,
+  } = useModels(request);
+
+  const onSearch = (value) => {
+    clearTimeout(timer);
+    const newTimer = setTimeout(() => {
+      setBuscarValue(value);
+    }, 300);
+
+    setTimer(newTimer);
+  };
+
+  if (!append) {
+    append = [];
+  }
+
+  React.useEffect(() => {
+    setRequest(requestMemo);
+    return () => {
+      setRequest({});
+    };
+  }, [requestMemo]);
+
+  return (
+    <Select
+      {...props}
+      showSearch
+      onSearch={onSearch}
+      defaultActiveFirstOption={false}
+      filterOption={false}
+      notFoundContent={null}
+      allowClear={true}
+      style={{ width: '100%' }}
+      loading={modelsLoading}
+      options={models.length > 0 && agregarFaltantes([...models], [...append], valueProp).map(i => ({
+        ...i,
+        label: `${i?.nombre}`,
+        value: i?.[valueProp],
+      }))}
+    />
+  )
+}
+
+BeneficiarioSelect.propTypes = {
+  valueProp: PropTypes.string.isRequired,
+  append: PropTypes.array
+};
+
+export default BeneficiarioSelect

+ 61 - 0
src/components/ButtonGroup.jsx

@@ -0,0 +1,61 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Button } from "antd";
+
+const ButtonGroup = ({ data }) => {
+  const btnGroup = data?.btnGroup || [];
+  const flex = {
+    justifyContent: data?.flex?.justifyContent || "end",
+    flexDirection: data?.flex?.flexDirection || "row",
+  };
+
+  const styles = {
+    buttons: {
+      background: "#fff",
+      borderRadius: 6,
+      display: "flex",
+      justifyContent: flex.justifyContent,
+      flexDirection: flex.flexDirection,
+    },
+    btn: {
+      marginLeft: flex.justifyContent === "end" ? 10 : 0,
+      marginRight: flex.justifyContent === "start" ? 10 : 0,
+      marginBottom: flex.flexDirection === "column" ? 10 : 0,
+    },
+  };
+
+  return (
+    <div style={styles.buttons}>
+      {btnGroup.length > 0 &&
+        btnGroup.map((i, index) =>
+          Boolean(i.hide)
+            ? false
+            : true &&
+              (Boolean(i.text) ? (
+                <Button
+                  key={`btnGroup-${index}`}
+                  onClick={i.onClick}
+                  style={styles.btn}
+                  {...i.props}
+                >
+                  {i.icon} {i.text}
+                </Button>
+              ) : (
+                <Button
+                  key={`btnGroup-${index}`}
+                  onClick={i.onClick}
+                  style={styles.btn}
+                  icon={i.icon}
+                  {...i.props}
+                />
+              ))
+        )}
+    </div>
+  );
+};
+
+ButtonGroup.propTypes = {
+  data: PropTypes.object.isRequired,
+};
+
+export default ButtonGroup;

+ 23 - 0
src/components/Card.jsx

@@ -0,0 +1,23 @@
+import { Card, Col } from 'antd'
+import Meta from 'antd/es/card/Meta'
+
+const Cards = ({url, icon, color, fondo, title, idValue, hidden }) => {
+  return <Col
+    className="gutter-row"
+    xs={{ span: 24 }}
+    sm={{ span: 24 }}
+    md={{ span: 6 }}
+    lg={{ span: 6 }}
+    hidden={hidden}
+  >
+    <Card
+      className="link-pointer"
+      cover={icon}
+      style={{ color: color, backgroundColor: fondo }}
+
+    >
+      <Meta title={title} style={{ textAlign: 'center' }}/>
+    </Card>
+  </Col>
+}
+export default Cards

+ 29 - 0
src/components/EditorTexto.jsx

@@ -0,0 +1,29 @@
+import React from "react";
+import EditorToolbar, { modules, formats } from "./EditorToolbar";
+
+import ReactQuill from "react-quill";
+import "react-quill/dist/quill.snow.css";
+
+const EditorTexto = ({ altura, ...props}) => {
+  const styles = {
+    display: "block",
+    width: "100%",
+    height: altura === '' ? altura="550px": altura,
+    backgroundColor:"white",
+  };
+
+  return (
+    <>
+      <EditorToolbar />
+      <ReactQuill
+        {...props}
+        style={styles}
+        theme="snow"
+        modules={modules}
+        formats={formats}
+      />
+    </>
+  );
+};
+
+export default EditorTexto;

+ 161 - 0
src/components/EditorToolbar.jsx

@@ -0,0 +1,161 @@
+import React from "react";
+import { Quill } from "react-quill";
+
+// Custom Undo button icon component for Quill editor. You can import it directly
+// from 'quill/assets/icons/undo.svg' but I found that a number of loaders do not
+// handle them correctly
+const CustomUndo = () => (
+  <svg viewBox="0 0 18 18">
+    <polygon className="ql-fill ql-stroke" points="6 10 4 12 2 10 6 10" />
+    <path
+      className="ql-stroke"
+      d="M8.09,13.91A4.6,4.6,0,0,0,9,14,5,5,0,1,0,4,9"
+    />
+  </svg>
+);
+
+// Redo button icon component for Quill editor
+const CustomRedo = () => (
+  <svg viewBox="0 0 18 18">
+    <polygon className="ql-fill ql-stroke" points="12 10 14 12 16 10 12 10" />
+    <path
+      className="ql-stroke"
+      d="M9.91,13.91A4.6,4.6,0,0,1,9,14a5,5,0,1,1,5-5"
+    />
+  </svg>
+);
+
+// Undo and redo functions for Custom Toolbar
+function undoChange() {
+  this.quill.history.undo();
+}
+function redoChange() {
+  this.quill.history.redo();
+}
+
+// Add sizes to whitelist and register them
+const Size = Quill.import('attributors/style/size');
+Size.whitelist = ["10px","12px", "14px", "16px", "18px","20px"];
+Quill.register(Size, true);
+
+// Add fonts to whitelist and register them
+const Font = Quill.import("formats/font");
+Font.whitelist = [
+  "arial",
+  "comic-sans",
+  "courier-new",
+  "georgia",
+  "helvetica",
+  "lucida"
+];
+Quill.register(Font, true);
+
+// Modules object for setting up the Quill editor
+export const modules = {
+  toolbar: {
+    container: "#toolbar",
+    handlers: {
+      undo: undoChange,
+      redo: redoChange
+    }
+  },
+  history: {
+    delay: 500,
+    maxStack: 100,
+    userOnly: true
+  }
+};
+
+// Formats objects for setting up the Quill editor
+export const formats = [
+  "header",
+  "font",
+  "size",
+  "bold",
+  "italic",
+  "underline",
+  "align",
+  "strike",
+  "script",
+  "blockquote",
+  "background",
+  "list",
+  "bullet",
+  "indent",
+  "link",
+  "image",
+  "color",
+  "code-block"
+];
+
+// Quill Toolbar component
+export const QuillToolbar = () => (
+  <div id="toolbar">
+    <span className="ql-formats">
+      <select className="ql-font" defaultValue="arial">
+        <option value="arial">Arial</option>
+        <option value="comic-sans">Comic Sans</option>
+        <option value="courier-new">Courier New</option>
+        <option value="georgia">Georgia</option>
+        <option value="helvetica">Helvetica</option>
+        <option value="lucida">Lucida</option>
+      </select>
+      <select className="ql-size" defaultValue="medium">
+        <option value="10px">10px</option>
+        <option value="12px">12px</option>
+        <option value="14px">14px</option>
+        <option value="16px">16px</option>
+        <option value="18px">18px</option>
+        <option value="20px">20px</option>
+      </select>
+      <select className="ql-header" defaultValue="3">
+        <option value="1">Heading</option>
+        <option value="2">Subheading</option>
+        <option value="3">Normal</option>
+      </select>
+    </span>
+    <span className="ql-formats">
+      <button className="ql-bold" />
+      <button className="ql-italic" />
+      <button className="ql-underline" />
+      <button className="ql-strike" />
+    </span>
+    <span className="ql-formats">
+      <button className="ql-list" value="ordered" />
+      <button className="ql-list" value="bullet" />
+      <button className="ql-indent" value="-1" />
+      <button className="ql-indent" value="+1" />
+    </span>
+    <span className="ql-formats">
+      <button className="ql-script" value="super" />
+      <button className="ql-script" value="sub" />
+      <button className="ql-blockquote" />
+      <button className="ql-direction" />
+    </span>
+    <span className="ql-formats">
+      <select className="ql-align" />
+      <select className="ql-color" />
+      <select className="ql-background" />
+    </span>
+    <span className="ql-formats">
+      <button className="ql-link" />
+      <button className="ql-image" />
+      <button className="ql-video" />
+    </span>
+    <span className="ql-formats">
+      <button className="ql-formula" />
+      <button className="ql-code-block" />
+      <button className="ql-clean" />
+    </span>
+    <span className="ql-formats">
+      <button className="ql-undo">
+        <CustomUndo />
+      </button>
+      <button className="ql-redo">
+        <CustomRedo />
+      </button>
+    </span>
+  </div>
+);
+
+export default QuillToolbar;

+ 27 - 0
src/components/FormItem.jsx

@@ -0,0 +1,27 @@
+import React from "react";
+import PropTypes from "prop-types";
+import { Form } from "antd";
+
+const FormItem = ({ children, name, rules = [], errores = {}, ...props }) => {
+  if (errores[name]) {
+    rules.push({
+      required: true,
+      message: errores[name],
+    });
+  }
+
+  return (
+    <Form.Item name={name} rules={rules} {...props}>
+      {children}
+    </Form.Item>
+  );
+};
+
+FormItem.propTypes = {
+  children: PropTypes.any.isRequired,
+  name: PropTypes.string,
+  rules: PropTypes.array,
+  errores: PropTypes.object,
+};
+
+export default FormItem;

+ 125 - 0
src/components/ImageUploader.jsx

@@ -0,0 +1,125 @@
+import React from 'react'
+import { Upload as AntdUpload } from 'antd'
+import { DownloadOutlined } from '@ant-design/icons'
+import { respuestas } from '../utilities'
+import { useApp } from '../hooks'
+import PropTypes from 'prop-types'
+import ImgCrop from 'antd-img-crop'
+
+const baseUrl = import.meta.env.VITE_API_URL
+
+const ImageUploader = ({
+  endPoint,
+  setReferencias,
+  setListaArchivos,
+  accept = 'image/*',
+  listaArchivos,
+  maxCount = null,
+  onRemove = undefined,
+  aspect
+}) => {
+
+  const { token } = useApp()
+
+  const [lista, setLista] = React.useState([])
+
+  const props = {
+    name: 'archivo',
+    headers: {
+      Authorization: `Bearer ${token}`,
+    },
+    method: 'POST',
+    progress: {
+      strokeColor: {
+        '0%': '#108ee9',
+        '100%': '#87d068',
+      },
+      strokeWidth: 3,
+      format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`,
+    },
+  }
+
+  const onFinishSubir = ({ fileList, file }) => {
+
+    file.id = file.response.detalle.id
+    file.url = file.response.detalle.ruta
+
+    let listaReferencias = []
+    let _listaArchivos = []
+    fileList.forEach(file => {
+      const res = {
+        status: file?.xhr?.status,
+        mensaje: file?.response?.mensaje
+      }
+      respuestas(res)
+      _listaArchivos.push(file)
+      if (file?.id) {
+        listaReferencias.push(file)
+      } else {
+        listaReferencias.push(file?.response?.detalle)
+      }
+    })
+    setListaArchivos(_listaArchivos)
+    setReferencias(listaReferencias)
+  }
+
+  const onChangeArchivos = (info) => {
+    setLista(info.fileList)
+    let fileList = info?.fileList
+    let l = fileList.length
+    let completo = true
+    for (let i = 0; i < l; i++) {
+      if (!fileList[i]?.id && !fileList[i]?.xhr) {
+        completo = false
+      }
+    }
+    if (completo) {
+      onFinishSubir(info)
+    }
+  }
+
+  React.useEffect(() => {
+    if (listaArchivos) {
+      setLista(listaArchivos)
+      return () => setLista([])
+    }
+  }, [listaArchivos])
+
+  return (
+    <ImgCrop
+      showGrid
+      rotationSlider
+      aspectSlider={true}
+      showReset
+      aspect={aspect}
+      modalTitle="Editar Imagen"
+      resetText="Reiniciar"
+      fillColor="transparent"
+    >
+      <AntdUpload
+        {...props}
+        action={`${baseUrl}${endPoint}`}
+        multiple={true}
+        listType="picture-card"
+        fileList={lista}
+        onChange={onChangeArchivos}
+        accept={accept}
+        style={{ width: '100% !importat' }}
+        maxCount={maxCount}
+        onRemove={onRemove}
+      >
+        <DownloadOutlined/> Subir Imagen
+      </AntdUpload>
+    </ImgCrop>
+  )
+}
+
+ImageUploader.propTypes = {
+  endPoint: PropTypes.string.isRequired,
+  setReferencias: PropTypes.any.isRequired,
+  setListaArchivos: PropTypes.any.isRequired,
+  accept: PropTypes.string,
+  listaArchivos: PropTypes.array.isRequired
+}
+
+export default ImageUploader

+ 35 - 0
src/components/InputPass.jsx

@@ -0,0 +1,35 @@
+import { Form, Input } from 'antd'
+import React from 'react'
+
+const InputPass = ({ name, label, props, obligatorio = true }) => {
+  return (
+    <Form.Item
+      {...props}
+      name={name}
+      label={label}
+      rules={[
+        obligatorio && {
+          required: true,
+          message: 'Este campo es obligatorio',
+        },
+        {
+          pattern: /^(?=.*[#$%@-_])(?=.*\d)(?=.*[A-Z]).{8,}$/,
+          message:
+            <span>
+              El patrón de la contraseña es poco seguro,
+              <ul>
+                <li>debe contener al menos 8 caracteres</li>
+                <li>una mayuscula</li>
+                <li>un caracter especial (#$%@-_)</li>
+                <li>un número</li>
+              </ul>
+              por ejemplo: Ej3mpl0#</span>
+        },
+      ]}
+    >
+      <Input.Password autoComplete="one-time-code"/>
+    </Form.Item>
+  )
+}
+
+export default InputPass

+ 75 - 0
src/components/MediaCards.jsx

@@ -0,0 +1,75 @@
+import React from 'react';
+import { BsFiletypeTxt, BsFiletypeXml } from 'react-icons/bs';
+import { GrDocument } from 'react-icons/gr';
+import { ImFilePdf } from 'react-icons/im';
+import { SiMicrosoftexcel, SiMicrosoftpowerpoint, SiMicrosoftword } from 'react-icons/si';
+import { Col, Card, Row } from 'antd';
+
+const MediaCards = ({
+  lista
+}) => {
+
+  const miniaturaAdjunto = React.useCallback((archivo) => {
+    let tipo = archivo?.mimetype;
+
+    if (tipo?.indexOf('image') >= 0) {
+      return <img src={archivo?.ruta} alt='Archivo Adjunto' height='40px' />
+    } else if (tipo?.indexOf('sheet') >= 0) {
+      return <SiMicrosoftexcel style={{ fontSize: '50px', color: '#2EB275' }} />
+    } else if (tipo?.indexOf('word') >= 0) {
+      return <SiMicrosoftword style={{ fontSize: '50px', color: '#1755B3' }} />
+    } else if (tipo?.indexOf('pdf') >= 0) {
+      return <ImFilePdf style={{ fontSize: '50px', color: '#FF4343' }} />
+    } else if (tipo?.indexOf('presentation') >= 0) {
+      return <SiMicrosoftpowerpoint style={{ fontSize: '50px', color: '#B7472A' }} />
+    } else if (tipo?.indexOf('xml') >= 0) {
+      return <BsFiletypeXml style={{ fontSize: '50px', color: '#863695' }} />
+    } else if (tipo?.indexOf('text') >= 0) {
+      return <BsFiletypeTxt style={{ fontSize: '50px' }} />
+    } else {
+      return <GrDocument style={{ fontSize: '50px', color: '#863695' }} />
+    }
+  }, [])
+
+  return (
+    <Row gutter={[10, 10]}>
+      {lista?.map((item, index) => (
+        <Col
+          key={index}
+          xs={24}
+          sm={24}
+          md={12}
+          lg={6}
+        >
+          <Card
+            bordered
+            style={{
+              width: '100%'
+            }}
+          >
+            <Row gutter={[10, 10]}>
+              <Col span={24}>
+                <Row gutter={[10, 10]}>
+                  <Col span={6}>
+                    {miniaturaAdjunto(item)}
+                  </Col>
+                  <Col span={18}>
+                    <div
+                      style={{
+                        wordWrap: 'break-word'
+                      }}
+                    >
+                      <a href={item?.ruta} target='blank'>{item?.nombre}</a>
+                    </div>
+                  </Col>
+                </Row>
+              </Col>
+            </Row>
+          </Card>
+        </Col>
+      ))}
+    </Row>
+  )
+}
+
+export default MediaCards

+ 170 - 0
src/components/Select.jsx

@@ -0,0 +1,170 @@
+import React from 'react'
+import PropTypes from 'prop-types'
+import { Select as AntdSelect, Tag, Collapse, Divider } from 'antd'
+import { useModels } from '../hooks'
+import { agregarFaltantes } from '../utilities'
+import { PlusOutlined } from '@ant-design/icons'
+
+const { Panel } = Collapse
+
+const Select = ({
+  modelsParams,
+  labelProp,
+  valueProp,
+  render,
+  append,
+  notIn,
+  deleteSelected,
+  formulario,
+  extraParams,
+  ...props
+}) => {
+
+  const [request, setRequest] = React.useState({})
+  const [buscarValue, setBuscarValue] = React.useState('')
+  const [timer, setTimer] = React.useState(null)
+  const [notInState, setNotIn] = React.useState('')
+
+  const extraParamsMemo = React.useMemo(
+    () => ({ q: buscarValue, notIn: notInState, ...extraParams }),
+    [buscarValue, extraParams, notInState]
+  )
+
+  const requestMemo = React.useMemo(() => ({
+    name: modelsParams?.name || '',
+    ordenar: modelsParams?.ordenar || 'id-desc',
+    limite: modelsParams?.limite || 20,
+    expand: modelsParams?.expand || '',
+    extraParams: extraParamsMemo,
+  }), [extraParamsMemo, modelsParams])
+
+  const Formulario = () => {
+    <></>
+  }
+
+  const {
+    models,
+    modelsLoading,
+    modelsError
+  } = useModels(request)
+
+  const onSearch = (value) => {
+    clearTimeout(timer)
+    const newTimer = setTimeout(() => {
+      setBuscarValue(value)
+    }, 300)
+
+    setTimer(newTimer)
+  }
+
+  const quitarDuplicados = (string) => {
+    if (!string) return
+    const arr = String(string).split(',') || []
+    const sinDuplicados = arr.filter((item, index) => arr.indexOf(item) === index)
+    return sinDuplicados.join(',')
+  }
+
+  if (!render) {
+    render = (value) => value
+  }
+
+  if (!append) {
+    append = []
+  }
+
+  const onDeselect = React.useCallback((labeledValue) => {
+    if (!labeledValue && !notIn) return
+    setNotIn(lastState => {
+      const sinDuplicados = quitarDuplicados(
+        lastState?.length
+          ? lastState += `,${labeledValue}`
+          : labeledValue
+      ).split(',')
+      return sinDuplicados.filter(f => f !== String(labeledValue)).join(',') || ''
+    })
+  }, [notIn])
+
+  React.useEffect(() => {
+    setRequest(requestMemo)
+    return () => {
+      setRequest({})
+    }
+  }, [requestMemo])
+
+  React.useEffect(() => {
+    if (notIn) {
+      setNotIn(lastState => {
+        const sinDuplicados = quitarDuplicados(
+          lastState?.length
+            ? lastState += `,${notIn}`
+            : notIn
+        ).split(',')
+        return sinDuplicados.join(',') || ''
+      })
+    }
+  }, [notIn])
+
+  React.useEffect(() => {
+    if (deleteSelected) {
+      onDeselect(deleteSelected)
+    }
+  }, [deleteSelected, onDeselect])
+
+  if (modelsError) {
+    return <Tag color="red">error al obtener información de selector.</Tag>
+  }
+
+  return (
+    <AntdSelect
+      {...props}
+      showSearch
+      onSearch={onSearch}
+      defaultActiveFirstOption={false}
+      filterOption={false}
+      notFoundContent={null}
+      allowClear={true}
+      style={{ width: '100%' }}
+      loading={modelsLoading}
+      options={models?.length > 0 && agregarFaltantes([...models], [...append], valueProp).map(i => ({
+        ...i,
+        label: render(i[labelProp], i),
+        value: i[valueProp],
+      }))}
+      onDeselect={(labeledValue) => {
+        onDeselect(labeledValue)
+      }}
+      dropdownRender={(optionsm) => (
+        formulario ? <> {optionsm}
+          <Divider
+            style={{
+              margin: '8px 0',
+            }}
+          />
+          <Collapse
+            bordered={false}
+            ghost
+            expandIcon=""
+            expandIconPosition={'end'}
+            className="site-collapse-custom-collapse"
+          >
+            <Panel key="1" extra={<><PlusOutlined/> Agregar </>}>
+              {formulario}
+            </Panel>
+          </Collapse>
+        </> : <>{optionsm}</>
+      )}
+    />
+  )
+}
+
+Select.propTypes = {
+  modelsParams: PropTypes.object.isRequired,
+  labelProp: PropTypes.string.isRequired,
+  valueProp: PropTypes.string.isRequired,
+  render: PropTypes.func,
+  notIn: PropTypes.string,
+  onDeselected: PropTypes.func,
+  deleteSelected: PropTypes.string,
+}
+
+export default Select

+ 139 - 0
src/components/Selector.jsx

@@ -0,0 +1,139 @@
+import React from 'react'
+import PropTypes from "prop-types";
+import { Select as AntdSelect, Tag } from 'antd'
+import { useModels } from '../hooks';
+import { agregarFaltantes } from '../utilities';
+
+const Selector = ({ 
+  modelsParams, 
+  labelProp, 
+  valueProp, 
+  render, 
+  append, 
+  deleteSelected,
+  extraParams,
+  onChange,
+  labelInValue = false,
+  filtered = [],
+  tagOptions,
+  ...props 
+}) => {
+
+  const [request, setRequest] = React.useState({});
+  const [buscarValue, setBuscarValue] = React.useState('');
+  const [timer, setTimer] = React.useState(null);
+  const [selected, setSelected] = React.useState([...filtered]);
+
+  const extraParamsMemo = React.useMemo(() => ({ buscar: buscarValue, ...extraParams  }),
+    [buscarValue, extraParams]
+  );
+
+  const requestMemo = React.useMemo(() => ({
+    name: modelsParams?.name || "",
+    ordenar: modelsParams?.ordenar || "id-desc",
+    limite: modelsParams?.limite || 20,
+    expand: modelsParams?.expand || "",
+    extraParams: extraParamsMemo,
+  }), [extraParamsMemo, modelsParams]);
+
+  const {
+    models: modelsData,
+    modelsLoading: modelsDataLoading,
+    modelsError
+  } = useModels(request);
+
+  const onSearch = (value) => {
+    clearTimeout(timer);
+    const newTimer = setTimeout(() => {
+      setBuscarValue(value);
+    }, 300);
+
+    setTimer(newTimer);
+  };
+
+  if(!render) {
+    render = (value) => value;
+  }
+
+  if (!append) {
+    append = [];
+  }
+
+  const _onChange = (_, option) => {
+    setSelected(ultimoEstado => agregarFaltantes(ultimoEstado, option, "id"));
+  }
+
+  
+  const options = React.useMemo(() => {
+    let aux = agregarFaltantes([...modelsData], [...append], valueProp);
+    aux = aux.filter(item => !selected.find(i => item.id === i.id));
+    return aux;
+  }, [append, modelsData, selected, valueProp]);
+
+  let _options = []
+
+  if (tagOptions) {
+    for (let i = 10; i < 36; i++) {
+      _options.push({
+        value: i.toString(36) + i,
+        label: i.toString(36) + i,
+      });
+    }
+  }
+  
+  React.useEffect(() => {
+    setRequest(requestMemo);
+    return () => {
+      setRequest({});
+    };
+  }, [requestMemo]);
+
+  React.useEffect(() => {
+    if(onChange) {
+      onChange && onChange(selected);
+    }
+  }, [selected, onChange]);
+
+  if(modelsError) {
+    return <Tag color='red'>error al obtener información de selector.</Tag>
+  }
+
+  return (
+    <AntdSelect
+      {...props}
+      labelInValue={labelInValue}
+      mode="multiple"
+      showSearch
+      onSearch={onSearch}
+      defaultActiveFirstOption={false}
+      filterOption={false}
+      notFoundContent={null}
+      allowClear={true}
+      style={{ width: '100%' }}
+      loading={modelsDataLoading}
+      onChange={_onChange}
+      onClear={() => setSelected([])}
+      options={ options.map(i => ({
+        ...i,
+        label: render(i[labelProp], i),
+        value: i[valueProp],
+      }))}
+      onDeselect={(v) => {
+        setSelected(ls => ls.filter(i => i.id !== v.value))
+      }}
+    />
+  )
+}
+
+Selector.propTypes = {
+  modelsParams: PropTypes.object.isRequired,
+  labelProp: PropTypes.string.isRequired,
+  valueProp: PropTypes.string.isRequired,
+  render: PropTypes.func,
+  notIn: PropTypes.string,
+  onDeselected: PropTypes.func,
+  deleteSelected: PropTypes.string,
+  filtered: PropTypes.array,
+};
+
+export default Selector

+ 89 - 0
src/components/Tabla.jsx

@@ -0,0 +1,89 @@
+import React from "react";
+import { Table } from "antd";
+import { useModels, useSortColumns, usePagination } from "../hooks";
+import PropTypes from "prop-types";
+import { emptyRequest } from "../constants/requests";
+
+const Tabla = ({
+  nameURL = "",
+  expand = "",
+  extraParams = null,
+  columns,
+  order,
+  innerRef,
+  scrollX = "80vw",
+  ...props
+}) => {
+  const [columnsData, setColumnsData] = React.useState([]);
+  const [request, setRequest] = React.useState(emptyRequest);
+
+  const { limit, page, configPagination, setTotal } = usePagination();
+
+  const { sortValue, columnsContent } = useSortColumns({
+    columnsData,
+    order: order || "id-desc",
+  });
+
+  const requestParams = React.useMemo(() => {
+    let obj = {
+      name: nameURL || "",
+      ordenar: sortValue,
+      expand: expand || "",
+      limite: limit,
+      pagina: page,
+    };
+
+    if (extraParams !== null) {
+      obj.extraParams = extraParams;
+    }
+    return obj;
+  }, [expand, extraParams, limit, nameURL, page, sortValue]);
+
+  const { models, modelsLoading, modelsPage, refresh } = useModels(request);
+
+  React.useEffect(() => {
+    setRequest(requestParams);
+    return () => setRequest({});
+  }, [requestParams]);
+
+  React.useEffect(() => {
+    setColumnsData(columns);
+  }, [columns]);
+
+  React.useEffect(() => {
+    if (modelsPage) {
+      setTotal(modelsPage?.total);
+    }
+  }, [modelsPage, setTotal]);
+
+  if (innerRef) {
+    innerRef.current = {
+      refresh,
+    };
+  }
+
+  return (
+    <Table
+      {...props}
+      dataSource={models}
+      columns={columnsContent}
+      rowKey={"id"}
+      loading={modelsLoading}
+      pagination={configPagination}
+      style={{ whiteSpace: "pre" }}
+      scroll={{ x: scrollX }}
+      size="small"
+    />
+  );
+};
+
+Tabla.propTypes = {
+  nameURL: PropTypes.string.isRequired,
+  columns: PropTypes.array.isRequired,
+  expand: PropTypes.string,
+  extraParams: PropTypes.object,
+  order: PropTypes.string,
+  scrollX: PropTypes.string,
+};
+
+export default Tabla;

+ 128 - 0
src/components/Upload.jsx

@@ -0,0 +1,128 @@
+import React, {useEffect, useState} from 'react'
+import {Modal, Upload as UploadAntd} from 'antd'
+import {useApp, useAuth} from "../hooks";
+import {ExclamationCircleOutlined, UploadOutlined} from "@ant-design/icons";
+import StatusResponse from "../services/statusResponse";
+import {Respuestas} from "../utilities";
+
+const baseUrl = import.meta.env.VITE_API_URL
+const _baseUrl = baseUrl.replace("v1/", "")
+
+const Upload = ({
+                  action,
+                  onChange,
+                  fileList,
+                  listType,
+                  className,
+                  accept = ".xml, .pdf, .png, .jpg, .doc, .xls, .docx, .xlsx",
+                  text,
+                  onRemove,
+                  loading,
+                  setLoading,
+                  showDownloadIcon = true,
+                  showPreviewIcon = true,
+                  showRemoveIcon = true,
+                  onRefresh,
+                }) => {
+
+  const {token} = useApp();
+  const {confirm} = Modal;
+
+  const [listaArchivos, setListaArchivos] = useState([]);
+
+  const props = {
+    name: 'file',
+    headers: {
+      Authorization: `Bearer ${token}`,
+    },
+    method: "POST",
+    progress: {
+      strokeColor: {
+        '0%': '#108ee9',
+        '100%': '#87d068',
+      },
+      strokeWidth: 3,
+      format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`,
+    },
+  };
+
+
+  const onRemoveFile = async (file) => {
+    let body = {...file};
+    let _listaArchivos = listaArchivos
+
+    if (!file.idMedia)
+      body = {...file?.response?.detalle}
+
+    confirm({
+      title: `¿Estás seguro de eliminar el Archivo ${file.name}?`,
+      icon: <ExclamationCircleOutlined/>,
+      okText: 'Si, Eliminar',
+      okType: 'danger',
+      cancelText: 'Cancelar',
+      onOk: async () => {
+        try {
+          const res = await StatusResponse.post('media/eliminar-archivo', body);
+          if (Respuestas(res)) {
+            _listaArchivos.filter(item => item?.idMedia !== res?.response?.detalle?.idMedia)
+            setListaArchivos(_listaArchivos)
+            onRefresh && onRefresh()
+          }
+        } catch (e) {
+          console.log('Error al guardar: ', e);
+        } finally {
+        }
+      },
+      onCancel() {
+        setListaArchivos(_listaArchivos);
+        onRefresh && onRefresh()
+      },
+    });
+  }
+
+  useEffect(() => {
+    if (fileList) {
+      if (setLoading) {
+        if (fileList?.filter(item => item.status === 'done').length !== listaArchivos?.filter(item => item.status === 'done').length) {
+          let _loading = loading - 1
+          setLoading(_loading)
+        }
+      }
+      setListaArchivos([...fileList])
+    }
+  }, [setLoading, fileList,])
+
+
+  return (
+    <UploadAntd
+      {...props}
+      action={action || _baseUrl + '/v1/media/guardar'}
+      onChange={onChange}
+      multiple={true}
+      fileList={listaArchivos}
+      onRemove={onRemove === false ? false : onRemoveFile}
+      accept={accept}
+      listType={listType}
+      className={className}
+      beforeUpload={(file) => {
+        const isLt2M = file.size;
+        if (isLt2M > 80000000) {
+          Respuestas('error', 'Archivo demasiado grande');
+          return false;
+        }
+      }}
+      showUploadList={{
+        showPreviewIcon: showPreviewIcon,
+        showRemoveIcon: showRemoveIcon,
+        showDownloadIcon: showDownloadIcon,
+      }}
+      percent={(e)=>{console.log(e)}}
+      openFileDialogOnClick={true}
+      style={{width: "100%"}}
+    >
+      {text}
+    </UploadAntd>
+  )
+}
+
+export default Upload

+ 144 - 0
src/components/Uploader.jsx

@@ -0,0 +1,144 @@
+import React from "react";
+import { Upload as AntdUpload } from "antd";
+import { DownloadOutlined } from "@ant-design/icons";
+import { openInNewTab, respuestas } from "../utilities";
+import { useApp } from "../hooks";
+import PropTypes from "prop-types";
+
+const baseUrl = import.meta.env.VITE_API_URL;
+const modulo = import.meta.env.VITE_API_MODULE;
+
+const Uploader = ({
+  endPoint,
+  setReferencias,
+  setListaArchivos,
+  accept = "*",
+  listaArchivos,
+  onRemove = undefined,
+  tipoLista = "picture-card",
+  estilo,
+  ...props
+}) => {
+  const { token } = useApp();
+
+  const { Dragger } = AntdUpload;
+
+  const [lista, setLista] = React.useState([]);
+
+  const _props = {
+    ...props,
+    name: "archivo",
+    headers: {
+      Authorization: `Bearer ${token}`,
+    },
+    method: "POST",
+    progress: {
+      strokeColor: {
+        "0%": "#108ee9",
+        "100%": "#87d068",
+      },
+      strokeWidth: 3,
+      format: (percent) => percent && `${parseFloat(percent.toFixed(2))}%`,
+    },
+  };
+
+  const onFinishSubir = ({ fileList }) => {
+    let listaReferencias = [];
+    let _listaArchivos = [];
+    fileList.forEach((file) => {
+      const res = {
+        status: file?.xhr?.status,
+        mensaje: file?.response?.mensaje,
+      };
+      if (res?.status !== 200) {
+        respuestas(res);
+      }
+      if (file?.id) {
+        listaReferencias.push(file);
+        _listaArchivos.push(file);
+      } else {
+        listaReferencias.push(file?.response?.detalle);
+        _listaArchivos.push({
+          ...file,
+          id: file?.response?.detalle?.id,
+          url: file?.response?.detalle?.ruta,
+          ...file?.response?.detalle,
+        });
+      }
+    });
+    setListaArchivos(_listaArchivos);
+    setReferencias((prev) => {
+      if (prev?.lengt > 0) {
+        return [...prev, ...listaReferencias];
+      } else {
+        return listaReferencias;
+      }
+    });
+  };
+
+  const onChangeArchivos = (info) => {
+    setLista(info.fileList);
+    let fileList = info?.fileList;
+    let l = fileList.length;
+    let completo = true;
+    for (let i = 0; i < l; i++) {
+      if (!fileList[i]?.id /*  && !fileList[i]?.xhr */) {
+        completo = false;
+      }
+    }
+    if (!completo) {
+      onFinishSubir(info);
+    }
+  };
+
+  const onDownloadArchivo = (info) => {
+    openInNewTab(
+      `/v1/media/descarga?id=${
+        info?.id ? info?.id : info?.uid
+      }&access-token=${token}`
+    );
+  };
+
+  React.useEffect(() => {
+    if (listaArchivos) {
+      setLista(listaArchivos);
+      return () => setLista([]);
+    }
+  }, [listaArchivos]);
+
+  return (
+    <Dragger
+      {..._props}
+      action={`${baseUrl}${modulo}${endPoint}`}
+      multiple={true}
+      listType={tipoLista}
+      fileList={lista}
+      onChange={onChangeArchivos}
+      accept={accept}
+      style={estilo ? estilo : { width: "100% !importat", height: "200px" }}
+      onPreview={onDownloadArchivo}
+      onRemove={onRemove}
+    >
+      <p className="ant-upload-drag-icon">
+        <DownloadOutlined />
+      </p>
+      <p className="ant-upload-text">Subir Archivo</p>
+      {!estilo && (
+        <p className="ant-upload-hint">
+          Haga clic y seleccione los archivos o arrástelos y suéltelos sobre
+          este espacio
+        </p>
+      )}
+    </Dragger>
+  );
+};
+
+Uploader.propTypes = {
+  endPoint: PropTypes.string.isRequired,
+  setReferencias: PropTypes.any.isRequired,
+  setListaArchivos: PropTypes.any.isRequired,
+  accept: PropTypes.string,
+  listaArchivos: PropTypes.array.isRequired,
+};
+
+export default Uploader;

+ 20 - 0
src/components/ViewLoading.jsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { Spin } from 'antd';
+
+const ViewLoading = ({Titulo = "Cargando..."}) => {
+  return(
+    <div
+      style={{ 
+        display: 'flex',
+        justifyContent: 'center',
+        alignItems: 'center',
+        height: '100vh',
+        background: '#fff',
+      }}
+    >
+      <Spin tip={Titulo}  size="large" />
+    </div>
+  );
+};
+
+export default ViewLoading;

+ 31 - 0
src/components/index.js

@@ -0,0 +1,31 @@
+import Tabla from "./Tabla";
+import Select from "./Select";
+import Uploader from "./Uploader";
+import FormItem from "./FormItem";
+import AppLoading from "./AppLoading";
+import ButtonGroup from "./ButtonGroup";
+import ActionsButton from "./ActionsButton";
+import BeneficiarioSelect from "./BeneficiariosSelect";
+import ImageUploader from "./ImageUploader";
+import ArbolPermisos from "./ArbolPermisos";
+import ViewLoading from "./ViewLoading";
+import MediaCards from "./MediaCards";
+import InputPass from "./InputPass";
+import Selector from "./Selector";
+
+export {
+  Tabla,
+  Select,
+  FormItem,
+  AppLoading,
+  ButtonGroup,
+  ActionsButton,
+  Uploader,
+  ImageUploader,
+  BeneficiarioSelect,
+  Selector,
+  ViewLoading,
+  MediaCards,
+  InputPass,
+  ArbolPermisos
+};

+ 34 - 0
src/components/layouts/AuthLayout.jsx

@@ -0,0 +1,34 @@
+import React from "react";
+import { Layout } from "antd";
+const { Content, Footer } = Layout;
+const version = import.meta.env.VITE_API_VERSION;
+
+const AuthStyles = {
+  layout: {
+    height: "100vh",
+  },
+  content: {
+    padding: "0 50px",
+    display: "flex",
+    justifyContent: "center",
+    alignItems: "center",
+  },
+  footer: {
+    textAlign: "center",
+  },
+};
+
+const AuthLayout = ({ children }) => {
+  return (
+    <Layout className="layout" style={AuthStyles.layout}>
+      <Content style={AuthStyles.content}>
+        <div className="site-layout-content">{children}</div>
+      </Content>
+      <Footer style={AuthStyles.footer}>
+        {version} - &copy; Derechos reservados.
+      </Footer>
+    </Layout>
+  );
+};
+
+export default AuthLayout;

+ 10 - 0
src/components/layouts/DashboardLayout.css

@@ -0,0 +1,10 @@
+.layout-menu .ant-menu-inline .ant-menu-item {
+  height: 100%;
+  padding: 5px 0 5px
+}
+
+.layout-menu .ant-menu-title-content {
+  white-space: wrap;
+  line-height: 15px;
+  padding: 5px 0 5px;
+}

+ 316 - 0
src/components/layouts/DashboardLayout.jsx

@@ -0,0 +1,316 @@
+import React from 'react'
+import {
+  Layout,
+  Menu,
+  Breadcrumb,
+  Avatar,
+  Button,
+  Row,
+  Col,
+  Dropdown,
+  Tooltip,
+  Typography,
+  Grid,
+} from 'antd'
+import {
+  HomeOutlined,
+  LoginOutlined,
+  LogoutOutlined,
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+  SettingOutlined,
+  UserOutlined,
+} from '@ant-design/icons'
+import { useNavigate, useLocation, Link } from 'react-router-dom'
+import { useAuth } from '../../hooks'
+import { dashboardRoutes } from '../../routers'
+import { Helmet } from 'react-helmet-async'
+import './DashboardLayout.css'
+
+const { Header, Content, Sider, Footer } = Layout
+const rootSubmenuKeys = ['']
+
+const nombrePagina = import.meta.env.VITE_NOMBRE_PAGINA
+const version = import.meta.env.VITE_API_VERSION
+
+const { useBreakpoint } = Grid
+
+const DashboardLayout = ({ children }) => {
+  // const themeMode = "dark";
+  const navigate = useNavigate()
+  const location = useLocation()
+  const { userLoading, signOut, session, user } = useAuth()
+
+  const [collapsed, setCollapsed] = React.useState(false)
+  const [openKeys, setOpenKeys] = React.useState([''])
+  const [selectedKey, setSelectedKey] = React.useState('')
+  const [breadcrumbItems, setBreadcrumbItems] = React.useState([])
+  const [titulo, setTitulo] = React.useState('')
+
+  const pantalla = useBreakpoint()
+
+  const dashStyles = {
+    logoCollapsed: {
+      height: 48,
+      margin: 6,
+      backgroundSize: 'contain',
+      backgroundPosition: 'center center',
+      transition: 'opacity 1s ease-in-out',
+      backgroundImage: `url("/logoCollapsed.png")`,
+      backgroundRepeat: 'no-repeat',
+    },
+    logo: {
+      height: 120,
+      margin: 20,
+      backgroundSize: 'contain',
+      backgroundRepeat: 'no-repeat',
+      backgroundPosition: 'center center',
+      transition: 'opacity 1s ease-in-out',
+      backgroundImage: `url("/logo.png")`,
+    },
+    header: {
+      padding: 0,
+      backgroundColor: '#fff',
+    },
+    trigger: {
+      color: '#333',
+      paddingLeft: 10,
+    },
+    breadcrumb: {
+      display: 'flex',
+      alignItems: 'center',
+      marginLeft: 5,
+      height: '100%',
+    },
+    user: {
+      display: 'flex',
+      justifyContent: 'space-evenly',
+      alignItems: 'center',
+      paddingRight: 10,
+    },
+    footer: {
+      textAlign: 'center',
+    },
+  }
+
+  const items = [
+    {
+      key: '1',
+      label: <Link to="/perfil">Configuración del perfil</Link>,
+      icon: <SettingOutlined/>,
+    },
+    {
+      key: '2',
+      danger: true,
+      label: 'Cerrar sesión',
+      icon: <LoginOutlined/>,
+      onClick: () => signOut(),
+    },
+  ]
+
+  const onOpenChange = (keys) => {
+    const latestOpenKey = keys.find((key) => openKeys.indexOf(key) === -1)
+    if (rootSubmenuKeys.indexOf(latestOpenKey) === -1) {
+      setOpenKeys(keys)
+    } else {
+      setOpenKeys(latestOpenKey ? [latestOpenKey] : [])
+    }
+  }
+
+  const sidebarMapper = (route) => {
+    let puedeVer = user?.permisos.find((permiso) => permiso === route?.ver)
+    if (puedeVer) {
+      if (route.sidebar === 'single') {
+        return {
+          key: route.path,
+          icon: route.icon,
+          label: route.name,
+          onClick: () => {
+            setSelectedKey(route.path)
+            navigate(route.path)
+          },
+        }
+      } else if (route.sidebar === 'collapse') {
+        const innerMap = (r) => ({ ...r, path: route.path + r.path })
+        const finalKey = 'collapse-' + route.path
+        return {
+          key: finalKey,
+          icon: route.icon,
+          label: route.name,
+          children: route.routes.map(innerMap).map(sidebarMapper),
+        }
+      }
+    }
+    return null
+  }
+
+  React.useEffect(() => {
+    const rutasBreadCrumbs = (
+      rutasOrig,
+      rutaDividida,
+      indice = 0,
+      ruta = ''
+    ) => {
+      let rutas = []
+      let path = ''
+      if (indice === 0) {
+        return rutasBreadCrumbs(rutasOrig, rutaDividida, indice + 1)
+      }
+      if (indice > rutaDividida.length - 1) {
+        return rutas
+      }
+      if (rutaDividida.length >= indice + 1 && rutaDividida[indice] !== '') {
+        path = rutasOrig?.find(
+          (r) => r?.path?.indexOf(rutaDividida[indice]) !== -1
+        )
+        if (path !== undefined) {
+          ruta += path?.path
+          rutas.push({
+            name: path?.name,
+            to: ruta,
+            icon: path?.icon,
+          })
+        }
+        rutas = [
+          ...rutas,
+          ...rutasBreadCrumbs(path?.routes, rutaDividida, indice + 1, ruta),
+        ]
+      }
+      return rutas
+    }
+
+    let rutas = [
+      { name: 'Inicio', to: '/', icon: <HomeOutlined/> },
+      ...rutasBreadCrumbs(dashboardRoutes, location?.pathname?.split('/')),
+    ]
+
+    setTitulo(rutas[rutas?.length - 1]?.name)
+    setBreadcrumbItems(rutas)
+  }, [location?.pathname])
+
+  React.useEffect(() => {
+    const flatter = (r) =>
+      r?.routes
+        ? [
+          r,
+          ...r?.routes
+            .map((sub) => ({ ...sub, path: r.path + sub.path }))
+            .flatMap(flatter),
+        ]
+        : r
+    const flatted = dashboardRoutes.flatMap(flatter)
+    const paths = flatted.map((r) => r.path)
+    const key = paths.find((path) => path === location?.pathname)
+    setSelectedKey(key)
+    const tmpOpenKeys = flatted
+      .filter(
+        (r) => r.sidebar === 'collapse' && location?.pathname.includes(r.path)
+      )
+      .map((r) => 'collapse-' + r.path)
+    setOpenKeys(tmpOpenKeys)
+  }, [location])
+
+  if (!session && userLoading) return null
+
+  return (
+    <Layout style={{ minHeight: '100vh' }}>
+      <Helmet>
+        <title>
+          {nombrePagina} - {titulo || ''}{' '}
+        </title>
+      </Helmet>
+      <Sider
+        trigger={null}
+        collapsible
+        theme="light"
+        collapsed={collapsed}
+        width={250}
+      >
+        {/* AQUÍ VA EL LOGO */}
+        <div /* style={collapsed ? dashStyles.logoCollapsed : dashStyles.logo} *//>
+        <Menu
+          theme="light"
+          mode="inline"
+          openKeys={openKeys}
+          onOpenChange={onOpenChange}
+          selectedKeys={selectedKey}
+          items={[
+            ...dashboardRoutes.map(sidebarMapper),
+            {
+              key: 'logout',
+              icon: <LogoutOutlined/>,
+              label: 'Cerrar sesión',
+              onClick: () => signOut(),
+            }
+          ]}
+          className="layout-menu"
+        />
+      </Sider>
+      <Layout className="site-layout">
+        <Header
+          className="site-layout-background"
+          style={dashStyles.header}
+          theme="light"
+        >
+          <Row>
+            <Col span={4} sm={2} xxl={1} style={{ textAlign: 'center' }}>
+              <Button
+                type="link"
+                icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
+                style={dashStyles.trigger}
+                onClick={() => setCollapsed(!collapsed)}
+              />
+            </Col>
+            <Col xs={20} sm={15} md={16} lg={16} xxl={19}>
+              <Breadcrumb
+                style={dashStyles.breadcrumb}
+                items={breadcrumbItems?.map((item, index) => ({
+                  title: (
+                    <Link to={item?.to}>
+                      {item?.icon}
+                      <span> {item?.name} </span>
+                    </Link>
+                  ),
+                }))}
+              />
+            </Col>
+            <Col xs={24} sm={7} md={6} lg={6} xxl={4} style={dashStyles.user}>
+              <Tooltip title={user?.correo}>
+                <div style={{ display: 'flex', flexDirection: 'column' }}>
+                  <Typography.Text strong style={{ textAlign: 'end' }}>
+                    {user?.nombre}
+                  </Typography.Text>
+                  {pantalla?.xl ? (
+                    <Typography.Text type="secondary">
+                      {user?.correo}
+                    </Typography.Text>
+                  ) : (
+                    ''
+                  )}
+                </div>
+              </Tooltip>
+              <Dropdown menu={{ items }}>
+                <Avatar shape="square" size={user?.foto ? 60 : 40} icon={
+                  <UserOutlined/>} src={user?.foto}/>
+              </Dropdown>
+            </Col>
+          </Row>
+        </Header>
+        <Content
+          className="site-layout-background"
+          style={{
+            margin: 10,
+            minHeight: 280,
+          }}
+          children={children}
+        />
+        <br/>
+        <Footer style={dashStyles.footer}>
+          {version} - &copy; Derechos reservados.
+        </Footer>
+      </Layout>
+    </Layout>
+  )
+}
+
+export default DashboardLayout

+ 45 - 0
src/components/layouts/DefaultLayout.jsx

@@ -0,0 +1,45 @@
+import { Spin } from "antd";
+import PropTypes from "prop-types";
+import ButtonGroup from "../ButtonGroup";
+
+const DefaultLayout = ({ children, btnGroup, viewLoading }) => {
+
+  const text = viewLoading?.text || 'Cargando...';
+  const size = viewLoading?.size || 'small' ;
+  const spinning = viewLoading?.spinning || false;
+  
+  const styles = {
+    content: {
+      background: "#fff",
+      padding: "10px 10px 10px 10px",
+      borderRadius: 6,
+      marginBottom: 10,
+      marginTop: 10,
+    },
+    buttons: {
+      background: "#fff",
+      padding: 10,
+      borderRadius: 6,
+    },
+  };
+
+  return (
+    <>
+      <Spin spinning={spinning} tip={text} size={size}>
+      {Boolean(btnGroup) && (
+        <div style={ styles.buttons }>
+          <ButtonGroup data={btnGroup} />
+        </div>
+      )}
+      <div style={styles.content}>{children}</div>
+    </Spin>
+    </>
+  );
+};
+
+DefaultLayout.propTypes = {
+  children: PropTypes.any.isRequired,
+  btnGroup: PropTypes.object,
+};
+
+export default DefaultLayout;

+ 78 - 0
src/components/layouts/SimpleTableLayout.jsx

@@ -0,0 +1,78 @@
+import { Col, Row, Input, Divider } from "antd";
+import PropTypes from "prop-types";
+import DefaultLayout from "./DefaultLayout";
+import ButtonGroup from "../ButtonGroup";
+import React from "react";
+
+const SimpleTableLayout = ({
+  children,
+  btnGroup,
+  onSearch,
+  searchLoading = false,
+  customRender,
+  viewLoading,
+  titulo = null,
+}) => {
+  const styles = {
+    container: {
+      background: "#fff",
+      borderRadius: 6,
+      padding: 10,
+    },
+    inputSearch: {
+      display: "flex",
+      alignItems: "center",
+    },
+  };
+
+  return (
+    <>
+      {Boolean(onSearch) && !Boolean(customRender) && (
+        <div style={styles.container}>
+          <Row gutter={10}>
+            {titulo && (<Col span={24}>{titulo}</Col>)}
+            <Col
+              xs={24}
+              sm={24}
+              md={11}
+              lg={7}
+              xxl={7}
+              style={styles.inputSearch}
+            >
+              <Input.Search
+                enterButton
+                onSearch={onSearch}
+                loading={searchLoading}
+              />
+            </Col>
+            {Boolean(btnGroup) && (
+              <Col xs={24} sm={24} md={13} lg={17} xxl={17}>
+                <ButtonGroup data={btnGroup} />
+              </Col>
+            )}
+          </Row>
+        </div>
+      )}
+
+      {Boolean(customRender) && (
+        <div style={styles.container} children={customRender} />
+      )}
+
+      <DefaultLayout
+        btnGroup={Boolean(onSearch) ? null : btnGroup}
+        viewLoading={Boolean(viewLoading) ? viewLoading : null}
+        children={children}
+      />
+    </>
+  );
+};
+
+DefaultLayout.propTypes = {
+  children: PropTypes.any.isRequired,
+  btnGroup: PropTypes.object,
+  onSearch: PropTypes.object,
+  searchLoading: PropTypes.bool,
+  customRender: PropTypes.element,
+};
+
+export default SimpleTableLayout;

+ 11 - 0
src/components/layouts/index.js

@@ -0,0 +1,11 @@
+import AuthLayout from "./AuthLayout";
+import DashboardLayout from "./DashboardLayout";
+import SimpleTableLayout from "./SimpleTableLayout";
+import DefaultLayout from "./DefaultLayout";
+
+export { 
+  AuthLayout,
+  DashboardLayout,
+  SimpleTableLayout,
+  DefaultLayout
+};

+ 59 - 0
src/constants/httpStatusCodes.js

@@ -0,0 +1,59 @@
+const httpStatusCodes = {
+  ACCEPTED: 202,
+  BAD_GATEWAY: 502,
+  BAD_REQUEST: 400,
+  CONFLICT: 409,
+  CONTINUE: 100,
+  CREATED: 201,
+  EXPECTATION_FAILED: 417,
+  FAILED_DEPENDENCY: 424,
+  FORBIDDEN: 403,
+  GATEWAY_TIMEOUT: 504,
+  GONE: 410,
+  HTTP_VERSION_NOT_SUPPORTED: 505,
+  IM_A_TEAPOT: 418,
+  INSUFFICIENT_SPACE_ON_RESOURCE: 419,
+  INSUFFICIENT_STORAGE: 507,
+  INTERNAL_SERVER_ERROR: 500,
+  LENGTH_REQUIRED: 411,
+  LOCKED: 423,
+  METHOD_FAILURE: 420,
+  METHOD_NOT_ALLOWED: 405,
+  MOVED_PERMANENTLY: 301,
+  MOVED_TEMPORARILY: 302,
+  MULTI_STATUS: 207,
+  MULTIPLE_CHOICES: 300,
+  NETWORK_AUTHENTICATION_REQUIRED: 511,
+  NO_CONTENT: 204,
+  NON_AUTHORITATIVE_INFORMATION: 203,
+  NOT_ACCEPTABLE: 406,
+  NOT_FOUND: 404,
+  NOT_IMPLEMENTED: 501,
+  NOT_MODIFIED: 304,
+  OK: 200,
+  PARTIAL_CONTENT: 206,
+  PAYMENT_REQUIRED: 402,
+  PERMANENT_REDIRECT: 308,
+  PRECONDITION_FAILED: 412,
+  PRECONDITION_REQUIRED: 428,
+  PROCESSING: 102,
+  PROXY_AUTHENTICATION_REQUIRED: 407,
+  REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
+  REQUEST_TIMEOUT: 408,
+  REQUEST_TOO_LONG: 413,
+  REQUEST_URI_TOO_LONG: 414,
+  REQUESTED_RANGE_NOT_SATISFIABLE: 416,
+  RESET_CONTENT: 205,
+  SEE_OTHER: 303,
+  SERVICE_UNAVAILABLE: 503,
+  SWITCHING_PROTOCOLS: 101,
+  TEMPORARY_REDIRECT: 307,
+  TOO_MANY_REQUESTS: 429,
+  UNAUTHORIZED: 401,
+  UNAVAILABLE_FOR_LEGAL_REASONS: 451,
+  UNPROCESSABLE_ENTITY: 422,
+  UNSUPPORTED_MEDIA_TYPE: 415,
+  USE_PROXY: 305,
+};
+
+export default Object.freeze(httpStatusCodes);

+ 38 - 0
src/constants/index.js

@@ -0,0 +1,38 @@
+const dateFormat = 'DD/MM/YYYY'
+const dateUSFormat = 'YYYY-MM-DD'
+
+const localTimeZone = "America/Hermosillo"
+const NON_DIGIT = "/[^d]/g"
+
+const timeFormat12 = 'h:mm a'
+const timeFormat24 = 'HH:mm:ss'
+
+const TIPO_PROJECTO_FIREBASE = "firebase"
+const TIPO_PROJECTO_JWT = "jwt"
+
+const stateOptions = [
+  {
+    value: 1,
+    label: 'Planeación'
+  },
+  {
+    value: 2,
+    label: "En Proceso"
+  },
+  {
+    value: 3,
+    label: 'Finalizado'
+  }
+]
+
+export {
+  dateFormat,
+  dateUSFormat,
+  localTimeZone,
+  NON_DIGIT,
+  timeFormat12,
+  timeFormat24,
+  TIPO_PROJECTO_FIREBASE,
+  TIPO_PROJECTO_JWT,
+  stateOptions
+}

+ 31 - 0
src/constants/requests.js

@@ -0,0 +1,31 @@
+const emptyRequest = () => ({
+  req: null,
+  url: null,
+  params: null,
+  body: null,
+});
+
+const getRequest = (url, params = {}) => ({
+  req: "GET",
+  url,
+  params,
+  body: null,
+});
+
+const postRequest = (url, body, params = {}) => ({
+  req: "POST",
+  url,
+  params,
+  body,
+});
+
+const deleteRequest = (url, id, params = {}) => ({
+  req: "DELETE",
+  url: `${url}/eliminar`,
+  body: {
+    ...params,
+    id: id,
+  },
+});
+
+export { emptyRequest, getRequest, postRequest, deleteRequest };

+ 9 - 0
src/hooks/index.js

@@ -0,0 +1,9 @@
+export * from "./useApp";
+export * from "./useAuth";
+export * from "./useHttp";
+export * from "./useAlert";
+export * from "./useModel";
+export * from "./useModels";
+export * from "./useQuery";
+export * from "./useSortColumns";
+export * from "./usePagination";

+ 66 - 0
src/hooks/useAlert.jsx

@@ -0,0 +1,66 @@
+import React from "react";
+
+const AlertContext = React.createContext();
+
+export function AlertProvider(props) {
+  const [open, setOpen] = React.useState(false);
+  const [position, setPosition] = React.useState({
+    vertical: "bottom",
+    horizontal: "right",
+  });
+  const [severity, setSeverity] = React.useState("info");
+  const [message, setMessage] = React.useState("");
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted) {
+      setTimeout(() => {
+        setOpen(false);
+      }, 5000);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [open]);
+
+  const showAlert = React.useCallback(
+    ({ message, severity = "info", position = null }) => {
+      setOpen(true);
+      setMessage(message);
+      setSeverity(severity);
+      if (position) setPosition(position);
+    },
+    []
+  );
+
+  const memData = React.useMemo(() => {
+    // const closeAlert = () => {
+    //   setOpen(false);
+    //   setTimeout(() => {
+    //     setPosition(defaultPlace);
+    //     setSeverity(defaultColor);
+    //     setIcon(defaultIcon);
+    //     setMessage(defaultMessage);
+    //   }, 2000);
+    // };
+    return {
+      open,
+      position,
+      severity,
+      message,
+      showAlert,
+      // closeAlert,
+    };
+  }, [open, position, severity, message, showAlert]);
+
+  return <AlertContext.Provider value={memData} {...props} />;
+}
+
+export function useAlert() {
+  const context = React.useContext(AlertContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: alert context not defined.";
+  }
+  return context;
+}

+ 34 - 0
src/hooks/useApp.jsx

@@ -0,0 +1,34 @@
+import React from "react";
+const localStorageKey = "usr_jwt";
+
+const AppContext = React.createContext();
+
+export function AppProvider(props) {
+  const [token, setToken] = React.useState(null);
+
+  React.useEffect(() => {
+    const jwt = localStorage.getItem(localStorageKey);
+    setToken(jwt);
+  }, []);
+
+  React.useEffect(() => {
+    if (token && token !== "") {
+      localStorage.setItem(localStorageKey, token);
+    }
+  }, [token]);
+
+  const memData = React.useMemo(() => {
+    return { token, setToken };
+  }, [token, setToken]);
+
+  return <AppContext.Provider value={memData} {...props} />;
+}
+
+export function useApp() {
+  const context = React.useContext(AppContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: app context not defined.";
+  }
+  return context;
+}

+ 102 - 0
src/hooks/useAuth.jsx

@@ -0,0 +1,102 @@
+import React from 'react'
+import { emptyRequest, getRequest, postRequest } from '../constants/requests'
+import { useHttp } from './useHttp'
+import { useApp } from './useApp'
+import { Modal } from 'antd'
+import { ExclamationCircleOutlined } from '@ant-design/icons'
+import { useNavigate } from "react-router-dom";
+
+const AuthContext = React.createContext()
+const empty = emptyRequest()
+
+export function AuthProvider (props) {
+  const { token, setToken } = useApp()
+  const [sessionRequest, setSessionRequest] = React.useState(empty)
+  const [userRequest, setUserRequest] = React.useState(empty)
+  const [session, sessionLoading] = useHttp(sessionRequest)
+  const [userResponse, userResponseLoading, userError] = useHttp(userRequest)
+  const navigate = useNavigate();
+
+  const signIn = React.useCallback(async (correo, password) => {
+    try {
+      if (correo !== '' && password !== '') {
+        const req = postRequest('iniciar-sesion', {
+          correo: correo,
+          clave: password,
+        })
+        setSessionRequest({ ...req })
+      }
+    } catch (e) {
+      console.log(e)
+    }
+  }, [])
+
+  const signOut = React.useCallback(async () => {
+    try {
+      Modal.confirm({
+        title: 'Atención',
+        icon: <ExclamationCircleOutlined/>,
+        content: '¿Estás seguro de que deseas cerrar sesión?',
+        okText: 'Cerrar sesión',
+        cancelText: 'Cancelar',
+        onOk: async () => {
+          setToken(null)
+          setSessionRequest(empty)
+          localStorage.clear()
+          setUserRequest(empty)
+          navigate("/");
+        },
+      })
+    } catch (e) {
+      console.error(e)
+    }
+  }, [navigate, setToken])
+
+  const memData = React.useMemo(() => {
+    return {
+      session: session,
+      sessionLoading: sessionLoading,
+      user: userResponse?.resultado[0],
+      userLoading: userResponseLoading,
+      userError: userError,
+      signIn,
+      signOut,
+    }
+  }, [
+    userError,
+    userResponse,
+    userResponseLoading,
+    session,
+    sessionLoading,
+    signIn,
+    signOut,
+  ])
+
+  React.useEffect(() => {
+    if (session && !sessionLoading) {
+      if (session?.detalle) {
+        setToken(session?.detalle?.token)
+      }
+    }
+  }, [session, sessionLoading, setToken])
+
+  React.useEffect(() => {
+    if (token) {
+      const agendaReq = getRequest('perfil?expand=permisos,estatusPermiso,sujetoObligado')
+      setUserRequest(() => agendaReq)
+    } else {
+      setUserRequest(empty)
+    }
+  }, [token])
+
+  return <AuthContext.Provider value={memData} {...props} />
+}
+
+export function useAuth () {
+  const context = React.useContext(AuthContext)
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw 'error: auth context not defined.'
+  }
+  return context
+}

+ 185 - 0
src/hooks/useHttp.jsx

@@ -0,0 +1,185 @@
+import React from "react";
+import { useAlert } from "./useAlert";
+import { useNavigate } from "react-router-dom";
+import httpCodes from "../constants/httpStatusCodes";
+import { respuestas } from "../utilities";
+
+const baseUrl = import.meta.env.VITE_API_URL;
+const baseModule = import.meta.env.VITE_API_MODULE;
+const baseModulePdf = import.meta.env.VITE_API_MODULE_PDF;
+const baseModuleExcel = import.meta.env.VITE_API_MODULE_EXCEL;
+const baseModuleWord = import.meta.env.VITE_API_MODULE_WORD;
+const baseModulePublico = import.meta.env.VITE_API_MODULE_PUBLIC;
+const localStorageKey = "usr_jwt";
+
+const defaultHeaders = {
+  "Content-Type": "application/json",
+  Accept: "application/json",
+};
+
+const makeHeaders = (token) =>
+  token
+    ? {
+        ...defaultHeaders,
+        Authorization: `Bearer ${token}`,
+      }
+    : defaultHeaders;
+
+const paramsToQuery = (params) =>
+  Object.keys(params)
+    .map(
+      (key) => encodeURIComponent(key) + "=" + encodeURIComponent(params[key])
+    )
+    .join("&");
+
+const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
+
+export function useHttp({
+  req = "GET",
+  url = null,
+  params = null,
+  body = null,
+  alert = false,
+}) {
+  const navigate = useNavigate();
+  const { showAlert } = useAlert();
+  const [response, setResponse] = React.useState(null);
+  const [error, setError] = React.useState(null);
+  const [loading, setLoading] = React.useState(true);
+
+  const refresh = React.useCallback(
+    async (showAlert, inlineParams = {}) => {
+      try {
+        if (!url || !params) {
+          setResponse(null);
+          setError(null);
+          setLoading(true);
+          return;
+        }
+        let modulo;
+        switch (params.modulo) {
+          case "pdf":
+            modulo = baseModulePdf;
+            break;
+          case "excel":
+            modulo = baseModuleExcel;
+            break;
+          case "word":
+            modulo = baseModuleWord;
+            break;
+          case "publico":
+            modulo = baseModulePublico;
+            break;
+          default:
+            modulo = baseModule;
+            break;
+        }
+        if (inlineParams.isCargando === false) {
+          setLoading(() => false);
+        } else {
+          setLoading(() => true);
+        }
+        const jwt = localStorage.getItem(localStorageKey);
+        let fetchReq = {
+          method: req,
+          headers: makeHeaders(jwt),
+        };
+        if (body) {
+          fetchReq = { ...fetchReq, body: JSON.stringify(body) };
+        }
+        const paramsFinal = { ...params, ...inlineParams };
+        const str = `${baseUrl}${modulo}${url}${
+          params && Object.keys(paramsFinal).length > 0
+            ? `?${paramsToQuery(paramsFinal)}`
+            : ""
+        }`;
+        const httpRes = await fetch(str, fetchReq);
+        const resBody = await httpRes.json();
+        switch (httpRes.status) {
+          case httpCodes.OK:
+            setResponse(resBody);
+            setError(null);
+            alert &&
+              showAlert({
+                severity: "success",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Solicitud completada correctamente!",
+              });
+            break;
+          case httpCodes.BAD_REQUEST:
+            window["scrollTo"]({ top: 0, behavior: "smooth" });
+            setError(resBody.errores);
+            alert &&
+              showAlert({
+                severity: "warning",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Datos erróneos o inválidos.",
+              });
+            break;
+          case httpCodes.FORBIDDEN:
+          case httpCodes.UNAUTHORIZED:
+            window["scrollTo"]({ top: 0, behavior: "smooth" });
+            setError(resBody.errores);
+            alert &&
+              showAlert({
+                severity: "warning",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Datos erróneos o inválidos.",
+              });
+            // redirect('/no-autorizado')
+            navigate("/no-autorizado");
+            break;
+          case httpCodes.INTERNAL_SERVER_ERROR:
+          default:
+            alert &&
+              showAlert({
+                severity: "error",
+                message: resBody.mensaje
+                  ? capitalize(resBody.mensaje)
+                  : "Ocurrió un error en el servidor.",
+              });
+        }
+
+        if (str.includes("login"))
+          if (httpRes?.status >= 200 && httpRes?.status < 399)
+            respuestas({
+              mensaje: resBody?.message || resBody.mensaje,
+              status: httpRes.status,
+            });
+          else
+            respuestas({
+              errores: { mensaje: resBody?.message || resBody.mensaje },
+              status: httpRes.status,
+            });
+      } catch (error) {
+        alert &&
+          showAlert({
+            severity: "error",
+            message: "No se pudo establecer conexión con el servidor.",
+          });
+        console.error(error);
+      } finally {
+        setLoading(false);
+      }
+    },
+    [alert, body, navigate, params, req, url]
+  );
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted) {
+      refresh(showAlert);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [refresh, showAlert]);
+
+  return React.useMemo(
+    () => [response, loading, error, refresh],
+    [response, loading, error, refresh]
+  );
+}

+ 90 - 0
src/hooks/useModel.jsx

@@ -0,0 +1,90 @@
+import React from "react";
+import { useNavigate } from "react-router-dom";
+import { emptyRequest, getRequest, postRequest } from "../constants/requests";
+import { useHttp } from "./useHttp";
+
+const empty = emptyRequest();
+
+export function useModel({
+  name,
+  id,
+  fields = null,
+  expand = null,
+  extraParams = null,
+  redirectOnPost = false,
+  path = "guardar",
+}) {
+  const [modelRequest, setProfileRequest] = React.useState(empty);
+  const [model, modelLoading, modelError, refreshModel] = useHttp(modelRequest);
+
+  const [updateRequest, setUpdateRequest] = React.useState(empty);
+  const [postResult, postResultLoading, postResultError] =
+    useHttp(updateRequest);
+  const navigate = useNavigate();
+
+  const updateModel = React.useCallback(
+    (newModel, alert = true) => {
+      if (!postResultLoading) {
+        if (newModel.id) {
+          newModel = { id: newModel.id };
+          delete newModel.id;
+        }
+        const updateReq = postRequest(`${name}/${path}`, newModel);
+        updateReq.alert = alert;
+        setUpdateRequest(updateReq);
+      }
+    },
+    [name, postResultLoading, path]
+  );
+
+  React.useEffect(() => {
+    let mounted = true;
+    if (mounted && postResult && redirectOnPost && !postResultError) {
+      const { pathname } = navigate.location; // todo revisar esto
+      const redirectTo = pathname.split("/").filter((e) => e !== "");
+      navigate.push("/" + redirectTo[0]);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [postResult, redirectOnPost, postResultError, navigate]);
+
+  React.useEffect(() => {
+    if (!name || !id) return;
+    let params = { id: id };
+    if (fields) params = { ...params, fields };
+    if (expand) params = { ...params, expand };
+    if (extraParams) params = { ...params, ...extraParams };
+    const modelReq = getRequest(name, params);
+    setProfileRequest(modelReq);
+  }, [name, id, fields, expand, extraParams]);
+
+  return React.useMemo(() => {
+    let modelTmp = null;
+    if (model && model.resultado && model.resultado.length > 0) {
+      modelTmp = model.resultado[0];
+      if (model.detalle) modelTmp.detalleExtra = model.detalle;
+    }
+    let finalError = {};
+    if (modelError) finalError = { ...modelError };
+    if (postResultError) finalError = { ...postResultError };
+    return {
+      model: modelTmp,
+      modelLoading,
+      modelError: finalError,
+      refreshModel,
+      updateModel,
+      updateModelResult: postResult,
+      updateModelLoading: postResultLoading,
+    };
+  }, [
+    model,
+    modelLoading,
+    modelError,
+    refreshModel,
+    postResult,
+    postResultLoading,
+    postResultError,
+    updateModel,
+  ]);
+}

+ 86 - 0
src/hooks/useModels.jsx

@@ -0,0 +1,86 @@
+import React from "react";
+import { emptyRequest, getRequest, deleteRequest } from "../constants/requests";
+import { useHttp } from "./useHttp";
+
+const empty = emptyRequest();
+
+export function useModels({
+  name,
+  fields = null,
+  expand = null,
+  ordenar = null,
+  limite = null,
+  pagina = null,
+  extraParams = null,
+}) {
+  const [modelRequest, setModelsRequest] = React.useState(empty);
+  const [modelsPage, setModelsPage] = React.useState(null);
+  const [models, modelsLoading, modelsError, refreshModels] = useHttp(modelRequest);
+
+  const [delRequest, setDelRequest] = React.useState(empty);
+  const [deleteResult, deleteResultLoading] = useHttp(delRequest);
+
+  const deleteModel = React.useCallback(
+    async (id) => {
+      if (!deleteResultLoading) {
+        const deleteReq = deleteRequest(name, id);
+        deleteReq.alert = true;
+        setDelRequest(deleteReq);
+      }
+    },
+    [name, deleteResultLoading]
+  );
+
+  React.useEffect(() => {
+    if (!name) {
+      setModelsRequest(empty);
+      return;
+    }
+    let params = {};
+    if (fields) params = { ...params, fields };
+    if (expand) params = { ...params, expand };
+    if (ordenar) params = { ...params, ordenar };
+    if (limite) params = { ...params, limite };
+    if (pagina) params = { ...params, pagina };
+    if (extraParams) params = { ...params, ...extraParams };
+    const modelReq = getRequest(name, params);
+    setModelsRequest(modelReq);
+  }, [name, fields, expand, ordenar, limite, pagina, extraParams]);
+
+  React.useEffect(() => {
+    if (!modelsLoading && !modelsError && models) {
+      const { paginacion } = models;
+      setModelsPage(paginacion);
+    }
+  }, [models, modelsLoading, modelsError]);
+
+  React.useEffect(() => {
+    if (!deleteResultLoading && deleteResult) {
+      refreshModels();
+    }
+  }, [deleteResult, deleteResultLoading, refreshModels]);
+
+  return React.useMemo(() => {
+    let resultado = [];
+    if (models && models.resultado && models.resultado.length > 0) {
+      resultado = [...models.resultado];
+    }
+    let modelsLoadingFinal = modelsLoading || deleteResultLoading;
+    return {
+      models: resultado,
+      modelsLoading: modelsLoadingFinal,
+      modelsError,
+      modelsPage,
+      refresh: refreshModels,
+      deleteModel,
+    };
+  }, [
+    models,
+    modelsLoading,
+    modelsError,
+    modelsPage,
+    refreshModels,
+    deleteResultLoading,
+    deleteModel,
+  ]);
+}

+ 40 - 0
src/hooks/usePagination.jsx

@@ -0,0 +1,40 @@
+import React from "react";
+
+export function usePagination(props) {
+  const [page, setPage] = React.useState(1);
+  const [limit, setLimit] = React.useState(10);
+  const [total, setTotal] = React.useState(0);
+
+  const onSetPageCallback = React.useCallback(async (page, size) => {
+    setPage(page);
+    setLimit(size);
+  }, []);
+
+  const configPagination = React.useMemo(() => {
+    let size = limit;
+
+    return {
+      total: total,
+      pageSize: limit,
+      current: parseInt(page),
+      onShowSizeChange: (_, newSize) => (size = newSize),
+      onChange: async (v) => await onSetPageCallback(v, size),
+      showTotal: (total, range) => `Total: ${total}`,
+      locale: { items_per_page: "/ página" },
+      pageSizeOptions: [10, 20, 30].filter((val) => val <= total),
+      showSizeChanger: true,
+    };
+  }, [limit, onSetPageCallback, page, total]);
+
+  return React.useMemo(() => {
+    return {
+      configPagination,
+      page,
+      limit,
+      total,
+      setPage,
+      setLimit,
+      setTotal,
+    };
+  }, [configPagination, limit, page, total]);
+}

+ 7 - 0
src/hooks/useQuery.jsx

@@ -0,0 +1,7 @@
+import React from "react";
+import { useLocation } from "react-router-dom";
+
+export function useQuery() {
+  const search = useLocation().search;
+  return React.useMemo(() => new URLSearchParams(search), [search]);
+}

+ 52 - 0
src/hooks/useSortColumns.jsx

@@ -0,0 +1,52 @@
+import React from "react";
+
+export function useSortColumns({
+  columnsData = [],
+  order = "",
+  onHeaderCell = null,
+}) {
+  const [sortValue, setSortValue] = React.useState(order);
+  const [columnsContent, setColumnsContent] = React.useState([]);
+
+  const _onHeaderCell = React.useCallback((column) => ({
+    onClick: () => {
+      let _sort = sortValue.indexOf("asc") >= 0 ? "desc" : "asc";
+      setSortValue(
+        `${column?.orden ? column?.orden : column?.dataIndex}-${_sort}`
+      );
+    }
+  }), [sortValue]);
+
+  React.useEffect(() => {
+    const columnsDefaultProps = {
+      sorter: { multiple: 2 },
+      sortOrder: sortValue.indexOf("asc") >= 0 ? "ascend" : "descend",
+      onHeaderCell: _onHeaderCell,
+      showSorterTooltip: false
+    };
+
+    const _columns = columnsData?.map((column) => {
+      column.sortOrder = null;
+      if(column?.orden === false) {
+        return column;
+      }
+      if (column?.orden) {
+        if (sortValue.indexOf(column.orden) >= 0) {
+          column.sortOrder = sortValue.indexOf("asc") >= 0 ? "ascend" : "descend";
+        }
+      } else if (sortValue.indexOf(column.dataIndex) >= 0) {
+        column.sortOrder = sortValue.indexOf("asc") >= 0 ? "ascend" : "descend";
+      }
+      return { ...columnsDefaultProps, ...column };
+    });
+
+    setColumnsContent(_columns);
+  }, [_onHeaderCell, columnsData, sortValue]);
+
+  return React.useMemo(() => {
+    return {
+      sortValue,
+      columnsContent,
+    };
+  }, [sortValue, columnsContent]);
+}

+ 12 - 0
src/main.jsx

@@ -0,0 +1,12 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import { BrowserRouter } from 'react-router-dom';
+
+ReactDOM.createRoot(document.getElementById('root')).render(
+  <React.StrictMode>
+    <BrowserRouter>
+      <App />
+    </BrowserRouter>
+  </React.StrictMode>,
+)

+ 16 - 0
src/routers/AppRouting.jsx

@@ -0,0 +1,16 @@
+import React from "react";
+import { useApp, useAuth } from "../hooks";
+
+import PrivateRouter from "./PrivateRouter";
+import PublicRouter from "./PublicRouter";
+
+const AppRouting = () => {
+  const { sessionLoading } = useAuth();
+  const { token } = useApp();
+
+  if (sessionLoading) return null;
+
+  return Boolean(token) ? <PrivateRouter /> : <PublicRouter />;
+};
+
+export default AppRouting;

+ 38 - 0
src/routers/PrivateRouter.jsx

@@ -0,0 +1,38 @@
+import React from "react";
+import { Routes, Route } from "react-router-dom";
+import { AppLoading } from "../components";
+import { DashboardLayout } from "../components/layouts";
+import { useAuth } from "../hooks";
+import { dashboardRoutes } from "./routes";
+
+const routeMapper = (route, index) =>
+  route?.routes?.length > 0 ? (
+    route?.routes
+      ?.map((r) => ({ ...r, path: route.path + r.path, layout: route.layout }))
+      .map(routeMapper)
+  ) : (
+    <Route
+      key={route.path + (index + 1).toString()}
+      exact={Boolean(route.layout === "dashboard")}
+      path={route.path}
+      // render={(props) => <route.component {...props} route={route} />}
+      element={ <route.element /> }
+    />
+  );
+
+const PrivateRouter = () => {
+
+  const { user, userLoading } = useAuth();
+
+  if (!user && userLoading) {
+    return <AppLoading />;
+  }
+
+  return (
+    <DashboardLayout>
+      <Routes>{dashboardRoutes.map(routeMapper)}</Routes>
+    </DashboardLayout>
+  );
+};
+
+export default PrivateRouter;

+ 20 - 0
src/routers/PublicRouter.jsx

@@ -0,0 +1,20 @@
+import React from "react";
+import { Routes, Route } from "react-router-dom";
+import { AuthLayout } from "../components/layouts";
+import { Ingresar, Recuperar, Registrar } from "../views/auth";
+import { NoEncontrado } from "../views/error";
+
+const PublicRouter = () => {
+  return (
+    <AuthLayout>
+      <Routes>
+        <Route path="/" element={<Ingresar />} />
+        <Route path="/registrar" element={<Registrar />} />
+        <Route path="/recuperar-contrasena" element={<Recuperar />} />
+        <Route path="*" element={<NoEncontrado />} />
+      </Routes>
+    </AuthLayout>
+  );
+};
+
+export default PublicRouter;

+ 4 - 0
src/routers/index.js

@@ -0,0 +1,4 @@
+import AppRouting from "./AppRouting";
+export * from "./routes";
+
+export { AppRouting };

+ 181 - 0
src/routers/routes.jsx

@@ -0,0 +1,181 @@
+import React from "react";
+
+//Íconos de Ant Design
+import {
+  HomeOutlined,
+  UserOutlined,
+  LogoutOutlined,
+  SettingOutlined,
+  FolderOpenOutlined,
+  ControlOutlined,
+  DatabaseOutlined,
+  LockOutlined,
+  UsergroupAddOutlined,
+} from "@ant-design/icons";
+//Íconos de Ant Design
+
+//Íconos React Icons
+//Íconos React Icons
+
+import { /* NoEncontrado,  */NoAutorizado } from "../views/error";
+import { Inicio } from '../views/inicio'
+import { Usuarios, UsuarioDetalle } from "../views/admin/usuarios";
+
+/* CATÁLOGOS */
+/* CATÁLOGOS */
+import { Perfil } from "../views/perfil";
+import { Modulos } from "../views/admin/permisos/modulos";
+import { Permisos } from "../views/admin/permisos/permisos";
+import { Perfiles, PerfilDetalle } from "../views/admin/permisos/perfiles";
+
+/* IMPORTACIONES SISTAI */
+
+const singOutRoute = () => {
+  return "Cargando...";
+};
+
+const sharedRoutes = [
+  {
+    path: "/no-autorizado",
+    element: NoAutorizado,
+  },
+  {
+    path: "/salir",
+    icon: LogoutOutlined,
+    element: singOutRoute,
+  },
+  {
+    path: "*",
+    element: Inicio,
+  },
+];
+
+const dashboardRoutes = [
+  {
+    layout: "dashboard",
+    path: "/",
+    name: "Inicio",
+    icon: <HomeOutlined />,
+    sidebar: "single",
+    ver: "MENU-SOLICITUD",
+    element: Inicio,
+  },
+  {
+    layout: "dashboard",
+    path: "/perfil",
+    name: "Perfil",
+    icon: <UserOutlined />,
+    sidebar: "none",
+    element: Perfil,
+  },
+  {
+    layout: "dashboard",
+    path: "/administracion",
+    name: "Administración",
+    icon: <SettingOutlined />,
+    sidebar: "collapse",
+    ver: "MENU-ADMIN",
+    routes: [
+      {
+        layout: "dashboard",
+        path: "/usuarios",
+        name: "Usuarios",
+        icon: <UserOutlined />,
+        sidebar: "single",
+        ver: "MENU-ADMIN",
+        routes: [
+          {
+            path: "/",
+            element: Usuarios,
+          },
+          {
+            path: "/agregar",
+            element: UsuarioDetalle,
+          },
+          {
+            path: "/editar",
+            element: UsuarioDetalle,
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/permisos",
+        name: "Permisos",
+        icon: <ControlOutlined />,
+        sidebar: "collapse",
+        ver: "MENU-ADMIN",
+        routes: [
+          {
+            layout: "dashboard",
+            path: "/modulos",
+            name: "Módulos",
+            icon: <DatabaseOutlined />,
+            sidebar: "single",
+            ver: "MENU-ADMIN",
+            routes: [
+              {
+                path: "/",
+                element: Modulos,
+              },
+            ],
+          },
+          {
+            layout: "dashboard",
+            path: "/permisos",
+            name: "Permisos",
+            icon: <LockOutlined />,
+            sidebar: "single",
+            ver: "MENU-ADMIN",
+            routes: [
+              {
+                path: "/",
+                element: Permisos,
+              },
+            ],
+          },
+          {
+            layout: "dashboard",
+            path: "/perfiles",
+            name: "Perfiles",
+            icon: <UsergroupAddOutlined />,
+            sidebar: "single",
+            ver: "MENU-ADMIN",
+            routes: [
+              {
+                path: "/",
+                name: "Perfiles",
+                element: Perfiles,
+              },
+              {
+                path: "/agregar",
+                name: "Agregar un Perfil",
+                element: PerfilDetalle,
+              },
+              {
+                path: "/editar",
+                name: "Editar un Perfil",
+                element: PerfilDetalle,
+              },
+            ],
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/catalogos",
+        name: "Catálogos",
+        icon: <FolderOpenOutlined />,
+        sidebar: "collapse",
+        ver: "MENU-ADMIN",
+        routes: [
+        ],
+      },
+    ],
+  },
+  ...sharedRoutes,
+];
+
+const publicRoutes = [...sharedRoutes];
+
+export { dashboardRoutes, publicRoutes };

+ 200 - 0
src/services/httpService.js

@@ -0,0 +1,200 @@
+const localStorageKey = "usr_jwt";
+const baseUrl = import.meta.env.VITE_API_URL;
+const baseModule = import.meta.env.VITE_API_MODULE;
+
+const getCurrentToken = async () => {
+  try {
+    const jwt = localStorage.getItem(localStorageKey);
+    if (!jwt) throw new Error("No hay sesión.");
+    return jwt;
+  } catch (error) {
+    console.error("Error getting token:", error.message);
+    throw new Error("Error getting token.");
+  }
+};
+
+const getHeaders = (token) => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+  Authorization: `Bearer ${token}`,
+});
+
+const getHeadersWithoutToken = () => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+});
+
+const handleFetchErrors = async (response) => {
+  if (!response.ok) {
+    const errorDetails = await response.json().catch(() => ({}));
+    console.error("Request failed:", response.status, errorDetails);
+    throw new Error("Request failed.");
+  }
+  return response;
+};
+
+const HttpService = {
+  get: async (url, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + baseModule + url, {
+      method: "GET",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+    });
+
+    await handleFetchErrors(response);
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: false,
+      status: response.status,
+      resultado: serverResponse?.resultado || serverResponse || null,
+      paginacion: serverResponse?.paginacion || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+
+  getPublico: async (url, withHandlerError = true) => {
+    const response = await fetch(baseUrl + "/publico/" + url, {
+      method: "GET",
+      headers: getHeadersWithoutToken(),
+    });
+
+    if (withHandlerError) await handleFetchErrors(response);
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: false,
+      status: response.status,
+      resultado: serverResponse?.resultado || serverResponse || null,
+      paginacion: serverResponse?.paginacion || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+
+  post: async (url, data, auth = true, type = 1, withHandlerError = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + baseModule + url, {
+      method: "POST",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    if (withHandlerError) await handleFetchErrors(response);
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.error("Error parsing response:", error.message);
+    }
+
+    return {
+      isError: false,
+      status: response.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+
+  postPublico: async (url, data, type = 1, withHandlerError = true) => {
+    const response = await fetch(baseUrl + "/publico/" + url, {
+      method: "POST",
+      headers: getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    if (withHandlerError) await handleFetchErrors(response);
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.error("Error parsing response:", error.message);
+    }
+    return {
+      isError: false,
+      status: response.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+
+  postFormData: async (url, data, auth = true, type = 1) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + baseModule + url, {
+      method: "POST",
+      headers: {
+        Accept: "application/json",
+        Authorization: `Bearer ${token}`,
+      },
+      body: data,
+    });
+
+    await handleFetchErrors(response);
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.error("Error parsing response:", error.message);
+    }
+
+    return {
+      isError: false,
+      status: response.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: response || null,
+      resultado: serverResponse?.resultado || serverResponse || null,
+    };
+  },
+
+  delete: async (url, data, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + baseModule + url, {
+      method: "DELETE",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    await handleFetchErrors(response);
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: false,
+      status: response.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+};
+
+export default HttpService;

+ 1 - 0
src/services/index.js

@@ -0,0 +1 @@
+export * from "./httpService";

+ 24 - 0
src/utilities/InformacionArchivos.jsx

@@ -0,0 +1,24 @@
+import {QuitarObjetosDuplicados} from "./QuitarObjetosDuplicados";
+
+const baseUrl = import.meta.env.VITE_API_URL
+
+export const InformacionArchivos = (archivo, info, setInfo, tipo, token) => {
+  if (!archivo)
+    return;
+  const _baseUrl = baseUrl.replace("v1/", "");
+
+  let _info = info;
+
+  if (!info.includes(archivo?.idMedia))
+    _info.push({
+      idMedia: archivo?.idMedia,
+      name: archivo?.descripcion,
+      uid: archivo?.uuid,
+      url: `${_baseUrl}/v1/descarga/documento?uuid=${archivo?.uuid}&access-token=${token}&t=exp`,
+      status: "done",
+      thumbUrl: `${archivo?.ruta}`,
+      tipo: tipo
+    });
+
+  setInfo( QuitarObjetosDuplicados(_info))
+}

+ 4 - 0
src/utilities/QuitarObjetosDuplicados.jsx

@@ -0,0 +1,4 @@
+export const QuitarObjetosDuplicados = (array, idPrincipal = 'idMedia') => {
+  let hash = {};
+  return array.filter(o => hash[o[idPrincipal]] ? false : hash[o[idPrincipal]] = true)
+}

+ 13 - 0
src/utilities/QuitarSignos.jsx

@@ -0,0 +1,13 @@
+export const QuitarSignos = (values) => {
+  let _values = values
+  if (typeof values === "string")
+    _values = values
+      .replaceAll("$", "")
+      .replaceAll("(", "")
+      .replaceAll(")", "")
+      .replaceAll(" ", "")
+      .replaceAll(",", "");
+
+
+  return parseFloat(_values);
+}

+ 31 - 0
src/utilities/RenderEstatusSolicitudPrimaria.jsx

@@ -0,0 +1,31 @@
+import {CheckCircleOutlined, ClockCircleOutlined, CloseCircleOutlined} from "@ant-design/icons";
+import {Tag} from "antd";
+
+
+export const RenderEstatusSolicitudPrimaria = ({item, style = {fontSize: 13}}) => {
+
+  if (!item) return "error";
+
+  let color = "";
+  let icon = "";
+
+  if (item === "NUEVO") {
+    icon = <ClockCircleOutlined/>;
+    color = "default";
+  } else if (item === "ENVIADO") {
+    icon = <ClockCircleOutlined/>;
+    color = "default";
+  } else if (item === "APROBADO") {
+    icon = <CheckCircleOutlined/>;
+    color = "success";
+  } else if (item === "RECHAZADO") {
+    icon = <CloseCircleOutlined/>;
+    color = "error";
+  }
+
+  return (
+    <Tag icon={icon} color={color} style={style}>
+      {item}
+    </Tag>
+  )
+};

+ 13 - 0
src/utilities/ValidarPermisosVista.jsx

@@ -0,0 +1,13 @@
+import {useAuth} from "../hooks";
+/**
+ * @ValidarPermisoVista
+ * Valida que un usuario tenga permisos nesesarios para entrar en la vista, si no regresa a la ultima pagina visitada
+ * @permiso.- es el id del permiso creado en tabla de permisos
+ * @permisoExtra.- Trae los isPermiso con los que cuenta el usuario
+ */
+export const ValidarPermisosVista = (permiso) => {
+  const {user} = useAuth();
+  if (permiso && user?.permisoExtra && !user?.permisoExtra?.includes(permiso)) {
+    window.history.back();
+  }
+}

+ 11 - 0
src/utilities/estatusExpediente.jsx

@@ -0,0 +1,11 @@
+const ATR = "ATR";
+const ACO = "ACO";
+const HIS = "HIS";
+const BAJ = "BAJ";
+
+export const estatusExpediente = [
+  { value: ATR, label: "Trámite" },
+  { value: ACO, label: "Concentración" },
+  { value: HIS, label: "Histórico" },
+  { value: BAJ, label: "Baja" }
+];

+ 396 - 0
src/utilities/index.jsx

@@ -0,0 +1,396 @@
+import { NON_DIGIT } from '../constants'
+import { Modal, notification } from 'antd'
+import { DeleteOutlined } from '@ant-design/icons'
+import httpService from '../services/httpService'
+import React from 'react'
+import { estatusExpediente } from './estatusExpediente'
+import { inventarioConcentracion } from './inventarioConcentracion'
+import { reporteExpediente } from './reporteExpediente'
+import { obtenerExtensionImagen } from './obtenerExtencionImagen'
+import { ValidarPermisosVista } from './ValidarPermisosVista'
+import { RenderEstatusSolicitudPrimaria } from './RenderEstatusSolicitudPrimaria'
+import { QuitarObjetosDuplicados } from './QuitarObjetosDuplicados'
+import { InformacionArchivos } from './InformacionArchivos'
+import { QuitarSignos } from './QuitarSignos'
+
+const baseUrl = import.meta.env.VITE_API_URL
+
+export const abrirArchivo = (url) => {
+  if (url) {
+    const a = document.createElement('a')
+    a.target = '_blank'
+    a.href = url
+    a.click()
+  }
+  return null
+}
+
+const openInNewTab = (ruta) => {
+  window.open(`${baseUrl}${ruta}`, '_blank', 'noopener,noreferrer')
+}
+
+const capitalizeFirst = (string) => {
+  const split = string.split('-')
+  let palabraUnida = ''
+  split.forEach((s) => {
+    palabraUnida = palabraUnida + s.charAt(0).toUpperCase() + s.slice(1)
+  })
+  return palabraUnida
+}
+
+const propertyAccesor = (rootObj, accesor = '') => {
+  if (!rootObj) return ''
+  const properties = accesor.split('.')
+  let tmp = rootObj
+  properties.forEach((prop) => (tmp = tmp[prop]))
+  return tmp.toString()
+}
+
+const serialDateToJSDate = serial => {
+  const step = new Date().getTimezoneOffset() <= 0 ? 25567 + 2 : 25567 + 1
+  const utc_days = Math.floor(serial - step)
+  const utc_value = utc_days * 86400
+  const date_info = new Date(utc_value * 1000)
+  const fractional_day = serial - Math.floor(serial) + 0.0000001
+  let total_seconds = Math.floor(86400 * fractional_day)
+  const seconds = total_seconds % 60
+  total_seconds -= seconds
+  const hours = Math.floor(total_seconds / (60 * 60))
+  const minutes = Math.floor(total_seconds / 60) % 60
+  return new Date(date_info.getFullYear(), date_info.getMonth(), date_info.getDate(), hours, minutes, seconds)
+}
+
+const validateName = (name) => {
+  let re = /^[a-zA-Z]+(([',. -][a-zA-Z ])?[a-zA-Z]*)*$/
+  return re.test(name)
+}
+
+const validateNumber = (number) => {
+  const intValue = number.toString().replace(NON_DIGIT, '')
+  return !isNaN(intValue)
+}
+
+const agregarFaltantes = (data, newData, campo) => {
+  let ids = data.map(item => item[campo])
+  let aux = [...data]
+  for (let i in newData) {
+    let modelo = newData[i]
+    if (!modelo) {
+      continue
+    }
+    const indice = ids.indexOf(modelo[campo])
+    if (modelo && indice === -1) {
+      aux.push(modelo)
+    } else {
+      aux[indice] = modelo
+    }
+  }
+  return aux
+}
+
+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: 'danger',
+    },
+    cancelText: 'Cancelar',
+    onOk: async () => {
+      try {
+        const res = await httpService.delete(url, { id: id })
+        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 respuestas = (res) => {
+
+  let estatus = false
+
+  if (!res) return 'Error en respuesta'
+  console.log(res)
+
+  if ((res?.status >= 400 && res?.status < 499)) {
+    if (res?.errores !== null) {
+      const errores = Object.values(res?.errores)
+      notification.error({
+        message: 'Atención',
+        description: errores.map((e, i) => <React.Fragment key={`${i}-error`}><span>- {e}</span><br/></React.Fragment>),
+        placement: 'bottomRight'
+      })
+      estatus = false
+    } else {
+      notification.error({
+        message: 'Atención',
+        description: res?.mensaje ? res?.mensaje : 'Hubo un problema del lado del servidor.',
+        placement: 'bottomRight'
+      })
+      estatus = false
+    }
+  } else if (res?.status >= 500) {
+    notification.error({
+      message: 'Atención',
+      description: 'Hubo un problema del lado del servidor.',
+      placement: 'bottomRight'
+    })
+    estatus = false
+  } else if (res?.status >= 200 && res?.status < 299) {
+    notification.success({
+      message: '¡Éxito!',
+      description: `${res?.mensaje}`,
+      placement: 'bottomRight'
+    })
+    estatus = true
+  }
+
+  return estatus
+}
+
+const generateDefaultChartOptions = (chartType = 'pie', options = {}, callback) => ({
+  chart: {
+    type: chartType,
+    inverted: options.inverted || false,
+    options3d: {
+      enabled: chartType === 'pie',
+      alpha: 45,
+      beta: 0,
+    },
+    height: options.chartHeight || null,
+  },
+  colors: options?.colores || ['#2f7ed8', '#0d233a', '#8bbc21', '#910000', '#1aadce', '#492970', '#f28f43', '#77a1e5', '#c42525', '#a6c96a'],
+  credits: {
+    enabled: false,
+  },
+  title: {
+    text: options?.titulo || 'TITULO POR DEFAULT',
+  },
+  plotOptions: {
+    [chartType]: {
+      innerSize: 100,
+      depth: 45,
+      events: {
+        click: typeof callback === 'function' ? callback : () => {},
+      },
+      series: {
+        stacking: 'normal'
+      }
+    },
+  },
+  series: [
+    {
+      name: options?.nombre || 'NOMBRE DE LA COLECCION DE DATOS',
+      data: options?.datos || [],
+    },
+  ],
+  subtitle: {
+    text: options?.subtitulo || 'SUBTITULO POR DEFAULT',
+  },
+  ...options?.options
+})
+
+const lastPathName = () => {
+  const url = window.location.pathname
+  return {
+    lastPath: url.split('/').pop(), // cambiar por pathname
+    beforePath: url.split('/')[url.split('/').length - 2]
+  }
+}
+
+const quitarSignos = (values) => {
+  let _values = values
+  if (typeof values === 'string')
+    _values = values.replaceAll('$', '').replaceAll(',', '')
+
+  return parseFloat(_values)
+}
+
+function makeKey (length) {
+  let result = ''
+  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+  const charactersLength = characters.length
+  for (var i = 0; i < length; i++) {
+    result += characters.charAt(Math.floor(Math.random() * charactersLength))
+  }
+  return result
+}
+
+function eliminarObjeto (arr, key) {
+  const obj = arr.findIndex((obj) => obj.key === key)
+  if (obj > -1) {
+    arr.splice(obj, 1)
+  }
+  return arr
+}
+
+const isEllipsis = (columns, key) => {
+  const obtenerColumna = columns.find(column => column.key === key)
+  return Boolean(obtenerColumna && obtenerColumna?.ellipsis)
+}
+
+const getRandomUid = () => Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
+
+const renderTotal = (array, prop) => {
+  if (prop?.length > 0 && array?.length > 0) {
+    const total = array.reduce((acc, curr) => acc + Number(curr[prop]), 0)
+    return Number(total) > 0
+      ? Number(total).toFixed(2)
+      : (0).toFixed(2)
+  }
+  return (0).toFixed(2)
+}
+
+const FormatoPesos = (number = 0) => {
+  const exp = /(\d)(?=(\d{3})+(?!\d))/g
+  const rep = '$1,'
+  return number?.toString().replace(exp, rep)
+}
+
+const ValidarRfc = (item) => {
+  let re = /^[A-Z&Ñ]{3,4}[0-9]{2}(0[1-9]|1[012])(0[1-9]|[12][0-9]|3[01])[A-Z0-9]{2}[0-9A]$/
+  let validado = item.match(re)
+
+  if (!validado)
+    return false
+  else
+    return true
+}
+
+const ValidarTelefono = (item) => {
+  let re = /^[0-9]{10}$/
+  let validado = item.match(re)
+
+  if (!validado)
+    return false
+  else
+    return true
+}
+
+const ValidarCorreo = (item) => {
+  let re = /^[^@]+@[^@]+\.[a-zA-Z]{2,}$/
+  let validado = item.match(re)
+
+  if (!validado)
+    return false
+  else
+    return true
+}
+
+const GetMesTexto = (number, corto = false) => {
+  let mes = ''
+  let mesCorto = ''
+  let numero = parseInt(number)
+
+  if (numero === 1) {
+    mes = 'Enero'
+    mesCorto = 'Ene'
+  }
+  if (numero === 2) {
+    mes = 'Febrero'
+    mesCorto = 'Feb'
+  }
+  if (numero === 3) {
+    mes = 'Marzo'
+    mesCorto = 'Mar'
+  }
+  if (numero === 4) {
+    mes = 'Abril'
+    mesCorto = 'Abr'
+  }
+  if (numero === 5) {
+    mes = 'Mayo'
+    mesCorto = 'May'
+  }
+  if (numero === 6) {
+    mes = 'Junio'
+    mesCorto = 'Jun'
+  }
+  if (numero === 7) {
+    mes = 'Julio'
+    mesCorto = 'Jul'
+  }
+  if (numero === 8) {
+    mes = 'Agosto'
+    mesCorto = 'Ago'
+  }
+  if (numero === 9) {
+    mes = 'Septiembre'
+    mesCorto = 'Sep'
+  }
+  if (numero === 10) {
+    mes = 'Octubre'
+    mesCorto = 'Oct'
+  }
+  if (numero === 11) {
+    mes = 'Noviembre'
+    mesCorto = 'Nov'
+  }
+  if (numero === 12) {
+    mes = 'Diciembre'
+    mesCorto = 'Dic'
+  }
+
+  if (corto)
+    return mesCorto
+  else
+    return mes
+}
+
+const formatearMoneda = new Intl.NumberFormat('es-MX', { style: 'currency', currency: 'MXN' }).format
+
+export {
+  agregarFaltantes,
+  capitalizeFirst,
+  propertyAccesor,
+  serialDateToJSDate,
+  validateName,
+  validateNumber,
+  QuitarObjetosDuplicados,
+  eliminarRegistro,
+  generateDefaultChartOptions,
+  respuestas,
+  lastPathName,
+  makeKey,
+  eliminarObjeto,
+  openInNewTab,
+  quitarSignos,
+  isEllipsis,
+  getRandomUid,
+  renderTotal,
+  FormatoPesos,
+  ValidarRfc,
+  InformacionArchivos,
+  ValidarTelefono,
+  ValidarCorreo,
+  estatusExpediente,
+  inventarioConcentracion,
+  obtenerExtensionImagen,
+  RenderEstatusSolicitudPrimaria,
+  reporteExpediente,
+  GetMesTexto,
+  QuitarSignos,
+  ValidarPermisosVista,
+  formatearMoneda,
+}

+ 258 - 0
src/utilities/inventarioConcentracion.jsx

@@ -0,0 +1,258 @@
+import * as ExcelJS from 'exceljs';
+import {saveAs} from 'file-saver';
+import imageToBase64 from 'image-to-base64/browser';
+import {message} from 'antd';
+import {obtenerExtensionImagen} from "./obtenerExtencionImagen";
+import moment from 'moment';
+import {GetMesTexto} from "./index";
+
+export const inventarioConcentracion = async (cols, data, nombre = 'archivo-excel', titulo = '', subtitulo = '', path = null, tipo = "") => {
+  try {
+    const workbook = new ExcelJS.Workbook();
+
+    const worksheet = workbook.addWorksheet(titulo);
+
+    if (path !== null && typeof path === 'string') {
+      const img64 = await imageToBase64(path);
+      const idImagen = workbook.addImage({
+        base64: img64,
+        extension: obtenerExtensionImagen(path),  // * jpg, gif, png
+      });
+      worksheet.addImage(idImagen, {  // * Aquí se acomoda la imagen
+        tl: {col: 0.2, row: 0.2}, // * midpoints
+        ext: {width: 258, height: 98},
+      });
+    }
+
+    const header = cols?.map(c => (c.title));
+
+    worksheet.columns = cols
+
+    const styleTitle = { // * estilo para el título
+      font: {
+        bold: true,
+        size: 18,
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+    };
+    const styleSub = { // * estilo para el título
+      font: {
+        bold: true,
+        size: 12,
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+    };
+    const rowHeaderStyle = { // * estilo para el título
+      font: {
+        bold: true,
+        size: 8,
+        color: {argb: 'FFFFFFFF'}
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+      fill: {
+        type: "pattern",
+        pattern: "solid",
+        bgColor: {argb: 'FFFFFFFF'},
+        fgColor: {argb: '00736C'}
+      }
+    };
+    const border = { //estilo de borde
+      top: {style: 'thin'},
+      left: {style: 'thin'},
+      bottom: {style: 'thin'},
+      right: {style: 'thin'}
+    }
+
+
+    worksheet.mergeCells('A1:D5');  // * combinar celdas  (lugar imagen)
+
+    const row1 = worksheet.getRow("2");
+    row1.height = 18;
+    const row2 = worksheet.getRow("3");
+    row2.height = 23.25;
+    const row3 = worksheet.getRow("4");
+    row3.height = 16.5;
+
+    // * Despues de mergeCells se debe aplicar estilos y valores
+    worksheet.mergeCells('E3:K3')
+    worksheet.getCell('E3').value = titulo; // * valor de la celda que se combinará
+    worksheet.getCell('E3').style = styleTitle; // * estilo de la celda que se combinará
+
+    // * Despues de mergeCells se debe aplicar estilos y valores
+    worksheet.mergeCells('E4:K4')
+    worksheet.getCell('E4').value = subtitulo; // * valor de la celda que se combinará
+    worksheet.getCell('E4').style = styleSub; // * estilo de la celda que se combinará
+
+    worksheet.addRow([]);
+    worksheet.addRow(header);
+
+    worksheet.mergeCells('A6:A7');
+    worksheet.getCell('A6').value = 'Consecutivo';
+    worksheet.getCell('A6').style = rowHeaderStyle;
+    worksheet.getCell('A6').border = border;
+    worksheet.getCell('A7').border = border;
+
+    worksheet.mergeCells('B6:B7');
+    worksheet.getCell('B6').value = 'Clasificación Archivística';
+    worksheet.getCell('B6').style = rowHeaderStyle;
+    worksheet.getCell('B6').border = border;
+    worksheet.getCell('B7').border = border;
+
+    worksheet.mergeCells('C6:C7');
+    worksheet.getCell('C6').value = 'Núm. de Expediente por Serie';
+    worksheet.getCell('C6').style = rowHeaderStyle;
+    worksheet.getCell('C6').border = border;
+    worksheet.getCell('C7').border = border;
+
+    worksheet.mergeCells('D6:D7');
+    worksheet.getCell('D6').value = 'Legajos';
+    worksheet.getCell('D6').style = rowHeaderStyle;
+    worksheet.getCell('D6').border = border;
+    worksheet.getCell('D7').border = border;
+
+    worksheet.mergeCells('E6:E7');
+    worksheet.getCell('E6').value = 'Fojas';
+    worksheet.getCell('E6').style = rowHeaderStyle;
+    worksheet.getCell('E6').border = border;
+    worksheet.getCell('E7').border = border;
+
+    worksheet.mergeCells('F6:F7');
+    worksheet.getCell('F6').value = 'Título y Descripción del Expediente/Asunto';
+    worksheet.getCell('F6').style = rowHeaderStyle;
+    worksheet.getCell('F6').border = border;
+    worksheet.getCell('F7').border = border;
+
+    worksheet.mergeCells('G6:H6');
+    worksheet.getCell('G6').value = 'Período';
+    worksheet.getCell('G6').style = rowHeaderStyle;
+    worksheet.getCell('G6').border = border;
+    worksheet.getCell('H6').border = border;
+
+    worksheet.getCell('G7').value = 'Apertura';
+    worksheet.getCell('G7').style = rowHeaderStyle;
+    worksheet.getCell('G7').border = border;
+
+    worksheet.getCell('H7').value = 'Cierre';
+    worksheet.getCell('H7').style = rowHeaderStyle;
+    worksheet.getCell('H7').border = border;
+
+   worksheet.mergeCells('I6:I7');
+    worksheet.getCell('I6').value = 'Valoración Primaria';
+    worksheet.getCell('I6').style = rowHeaderStyle;
+    worksheet.getCell('I6').border = border;
+    worksheet.getCell('I7').border = border;
+
+    worksheet.mergeCells('J6:J7');
+    worksheet.getCell('J6').value = 'Valoración Secundaria';
+    worksheet.getCell('J6').style = rowHeaderStyle;
+    worksheet.getCell('J6').border = border;
+    worksheet.getCell('J7').border = border;
+
+    worksheet.mergeCells('K6:L6');
+    worksheet.getCell('K6').value = 'Vigencia Documental';
+    worksheet.getCell('K6').style = rowHeaderStyle;
+    worksheet.getCell('K6').border = border;
+    worksheet.getCell('L6').border = border;
+
+    worksheet.getCell('K7').value = 'AT';
+    worksheet.getCell('K7').style = rowHeaderStyle;
+    worksheet.getCell('K7').border = border;
+
+    worksheet.getCell('L7').value = 'AC';
+    worksheet.getCell('L7').style = rowHeaderStyle;
+    worksheet.getCell('L7').border = border;
+
+    worksheet.mergeCells('M6:M7');
+    worksheet.getCell('M6').value = 'Destino Final';
+    worksheet.getCell('M6').style = rowHeaderStyle;
+    worksheet.getCell('M6').border = border;
+    worksheet.getCell('M7').border = border;
+
+
+    worksheet.mergeCells('N6:N7');
+    worksheet.getCell('N6').value = 'Clasificación de la Información';
+    worksheet.getCell('N6').style = rowHeaderStyle;
+    worksheet.getCell('N6').border = border;
+    worksheet.getCell('N7').border = border;
+
+    worksheet.mergeCells('O6:O7');
+    worksheet.getCell('O6').value = 'Archivos';
+    worksheet.getCell('O6').style = rowHeaderStyle;
+    worksheet.getCell('O6').border = border;
+    worksheet.getCell('O7').border = border;
+
+    worksheet.mergeCells('P6:P7');
+    worksheet.getCell('P6').value = 'Signatura Topografica';
+    worksheet.getCell('P6').style = rowHeaderStyle;
+    worksheet.getCell('P6').border = border;
+    worksheet.getCell('P7').border = border;
+
+    worksheet.mergeCells('Q6:Q7');
+    worksheet.getCell('Q6').value = 'Soporte';
+    worksheet.getCell('Q6').style = rowHeaderStyle;
+    worksheet.getCell('Q6').border = border;
+    worksheet.getCell('Q7').border = border;
+
+    if(tipo === 'concentracion') {
+
+      worksheet.mergeCells('R6:R7');
+      worksheet.getCell('R6').value = 'Cotejo';
+      worksheet.getCell('R6').style = rowHeaderStyle;
+      worksheet.getCell('R6').border = border;
+      worksheet.getCell('R7').border = border;
+
+      worksheet.mergeCells('S6:S7');
+      worksheet.getCell('S6').value = 'Caja';
+      worksheet.getCell('S6').style = rowHeaderStyle;
+      worksheet.getCell('S6').border = border;
+      worksheet.getCell('S7').border = border;
+
+      worksheet.mergeCells('T6:T7');
+      worksheet.getCell('T6').value = 'Observaciones';
+      worksheet.getCell('T6').style = rowHeaderStyle;
+      worksheet.getCell('T6').border = border;
+      worksheet.getCell('T7').border = border;
+
+    }
+    worksheet.mergeCells('M5:V5');
+    worksheet.getCell('M5').value = `Hermosillo, Sonora, a ${moment().format("DD")} de ${GetMesTexto(moment().format("MM"))} del año ${moment().format("YYYY")}`;
+
+
+    for (let i = 0; i < data?.length; i++) { // * agregar datos (contenido)
+      const row = data[i];
+      worksheet.addRow(row);
+    }
+
+    worksheet.columns.forEach((column) =>{
+      var dataMax = 2;
+      column.eachCell({ includeEmpty: true }, (cell) =>{
+        var columnLength = cell.value?.length;
+        if (columnLength > dataMax) {
+          dataMax = columnLength;
+        }
+      })
+      column.width = dataMax;
+    });
+
+    workbook.xlsx.writeBuffer().then((data) => {
+      const blob = new Blob([data], {type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'});
+      saveAs(blob, `${nombre}.xlsx`);
+    });
+
+  } catch (error) {
+    console.log(error);
+    message.error('Ocurrió un Error al Importar a Excel');
+  }
+}

+ 4 - 0
src/utilities/obtenerExtencionImagen.jsx

@@ -0,0 +1,4 @@
+export const obtenerExtensionImagen = (path) => {
+  const ext = path.split('.').pop();
+  return ext.toLowerCase();
+}

+ 158 - 0
src/utilities/reporteExpediente.jsx

@@ -0,0 +1,158 @@
+import * as ExcelJS from 'exceljs';
+import { saveAs } from 'file-saver';
+import imageToBase64 from 'image-to-base64/browser';
+import { message } from 'antd';
+import {obtenerExtensionImagen} from "./obtenerExtencionImagen";
+
+export const reporteExpediente = async (cols, data, nombre = 'archivo-excel', titulo = '', subtitulo = '', path = null) => {
+  try {
+    const workbook = new ExcelJS.Workbook();
+    const worksheet = workbook.addWorksheet(titulo);
+
+    if(path !== null && typeof path === 'string') {
+      const img64 = await imageToBase64(path);
+      const idImagen = workbook.addImage({
+        base64: img64,
+        extension: obtenerExtensionImagen(path),  // * jpg, gif, png
+      })
+
+      worksheet.addImage(idImagen, {  // * Aquí se acomoda la imagen
+        tl: { col: 0.2, row: 0.2 }, // * midpoints
+        ext: { width: 208, height: 111 },
+      })
+
+    }
+
+    const header = cols?.map(c => (c.title));
+    worksheet.columns = cols;
+
+    const styleTitle = {
+      font: {
+        bold: true,
+        size: 18,
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'center',
+        wrapText: true
+      },
+    };
+
+    const styleSubTitle = {
+      font: {
+        bold: true,
+        size: 12,
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+    };
+
+    const stylesTotales = {
+      font: {
+        bold: false,
+        size: 12,
+        color: {argb: '693b7c'}
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+    };
+
+    const rowHeaderStyle = { // * estilo para el título
+      font: {
+        bold: true,
+        size: 12,
+        color: {argb:'FFFFFFFF'}
+      },
+      alignment: {
+        horizontal: 'center',
+        vertical: 'middle',
+        wrapText: true
+      },
+      fill: {
+        type: "pattern",
+        pattern: "solid",
+        bgColor: {argb: 'FFFFFFFF'},
+        fgColor: {argb: '00736C'}
+      }
+    };
+
+    worksheet.mergeCells('A1:B6') // Logo
+    worksheet.mergeCells('D1:E1') // Titulo
+    worksheet.mergeCells('D2:E2')
+    worksheet.mergeCells('D3:E3') // Subtitulo
+    worksheet.mergeCells('D4:E4')
+    worksheet.mergeCells('D5:E5') // Totales
+
+    worksheet.addRow(header)
+    for( let i = 0; i < data?.length; i++ ) {
+      let row = data[i]
+      worksheet.addRow(row)
+    }
+
+    worksheet.columns.forEach(( column, index ) => {
+
+      let dataMax = 0;
+      column.eachCell({ includeEmpty: true }, (cell, index) => {
+
+        if((index % 2) === 0  && index > 7){
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "E7E7E7" },
+          };
+        }
+
+
+        if(index === 7){
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "009688" },
+          };
+        }
+
+        let columnLength = cell.value?.length;
+        if( columnLength > dataMax ) {
+          dataMax = columnLength;
+        }
+      })
+      column.width = dataMax < 10 ? 10 : dataMax;
+    })
+
+
+    const colA = worksheet.getColumn("A")
+    colA.width = 15
+    const colB = worksheet.getColumn("B")
+    colB.width = 15
+    const colC = worksheet.getColumn("C")
+    colC.width = 15
+
+    worksheet.getCell('D1').value = titulo
+    worksheet.getCell('D1').style = styleTitle
+    worksheet.getCell('D1').alignment = { vertical: 'middle', horizontal: 'center' }
+
+    worksheet.getCell('D3').value = subtitulo
+    worksheet.getCell('D3').style = styleSubTitle
+    worksheet.getCell('D3').alignment = { vertical: 'middle', horizontal: 'center' }
+
+    worksheet.getCell('D5').value = `Totales: ${data?.length}`
+    worksheet.getCell('D5').style = stylesTotales
+    worksheet.getCell('D5').alignment = { vertical: 'middle', horizontal: 'center' }
+
+
+    workbook.xlsx.writeBuffer().then((data) => {
+      const blob = new Blob([data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
+      saveAs(blob, `${nombre}.xlsx`);
+    });
+
+  } catch(error) {
+    console.log(error);
+    message.error('Ocurrió un Error al Importar a Excel');
+  }
+}

+ 239 - 0
src/views/admin/permisos/modulos/Modulos.jsx

@@ -0,0 +1,239 @@
+import React, { useState } from "react";
+import { SimpleTableLayout } from "../../../../components/layouts";
+import { Tabla, ActionsButton } from "../../../../components";
+import { Modal, Form, Row, Col, Input, Button, message, Divider } from "antd";
+import { PlusOutlined, SaveOutlined } from "@ant-design/icons";
+import { Link } from "react-router-dom";
+import { eliminarRegistro } from "../../../../utilities";
+import HttpService from "../../../../services/httpService";
+import { respuestas } from "../../../../utilities";
+
+const Modulos = () => {
+  let tablaRef = React.useRef();
+  const endPoint = "modulo";
+
+  const [form] = Form.useForm();
+
+  const [buscarValue, setBuscarValue] = useState("");
+  const [open, setOpen] = useState(false);
+  const [modelValue, setModelValue] = useState({});
+  const [saveLoading, setSaveLoading] = useState(false);
+
+  const btnGroup = [
+    {
+      id: 1,
+      onClick: () => {
+        setOpen(true);
+        setModelValue({});
+        form.resetFields();
+      },
+      props: { disabled: false, type: "primary" },
+      text: "Nuevo",
+      icon: <PlusOutlined />,
+    },
+  ];
+
+  const columns = [
+    {
+      title: "Acciones",
+      key: "id",
+      dataIndex: "id",
+      width: 100,
+      align: "center",
+      render: (_, item) => (
+        <ActionsButton
+          data={[
+            {
+              key: '1',
+              label: "Editar",
+              onClick: () => {
+                setOpen(true);
+                setModelValue(item);
+                form.setFieldsValue({ ...item });
+              },
+            },
+            {
+              key: '2',
+              label: "Eliminar",
+              onClick: () => {
+                eliminarRegistro(item?.nombre, item?.id, endPoint, () => {
+                  tablaRef?.current?.refresh();
+                });
+              },
+              danger: true,
+            },
+          ]}
+        />
+      ),
+    },
+    {
+      title: "Clave",
+      key: "id",
+      dataIndex: "id",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          style={{ color: "black" }}
+          to="#"
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.id}
+        </Link>
+      ),
+    },
+    {
+      title: "Nombre",
+      key: "nombre",
+      dataIndex: "nombre",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          style={{ color: "black" }}
+          to="#"
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.nombre}
+        </Link>
+      ),
+    },
+  ];
+
+  const onSearch = (search) => {
+    setBuscarValue(search);
+  };
+
+  const onFinish = async (values) => {
+    try {
+      setSaveLoading(true);
+
+      let body = {
+        ...values,
+      };
+
+      const res = await HttpService.post("modulo", body);
+      respuestas(res);
+      if (res.status === 200) {
+        setOpen(false);
+        setModelValue({});
+      }
+    } catch (e) {
+      console.log(e);
+    } finally {
+      setSaveLoading(false);
+    }
+  };
+
+  const onFinishFailed = ({ values, errorFields, outOfDate }) => {
+    message.warning({
+      content: "Verifica que todos los campos estén correctos.",
+      style: {
+        marginTop: "10vh",
+      },
+    });
+  };
+
+  return (
+    <SimpleTableLayout
+      onSearch={onSearch}
+      btnGroup={{ btnGroup }}
+      children={
+        <>
+          <Tabla
+            innerRef={tablaRef}
+            nameURL={endPoint}
+            extraParams={{ buscar: buscarValue }}
+            columns={columns}
+          />
+          <Modal
+            open={open}
+            title={
+              modelValue?.id
+                ? `Editar Módulo ${modelValue?.nombre}`
+                : "Agregar Módulo"
+            }
+            onOk={() => setOpen(false)}
+            onCancel={() => {
+              setOpen(false);
+              setModelValue({});
+              form.resetFields();
+            }}
+            footer={false}
+            width="50vw"
+          >
+            <Form
+              form={form}
+              name="form"
+              layout="vertical"
+              onFinish={onFinish}
+              onFinishFailed={onFinishFailed}
+            >
+              <Row gutter={10}>
+                <Col span={24}>
+                  <Form.Item
+                    name="id"
+                    label="Clave"
+                    rules={[
+                      {
+                        required: true,
+                        message: "Ingrese una clave.",
+                      },
+                    ]}
+                  >
+                    <Input autoComplete="off" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Row gutter={10}>
+                <Col span={24}>
+                  <Form.Item name="nombre" label="Nombre">
+                    <Input autoComplete="off" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              <Divider />
+              <Row gutter={10} justify="end">
+                <Col span={6}>
+                  <Form.Item>
+                    <Button
+                      block
+                      onClick={() => {
+                        setOpen(false);
+                        setModelValue({});
+                        form.resetFields();
+                      }}
+                    >
+                      Cancelar
+                    </Button>
+                  </Form.Item>
+                </Col>
+                <Col span={9}>
+                  <Form.Item>
+                    <Button
+                      icon={<SaveOutlined />}
+                      type="primary"
+                      block
+                      htmlType="submit"
+                      loading={saveLoading}
+                    >
+                      Guardar
+                    </Button>
+                  </Form.Item>
+                </Col>
+              </Row>
+            </Form>
+          </Modal>
+        </>
+      }
+    />
+  );
+};
+
+export default Modulos;

+ 5 - 0
src/views/admin/permisos/modulos/index.js

@@ -0,0 +1,5 @@
+import Modulos from "./Modulos";
+
+export {
+  Modulos
+}

+ 224 - 0
src/views/admin/permisos/perfiles/FormPerfil.jsx

@@ -0,0 +1,224 @@
+import React, { useState, useEffect } from 'react';
+import { Form, Row, Col, Input, Typography, Tree, Divider, Button } from 'antd';
+import { SaveOutlined } from '@ant-design/icons';
+import { useNavigate } from 'react-router-dom';
+import HttpService from '../../../../services/httpService';
+import { lastPathName, respuestas } from '../../../../utilities';
+import { useModels } from '../../../../hooks';
+
+const FormPerfil = ({
+  model,
+  editing,
+  id
+}) => {
+
+  const history = useNavigate();
+
+  const [ form ] = Form.useForm();
+  const { TextArea } = Input;
+  const { Title } = Typography;
+  const { beforePath } = lastPathName();
+
+  const [ saveLoading, setSaveLoading ] = useState(false);
+  const [ modulosRequest, setModulosRequest] = useState({});
+  const [ listaPermisos, setListaPermisos ] = useState([]);
+  const [ arbolPermisos, setArbolPermisos ] = useState([]);
+  const [ ape, setApe ] = useState([]); // Arbol de permisos expandido
+  const [ checkedList, setCheckedList ] = useState([]);
+
+  // Módulos de permisos
+  const [modulos] = useModels(modulosRequest)
+
+  const onCheckedTree = (checkedList, info) => {
+    checkedList = checkedList
+      .map(item => { 
+        const val = item.replace("p-", "");
+        return val
+      })
+
+    let _checkedList = [];
+    for(let i = 0, l = listaPermisos.length; i < l; i++) {
+      const _permiso = listaPermisos[i];
+      if(checkedList.includes(_permiso["id"])) {
+        _checkedList.push(_permiso["id"]);
+      }
+    }
+
+    setCheckedList(_checkedList);
+  }
+
+  const onFinish = async values => {
+    try {
+      setSaveLoading(true);
+
+      let body = {
+        ...values,
+        permisos: checkedList
+      }
+
+      if (id) {
+        body.id = id;
+      }
+
+      console.log(body);
+
+      const res = await HttpService.post('perfil-permiso', body);
+      respuestas(res);
+      if (res.status === 200) {
+        history(`/administracion/permisos/perfiles`)
+      }
+    } catch (e) {
+      console.log(e);
+    } finally {
+      setSaveLoading(false)
+    }
+  }
+
+  const ordenarLista = React.useCallback((a, b) => {
+    const nombreA = a.nombre ? a.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, "") : a.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, "");
+    const nombreB = b.nombre ? b.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, "") : b.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, "");
+
+    if (nombreA < nombreB) {
+      return -1;
+    }
+
+    if (nombreA > nombreB) {
+      return 1;
+    }
+
+    return 0;
+  }, [])
+
+  useEffect(() => {
+    setModulosRequest({
+      name: 'modulo',
+      limite: 100,
+      expand: 'permisos'
+    });
+    return () => setModulosRequest({})
+  }, [])
+
+  useEffect(() => {
+    if (modulos && modulos?.length) {
+      let _arbolPermisos = [];
+      let _listaPermisos = [];
+      let _ape = [];
+      let _modulosPermisos = modulos;
+
+      _modulosPermisos.sort((a, b) => ordenarLista(a, b));
+
+      for (let i = 0, l = modulos.length; i < l; i++) {
+        let _permisos = modulos[i]["permisos"];
+        let _modulos = [];
+        for (let j = 0, k = _permisos?.length; j < k; j++) {
+          const nPermiso = {
+            title: _permisos[j]["nombre"],
+            id: _permisos[j]["id"],
+            selectable: false,
+            key: `p-${_permisos[j]["id"]}`,
+          }
+          _listaPermisos.push(nPermiso);
+          _modulos.push(nPermiso);
+        }
+
+        _modulos.sort((a, b) => ordenarLista(a, b));
+
+        const llave = `m-${modulos[i]["id"]}`;
+        _ape.push(llave);
+        _arbolPermisos.push({
+          title: modulos[i]["nombre"],
+          key: llave,
+          id: modulos[i]["id"],
+          children: _modulos
+        })
+      }
+
+      setApe(_ape);
+      setListaPermisos(_listaPermisos);
+      setArbolPermisos(_arbolPermisos);
+    }
+  }, [modulos, ordenarLista])
+
+  useEffect(() => {
+    if (editing && model) {
+      form.setFieldsValue({
+        ...model
+      });
+      setCheckedList(model?.permisos.map(i => i.id));
+    }
+  }, [editing, form, model])
+
+  return (
+    <Form
+      form={form}
+      name='form'
+      layout='vertical'
+      onFinish={onFinish}
+    >
+      <Row gutter={10}>
+        <Col
+          xs={{ span: 24 }}
+          sm={{ span: 24 }}
+          md={{ span: 12 }}
+          lg={{ span: 12 }}
+        >
+          <Form.Item name='clave' label='Clave'>
+            <Input />
+          </Form.Item>
+        </Col>
+        <Col
+          xs={{ span: 24 }}
+          sm={{ span: 24 }}
+          md={{ span: 12 }}
+          lg={{ span: 12 }}
+        >
+          <Form.Item name='nombre' label='Nombre'>
+            <Input />
+          </Form.Item>
+        </Col>
+      </Row>
+      <Row gutter={10}>
+        <Col span={24}>
+          <Form.Item name='descripcion' label='Descripción'>
+            <TextArea rows={4} />
+          </Form.Item>
+        </Col>
+      </Row>
+      <Row gutter={10}>
+        <Col span={24}>
+          <Title level={5}>Permisos</Title>
+        </Col>
+      </Row>
+      <Row gutter={10}>
+        <Col span={24}>
+          <Tree
+            checkable
+            defaultExpandedKeys={ape}
+            selectable={false}
+            checkedKeys={checkedList.map(i => `p-${i}`)}
+            onCheck={onCheckedTree}
+            treeData={arbolPermisos}
+          />
+        </Col>
+      </Row>
+
+      <Divider />
+
+      <Row gutter={10}>
+        <Col span={6}>
+          <Button
+            block
+            type="primary"
+            htmlType="submit"
+            icon={<SaveOutlined />}
+            loading={saveLoading}
+          >
+            Guardar
+          </Button>
+        </Col>
+      </Row>
+    </Form>
+  );
+}
+
+export default FormPerfil

+ 245 - 0
src/views/admin/permisos/perfiles/PerfilDetalle.jsx

@@ -0,0 +1,245 @@
+import React, { useState, useEffect } from 'react';
+import { DefaultLayout } from '../../../../components/layouts';
+import { useQuery, useModel, useModels } from '../../../../hooks';
+import { Form, Row, Col, Input, Typography, Tree, Divider, Button } from 'antd';
+import { SaveOutlined } from '@ant-design/icons';
+import HttpService from '../../../../services/httpService';
+import { respuestas } from '../../../../utilities';
+import { useNavigate } from 'react-router-dom';
+
+const { Title } = Typography;
+const { TextArea } = Input;
+
+const PerfilDetalle = () => {
+
+  const history = useNavigate();
+  const [form] = Form.useForm();
+  const q = useQuery();
+  const id = q.get('id');
+  const editing = Boolean(id);
+  const endPoint = 'coleccion-permiso';
+
+  const [request, setRequest] = React.useState({});
+  const [saveLoading, setSaveLoading] = useState(false);
+  const [listaPermisos, setListaPermisos] = useState([]);
+  const [arbolPermisos, setArbolPermisos] = useState([]);
+  const [ape, setApe] = useState([]); // Arbol de permisos expandido
+  const [checkedList, setCheckedList] = useState([]);
+  const [modulos, setModulos] = useState([]);
+
+  const onCheckedTree = (checkedList, info) => {
+    checkedList = checkedList
+      ?.map(item => {
+        const val = item.replace("p-", "");
+        return val
+      })
+
+    let _checkedList = [];
+    for (let i = 0, l = listaPermisos.length; i < l; i++) {
+      const _permiso = listaPermisos[i];
+      if (checkedList.includes(_permiso["id"])) {
+        _checkedList.push(_permiso["id"]);
+      }
+    }
+
+    setCheckedList(_checkedList);
+  }
+
+  const requestParams = React.useMemo(() => ({
+    name: endPoint,
+    id: id,
+    expand: 'permisos'
+  }), [id])
+
+  const {
+    model,
+    modelLoading
+  } = useModel(request);
+
+  const onFinish = async values => {
+    try {
+      setSaveLoading(true);
+
+      let body = {
+        ...values,
+        permisos: checkedList
+      }
+
+      if (id) {
+        body.id = id;
+      }
+
+      console.log(body);
+
+      const res = await HttpService.post('coleccion-permiso', body);
+      respuestas(res);
+      if (res.status === 200) {
+        history(`/administracion/permisos/perfiles`)
+      }
+    } catch (e) {
+      console.log(e);
+    } finally {
+      setSaveLoading(false)
+    }
+  }
+
+  const ordenarLista = React.useCallback((a, b) => {
+    const nombreA = a.nombre ? a.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, "") : a.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, "");
+    const nombreB = b.nombre ? b.nombre.normalize('NFD')?.replace(/\p{Diacritic}/gu, "") : b.title.normalize('NFD')?.replace(/\p{Diacritic}/gu, "");
+
+    if (nombreA < nombreB) {
+      return -1;
+    }
+
+    if (nombreA > nombreB) {
+      return 1;
+    }
+
+    return 0;
+  }, [])
+
+  const getModulos = React.useCallback(async () => {
+    const res = await HttpService.get('modulo?limite=100&expand=permisos');
+    if (res?.status === 200) {
+      setModulos(res.resultado);
+    }
+  }, []);
+
+  useEffect(() => {
+    getModulos();
+    return () => setModulos([]);
+  }, [getModulos])
+
+  useEffect(() => {
+    if (modulos && modulos?.length) {
+      let _arbolPermisos = [];
+      let _listaPermisos = [];
+      let _ape = [];
+      let _modulosPermisos = modulos;
+
+      _modulosPermisos.sort((a, b) => ordenarLista(a, b));
+
+      for (let i = 0, l = modulos.length; i < l; i++) {
+        let _permisos = modulos[i]["permisos"];
+        let _modulos = [];
+        for (let j = 0, k = _permisos?.length; j < k; j++) {
+          const nPermiso = {
+            title: _permisos[j]["nombre"],
+            id: _permisos[j]["id"],
+            selectable: false,
+            key: `p-${_permisos[j]["id"]}`,
+          }
+          _listaPermisos.push(nPermiso);
+          _modulos.push(nPermiso);
+        }
+
+        _modulos.sort((a, b) => ordenarLista(a, b));
+
+        const llave = `m-${modulos[i]["id"]}`;
+        _ape.push(llave);
+        _arbolPermisos.push({
+          title: modulos[i]["nombre"],
+          key: llave,
+          id: modulos[i]["id"],
+          children: _modulos
+        })
+      }
+
+      setApe(_ape);
+      setListaPermisos(_listaPermisos);
+      setArbolPermisos(_arbolPermisos);
+    }
+  }, [modulos, ordenarLista])
+
+  useEffect(() => {
+    if (editing && model) {
+      form.setFieldsValue({
+        ...model
+      });
+      setCheckedList(model?.permisos?.map(i => i.id));
+    }
+  }, [editing, form, model])
+
+  React.useEffect(() => {
+    setRequest(requestParams);
+    return () => setRequest({});
+  }, [requestParams])
+
+  return (
+    <DefaultLayout
+      viewLoading={modelLoading}
+    >
+      <Form
+        form={form}
+        name='form'
+        layout='vertical'
+        onFinish={onFinish}
+      >
+        <Row gutter={10}>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Form.Item name='clave' label='Clave'>
+              <Input />
+            </Form.Item>
+          </Col>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Form.Item name='nombre' label='Nombre'>
+              <Input />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={10}>
+          <Col span={24}>
+            <Form.Item name='descripcion' label='Descripción'>
+              <TextArea rows={4} />
+            </Form.Item>
+          </Col>
+        </Row>
+        <Row gutter={10}>
+          <Col span={24}>
+            <Title level={5}>Permisos</Title>
+          </Col>
+        </Row>
+        <Row gutter={10}>
+          <Col span={24}>
+            <Tree
+              checkable
+              defaultExpandedKeys={ape}
+              selectable={false}
+              checkedKeys={checkedList?.map(i => `p-${i}`)}
+              onCheck={onCheckedTree}
+              treeData={arbolPermisos}
+            />
+          </Col>
+        </Row>
+
+        <Divider />
+
+        <Row gutter={10}>
+          <Col span={6}>
+            <Button
+              block
+              type="primary"
+              htmlType="submit"
+              icon={<SaveOutlined />}
+              loading={saveLoading}
+            >
+              Guardar
+            </Button>
+          </Col>
+        </Row>
+      </Form>
+    </DefaultLayout>
+  );
+}
+
+export default PerfilDetalle

+ 115 - 0
src/views/admin/permisos/perfiles/Perfiles.jsx

@@ -0,0 +1,115 @@
+import React from "react";
+import { SimpleTableLayout } from "../../../../components/layouts";
+import { Tabla, ActionsButton } from "../../../../components";
+import { useNavigate, Link } from "react-router-dom";
+import { PlusOutlined } from "@ant-design/icons";
+import { eliminarRegistro, isEllipsis } from "../../../../utilities";
+import { Tooltip } from "antd";
+
+const Perfiles = () => {
+  let tablaRef = React.useRef();
+  const endPoint = "coleccion-permiso";
+  const history = useNavigate();
+
+  const [buscarValue, setBuscarValue] = React.useState("");
+
+  const btnGroup = [
+    {
+      id: 1,
+      onClick: () => history(`/administracion/permisos/perfiles/agregar`),
+      props: { disabled: false, type: "primary" },
+      text: "Nuevo",
+      icon: <PlusOutlined />,
+    },
+  ];
+
+  const textLink = (value, key) => (
+    <Link
+      style={{ color: "black" }}
+      to={`/administracion/permisos/perfiles/editar?id=${key?.id}`}
+    >
+      {isEllipsis(columns, key) ? (
+        <Tooltip title={value}>{value}</Tooltip>
+      ) : (
+        value
+      )}
+    </Link>
+  );
+
+  const columns = [
+    {
+      title: "Acciones",
+      key: "id",
+      dataIndex: "id",
+      width: 50,
+      align: "center",
+      render: (_, item) => (
+        <ActionsButton
+          options={[
+            {
+              name: "Editar",
+              onClick: () =>
+                history(
+                  `/administracion/permisos/perfiles/editar?id=${item?.id}`
+                ),
+            },
+            {
+              name: "Eliminar",
+              onClick: () => {
+                eliminarRegistro(item?.nombre, item?.id, endPoint, () => {
+                  tablaRef?.current?.refresh();
+                });
+              },
+              danger: true,
+            },
+          ]}
+        />
+      ),
+    },
+    {
+      title: "Clave",
+      key: "clave",
+      dataIndex: "clave",
+      ellipsis: true,
+      render: textLink,
+      width: "100px",
+    },
+    {
+      title: "Nombre",
+      key: "nombre",
+      dataIndex: "nombre",
+      ellipsis: true,
+      render: textLink,
+      width: "100px",
+    },
+    {
+      title: "Descripción",
+      key: "descripcion",
+      dataIndex: "descripcion",
+      ellipsis: true,
+      render: textLink,
+      width: "200px",
+    },
+  ];
+
+  const onSearch = (search) => {
+    setBuscarValue(search);
+  };
+
+  return (
+    <SimpleTableLayout
+      onSearch={onSearch}
+      btnGroup={{ btnGroup }}
+      children={
+        <Tabla
+          innerRef={tablaRef}
+          nameURL={endPoint}
+          extraParams={{ buscar: buscarValue }}
+          columns={columns}
+        />
+      }
+    />
+  );
+};
+
+export default Perfiles;

+ 7 - 0
src/views/admin/permisos/perfiles/index.js

@@ -0,0 +1,7 @@
+import Perfiles from "./Perfiles";
+import PerfilDetalle from "./PerfilDetalle";
+
+export {
+  Perfiles,
+  PerfilDetalle
+}

+ 56 - 0
src/views/admin/permisos/permisos/BuscarComponente.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes  from 'prop-types';
+import { Form, Row, Col, Input } from 'antd';
+import { Select, ButtonGroup } from '../../../../components';
+
+const BuscarComponente = ({ btnGroup, formBuscar }) => {
+
+  const modelParams = {
+    modulos: {
+      name: 'modulo',
+      limite: -1,
+      ordenar: 'nombre-asc'
+    },
+  };
+
+  return (
+    <Form
+      form={formBuscar}
+      layout='vertical'
+    >
+      <Row gutter={10}>
+        <Col span={8}>
+          <Form.Item label='Buscar' name='buscar'>
+            <Input placeholder='Escribir...'/>
+          </Form.Item>
+        </Col>
+        <Col span={8}>
+          <Form.Item label='Seleccione un módulo' name='idModulo'>
+          <Select 
+              placeholder="Seleccione un módulo"
+              allowClear={true}
+              modelsParams={modelParams.modulos}
+              labelProp="nombre" 
+              valueProp="id"
+              render={(_, row) => `${row.id} - ${row.nombre}`}
+            />
+          </Form.Item>
+        </Col>
+        <Col span={4}>
+          <Form.Item label="&nbsp;">
+            <ButtonGroup
+              data={btnGroup}
+            />
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form>
+  )
+}
+
+BuscarComponente.propTypes = {
+  btnGroup: PropTypes.object,
+  formBuscar: PropTypes.any.isRequired
+}
+
+export default BuscarComponente

+ 355 - 0
src/views/admin/permisos/permisos/Permisos.jsx

@@ -0,0 +1,355 @@
+import React, { useState } from "react";
+import { SimpleTableLayout } from "../../../../components/layouts";
+import { Tabla, ActionsButton, Select } from "../../../../components";
+import { Modal, Form, Row, Col, Input, Button, message } from "antd";
+import {
+  SearchOutlined,
+  ClearOutlined,
+  PlusOutlined,
+  SaveOutlined,
+} from "@ant-design/icons";
+import { Link } from "react-router-dom";
+import { eliminarRegistro } from "../../../../utilities";
+import HttpService from "../../../../services/httpService";
+import { respuestas } from "../../../../utilities";
+import BuscarComponente from "./BuscarComponente";
+
+const Permisos = () => {
+  let tablaRef = React.useRef();
+  const endPoint = "permiso";
+
+  const [form] = Form.useForm();
+  const [formBuscar] = Form.useForm();
+
+  const { TextArea } = Input;
+
+  const [buscarParams, setBuscarParams] = useState({});
+  const [open, setOpen] = useState(false);
+  const [modelValue, setModelValue] = useState({});
+  const [saveLoading, setSaveLoading] = useState(false);
+
+  const btnGroup = [
+    {
+      id: 1,
+      onClick: () => onSearch(),
+      props: { disabled: false, type: "default", block: true },
+      text: "Buscar",
+      icon: <SearchOutlined />,
+    },
+    {
+      id: 2,
+      onClick: () => {
+        setBuscarParams({});
+        formBuscar.resetFields();
+      },
+      props: { disabled: false, type: "dashed", block: true },
+      text: "Limpiar",
+      icon: <ClearOutlined />,
+    },
+    {
+      id: 3,
+      onClick: () => {
+        setOpen(true);
+        setModelValue({});
+        form.resetFields();
+      },
+      props: { disabled: false, type: "primary", block: true },
+      text: "Nuevo",
+      icon: <PlusOutlined />,
+    },
+  ];
+
+  const columns = [
+    {
+      title: "Acciones",
+      key: "nombre",
+      dataIndex: "nombre",
+      width: 100,
+      align: "center",
+      render: (_, item) => (
+        <ActionsButton
+          options={[
+            {
+              name: "Editar",
+              onClick: () => {
+                setOpen(true);
+                setModelValue(item);
+                form.setFieldsValue({ ...item });
+              },
+            },
+            {
+              name: "Eliminar",
+              onClick: () => {
+                eliminarRegistro(item?.nombre, item?.id, endPoint, () => {
+                  tablaRef?.current?.refresh();
+                });
+              },
+              danger: true,
+            },
+          ]}
+        />
+      ),
+    },
+    {
+      title: "Clave",
+      key: "id",
+      dataIndex: "id",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          style={{ color: "black" }}
+          to="#"
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.id}
+        </Link>
+      ),
+    },
+    {
+      title: "Nombre",
+      key: "nombre",
+      dataIndex: "nombre",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          style={{ color: "black" }}
+          to="#"
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.nombre}
+        </Link>
+      ),
+    },
+    {
+      title: "Módulo",
+      key: "modulo",
+      dataIndex: "modulo",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          to="#"
+          style={{ color: "black" }}
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.modulo.nombre}
+        </Link>
+      ),
+    },
+    {
+      title: "Descripción",
+      key: "descripcion",
+      dataIndex: "descripcion",
+      ellipsis: true,
+      render: (_, item) => (
+        <Link
+          style={{ color: "black" }}
+          to="#"
+          onClick={() => {
+            setOpen(true);
+            setModelValue(item);
+            form.setFieldsValue({ ...item });
+          }}
+        >
+          {item?.descripcion}
+        </Link>
+      ),
+    },
+  ];
+
+  const onSearch = () => {
+    const { buscar, idModulo } = formBuscar.getFieldsValue();
+    let params = { ...buscarParams };
+
+    if (buscar) {
+      params.buscar = buscar;
+    }
+
+    if (idModulo) {
+      params.idModulo = idModulo;
+    }
+
+    setBuscarParams(params);
+  };
+
+  const onFinish = async (values) => {
+    try {
+      setSaveLoading(true);
+
+      let body = {
+        ...values,
+      };
+
+      body.claveOld = modelValue?.id;
+
+      const res = await HttpService.post(endPoint, body);
+      respuestas(res);
+      if (res.status === 200) {
+        setOpen(false);
+        setModelValue({});
+        form.resetFields();
+        tablaRef?.current?.refresh();
+      }
+    } catch (e) {
+      console.log(e);
+    } finally {
+      setSaveLoading(false);
+    }
+  };
+
+  const onFinishFailed = ({ values, errorFields, outOfDate }) => {
+    message.warning({
+      content: "Verifica que todos los campos estén correctos.",
+      style: {
+        marginTop: "10vh",
+      },
+    });
+  };
+
+  return (
+    <SimpleTableLayout
+      customRender={
+        <BuscarComponente
+          formBuscar={formBuscar}
+          btnGroup={{
+            btnGroup,
+            flex: { justifyContent: "start", flexDirection: "row" },
+          }}
+        />
+      }
+    >
+      <>
+        <Tabla
+          innerRef={tablaRef}
+          nameURL={endPoint}
+          expand="modulo"
+          extraParams={buscarParams}
+          columns={columns}
+        />
+        <Modal
+          open={open}
+          title={
+            modelValue?.id
+              ? `Editar Permiso ${modelValue?.nombre}`
+              : "Agregar Permiso"
+          }
+          onOk={() => setOpen(false)}
+          onCancel={() => {
+            setOpen(false);
+            setModelValue({});
+          }}
+          footer={false}
+          width="50vw"
+        >
+          <Form
+            form={form}
+            name="form"
+            layout="vertical"
+            onFinish={onFinish}
+            onFinishFailed={onFinishFailed}
+          >
+            <Row gutter={10}>
+              <Col
+                xs={{ span: 24 }}
+                sm={{ span: 24 }}
+                md={{ span: 12 }}
+                lg={{ span: 12 }}
+              >
+                <Form.Item name="nombre" label="Nombre">
+                  <Input autoComplete="off" />
+                </Form.Item>
+              </Col>
+              <Col
+                xs={{ span: 24 }}
+                sm={{ span: 24 }}
+                md={{ span: 12 }}
+                lg={{ span: 12 }}
+              >
+                <Form.Item name="idModulo" label="Módulo">
+                  <Select
+                    placeholder="Selecciona un ejercicio fiscal"
+                    allowClear
+                    modelsParams={{
+                      name: "modulo",
+                      limite: -1,
+                      ordenar: "nombre",
+                    }}
+                    labelProp="nombre"
+                    valueProp="id"
+                    render={(_, row) => `${row.id} - ${row.nombre}`}
+                    append={[modelValue?.modulo]}
+                  />
+                </Form.Item>
+              </Col>
+            </Row>
+            <Row gutter={10}>
+              <Col span={24}>
+                <Form.Item
+                  name="id"
+                  label="Clave"
+                  rules={[
+                    {
+                      required: true,
+                      message: "Ingrese una clave.",
+                    },
+                  ]}
+                >
+                  <Input rows={2} />
+                </Form.Item>
+              </Col>
+            </Row>
+            <Row gutter={10}>
+              <Col span={24}>
+                <Form.Item name="descripcion" label="Descripción">
+                  <TextArea autoComplete="off" />
+                </Form.Item>
+              </Col>
+            </Row>
+            <Row gutter={10} justify="end">
+              <Col span={6}>
+                <Form.Item>
+                  <Button
+                    block
+                    onClick={() => {
+                      setOpen(false);
+                      setModelValue({});
+                      form.resetFields();
+                    }}
+                  >
+                    Cancelar
+                  </Button>
+                </Form.Item>
+              </Col>
+              <Col span={9}>
+                <Form.Item>
+                  <Button
+                    icon={<SaveOutlined />}
+                    type="primary"
+                    block
+                    htmlType="submit"
+                    loading={saveLoading}
+                  >
+                    Guardar
+                  </Button>
+                </Form.Item>
+              </Col>
+            </Row>
+          </Form>
+        </Modal>
+      </>
+    </SimpleTableLayout>
+  );
+};
+
+export default Permisos;

+ 5 - 0
src/views/admin/permisos/permisos/index.js

@@ -0,0 +1,5 @@
+import Permisos from "./Permisos";
+
+export {
+  Permisos
+}

+ 617 - 0
src/views/admin/usuarios/Formulario.jsx

@@ -0,0 +1,617 @@
+import React, { useCallback, useEffect, useState, useMemo } from 'react'
+import {
+  Select as AntdSelect,
+  Form,
+  Row,
+  Col,
+  Input,
+  Button,
+  message,
+  Divider,
+  Typography,
+  notification,
+  Tree,
+  Upload, Collapse, Checkbox
+} from 'antd'
+import { SaveOutlined, UserOutlined } from '@ant-design/icons'
+import httpService from '../../../services/httpService'
+import { respuestas } from '../../../utilities'
+import { useNavigate } from 'react-router-dom'
+import { useApp, useAuth, useModels } from '../../../hooks'
+import { ArbolPermisos, InputPass, Select } from '../../../components'
+import { emptyRequest } from '../../../constants/requests'
+import Avatar from 'antd/es/avatar/avatar'
+
+const baseUrl = import.meta.env.VITE_API_URL
+
+const { Option } = AntdSelect
+const { Panel } = Collapse
+
+const Formulario = ({ setGuardando, endPoint, model, editing, id }) => {
+  const [form] = Form.useForm()
+  const navigate = useNavigate()
+  const { Title } = Typography
+  const { user } = useAuth()
+  const { token } = useApp()
+
+  //Estados
+  const [saveLoading, setSaveLoading] = useState(false)
+
+  //Permisos
+  const [checkedList, setCheckedList] = useState([])
+  const [estatusRequest, setEstatusRequest] = useState(emptyRequest)
+
+  const [arbolEstatus, setArbolEstatus] = useState([])
+  const [arbolRecurso, setArbolRecurso] = useState([])
+  const [listaCheckSolicitud, setListaCheckSolicitud] = useState([])
+  const [listaCheckRecurso, setListaCheckRecurso] = useState([])
+  // const [aee, setAee] = useState([]); //Arbol estatus expandido
+
+  const [urlFoto, setUrlFoto] = useState(null)
+  const [listaArchivosFotoPerfil, setListaArchivosFotoPerfil] = useState([])
+  // const [archivoCargando, setArchivoCargando] = useState(null)
+
+  const [correoValidadoChecked, setCorreoValidadoChecked] = useState(false)
+
+  //Parámetros de estatus.
+  const estatusParams = useMemo(
+    () => ({
+      name: 'estatus',
+      expand: 'subestatus',
+    }),
+    []
+  )
+
+  const onChangePicturePerfil = ({ fileList: newFileList }) => {
+    let _archivo = newFileList[0]?.originFileObj
+    const isImage = _archivo.type.includes('image/')
+
+    if (!isImage) {
+      message.error(`${_archivo.name} no es un archivo de Imagen`)
+      return
+    }
+
+    subirImagenPerfil(newFileList)
+
+    // setListaArchivosFotoPerfil(newFileList);
+  }
+
+  const subirImagenPerfil = async (file, idDocumento) => {
+    try {
+      // setArchivoCargando(true)
+
+      let _archivo = file[0]?.originFileObj
+
+      if (!_archivo) {
+        message.info({
+          content: 'Debes de seleccionarun archivo',
+          style: { marginTop: '20vh' },
+        })
+        return
+      }
+
+      const form = new FormData()
+      form.append('archivo', _archivo)
+
+      const response = await fetch(baseUrl + '/v1/perfil/cambiar-foto-perfil', {
+        method: 'POST',
+        headers: {
+          Authorization: `Bearer ${token}`,
+        },
+        body: form,
+      })
+
+      const data = await response.json()
+
+      setUrlFoto(data?.resultado[0])
+    } catch (error) {
+      console.log('error al cargar archivo: ', error)
+    } finally {
+      // setArchivoCargando(false)
+      // window.location.reload(false);
+    }
+  }
+  const { models: estatus } = useModels(estatusRequest)
+
+  const onCheckSolicitud = useCallback((checkedList) => {
+    setListaCheckSolicitud(checkedList)
+  }, [])
+
+  const onCheckRecurso = useCallback((checkedList) => {
+    setListaCheckRecurso(checkedList)
+  }, [])
+
+  const onFinish = async (values) => {
+    try {
+      const { clave1, clave, rol } = values
+
+      setSaveLoading(true)
+      setGuardando(true)
+      let body = {
+        ...values,
+        permisos: checkedList,
+        foto: urlFoto,
+        verificarCorreo: correoValidadoChecked
+      }
+
+      if (!editing) {
+        if (clave1 !== clave) {
+          message.error('Las contraseñas no coinciden.')
+          return
+        }
+
+        if (user?.rol !== 'admin' && rol === 'admin') {
+          notification.info({
+            message: 'Atención',
+            description: 'No puede asignar el rol de Administrador',
+          })
+          return
+        }
+
+        body.pwd = clave1 ? clave1 : ''
+
+        delete body.clave1
+        delete body.clave
+
+        if (listaCheckSolicitud || listaCheckRecurso) {
+          body.permisoEstatus = [...listaCheckSolicitud, ...listaCheckRecurso]
+        }
+      } else {
+        if (clave1 && clave) {
+          if (clave1 !== clave) {
+            message.error('Las contraseñas no coinciden.')
+            return
+          }
+        }
+
+        //   let _body = {
+        //     clave: clave1,
+        //     confirmarClave: clave,
+        //     idUsuario: id,
+        //   };
+
+        //   const resClave = await httpService.post(
+        //     `${endPoint}/cambiar-clave`,
+        //     _body
+        //   );
+        //   if (resClave?.status !== 200) {
+        //     respuestas(resClave);
+        //     return;
+        //   }
+
+        if (user?.rol !== 'admin' && rol === 'admin') {
+          notification.info({
+            message: 'Atención',
+            description: 'No puede asignar el rol de Super Administrador',
+          })
+          return
+        }
+
+        body.id = id;
+
+        /* body = {
+          ...values,
+          id: id ? id : '',
+          pwd: clave1 ? clave1 : '',
+          permisos: checkedList,
+          foto: urlFoto,
+          verificarCorreo: correoValidadoChecked
+        } */
+        delete body.clave1
+        delete body.clave
+
+        if (listaCheckSolicitud || listaCheckRecurso) {
+          body.permisoEstatus = [...listaCheckSolicitud, ...listaCheckRecurso]
+        }
+      }
+
+      const res = await httpService.post(endPoint, body)
+      respuestas(res)
+      if (res?.status === 200) {
+        navigate(`/administracion/usuarios`)
+      }
+    } catch (e) {
+      console.log(e)
+    } finally {
+      setSaveLoading(false)
+      setGuardando(false)
+    }
+  }
+
+  const onFinishFailed = ({ values, errorFields, outOfDate }) => {
+    message.warning({
+      content: 'Verifica que todos los campos estén correctos.',
+      style: {
+        marginTop: '10vh',
+      },
+    })
+  }
+
+  // const onCheck = useCallback(
+  //   (checkedList, info) => {
+  //     checkedList = checkedList.map((item) => {
+  //       const val = item?.replace("p-", "");
+  //       return val;
+  //     });
+
+  //     let _checkedList = [];
+  //     for (let i = 0, l = listaPermisos.length; i < l; i++) {
+  //       const _permiso = listaPermisos[i];
+  //       if (checkedList.includes(_permiso["id"])) {
+  //         _checkedList.push(_permiso["id"]);
+  //       }
+  //     }
+
+  //     setCheckedList(_checkedList);
+  //   },
+  //   [listaPermisos]
+  // );
+
+  useEffect(() => {
+    if (editing && model) {
+      form.setFieldsValue({
+        ...model,
+        clave: '',
+        unidadesAdministrativas: model?.unidadAdministrativaUsuarios,
+      })
+
+      if (model?.foto) {
+        setUrlFoto(model?.foto)
+      }
+
+      if (model?.verificarCorreo !== null) {
+        setCorreoValidadoChecked(true);
+      }
+
+      setCheckedList(model?.permisos?.map((i) => i))
+    }
+  }, [editing, form, model])
+
+  useEffect(() => {
+    if (estatus) {
+      let arbolEstatus = estatus.map((_estatus) => {
+        if (_estatus?.idEstatusPadre === null) {
+          return {
+            title: _estatus?.nombre,
+            key: _estatus?.id,
+            id: _estatus?.id,
+            tipo: _estatus?.tipo,
+            children: _estatus?.subestatus?.map((subestatus) => ({
+              title: subestatus?.nombre,
+              key: subestatus?.id,
+              id: subestatus?.id,
+              tipo: _estatus?.tipo,
+            })),
+          }
+        } else {
+          return null
+        }
+      })
+      let arbolFiltrado = arbolEstatus.filter((item) => item !== null)
+      let arbolSolicitud = arbolFiltrado.filter((item) => item.tipo === 'S')
+      let arbolRecurso = arbolFiltrado.filter((item) => item.tipo === 'RR')
+      setArbolEstatus(arbolSolicitud)
+      setArbolRecurso(arbolRecurso)
+    }
+  }, [estatus])
+
+  useEffect(() => {
+    if (model?.estatusPermiso) {
+      let permisosMarcadosS = []
+      let permisosMarcadosRR = []
+      let estatus = model?.estatusPermiso
+
+      estatus.forEach((permiso) => {
+        if (permiso?.estatus?.tipo === 'S') {
+          permisosMarcadosS.push(permiso.idEstatus)
+        } else {
+          permisosMarcadosRR.push(permiso.idEstatus)
+        }
+      })
+
+      setListaCheckSolicitud(permisosMarcadosS)
+      setListaCheckRecurso(permisosMarcadosRR)
+    }
+  }, [model])
+
+  useEffect(() => {
+    setEstatusRequest(estatusParams)
+    return () => setEstatusRequest({})
+  }, [estatusParams])
+
+  return (
+    <Form
+      form={form}
+      name="form"
+      layout="vertical"
+      onFinish={onFinish}
+      onFinishFailed={onFinishFailed}
+    >
+      <Row gutter={[16, 0]}>
+        <Col span={24} md={6}>
+          <Form.Item
+            label={'Elegir foto de Perfil:'}
+          >
+            <Upload
+              beforeUpload={() => false}
+              multiple={true}
+              listType="picture-card"
+              fileList={listaArchivosFotoPerfil}
+              onChange={onChangePicturePerfil}
+              onRemove={() => {
+                setListaArchivosFotoPerfil([])
+                setUrlFoto(null)
+              }}
+            >
+              <Avatar size={100} icon={
+                <UserOutlined />} src={urlFoto} />
+            </Upload>
+          </Form.Item>
+        </Col>
+        <Col span={24} md={18}>
+          <Row gutter={[16, 0]}>
+            <Col span={24} md={12}>
+              <Form.Item
+                name="nombre"
+                label="Nombre"
+                rules={[
+                  {
+                    required: true,
+                    message: 'Por favor ingresar nombre de Usuario',
+                  },
+                ]}
+              >
+                <Input autoComplete="one-time-code" />
+              </Form.Item>
+            </Col>
+            {/* <Col span={24} md={12}>
+          <Form.Item
+            name="usuario"
+            label="Usuario"
+            rules={[
+              { required: true, message: "Por favor ingresar el usuario" },
+            ]}
+          >
+            <Input autocomplete="one-time-code" />
+          </Form.Item>
+        </Col> */}
+            <Col span={24} md={12}>
+              <Form.Item
+                label="Correo Electronico"
+                name="correo"
+                rules={[{ required: true, message: 'Por favor escriba su correo' }]}
+              >
+                <Input autoComplete="one-time-code" disabled={editing} />
+              </Form.Item>
+            </Col>
+            <Col span={24} md={12}>
+              <Form.Item
+                label="Rol"
+                name="rol"
+                rules={[{ required: true, message: 'Por favor Seleccionar Rol' }]}
+              >
+                <AntdSelect>
+                  <Option value={'admin'}>Super Administrador</Option>
+                  <Option value={'unidadAdministrativa'}>
+                    Unidad Administrativa
+                  </Option>
+                  <Option value={'usuario'}>Usuario</Option>
+                </AntdSelect>
+              </Form.Item>
+            </Col>
+            <Col span={24} md={12}>
+              <Form.Item
+                label="Teléfono"
+                name="telefono"
+                rules={[
+                  {
+                    required: true,
+                    message: 'Es necesario ingresar un número telefónico',
+                  },
+                  // solo numeros
+                  ({ getFieldValue }) => ({
+                    validator(_, value) {
+                      if (!value || /^[0-9]*$/.test(value)) {
+                        return Promise.resolve()
+                      }
+                      return Promise.reject(new Error('Solo se permiten números'))
+                    },
+                  }),
+                  // maximo 10 caracteres minimo 10
+                  ({ getFieldValue }) => ({
+                    validator(_, value) {
+                      if (!value || value.length === 10) {
+                        return Promise.resolve()
+                      }
+                      return Promise.reject(
+                        new Error('El teléfono debe tener 10 dígitos')
+                      )
+                    },
+                  }),
+                ]}
+              >
+                <Input autoComplete="one-time-code" maxLength={10} />
+              </Form.Item>
+            </Col>
+            <Col span={24} md={12}>
+              <Form.Item label="Sujeto Obligado" name="idSujetoObligado">
+                <Select
+                  modelsParams={{
+                    name: 'sujeto-obligado',
+                    limite: 20,
+                    ordenar: 'nombre-asc',
+                  }}
+                  labelProp="nombre"
+                  valueProp="id"
+                  placeholder="Seleccione un perfil"
+                  append={[model?.sujetoObligado]}
+                />
+              </Form.Item>
+            </Col>
+            <Col span={24} md={12}>
+              <Form.Item name={'idPonencia'} label="Ponencia">
+                <Select
+                  modelsParams={{
+                    name: 'ponencia',
+                    ordenar: 'nombre-asc',
+                  }}
+                  labelProp={'nombre'}
+                  valueProp={'id'}
+                  append={[model?.ponencia]}
+                />
+              </Form.Item>
+            </Col>
+            <Col>
+              <Form.Item label={"Correo Verificado"} name="verificarCorreo">
+                <Checkbox
+                  checked={correoValidadoChecked}
+                  onChange={(e) => setCorreoValidadoChecked(e.target.checked)}
+                />
+              </ Form.Item>
+            </Col>
+          </Row>
+        </Col>
+      </Row>
+      <Divider />
+      <Row gutter={[16, 0]}>
+        {!editing ? (
+          <>
+            <Col
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 12 }}
+              lg={{ span: 12 }}
+            >
+              <InputPass
+                label="Contraseña"
+                name="clave"
+              />
+            </Col>
+            <Col
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 12 }}
+              lg={{ span: 12 }}
+            >
+              <InputPass
+                label="Repetir Contraseña"
+                name="clave1"
+              />
+            </Col>
+          </>
+        ) :
+          <Col span={24}>
+            <Collapse>
+              <Panel header={'Cambiar la contraseña'} key="1">
+                <p
+                  style={{
+                    color: '#777',
+                    marginBottom: '20px',
+                    marginTop: '-10px',
+                  }}
+                >
+                  Si no desea cambiar la contraseña, deje los campos en blanco.
+                </p>
+                <Row gutter={[16, 0]}>
+                  {/* <Col
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 12 }}
+              lg={{ span: 12 }}
+            >
+              <Form.Item label="Contraseña Actual" name="claveActual">
+                <Input.Password
+                  autocomplete="one-time-code"
+                  visibilitytoggle="false"
+                />
+              </Form.Item>
+            </Col> */}
+                  <Col span={24} md={12}>
+                    <InputPass
+                      label="Confirmar Contraseña"
+                      name="clave"
+                      obligatorio={false}
+                    />
+                  </Col>
+                  <Col
+                    xs={{ span: 24 }}
+                    sm={{ span: 24 }}
+                    md={{ span: 12 }}
+                    lg={{ span: 12 }}
+                  >
+                    <InputPass
+                      label="Contraseña"
+                      name="clave1"
+                      obligatorio={false}
+                    />
+                  </Col>
+                </Row>
+              </Panel>
+            </Collapse>
+          </Col>
+        }
+      </Row>
+      <Divider />
+      <Row gutter={[10, 10]}>
+        <Col span={24}>
+          <Title level={5}>Permisos</Title>
+        </Col>
+        <Col span={24} md={8}>
+          <Typography.Title style={{ margin: 0 }} level={5}>
+            Permisos del Sistema
+          </Typography.Title>
+          <ArbolPermisos
+            conPerfil={true}
+            urlPerfil='coleccion-permiso'
+            urlModulo={'modulo'}
+            permisosCargados={model?.permisos}
+            alMarcar={(v) => setCheckedList(v)}
+          />
+        </Col>
+        <Col span={24} md={8}>
+          <Typography.Title style={{ margin: 0 }} level={5}>
+            Permisos de Estatus de Solicitud
+          </Typography.Title>
+          <Tree
+            checkable
+            // defaultExpandedKeys={aee}
+            checkedKeys={listaCheckSolicitud}
+            onCheck={onCheckSolicitud}
+            treeData={arbolEstatus}
+          />
+        </Col>
+        <Col span={24} md={8}>
+          <Typography.Title style={{ margin: 0 }} level={5}>
+            Permisos de Estatus de Recurso de Revisión
+          </Typography.Title>
+          <Tree
+            checkable
+            // defaultExpandedKeys={aee}
+            checkedKeys={listaCheckRecurso}
+            onCheck={onCheckRecurso}
+            treeData={arbolRecurso}
+          />
+        </Col>
+      </Row>
+      <Divider />
+      <Row gutter={[16, 0]}>
+        <Col span={6}>
+          <Form.Item>
+            <Button
+              icon={<SaveOutlined />}
+              type="primary"
+              block
+              size="large"
+              htmlType="submit"
+              loading={saveLoading}
+            >
+              Guardar
+            </Button>
+          </Form.Item>
+        </Col>
+      </Row>
+    </Form>
+  )
+}
+
+export default Formulario

+ 57 - 0
src/views/admin/usuarios/UsuarioDetalle.jsx

@@ -0,0 +1,57 @@
+import React, { useState, useEffect } from "react";
+import { DefaultLayout } from "../../../components/layouts";
+import { useModel, useQuery } from "../../../hooks";
+import Formulario from "./Formulario";
+
+const UsuarioDetalle = () => {
+  const endPoint = "usuario";
+  const q = useQuery();
+  const id = q.get("id");
+  const editing = Boolean(id);
+
+  const [request, setRequest] = useState({});
+  const [guardando, setGuardando] = useState(false);
+
+  const requestParams = React.useMemo(
+    () => ({
+      name: endPoint,
+      expand:
+        "permisos,"+
+        "unidadAdministrativaUsuarios,"+
+        "sujetoObligado,"+
+        "estatusPermiso.estatus,"+
+        "ponencia",
+      id: id,
+    }),
+    [id]
+  );
+
+  const { model, modelLoading } = useModel(request);
+
+  useEffect(() => {
+    if (editing) {
+      setRequest(requestParams);
+      return () => setRequest({});
+    }
+  }, [editing, requestParams]);
+
+  return (
+    <DefaultLayout
+      viewLoading={{
+        text: "Guardando...",
+        size: "large",
+        spinning: guardando || modelLoading,
+      }}
+    >
+      <Formulario
+        setGuardando={setGuardando}
+        endPoint={endPoint}
+        model={model}
+        editing={editing}
+        id={id}
+      />
+    </DefaultLayout>
+  );
+};
+
+export default UsuarioDetalle;

+ 103 - 0
src/views/admin/usuarios/Usuarios.jsx

@@ -0,0 +1,103 @@
+import React, { useRef, useState } from "react";
+import { Tooltip } from "antd";
+import { PlusOutlined } from "@ant-design/icons";
+import { Tabla } from "../../../components";
+import { SimpleTableLayout } from "../../../components/layouts";
+import { ActionsButton } from "../../../components";
+import { lastPathName, eliminarRegistro, isEllipsis } from "../../../utilities";
+import { Link, useNavigate } from "react-router-dom";
+
+const Usuarios = () => {
+  const endPoint = "usuario";
+  let tablaRef = useRef(null);
+  const navigate = useNavigate();
+  const { lastPath } = lastPathName();
+  const [buscarParams, setBuscarParams] = useState("");
+
+  const onSearch = (value) => {
+    setBuscarParams(value);
+  };
+  console.log(buscarParams);
+
+  const botones = [
+    {
+      onClick: () => navigate(`/administracion/usuarios/agregar`),
+      props: { disabled: false, type: "primary", block: false },
+      text: "Nuevo",
+      icon: <PlusOutlined />,
+    },
+  ];
+
+  const linkText = (value, row, key) => (
+    <Link
+      to={`/administracion/usuarios/editar?id=${row.id}`}
+      style={{ color: "black" }}
+    >
+      {isEllipsis(columns, key) ? (
+        <Tooltip title={value}>{value}</Tooltip>
+      ) : (
+        value
+      )}
+    </Link>
+  );
+
+  const columns = [
+    {
+      title: "Acciones",
+      key: "id",
+      dataIndex: "id",
+      width: 100,
+      align: "center",
+      render: (_, item) => (
+        <ActionsButton
+          data={[
+            {
+              label: "Editar",
+              onClick: () =>
+                navigate(`/administracion/usuarios/editar?id=${item.id}`),
+            },
+            {
+              label: "Eliminar",
+              onClick: () => {
+                eliminarRegistro(item?.nombre, item?.id, endPoint, () =>
+                  tablaRef?.current?.refresh()
+                );
+              },
+              danger: true,
+            },
+          ]}
+        />
+      ),
+    },
+    {
+      title: "Nombre",
+      key: "nombre",
+      dataIndex: "nombre",
+      render: linkText,
+    },
+    {
+      title: "Corrreo",
+      key: "correo",
+      dataIndex: "correo",
+      render: linkText,
+    },
+  ];
+
+  return (
+    <SimpleTableLayout
+      onSearch={onSearch}
+      btnGroup={{
+        btnGroup: botones,
+      }}
+    >
+      <Tabla
+        columns={columns}
+        nameURL={endPoint}
+        extraParams={{ q: buscarParams }}
+        scroll={{ x: "30vw" }}
+      />
+    </SimpleTableLayout>
+  );
+};
+
+export default Usuarios;

+ 7 - 0
src/views/admin/usuarios/index.jsx

@@ -0,0 +1,7 @@
+import Usuarios from "./Usuarios";
+import UsuarioDetalle from "./UsuarioDetalle";
+
+export {
+  Usuarios,
+  UsuarioDetalle
+}

+ 206 - 0
src/views/auth/Ingresar.jsx

@@ -0,0 +1,206 @@
+import React, { useState } from "react";
+import {
+  ArrowRightOutlined,
+  LockOutlined,
+  UserOutlined,
+  LoadingOutlined,
+} from "@ant-design/icons";
+import { Button, Form, Input, Spin, Typography, List, Row, Col } from "antd";
+import { useAuth } from "../../hooks";
+import { Link, useNavigate } from "react-router-dom";
+import Recuperar from "./Recuperar";
+
+const SignInStyles = {
+  container: {
+    background: "#fff",
+    backdropFilter: "blur(50px)",
+    boxShadow: "0 2px 10px 2px rgb(0 0 0 / 10%)",
+    borderRadius: 6,
+    padding: "16px 20px 20px 20px",
+  },
+  logoContainer: {
+    textAlign: "center",
+    marginBottom: 20,
+    // hacer que se vea mas pequeño
+    "& img": {
+      width: 200,
+    },
+  },
+};
+
+const antIcon = (
+  <LoadingOutlined
+    style={{
+      fontSize: 24,
+    }}
+    spin
+  />
+);
+
+const reglas = {
+  correo: [
+    {
+      type: "email",
+    },
+  ],
+  clave: [
+    {
+      min: 6,
+    },
+  ],
+};
+
+const Ingresar = () => {
+  const { signIn, sessionLoading } = useAuth();
+  const navigate = useNavigate();
+  const onFinish = (values) => {
+    const { correo, clave } = values;
+    signIn(correo, clave);
+  };
+
+  const data = ["Formatos", "Guía de uso", "Aviso de privacidad"];
+
+  return (
+    <Row gutter={[10, 10]}>
+      {/* Listado Izquierdo */}
+      <Col
+        sm={24}
+        md={6}
+        style={{
+          background: "#fff",
+          backdropFilter: "blur(50px)",
+          boxShadow: "0 2px 10px 2px rgb(0 0 0 / 10%)",
+          borderRadius: 6,
+        }}
+      >
+        <div style={{ padding: 20 }}>
+          <Typography.Title level={5} style={{ margin: 0 }}>
+            Documentación
+          </Typography.Title>
+          <List
+            size="small"
+            header={<div>Accesos</div>}
+            bordered
+            dataSource={data}
+            renderItem={(item) => (
+              <List.Item style={{ cursor: "pointer" }}>
+                <ArrowRightOutlined style={{ color: "green" }} /> {item}
+              </List.Item>
+            )}
+          />
+          <p>
+            <i>
+              Si deseas solicitar información a otros gobiernos estatales,
+              <a href="#" target="_blank" rel="noreferrer">
+                {" "}
+                da clic aquí.
+              </a>
+            </i>{" "}
+          </p>
+        </div>
+      </Col>
+
+      {/* Formulario */}
+      <Col sm={24} md={12}>
+        <div style={SignInStyles.container}>
+          <p>
+            <strong>Ingresa aquí tu solicitud.</strong> A través del sistema
+            SIISTAI podrás solicitar toda la información pública del Gobierno
+            del Estado.
+          </p>
+          <div style={SignInStyles.logoContainer}>
+            <img src={"./logo_istai_lg.png"} style={{ width: "60%" }} alt="" />
+          </div>
+          <p>
+            <i>
+              Si desea consultar las versiones públicas de las resoluciones de
+              los recursos de revisión que han realizado otras personas, a
+              través del SIISTAI,
+              <a href="#" target="_blank" rel="noreferrer">
+                {" "}
+                da clic aquí.
+              </a>
+            </i>{" "}
+          </p>
+          <Spin indicator={antIcon} spinning={sessionLoading}>
+            <Form
+              name="normal_login"
+              className="login-form"
+              layout="vertical"
+              initialValues={{
+                remember: false,
+              }}
+              onFinish={onFinish}
+            >
+              <Row gutter={[10, 10]}>
+                <Col span={24}>
+                  <Form.Item name="correo" label="Correo electrónico">
+                    <Input
+                      prefix={<UserOutlined className="site-form-item-icon" />}
+                      placeholder="correo@ejemplo.com"
+                    />
+                  </Form.Item>
+                </Col>
+              </Row>
+
+              <Row gutter={10}>
+                <Col span={24}>
+                  <Form.Item
+                    name="clave"
+                    label="Contraseña"
+                    rules={reglas.clave}
+                    extra={
+                      <Button
+                        type="link"
+                        block
+                        className="login-form-button"
+                        style={{ padding: 0, textAlign: "right" }}
+                        onClick={()=>navigate('/recuperar-contrasena')}
+                      >
+                        ¿Olvidaste tu contraseña?
+                      </Button>
+                    }
+                  >
+                    <Input.Password
+                      prefix={<LockOutlined className="site-form-item-icon" />}
+                      type="new-password"
+                      autoComplete="off"
+                      autoCorrect="off"
+                      placeholder="Contraseña"
+                    />
+                  </Form.Item>
+                </Col>
+              </Row>
+
+              <Row gutter={[10, 10]} justify="center">
+                <Col span={8}>
+                  <Form.Item>
+                    <Button
+                      type="primary"
+                      htmlType="submit"
+                      block
+                      className="login-form-button"
+                    >
+                      Ingresar
+                    </Button>
+                  </Form.Item>
+                </Col>
+              </Row>
+              <div style={{ textAlign: "center" }}>
+                <Typography.Text type="secondary">
+                  ¿No tienes cuenta? <Link to="/registrar">Regístrate</Link>
+                </Typography.Text>
+              </div>
+            </Form>
+          </Spin>
+        </div>
+      </Col>
+      <Col>
+        <div style={SignInStyles.container}></div>
+      </Col>
+      <br />
+    </Row>
+  );
+};
+
+export default Ingresar;

+ 397 - 0
src/views/auth/Recuperar.jsx

@@ -0,0 +1,397 @@
+import {
+  Button,
+  Card,
+  Col,
+  Form,
+  Input,
+  Row,
+  Typography,
+  notification,
+} from "antd";
+import React, { useState } from "react";
+import HttpService from "../../services/httpService";
+import {
+  CheckCircleFilled,
+  CloseCircleOutlined,
+  LoadingOutlined,
+} from "@ant-design/icons";
+import { respuestas } from "../../utilities";
+import { InputPass } from "../../components";
+import { useApp } from "../../hooks";
+import { useNavigate } from "react-router-dom";
+
+const Recuperar = () => {
+  const [formRecuperar] = Form.useForm();
+  const { setToken } = useApp();
+  const navigate = useNavigate();
+
+  const [loading, setLoading] = useState(false);
+  const [esperarToken, setEsperarToken] = useState(false);
+  const [cambiarPassword, setCambiarPassword] = useState("cargando");
+  const [correo, setCorreo] = useState("");
+
+  const onFinish = async (values) => {
+    setLoading(!loading);
+
+    let res;
+
+    try {
+      let body = {
+        ...values,
+      };
+
+      if (cambiarPassword === "valido") {
+        let token = formRecuperar.getFieldsValue([
+          "n1",
+          "n2",
+          "n3",
+          "n4",
+          "n5",
+          "n6",
+          "n7",
+          "n8",
+        ]);
+        let valoresToken = Object.values(token).join("");
+        let tokenEntero = parseInt(valoresToken);
+
+        body.correo = correo;
+        body.token = tokenEntero;
+        res = await HttpService.postPublico(
+          "recuperar-contrasena/cambiar",
+          body
+        );
+        if (res?.status === 200) {
+          setToken(res?.detalle?.token);
+          navigate("/");
+        }
+      } else {
+        res = await HttpService.postPublico(
+          "recuperar-contrasena",
+          body,
+          1,
+          false
+        );
+        if (res?.status === 200) {
+          setCorreo(values?.correo);
+          respuestas(res);
+          setEsperarToken(true);
+          setLoading(false);
+        } else {
+          respuestas(res);
+          setLoading(false);
+        }
+      }
+    } catch (e) {
+      setLoading(false);
+    }
+  };
+
+  const validarToken = async () => {
+    let token = formRecuperar.getFieldsValue([
+      "n1",
+      "n2",
+      "n3",
+      "n4",
+      "n5",
+      "n6",
+      "n7",
+      "n8",
+    ]);
+    let valoresToken = Object.values(token).join("");
+    let tokenEntero = parseInt(valoresToken);
+    try {
+      const res = await HttpService.getPublico(
+        `recuperar-contrasena/verificar?token=${tokenEntero}&correo=${correo}`,
+        false
+      );
+
+      if (res?.status === 200) {
+        setCambiarPassword("valido");
+      } else {
+        setCambiarPassword("fallo");
+        notification.error({
+          message: res?.mensaje,
+        });
+      }
+    } catch (e) {
+      console.log(e);
+    }
+  };
+
+  const passwordValidator = (rule, value, callback) => {
+    if (value && value !== formRecuperar.getFieldValue("pwd")) {
+      callback("Las contraseñas no coinciden");
+    } else {
+      callback();
+    }
+  };
+
+  const tokenIcono = {
+    valido: <CheckCircleFilled style={{ color: "#863695" }} />,
+    fallo: <CloseCircleOutlined style={{ color: "#f00821" }} />,
+    cargando: <LoadingOutlined />,
+  };
+
+  return (
+    <Card
+      title={
+        <Typography.Title style={{ margin: 0 }} level={4}>
+          Cambiar contraseña
+        </Typography.Title>
+      }
+      style={{ width: "60vw" }}
+    >
+      <Form
+        form={formRecuperar}
+        name="form"
+        layout="vertical"
+        onFinish={onFinish}
+      >
+        <Row gutter={[10, 10]} justify="end">
+          {correo === "" && (
+            <>
+              <Col span={24}>
+                <Form.Item
+                  label={<strong>Correo</strong>}
+                  name={"correo"}
+                  rules={[
+                    { required: true, message: "Este campo es obligatorio" },
+                  ]}
+                >
+                  <Input size="large" placeholder="Ingresar correo." />
+                </Form.Item>
+              </Col>
+              <Col span={24}>
+                <Typography.Text type="secondary">
+                  Favor de ingresar el correo relacionado a su cuenta utilizado
+                  para ingresar al sistema.
+                </Typography.Text>
+              </Col>
+              <Col></Col>
+              <Button
+                style={{ backgroundColor: "#863695" }}
+                size="large"
+                type="primary"
+                loading={loading}
+                onClick={() => {
+                  if (
+                    formRecuperar.getFieldValue("correo") &&
+                    formRecuperar.getFieldValue("correo") !== ""
+                  ) {
+                    setEsperarToken(true);
+                    setCorreo(formRecuperar.getFieldValue("correo"));
+                  } else {
+                    notification.warning({
+                      message: "Error",
+                      description: "Favor de ingresar un correo.",
+                    });
+                  }
+                }}
+              >
+                Ya tengo Token
+              </Button>
+              <Col>
+                <Button
+                  size="large"
+                  type="primary"
+                  htmlType="submit"
+                  loading={loading}
+                >
+                  Recuperar
+                </Button>
+              </Col>
+            </>
+          )}
+          {esperarToken && (
+            <>
+              <Col span={24}>
+                <Row gutter={[10, 10]} justify="center">
+                  <Col span={24} style={{ textAlign: "center" }}>
+                    <Typography.Title style={{ margin: 0 }} level={4}>
+                      Ingrese el Token {tokenIcono[cambiarPassword]}
+                    </Typography.Title>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n1">
+                      <Input
+                        id="1"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("2").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n2">
+                      <Input
+                        id="2"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("3").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n3">
+                      <Input
+                        id="3"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("4").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n4">
+                      <Input
+                        id="4"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("5").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n5">
+                      <Input
+                        id="5"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("6").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n6">
+                      <Input
+                        id="6"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("7").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n7">
+                      <Input
+                        id="7"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor !== "") {
+                            return document.getElementById("8").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  <Col span={1}>
+                    <Form.Item name="n8">
+                      <Input
+                        id="8"
+                        size="large"
+                        maxLength={1}
+                        onChange={(v) => {
+                          let valor = v?.target?.value;
+                          if (valor && valor === "") {
+                            return document.getElementById("7").focus();
+                          }
+                        }}
+                      />
+                    </Form.Item>
+                  </Col>
+                  {cambiarPassword === "fallo" && (
+                    <Col span={24} style={{ textAlign: "center" }}>
+                      <Typography.Text style={{ margin: 0, color: "red" }}>
+                        Error, Token Incorrecto, favor de intentar de nuevo.
+                      </Typography.Text>
+                    </Col>
+                  )}
+                </Row>
+              </Col>
+              {cambiarPassword !== "valido" && (
+                <Col>
+                  <Button
+                    size="large"
+                    type="primary"
+                    loading={loading}
+                    onClick={() => validarToken()}
+                  >
+                    Confirmar
+                  </Button>
+                </Col>
+              )}
+            </>
+          )}
+          {cambiarPassword === "valido" && (
+            <>
+              <Col span={24}>
+                <InputPass
+                  name={"pwd"}
+                  label={<strong>Nueva Contraseña</strong>}
+                />
+              </Col>
+              <Col span={24}>
+                <Form.Item
+                  label={<strong>Confirmar Cotraseña</strong>}
+                  name="pwdConf"
+                  rules={[
+                    {
+                      validator: passwordValidator,
+                      validateTrigger: "onChange",
+                    },
+                  ]}
+                >
+                  <Input.Password />
+                </Form.Item>
+              </Col>
+              <Col>
+                <Button
+                  size="large"
+                  type="primary"
+                  htmlType="submit"
+                  loading={loading}
+                >
+                  Confirmar
+                </Button>
+              </Col>
+            </>
+          )}
+        </Row>
+      </Form>
+    </Card>
+  );
+};
+
+export default Recuperar;

+ 181 - 0
src/views/auth/Registrar.jsx

@@ -0,0 +1,181 @@
+import React, { useState } from "react";
+import {
+  Input,
+  Form,
+  Row,
+  Col,
+  Button,
+  Divider,
+  notification,
+  message,
+  Tooltip,
+} from "antd";
+import { respuestas } from "../../utilities";
+import { SaveOutlined, LeftOutlined } from "@ant-design/icons";
+import { useNavigate } from "react-router-dom";
+import Title from "antd/es/typography/Title";
+import HttpService from "../../services/httpService";
+import { useAuth } from "../../hooks";
+import InputPass from "../../components/InputPass";
+
+export const Registrar = () => {
+  const { signIn } = useAuth();
+  const [form] = Form.useForm();
+  const navigate = useNavigate();
+  const [guardando, setGuardando] = useState(false);
+
+  const onFinish = async (values) => {
+    let body = { ...values };
+    body.clave = body.pwd;
+    delete body.pwd;
+    delete body.pwdConf;
+
+    setGuardando(true);
+
+    const res = await HttpService.post(
+      `iniciar-sesion/registrar`,
+      body,
+      false,
+      1,
+      false
+    );
+    if (res?.status === 200) {
+      respuestas(res);
+      signIn(res?.detalle?.correo, body?.clave);
+      navigate(-1);
+      setGuardando(false);
+    } else {
+      if (res?.mensaje?.length > 0) {
+        setGuardando(false);
+        notification.error({
+          message: "Error",
+          description: res?.mensaje,
+        });
+      }
+    }
+  };
+
+  const passwordValidator = (rule, value, callback) => {
+    if (value && value !== form.getFieldValue("pwd")) {
+      callback("Las contraseñas no coinciden");
+    } else {
+      callback();
+    }
+  };
+
+  return (
+    <div
+      style={{
+        background: "#fff",
+        backdropFilter: "blur(50px)",
+        boxShadow: "0 2px 10px 2px rgb(0 0 0 / 10%)",
+        borderRadius: 6,
+        padding: "16px 20px 20px 20px",
+      }}
+    >
+      <Form layout="vertical" form={form} onFinish={onFinish}>
+        <Row>
+          <Col span={24}>
+            {/* Logo */}
+            <Row justify="space-between">
+              <Col>
+                <img
+                  src="/logo.png"
+                  alt="Logo"
+                  style={{
+                    width: 200,
+                  }}
+                />
+              </Col>
+            </Row>
+            <br />
+            {/* Titutlo de la página */}
+            <Title level={3} style={{ margin: 0 }}>
+              Registrar
+            </Title>
+            <Divider style={{ margin: "10px 0" }} />
+          </Col>
+          <br />
+        </Row>
+        <Row gutter={[10, 10]} justify="center">
+          <Col span={23}>
+            <Form.Item
+              name="correo"
+              label="Correo Electrónico"
+              rules={[{ required: true, message: "Este campo es obligatorio" }]}
+            >
+              <Input
+                onBlur={() => console.log("Salió del campo")}
+                placeholder="Ingresa el nombre de Usuario"
+                autocomplete="one-time-code"
+              />
+            </Form.Item>
+          </Col>
+          <Col span={23}>
+            <InputPass name={"pwd"} label={"Contraseña"}/>
+          </Col>
+          <Col span={23}>
+            <Form.Item
+              name="pwdConf"
+              label="Confirmar Contraseña"
+              rules={[
+                { validator: passwordValidator, validateTrigger: "onChange" },
+              ]}
+            >
+              <Input.Password autocomplete="one-time-code" />
+            </Form.Item>
+          </Col>
+          <Col span={23}>
+            <Form.Item name="nombre" label="Nombre">
+              <Input placeholder="Ingresa el nombre" />
+            </Form.Item>
+          </Col>
+          <Col span={23}>
+            <Form.Item
+              name="telefono"
+              label="Teléfono"
+              rules={[
+                { pattern: /^[0-9]+$/, message: "Solo números" },
+                { min: 10, max: 10, message: "Sólo 10 caracteres" },
+              ]}
+            >
+              <Input placeholder="Ingresa el teléfono" />
+            </Form.Item>
+          </Col>
+          <Col span={23}>
+            <Row gutter={[10, 10]} justify={"space-between"}>
+              <Col>
+                <Button
+                  type="primary"
+                  size="large"
+                  onClick={() => {
+                    navigate(-1);
+                  }}
+                  style={{
+                    backgroundColor: "#6f2fa0ff",
+                  }}
+                  icon={<LeftOutlined />}
+                >
+                  Volver
+                </Button>
+              </Col>
+              <Col>
+                <Form.Item>
+                  <Button
+                    type="primary"
+                    htmlType="submit"
+                    size="large"
+                    icon={<SaveOutlined />}
+                    loading={guardando}
+                  >
+                    Guardar
+                  </Button>
+                </Form.Item>
+              </Col>
+            </Row>
+          </Col>
+        </Row>
+      </Form>
+    </div>
+  );
+};

+ 5 - 0
src/views/auth/index.js

@@ -0,0 +1,5 @@
+import Ingresar from "./Ingresar";
+import { Registrar } from "./Registrar";
+import Recuperar from "./Recuperar";
+
+export { Ingresar, Registrar, Recuperar };

+ 236 - 0
src/views/dashboard/DashboardChart.jsx

@@ -0,0 +1,236 @@
+import { useState, useMemo, useEffect } from 'react'
+import { DefaultLayout } from '../../components/layouts'
+import { useQuery, useModel } from '../../hooks'
+import HighchartsReact from "highcharts-react-official";
+import Highcharts from "highcharts";
+import HighchartsExporting from "highcharts/modules/exporting";
+import { Col, Divider, Row } from 'antd';
+
+const generateDefaultChartOptions = (chartType = "pie", options = {}, callback) => ({
+  chart: {
+    type: chartType,
+    inverted: options.inverted || false,
+    options3d: {
+      enabled: chartType === "pie",
+      alpha: 45,
+      beta: 0,
+    },
+    height: options.chartHeight || null,
+  },
+  colors: options?.colores || ["#2f7ed8", "#0d233a", "#8bbc21", "#910000", "#1aadce", "#492970", "#f28f43", "#77a1e5", "#c42525", "#a6c96a"],
+  credits: {
+    enabled: false,
+  },
+  title: {
+    text: options?.titulo || "",
+  },
+  plotOptions: {
+    [chartType]: {
+      innerSize: 100,
+      depth: 45,
+      events: {
+        click: typeof callback === "function" ? callback : () => { },
+      },
+    },
+  },
+  series: [
+    {
+      name: options?.nombre || "NOMBRE DE LA COLECCION DE DATOS",
+      data: options?.datos || [],
+    },
+  ],
+  subtitle: {
+    text: options?.subtitulo || "SUBTITULO POR DEFAULT",
+  },
+  ...options?.options
+});
+
+const DashboardChart = ({ endPoint, expand, url, orden }) => {
+  const q = useQuery()
+  const id = q.get('id')
+  const editando = Boolean(id)
+
+  const [request, setRequest] = useState({})
+
+  const requestParams = useMemo(() => ({
+    name: endPoint,
+    id: id,
+    ordenar: orden,
+    expand: expand,
+  }), [endPoint, expand, id, orden])
+
+  const {
+    model,
+    modelLoading
+  } = useModel(request)
+
+  const chartOptionsPie = generateDefaultChartOptions(
+    "pie",
+    {
+      colores: ["#127d67", "#0d233a", "#8bbc21"],
+      titulo: "Ausentismo",
+      subtitulo: "",
+      nombre: "Asuntos",
+      datos: [
+        {
+          name: "Hermosillo",
+          y: 50,
+        },
+        {
+          name: "Obregón",
+          y: 14,
+        },
+        {
+          name: "SLRC",
+          y: 6,
+        },
+      ],
+    },
+    // callbackExample
+  );
+
+  const chartOptionsLine = generateDefaultChartOptions(
+    "line",
+    {
+      colores: ["#127d67", "#0d233a", "#8bbc21"],
+      titulo: "Promedio Académico",
+      subtitulo: "",
+      nombre: "Hermosillo I",
+      datos: [
+        {
+          name: "Hermosillo",
+          y: 50,
+        },
+        {
+          name: "Obregón",
+          y: 14,
+        },
+        {
+          name: "SLRC",
+          y: 6,
+        },
+      ],
+      options: {
+        yAxis: {
+          title: {
+            text: 'Calificación'
+          }
+        }
+      }
+    },
+    // callbackExample
+  );
+
+  const chartOptionsBar = generateDefaultChartOptions(
+    "bar",
+    {
+      colores: ["#127d67", "#0d233a", "#8bbc21"],
+      titulo: "Servicios Escolares",
+      subtitulo: "Total: 32",
+      nombre: "Asuntos Pendientes",
+      datos: [
+        {
+          name: "Hermosillo",
+          y: 50,
+        },
+        {
+          name: "Obregón",
+          y: 14,
+        },
+        {
+          name: "SLRC",
+          y: 6,
+        },
+      ],
+    },
+    // callbackExample
+  );
+
+  const chartOptionsColumn = generateDefaultChartOptions(
+    "column",
+    {
+      colores: ["#127d67", "#0d233a", "#8bbc21"],
+      titulo: "Ingresos por Plantel",
+      subtitulo: "Total: 32",
+      nombre: "Asuntos",
+      datos: [
+        {
+          name: "Hermosillo",
+          y: 50,
+        },
+        {
+          name: "Obregón",
+          y: 14,
+        },
+        {
+          name: "SLRC",
+          y: 6,
+        },
+      ],
+      options: {
+        xAxis: {
+          categories: [
+            'Hermosillo I',
+            'Obregón',
+            'SLRC'
+          ]
+        }
+      }
+    },
+    // callbackExample
+  );
+
+  HighchartsExporting(Highcharts);
+
+  useEffect(() => {
+    setRequest(requestParams)
+    return () => {
+      setRequest({})
+    }
+  }, [requestParams])
+
+  return (
+    <DefaultLayout
+      viewLoading={{
+        text: 'Cargando ...',
+        spinning: modelLoading
+      }}
+    >
+      <Row>
+        <Col span={12}>
+          <HighchartsReact
+            highcharts={Highcharts}
+            options={chartOptionsPie}
+            constructorType={"chart"}
+          />
+        </Col>
+        <Col span={12}>
+          <HighchartsReact
+            highcharts={Highcharts}
+            options={chartOptionsLine}
+            constructorType={"chart"}
+          />
+        </Col>
+      </Row>
+      <Divider />
+      <Row>
+        <Col span={12}>
+          <HighchartsReact
+            highcharts={Highcharts}
+            options={chartOptionsBar}
+            constructorType={"chart"}
+          />
+        </Col>
+        <Col span={12}>
+          <HighchartsReact
+            highcharts={Highcharts}
+            options={chartOptionsColumn}
+            constructorType={"chart"}
+          />
+        </Col>
+      </Row>
+    </DefaultLayout>
+  )
+}
+
+export default  DashboardChart;

+ 17 - 0
src/views/dashboard/index.jsx

@@ -0,0 +1,17 @@
+import React from 'react'
+import DashboardChart from './DashboardChart'
+
+const endPoint = 'categoria-producto'
+const url = '/expediente'
+const expand = ''
+const orden = 'id-desc'
+
+
+const Dashboard = () => (<DashboardChart
+  endPoint={endPoint}
+  expand={expand}
+  url={url}
+  orden={orden}
+/>)
+
+export { Dashboard }

+ 14 - 0
src/views/error/NoAutorizado.jsx

@@ -0,0 +1,14 @@
+import React from "react";
+import { Result } from "antd";
+
+const NoAutorizado = () => {
+  return (
+    <Result
+      status="403"
+      title="403"
+      subTitle="No tienes permisos para acceder a esta página."
+    />
+  );
+};
+
+export default NoAutorizado;

+ 14 - 0
src/views/error/NoEncontrado.jsx

@@ -0,0 +1,14 @@
+import React from 'react'
+import { useNavigate } from 'react-router-dom'
+
+const NoEncontrado = () => {
+  const navigate = useNavigate()
+
+  React.useEffect(() => {
+    // navigate("/")
+  }, [])
+
+  return 'NO ENCONTRADO'
+}
+
+export default NoEncontrado

+ 7 - 0
src/views/error/index.js

@@ -0,0 +1,7 @@
+import NoEncontrado from "./NoEncontrado";
+import NoAutorizado from "./NoAutorizado";
+
+export {
+  NoEncontrado,
+  NoAutorizado
+}

+ 92 - 0
src/views/inicio/Inicio.jsx

@@ -0,0 +1,92 @@
+import React from "react";
+import { Col, Row, Typography, Card, Statistic } from "antd";
+import { FileExclamationOutlined, FileUnknownOutlined, FormOutlined, ScheduleOutlined } from "@ant-design/icons";
+import { PiFoldersBold } from "react-icons/pi"
+import { DefaultLayout } from "../../components/layouts";
+
+const Inicio = () => {
+
+  const { Title } = Typography;
+
+  return (
+    <DefaultLayout>
+      <Row gutter={10}>
+        <Title level={3}>
+          Inicio
+        </Title>
+      </Row>
+      <Row gutter={[10, 10]}>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+            onClick={() => {}}
+          >
+            <Statistic
+              value=''
+              prefix={<FormOutlined />}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+          >
+            <Statistic
+              value=''
+              prefix={<FileExclamationOutlined />}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+            onClick={() => {}}
+          >
+            <Statistic
+              value=''
+              prefix={<ScheduleOutlined />}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+          >
+            <Statistic
+              value=''
+              prefix={<ScheduleOutlined />}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+          >
+            <Statistic
+              value=''
+              prefix={<PiFoldersBold />}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card
+            hoverable={true}
+            style={{ cursor: 'pointer' }}
+          >
+            <Statistic
+              value=''
+              prefix={<FileUnknownOutlined />}
+            />
+          </Card>
+        </Col>
+      </Row>
+    </DefaultLayout>
+  );
+};
+
+export default Inicio;

+ 5 - 0
src/views/inicio/index.js

@@ -0,0 +1,5 @@
+import Inicio from "./Inicio"
+
+export {
+  Inicio
+}

+ 269 - 0
src/views/perfil/Perfil.jsx

@@ -0,0 +1,269 @@
+import React, { useEffect, useState } from 'react'
+import { Row, Col, Form, Button, Card, Avatar, Divider, Upload, message, Collapse } from 'antd'
+import { UserOutlined, MailOutlined, PhoneOutlined, LockOutlined } from '@ant-design/icons'
+import { InputPass, ViewLoading } from '../../components'
+import { useApp, useAuth } from '../../hooks'
+import HttpService from '../../services/httpService'
+import { DefaultLayout } from '../../components/layouts'
+import { respuestas } from '../../utilities'
+
+const baseUrl = import.meta.env.VITE_API_URL
+
+const Perfil = () => {
+
+  const { user } = useAuth()
+  const [form] = Form.useForm()
+  const { token } = useApp()
+
+  const [guardando, setGuardando] = useState(false)
+
+  const [urlFoto, setUrlFoto] = useState(null)
+  const [listaArchivosFotoPerfil, setListaArchivosFotoPerfil] = useState([])
+  const [archivoCargando, setArchivoCargando] = useState(null)
+
+  const onChangePicturePerfil = ({ fileList: newFileList }) => {
+    let _archivo = newFileList[0]?.originFileObj
+    const isImage = _archivo.type.includes('image/')
+
+    if (!isImage) {
+      message.error(`${_archivo.name} no es un archivo de Imagen`)
+      return
+    }
+
+    subirImagenPerfil(newFileList)
+  }
+
+  const subirImagenPerfil = async (file, idDocumento) => {
+    try {
+      setArchivoCargando(true)
+
+      let _archivo = file[0]?.originFileObj
+
+      if (!_archivo) {
+        message.info({
+          content: 'Debes de seleccionarun archivo',
+          style: { marginTop: '20vh' },
+        })
+        return
+      }
+
+      const form = new FormData()
+      form.append('archivo', _archivo)
+      form.append('idUsuario', user?.id)
+
+      const response = await fetch(baseUrl + '/v1/perfil/cambiar-foto-perfil', {
+        method: 'POST',
+        headers: {
+          Authorization: `Bearer ${token}`,
+        },
+        body: form,
+      })
+
+      const data = await response.json()
+      setUrlFoto(data?.resultado[0])
+
+      response.mensaje = data.mensaje
+      respuestas(response)
+
+    } catch (error) {
+      console.log('error al cargar archivo: ', error)
+    } finally {
+      setArchivoCargando(false)
+
+    }
+  }
+
+  const items = [
+    {
+      key: '1', label: 'Cambiar la contraseña', children: <>
+        <p
+          style={{
+            color: '#777',
+            marginBottom: '20px',
+            marginTop: '-10px',
+          }}
+        >
+          Si no desea cambiar la contraseña, deje los campos en blanco.
+        </p>
+        <Row gutter={[16, 16]}>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 24 }}
+            lg={{ span: 24 }}
+          >
+            <InputPass
+              label="Confirmar Contraseña"
+              name="clave"
+            />
+          </Col>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 24 }}
+            lg={{ span: 24 }}
+          >
+            <InputPass
+              label="Contraseña"
+              name="clave1"
+            />
+          </Col>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 24 }}
+            lg={{ span: 24 }}
+          >
+            <Form.Item>
+              <Button type="primary" htmlType="submit" loading={guardando}>
+                Guardar
+              </Button>
+            </Form.Item>
+          </Col>
+        </Row>
+      </>
+    },
+  ]
+
+  const onFinish = async (values) => {
+    try {
+      setGuardando(true)
+
+      let body = {
+        ...values,
+        idUsuario: user.id
+      }
+
+      const resp = await HttpService.post('usuario/cambiar-clave', body)
+
+      if (respuestas(resp)) {
+        form.resetFields()
+      }
+    } catch (error) {
+      console.log('error en Perfil.js: ', error)
+    } finally {
+      setGuardando(false)
+    }
+  }
+
+  useEffect(() => {
+    if (!user) return
+
+    form.setFieldsValue({
+      nombre: user.nombre,
+      correo: user.correo,
+    })
+
+    setUrlFoto(user?.foto)
+
+  }, [form, user])
+
+  if (!user) return <ViewLoading/>
+
+  return (
+    <DefaultLayout>
+      <Form onFinish={onFinish} form={form} layout="vertical" name="form">
+        <Row gutter={10} justify="center">
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Card>
+              <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+                <Col span={24} md={6}>
+                  <Form.Item
+                    label={'Elegir foto de Perfil:'}
+                  >
+                    <Upload
+                      beforeUpload={() => false}
+                      multiple={true}
+                      listType="picture-card"
+                      fileList={listaArchivosFotoPerfil}
+                      loading={archivoCargando}
+                      onChange={onChangePicturePerfil}
+                      onRemove={() => {
+                        setListaArchivosFotoPerfil([])
+                        setUrlFoto(null)
+                      }}
+                    >
+                      <Avatar size={100} icon={
+                        <UserOutlined/>} src={urlFoto}/>
+                    </Upload>
+                  </Form.Item>
+                </Col>
+                <div /* style={{ paddingLeft: '20px' }} */>
+                  <h2>{user.nombre}</h2>
+                </div>
+              </div>
+            </Card>
+          </Col>
+        </Row>
+        <br/>
+        <Row gutter={10} justify="center">
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Card>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <Avatar size={50} icon={<UserOutlined/>}/>
+                <div style={{ marginLeft: '10px' }}>
+                <span>
+                  Usuario: {user?.usuario} - <span style={{ color: 'blue' }}>{user?.nombre}</span>
+                </span>
+                </div>
+              </div>
+              <Divider/>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <Avatar size={50} icon={<MailOutlined/>}/>
+                <div style={{ marginLeft: '10px' }}>
+                <span>
+                  Correo: {user?.correo}
+                </span>
+                </div>
+              </div>
+              <Divider/>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <Avatar size={50} icon={<PhoneOutlined/>}/>
+                <div style={{ marginLeft: '10px' }}>
+                <span>
+                  Teléfono: {user?.telefono ? user?.telefono : '---'}
+                </span>
+                </div>
+              </div>
+              <Divider/>
+              <div style={{ display: 'flex', alignItems: 'center' }}>
+                <Avatar size={50} icon={<LockOutlined/>}/>
+                <div style={{ marginLeft: '10px' }}>
+                <span>
+                  Rol: {user?.rol ? user?.rol : '---'}
+                </span>
+                </div>
+              </div>
+            </Card>
+          </Col>
+        </Row>
+        <br/>
+        <br/>
+        <Row gutter={10} justify="center">
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Card>
+              <Collapse items={items}/>
+            </Card>
+          </Col>
+        </Row>
+      </Form>
+    </DefaultLayout>
+  )
+}
+
+export default Perfil

+ 5 - 0
src/views/perfil/index.js

@@ -0,0 +1,5 @@
+import Perfil from "./Perfil";
+
+export {
+  Perfil
+}

+ 9 - 0
vite.config.js

@@ -0,0 +1,9 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+// https://vitejs.dev/config/
+export default () => {
+  return defineConfig({
+      plugins: [react()],
+  })
+}