ElPoteito vor 1 Jahr
Commit
9e1c020f86
100 geänderte Dateien mit 6725 neuen und 0 gelöschten Zeilen
  1. 19 0
      .env.development.local
  2. 19 0
      .env.production.local
  3. 5 0
      .firebaserc
  4. 42 0
      .gitignore
  5. 70 0
      README.md
  6. 16 0
      craco.config.js
  7. 19 0
      firebase.json
  8. 51 0
      package.json
  9. BIN
      public/LAUD-Logotipo.png
  10. BIN
      public/assets/Facebook.png
  11. BIN
      public/assets/Instagram.png
  12. BIN
      public/assets/LAUD-Avatar.png
  13. BIN
      public/assets/LAUD-Logo-Blanco.png
  14. BIN
      public/assets/LAUD-Logotipo-Blanco.png
  15. BIN
      public/assets/LAUD-Logotipo.png
  16. BIN
      public/assets/Otro.png
  17. BIN
      public/assets/Tiktok.png
  18. BIN
      public/assets/Twitter.png
  19. BIN
      public/assets/Whatsapp.png
  20. BIN
      public/assets/Youtube.png
  21. BIN
      public/assets/comparativa.png
  22. BIN
      public/assets/dependencia.png
  23. BIN
      public/assets/eventos.png
  24. BIN
      public/assets/grupos.png
  25. BIN
      public/assets/individual.png
  26. BIN
      public/assets/login-bg.png
  27. BIN
      public/assets/logo-collapsed.png
  28. BIN
      public/assets/logo-light.png
  29. BIN
      public/assets/logo.png
  30. BIN
      public/assets/profile.png
  31. BIN
      public/assets/reportes.png
  32. BIN
      public/assets/usuarios.png
  33. BIN
      public/assets/verificado.png
  34. BIN
      public/favicon.ico
  35. BIN
      public/favicono.ico
  36. BIN
      public/icono.png
  37. 20 0
      public/index.html
  38. BIN
      public/logo.png
  39. BIN
      public/logo192.png
  40. BIN
      public/logo512.png
  41. 25 0
      public/manifest.json
  42. 3 0
      public/robots.txt
  43. BIN
      public/white-logo.png
  44. 36 0
      src/App.js
  45. 151 0
      src/Theme.less
  46. 40 0
      src/components/ActionsButton.js
  47. 28 0
      src/components/AppLoading.js
  48. 16 0
      src/components/CircularProgress.js
  49. 7 0
      src/components/NotFound.js
  50. 143 0
      src/components/Select.js
  51. 98 0
      src/components/Tabla.js
  52. 20 0
      src/components/ViewLoading.js
  53. 91 0
      src/components/asyncAutocomplete/AsyncAutocompleteAudioSearch.js
  54. 92 0
      src/components/asyncAutocomplete/AsyncAutocompleteGuidelineGroup.js
  55. 15 0
      src/components/index.js
  56. 11 0
      src/components/layouts/AuthLayout.js
  57. 230 0
      src/components/layouts/DashboardLayout.js
  58. 50 0
      src/components/layouts/DefaultLayout.js
  59. 71 0
      src/components/layouts/SimpleTableLayout.js
  60. 11 0
      src/components/layouts/index.js
  61. 59 0
      src/constants/httpStatusCodes.js
  62. 22 0
      src/constants/index.js
  63. 32 0
      src/constants/requests.js
  64. 11 0
      src/hooks/index.js
  65. 66 0
      src/hooks/useAlert.js
  66. 60 0
      src/hooks/useApp.js
  67. 143 0
      src/hooks/useAuth.js
  68. 84 0
      src/hooks/useDialog.js
  69. 163 0
      src/hooks/useHttp.js
  70. 90 0
      src/hooks/useModel.js
  71. 87 0
      src/hooks/useModels.js
  72. 28 0
      src/hooks/useNotifications.js
  73. 40 0
      src/hooks/usePagination.js
  74. 7 0
      src/hooks/useQuery.js
  75. 57 0
      src/hooks/useSortColumns.js
  76. 16 0
      src/index.js
  77. BIN
      src/menu-bg.jpeg
  78. 13 0
      src/reportWebVitals.js
  79. 22 0
      src/routers/AppRouting.js
  80. 45 0
      src/routers/PrivateRouter.js
  81. 30 0
      src/routers/PublicRouter.js
  82. 4 0
      src/routers/index.js
  83. 302 0
      src/routers/routes.js
  84. 215 0
      src/services/excelCondensado.js
  85. 115 0
      src/services/exportarExcel.js
  86. 41 0
      src/services/firebase.js
  87. 223 0
      src/services/httpService.js
  88. 3 0
      src/services/index.js
  89. 5 0
      src/setupTests.js
  90. 0 0
      src/styles/AuthLayout.less
  91. 31 0
      src/styles/DashboardLayout.less
  92. 190 0
      src/styles/LoginForm.less
  93. 74 0
      src/utilities/index.js
  94. 338 0
      src/views/administracion/dependencias/DependenciaDetalle.js
  95. 142 0
      src/views/administracion/dependencias/DependenciasListado.js
  96. 7 0
      src/views/administracion/dependencias/index.js
  97. 1125 0
      src/views/administracion/eventos/EventoDetalleGrupo.js
  98. 934 0
      src/views/administracion/eventos/EventosDetalle.js
  99. 502 0
      src/views/administracion/eventos/EventosListado.js
  100. 0 0
      src/views/administracion/eventos/ResultadoEventoAccion.js

+ 19 - 0
.env.development.local

@@ -0,0 +1,19 @@
+# Projecto   firebase | jwt
+REACT_APP_PROJECT=firebase
+# REACT_APP_API_URL=http://localhost:8080/
+REACT_APP_API_URL=https://yager.api.fourier.audio/
+REACT_APP_WEB_URL=https://laud-sec.web.app/
+# REACT_APP_WEB_URL=https://yager.api.fourier.audio/
+REACT_APP_CLOUD_FUNC=https://us-central1-yagermx.cloudfunctions.net
+# REACT_APP_CLOUD_FUNC=https://yager.api.fourier.audio/
+REACT_APP_VERSION=1.23.08.29+2
+
+# Firebase
+REACT_APP_FB_API_KEY=AIzaSyB_o2gy7tsyha2JFiCgdztk04dp1pzc4jQ
+REACT_APP_FB_AUTH_DOMAIN=laud-sec.firebaseapp.com
+# REACT_APP_FB_DB_URL=https://laud-sec.firebaseio.com
+REACT_APP_FB_DB_URL=https://yagermx.firebaseio.com
+REACT_APP_FB_PROJ_ID=laud-sec
+REACT_APP_FB_STORAGE=laud-sec.appspot.com
+REACT_APP_FB_SENDER_ID=297210850630
+REACT_APP_FB_APP_ID=1:297210850630:web:6863277326d931e2cd4947s

+ 19 - 0
.env.production.local

@@ -0,0 +1,19 @@
+# Projecto   firebase | jwt
+REACT_APP_PROJECT=firebase
+REACT_APP_API_URL=https://yager.api.fourier.audio/
+REACT_APP_WEB_URL=https://laud-sec.web.app/
+# REACT_APP_WEB_URL=https://yager.api.fourier.audio/
+REACT_APP_CLOUD_FUNC=https://us-central1-yagermx.cloudfunctions.net
+# REACT_APP_CLOUD_FUNC=https://yager.api.fourier.audio/
+REACT_APP_VERSION=1.23.08.29+2
+
+# Firebase
+REACT_APP_FB_API_KEY=AIzaSyB_o2gy7tsyha2JFiCgdztk04dp1pzc4jQ
+REACT_APP_FB_AUTH_DOMAIN=laud-sec.firebaseapp.com
+REACT_APP_FB_DB_URL=https://laud-sec.firebaseio.com
+REACT_APP_FB_PROJ_ID=laud-sec
+REACT_APP_FB_STORAGE=laud-sec.appspot.com
+REACT_APP_FB_SENDER_ID=297210850630
+REACT_APP_FB_APP_ID=1:297210850630:web:6863277326d931e2cd4947
+
+GENERATE_SOURCEMAP=false

+ 5 - 0
.firebaserc

@@ -0,0 +1,5 @@
+{
+  "projects": {
+    "default": "laud-sec"
+  }
+}

+ 42 - 0
.gitignore

@@ -0,0 +1,42 @@
+.DS_STORE
+node_modules
+scripts/flow/*/.flowconfig
+.flowconfig
+*~
+*.pyc
+.grunt
+_SpecRunner.html
+__benchmarks__
+build/
+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

+ 70 - 0
README.md

@@ -0,0 +1,70 @@
+# Getting Started with Create React App
+
+This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
+
+## Available Scripts
+
+In the project directory, you can run:
+
+### `yarn start`
+
+Runs the app in the development mode.\
+Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
+
+The page will reload when you make changes.\
+You may also see any lint errors in the console.
+
+### `yarn test`
+
+Launches the test runner in the interactive watch mode.\
+See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
+
+### `yarn build`
+
+Builds the app for production to the `build` folder.\
+It correctly bundles React in production mode and optimizes the build for the best performance.
+
+The build is minified and the filenames include the hashes.\
+Your app is ready to be deployed!
+
+See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
+
+### `yarn eject`
+
+**Note: this is a one-way operation. Once you `eject`, you can't go back!**
+
+If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
+
+Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
+
+You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
+
+## Learn More
+
+You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
+
+To learn React, check out the [React documentation](https://reactjs.org/).
+
+### Code Splitting
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
+
+### Analyzing the Bundle Size
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
+
+### Making a Progressive Web App
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
+
+### Advanced Configuration
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
+
+### Deployment
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
+
+### `yarn build` fails to minify
+
+This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

+ 16 - 0
craco.config.js

@@ -0,0 +1,16 @@
+const CracoLessPlugin = require("craco-less");
+
+module.exports = {
+  plugins: [
+    {
+      plugin: CracoLessPlugin,
+      options: {
+        lessLoaderOptions: {
+          lessOptions: {
+            javascriptEnabled: true,
+          },
+        },
+      },
+    },
+  ],
+};

+ 19 - 0
firebase.json

@@ -0,0 +1,19 @@
+{
+  "hosting": {
+    "public": "build",
+    "site": "laud-sec",
+    "siteYager": "yager-red",
+    "siteLaud": "laud-red",
+    "ignore": [
+      "firebase.json",
+      "**/.*",
+      "**/node_modules/**"
+    ],
+    "rewrites": [
+      {
+        "source": "**",
+        "destination": "/index.html"
+      }
+    ]
+  }
+}

+ 51 - 0
package.json

@@ -0,0 +1,51 @@
+{
+  "name": "web-base",
+  "version": "0.1.0",
+  "private": true,
+  "dependencies": {
+    "@ant-design/icons": "^4.7.0",
+    "@craco/craco": "^6.4.5",
+    "@testing-library/jest-dom": "^5.14.1",
+    "@testing-library/react": "^13.0.0",
+    "@testing-library/user-event": "^13.2.1",
+    "antd": "^4.22.2",
+    "craco-less": "^2.0.0",
+    "exceljs": "^4.3.0",
+    "file-saver": "^2.0.5",
+    "firebase": "^9.9.1",
+    "highcharts": "^10.3.3",
+    "highcharts-react-official": "^3.1.0",
+    "image-to-base64": "^2.2.0",
+    "react": "^18.2.0",
+    "react-dom": "^18.2.0",
+    "react-firebase-hooks": "^5.0.3",
+    "react-helmet-async": "^1.3.0",
+    "react-router-dom": "^5.2.0",
+    "react-scripts": "5.0.1",
+    "web-vitals": "^2.1.0"
+  },
+  "scripts": {
+    "start": "craco start",
+    "build": "craco build",
+    "test": "craco test",
+    "eject": "craco eject"
+  },
+  "eslintConfig": {
+    "extends": [
+      "react-app",
+      "react-app/jest"
+    ]
+  },
+  "browserslist": {
+    "production": [
+      ">0.2%",
+      "not dead",
+      "not op_mini all"
+    ],
+    "development": [
+      "last 1 chrome version",
+      "last 1 firefox version",
+      "last 1 safari version"
+    ]
+  }
+}

BIN
public/LAUD-Logotipo.png


BIN
public/assets/Facebook.png


BIN
public/assets/Instagram.png


BIN
public/assets/LAUD-Avatar.png


BIN
public/assets/LAUD-Logo-Blanco.png


BIN
public/assets/LAUD-Logotipo-Blanco.png


BIN
public/assets/LAUD-Logotipo.png


BIN
public/assets/Otro.png


BIN
public/assets/Tiktok.png


BIN
public/assets/Twitter.png


BIN
public/assets/Whatsapp.png


BIN
public/assets/Youtube.png


BIN
public/assets/comparativa.png


BIN
public/assets/dependencia.png


BIN
public/assets/eventos.png


BIN
public/assets/grupos.png


BIN
public/assets/individual.png


BIN
public/assets/login-bg.png


BIN
public/assets/logo-collapsed.png


BIN
public/assets/logo-light.png


BIN
public/assets/logo.png


BIN
public/assets/profile.png


BIN
public/assets/reportes.png


BIN
public/assets/usuarios.png


BIN
public/assets/verificado.png


BIN
public/favicon.ico


BIN
public/favicono.ico


BIN
public/icono.png


+ 20 - 0
public/index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="utf-8" />
+    <link rel="icon" href="favicon.ico" />
+    <meta name="viewport" content="width=device-width, initial-scale=1" />
+    <meta name="theme-color" content="#0d1f2b" />
+    <meta
+      name="description"
+      content="LAUD"
+    />
+    <link rel="apple-touch-icon" href="assets/LAUD-Avatar.png" />
+    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
+    <title>LAUD</title>
+  </head>
+  <body>
+    <noscript>You need to enable JavaScript to run this app.</noscript>
+    <div id="root"></div>
+  </body>
+</html>

BIN
public/logo.png


BIN
public/logo192.png


BIN
public/logo512.png


+ 25 - 0
public/manifest.json

@@ -0,0 +1,25 @@
+{
+  "short_name": "React App",
+  "name": "Create React App Sample",
+  "icons": [
+    {
+      "src": "favicon.ico",
+      "sizes": "64x64 32x32 24x24 16x16",
+      "type": "image/x-icon"
+    },
+    {
+      "src": "logo192.png",
+      "type": "image/png",
+      "sizes": "192x192"
+    },
+    {
+      "src": "logo512.png",
+      "type": "image/png",
+      "sizes": "512x512"
+    }
+  ],
+  "start_url": ".",
+  "display": "standalone",
+  "theme_color": "#000000",
+  "background_color": "#ffffff"
+}

+ 3 - 0
public/robots.txt

@@ -0,0 +1,3 @@
+# https://www.robotstxt.org/robotstxt.html
+User-agent: *
+Disallow:

BIN
public/white-logo.png


+ 36 - 0
src/App.js

@@ -0,0 +1,36 @@
+import React from "react";
+import {
+  AppProvider,
+  AuthProvider,
+  AlertProvider,
+  DialogProvider,
+  NotificationsProvider,
+} from "./hooks";
+import "./Theme.less";
+import esES from "antd/lib/locale/es_ES";
+import moment from "moment";
+import { AppRouting } from "./routers";
+import { ConfigProvider } from "antd";
+import { HelmetProvider } from "react-helmet-async";
+
+moment.locale("es");
+
+const App = () => (
+  <ConfigProvider locale={esES}>
+    <HelmetProvider>
+      <AppProvider>
+        <AlertProvider>
+          <DialogProvider>
+            <NotificationsProvider>
+              <AuthProvider>
+                <AppRouting />
+              </AuthProvider>
+            </NotificationsProvider>
+          </DialogProvider>
+        </AlertProvider>
+      </AppProvider>
+    </HelmetProvider>
+  </ConfigProvider>
+);
+
+export default App;

+ 151 - 0
src/Theme.less

@@ -0,0 +1,151 @@
+@import "~antd/dist/antd.less";
+@menu-image: "menu-bg.jpeg";
+// @primary-color: #92b630;
+@primary-color: #ffd744;
+@layout-header-background: transparent;
+
+#components-layout-demo-custom-trigger .trigger {
+  padding: 0 24px;
+  font-size: 18px;
+  line-height: 64px;
+  cursor: pointer;
+  transition: color 0.3s;
+}
+
+#auth-sider .logo {
+  height: 32px;
+  margin: 14px;
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: center center;
+  -webkit-transition: opacity 1s ease-in-out;
+  -moz-transition: opacity 1s ease-in-out;
+  -o-transition: opacity 1s ease-in-out;
+  transition: opacity 1s ease-in-out;
+}
+
+#auth-sider .logo-collapsed {
+  height: 48px;
+  margin: 7px;
+  background-size: contain;
+  background-repeat: no-repeat;
+  background-position: center center;
+  -webkit-transition: opacity 1s ease-in-out;
+  -moz-transition: opacity 1s ease-in-out;
+  -o-transition: opacity 1s ease-in-out;
+  transition: opacity 1s ease-in-out;
+}
+
+#components-layout-demo-custom-trigger .trigger:hover {
+  color: #24a0d6;
+}
+
+.site-layout .site-layout-background {
+  background: #fff;
+  margin: 10px;
+}
+
+.ant-layout-sider-light {
+  // background: #262626;
+  background: #0f1e2e;
+}
+
+.ant-layout-sider-light::before {
+  content: '' !important;
+  width: 100% !important;
+  height: 100% !important;
+  position: absolute !important;
+  top: 0 !important;
+  left: 0 !important;
+  display: block !important;
+  z-index: 0 !important;
+  background-image: url(@menu-image) !important;
+  background-position: center center !important;
+  opacity: 0.1 !important;
+  background-size: cover !important;
+}
+
+.sidebar-logo {
+  display: flex;
+  justify-content: center;
+  border-top: solid 1px #314e68;
+  width: 95%;
+  margin: 0px auto;
+  position: absolute;
+  bottom: 5px;
+}
+
+.ant-layout-header {
+  padding: 0px 20px !important;
+  z-index: 99;
+  background-color: #fff;
+  border-bottom: solid 1px #eaeaea;
+}
+
+.ant-layout-footer {
+  padding: 24px 50px;
+  color: #b9b9b9;
+  font-size: 14px;
+  border-top: solid 1px #eaeaea;
+  background: #ffffff;
+}
+
+.ed-lista-botones {
+  display: flex;
+  flex-direction: row;
+  justify-content: flex-end;
+  margin-bottom: 10px;
+}
+
+.ed-lista-botones .ant-btn {
+  margin-left: 10px;
+}
+
+@media only screen and (max-width: 767px) {
+  .ed-lista-botones .ant-btn {
+    margin-right: 10px;
+    margin-left: 0px;
+  }
+}
+
+.ant-menu-dark .ant-menu-inline.ant-menu-sub {
+  // background: #3b3b3b !important;
+  background: #1d3044 !important;
+}
+
+.ant-menu-dark.ant-menu-submenu-popup {
+  background: #333;
+}
+
+.ant-menu .ant-menu-sub .ant-menu-vertical .ant-menu-item {
+  color: #333 !important
+}
+
+.ant-table-cell {
+  cursor: pointer;
+}
+
+.ant-message {
+  position: absolute;
+	width: fit-content;
+	bottom: 20px;
+	top: initial;
+	right: 20px;
+	left: initial;
+}
+
+.ant-menu-dark .ant-menu-item-selected .ant-menu-item-icon, .ant-menu-dark .ant-menu-item-selected .anticon {
+    color: #0f1e2e;
+}
+
+.ant-btn-primary {
+    color: #0f1e2e;
+    border-color: #ffd744;
+    background: #ffd744;
+    text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.12);
+    box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
+}
+
+.ant-menu-dark .ant-menu-item-selected .ant-menu-item-icon + span, .ant-menu-dark .ant-menu-item-selected .anticon + span {
+  color: #0f1e2e;
+}

+ 40 - 0
src/components/ActionsButton.js

@@ -0,0 +1,40 @@
+import React from 'react';
+import { Button, Menu, Dropdown } from 'antd';
+import { MoreOutlined } from '@ant-design/icons';
+
+const ActionsButton = ({ options = [] }) => {
+
+  const [ menuItems, setMenuItems ] = React.useState([]);
+
+  React.useEffect(() => {
+    if( options?.length > 0 ) {
+      const items = [];
+      for( let i = 0; i < options.length; i ++ ) {
+        items.push({
+          ...[i]?.props,
+          key: i +  options[i]?.name,
+          icon: options[i].icon,
+          label: options[i]?.name,
+          onClick: options[i]?.onClick,
+          style: options[i]?.styleProps,
+          disabled: options[i]?.disabled,
+        });
+      }
+      setMenuItems(items);
+    }
+  }, [options])
+
+  return (
+    <Dropdown
+      overlay={<Menu items={menuItems} />}
+      placement="right"
+      arrow
+    >
+      <Button>
+        <MoreOutlined />
+      </Button>
+    </Dropdown>
+  );
+};
+
+export default ActionsButton;

+ 28 - 0
src/components/AppLoading.js

@@ -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;

+ 16 - 0
src/components/CircularProgress.js

@@ -0,0 +1,16 @@
+import React from "react";
+import { Spin } from "antd";
+import { LoadingOutlined } from "@ant-design/icons";
+
+const CircularProgress = ({ color = "" }) => {
+  const antIcon = (
+    <LoadingOutlined
+      style={{ fontSize: 24, color: color ? color : "white" }}
+      spin
+    />
+  );
+
+  return <Spin indicator={antIcon} />;
+};
+
+export default CircularProgress;

+ 7 - 0
src/components/NotFound.js

@@ -0,0 +1,7 @@
+import React from "react";
+
+const NotFound = () => {
+  return <div>NO ENCONTRADO</div>;
+};
+
+export default NotFound;

+ 143 - 0
src/components/Select.js

@@ -0,0 +1,143 @@
+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 Select = ({ 
+  modelsParams, 
+  labelProp, 
+  valueProp, 
+  render, 
+  append, 
+  notIn, 
+  deleteSelected,
+  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 [
+    modelsData,
+    modelsDataLoading,
+    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={modelsDataLoading}
+      options={ modelsData.length > 0 && agregarFaltantes([...modelsData], [...append], valueProp).map(i => ({
+        ...i,
+        label: render(i[labelProp], i),
+        value: i[valueProp],
+      }))}
+      onDeselect={(labeledValue) => {
+        onDeselect(labeledValue);
+      }}
+    />
+  )
+}
+
+
+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

+ 98 - 0
src/components/Tabla.js

@@ -0,0 +1,98 @@
+import React from 'react';
+import { Table } from 'antd';
+import { useModels, useSortColumns, usePagination } from '../hooks';
+import PropTypes from "prop-types";
+
+const Tabla = ({
+  nameURL,
+  expand = '',
+  extraParams = {},
+  columns,
+  order,
+  innerRef,
+  scrollX = '80vw',
+  summary = null,
+  paginacion = null,
+  ...props
+}) => {
+
+  const [columnsData, setColumnsData] = React.useState([]);
+  const [request, setRequest] = React.useState({});
+
+  const {
+    limit,
+    page,
+    configPagination,
+    setTotal,
+    setLimit,
+  } = usePagination();
+
+  const { sortValue, columnsContent } = useSortColumns({ columnsData, order: order || 'id-desc' });
+
+  const requestParams = React.useMemo(() => {
+    return ({
+      name: nameURL,
+      ordenar: sortValue,
+      expand: expand || '',
+      extraParams: extraParams,
+      limite: paginacion === false ? -1 : limit,
+      pagina: page
+    })
+  }, [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={paginacion === false ? paginacion : configPagination}
+      style={{ whiteSpace: 'pre' }}
+      scroll={{ x: scrollX }}
+      size="small"
+      summary={(pageData) => summary && summary(pageData)}
+    />
+  )
+}
+
+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

+ 20 - 0
src/components/ViewLoading.js

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

+ 91 - 0
src/components/asyncAutocomplete/AsyncAutocompleteAudioSearch.js

@@ -0,0 +1,91 @@
+import React from "react";
+import { AutoComplete, Spin } from "antd";
+import { useModels } from "../../hooks";
+
+const AsyncAutocompleteAudioSearch = ({
+  name,
+  expand = "",
+  labelProp,
+  index,
+  defaultVal = null,
+  onSelect,
+  paramsValues = "id",
+  extraParams,
+  searchParam = "",
+  disabled = false,
+  notFoundContent = "No hay opciones disponibles.",
+  size,
+  ...rest
+}) => {
+  const [open, setOpen] = React.useState(false)
+  const [options, setOptions] = React.useState([])
+  
+  const [
+    models,
+    modelsLoading,
+    modelsError,
+    modelsPage,
+    modelsRefresh,
+  ] = useModels({
+    name,
+    extraParams,
+    expand,
+  });
+
+  React.useEffect(() => {
+    if(models && !modelsLoading){
+      let arr = []
+
+      models.forEach(m => {
+        arr = [ 
+          ...arr, 
+          {
+            value: `{${m.id}} - [${m.clave}] - ${m.nombre}`,
+            obj: m         
+          }
+        ]
+      })
+      setOptions(arr)
+    }
+  }, [models, modelsLoading])
+
+  const refreshSearch = React.useCallback(
+    async (params) => {
+      await modelsRefresh(true, params);
+    },
+    [modelsRefresh]
+  );
+
+  const onKeyPressCallback = async (e) => {
+    let params = {
+      [searchParam !== "" ? searchParam : labelProp]: e.target.value,
+    };
+    await refreshSearch(params);
+  };
+
+  return (
+    <AutoComplete
+      allowClear={true}
+      placeholder="Buscar por clave, nombre..."
+      disabled={disabled}
+      id="autocomplete-test"
+      size={size}
+      style={{width: "100%"}}
+      open={open}
+      onDropdownVisibleChange={() => setOpen(op => !op)}
+      notFoundContent={
+        modelsLoading ? <Spin /> : notFoundContent 
+      }
+      options={options}
+      onKeyUp={onKeyPressCallback}
+      onSelect={(val, opt) => {
+        isNaN(index) 
+          ? onSelect(val, opt) 
+          : onSelect(val, opt, index);
+      }}
+      defaultValue={defaultVal}
+    />
+  )
+}
+
+export default AsyncAutocompleteAudioSearch

+ 92 - 0
src/components/asyncAutocomplete/AsyncAutocompleteGuidelineGroup.js

@@ -0,0 +1,92 @@
+import React from "react";
+import { AutoComplete, Spin } from "antd";
+import { useModels } from "../../hooks";
+
+const AsyncAutocompleteGuidelineGroup = ({
+  name,
+  expand = "",
+  labelProp,
+  index,
+  defaultVal = null,
+  onSelect,
+  paramsValues = "id",
+  extraParams,
+  searchParam = "",
+  disabled = false,
+  notFoundContent = "No hay opciones disponibles.",
+  size,
+  placeholder="Buscar por nombre...",
+  ...rest
+}) => {
+  const [open, setOpen] = React.useState(false)
+  const [options, setOptions] = React.useState([])
+  
+  const [
+    models,
+    modelsLoading,
+    modelsError,
+    modelsPage,
+    modelsRefresh,
+  ] = useModels({
+    name,
+    extraParams,
+    expand,
+  });
+
+  React.useEffect(() => {
+    if(models && !modelsLoading){
+      let arr = []
+
+      models.forEach(m => {
+        arr = [ 
+          ...arr, 
+          {
+            value: `[${m?.id || "Sin definir"}] - ${m?.nombre || "Sin definir"}`,
+            obj: m       
+          }
+        ]
+      })
+      setOptions(arr)
+    }
+  }, [models, modelsLoading])
+
+  const refreshSearch = React.useCallback(
+    async (params) => {
+      await modelsRefresh(true, params);
+    },
+    [modelsRefresh]
+  );
+
+  const onKeyPressCallback = async (e) => {
+    let params = {
+      [searchParam !== "" ? searchParam : labelProp]: e.target.value,
+    };
+    await refreshSearch(params);
+  };
+
+  return (
+    <AutoComplete
+      allowClear={true}
+      placeholder={placeholder}
+      disabled={disabled}
+      id="autocomplete-test"
+      size={size}
+      style={{width: "100%"}}
+      open={open}
+      onDropdownVisibleChange={() => setOpen(op => !op)}
+      notFoundContent={
+        modelsLoading ? <Spin /> : notFoundContent 
+      }
+      options={options}
+      onKeyUp={onKeyPressCallback}
+      onSelect={(val, opt) => {
+        isNaN(index) 
+          ? onSelect(val, opt) 
+          : onSelect(val, opt, index);
+      }}
+      defaultValue={defaultVal}
+    />
+  )
+}
+
+export default AsyncAutocompleteGuidelineGroup

+ 15 - 0
src/components/index.js

@@ -0,0 +1,15 @@
+import NotFound from "./NotFound";
+import AppLoading from "./AppLoading";
+import ViewLoading from "./ViewLoading";
+import ActionsButton from "./ActionsButton";
+import CircularProgress from "./CircularProgress";
+import Select from "./Select";
+
+export {
+  NotFound,
+  AppLoading,
+  ViewLoading,
+  ActionsButton,
+  CircularProgress,
+  Select
+};

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

@@ -0,0 +1,11 @@
+import React from "react";
+
+const AuthLayout = ({ children }) => {
+  return (
+    <div className="full-layout">
+      {children}
+    </div>
+  );
+};
+
+export default AuthLayout;

+ 230 - 0
src/components/layouts/DashboardLayout.js

@@ -0,0 +1,230 @@
+import React, { useState, useEffect } from "react";
+import { Layout, Menu, Breadcrumb, Button } from "antd";
+import {
+  HomeOutlined,
+  LogoutOutlined,
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+} from "@ant-design/icons";
+import { useHistory, useLocation, Link } from "react-router-dom";
+import { useAuth } from "../../hooks";
+import { dashboardRoutes } from "../../routers";
+import "../../styles/DashboardLayout.less";
+import {Helmet} from "react-helmet-async";
+const { REACT_APP_VERSION } = process.env;
+
+const { Header, Content, Footer, Sider } = Layout;
+
+const rootSubmenuKeys = [""];
+
+const DashboardLayout = ({ children: PanelContent }) => {
+  const history = useHistory();
+  const [collapsed, setCollapsed] = useState(true);
+  const [openKeys, setOpenKeys] = useState([""]);
+  const [selectedKey, setSelectedKey] = useState("");
+  const [breadcrumbItems, setBreadcrumbItems] = useState([]);
+  const [titulo, setTitulo] = useState("");
+  const location = useLocation();
+  const { userLoading, signOut, session, userDoc } = useAuth();
+
+  const esAdmin = userDoc?.rol === "admin";
+  let finalDashboardRoutes = dashboardRoutes;
+  if( !esAdmin ) {
+    finalDashboardRoutes = finalDashboardRoutes.filter(f => !f?.path.includes("administracion"));
+  }
+
+  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]);
+
+  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) => {
+    if (route.sidebar === "single") {
+      return {
+        key: route.path,
+        icon: route.icon,
+        label: route.name,
+        onClick: () => {
+          setSelectedKey(route.path);
+          history.push(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;
+  };
+
+  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),
+        ];
+
+        setTitulo(`${rutas[rutas?.length - 1]?.name} - LAUD`);
+      }
+      return rutas;
+    };
+
+    let rutas = [
+      { name: "Inicio", to: "/", icon: <HomeOutlined /> },
+      ...rutasBreadCrumbs(dashboardRoutes, location?.pathname?.split("/")),
+    ];
+
+    setBreadcrumbItems(rutas);
+  }, [location?.pathname]);
+
+  if (!session && userLoading) return null;
+
+  return (
+    <Layout style={{ minHeight: "100vh" }}>
+      <Helmet><title>{titulo}</title></Helmet>
+      <Sider
+        trigger={null}
+        id="auth-sider"
+        breakpoint="md"
+        collapsedWidth={60}
+        collapsible
+        onBreakpoint={()=>setCollapsed(!collapsed)}
+        collapsed={collapsed}
+        theme="light"
+        width={236}
+      >
+        <div
+          className={collapsed ? "logo-collapsed" : "logo"}
+          style={{
+            height: collapsed ? 50 : 103,
+            margin: 0,
+            backgroundImage: collapsed
+              ? `url("/assets/LAUD-Avatar.png")`
+              : `url("/assets/LAUD-Logotipo.png")`,
+          }}
+        />
+
+        <Menu
+          theme="dark"
+          mode="inline"
+          openKeys={openKeys}
+          onOpenChange={onOpenChange}
+          className="sider-menu"
+          selectedKeys={selectedKey}
+          inlineIndent={18}
+          items={[
+            ...finalDashboardRoutes.map(sidebarMapper),
+            {
+              key: "logout",
+              icon: <LogoutOutlined />,
+              label: "Cerrar sesión",
+              onClick: () => signOut(),
+            },
+          ]}
+        ></Menu>
+      </Sider>
+
+      <Layout className="site-layout">
+        <Header style={{ padding: 0 }}>
+          <div className="ed-topbar">
+            <div className="breadcrumb">
+              <Button
+                type="link"
+                icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
+                className="trigger"
+                onClick={() => setCollapsed(!collapsed)}
+              />
+
+              <Breadcrumb>
+                {breadcrumbItems?.map((item, index) => (
+                  <Breadcrumb.Item key={index}>
+                    <Link to={item?.to}>
+                      {item?.icon}
+                      <span> {item?.name} </span>
+                    </Link>
+                  </Breadcrumb.Item>
+                ))}
+              </Breadcrumb>
+            </div>
+
+            <div className="user">
+              {userDoc?.email}
+            </div>
+          </div>
+        </Header>
+        <Content
+          className="site-layout-background"
+          style={{
+            padding: 0,
+            minHeight: 280,
+          }}
+        >
+          {PanelContent}
+        </Content>
+        <Footer style={{ textAlign: "center" }}>
+          Derechos reservados {new Date().getFullYear()} &#xa9; - {REACT_APP_VERSION}
+        </Footer>
+      </Layout>
+    </Layout>
+  );
+};
+
+export default DashboardLayout;

+ 50 - 0
src/components/layouts/DefaultLayout.js

@@ -0,0 +1,50 @@
+import { Content } from "antd/lib/layout/layout";
+import { Row, Button } from "antd";
+
+const DefaultLayout = ({ children, multipleButtonData }) => {
+  const styles = {
+    padding: {
+      marginLeft: 15,
+      marginRight: 15,
+      marginTop: 10,
+    },
+    content: {
+      paddingTop: 0,
+      paddingBottom: 0,
+      paddingLeft: 0,
+      paddingRight: 0,
+      minHeight: 280,
+      borderRadius: 5,
+    },
+  };
+
+  return (
+    <>
+      <div style={styles.padding}>
+        <Row style={{ justifyContent: "space-between" }}>
+          {multipleButtonData ? (
+            <div className="ed-lista-botones">
+              {multipleButtonData.map((item, index) => (
+                <Button
+                  key={index}
+                  tabIndex={index}
+                  style={{ backgroundColor: item.color, color: item.texColor }}
+                  onClick={item.to ? item.to : undefined}
+                  {...item.props}
+                  block
+                >
+                  {item.icon} {item.text}
+                </Button>
+              ))}
+            </div>
+          ) : null}
+        </Row>
+        <Content className="site-layout-background" style={styles.content}>
+          {children}
+        </Content>
+      </div>
+    </>
+  );
+};
+
+export default DefaultLayout;

+ 71 - 0
src/components/layouts/SimpleTableLayout.js

@@ -0,0 +1,71 @@
+import DefaultLayout from "./DefaultLayout";
+import { Col, Row, Input, Empty, Button, ConfigProvider } from "antd";
+
+const { Search } = Input;
+
+const SimpleTableLayout = ({
+  breadcrumbItems,
+  multipleButtonData,
+  searchPlaceholder = "Búsqueda",
+  searchLoading,
+  onSearchClicked,
+  children,
+  emptyText = "Aún no hay registros.",
+  withSearchButton = true,
+  searchProps,
+}) => {
+  return (
+    <DefaultLayout breadcrumbItems={breadcrumbItems}>
+      <Row style={{ justifyContent: "space-between" }}>
+        {withSearchButton && (
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 10 }}
+            lg={{ span: 10 }}
+            xxl={{ span: 10 }}
+          >
+            <Search
+              placeholder={searchPlaceholder}
+              enterButton="Buscar"
+              style={{ width: "100%", marginBottom: 10 }}
+              loading={searchLoading}
+              onSearch={onSearchClicked}
+              {...searchProps}
+            />
+          </Col>
+        )}
+
+        {multipleButtonData ? (
+          <div className="ed-lista-botones">
+            {multipleButtonData.map((item, index) => (
+              <Button
+                key={index}
+                tabIndex={index}
+                style={{ backgroundColor: item.color, color: item.texColor }}
+                onClick={item.to ? item.to : undefined}
+                {...item.props}
+                block
+              >
+                {item.icon} {item.text}
+              </Button>
+            ))}
+          </div>
+        ) : null}
+      </Row>
+
+      <ConfigProvider
+        renderEmpty={() => (
+          <Empty
+            style={{ height: 300, paddingTop: "5%" }}
+            description={emptyText}
+          />
+        )}
+      >
+        {children}
+      </ConfigProvider>
+    </DefaultLayout>
+  );
+};
+
+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);

+ 22 - 0
src/constants/index.js

@@ -0,0 +1,22 @@
+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"
+
+export {
+  dateFormat,
+  dateUSFormat,
+  localTimeZone,
+  NON_DIGIT,
+  timeFormat12,
+  timeFormat24,
+  TIPO_PROJECTO_FIREBASE,
+  TIPO_PROJECTO_JWT
+}

+ 32 - 0
src/constants/requests.js

@@ -0,0 +1,32 @@
+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 };

+ 11 - 0
src/hooks/index.js

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

+ 66 - 0
src/hooks/useAlert.js

@@ -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;
+}

+ 60 - 0
src/hooks/useApp.js

@@ -0,0 +1,60 @@
+import React from "react";
+import { getIdToken, onIdTokenChanged } from "firebase/auth";
+import { auth } from "../services/firebase";
+import { TIPO_PROJECTO_JWT } from "../constants"
+const localStorageKey = "usr_jwt";
+const { REACT_APP_PROJECT: tipoProyecto  } = process.env;
+
+const AppContext = React.createContext();
+
+export function AppProvider(props) {
+  const [token, setToken] = React.useState(null);
+
+  React.useEffect(() => {
+    if( tipoProyecto === TIPO_PROJECTO_JWT ) {
+      const jwt = localStorage.getItem(localStorageKey)
+      setToken(jwt)
+    }
+  }, []);
+
+  React.useEffect(() => {
+    if( tipoProyecto === TIPO_PROJECTO_JWT ) {
+      if(token && token !== "") {
+        localStorage.setItem(localStorageKey, token)
+      }
+    }
+  }, [token]);
+
+  React.useEffect(() => {
+    if( tipoProyecto === TIPO_PROJECTO_JWT ) {
+      return
+    }
+
+    let unsub = onIdTokenChanged(auth, (user) => {
+      if(user) {
+        getIdToken(user).then(token => {
+          if(token) {
+            setToken(token);
+          }
+        });  
+      }
+    });
+    
+    return () => unsub()
+  }, []);
+  
+  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;
+}

+ 143 - 0
src/hooks/useAuth.js

@@ -0,0 +1,143 @@
+import React from "react";
+import { useAuthState } from "react-firebase-hooks/auth";
+import { emptyRequest, getRequest, postRequest } from "../constants/requests";
+import { useHttp } from "./useHttp";
+import { useApp } from "./useApp";
+import { signInWithEmailAndPassword, signOut as signOutFirebase } from 'firebase/auth';
+import { auth, firestore } from "../services/firebase";
+import { message, Modal } from "antd";
+import { ExclamationCircleOutlined } from "@ant-design/icons";
+import { collection, getDocs, where, query } from "firebase/firestore";
+import { TIPO_PROJECTO_FIREBASE, TIPO_PROJECTO_JWT } from "../constants";
+const { REACT_APP_PROJECT: tipoProyecto } = process.env;
+
+const AuthContext = React.createContext();
+const empty = emptyRequest();
+
+export function AuthProvider(props) {
+  const { token, setToken } = useApp();
+  const [sessionFb, sessionFbLoading, sessionFbError] = useAuthState(auth);
+  const [userDoc, setUserDoc] = React.useState(null);
+  const [sessionRequest, setSessionRequest] = React.useState(empty);
+  const [agendaRequest, setAgendaRequest] = React.useState(empty);
+  const [sessionJWT, sessionJWTLoading] = useHttp(sessionRequest);
+  const [agendaResponse, agendaResponseLoading, agendaError] = useHttp(agendaRequest);
+
+  const signIn = React.useCallback(async (email, password) => {
+    try {
+      if(tipoProyecto === TIPO_PROJECTO_FIREBASE){
+        await signInWithEmailAndPassword(auth, email, password);
+      } else {
+        if(email !== "" && password !== "") {
+          const req = postRequest("v1/iniciar-sesion", { correo: email, clave: password });
+          setSessionRequest({ ...req });
+        } 
+      }
+    } catch (e) {
+      if(tipoProyecto === TIPO_PROJECTO_JWT){
+        console.log(e)
+      } else  {
+        if( e.code === 'auth/wrong-password' || e.code === 'auth/user-not-found') {
+          message.warning({
+            content: 'Atención: Usuario/Contraseña son incorrectos.',
+          });
+        } else if ( e.code === 'auth/invalid-email' ) {
+          message.warning({
+            content: 'El correo electrónico no es válido.',
+          });
+        };
+      }
+    };
+  }, []);
+
+  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 () => {
+          if(tipoProyecto === TIPO_PROJECTO_FIREBASE) {
+            await signOutFirebase( auth );
+          } else {
+            setToken(null)
+            setSessionRequest(empty)
+          }
+          localStorage.clear();
+          setAgendaRequest(empty);
+        }
+      });
+    } catch (e) {
+      console.error(e);
+    }
+  }, [setToken]);
+
+  React.useEffect(() => {
+    if(tipoProyecto === TIPO_PROJECTO_JWT) {
+      if(sessionJWT && !sessionJWTLoading) {
+        if(sessionJWT?.detalle) {
+          setToken(sessionJWT?.detalle?.token)
+        }
+      }
+    }
+  }, [sessionJWT, sessionJWTLoading, setToken])
+
+  React.useEffect(() => {
+    if (token && tipoProyecto === TIPO_PROJECTO_JWT) {
+      const agendaReq = getRequest("v1/perfil");
+      setAgendaRequest(() => agendaReq);
+    } else {
+      setAgendaRequest(empty);
+    }
+  }, [token]);
+
+  React.useEffect(() => {
+    if(sessionFb) {
+      setTimeout( async () => {
+        const q = query(collection(firestore, "usuarios"), where('uid', '==', sessionFb?.uid));
+        const querySnapshot = await getDocs(q)
+        querySnapshot.forEach((doc) => {
+          setUserDoc( doc.data() )
+        })
+      }, 0);
+    }
+  }, [sessionFb]);
+
+  const memData = React.useMemo(() => {
+    return {
+      userDoc: tipoProyecto === TIPO_PROJECTO_FIREBASE ? userDoc : null,
+      session: tipoProyecto === TIPO_PROJECTO_FIREBASE ? sessionFb : sessionJWT,
+      sessionLoading: tipoProyecto === TIPO_PROJECTO_FIREBASE ? sessionFbLoading : sessionJWTLoading,
+      user: tipoProyecto === TIPO_PROJECTO_FIREBASE ?  agendaResponse : agendaResponse && agendaResponse.resultado[0],
+      userLoading: agendaResponseLoading,
+      userError: agendaError || tipoProyecto === TIPO_PROJECTO_FIREBASE ? sessionFbError : null,
+      signIn,
+      signOut,
+    };
+  }, [
+    userDoc,
+    agendaError, 
+    agendaResponse, 
+    agendaResponseLoading, 
+    sessionFb, 
+    sessionFbError, 
+    sessionFbLoading, 
+    sessionJWT, 
+    sessionJWTLoading, 
+    signIn, 
+    signOut
+  ]);
+
+  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;
+}

+ 84 - 0
src/hooks/useDialog.js

@@ -0,0 +1,84 @@
+import React from "react";
+
+const DialogContext = React.createContext();
+
+export function DialogProvider(props) {
+  const [open, setOpen] = React.useState(false);
+  const [dialogTitle] = React.useState("Advertencia!");
+  const [dialogMessage] = React.useState(
+    "Es necesario confirmar el correo para continuar:"
+  );
+  // 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 showDialog = React.useCallback(({ onCancel = null, onOk = null }) => {
+    setOpen(true);
+    if (onCancel) onCancel();
+    if (onOk) onOk();
+  }, []);
+
+  const closeDialog = React.useCallback(() => {
+    setOpen(false);
+    // 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,
+      dialogTitle,
+      dialogMessage,
+      showDialog,
+      closeDialog,
+      // position,
+      // severity,
+      // message,
+      // showDialog,
+      // closeAlert,
+    };
+  }, [
+    open,
+    dialogTitle,
+    dialogMessage,
+    showDialog,
+    closeDialog,
+    // , position, severity, message, showDialog
+  ]);
+
+  return <DialogContext.Provider value={memData} {...props} />;
+}
+
+export function useConfirmDialog() {
+  const context = React.useContext(DialogContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: confirm dialog context not defined.";
+  }
+  return context;
+}

+ 163 - 0
src/hooks/useHttp.js

@@ -0,0 +1,163 @@
+import React from "react";
+import { useAlert } from "./useAlert";
+import { useHistory } from "react-router";
+import httpCodes from "../constants/httpStatusCodes";
+import { onAuthStateChanged, getIdToken } from "@firebase/auth";
+import { auth } from "../services/firebase";
+import { TIPO_PROJECTO_FIREBASE } from "../constants";
+
+const { 
+  REACT_APP_API_URL: baseUrl, 
+  REACT_APP_PROJECT: tipoProyecto 
+} = process.env;
+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 { showAlert } = useAlert();
+  const [response, setResponse] = React.useState(null);
+  const [error, setError] = React.useState(null);
+  const [loading, setLoading] = React.useState(true);
+  const history = useHistory();
+
+  const getToken = () => {
+    return new Promise(async (resolve) => {
+      if( tipoProyecto === TIPO_PROJECTO_FIREBASE ) {
+        onAuthStateChanged(auth, (user) => {
+          if(user) {
+            getIdToken(user).then(token => {
+              if(token) {
+                resolve(token);
+              }
+            });
+          } else {
+            resolve('');
+          }
+        });
+      } else {
+        localStorage.getItem(localStorageKey);
+      }
+    });
+  };
+
+  const refresh = React.useCallback(
+    async (showAlert, inlineParams = {}) => {
+      try {
+        if (!url || !params) {
+          setResponse(null);
+          setError(null);
+          setLoading(true);
+          return;
+        }
+        if (inlineParams.isCargando === false) {
+          setLoading(() => false);
+        } else {
+          setLoading(() => true);
+        }
+        const jwt =  await getToken();
+        let fetchReq = {
+          method: req,
+          headers: makeHeaders(jwt),
+        };
+        if (body) {
+          fetchReq = { ...fetchReq, body: JSON.stringify(body) };
+        }
+        const paramsFinal = { ...params, ...inlineParams };
+        const str = `${baseUrl}${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:
+          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.",
+              });
+            history.push("/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.",
+              });
+        }
+      } catch (error) {
+        alert &&
+          showAlert({
+            severity: "error",
+            message: "No se pudo establecer conexión con el servidor.",
+          });
+        console.error(error);
+      } finally {
+        setLoading(false);
+      }
+    },
+    [body, params, req, url, alert, history]
+  );
+
+  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.js

@@ -0,0 +1,90 @@
+import React from "react";
+import { useHistory } 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 history = useHistory();
+
+  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 } = history.location;
+      const redirectTo = pathname.split("/").filter((e) => e !== "");
+      history.push("/" + redirectTo[0]);
+    }
+    return () => {
+      mounted = false;
+    };
+  }, [postResult, redirectOnPost, postResultError, history]);
+
+  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,
+  ]);
+}

+ 87 - 0
src/hooks/useModels.js

@@ -0,0 +1,87 @@
+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 [
+      resultado,
+      modelsLoadingFinal,
+      modelsError,
+      modelsPage,
+      refreshModels,
+      deleteModel,
+    ];
+  }, [
+    models,
+    modelsLoading,
+    modelsError,
+    modelsPage,
+    refreshModels,
+    deleteResultLoading,
+    deleteModel,
+  ]);
+}

+ 28 - 0
src/hooks/useNotifications.js

@@ -0,0 +1,28 @@
+import React from "react";
+
+const NotificationsContext = React.createContext();
+
+// const defaultNotifications = () => [1, 2];
+
+export function NotificationsProvider(props) {
+  const [notifications, setNotifications] = React.useState([]);
+
+  // COMPONENTE SE MONTÓ
+  React.useEffect(() => {}, []);
+
+  const memData = React.useMemo(() => {
+    // AQUI SE PONE TODO LO Q EL HOOK EXPORTA
+    return { notifications, setNotifications };
+  }, [notifications]);
+
+  return <NotificationsContext.Provider value={memData} {...props} />;
+}
+
+export function useNotifications() {
+  const context = React.useContext(NotificationsContext);
+  if (!context) {
+    // eslint-disable-next-line no-throw-literal
+    throw "error: notifications context not defined.";
+  }
+  return context;
+}

+ 40 - 0
src/hooks/usePagination.js

@@ -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, 50, 100].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.js

@@ -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]);
+}

+ 57 - 0
src/hooks/useSortColumns.js

@@ -0,0 +1,57 @@
+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) => {
+      return {
+        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]);
+}

+ 16 - 0
src/index.js

@@ -0,0 +1,16 @@
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);
+
+// If you want to start measuring performance in your app, pass a function
+// to log results (for example: reportWebVitals(console.log))
+// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
+reportWebVitals();

BIN
src/menu-bg.jpeg


+ 13 - 0
src/reportWebVitals.js

@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;

+ 22 - 0
src/routers/AppRouting.js

@@ -0,0 +1,22 @@
+import React from "react";
+import { useApp, useAuth } from "../hooks";
+import PrivateRouter from "./PrivateRouter";
+import PublicRouter from "./PublicRouter";
+import { TIPO_PROJECTO_FIREBASE } from "../constants"
+const { REACT_APP_PROJECT: tipoProyecto } = process.env;
+const AppRouting = () => {
+  const { session, sessionLoading } = useAuth();
+  const { token } = useApp();
+  
+  const user = tipoProyecto === TIPO_PROJECTO_FIREBASE ? session : token;
+
+  if (sessionLoading) return null;
+
+  return (
+    <>
+      {user ? <PrivateRouter /> : <PublicRouter />}
+    </>
+  );
+};
+
+export default AppRouting;

+ 45 - 0
src/routers/PrivateRouter.js

@@ -0,0 +1,45 @@
+import React from "react";
+import { BrowserRouter as Router, Route, Switch } 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} />}
+    />
+  );
+
+const PrivateRouter = () => {
+  const { userDoc, user, userLoading } = useAuth();
+  const esAdmin = userDoc?.rol === "admin";
+  let finalDashboardRoutes = dashboardRoutes;
+
+  if( !esAdmin ) {
+    finalDashboardRoutes = finalDashboardRoutes.filter( f => !f?.path.includes("administracion") );
+  }
+  
+  if (!user && userLoading) {
+    return <AppLoading />;
+  }
+
+  return (
+    <Router>
+      <DashboardLayout>
+        <Switch>{finalDashboardRoutes.map(routeMapper)}</Switch>
+      </DashboardLayout>
+    </Router>
+  );
+};
+
+export default PrivateRouter;

+ 30 - 0
src/routers/PublicRouter.js

@@ -0,0 +1,30 @@
+import React from "react";
+import {
+  BrowserRouter as Router,
+  Switch,
+  Route,
+  Redirect,
+} from "react-router-dom";
+import { AuthLayout } from "../components/layouts";
+import { Ingresar } from "../views/auth/";
+import { Verificar } from "../views/verificar";
+import { EventoResumenDependencia, EventoResumenYager, EventoYager } from "../views/evento-yager";
+
+const PublicRouter = () => {
+  return (
+    <Router>
+      <AuthLayout>
+        <Switch>
+          <Route exact path="/" component={Ingresar} />
+          <Route exact path="/verificar" component={Verificar} />
+          <Route exact path="/evento-laud" component={EventoYager} />
+          <Route exact path="/resumen-evento" component={EventoResumenYager} />
+          <Route exact path="/resumen-lider-dependencia" component={EventoResumenDependencia} />
+          <Route path="*" component={() => <Redirect to="/" />} /> 
+        </Switch>
+      </AuthLayout>
+    </Router>
+  );
+};
+
+export default PublicRouter;

+ 4 - 0
src/routers/index.js

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

+ 302 - 0
src/routers/routes.js

@@ -0,0 +1,302 @@
+import React from "react";
+import {
+  HomeOutlined,
+  UserOutlined,
+  BlockOutlined,
+  SettingOutlined,
+  CarryOutOutlined,
+  BarChartOutlined,
+  MergeCellsOutlined,
+} from "@ant-design/icons";
+
+import { Inicio } from "../views/inicio";
+import { Perfil } from "../views/perfil";
+import { Timeline } from "../views/administracion/timeline";
+import { EventoResumenDependencia, EventoResumenYager, EventoYager } from "../views/evento-yager";
+import { NoEncontrado, NoAutorizado } from "../views/error";
+
+import {
+  EventosListado, 
+  EventosDetalle,
+  EventoDetalleGrupo,
+  ResultadoGrupoEvento, 
+  ResultadoEventoAccion, 
+} from "../views/administracion/eventos";
+import { 
+  GruposListado, 
+  GruposDetalle 
+} from "../views/administracion/grupos";
+import {
+  UsuariosRedes,
+  UsuariosDetalle,
+  UsuariosListado,
+} from "../views/administracion/usuarios";
+import {
+  Reportes,
+  Felicitacion,
+  SinParticipar,
+  Individual,
+  Comparativa,
+  Evento,
+  Dependencia
+} from "../views/administracion/reportes";
+import {
+  Notificaciones
+} from "../views/administracion/notificaciones";
+import { 
+  DependenciaDetalle, 
+  DependenciasListado 
+} from "../views/administracion/dependencias";
+
+const singOutRoute = () => {
+  return "Cargando...";
+};
+
+const sharedRoutes = [
+  {
+    path: "/no-autorizado",
+    component: NoAutorizado,
+  },
+  {
+    path: "/salir",
+    component: singOutRoute,
+  },
+  {
+    path: "*",
+    component: NoEncontrado,
+  },
+];
+
+const dashboardRoutes = [
+  {
+    layout: "dashboard",
+    path: "/",
+    name: "Inicio",
+    icon: <HomeOutlined />,
+    sidebar: "single",
+    component: Inicio,
+  },
+  {
+    layout: "dashboard",
+    path: "/administracion",
+    name: "Administración",
+    icon: <SettingOutlined />,
+    sidebar: "collapse",
+    routes: [
+      {
+        layout: "dashboard",
+        path: "/eventos",
+        name: "Eventos",
+        icon: <CarryOutOutlined />,
+        sidebar: "single",
+        routes: [
+          {
+            path: "/",
+            name: "Eventos",
+            component: EventosListado,
+          },
+          {
+            path: "/nuevo",
+            name: "Nuevo",
+            component: EventosDetalle,
+          },
+          {
+            path: "/detalle",
+            name: "Editar",
+            component: EventosDetalle,
+          },
+          {
+            path: "/detalleGrupo",
+            name: "Eventos por grupo",
+            component: EventoDetalleGrupo,
+          },
+          {
+            path: "/resultado-acciones",
+            name: "Resultado por acciones",
+            component: ResultadoEventoAccion,
+          },
+          {
+            path: "/resultado-grupo-evento",
+            name: "Resultado por grupo",
+            component: ResultadoGrupoEvento,
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/usuarios",
+        name: "Usuarios",
+        icon: <UserOutlined />,
+        sidebar: "single",
+        routes: [
+          {
+            path: "/",
+            name: "Usuarios",
+            component: UsuariosListado,
+          },
+          {
+            path: "/nuevo",
+            name: "Nuevo",
+            component: UsuariosDetalle,
+          },
+          {
+            path: "/detalle",
+            name: "Editar",
+            component: UsuariosDetalle,
+          },
+          {
+            path: "/redes",
+            name: "Usuarios: Redes sociales",
+            component: UsuariosRedes,
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/grupos",
+        name: "Grupos",
+        icon: <BlockOutlined />,
+        sidebar: "single",
+        routes: [
+          {
+            path: "/",
+            name: "Grupos",
+            component: GruposListado,
+          },
+          {
+            path: "/nuevo",
+            name: "Nuevo",
+            component: GruposDetalle,
+          },
+          {
+            path: "/detalle",
+            name: "Editar",
+            component: GruposDetalle,
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/dependencias",
+        name: "Dependencias",
+        icon: <MergeCellsOutlined />,
+        sidebar: "single",
+        routes: [
+          {
+            path: "/",
+            name: "Dependencias",
+            component: DependenciasListado,
+          },
+          {
+            path: "/nueva",
+            name: "Nueva",
+            component: DependenciaDetalle,
+          },
+          {
+            path: "/detalle",
+            name: "Editar",
+            component: DependenciaDetalle,
+          },
+        ],
+      },
+      {
+        layout: "dashboard",
+        path: "/reportes",
+        name: "Reportes",
+        icon: <BarChartOutlined />,
+        sidebar: "single",
+        routes: [
+          {
+            path: "/",
+            name: "Reportes",
+            component: Reportes,
+          },
+          {
+            path: "/sin-participar",
+            name: "Sin participar",
+            component: SinParticipar,
+          },
+          {
+            path: "/felicitacion",
+            name: "Felicitación",
+            component: Felicitacion,
+          },
+          {
+            path: "/individual",
+            name: "Participación Individual",
+            component: Individual,
+          },
+          {
+            path: "/dependencia",
+            name: "Participación por Dependencia",
+            component: Dependencia,
+          },
+          {
+            path: "/comparativa",
+            name: "Participación Comparativa",
+            component: Comparativa,
+          },
+          {
+            path: "/evento",
+            name: "Participación por Evento",
+            component: Evento,
+          }
+        ]
+      },
+      {
+        layout: "dashboard",
+        path: "/timeline",
+        name: "Timeline procesos",
+        icon: <BlockOutlined />,
+        sidebar: false,
+        component: Timeline,
+      },
+      {
+        layout: "dashboard",
+        path: "/notificaciones",
+        name: "Notificaciones",
+        icon: <BlockOutlined />,
+        sidebar: false,
+        component: Notificaciones,
+      },
+    ],
+  },
+  {
+    layout: "dashboard",
+    path: "/perfil",
+    name: "Perfil",
+    icon: <UserOutlined />,
+    sidebar: "single",
+    component: Perfil,
+  },
+  {
+    layout: "dashboard",
+    path: "/evento-laud",
+    name: "Evento Laud",
+    icon: <HomeOutlined />,
+    sidebar: false,
+    component: EventoYager,
+  },
+  {
+    layout: "dashboard",
+    path: "/resumen-evento",
+    name: "Resumen Evento",
+    icon: <HomeOutlined />,
+    sidebar: false,
+    component: EventoResumenYager,
+  },
+  {
+    layout: "dashboard",
+    path: "/resumen-lider-dependencia",
+    name: "Resumen Lider Dependencia",
+    icon: <HomeOutlined />,
+    sidebar: false,
+    component: EventoResumenDependencia,
+  },
+
+  ...sharedRoutes,
+];
+
+const publicRoutes = [...sharedRoutes];
+
+export { dashboardRoutes, publicRoutes };

+ 215 - 0
src/services/excelCondensado.js

@@ -0,0 +1,215 @@
+import * as ExcelJS from 'exceljs'
+import { saveAs } from 'file-saver'
+import imageToBase64 from 'image-to-base64/browser'
+
+const obtenerExtensionImagen = (path) => {
+  const ext = path.split('.').pop();
+  return ext.toLowerCase();
+}
+
+const excelCondensado = async (
+  columnas,
+  datos,
+  totalEventos,
+  eventos,
+  nombre = "archivo-excel",
+  titulo = "archivo-excel",
+  subtitulo = "",
+  path = null
+) => {
+
+  try {
+    const workbook = new ExcelJS.Workbook()
+    const worksheet = workbook.addWorksheet(titulo.replace(/[^a-zA-Z ]/g, ""), {
+      views: [
+        { state: "frozen", xSplit: 6 }, // Fijar columna
+        { state: "frozen", ySplit: 7 }, // Fijar renglon
+      ],
+    })
+
+    if( path !== null && typeof path === 'string' ) {
+      const img64 = await imageToBase64(path)
+      const idImagen = workbook.addImage({
+        base64: img64,
+        extension: obtenerExtensionImagen(path), 
+      })
+
+      worksheet.addImage( idImagen, {
+        tl: { col: 1.5, row: 0.5 },
+        br: { col: 2.5, row: 3.5 },
+        editAs: 'oneCell'
+      })
+    }
+
+    const cols = columnas
+    const data = datos
+    const header = cols.map(c => (c.title));
+
+    worksheet.columns = cols
+
+    const styleTitle = {
+      font: {
+        bold: true,
+        size: 18,
+      },
+      aligment: {
+        horizontal: 'center',
+        vertical: 'center',
+        wrapText: true
+      },
+    }
+
+    const styleSubTitle = {
+      font: {
+        bold: false,
+        size: 15,
+      },
+      aligment: {
+        horizontal: 'center',
+        vertical: 'center',
+        wrapText: true
+      },
+    }
+
+    const totalTitle = {
+      font: {
+        bold: true,
+        size: 14,
+      },
+    }
+    const totalSubTitle = {
+      font: {
+        bold: false,
+        size: 14,
+      },
+    }
+
+    // Cobinar celdas
+    worksheet.mergeCells('A1:F4')
+    worksheet.mergeCells('A5:F5')
+    worksheet.mergeCells('A6:F6')
+
+
+    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 > 6){
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "E7E7E7" },
+          };
+        }
+
+        if(column.key === "participacion") {
+          cell.numFmt = '0.00%';
+        }
+
+        if( 
+          Number.isInteger(cell?.value) && 
+          cell?.value === 1 && 
+          column?.key !== "participacion" &&
+          column?.key !== "participo"
+        ) {
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "8EF0C3" },
+          };
+        }
+
+        if(!cell?.value || Number.isInteger(cell?.value)) {
+          return true;
+        }      
+        const tipoRed = `${cell?.value}`.split("-")?.length === 4 ? `${cell?.value}`.split("-")[0] : "";
+
+        if (tipoRed.trim() === "INS") {
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "C6ADF3" },
+          };
+        } else if (tipoRed.trim() === "TW") {
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "ADF3F0" },
+          };
+        } else if (tipoRed.trim() === "FB") {
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "ADCDF3" },
+          };
+        }  else if ( `${cell?.value}`.trim() === "Participacion" ) {
+          cell.fill = {
+            type: "pattern",
+            pattern: "solid",
+            fgColor: { argb: "F7DB15" },
+          };
+        }
+
+        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('H2').value = "Eventos Facebook"; 
+    worksheet.getCell('I2').value = "Eventos Instagram"; 
+    worksheet.getCell('J2').value = "Eventos Twitter"; 
+    worksheet.getCell('L2').value = "Total de eventos"; 
+
+    worksheet.getCell('H3').value = totalEventos?.Facebook; 
+    worksheet.getCell('I3').value = totalEventos?.Instagram; 
+    worksheet.getCell('J3').value = totalEventos?.Twitter; 
+    worksheet.getCell('L3').value = eventos; 
+
+    worksheet.getCell('H2').style = totalTitle;
+    worksheet.getCell('I2').style = totalTitle;
+    worksheet.getCell('J2').style = totalTitle;
+    worksheet.getCell('L2').style = totalTitle;
+
+    worksheet.getCell('H3').style = totalSubTitle;
+    worksheet.getCell('I3').style = totalSubTitle;
+    worksheet.getCell('J3').style = totalSubTitle;
+    worksheet.getCell('L3').style = totalSubTitle;
+
+    worksheet.getCell('A5').value = titulo;
+    worksheet.getCell('A5').style = styleTitle
+    worksheet.getCell('A5').alignment = { vertical: 'middle', horizontal: 'left' }
+
+    worksheet.getCell('A6').value = subtitulo;
+    worksheet.getCell('A6').style = styleSubTitle
+    worksheet.getCell('A6').alignment = { vertical: 'middle', horizontal: 'left' }
+
+    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)
+  }
+
+}
+
+export default excelCondensado

+ 115 - 0
src/services/exportarExcel.js

@@ -0,0 +1,115 @@
+import * as ExcelJS from 'exceljs'
+import { saveAs } from 'file-saver'
+import imageToBase64 from 'image-to-base64/browser'
+
+const obtenerExtensionImagen = (path) => {
+  const ext = path.split('.').pop();
+  return ext.toLowerCase();
+}
+
+const exportarExcelJS = async (
+  columnas,
+  datos,
+  nombre = "archivo-excel",
+  titulo = "archivo-excel",
+  subtitulo = "",
+  path = null
+) => {
+
+
+  try {
+    const workbook = new ExcelJS.Workbook()
+    const worksheet = workbook.addWorksheet(titulo.replace(/[^a-zA-Z ]/g, ""))
+
+    if( path !== null && typeof path === 'string' ) {
+      const img64 = await imageToBase64(path)
+      const idImagen = workbook.addImage({
+        base64: img64,
+        extension: obtenerExtensionImagen(path), 
+      })
+
+      worksheet.addImage( idImagen, {
+        tl: { col: 1.5, row: 0.5 },
+        br: { col: 2.5, row: 3.5 },
+        editAs: 'oneCell'
+      })
+    }
+
+    const cols = columnas
+    const data = datos
+    const header = cols.map(c => (c.title));
+
+    worksheet.columns = cols
+
+    const styleTitle = {
+      font: {
+        bold: true,
+        size: 18,
+      },
+      aligment: {
+        horizontal: 'center',
+        vertical: 'center',
+        wrapText: true
+      },
+    }
+
+    const styleSubTitle = {
+      font: {
+        bold: false,
+        size: 15,
+      },
+      aligment: {
+        horizontal: 'center',
+        vertical: 'center',
+        wrapText: true
+      },
+    }
+
+    worksheet.mergeCells('A1:F4')
+    worksheet.mergeCells('A5:F5')
+    worksheet.mergeCells('A6:F6')
+
+    worksheet.addRow(header)
+    for( let i = 0; i < data?.length; i++ ) {
+      let row = data[i]
+      worksheet.addRow(row)
+    }
+
+    worksheet.columns.forEach(( column ) => {
+      var dataMax = 0;
+      column.eachCell({ includeEmpty: true }, (cell) => {
+        var 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('A5').value = titulo.replace(/[^a-zA-Z ]/g, "")
+    worksheet.getCell('A5').style = styleTitle
+    worksheet.getCell('A5').alignment = { vertical: 'middle', horizontal: 'left' }
+
+    worksheet.getCell('A6').value = subtitulo.replace(/[^a-zA-Z ]/g, "")
+    worksheet.getCell('A6').style = styleSubTitle
+    worksheet.getCell('A6').alignment = { vertical: 'middle', horizontal: 'left' }
+
+    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)
+  }
+
+}
+
+export default exportarExcelJS

+ 41 - 0
src/services/firebase.js

@@ -0,0 +1,41 @@
+import { initializeApp} from "firebase/app";
+import { getFirestore } from 'firebase/firestore';
+import { getAuth } from 'firebase/auth';
+import { getStorage } from 'firebase/storage'
+import { getDatabase } from 'firebase/database';
+
+const {
+  REACT_APP_FB_API_KEY: apiKey,
+  REACT_APP_FB_AUTH_DOMAIN: authDomain,
+  REACT_APP_FB_DB_URL: databaseURL,
+  REACT_APP_FB_PROJ_ID: projectId,
+  REACT_APP_FB_STORAGE: storageBucket,
+  REACT_APP_FB_SENDER_ID: messagingSenderId,
+  REACT_APP_FB_APP_ID: appId,
+} = process.env;
+
+const config = {
+  apiKey,
+  authDomain,
+  databaseURL,
+  projectId,
+  storageBucket,
+  messagingSenderId,
+  appId,
+};
+
+
+const firebase = initializeApp(config);
+const firestore = getFirestore(firebase);
+const auth = getAuth(firebase);
+const storage = getStorage(firebase);
+const database = getDatabase(firebase);
+
+
+export { 
+  firestore,
+  auth, 
+  storage, 
+  database,
+};
+export default firebase;

+ 223 - 0
src/services/httpService.js

@@ -0,0 +1,223 @@
+import { getIdToken, onAuthStateChanged } from "firebase/auth";
+import { auth } from "./firebase";
+import { TIPO_PROJECTO_FIREBASE } from "../constants";
+const localStorageKey = "usr_jwt";
+const {
+  REACT_APP_CLOUD_FUNC: baseUrl,
+  REACT_APP_PROJECT: tipoProyecto,
+  REACT_APP_API_URL: apiUrl,
+} = process.env;
+
+const getCurrentToken = () => {
+  return new Promise(async (resolve, reject) => {
+    if (tipoProyecto === TIPO_PROJECTO_FIREBASE) {
+      onAuthStateChanged(auth, (user) => {
+        if (user) {
+          getIdToken(user).then((token) => {
+            if (token) {
+              resolve(token);
+            }
+          });
+        } else {
+          resolve("");
+        }
+      });
+    } else {
+      const jwt = localStorage.getItem(localStorageKey);
+      if (!jwt) reject("No hay sesión.");
+      resolve(jwt);
+    }
+  });
+};
+
+const getHeaders = (token) => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+  Authorization: `Bearer ${token}`,
+});
+
+const getHeadersWithoutToken = () => ({
+  "Content-Type": "application/json",
+  Accept: "application/json",
+});
+
+const HttpService = {
+  getCF: async (url, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "GET",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+    });
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      resultado: serverResponse?.resultado || serverResponse || null,
+      paginacion: serverResponse?.paginacion || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+  get: async (url, auth = true) => {
+    debugger;
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(apiUrl + url, {
+      method: "GET",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+    });
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      resultado: serverResponse?.resultado || serverResponse || null,
+      paginacion: serverResponse?.paginacion || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+  postCF: async (url, data, auth = true, type = 1) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "POST",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.log(error);
+    }
+
+    return {
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+  post: async (url, data, auth = true, type = 1) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(apiUrl + url, {
+      method: "POST",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.log(error);
+    }
+
+    return {
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+  delete: async (url, data, auth = true) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "DELETE",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    let serverResponse = await response.json();
+
+    return {
+      isError: response?.status !== 200 ? true : false,
+      status: response?.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+    };
+  },
+  putCF: async (url, data, auth = true, type = 1) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(baseUrl + url, {
+      method: "PUT",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.log(error);
+    }
+
+    return {
+      isError: response?.status >= 400 ? true : false,
+      status: response?.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+  put: async (url, data, auth = true, type = 1) => {
+    let token = null;
+    if (auth) token = await getCurrentToken();
+    const response = await fetch(apiUrl + url, {
+      method: "PUT",
+      headers: auth ? getHeaders(token) : getHeadersWithoutToken(),
+      body: JSON.stringify(data),
+    });
+
+    let serverResponse = null;
+    try {
+      if (type === 1) {
+        serverResponse = await response.json();
+      }
+      if (type === 2) {
+        serverResponse = await response.blob();
+      }
+    } catch (error) {
+      console.log(error);
+    }
+
+    return {
+      isError: response?.status >= 400 ? true : false,
+      status: response?.status,
+      errores: serverResponse?.errores || null,
+      detalle: serverResponse?.detalle || null,
+      mensaje: serverResponse?.mensaje || null,
+      response: serverResponse || null,
+    };
+  },
+};
+
+export default HttpService;

+ 3 - 0
src/services/index.js

@@ -0,0 +1,3 @@
+export * from "./httpService";
+export * from "./firebase";
+export * from './exportarExcel';

+ 5 - 0
src/setupTests.js

@@ -0,0 +1,5 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';

+ 0 - 0
src/styles/AuthLayout.less


+ 31 - 0
src/styles/DashboardLayout.less

@@ -0,0 +1,31 @@
+.ed-topbar {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  height: 100%;
+  justify-content: space-between;
+}
+
+.trigger {
+  margin-right: 20px;
+  color: #000;
+}
+
+.ed-topbar .title {
+  margin-top: 10px;
+  margin-left: 15px;
+}
+
+.ed-topbar .breadcrumb {
+  display: flex;
+  align-items: center;
+}
+
+.ed-topbar .user {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  justify-content: flex-end;
+}
+

+ 190 - 0
src/styles/LoginForm.less

@@ -0,0 +1,190 @@
+.full-layout {
+  background-color: #f5f5f5 !important;
+  height: 100vh;
+}
+
+.ed-login-container{
+  position:relative;
+  height:100%;
+  padding-bottom:30px;
+  -webkit-flex:1;
+  -ms-flex:1;
+  flex:1;
+  display:-webkit-flex;
+  display:-ms-flexbox;
+  display:-ms-flex;
+  display:flex;
+  -webkit-flex-direction:row;
+  -ms-flex-direction:row;
+  flex-direction:row;
+  -webkit-flex-wrap:wrap;
+  -ms-flex-wrap:wrap;
+  flex-wrap:wrap;
+  -webkit-align-items:center;
+  -ms-align-items:center;
+  align-items:center;
+  -webkit-justify-content:center;
+  -ms-justify-content:center;
+  justify-content:center
+}
+
+.ed-app-login-wrap{
+  height:100%;
+  display:-webkit-flex;
+  display:-ms-flexbox;
+  display:-ms-flex;
+  display:flex;
+  -webkit-flex-direction:column;
+  -ms-flex-direction:column;
+  flex-direction:column;
+  -webkit-flex-wrap:nowrap;
+  -ms-flex-wrap:nowrap;
+  flex-wrap:nowrap;
+  -webkit-justify-content:center;
+  -ms-justify-content:center;
+  justify-content:center;
+  overflow-x:hidden;
+}
+@media screen and (max-width:575px){
+  .ed-app-login-wrap{
+      padding-top:20px;
+      -webkit-justify-content:flex-start;
+      -ms-justify-content:flex-start;
+      justify-content:flex-start
+  }
+}
+.ed-app-login-container{
+  position:relative;
+  max-width:680px;
+  width:94%;
+  margin:0 auto
+}
+@media screen and (max-width:575px){
+  .ed-app-login-container{
+    padding-bottom:20px 
+  }
+}
+.ed-app-login-main-content{
+  display:-webkit-flex;
+  display:-ms-flexbox;
+  display:-ms-flex;
+  display:flex;
+  -webkit-flex-direction:row;
+  -ms-flex-direction:row;
+  flex-direction:row;
+  -webkit-flex-wrap:wrap;
+  -ms-flex-wrap:wrap;
+  flex-wrap:wrap;
+  background-color:#ffffff;
+  -webkit-box-shadow:0 0 5px 5px rgba(0,0,0,0.03);
+  -moz-box-shadow:0 0 5px 5px rgba(0,0,0,0.03);
+  box-shadow:0 0 5px 5px rgba(0,0,0,0.03);
+  -webkit-border-radius:12px;
+  -moz-border-radius:12px;
+  border-radius:12px;
+  font-size:14px;
+  overflow:hidden
+}
+.ed-app-login-content{
+  padding:35px 35px 20px;
+  width:60%
+}
+.ed-app-login-content .ant-input {
+  box-sizing: border-box;
+  margin: 0;
+  padding: 0;
+  font-variant: tabular-nums;
+  list-style: none;
+  position: relative;
+  display: inline-block;
+  width: 100%;
+  min-width: 0;
+  padding: 7.9px 11px;
+  color: #545454;
+  font-size: 14px;
+  line-height: 1.3;
+  background-color: #fff;
+  background-image: none;
+  border: 1px solid #d9d9d9;
+  border-radius: 6px;
+  transition: all .3s;
+}
+.ed-app-login-content .ed-btn{
+  padding:6px 35px !important;
+  height:auto;
+  border-radius: 6px
+}
+@media screen and (max-width:575px){
+  .ed-app-login-content{
+    width:100%;
+    padding:20px 20px 10px
+  }
+}
+.ed-app-logo-content{
+  color:#ffffff;
+  padding:35px 35px 20px;
+  width:40%;
+  position:relative;
+  overflow:hidden;
+  display:-webkit-flex;
+  display:-ms-flexbox;
+  display:-ms-flex;
+  display:flex;
+  -webkit-flex-direction:column;
+  -ms-flex-direction:column;
+  flex-direction:column;
+  -webkit-flex-wrap:nowrap;
+  -ms-flex-wrap:nowrap;
+  flex-wrap:nowrap
+}
+.ed-app-logo-content>*{
+  position:relative;
+  z-index:2
+}
+.ed-app-logo-content h1{
+  color:#ffffff
+}
+@media screen and (max-width:575px){
+  .ed-app-logo-content{
+      width:100%;
+      padding:20px 20px 10px
+  }
+}
+.ed-app-logo-content-bg{
+  position:absolute;
+  left:0;
+  top:0;
+  width:100%;
+  height:100%;
+  z-index:1
+}
+.ed-app-logo-content-bg:before{
+  content:"";
+  position:absolute;
+  left:0;
+  top:0;
+  z-index:1;
+  right:0;
+  bottom:0;
+  background-color: rgb(5 5 5 / 60%);
+}
+.ed-app-logo-content-bg img{
+  width:100%;
+  height:100%;
+  object-fit: cover;
+}
+.ed-app-logo-wid{
+  margin-bottom:auto;
+}
+.ant-form-item-control-input {
+  min-height:40px;
+}
+.ed-app-login-content .ant-input-password {
+  min-height:40px;
+}
+
+.ed-app-login-footer{
+  margin-top: 50px;
+  font-size: 12px;
+  margin-bottom: -5px;
+}

+ 74 - 0
src/utilities/index.js

@@ -0,0 +1,74 @@
+import { NON_DIGIT } from "../constants";
+
+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 maskCurrency = (value) => {
+  return value?.toLocaleString('es-MX', {minimumFractionDigits: 2})
+};
+
+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;
+};
+
+export { 
+  capitalizeFirst,
+  propertyAccesor,
+  serialDateToJSDate,
+  validateName,
+  validateNumber,
+  maskCurrency,
+  agregarFaltantes
+};

+ 338 - 0
src/views/administracion/dependencias/DependenciaDetalle.js

@@ -0,0 +1,338 @@
+import React, { useState, useEffect } from "react";
+import { Form, Button, Row, Col, Input, Modal, Table, Typography, Select } from "antd";
+import { SaveOutlined, PlusOutlined, CloseCircleOutlined } from "@ant-design/icons";
+import { useQuery, useModels } from "../../../hooks";
+import { useHistory } from "react-router-dom";
+import { firestore } from "../../../services/firebase";
+import { ViewLoading } from "../../../components";
+import { DefaultLayout } from "../../../components/layouts";
+import { addDoc, collection, doc, getDoc, updateDoc } from "firebase/firestore";
+import HttpService from "../../../services/httpService";
+
+const { Title } = Typography;
+
+const DependenciaDetalle = () => {
+
+  const firebaseCollection = "dependencias";
+  const query = useQuery();
+  const history = useHistory();
+  const { TextArea } = Input;
+  const id = query.get("id");
+  const editando = Boolean(id);
+  const titulo = editando ? "Editar dependencia" : "Agregar dependencia";
+  const url = "/administracion/dependencias";
+  const [form] = Form.useForm();
+
+  const [dependencia, setDependencia] = useState([]);
+  const [cargandoDependencia, setCargandoDependencia] = useState(true);
+  const [guadandoCargando, setGuardandoCargando] = useState(false);
+  const [buscarValue, setBuscarValue] = useState('');
+  const [requestUsuarios, setRequestUsuarios] = useState({});
+  const [timer, setTimer] = useState(null);
+  const [miembros, setMiembros] = useState([]);
+  const [miembrosLoading, setMiembrosLoading] = useState(false);
+  const [usuario, setUsuario] = useState(null);
+  const [lideres, setLideres] = useState([]);
+
+  const usuarioExtraParams = React.useMemo(() => ({ q: buscarValue }), [buscarValue])
+  const usuariosParams = React.useMemo(() => ({
+    name: 'v1/usuario',
+    limite: 20,
+    ordenar: 'nombre-asc',
+    expand: 'grupos',
+    extraParams: usuarioExtraParams
+  }), [usuarioExtraParams])
+
+  const [
+    models,
+    modelsLoading, , ,
+  ] = useModels(requestUsuarios);
+
+  const columnasMiembros = [
+    { title: 'Nombre', dataIndex: 'nombre', key: 'nombre', },
+    { title: 'Correo', dataIndex: 'email', key: 'email', },
+  ]
+
+  const columnasLideres = [
+    { title: 'Nombre', dataIndex: 'nombre', key: 'nombre', },
+    {
+      title: '', dataIndex: 'uid', key: 'uid', render: (text) =>
+        <Button
+          danger
+          icon={<CloseCircleOutlined />}
+          onClick={() => setLideres(lideres.filter(l => l.uid !== text))}
+        />
+    },
+  ]
+
+  const onSearchUsuarios = (value) => {
+    clearTimeout(timer);
+    const newTimer = setTimeout(() => {
+      setBuscarValue(value);
+    }, 300);
+
+    setTimer(newTimer);
+  };
+
+  React.useEffect(() => {
+    setRequestUsuarios(usuariosParams);
+    return () => setRequestUsuarios({});
+  }, [usuariosParams]);
+
+  React.useEffect(() => {
+    if (editando && id) {
+      const getMiembros = async () => {
+        setMiembrosLoading(true);
+        const response = await HttpService.get(`v1/usuario?ordenar=nombre-asc&idDependencia=${id}&limite=-1`);
+        if (!response.isError) {
+          setMiembros(response.resultado);
+          setMiembrosLoading(false);
+        }
+      }
+      getMiembros();
+    }
+  }, [editando, id]);
+
+  const onFinish = async (values) => {
+    try {
+      setGuardandoCargando(true);
+
+      let body = {
+        ...values,
+        timestamp: editando ? dependencia?.timestamp : new Date(),
+        estatus: editando ? dependencia?.estatus : true,
+        sincronizado: null,
+        usuarios: lideres.map(l => ({
+          uid: l.uid,
+          nombre: l.nombre,
+        })),
+      };
+
+      // Agregamos o editamos el documento grupo
+      if (editando) {
+        await updateDoc(doc(firestore, firebaseCollection, id), body);
+      } else {
+        await addDoc(collection(firestore, firebaseCollection), body);
+      }
+
+      Modal.success({
+        title: "Éxito",
+        content: "Dependencia guardado correctamente",
+      });
+
+      history.push(url);
+
+    } catch (error) {
+      Modal.warning({
+        title: "Atención",
+        content: "Hubo un problema al guardar, intentalo de nuevo.",
+      });
+      console.log("Error al guardar: ", error);
+    } finally {
+      setGuardandoCargando(false);
+    }
+  };
+
+  const onFinishFailed = () => {
+    Modal.warning({
+      title: "Atención",
+      content: "Por favor revise los datos.",
+      style: { marginTop: "20vh" },
+    });
+  };
+
+  useEffect(() => {
+    let mounted = true;
+    if (mounted && editando) {
+      try {
+        setTimeout(async () => {
+          const docRef = doc(firestore, "dependencias", id);
+          const docSnap = await getDoc(docRef);
+          setDependencia(docSnap.data());
+
+          const { usuarios } = docSnap.data();
+
+          if (usuarios?.length > 0) {
+            setLideres(usuarios);
+          }
+        }, 0);
+      } catch (error) {
+        console.log("Error al obtener dependencia: ", error);
+      } finally {
+        setCargandoDependencia(false);
+      }
+    }
+    return () => (mounted = false);
+  }, [editando, id]);
+
+  useEffect(() => {
+    let mounted = true;
+    if (mounted && editando) {
+      form.setFieldsValue({
+        ...dependencia,
+      });
+    }
+    return () => (mounted = false);
+  }, [dependencia, editando, form]);
+
+  if (cargandoDependencia && editando) return <ViewLoading />;
+
+  return (
+    <DefaultLayout title={titulo} multipleButtonData={[]}>
+      <Form
+        name="form"
+        form={form}
+        autoComplete="off"
+        layout="vertical"
+        onFinish={onFinish}
+        onFinishFailed={onFinishFailed}
+      >
+        {/* Nombre de la dependecia */}
+        <Row gutter={10}>
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Form.Item
+              label="Nombre de la dependencia"
+              name="nombre"
+              rules={[{ required: true, message: "Requerido" }]}
+            >
+              <Input placeholder="Ingresa un nombre..." />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        {/* Descripción  */}
+        <Row gutter={10}>
+          <Col className="gutter-row" span={24}>
+            <Form.Item
+              label="Descripción"
+              name="descripcion"
+              rules={[{ required: true, message: "Requerido" }]}
+            >
+              <TextArea rows={4} placeholder="Ingrese una Descripción" />
+            </Form.Item>
+          </Col>
+        </Row>
+
+        <Row gutter={10}>
+          <Col className="gutter-row" span={12}>
+            <Title level={4}>Miembros</Title>
+            <Table
+              columns={columnasMiembros}
+              dataSource={miembros}
+              loading={miembrosLoading}
+              size="small"
+              rowKey="id"
+              scroll={{ y: 400 }}
+              pagination={
+                miembros.length > 10
+                  ? {
+                    pageSize: 10,
+                    position: ["bottomCenter"],
+                    showTotal: (total, range) => `${range[0]}-${range[1]} de ${total} registros`,
+                  } : false
+              }
+            />
+            <br />
+          </Col>
+          <Col className="gutter-row" span={12}>
+            <Row>
+              <Col span={22}>
+                <Title level={4}>Líderes</Title>
+                <Form.Item
+                  label="Usuarios"
+                  rules={[{
+                    required: true,
+                    message: 'Selecciona un usuario!',
+                  }]}
+                  hasFeedback
+                >
+                  <Select
+                    showSearch
+                    onSearch={onSearchUsuarios}
+                    allowClear
+                    placeholder="Elige a un usuario"
+                    optionFilterProp="children"
+                    filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
+                    loading={modelsLoading}
+                    onChange={(_, item) => {
+                      setUsuario(item);
+                      console.log("item", item);
+                    }}
+                    options={models?.length > 0 && models?.map(i => ({
+                      ...i,
+                      value: i.uid,
+                      label: i.nombre
+                    }))}
+                    value={usuario}
+                  />
+                </Form.Item>
+              </Col>
+              <Col span={2}>
+                <Button
+                  type="default"
+                  icon={<PlusOutlined />}
+                  shape="circle"
+                  size="middle"
+                  style={{
+                    marginTop: 70,
+                    marginLeft: 10
+                  }}
+                  onClick={() => {
+                    if (usuario) {
+                      //agregar sólo si no existe en el array, con esto evitamos duplicados
+                      if (!lideres.find(i => i.uid === usuario.uid)) {
+                        setLideres([...lideres, usuario]);
+                        setUsuario(null);
+                      }
+                    }
+                  }}
+                />
+              </Col>
+            </Row>
+            <Table
+              columns={columnasLideres}
+              dataSource={lideres}
+              size="small"
+              rowKey="uid"
+              pagination={
+                lideres.length > 10
+                  ? {
+                    pageSize: 10,
+                    position: ["bottomCenter"],
+                    showTotal: (total, range) => `${range[0]}-${range[1]} de ${total} registros`,
+                  } : false
+              }
+            />
+          </Col>
+        </Row>
+        <Row gutter={10}>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 8 }}
+          >
+            <Button
+              type="primary"
+              htmlType="submit"
+              icon={<SaveOutlined />}
+              block
+              size="large"
+              loading={guadandoCargando}
+            >
+              Guardar
+            </Button>
+          </Col>
+        </Row>
+      </Form>
+    </DefaultLayout>
+  )
+}
+
+export default DependenciaDetalle

+ 142 - 0
src/views/administracion/dependencias/DependenciasListado.js

@@ -0,0 +1,142 @@
+import React, { useEffect, useState } from 'react';
+import { Table, Modal } from 'antd';
+import { useHistory } from 'react-router-dom';
+import { EditOutlined, DeleteOutlined, PlusCircleOutlined, ExclamationCircleOutlined } from "@ant-design/icons";
+import { firestore } from "../../../services/firebase";
+import { ActionsButton } from '../../../components';
+import { SimpleTableLayout } from '../../../components/layouts'
+import { collection, onSnapshot, doc, updateDoc } from "firebase/firestore";
+import moment from 'moment';
+
+const DependenciasListado = () => {
+
+  const titulo = "Dependencias";
+  const firebaseCollection = "dependencias";
+  const url = "/administracion/dependencias";
+  const history = useHistory();
+  const { confirm } = Modal;
+
+  const [cargandoDependencias, setCargandoDependencias] = useState(true);
+  const [dependencias, setDependencias] = useState([]);
+  const [filteredResults, setFilteredResults] = useState([]);
+  const [searchInput, setSearchInput] = useState('');
+
+  const defaultText = prop => prop || <span style={{ color: '#c7c3c3' }}>---</span>;
+
+  const columns = [
+    {
+      title: "Acciones",
+      dataIndex: "id",
+      key: "id",
+      align: "center",
+      width: 100,
+      render: (_, item ) => (
+        <ActionsButton
+          key={item}
+          options={[
+            {
+              name: "Editar",
+              icon: <EditOutlined />,
+              onClick: () => history.push(url+'/detalle?id='+item.id)
+            },
+            {
+              name: "Eliminar",
+              icon: <DeleteOutlined />,
+              onClick: () => {
+                eliminarRegistro(item)
+              },
+              styleProps: { color: '#e91e63' },
+            },
+          ]}
+        />
+      )
+    },
+    {
+      title: "Nombre",
+      dataIndex: "nombre",
+      key: "nombre",
+      render: defaultText,
+    },
+    {
+      title: "Creado",
+      dataIndex: "timestamp",
+      key: "timestamp",
+      render: (_, item) => (
+        moment(item?.timestamp?.toDate()).format('DD-MM-YYYY')
+      ),
+    },
+  ];
+  
+  const multipleButtonData = [{
+    text: "Agregar",
+    to: () => history.push(url+'/nueva'),
+    icon: <PlusCircleOutlined />,
+    props: { disabled: false, type: 'primary', }
+  }];
+
+  const eliminarRegistro = async (item) => {
+    confirm({
+      title: `Atención`,
+      icon: <ExclamationCircleOutlined />,
+      content: '¿Seguro que desea eliminar este registro?',
+      onOk: async () => {
+        try {
+          await updateDoc(doc(firestore, firebaseCollection, item.id), {estatus: false}); 
+        } catch (error) {
+          console.log('error al eliminar: ', error);
+        }
+      },
+      onCancel() {},
+    });
+  };
+
+  const onSearch = (searchValue) =>  {
+    setSearchInput(searchValue);
+    if( searchValue !== '' ){
+      const filteredData = dependencias.filter((item) => {
+        return Object.values(item).join('').toLowerCase().includes(searchValue.toLowerCase());
+      })
+      setFilteredResults(filteredData);
+    }else{
+      setFilteredResults(dependencias);
+    }
+  };
+
+  // Dependencias
+  useEffect (() => {  
+    try {
+      onSnapshot( collection(firestore, firebaseCollection), (querySnapshot) => {
+         const docs = [];
+         querySnapshot.forEach((doc) => {
+          if( doc.data().estatus ) {
+            docs.push({ ...doc.data(), id:doc.id });
+          }
+         });
+         setDependencias(docs);
+       })
+     } catch (error) {
+       console.log('error al cargar dependencias de firebase: ', error);
+     } finally {
+      setCargandoDependencias(false);
+     }
+  }, []);
+  
+  return (
+    <SimpleTableLayout 
+      title={titulo}
+      multipleButtonData={multipleButtonData}
+      onSearchClicked={onSearch}
+    >
+      <Table
+        dataSource={searchInput.length > 1 ? filteredResults : dependencias}
+        rowKey="id"
+        loading={cargandoDependencias}
+        columns={columns}
+        size='small'
+        scroll={{ x: 700 }}
+      />
+    </SimpleTableLayout>
+  )
+}
+
+export default DependenciasListado

+ 7 - 0
src/views/administracion/dependencias/index.js

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

Datei-Diff unterdrückt, da er zu groß ist
+ 1125 - 0
src/views/administracion/eventos/EventoDetalleGrupo.js


+ 934 - 0
src/views/administracion/eventos/EventosDetalle.js

@@ -0,0 +1,934 @@
+import React, { useEffect, useState } from "react";
+import {
+  Col,
+  Row,
+  Form,
+  Input,
+  Modal,
+  Table,
+  Select,
+  Button,
+  Upload,
+  Divider,
+  Checkbox,
+  DatePicker,
+  message,
+  Tooltip,
+} from "antd";
+import {
+  SaveOutlined,
+  UploadOutlined,
+  ArrowLeftOutlined,
+  PlusCircleOutlined,
+  MinusCircleOutlined,
+} from "@ant-design/icons";
+import "moment/locale/es-mx";
+import moment from "moment";
+import locale from "antd/es/date-picker/locale/es_ES";
+import { useHistory } from "react-router-dom";
+import { push, ref as refDatabase } from "firebase/database";
+import {
+  addDoc,
+  collection,
+  doc,
+  getDoc,
+  updateDoc,
+  onSnapshot,
+  query as firebseQuery,
+  getDocs,
+  where,
+} from "firebase/firestore";
+import {
+  ref as refStorage,
+  uploadBytes,
+  deleteObject,
+  getStorage,
+  getDownloadURL,
+} from "firebase/storage";
+
+import { useQuery } from "../../../hooks";
+import { ViewLoading } from "../../../components";
+import { DefaultLayout } from "../../../components/layouts";
+import { firestore, storage, database } from "../../../services/firebase";
+
+const EventosDetalle = () => {
+  const urlweb = "/administracion/eventos";
+  const query = useQuery();
+  const history = useHistory();
+  const { TextArea, Search } = Input;
+  const { Option } = Select;
+  const id = query.get("id");
+  const editando = !!id;
+  const [form] = Form.useForm();
+
+  const [evento, setEvento] = useState([]);
+  const [cargandoEvento, setCargandoEvento] = useState(true);
+  const [guadandoCargando, setGuardandoCargando] = useState(false);
+  const [sinFechaFin, setSinFechaFin] = useState(false);
+  const [visible, setVisible] = useState(false);
+  const [cargandoGrupos, setCargandoGrupos] = useState(true);
+  const [grupos, setGrupos] = useState([]);
+  const [filteredResults, setFilteredResults] = useState([]);
+  const [searchInput, setSearchInput] = useState("");
+  const [selectedGrupos, setSelectedGrupos] = useState([]);
+  const [selectedFile, setSelectedFile] = useState(null);
+  const [selectedFileList, setSelectedFileList] = useState([]);
+  const [previaImg, setPreviaImg] = useState(null);
+  const [redes, setRedes] = useState([]);
+
+  const defaultText = (prop) =>
+    prop || <span style={{ color: "#c7c3c3" }}>---</span>;
+
+  const multipleButtonData = [
+    {
+      text: "Volver",
+      to: () => history.goBack(),
+      icon: <ArrowLeftOutlined />,
+      props: { disabled: false, type: "primary" },
+    },
+  ];
+
+  const columns = [
+    {
+      title: "Nombre",
+      dataIndex: "nombre",
+      key: "nombre",
+      render: defaultText,
+    },
+    {
+      title: "Ciudad",
+      dataIndex: "ciudad",
+      key: "ciudad",
+      render: defaultText,
+    },
+    {
+      title: "Creado",
+      dataIndex: "timestamp",
+      key: "timestamp",
+      render: (_, item) =>
+        moment(item?.timestamp?.toDate()).format("DD-MM-YYYY"),
+    },
+  ];
+
+  const redesSociales = [
+    "Facebook",
+    "Twitter",
+    "Instagram",
+    "Whatsapp",
+    "YouTube",
+    "TikTok",
+    "Otro",
+  ];
+
+  const acciones = [
+    { id: 0, value: "Ver", label: "Ver", icon: "👁" },
+    { id: 1, value: "Me gusta", label: "Me gusta", icon: "👍" },
+    { id: 2, value: "Compartir", label: "Compartir", icon: "🔄" },
+  ];
+
+  const rowSelection = {
+    onChange: (selectedRowKeys, _) => {
+      setSelectedGrupos(selectedRowKeys);
+    },
+  };
+
+  const obtenerUsuariosGrupo = async () => {
+    try {
+      const q = firebseQuery(
+        collection(firestore, "usuarios"),
+        where("grupos", "array-contains-any", selectedGrupos)
+      );
+      const querySnapshot = await getDocs(q);
+      const docs = [];
+
+      querySnapshot.forEach((doc) => {
+        const data = doc.data();
+        if (data?.estatus) {
+          docs.push({
+            grupos: data?.grupos,
+            facebook: data?.facebook,
+            twitter: data?.twitter,
+            instagram: data?.instagram,
+            telefono: data?.telefono,
+            nombre: data?.nombre,
+            uid: data?.uid,
+            id: doc.id,
+            facebookVerificado: data?.facebookVerificado,
+            instagramVerificado: data?.instagramVerificado,
+            twitterVerificado: data?.twitterVerificado,
+          });
+        }
+      });
+
+      return docs;
+    } catch (error) {
+      console.log("error al obtener grupos: ", error);
+    }
+  };
+
+  const normFile = (e) => {
+    if (Array.isArray(e)) {
+      return e;
+    }
+    return e && e.fileList;
+  };
+
+  const dummyRequest = ({ onSuccess }) => {
+    setTimeout(() => {
+      onSuccess("ok");
+    }, 0);
+  };
+
+  const onChangeFile = (info) => {
+    let src = URL.createObjectURL(info.file.originFileObj);
+    setPreviaImg(src);
+    switch (info.file.status) {
+      case "uploading":
+        setSelectedFileList([info.file]);
+        break;
+      case "done":
+        setSelectedFile(info.file);
+        setSelectedFileList([info.file]);
+        break;
+      default:
+        setSelectedFile(null);
+        setSelectedFileList([]);
+    }
+  };
+
+  const eliminarImagen = async (fileRoute) => {
+    const storage = getStorage();
+    const desertRef = refStorage(storage, fileRoute);
+    deleteObject(desertRef)
+      .then((e) => {})
+      .catch((error) => {
+        console.log(error);
+      });
+  };
+
+  const onSearch = (searchValue) => {
+    setSearchInput(searchValue);
+    if (searchValue !== "") {
+      const filteredData = grupos.filter((item) => {
+        return Object.values(item)
+          .join("")
+          .toLowerCase()
+          .includes(searchValue.toLowerCase());
+      });
+      setFilteredResults(filteredData);
+    } else {
+      setFilteredResults(grupos);
+    }
+  };
+
+  const onFinish = async (values) => {
+    try {
+      const {
+        accion,
+        ciudad,
+        descripcion,
+        fechaFinal,
+        fechaInicio,
+        nombre,
+        redSocial,
+        url,
+      } = values;
+
+      setGuardandoCargando(true);
+
+      let file = selectedFile ? selectedFile.originFileObj : null;
+
+      if (!editando && !file) {
+        Modal.warning({
+          title: "Atención",
+          content: "Debes subir una imagen del evento.",
+          style: { marginTop: "20vh" },
+        });
+      }
+
+      let rutaFoto = evento?.fotoEvento;
+      let pathFirebase = evento?.pathFirebase;
+
+      if (file) {
+        if (editando) {
+          await eliminarImagen(evento?.fotoEvento);
+        }
+
+        const postListRef = refDatabase(database, "fotosEventos");
+        const newPostRef = push(postListRef);
+        rutaFoto = `fotosEventos/${newPostRef?.key}`;
+        const fotoRefStorage = refStorage(storage, rutaFoto);
+        try {
+          let res = await uploadBytes(fotoRefStorage, file);
+          pathFirebase = await getDownloadURL(res?.ref);
+        } catch (error) {
+          console.log("Error en el uploadbytes: ", error);
+        }
+      }
+
+      const docsUsuarios = await obtenerUsuariosGrupo();
+
+      let resultado = { ...evento?.resultado };
+      let usuarios = [];
+
+      for (let u = 0; u < docsUsuarios?.length; u++) {
+        if (resultado[docsUsuarios[u]?.uid] === undefined) {
+          resultado[docsUsuarios[u]?.uid] = [];
+        }
+
+        if (resultado[docsUsuarios[u]?.uid]?.length < 1) {
+          resultado = {
+            ...resultado,
+            [docsUsuarios[u].uid]: [],
+          };
+        }
+
+        usuarios.push({
+          id: docsUsuarios[u]?.id,
+          uid: docsUsuarios[u]?.uid,
+          grupos: docsUsuarios[u]?.grupos,
+          nombre: docsUsuarios[u]?.nombre,
+          twitter: docsUsuarios[u]?.twitter,
+          telefono: docsUsuarios[u]?.telefono,
+          facebook: docsUsuarios[u]?.facebook,
+          instagram: docsUsuarios[u]?.instagram,
+          facebookVerificado: Boolean(docsUsuarios[u]?.facebookVerificado),
+          instagramVerificado: Boolean(docsUsuarios[u]?.instagramVerificado),
+          twitterVerificado: Boolean(docsUsuarios[u]?.twitterVerificado),
+        });
+      }
+
+      let body = {
+        accion: accion || [],
+        ciudad: ciudad || "",
+        descripcion: descripcion || "",
+        fechaFinal: fechaFinal?.toString() || "",
+        fechaInicio: fechaInicio?.toString(),
+        sinFechaFin: sinFechaFin,
+        nombre: nombre || "",
+        redSocial: redSocial || "",
+        url: url || "",
+        grupos: selectedGrupos,
+        fotoEvento: rutaFoto,
+        pathFirebase: pathFirebase,
+        timestamp: editando ? evento?.timestamp : new Date(),
+        estatus: editando ? evento?.estatus : true,
+        usuarios: usuarios,
+        redes: redes,
+        resultado: resultado,
+        sincronizado: null,
+      };
+
+      const tag = makeid(8);
+
+      //Crear grupo de eventos
+      if (redes.length > 0) {
+        let grupoEvento = {
+          accion: accion,
+          ciudad: ciudad,
+          descripcion: descripcion,
+          fechaFinal: fechaFinal?.toString() || "",
+          fechaInicio: fechaInicio?.toString(),
+          sinFechaFin: sinFechaFin,
+          nombre: nombre || "",
+          grupos: selectedGrupos,
+          fotoEvento: rutaFoto,
+          pathFirebase: pathFirebase,
+          timestamp: editando ? evento?.timestamp : new Date(),
+          estatus: editando ? evento?.estatus : true,
+          tag: tag,
+          redes: redes,
+          usuarios: usuarios,
+          resultado: resultado,
+        };
+
+        await addDoc(collection(firestore, "gruposEvento"), grupoEvento);
+      }
+
+      if (editando) {
+        await updateDoc(doc(firestore, "eventos", id), body);
+        return;
+      }
+
+      for (let i = 0; i < redes?.length; i++) {
+        body = {
+          ...body,
+          redSocial: redes[i]?.redSocial,
+          url: redes[i]?.url,
+        };
+
+        if (!editando) {
+          body["tag"] = tag;
+        }
+        await addDoc(collection(firestore, "eventos"), body);
+      }
+
+      Modal.success({
+        title: "Éxito",
+        content: "Evento guardado correctamente",
+      });
+
+      history.push(urlweb);
+    } catch (error) {
+      Modal.warning({
+        title: "Atención",
+        content: "Hubo un problema al guardar, intentalo de nuevo.",
+      });
+      console.log("Error al guardar evento: ", error);
+    } finally {
+      setGuardandoCargando(false);
+    }
+  };
+
+  const onFinishFailed = () => {
+    Modal.warning({
+      title: "Atención",
+      content: "Por favor revise los datos.",
+      style: { marginTop: "20vh" },
+    });
+  };
+
+  function makeid(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;
+  }
+
+  const agregarRedSocial = () => {
+    const key = makeid(6);
+    const datos = [...redes];
+
+    const url = form.getFieldValue("url");
+    const red = form.getFieldValue("redSocial");
+
+    if (!url) {
+      message.warning({
+        content: "Ingresa una URL",
+      });
+      return;
+    }
+
+    if (!red) {
+      message.warning({
+        content: "Selecciona una red social",
+      });
+      return;
+    }
+
+    let existe = false;
+    for (let i = 0; i < redes.length; i++) {
+      if (redes[i]?.redSocial === red) existe = true;
+      break;
+    }
+
+    if (existe) {
+      message.warning({
+        content: "Esta red social ya existe, elige otra diferente.",
+      });
+      return;
+    }
+
+    datos.push({
+      key: key,
+      url: url,
+      redSocial: red,
+    });
+
+    setRedes(datos);
+    form.setFieldsValue({
+      url: null,
+      redSocial: null,
+    });
+  };
+
+  const quitarRedSocial = (key) => {
+    let data = [...redes];
+    for (let i = 0; i < data?.length; i++) {
+      if (redes[i].key === key) {
+        data.splice(i, 1);
+        break;
+      }
+    }
+    setRedes(data);
+  };
+
+  const leerLink = (evento) => {
+    let link = evento?.target?.value;
+    const facebook =
+      /https?:\/\/(www\.)?facebook\.com\/[a-zA-Z0-9.-]+\/?/ ||
+      /https?:\/\/(www\.)?fb\.com\/[a-zA-Z0-9.-]+\/?/;
+    const twitter = /https?:\/\/(www\.)?twitter\.com\/[a-zA-Z0-9.-]+\/?/;
+    const instagram = /https?:\/\/(www\.)?instagram\.com\/[a-zA-Z0-9.-]+\/?/;
+    const whatsapp = /https?:\/\/(www\.)?wa\.me\/[a-zA-Z0-9.-]+\/?/;
+    const youtube = /https?:\/\/(www\.)?youtube\.com\/[a-zA-Z0-9.-]+\/?/g;
+    const ytShort = /https?:\/\/(www\.)?youtu\.be\/[a-zA-Z0-9.-]+\/?/g;
+    const tiktok = /https?:\/\/(?:www\.)?tiktok\.com\/.*?/g;
+
+    if (facebook.test(link)) {
+      form.setFieldsValue({ redSocial: "Facebook" });
+    } else if (twitter.test(link)) {
+      form.setFieldsValue({ redSocial: "Twitter" });
+    } else if (instagram.test(link)) {
+      form.setFieldsValue({ redSocial: "Instagram" });
+    } else if (whatsapp.test(link)) {
+      form.setFieldsValue({ redSocial: "Whatsapp" });
+    } else if (youtube.test(link) || ytShort.test(link)) {
+      form.setFieldsValue({ redSocial: "YouTube" });
+    } else if (tiktok.test(link)) {
+      form.setFieldsValue({ redSocial: "TikTok" });
+    } else {
+      form.setFieldsValue({ redSocial: "Otro" });
+    }
+  };
+
+  // Grupos
+  useEffect(() => {
+    try {
+      onSnapshot(collection(firestore, "grupos"), (querySnapshot) => {
+        const docs = [];
+        querySnapshot.forEach((doc) => {
+          if (doc.data()?.estatus) {
+            docs.push({ ...doc.data(), id: doc.id });
+          }
+        });
+        setGrupos(docs);
+      });
+    } catch (error) {
+      console.log("error al cargar grupos de firebase: ", error);
+    } finally {
+      setCargandoGrupos(false);
+    }
+  }, []);
+
+  // Limpiar imagen
+  useEffect(() => {
+    if (!selectedFile) {
+      setPreviaImg(null);
+    }
+  }, [selectedFile]);
+
+  // Obtener detalles del evento.
+  useEffect(() => {
+    let mounted = true;
+    if (mounted && editando) {
+      try {
+        setTimeout(async () => {
+          const docRef = doc(firestore, "eventos", id);
+          const docSnap = await getDoc(docRef);
+          setEvento(docSnap.data());
+        }, 0);
+      } catch (error) {
+        console.log("error al obtener evento: ", error);
+      } finally {
+        setCargandoEvento(false);
+      }
+    }
+    return () => (mounted = false);
+  }, [editando, id]);
+
+  useEffect(() => {
+    let mounted = true;
+    if (mounted && evento && editando) {
+      setPreviaImg(evento?.pathFirebase);
+      setSinFechaFin(evento?.sinFechaFin);
+      setSelectedGrupos(evento?.grupos);
+      form.setFieldsValue({
+        ...evento,
+        fechaFinal: moment(evento?.fechaFinal),
+        fechaInicio: moment(evento?.fechaInicio),
+      });
+    }
+    return () => (mounted = false);
+  }, [editando, evento, form]);
+
+  if (cargandoEvento && editando) return <ViewLoading />;
+
+  return (
+    <DefaultLayout multipleButtonData={multipleButtonData}>
+      <Modal
+        title="Agregar grupos"
+        centered
+        open={visible}
+        onOk={() => setVisible(false)}
+        onCancel={() => setVisible(false)}
+        okText="Guardar"
+        width={800}
+      >
+        <Search
+          placeholder="Buscar..."
+          enterButton="Buscar"
+          size="large"
+          style={{ width: "100%" }}
+          onSearch={onSearch}
+        />
+        <Divider />
+        <Table
+          dataSource={searchInput?.length > 1 ? filteredResults : grupos}
+          rowKey="id"
+          pagination={{
+            pageSize: 5,
+          }}
+          rowSelection={{
+            type: "checkbox",
+            selectedRowKeys: selectedGrupos,
+            ...rowSelection,
+          }}
+          loading={cargandoGrupos}
+          columns={columns}
+          size="small"
+          scroll={{ x: 700 }}
+        />
+      </Modal>
+      <Form
+        name="form"
+        form={form}
+        autoComplete="off"
+        layout="vertical"
+        onFinish={onFinish}
+        onFinishFailed={onFinishFailed}
+      >
+        {/* Imagen - Nombre - Ciudad - Descripción - Url - Red social  */}
+        <Row gutter={10}>
+          {/* Columna 1  */}
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 8 }}
+            lg={{ span: 8 }}
+          >
+            <Row
+              gutter={10}
+              style={{
+                display: "flex",
+                flexDirection: "row",
+                height: "100%",
+              }}
+            >
+              <Col span={24}>
+                <Form.Item
+                  name="upload"
+                  label="Imagen del evento"
+                  valuePropName="fileList"
+                  rules={[
+                    { required: editando ? false : true, message: "Requerido" },
+                  ]}
+                  getValueFromEvent={normFile}
+                  extra={editando && evento ? evento?.fotoEvento : null}
+                >
+                  <Upload
+                    accept="image/gif, image/jpeg, image/png"
+                    fileList={selectedFileList}
+                    customRequest={dummyRequest}
+                    onChange={onChangeFile}
+                    name="fotoEvento"
+                    multiple={false}
+                    maxCount={1}
+                  >
+                    <Button icon={<UploadOutlined />}>Elegir imagen</Button>
+                  </Upload>
+                </Form.Item>
+              </Col>
+
+              <Col span={24}>
+                {previaImg ? <img src={previaImg} width="100%" alt="" /> : null}
+              </Col>
+
+              <Col span={24} style={{ marginTop: 133 }}>
+                <Button
+                  block
+                  type="dashed"
+                  icon={<PlusCircleOutlined />}
+                  onClick={() => setVisible(true)}
+                >
+                  Agregar grupos ({selectedGrupos?.length})
+                </Button>
+              </Col>
+            </Row>
+          </Col>
+
+          {/* Columna 2  */}
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 16 }}
+            lg={{ span: 16 }}
+          >
+            {/* Nombre evento - Ciuad  */}
+            <Row gutter={10}>
+              <Col
+                className="gutter-row"
+                xs={{ span: 24 }}
+                sm={{ span: 24 }}
+                md={{ span: 12 }}
+                lg={{ span: 12 }}
+              >
+                <Form.Item
+                  label="Nombre del evento"
+                  name="nombre"
+                  rules={[{ required: true, message: "Requerido" }]}
+                >
+                  <Input />
+                </Form.Item>
+              </Col>
+              <Col
+                className="gutter-row"
+                xs={{ span: 24 }}
+                sm={{ span: 24 }}
+                md={{ span: 12 }}
+                lg={{ span: 12 }}
+              >
+                <Form.Item
+                  label="Ciudad"
+                  name="ciudad"
+                  rules={[{ required: true, message: "Requerido" }]}
+                >
+                  <Input />
+                </Form.Item>
+              </Col>
+            </Row>
+
+            {/* Descripción */}
+            <Row gutter={10}>
+              <Col className="gutter-row" span={24}>
+                <Form.Item
+                  label="Descripción"
+                  name="descripcion"
+                  rules={[{ required: true, message: "Requerido" }]}
+                >
+                  <TextArea placeholder="Ingrese una descripción..." rows={4} />
+                </Form.Item>
+              </Col>
+            </Row>
+          </Col>
+        </Row>
+
+        {/* Fecha de inicio - Fecha fin - sin fecha - Accion  */}
+        <Row gutter={10} style={{ marginTop: 30 }}>
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 6 }}
+            lg={{ span: 6 }}
+          >
+            <Form.Item
+              rules={[{ required: true, message: "Requerido" }]}
+              label="Fecha de inicio"
+              name="fechaInicio"
+            >
+              <DatePicker
+                format="YYYY-MM-DD HH:mm"
+                style={{ width: "100%" }}
+                locale={locale}
+                showTime={{
+                  defaultValue: moment("00:00", "HH:mm"),
+                }}
+              />
+            </Form.Item>
+          </Col>
+
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 6 }}
+            lg={{ span: 6 }}
+          >
+            <Form.Item
+              rules={[{ required: !sinFechaFin, message: "Requerido" }]}
+              label="Fecha de finalización"
+              name="fechaFinal"
+            >
+              <DatePicker
+                disabled={sinFechaFin}
+                format="YYYY-MM-DD HH:mm"
+                style={{ width: "100%" }}
+                locale={locale}
+                showTime={{
+                  defaultValue: moment("00:00", "HH:mm"),
+                }}
+              />
+            </Form.Item>
+          </Col>
+
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 6 }}
+            lg={{ span: 6 }}
+          >
+            <Form.Item label="&nbsp;">
+              <Checkbox
+                checked={sinFechaFin}
+                onChange={(e) => setSinFechaFin(e.target.checked)}
+              >
+                Sin fecha de finalización
+              </Checkbox>
+            </Form.Item>
+          </Col>
+
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 6 }}
+            lg={{ span: 6 }}
+          >
+            <Form.Item label="Acción" name="accion">
+              <Select
+                mode="multiple"
+                style={{ width: "100%" }}
+                placeholder="Selecciona una o más opciones"
+                optionLabelProp="label"
+              >
+                {acciones?.map((item) => (
+                  <Option
+                    value={item?.value}
+                    label={item?.label}
+                    key={item?.id}
+                  >
+                    <div className="demo-option-label-item">
+                      <span role="img" aria-label={item?.label}>
+                        {item?.icon}
+                      </span>
+                      {item?.label}
+                    </div>
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+        </Row>
+
+        {/* URL - Red Social - Agregar [+] */}
+        <Row gutter={10}>
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 12 }}
+          >
+            <Form.Item label="URL" name="url">
+              <Input onChange={(v) => leerLink(v)} />
+            </Form.Item>
+          </Col>
+
+          <Col
+            className="gutter-row"
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: editando ? 12 : 9 }}
+            lg={{ span: editando ? 12 : 9 }}
+          >
+            <Form.Item label="Red social" name="redSocial">
+              <Select style={{ width: "100%" }} placeholder="Selecciona">
+                {redesSociales?.map((item) => (
+                  <Option key={item} value={item}>
+                    {item}
+                  </Option>
+                ))}
+              </Select>
+            </Form.Item>
+          </Col>
+
+          {!editando && (
+            <Col
+              className="gutter-row"
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 3 }}
+              lg={{ span: 3 }}
+            >
+              <Form.Item label="&nbsp;">
+                <Button
+                  type="primary"
+                  icon={<PlusCircleOutlined />}
+                  block
+                  onClick={agregarRedSocial}
+                />
+              </Form.Item>
+            </Col>
+          )}
+        </Row>
+
+        {redes.map((item) => (
+          <Row key={item.key} gutter={10}>
+            <Col
+              className="gutter-row"
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 12 }}
+              lg={{ span: 12 }}
+            >
+              <Form.Item label="&nbsp;">
+                <Input readOnly value={item?.url} />
+              </Form.Item>
+            </Col>
+
+            <Col
+              className="gutter-row"
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 9 }}
+              lg={{ span: 9 }}
+            >
+              <Form.Item label="&nbsp;">
+                <Input readOnly value={item?.redSocial} />
+              </Form.Item>
+            </Col>
+
+            <Col
+              className="gutter-row"
+              xs={{ span: 24 }}
+              sm={{ span: 24 }}
+              md={{ span: 3 }}
+              lg={{ span: 3 }}
+            >
+              <Form.Item label="&nbsp;">
+                <Tooltip title="Remover">
+                  <Button
+                    type="danger"
+                    icon={<MinusCircleOutlined />}
+                    block
+                    onClick={() => quitarRedSocial(item?.key)}
+                  />
+                </Tooltip>
+              </Form.Item>
+            </Col>
+          </Row>
+        ))}
+
+        <Row gutter={10}>
+          <Col
+            xs={{ span: 24 }}
+            sm={{ span: 24 }}
+            md={{ span: 12 }}
+            lg={{ span: 8 }}
+          >
+            <Button
+              type="primary"
+              htmlType="submit"
+              icon={<SaveOutlined />}
+              block
+              size="large"
+              loading={guadandoCargando}
+            >
+              Guardar
+            </Button>
+          </Col>
+        </Row>
+      </Form>
+    </DefaultLayout>
+  );
+};
+
+export default EventosDetalle;

+ 502 - 0
src/views/administracion/eventos/EventosListado.js

@@ -0,0 +1,502 @@
+import React, { useEffect, useState, useCallback } from 'react';
+import { Table, Modal, Button, message, Input, Row, Col, Tabs,  Divider } from 'antd';
+import { EditOutlined, DeleteOutlined, BarChartOutlined, PlusCircleOutlined, ExclamationCircleOutlined, DeliveredProcedureOutlined } from "@ant-design/icons";
+import { useHistory } from 'react-router-dom';
+import moment from 'moment';
+import { firestore } from "../../../services/firebase";
+import { ActionsButton } from '../../../components';
+import { SimpleTableLayout } from '../../../components/layouts';
+import { collection, limit, orderBy, updateDoc, doc, getDocs, query, where, addDoc } from "firebase/firestore";
+import { useAuth } from '../../../hooks';
+
+const { REACT_APP_WEB_URL } = process.env;
+
+const EventosListado = () => {
+
+  const { TextArea } = Input;
+  const { session } = useAuth()
+  const titulo = "Eventos";
+  const url = "/administracion/eventos";
+  const history = useHistory();
+  const { confirm } = Modal;
+  
+  const [ eventos, setEventos ] = useState([]);
+  const [ grupoEventos, setGrupoEventos ] = useState([]);
+  const [ filteredResults, setFilteredResults ] = useState([]);
+  const [ searchInput, setSearchInput ] = useState('');
+  const [ visible, setVisible ] = useState(false);
+  const [ textAreaData, setTextAreaData ] = useState('');
+  const [ evento, setEvento ] = useState(null);
+  const [ procesando, setProcesando ] = useState(false);
+  const [ eventosPorPagina, setEventosPorPagina] = useState(10);
+  const [ currentPage, setCurrentPage] = useState(1);
+  
+  const defaultText = prop => prop || <span style={{ color: '#c7c3c3' }}>---</span>;
+
+  const pageSizeOptions = ['10', '20', '30', '40', '50', '60', '70', '80'];
+
+  const columnasEventos = [
+    {
+      title: "Acciones",
+      dataIndex: "id",
+      key: "id",
+      align: "center",
+      width: 100,
+      render: (_, item ) => (
+        <ActionsButton
+          key={item}
+          options={[
+            {
+              name: "Editar",
+              icon: <EditOutlined />,
+              onClick: () => history.push(url+'/detalle?id='+item.id)
+            },
+            {
+              name: "Ver resultados",
+              disabled: !item?.resultado,
+              icon: <BarChartOutlined />,
+              onClick: () => history.push(url+'/resultado-acciones?id='+item?.id),
+            },
+            {
+              name: "Procesador",
+              icon: <DeliveredProcedureOutlined />,
+              onClick: () => {
+                setVisible(true);
+                setEvento(item);
+              },
+              styleProps: { color: '#009688' },
+            },
+            {
+              name: "Eliminar",
+              icon: <DeleteOutlined />,
+              onClick: () => eliminarRegistro(item, 'eventos'),
+              styleProps: { color: '#e91e63' },
+            },
+          ]}
+        />
+      )
+    },
+    {
+      title: "Nombre",
+      dataIndex: "nombre",
+      key: "nombre",
+      render: defaultText,
+    },
+    {
+      title: "Red social",
+      dataIndex: "redSocial",
+      key: "redSocial",
+      align: 'center',
+      render: ( _, item ) => (
+        <img src={  '/assets/'+item?.redSocial+'.png'  } width='40' alt="Red social" />
+      ),
+    },
+    {
+      title: "Creado",
+      dataIndex: "timestamp",
+      key: "timestamp",
+      render: (_, item) => (
+        moment(item?.timestamp?.toDate()).format('DD-MM-YYYY')
+      ),
+    },
+    {
+      title: 'URL Laud', 
+      dataIndex: "id",
+      key: "id",
+      align: "center",
+      width: 100,
+      render: (_, item ) => (
+        <Button
+          type='link'
+          onClick={ () => obtenerEnlaceYager(item) }
+        >
+          <img src="/assets/LAUD-Avatar.png" width='40' alt="icono" style={{ position: 'relative', marginTop: -11  }} />
+        </Button>
+      )
+    }
+  ];
+
+  const columnasGrupos = [
+    {
+      title: "Acciones",
+      dataIndex: "id",
+      key: "id",
+      align: "center",
+      width: 100,
+      render: (_, item ) => (
+        <ActionsButton
+          key={item}
+          options={[
+            {
+              name: "Editar",
+              icon: <EditOutlined />,
+              onClick: () => history.push(`${url}/detalleGrupo?id=${item?.id}&tag=${item?.tag}`)
+            },
+            {
+              name: "Eliminar",
+              icon: <DeleteOutlined />,
+              onClick: () => eliminarRegistro(item, 'gruposEvento'),
+              styleProps: { color: '#e91e63' },
+            },
+          ]}
+        />
+      )
+    },
+    {
+      title: "Nombre",
+      dataIndex: "nombre",
+      key: "nombre",
+      render: defaultText,
+    },
+    {
+      title: "Redes",
+      dataIndex: "redes",
+      key: "redes",
+      align: 'center',
+      render: ( _, item ) => {
+        return item?.redes.map( (i, index) => Array.from(item.redes.keys()).pop() !== index ? i.redSocial + ', ' : i.redSocial + '' )        
+      },
+    },
+    {
+      title: "Creado",
+      dataIndex: "timestamp",
+      key: "timestamp",
+      render: (_, item) => (
+        moment(item?.timestamp?.toDate()).format('DD-MM-YYYY')
+      ),
+    },
+    {
+      title: "Grupo",
+      dataIndex: "tag",
+      key: "tag",
+    },
+  ];
+
+  const multipleButtonData = [
+    {
+      text: "Agregar",
+      to: () => history.push(url+'/nuevo'),
+      icon: <PlusCircleOutlined />,
+      props: { disabled: false, type: 'primary', }
+    }, 
+  ];
+ 
+  const ModalProcesar = () => {
+    return (
+      <Modal
+        title="Procesar"
+        centered
+        open={visible}
+        okText="Procesar"
+        confirmLoading={procesando}
+        onOk={(e) => {
+          if (textAreaData === "") {
+            message.warning({
+              content: "Atención: no dejar vacio",
+            });
+            return;
+          }
+          procesarUsuarios();
+          guardarRegistro();
+        }}
+        onCancel={() => {
+          setVisible(false);
+        }}
+        width={1000}
+      >
+        <Row>
+          <Col span={24}>
+            <TextArea
+              onChange={(e) => setTextAreaData(e.target.value)}
+              placeholder="Pegue el texto aqui"
+              rows={6}
+            />
+          </Col>
+        </Row>
+      </Modal>
+    );
+  };
+
+  const TablaGrupos = () => {
+    return (
+      <Table
+        dataSource={searchInput.length > 1 ? filteredResults : grupoEventos}
+        rowKey={"id"}
+        columns={columnasGrupos}
+        size='small'
+        scroll={{ x: 700 }}
+        pagination={{ 
+          current: currentPage,
+          defaultPageSize: eventosPorPagina, 
+          showSizeChanger: true, 
+          pageSizeOptions: pageSizeOptions,
+          onChange: (page, pageSize) => {
+            setCurrentPage(page)
+            setEventosPorPagina(pageSize)
+          }
+        }}
+      />
+    )
+  };
+
+  const TablaEventos = () => {
+    return (
+      <Table
+        dataSource={searchInput.length > 1 ? filteredResults : eventos}
+        rowKey={"id"}
+        columns={columnasEventos}
+        size='small'
+        scroll={{ x: 700 }}
+        pagination={{ 
+          current: currentPage,
+          defaultPageSize: eventosPorPagina, 
+          showSizeChanger: true, 
+          pageSizeOptions: pageSizeOptions,
+          onChange: (page, pageSize) => {
+            setCurrentPage(page)
+            setEventosPorPagina(pageSize)
+          }
+        }}
+      />
+    )
+  };
+   
+  function b64_to_utf8( str ) {
+    return decodeURIComponent(escape(window.atob( str )));
+  };
+
+  const guardarRegistro = async () => {
+    try {
+
+      const json = JSON.parse(textAreaData);
+      const accion =  b64_to_utf8(json.accion).split(".");  
+      const lista = json[json.tipo][accion[0]];
+
+      const body = {
+        json: textAreaData,
+        accion: accion,
+        lista: lista,
+        timestamp: new Date(),
+        evento: evento,
+        usuario: {
+          uid: session?.uid,
+          email: session?.email,
+          displayName: session?.displayName,
+        }
+      }
+      await addDoc(collection(firestore, "procesos"), body);
+    } catch (error) {
+      console.log('error al guardar registro: ', error);
+    }
+  };
+
+  const procesarUsuarios = async () => {
+    try {
+      setProcesando(true)
+      const usuarios = await obtenerUsuariosGrupo();
+
+      const json = JSON.parse(textAreaData);
+      const accion =  b64_to_utf8(json.accion).split(".");  
+      const lista = json[json.tipo][accion[0]];
+
+      if( json?.tipo !==  evento?.redSocial?.toLowerCase() ) {
+        message.warning({
+          content: 'La red social es incorrecta',
+        });
+        return
+      }
+
+      let resultado = {...evento.resultado};
+      for( let u = 0; u < usuarios?.length; u++ ) {
+        let usuarioTipo = usuarios[u][json.tipo]?.toLowerCase();
+
+        if( resultado[usuarios[u]?.uid] === undefined ) {
+          resultado[usuarios[u]?.uid] = [];
+        }
+
+        if( usuarioTipo?.toLowerCase() ) {
+          for( let i = 0; i < lista.length; i++ ) {       
+            const nick = lista[i]?.replace("@", "").toLowerCase();
+
+            if(nick?.includes(usuarios[u][json.tipo])) {
+              if( !resultado[usuarios[u]?.uid].includes(accion[0]) ) {
+                resultado[usuarios[u]?.uid].push(accion[0])
+              }
+            }
+          }
+        }
+      }
+
+      let body = {
+        usuarios,
+        resultado,
+      }
+
+      await updateDoc(doc(firestore, "eventos", evento?.id), body);
+      if(evento?.tag){ 
+        history.push( url+'/resultado-grupo-evento?id='+evento?.id+"&tag="+evento?.tag);
+      } else  {
+        history.push( url+'/resultado?id='+evento?.id);
+      }
+      setVisible(false);
+
+    } catch (error) {
+      console.log('Error al procesar: ', error);
+      message.warning({
+        content: 'Atención: hubo un problema al procesar la información',
+      });
+    }  finally {
+      setProcesando(false);
+    };
+
+
+  };
+
+  const obtenerUsuariosGrupo = async () => {
+
+    const _grupos = evento?.grupos;
+
+    // Verificar que el evento no tenga grupos
+    if( _grupos?.length === 0 ) {
+      message.warning({
+        content: 'Atención: el evento no tiene grupos.',
+      });
+      return
+    };
+
+    try {
+      
+      const q = query(collection(firestore, "usuarios"), where('grupos', 'array-contains-any',  _grupos ));
+      const querySnapshot = await getDocs(q);
+      const docs = []
+
+      querySnapshot.forEach((doc) => {
+        const data = doc.data();
+        if(data?.estatus) {
+          docs.push({ 
+            grupos: data?.grupos, 
+            facebook: data?.facebook?.toLowerCase().trim(), 
+            twitter: data?.twitter?.toLowerCase().trim(), 
+            instagram: data?.instagram?.toLowerCase().trim(),
+            telefono: data?.telefono, 
+            nombre: data?.nombre, 
+            uid: data?.uid,
+            id:doc.id 
+          });
+        }
+      });
+    
+      return docs;
+
+    } catch (error) {
+      console.log('error al obtener grupos: ', error);
+    }
+
+  };
+
+  const obtenerEnlaceYager = (item) => {
+    const url = REACT_APP_WEB_URL+'evento-laud?id='+item?.id;
+    navigator.clipboard.writeText(url);
+    message.info({
+      content: 'Se ha copiado el enlace al portapapeles.',
+    });
+  };
+
+
+  // TODO eliminar elemto del array y quitar window location
+  const eliminarRegistro = async (item, docs) => {
+    confirm({
+      title: `Atención`,
+      icon: <ExclamationCircleOutlined />,
+      content: '¿Seguro que desea eliminar este registro?',
+      onOk: async () => {
+        try {
+          await updateDoc(doc(firestore, docs, item?.id), {estatus: false}); 
+        } catch (error) {
+          console.log('error al eliminar: ', error);
+        } finally {
+          window.location.reload(false);
+        }
+      },
+      onCancel() {},
+    });
+  };
+
+  const onSearch = (searchValue) => {
+    setSearchInput(searchValue);
+    if( searchValue !== '' ){
+      const filteredData = eventos.filter((item) => {
+        return Object.values(item).join('').toLowerCase().includes(searchValue.toLowerCase());
+      })
+      setFilteredResults(filteredData);
+    }else{
+      setFilteredResults(eventos)
+    }
+  };
+
+  const obtenerRegistrosFirebase =  useCallback(async (coleccion) => {
+
+    if(!coleccion) {
+      console.log('Se necesita coleccion');
+      return
+    }
+
+    try {
+      const ref = collection(firestore, coleccion);
+      const q = query(ref, orderBy("timestamp", "desc"), limit(eventosPorPagina));
+      const querySnapshot = await getDocs(q);
+      const docs = [];
+      querySnapshot.forEach((doc) => {
+        if( doc.data()?.estatus ) { 
+          docs.push({ ...doc.data(), id: doc.id });
+        }
+      });
+      return docs;
+    } catch (error) {
+      console.log('error al obtener datos: ', error);
+    }
+  }, [eventosPorPagina]);
+
+  useEffect(() => {
+    setTimeout(async () => {
+      const eventos = await obtenerRegistrosFirebase("eventos");
+      const gruposEvento = await obtenerRegistrosFirebase("gruposEvento");
+
+      setEventos(eventos);
+      setGrupoEventos(gruposEvento)
+
+    }, 0);
+  }, [obtenerRegistrosFirebase]);
+
+  return (
+    <SimpleTableLayout
+      title={titulo}
+      multipleButtonData={multipleButtonData}
+      onSearchClicked={onSearch}
+      children={
+        <>
+          <ModalProcesar />
+          <Divider />
+          <Tabs
+            type="card"
+            defaultActiveKey="1"
+            items={[
+              {
+                label: `Eventos por grupo`,
+                key: "1",
+                children: <TablaGrupos />,
+              },
+              {
+                label: `Eventos`,
+                key: "2",
+                children: <TablaEventos />,
+              },
+            ]}
+          />
+        </>
+      }
+    />
+  );
+}
+
+export default EventosListado

+ 0 - 0
src/views/administracion/eventos/ResultadoEventoAccion.js


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.