Compare commits
No commits in common. "ai-dev" and "master" have entirely different histories.
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -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',
|
||||||
|
|||||||
@ -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'`);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -1554,3 +1555,4 @@ await queryInterface.bulkInsert("rolesPermissionsPermissions", [
|
|||||||
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -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 }),
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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
|
|
||||||
@ -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_',
|
||||||
|
|||||||
@ -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,38 +99,29 @@ 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>
|
||||||
|
|||||||
@ -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,
|
const [users, setUsers] = React.useState(loadingMessage);
|
||||||
totalPatientsCount: 0,
|
const [roles, setRoles] = React.useState(loadingMessage);
|
||||||
totalAppointmentsCount: 0
|
const [permissions, setPermissions] = React.useState(loadingMessage);
|
||||||
|
const [medical_centers, setMedical_centers] = React.useState(loadingMessage);
|
||||||
|
const [services, setServices] = React.useState(loadingMessage);
|
||||||
|
const [patients, setPatients] = React.useState(loadingMessage);
|
||||||
|
const [appointments, setAppointments] = React.useState(loadingMessage);
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
const [widgetsRole, setWidgetsRole] = React.useState({
|
||||||
|
role: { value: '', label: '' },
|
||||||
});
|
});
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const { currentUser } = useAppSelector((state) => state.auth);
|
const { currentUser } = useAppSelector((state) => state.auth);
|
||||||
|
const { isFetchingQuery } = useAppSelector((state) => state.openAi);
|
||||||
|
|
||||||
async function loadMetrics() {
|
const { rolesWidgets, loading } = useAppSelector((state) => state.roles);
|
||||||
try {
|
|
||||||
const response = await axios.get('/dashboard/metrics');
|
|
||||||
setMetrics(response.data);
|
async function loadData() {
|
||||||
} catch (error) {
|
const entities = ['users','roles','permissions','medical_centers','services','patients','appointments','payments','settlements','expenses','extra_incomes','messages','pdf_templates','pdf_documents','event_logs','app_settings',];
|
||||||
console.error('Error loading metrics:', error);
|
const fns = [setUsers,setRoles,setPermissions,setMedical_centers,setServices,setPatients,setAppointments,setPayments,setSettlements,setExpenses,setExtra_incomes,setMessages,setPdf_templates,setPdf_documents,setEvent_logs,setApp_settings,];
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
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}
|
||||||
|
isFetchingQuery={isFetchingQuery}
|
||||||
|
setWidgetsRole={setWidgetsRole}
|
||||||
|
widgetsRole={widgetsRole}
|
||||||
|
/>}
|
||||||
|
{!!rolesWidgets.length &&
|
||||||
|
hasPermission(currentUser, 'CREATE_ROLES') && (
|
||||||
|
<p className=' text-gray-500 dark:text-gray-400 mb-4'>
|
||||||
|
{`${widgetsRole?.role?.label || 'Users'}'s widgets`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link href={'/payments/payments-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-yellow-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.pendingPayments', { defaultValue: 'Pagos Pendientes/Parciales' })}
|
w='w-16'
|
||||||
</div>
|
h='h-16'
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
size={48}
|
||||||
{loading ? '...' : metrics.pendingPaymentsCount}
|
path={icon.mdiLoading}
|
||||||
</div>
|
/>{' '}
|
||||||
</div>
|
Loading widgets...
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className="text-yellow-500"
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiCashClock}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</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-blue-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.todayAppointments', { defaultValue: 'Turnos de Hoy' })}
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{loading ? '...' : metrics.todayAppointmentsCount}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<BaseIcon
|
|
||||||
className="text-blue-500"
|
|
||||||
w="w-16"
|
|
||||||
h="h-16"
|
|
||||||
size={48}
|
|
||||||
path={icon.mdiCalendarToday}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link href={'/medical_centers/medical_centers-list'}>
|
|
||||||
<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`}>
|
|
||||||
<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.activeCenters', { defaultValue: 'Centros Activos' })}
|
|
||||||
</div>
|
|
||||||
<div className="text-3xl leading-tight font-semibold">
|
|
||||||
{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>
|
|
||||||
|
|
||||||
|
{ rolesWidgets &&
|
||||||
|
rolesWidgets.map((widget) => (
|
||||||
|
<SmartWidget
|
||||||
|
key={widget.id}
|
||||||
|
userId={currentUser?.id}
|
||||||
|
widget={widget}
|
||||||
|
roleId={widgetsRole?.role?.value || ''}
|
||||||
|
admin={hasPermission(currentUser, 'CREATE_ROLES')}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SectionTitleLineWithButton
|
{!!rolesWidgets.length && <hr className='my-6 ' />}
|
||||||
icon={icon.mdiSettingsHelper}
|
|
||||||
title={t('pages.dashboard.quickAccess', { defaultValue: 'Acceso Rápido' })}
|
|
||||||
main={false}>
|
|
||||||
{''}
|
|
||||||
</SectionTitleLineWithButton>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-4 mb-6">
|
<div id="dashboard" className='grid grid-cols-1 gap-6 lg:grid-cols-3 mb-6'>
|
||||||
{hasPermission(currentUser, 'READ_SETTLEMENTS') && (
|
|
||||||
<Link href="/settlements/settlements-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`}>
|
{hasPermission(currentUser, 'READ_USERS') && <Link href={'/users/users-list'}>
|
||||||
<BaseIcon path={icon.mdiFileDocumentMultiple} size="24" className="text-blue-600" />
|
<div
|
||||||
<span className="font-medium">{t('menu.Settlements', { defaultValue: 'Liquidaciones' })}</span>
|
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">
|
||||||
|
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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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')}{' '}
|
Don’t 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 />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user