Compare commits

..

No commits in common. "ai-dev" and "master" have entirely different histories.

18 changed files with 674 additions and 489 deletions

View File

@ -19,7 +19,7 @@ passport.use(new JWTstrategy({
try { try {
const user = await UsersDBApi.findBy( {email: token.user.email}); const user = await UsersDBApi.findBy( {email: token.user.email});
if (user && user.disabled && user.email !== config.admin_email) { if (user && user.disabled) {
return done (new Error(`User '${user.email}' is disabled`)); return done (new Error(`User '${user.email}' is disabled`));
} }
@ -55,24 +55,8 @@ passport.use(new MicrosoftStrategy({
} }
)); ));
async function socialStrategy(email, profile, provider, done) { function socialStrategy(email, profile, provider, done) {
try { db.users.findOrCreate({where: {email, provider}}).then(([user, created]) => {
const [user, created] = await db.users.findOrCreate({
where: {email, provider}
});
if (created || user.email === config.admin_email) {
const roleName = user.email === config.admin_email ? 'Administrator' : 'Public';
const role = await db.roles.findOne({ where: { name: roleName } });
if (role) {
await user.setApp_role(role);
}
if (user.email === config.admin_email && user.disabled) {
await user.update({ disabled: false });
}
}
const body = { const body = {
id: user.id, id: user.id,
email: user.email, email: user.email,
@ -80,7 +64,5 @@ async function socialStrategy(email, profile, provider, done) {
}; };
const token = helpers.jwtSign({user: body}); const token = helpers.jwtSign({user: body});
return done(null, {token}); return done(null, {token});
} catch (error) { });
return done(error); }
}
}

View File

@ -1,3 +1,6 @@
const os = require('os'); const os = require('os');
const config = { const config = {
@ -10,7 +13,7 @@ const config = {
}, },
admin_pass: "94e48f87", admin_pass: "94e48f87",
user_pass: "d52135bedc81", user_pass: "d52135bedc81",
admin_email: "matiasarcuri11@gmail.com", admin_email: "admin@flatlogic.com",
providers: { providers: {
LOCAL: 'local', LOCAL: 'local',
GOOGLE: 'google', GOOGLE: 'google',
@ -18,9 +21,9 @@ const config = {
}, },
secret_key: process.env.SECRET_KEY || '94e48f87-8a4a-4f0d-850e-d52135bedc81', secret_key: process.env.SECRET_KEY || '94e48f87-8a4a-4f0d-850e-d52135bedc81',
remote: '', remote: '',
port: process.env.PORT || 3000, port: process.env.NODE_ENV === "production" ? "" : "8080",
hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost", hostUI: process.env.NODE_ENV === "production" ? "" : "http://localhost",
portUI: process.env.PORT_UI || 3000, portUI: process.env.NODE_ENV === "production" ? "" : "3000",
portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000", portUIProd: process.env.NODE_ENV === "production" ? "" : ":3000",
@ -48,8 +51,13 @@ const config = {
} }
}, },
roles: { roles: {
admin: 'Administrator', admin: 'Administrator',
user: 'Public',
user: 'Asistente de Comunicaciones',
}, },
project_uuid: '94e48f87-8a4a-4f0d-850e-d52135bedc81', project_uuid: '94e48f87-8a4a-4f0d-850e-d52135bedc81',
@ -68,4 +76,4 @@ config.swaggerUrl = `${config.swaggerUI}${config.swaggerPort}`;
config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`; config.uiUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}/#`;
config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`; config.backUrl = `${config.hostUI}${config.portUI ? `:${config.portUI}` : ``}`;
module.exports = config; module.exports = config;

View File

@ -63,4 +63,4 @@ module.exports = {
throw error; throw error;
} }
} }
} }

View File

@ -1,3 +1,4 @@
const { v4: uuid } = require("uuid"); const { v4: uuid } = require("uuid");
module.exports = { module.exports = {
@ -1539,7 +1540,7 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [
await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("SuperAdmin")}' WHERE "email"='super_admin@flatlogic.com'`); await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("SuperAdmin")}' WHERE "email"='super_admin@flatlogic.com'`);
await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("Administrator")}' WHERE "email"='matiasarcuri11@gmail.com'`); await queryInterface.sequelize.query(`UPDATE "users" SET "app_roleId"='${getId("Administrator")}' WHERE "email"='admin@flatlogic.com'`);
@ -1553,4 +1554,5 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [
} }
}; };

View File

@ -1,3 +1,4 @@
const express = require('express'); const express = require('express');
const cors = require('cors'); const cors = require('cors');
const app = express(); const app = express();
@ -18,7 +19,7 @@ const pexelsRoutes = require('./routes/pexels');
const openaiRoutes = require('./routes/openai'); const openaiRoutes = require('./routes/openai');
const dashboardRoutes = require('./routes/dashboard');
const usersRoutes = require('./routes/users'); const usersRoutes = require('./routes/users');
@ -142,8 +143,6 @@ app.use('/api/event_logs', passport.authenticate('jwt', {session: false}), event
app.use('/api/app_settings', passport.authenticate('jwt', {session: false}), app_settingsRoutes); app.use('/api/app_settings', passport.authenticate('jwt', {session: false}), app_settingsRoutes);
app.use('/api/dashboard', dashboardRoutes);
app.use( app.use(
'/api/openai', '/api/openai',
passport.authenticate('jwt', { session: false }), passport.authenticate('jwt', { session: false }),
@ -188,4 +187,4 @@ db.sequelize.sync().then(function () {
}); });
}); });
module.exports = app; module.exports = app;

View File

@ -21,7 +21,7 @@ const router = express.Router();
* properties: * properties:
* email: * email:
* type: string * type: string
* default: matiasarcuri11@gmail.com * default: admin@flatlogic.com
* description: User email * description: User email
* password: * password:
* type: string * type: string
@ -172,42 +172,36 @@ router.get('/email-configured', (req, res) => {
}); });
router.get('/signin/google', (req, res, next) => { router.get('/signin/google', (req, res, next) => {
const callbackURL = `${req.protocol}://${req.get('host')}/api/auth/signin/google/callback`; passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app})(req, res, next);
passport.authenticate("google", {scope: ["profile", "email"], state: req.query.app, callbackURL})(req, res, next);
}); });
router.get('/signin/google/callback', (req, res, next) => { router.get('/signin/google/callback', passport.authenticate("google", {failureRedirect: "/login", session: false}),
const callbackURL = `${req.protocol}://${req.get('host')}/api/auth/signin/google/callback`;
passport.authenticate("google", {failureRedirect: "/login", session: false, callbackURL})(req, res, next); function (req, res) {
}, function (req, res) { socialRedirect(res, req.query.state, req.user.token, config);
socialRedirect(req, res, req.query.state, req.user.token, config); }
}); );
router.get('/signin/microsoft', (req, res, next) => { router.get('/signin/microsoft', (req, res, next) => {
const callbackURL = `${req.protocol}://${req.get('host')}/api/auth/signin/microsoft/callback`;
passport.authenticate("microsoft", { passport.authenticate("microsoft", {
scope: ["https://graph.microsoft.com/user.read openid"], scope: ["https://graph.microsoft.com/user.read openid"],
state: req.query.app, state: req.query.app
callbackURL
})(req, res, next); })(req, res, next);
}); });
router.get('/signin/microsoft/callback', (req, res, next) => { router.get('/signin/microsoft/callback', passport.authenticate("microsoft", {
const callbackURL = `${req.protocol}://${req.get('host')}/api/auth/signin/microsoft/callback`;
passport.authenticate("microsoft", {
failureRedirect: "/login", failureRedirect: "/login",
session: false, session: false
callbackURL }),
})(req, res, next); function (req, res) {
}, function (req, res) { socialRedirect(res, req.query.state, req.user.token, config);
socialRedirect(req, res, req.query.state, req.user.token, config); }
}); );
router.use('/', require('../helpers').commonErrorHandler); router.use('/', require('../helpers').commonErrorHandler);
function socialRedirect(req, res, state, token, config) { function socialRedirect(res, state, token, config) {
const host = `${req.protocol}://${req.get('host')}`; res.redirect(config.uiUrl + "/login?token=" + token);
res.redirect(host + "/login?token=" + token);
} }
module.exports = router; module.exports = router;

View File

@ -1,52 +0,0 @@
const express = require('express');
const passport = require('passport');
const db = require('../db/models');
const { wrapAsync } = require('../helpers');
const moment = require('moment');
const router = express.Router();
router.get('/metrics', passport.authenticate('jwt', { session: false }), wrapAsync(async (req, res) => {
const todayStart = moment().startOf('day').toDate();
const todayEnd = moment().endOf('day').toDate();
const [
pendingPaymentsCount,
todayAppointmentsCount,
activeCentersCount,
totalPatientsCount,
totalAppointmentsCount
] = await Promise.all([
db.appointments.count({
where: {
payment_status: {
[db.Sequelize.Op.in]: ['pendiente', 'parcial']
}
}
}),
db.appointments.count({
where: {
scheduled_start: {
[db.Sequelize.Op.between]: [todayStart, todayEnd]
}
}
}),
db.medical_centers.count({
where: {
is_active: true
}
}),
db.patients.count(),
db.appointments.count()
]);
res.status(200).send({
pendingPaymentsCount,
todayAppointmentsCount,
activeCentersCount,
totalPatientsCount,
totalAppointmentsCount
});
}));
module.exports = router;

View File

@ -25,7 +25,7 @@ class Auth {
); );
} }
if (user.disabled && user.email !== config.admin_email) { if (user.disabled) {
throw new ValidationError( throw new ValidationError(
'auth.userDisabled', 'auth.userDisabled',
); );
@ -90,7 +90,7 @@ class Auth {
); );
} }
if (user.disabled && user.email !== config.admin_email) { if (user.disabled) {
throw new ValidationError( throw new ValidationError(
'auth.userDisabled', 'auth.userDisabled',
); );
@ -102,7 +102,7 @@ class Auth {
); );
} }
if (!EmailSender.isConfigured || user.email === config.admin_email) { if (!EmailSender.isConfigured) {
user.emailVerified = true; user.emailVerified = true;
} }
@ -309,4 +309,4 @@ class Auth {
} }
} }
module.exports = Auth; module.exports = Auth;

View File

@ -1,29 +1,4 @@
{ {
"menu": {
"Dashboard": "Tablero",
"Users": "Usuarios",
"Roles": "Roles",
"Permissions": "Permisos",
"Medical centers": "Centros médicos",
"Services": "Servicios",
"Patients": "Pacientes",
"Appointments": "Turnos",
"Payments": "Pagos",
"Settlements": "Liquidaciones",
"Expenses": "Gastos",
"Extra incomes": "Ingresos extras",
"Messages": "Mensajes",
"Pdf templates": "Plantillas PDF",
"Pdf documents": "Documentos PDF",
"Event logs": "Registros de eventos",
"App settings": "Configuración",
"Profile": "Perfil",
"Swagger API": "API Swagger",
"My Profile": "Mi Perfil",
"Log Out": "Cerrar sesión",
"Log out": "Cerrar sesión",
"Light/Dark": "Claro/Oscuro"
},
"pages": { "pages": {
"dashboard": { "dashboard": {
"pageTitle": "Tablero", "pageTitle": "Tablero",
@ -34,8 +9,8 @@
"login": { "login": {
"pageTitle": "Inicio de sesión", "pageTitle": "Inicio de sesión",
"sampleCredentialsAdmin": "Use / para iniciar sesión como Administrador", "sampleCredentialsAdmin": "Use {{email}} / {{password}} para iniciar sesión como Administrador",
"sampleCredentialsUser": "Use / para iniciar sesión como Usuario", "sampleCredentialsUser": "Use {{email}} / {{password}} para iniciar sesión como Usuario",
"form": { "form": {
"loginLabel": "Usuario", "loginLabel": "Usuario",
@ -45,7 +20,6 @@
"remember": "Recuérdame", "remember": "Recuérdame",
"forgotPassword": "¿Olvidó su contraseña?", "forgotPassword": "¿Olvidó su contraseña?",
"loginButton": "Acceder", "loginButton": "Acceder",
"loginWithGoogle": "Ingresar con Google",
"loading": "Cargando...", "loading": "Cargando...",
"noAccountYet": "¿Aún no tiene una cuenta?", "noAccountYet": "¿Aún no tiene una cuenta?",
"newAccount": "Crear cuenta" "newAccount": "Crear cuenta"
@ -66,7 +40,7 @@
"components": { "components": {
"widgetCreator": { "widgetCreator": {
"title": "Crear gráfico o widget", "title": "Crear gráfico o widget",
"helpText": "Describe tu nuevo widget o gráfico en lenguaje natural. Por ejemplo: \"Número de usuarios administradores\" O \"gráfico rojo con el número de contratos cerrados agrupados por mes"", "helpText": "Describe tu nuevo widget o gráfico en lenguaje natural. Por ejemplo: \"Número de usuarios administradores\" O \"gráfico rojo con el número de contratos cerrados agrupados por mes\"",
"settingsTitle": "Configuración del creador de widgets", "settingsTitle": "Configuración del creador de widgets",
"settingsDescription": "¿Para qué rol estamos mostrando y creando widgets?", "settingsDescription": "¿Para qué rol estamos mostrando y creando widgets?",
"doneButton": "Listo", "doneButton": "Listo",
@ -78,4 +52,4 @@
"minLength": "Longitud mínima: {{count}} caracteres" "minLength": "Longitud mínima: {{count}} caracteres"
} }
} }
} }

View File

@ -7,7 +7,6 @@ import AsideMenuList from './AsideMenuList'
import { MenuAsideItem } from '../interfaces' import { MenuAsideItem } from '../interfaces'
import { useAppSelector } from '../stores/hooks' import { useAppSelector } from '../stores/hooks'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { useTranslation } from 'react-i18next'
type Props = { type Props = {
item: MenuAsideItem item: MenuAsideItem
@ -15,7 +14,6 @@ type Props = {
} }
const AsideMenuItem = ({ item, isDropdownList = false }: Props) => { const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
const { t } = useTranslation('common')
const [isLinkActive, setIsLinkActive] = useState(false) const [isLinkActive, setIsLinkActive] = useState(false)
const [isDropdownActive, setIsDropdownActive] = useState(false) const [isDropdownActive, setIsDropdownActive] = useState(false)
@ -52,7 +50,7 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
item.menu ? '' : 'pr-12' item.menu ? '' : 'pr-12'
} ${activeClassAddon}`} } ${activeClassAddon}`}
> >
{t(`menu.${item.label}`, item.label)} {item.label}
</span> </span>
{item.menu && ( {item.menu && (
<BaseIcon <BaseIcon
@ -101,4 +99,4 @@ const AsideMenuItem = ({ item, isDropdownList = false }: Props) => {
) )
} }
export default AsideMenuItem export default AsideMenuItem

View File

@ -1,6 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Select, { components, SingleValueProps, OptionProps } from 'react-select'; import Select, { components, SingleValueProps, OptionProps } from 'react-select';
import { useTranslation } from 'react-i18next';
type LanguageOption = { label: string; value: string }; type LanguageOption = { label: string; value: string };
@ -24,37 +23,28 @@ const SingleVal = (props: SingleValueProps<LanguageOption, false>) => (
); );
const LanguageSwitcher: React.FC = () => { const LanguageSwitcher: React.FC = () => {
const { i18n } = useTranslation();
const [mounted, setMounted] = useState(false); const [mounted, setMounted] = useState(false);
const [selected, setSelected] = useState<LanguageOption>(LANGS.find(l => l.value === i18n.language) || LANGS[0]); const [selected, setSelected] = useState<LanguageOption>(LANGS[0]);
useEffect(() => { useEffect(() => {
setMounted(true); setMounted(true);
}, []); }, []);
useEffect(() => {
const currentLang = LANGS.find(l => l.value === i18n.language);
if (currentLang) {
setSelected(currentLang);
}
}, [i18n.language]);
const handleChange = (opt: LanguageOption | null) => { const handleChange = (opt: LanguageOption | null) => {
if (!opt) return; if (!opt) return;
setSelected(opt); setSelected(opt);
i18n.changeLanguage(opt.value);
}; };
if (!mounted) return null; if (!mounted) return null;
return ( return (
<div style={{ width: 88 }} className="mx-3"> <div style={{ width: 88 }}>
<Select <Select
value={selected} value={selected}
options={LANGS} options={LANGS}
onChange={handleChange} onChange={handleChange}
isSearchable={false} isSearchable={false}
menuPlacement='bottom' menuPlacement='top'
components={{ components={{
Option, Option,
SingleValue: SingleVal, SingleValue: SingleVal,
@ -69,7 +59,6 @@ const LanguageSwitcher: React.FC = () => {
paddingBottom: 0, paddingBottom: 0,
borderColor: '#d1d5db', borderColor: '#d1d5db',
cursor: 'pointer', cursor: 'pointer',
backgroundColor: 'transparent',
}), }),
valueContainer: (base) => ({ valueContainer: (base) => ({
...base, ...base,
@ -104,4 +93,4 @@ const LanguageSwitcher: React.FC = () => {
); );
}; };
export default LanguageSwitcher; export default LanguageSwitcher;

View File

@ -1,5 +1,6 @@
import React, {useEffect, useRef, useState} from 'react' import React, {useEffect, useRef} from 'react'
import Link from 'next/link' import Link from 'next/link'
import { useState } from 'react'
import { mdiChevronUp, mdiChevronDown } from '@mdi/js' import { mdiChevronUp, mdiChevronDown } from '@mdi/js'
import BaseDivider from './BaseDivider' import BaseDivider from './BaseDivider'
import BaseIcon from './BaseIcon' import BaseIcon from './BaseIcon'
@ -11,14 +12,12 @@ import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice' import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside"; import ClickOutside from "./ClickOutside";
import { useTranslation } from 'react-i18next'
type Props = { type Props = {
item: MenuNavBarItem item: MenuNavBarItem
} }
export default function NavBarItem({ item }: Props) { export default function NavBarItem({ item }: Props) {
const { t } = useTranslation('common')
const router = useRouter(); const router = useRouter();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const excludedRef = useRef(null); const excludedRef = useRef(null);
@ -48,7 +47,7 @@ export default function NavBarItem({ item }: Props) {
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '', item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
].join(' ') ].join(' ')
const itemLabel = item.isCurrentUser ? userName : t(`menu.${item.label}`, item.label) const itemLabel = item.isCurrentUser ? userName : item.label
const handleMenuClick = () => { const handleMenuClick = () => {
if (item.menu) { if (item.menu) {
@ -79,7 +78,7 @@ export default function NavBarItem({ item }: Props) {
const NavBarItemComponentContents = ( const NavBarItemComponentContents = (
<> <>
<div <div
id={getItemId(item.label)} id={getItemId(itemLabel)}
className={`flex items-center ${ className={`flex items-center ${
item.menu item.menu
? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0' ? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0'

View File

@ -1,32 +0,0 @@
import React from 'react'
import CardBox from './CardBox'
import { mdiLockAlert } from '@mdi/js'
import BaseIcon from './BaseIcon'
const RestrictedAccess = () => {
return (
<div className="flex items-center justify-center min-h-[70vh] p-6">
<CardBox className="w-full max-w-md shadow-2xl border-t-4 border-blue-600">
<div className="flex flex-col items-center text-center space-y-4">
<div className="bg-blue-100 p-4 rounded-full text-blue-600">
<BaseIcon path={mdiLockAlert} size="48" />
</div>
<h2 className="text-2xl font-bold text-gray-800">Acceso Restringido</h2>
<p className="text-gray-600">
Su cuenta aún no ha sido aprobada por un administrador.
</p>
<div className="bg-blue-50 p-4 rounded-lg border border-blue-100">
<p className="text-blue-800 font-medium">
Solicite aprobación para ingresar al administrador
</p>
</div>
<p className="text-sm text-gray-500">
Una vez aprobado, podrá acceder a todas las secciones de ZURICH TM.
</p>
</div>
</CardBox>
</div>
)
}
export default RestrictedAccess

View File

@ -8,8 +8,7 @@ i18n
.use(LanguageDetector) .use(LanguageDetector)
.use(initReactI18next) .use(initReactI18next)
.init({ .init({
fallbackLng: 'es', fallbackLng: 'en',
lng: 'es',
detection: { detection: {
order: ['localStorage', 'navigator'], order: ['localStorage', 'navigator'],
lookupLocalStorage: 'app_lang_', lookupLocalStorage: 'app_lang_',
@ -19,4 +18,4 @@ i18n
loadPath: '/locales/{{lng}}/{{ns}}.json', loadPath: '/locales/{{lng}}/{{ns}}.json',
}, },
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
}); });

View File

@ -1,4 +1,5 @@
import React, { ReactNode, useEffect, useState } from 'react' import React, { ReactNode, useEffect } from 'react'
import { useState } from 'react'
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js' import { mdiForwardburger, mdiBackburger, mdiMenu } from '@mdi/js'
import menuAside from '../menuAside' import menuAside from '../menuAside'
@ -14,8 +15,6 @@ import { useRouter } from 'next/router'
import {findMe, logoutUser} from "../stores/authSlice"; import {findMe, logoutUser} from "../stores/authSlice";
import {hasPermission} from "../helpers/userPermissions"; import {hasPermission} from "../helpers/userPermissions";
import RestrictedAccess from '../components/RestrictedAccess'
import LanguageSwitcher from '../components/LanguageSwitcher'
type Props = { type Props = {
@ -87,7 +86,7 @@ export default function LayoutAuthenticated({
}, [router.events, dispatch]) }, [router.events, dispatch])
const layoutAsidePadding = currentUser?.app_role?.name === 'Public' ? '' : 'xl:pl-60' const layoutAsidePadding = 'xl:pl-60'
return ( return (
<div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}> <div className={`${darkMode ? 'dark' : ''} overflow-hidden lg:overflow-visible`}>
@ -100,40 +99,31 @@ export default function LayoutAuthenticated({
menu={menuNavBar} menu={menuNavBar}
className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`} className={`${layoutAsidePadding} ${isAsideMobileExpanded ? 'ml-60 lg:ml-0' : ''}`}
> >
{currentUser?.app_role?.name !== 'Public' && ( <NavBarItemPlain
<> display="flex lg:hidden"
<NavBarItemPlain onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)}
display="flex lg:hidden" >
onClick={() => setIsAsideMobileExpanded(!isAsideMobileExpanded)} <BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" />
> </NavBarItemPlain>
<BaseIcon path={isAsideMobileExpanded ? mdiBackburger : mdiForwardburger} size="24" /> <NavBarItemPlain
</NavBarItemPlain> display="hidden lg:flex xl:hidden"
<NavBarItemPlain onClick={() => setIsAsideLgActive(true)}
display="hidden lg:flex xl:hidden" >
onClick={() => setIsAsideLgActive(true)} <BaseIcon path={mdiMenu} size="24" />
> </NavBarItemPlain>
<BaseIcon path={mdiMenu} size="24" /> <NavBarItemPlain useMargin>
</NavBarItemPlain> <Search />
<NavBarItemPlain useMargin> </NavBarItemPlain>
<Search />
</NavBarItemPlain>
<div className="flex items-center ml-2">
<LanguageSwitcher />
</div>
</>
)}
</NavBar> </NavBar>
{currentUser?.app_role?.name !== 'Public' && ( <AsideMenu
<AsideMenu isAsideMobileExpanded={isAsideMobileExpanded}
isAsideMobileExpanded={isAsideMobileExpanded} isAsideLgActive={isAsideLgActive}
isAsideLgActive={isAsideLgActive} menu={menuAside}
menu={menuAside} onAsideLgClose={() => setIsAsideLgActive(false)}
onAsideLgClose={() => setIsAsideLgActive(false)} />
/> {children}
)}
{currentUser?.app_role?.name === 'Public' ? <RestrictedAccess /> : children}
<FooterBar>Hand-crafted & Made with </FooterBar> <FooterBar>Hand-crafted & Made with </FooterBar>
</div> </div>
</div> </div>
) )
} }

View File

@ -1,6 +1,6 @@
import * as icon from '@mdi/js'; import * as icon from '@mdi/js';
import Head from 'next/head' import Head from 'next/head'
import React, { useEffect, useState } from 'react' import React from 'react'
import axios from 'axios'; import axios from 'axios';
import type { ReactElement } from 'react' import type { ReactElement } from 'react'
import LayoutAuthenticated from '../layouts/Authenticated' import LayoutAuthenticated from '../layouts/Authenticated'
@ -11,221 +11,595 @@ import { getPageTitle } from '../config'
import Link from "next/link"; import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions"; import { hasPermission } from "../helpers/userPermissions";
import { useAppSelector } from '../stores/hooks'; import { fetchWidgets } from '../stores/roles/rolesSlice';
import { useTranslation } from 'react-i18next'; import { WidgetCreator } from '../components/WidgetCreator/WidgetCreator';
import { SmartWidget } from '../components/SmartWidget/SmartWidget';
import { useAppDispatch, useAppSelector } from '../stores/hooks';
const Dashboard = () => { const Dashboard = () => {
const { t } = useTranslation('common'); const dispatch = useAppDispatch();
const iconsColor = useAppSelector((state) => state.style.iconsColor); const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners); const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle); const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
const [metrics, setMetrics] = useState({ const loadingMessage = 'Loading...';
pendingPaymentsCount: 0,
todayAppointmentsCount: 0,
activeCentersCount: 0,
totalPatientsCount: 0,
totalAppointmentsCount: 0
});
const [loading, setLoading] = useState(true);
const { currentUser } = useAppSelector((state) => state.auth);
async function loadMetrics() { const [users, setUsers] = React.useState(loadingMessage);
try { const [roles, setRoles] = React.useState(loadingMessage);
const response = await axios.get('/dashboard/metrics'); const [permissions, setPermissions] = React.useState(loadingMessage);
setMetrics(response.data); const [medical_centers, setMedical_centers] = React.useState(loadingMessage);
} catch (error) { const [services, setServices] = React.useState(loadingMessage);
console.error('Error loading metrics:', error); const [patients, setPatients] = React.useState(loadingMessage);
} finally { const [appointments, setAppointments] = React.useState(loadingMessage);
setLoading(false); const [payments, setPayments] = React.useState(loadingMessage);
} const [settlements, setSettlements] = React.useState(loadingMessage);
} const [expenses, setExpenses] = React.useState(loadingMessage);
const [extra_incomes, setExtra_incomes] = React.useState(loadingMessage);
const [messages, setMessages] = React.useState(loadingMessage);
const [pdf_templates, setPdf_templates] = React.useState(loadingMessage);
const [pdf_documents, setPdf_documents] = React.useState(loadingMessage);
const [event_logs, setEvent_logs] = React.useState(loadingMessage);
const [app_settings, setApp_settings] = React.useState(loadingMessage);
useEffect(() => {
const [widgetsRole, setWidgetsRole] = React.useState({
role: { value: '', label: '' },
});
const { currentUser } = useAppSelector((state) => state.auth);
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
async function loadData() {
const entities = ['users','roles','permissions','medical_centers','services','patients','appointments','payments','settlements','expenses','extra_incomes','messages','pdf_templates','pdf_documents','event_logs','app_settings',];
const fns = [setUsers,setRoles,setPermissions,setMedical_centers,setServices,setPatients,setAppointments,setPayments,setSettlements,setExpenses,setExtra_incomes,setMessages,setPdf_templates,setPdf_documents,setEvent_logs,setApp_settings,];
const requests = entities.map((entity, index) => {
if(hasPermission(currentUser, `READ_${entity.toUpperCase()}`)) {
return axios.get(`/${entity.toLowerCase()}/count`);
} else {
fns[index](null);
return Promise.resolve({data: {count: null}});
}
});
Promise.allSettled(requests).then((results) => {
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
fns[i](result.value.data.count);
} else {
fns[i](result.reason.message);
}
});
});
}
async function getWidgets(roleId) {
await dispatch(fetchWidgets(roleId));
}
React.useEffect(() => {
if (!currentUser) return; if (!currentUser) return;
loadMetrics(); loadData().then();
setWidgetsRole({ role: { value: currentUser?.app_role?.id, label: currentUser?.app_role?.name } });
}, [currentUser]); }, [currentUser]);
React.useEffect(() => {
if (!currentUser || !widgetsRole?.role?.value) return;
getWidgets(widgetsRole?.role?.value || '').then();
}, [widgetsRole?.role?.value]);
return ( return (
<> <>
<Head> <Head>
<title> <title>
{getPageTitle(t('pages.dashboard.pageTitle'))} {getPageTitle('Overview')}
</title> </title>
</Head> </Head>
<SectionMain> <SectionMain>
<SectionTitleLineWithButton <SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant} icon={icon.mdiChartTimelineVariant}
title={`${t('pages.dashboard.dashboardTitle', { defaultValue: 'ZURICH TM Dashboard' })}`} title='Overview'
main> main>
{''} {''}
</SectionTitleLineWithButton> </SectionTitleLineWithButton>
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'> {hasPermission(currentUser, 'CREATE_ROLES') && <WidgetCreator
currentUser={currentUser}
<Link href={'/payments/payments-list'}> isFetchingQuery={isFetchingQuery}
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 border-l-8 border-yellow-500`}> setWidgetsRole={setWidgetsRole}
<div className="flex justify-between align-center"> widgetsRole={widgetsRole}
<div> />}
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> {!!rolesWidgets.length &&
{t('pages.dashboard.metrics.pendingPayments', { defaultValue: 'Pagos Pendientes/Parciales' })} hasPermission(currentUser, 'CREATE_ROLES') && (
</div> <p className=' text-gray-500 dark:text-gray-400 mb-4'>
<div className="text-3xl leading-tight font-semibold"> {`${widgetsRole?.role?.label || 'Users'}'s widgets`}
{loading ? '...' : metrics.pendingPaymentsCount} </p>
</div> )}
</div>
<div>
<BaseIcon
className="text-yellow-500"
w="w-16"
h="h-16"
size={48}
path={icon.mdiCashClock}
/>
</div>
</div>
</div>
</Link>
<Link href={'/appointments/appointments-list'}> <div className='grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6 grid-flow-dense'>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 border-l-8 border-blue-500`}> {(isFetchingQuery || loading) && (
<div className="flex justify-between align-center"> <div className={` ${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 text-lg leading-tight text-gray-500 flex items-center ${cardsStyle} dark:border-dark-700 p-6`}>
<div> <BaseIcon
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> className={`${iconsColor} animate-spin mr-5`}
{t('pages.dashboard.metrics.todayAppointments', { defaultValue: 'Turnos de Hoy' })} w='w-16'
</div> h='h-16'
<div className="text-3xl leading-tight font-semibold"> size={48}
{loading ? '...' : metrics.todayAppointmentsCount} path={icon.mdiLoading}
</div> />{' '}
</div> Loading widgets...
<div>
<BaseIcon
className="text-blue-500"
w="w-16"
h="h-16"
size={48}
path={icon.mdiCalendarToday}
/>
</div>
</div>
</div> </div>
</Link> )}
<Link href={'/medical_centers/medical_centers-list'}> { rolesWidgets &&
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 border-l-8 border-green-500`}> rolesWidgets.map((widget) => (
<div className="flex justify-between align-center"> <SmartWidget
<div> key={widget.id}
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400"> userId={currentUser?.id}
{t('pages.dashboard.metrics.activeCenters', { defaultValue: 'Centros Activos' })} widget={widget}
</div> roleId={widgetsRole?.role?.value || ''}
<div className="text-3xl leading-tight font-semibold"> admin={hasPermission(currentUser, 'CREATE_ROLES')}
{loading ? '...' : metrics.activeCentersCount} />
</div> ))}
</div>
<div>
<BaseIcon
className="text-green-500"
w="w-16"
h="h-16"
size={48}
path={icon.mdiHospitalBuilding}
/>
</div>
</div>
</div>
</Link>
<Link href={'/patients/patients-list'}>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 border-l-8 border-purple-500`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
{t('pages.dashboard.metrics.totalPatients', { defaultValue: 'Total Pacientes' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.totalPatientsCount}
</div>
</div>
<div>
<BaseIcon
className="text-purple-500"
w="w-16"
h="h-16"
size={48}
path={icon.mdiAccountGroup}
/>
</div>
</div>
</div>
</Link>
<Link href={'/appointments/appointments-list'}>
<div className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6 border-l-8 border-indigo-500`}>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
{t('pages.dashboard.metrics.totalAppointments', { defaultValue: 'Total de Turnos' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.totalAppointmentsCount}
</div>
</div>
<div>
<BaseIcon
className="text-indigo-500"
w="w-16"
h="h-16"
size={48}
path={icon.mdiCalendarClock}
/>
</div>
</div>
</div>
</Link>
</div> </div>
<SectionTitleLineWithButton {!!rolesWidgets.length && <hr className='my-6 ' />}
icon={icon.mdiSettingsHelper}
title={t('pages.dashboard.quickAccess', { defaultValue: 'Acceso Rápido' })} <div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
main={false}>
{''}
</SectionTitleLineWithButton> {hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
<div
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6"> className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
{hasPermission(currentUser, 'READ_SETTLEMENTS') && ( >
<Link href="/settlements/settlements-list"> <div className="flex justify-between align-center">
<div className={`${corners} ${cardsStyle} p-4 flex items-center space-x-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors`}> <div>
<BaseIcon path={icon.mdiFileDocumentMultiple} size="24" className="text-blue-600" /> <div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
<span className="font-medium">{t('menu.Settlements', { defaultValue: 'Liquidaciones' })}</span> Users
</div>
<div className="text-3xl leading-tight font-semibold">
{users}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiAccountGroup || icon.mdiTable}
/>
</div>
</div> </div>
</Link> </div>
)} </Link>}
{hasPermission(currentUser, 'READ_EXPENSES') && (
<Link href="/expenses/expenses-list"> {hasPermission(currentUser, 'READ_ROLES') && <Link href={'/roles/roles-list'}>
<div className={`${corners} ${cardsStyle} p-4 flex items-center space-x-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors`}> <div
<BaseIcon path={icon.mdiReceipt} size="24" className="text-red-600" /> className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<span className="font-medium">{t('menu.Expenses', { defaultValue: 'Gastos' })}</span> >
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Roles
</div>
<div className="text-3xl leading-tight font-semibold">
{roles}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountVariantOutline || icon.mdiTable}
/>
</div>
</div> </div>
</Link> </div>
)} </Link>}
{hasPermission(currentUser, 'READ_EVENT_LOGS') && (
<Link href="/event_logs/event_logs-list"> {hasPermission(currentUser, 'READ_PERMISSIONS') && <Link href={'/permissions/permissions-list'}>
<div className={`${corners} ${cardsStyle} p-4 flex items-center space-x-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors`}> <div
<BaseIcon path={icon.mdiClipboardTextClock} size="24" className="text-gray-600" /> className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<span className="font-medium">{t('menu.Event logs', { defaultValue: 'Registro de Eventos' })}</span> >
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Permissions
</div>
<div className="text-3xl leading-tight font-semibold">
{permissions}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={icon.mdiShieldAccountOutline || icon.mdiTable}
/>
</div>
</div> </div>
</Link> </div>
)} </Link>}
{hasPermission(currentUser, 'READ_APP_SETTINGS') && (
<Link href="/app_settings/app_settings-list"> {hasPermission(currentUser, 'READ_MEDICAL_CENTERS') && <Link href={'/medical_centers/medical_centers-list'}>
<div className={`${corners} ${cardsStyle} p-4 flex items-center space-x-4 cursor-pointer hover:bg-gray-50 dark:hover:bg-dark-800 transition-colors`}> <div
<BaseIcon path={icon.mdiCog} size="24" className="text-indigo-600" /> className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
<span className="font-medium">{t('menu.App settings', { defaultValue: 'Configuración' })}</span> >
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Medical centers
</div>
<div className="text-3xl leading-tight font-semibold">
{medical_centers}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiHospitalBuilding' in icon ? icon['mdiHospitalBuilding' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div> </div>
</Link> </div>
)} </Link>}
{hasPermission(currentUser, 'READ_SERVICES') && <Link href={'/services/services-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Services
</div>
<div className="text-3xl leading-tight font-semibold">
{services}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiStethoscope' in icon ? icon['mdiStethoscope' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PATIENTS') && <Link href={'/patients/patients-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Patients
</div>
<div className="text-3xl leading-tight font-semibold">
{patients}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiAccountInjury' in icon ? icon['mdiAccountInjury' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_APPOINTMENTS') && <Link href={'/appointments/appointments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Appointments
</div>
<div className="text-3xl leading-tight font-semibold">
{appointments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCalendarClock' in icon ? icon['mdiCalendarClock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PAYMENTS') && <Link href={'/payments/payments-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Payments
</div>
<div className="text-3xl leading-tight font-semibold">
{payments}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCashRegister' in icon ? icon['mdiCashRegister' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_SETTLEMENTS') && <Link href={'/settlements/settlements-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Settlements
</div>
<div className="text-3xl leading-tight font-semibold">
{settlements}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFileDocumentMultiple' in icon ? icon['mdiFileDocumentMultiple' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_EXPENSES') && <Link href={'/expenses/expenses-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Expenses
</div>
<div className="text-3xl leading-tight font-semibold">
{expenses}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiReceipt' in icon ? icon['mdiReceipt' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_EXTRA_INCOMES') && <Link href={'/extra_incomes/extra_incomes-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Extra incomes
</div>
<div className="text-3xl leading-tight font-semibold">
{extra_incomes}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCashPlus' in icon ? icon['mdiCashPlus' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_MESSAGES') && <Link href={'/messages/messages-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Messages
</div>
<div className="text-3xl leading-tight font-semibold">
{messages}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiMessageText' in icon ? icon['mdiMessageText' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PDF_TEMPLATES') && <Link href={'/pdf_templates/pdf_templates-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Pdf templates
</div>
<div className="text-3xl leading-tight font-semibold">
{pdf_templates}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFilePdfBox' in icon ? icon['mdiFilePdfBox' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_PDF_DOCUMENTS') && <Link href={'/pdf_documents/pdf_documents-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Pdf documents
</div>
<div className="text-3xl leading-tight font-semibold">
{pdf_documents}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiFilePdfBox' in icon ? icon['mdiFilePdfBox' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_EVENT_LOGS') && <Link href={'/event_logs/event_logs-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Event logs
</div>
<div className="text-3xl leading-tight font-semibold">
{event_logs}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiClipboardTextClock' in icon ? icon['mdiClipboardTextClock' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
{hasPermission(currentUser, 'READ_APP_SETTINGS') && <Link href={'/app_settings/app_settings-list'}>
<div
className={`${corners !== 'rounded-full'? corners : 'rounded-3xl'} dark:bg-dark-900 ${cardsStyle} dark:border-dark-700 p-6`}
>
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
App settings
</div>
<div className="text-3xl leading-tight font-semibold">
{app_settings}
</div>
</div>
<div>
<BaseIcon
className={`${iconsColor}`}
w="w-16"
h="h-16"
size={48}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
path={'mdiCog' in icon ? icon['mdiCog' as keyof typeof icon] : icon.mdiTable || icon.mdiTable}
/>
</div>
</div>
</div>
</Link>}
</div> </div>
</SectionMain> </SectionMain>
</> </>

View File

@ -1,10 +1,12 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import type { ReactElement } from 'react'; import type { ReactElement } from 'react';
import Head from 'next/head'; import Head from 'next/head';
import BaseButton from '../components/BaseButton'; import BaseButton from '../components/BaseButton';
import CardBox from '../components/CardBox'; import CardBox from '../components/CardBox';
import BaseIcon from "../components/BaseIcon"; import BaseIcon from "../components/BaseIcon";
import { mdiInformation, mdiEye, mdiEyeOff, mdiGoogle } from '@mdi/js'; import { mdiInformation, mdiEye, mdiEyeOff } from '@mdi/js';
import SectionFullScreen from '../components/SectionFullScreen'; import SectionFullScreen from '../components/SectionFullScreen';
import LayoutGuest from '../layouts/Guest'; import LayoutGuest from '../layouts/Guest';
import { Field, Form, Formik } from 'formik'; import { Field, Form, Formik } from 'formik';
@ -19,11 +21,8 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link'; import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify"; import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels' import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { useTranslation } from 'react-i18next';
import LanguageSwitcher from '../components/LanguageSwitcher';
export default function Login() { export default function Login() {
const { t } = useTranslation('common');
const router = useRouter(); const router = useRouter();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const textColor = useAppSelector((state) => state.style.linkColor); const textColor = useAppSelector((state) => state.style.linkColor);
@ -41,7 +40,7 @@ export default function Login() {
const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector( const { currentUser, isFetching, errorMessage, token, notify:notifyState } = useAppSelector(
(state) => state.auth, (state) => state.auth,
); );
const [initialValues, setInitialValues] = React.useState({ email:'matiasarcuri11@gmail.com', const [initialValues, setInitialValues] = React.useState({ email:'admin@flatlogic.com',
password: '94e48f87', password: '94e48f87',
remember: true }) remember: true })
@ -101,10 +100,6 @@ export default function Login() {
})); }));
}; };
const handleGoogleLogin = () => {
window.location.href = `${process.env.NEXT_PUBLIC_BACK_API}/auth/signin/google`;
};
const imageBlock = (image) => ( const imageBlock = (image) => (
<div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3" <div className="hidden md:flex flex-col justify-end relative flex-grow-0 flex-shrink-0 w-1/3"
style={{ style={{
@ -114,9 +109,8 @@ export default function Login() {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
}}> }}>
<div className="flex justify-center w-full bg-blue-300/20"> <div className="flex justify-center w-full bg-blue-300/20">
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer"> <a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
{t('pages.login.pexels.photoCredit', { photographer: image?.photographer })} by {image?.photographer} on Pexels</a>
</a>
</div> </div>
</div> </div>
) )
@ -132,7 +126,7 @@ export default function Login() {
muted muted
> >
<source src={video.video_files[0]?.link} type='video/mp4'/> <source src={video.video_files[0]?.link} type='video/mp4'/>
{t('pages.login.pexels.videoUnsupported')} Your browser does not support the video tag.
</video> </video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'> <div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a <a
@ -141,7 +135,7 @@ export default function Login() {
target='_blank' target='_blank'
rel='noreferrer' rel='noreferrer'
> >
{t('pages.login.pexels.videoCredit', { name: video.user.name })} Video by {video.user.name} on Pexels
</a> </a>
</div> </div>
</div>) </div>)
@ -160,13 +154,10 @@ export default function Login() {
backgroundRepeat: 'no-repeat', backgroundRepeat: 'no-repeat',
} : {}}> } : {}}>
<Head> <Head>
<title>{getPageTitle(t('pages.login.pageTitle'))}</title> <title>{getPageTitle('Login')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
<div className="absolute top-4 right-4 z-50">
<LanguageSwitcher />
</div>
<div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}> <div className={`flex ${contentPosition === 'right' ? 'flex-row-reverse' : 'flex-row'} min-h-screen w-full`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null} {contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null} {contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
@ -179,18 +170,18 @@ export default function Login() {
<div className='flex flex-row text-gray-500 justify-between'> <div className='flex flex-row text-gray-500 justify-between'>
<div> <div>
<p className='mb-2'>{t('pages.login.sampleCredentialsAdmin', { email: '', password: '' }).split('/')[0]} <p className='mb-2'>Use{' '}
<code className={`cursor-pointer ${textColor} `} <code className={`cursor-pointer ${textColor} `}
data-password="94e48f87" data-password="94e48f87"
onClick={(e) => setLogin(e.target)}>matiasarcuri11@gmail.com</code>{' / '} onClick={(e) => setLogin(e.target)}>admin@flatlogic.com</code>{' / '}
<code className={`${textColor}`}>94e48f87</code>{' / '} <code className={`${textColor}`}>94e48f87</code>{' / '}
{t('pages.login.sampleCredentialsAdmin', { email: '', password: '' }).split('/')[1]}</p> to login as Admin</p>
<p>{t('pages.login.sampleCredentialsUser', { email: '', password: '' }).split('/')[0]}<code <p>Use <code
className={`cursor-pointer ${textColor} `} className={`cursor-pointer ${textColor} `}
data-password="d52135bedc81" data-password="d52135bedc81"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '} onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>d52135bedc81</code>{' / '} <code className={`${textColor}`}>d52135bedc81</code>{' / '}
{t('pages.login.sampleCredentialsUser', { email: '', password: '' }).split('/')[1]}</p> to login as User</p>
</div> </div>
<div> <div>
<BaseIcon <BaseIcon
@ -212,15 +203,15 @@ export default function Login() {
> >
<Form> <Form>
<FormField <FormField
label={t('pages.login.form.loginLabel')} label='Login'
help={t('pages.login.form.loginHelp')}> help='Please enter your login'>
<Field name='email' /> <Field name='email' />
</FormField> </FormField>
<div className='relative'> <div className='relative'>
<FormField <FormField
label={t('pages.login.form.passwordLabel')} label='Password'
help={t('pages.login.form.passwordHelp')}> help='Please enter your password'>
<Field name='password' type={showPassword ? 'text' : 'password'} /> <Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField> </FormField>
<div <div
@ -236,12 +227,12 @@ export default function Login() {
</div> </div>
<div className={'flex justify-between'}> <div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label={t('pages.login.form.remember')}> <FormCheckRadio type='checkbox' label='Remember'>
<Field type='checkbox' name='remember' /> <Field type='checkbox' name='remember' />
</FormCheckRadio> </FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}> <Link className={`${textColor} text-blue-600`} href={'/forgot'}>
{t('pages.login.form.forgotPassword')} Forgot password?
</Link> </Link>
</div> </div>
@ -251,29 +242,16 @@ export default function Login() {
<BaseButton <BaseButton
className={'w-full'} className={'w-full'}
type='submit' type='submit'
label={isFetching ? t('pages.login.form.loading') : t('pages.login.form.loginButton')} label={isFetching ? 'Loading...' : 'Login'}
color='info' color='info'
disabled={isFetching} disabled={isFetching}
/> />
</BaseButtons> </BaseButtons>
<BaseDivider />
<BaseButtons>
<BaseButton
className={'w-full'}
label={t('pages.login.form.loginWithGoogle', { defaultValue: 'Ingresar con Google' })}
color="white"
icon={mdiGoogle}
onClick={handleGoogleLogin}
/>
</BaseButtons>
<br /> <br />
<p className={'text-center'}> <p className={'text-center'}>
{t('pages.login.form.noAccountYet')}{' '} Dont have an account yet?{' '}
<Link className={`${textColor}`} href={'/register'}> <Link className={`${textColor}`} href={'/register'}>
{t('pages.login.form.newAccount')} New Account
</Link> </Link>
</p> </p>
</Form> </Form>
@ -283,9 +261,9 @@ export default function Login() {
</div> </div>
</SectionFullScreen> </SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'> <div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>{t('pages.login.footer.copyright', { year: 2026, title: title })}</p> <p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'> <Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
{t('pages.login.footer.privacy')} Privacy Policy
</Link> </Link>
</div> </div>
<ToastContainer /> <ToastContainer />

View File

@ -12,7 +12,6 @@ import BaseDivider from '../components/BaseDivider';
import BaseButtons from '../components/BaseButtons'; import BaseButtons from '../components/BaseButtons';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { getPageTitle } from '../config'; import { getPageTitle } from '../config';
import { mdiGoogle } from '@mdi/js';
import axios from "axios"; import axios from "axios";
@ -37,14 +36,10 @@ export default function Register() {
} }
}; };
const handleGoogleLogin = () => {
window.location.href = `${process.env.NEXT_PUBLIC_BACK_API}/auth/signin/google`;
};
return ( return (
<> <>
<Head> <Head>
<title>{getPageTitle('Register')}</title> <title>{getPageTitle('Login')}</title>
</Head> </Head>
<SectionFullScreen bg='violet'> <SectionFullScreen bg='violet'>
@ -83,18 +78,6 @@ export default function Register() {
color='info' color='info'
/> />
</BaseButtons> </BaseButtons>
<BaseDivider />
<BaseButtons>
<BaseButton
className={'w-full'}
label="Registrarse con Google"
color="white"
icon={mdiGoogle}
onClick={handleGoogleLogin}
/>
</BaseButtons>
</Form> </Form>
</Formik> </Formik>
</CardBox> </CardBox>
@ -106,4 +89,4 @@ export default function Register() {
Register.getLayout = function getLayout(page: ReactElement) { Register.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>; return <LayoutGuest>{page}</LayoutGuest>;
}; };