Auto commit: 2026-02-09T01:19:47.913Z

This commit is contained in:
Flatlogic Bot 2026-02-09 01:19:47 +00:00
parent 043ebe8507
commit 45f6e99770
8 changed files with 106 additions and 51 deletions

View File

@ -1,4 +1,29 @@
{
"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": {
"dashboard": {
"pageTitle": "Tablero",
@ -9,8 +34,8 @@
"login": {
"pageTitle": "Inicio de sesión",
"sampleCredentialsAdmin": "Use {{email}} / {{password}} para iniciar sesión como Administrador",
"sampleCredentialsUser": "Use {{email}} / {{password}} para iniciar sesión como Usuario",
"sampleCredentialsAdmin": "Use / para iniciar sesión como Administrador",
"sampleCredentialsUser": "Use / para iniciar sesión como Usuario",
"form": {
"loginLabel": "Usuario",
@ -20,6 +45,7 @@
"remember": "Recuérdame",
"forgotPassword": "¿Olvidó su contraseña?",
"loginButton": "Acceder",
"loginWithGoogle": "Ingresar con Google",
"loading": "Cargando...",
"noAccountYet": "¿Aún no tiene una cuenta?",
"newAccount": "Crear cuenta"
@ -40,7 +66,7 @@
"components": {
"widgetCreator": {
"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",
"settingsDescription": "¿Para qué rol estamos mostrando y creando widgets?",
"doneButton": "Listo",
@ -52,4 +78,4 @@
"minLength": "Longitud mínima: {{count}} caracteres"
}
}
}
}

View File

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

View File

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

View File

@ -11,12 +11,14 @@ import { setDarkMode } from '../stores/styleSlice'
import { logoutUser } from '../stores/authSlice'
import { useRouter } from 'next/router';
import ClickOutside from "./ClickOutside";
import { useTranslation } from 'react-i18next'
type Props = {
item: MenuNavBarItem
}
export default function NavBarItem({ item }: Props) {
const { t } = useTranslation('common')
const router = useRouter();
const dispatch = useAppDispatch();
const excludedRef = useRef(null);
@ -46,7 +48,7 @@ export default function NavBarItem({ item }: Props) {
item.isDesktopNoLabel ? 'lg:w-16 lg:justify-center' : '',
].join(' ')
const itemLabel = item.isCurrentUser ? userName : item.label
const itemLabel = item.isCurrentUser ? userName : t(`menu.${item.label}`, item.label)
const handleMenuClick = () => {
if (item.menu) {
@ -77,7 +79,7 @@ export default function NavBarItem({ item }: Props) {
const NavBarItemComponentContents = (
<>
<div
id={getItemId(itemLabel)}
id={getItemId(item.label)}
className={`flex items-center ${
item.menu
? 'bg-gray-100 dark:bg-dark-800 lg:bg-transparent lg:dark:bg-transparent p-3 lg:p-0'
@ -128,4 +130,4 @@ export default function NavBarItem({ item }: Props) {
}
return <div className={componentClass} ref={excludedRef}>{NavBarItemComponentContents}</div>
}
}

View File

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

View File

@ -15,6 +15,7 @@ import {findMe, logoutUser} from "../stores/authSlice";
import {hasPermission} from "../helpers/userPermissions";
import RestrictedAccess from '../components/RestrictedAccess'
import LanguageSwitcher from '../components/LanguageSwitcher'
type Props = {
@ -116,6 +117,9 @@ export default function LayoutAuthenticated({
<NavBarItemPlain useMargin>
<Search />
</NavBarItemPlain>
<div className="flex items-center ml-2">
<LanguageSwitcher />
</div>
</>
)}
</NavBar>
@ -132,4 +136,4 @@ export default function LayoutAuthenticated({
</div>
</div>
)
}
}

View File

@ -12,8 +12,10 @@ import Link from "next/link";
import { hasPermission } from "../helpers/userPermissions";
import { useAppSelector } from '../stores/hooks';
import { useTranslation } from 'react-i18next';
const Dashboard = () => {
const { t } = useTranslation('common');
const iconsColor = useAppSelector((state) => state.style.iconsColor);
const corners = useAppSelector((state) => state.style.corners);
const cardsStyle = useAppSelector((state) => state.style.cardsStyle);
@ -49,13 +51,13 @@ const Dashboard = () => {
<>
<Head>
<title>
{getPageTitle('Overview')}
{getPageTitle(t('pages.dashboard.pageTitle'))}
</title>
</Head>
<SectionMain>
<SectionTitleLineWithButton
icon={icon.mdiChartTimelineVariant}
title='ZURICH TM Dashboard'
title={`${t('pages.dashboard.dashboardTitle', { defaultValue: 'ZURICH TM Dashboard' })}`}
main>
{''}
</SectionTitleLineWithButton>
@ -67,7 +69,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Pagos Pendientes/Parciales
{t('pages.dashboard.metrics.pendingPayments', { defaultValue: 'Pagos Pendientes/Parciales' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.pendingPaymentsCount}
@ -91,7 +93,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Turnos de Hoy
{t('pages.dashboard.metrics.todayAppointments', { defaultValue: 'Turnos de Hoy' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.todayAppointmentsCount}
@ -115,7 +117,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Centros Activos
{t('pages.dashboard.metrics.activeCenters', { defaultValue: 'Centros Activos' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.activeCentersCount}
@ -139,7 +141,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Total Pacientes
{t('pages.dashboard.metrics.totalPatients', { defaultValue: 'Total Pacientes' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.totalPatientsCount}
@ -163,7 +165,7 @@ const Dashboard = () => {
<div className="flex justify-between align-center">
<div>
<div className="text-lg leading-tight text-gray-500 dark:text-gray-400">
Total de Turnos
{t('pages.dashboard.metrics.totalAppointments', { defaultValue: 'Total de Turnos' })}
</div>
<div className="text-3xl leading-tight font-semibold">
{loading ? '...' : metrics.totalAppointmentsCount}
@ -186,7 +188,7 @@ const Dashboard = () => {
<SectionTitleLineWithButton
icon={icon.mdiSettingsHelper}
title='Acceso Rápido'
title={t('pages.dashboard.quickAccess', { defaultValue: 'Acceso Rápido' })}
main={false}>
{''}
</SectionTitleLineWithButton>
@ -196,7 +198,7 @@ const Dashboard = () => {
<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`}>
<BaseIcon path={icon.mdiFileDocumentMultiple} size="24" className="text-blue-600" />
<span className="font-medium">Liquidaciones</span>
<span className="font-medium">{t('menu.Settlements', { defaultValue: 'Liquidaciones' })}</span>
</div>
</Link>
)}
@ -204,7 +206,7 @@ const Dashboard = () => {
<Link href="/expenses/expenses-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`}>
<BaseIcon path={icon.mdiReceipt} size="24" className="text-red-600" />
<span className="font-medium">Gastos</span>
<span className="font-medium">{t('menu.Expenses', { defaultValue: 'Gastos' })}</span>
</div>
</Link>
)}
@ -212,7 +214,7 @@ const Dashboard = () => {
<Link href="/event_logs/event_logs-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`}>
<BaseIcon path={icon.mdiClipboardTextClock} size="24" className="text-gray-600" />
<span className="font-medium">Registro de Eventos</span>
<span className="font-medium">{t('menu.Event logs', { defaultValue: 'Registro de Eventos' })}</span>
</div>
</Link>
)}
@ -220,7 +222,7 @@ const Dashboard = () => {
<Link href="/app_settings/app_settings-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`}>
<BaseIcon path={icon.mdiCog} size="24" className="text-indigo-600" />
<span className="font-medium">Configuración</span>
<span className="font-medium">{t('menu.App settings', { defaultValue: 'Configuración' })}</span>
</div>
</Link>
)}
@ -234,4 +236,4 @@ Dashboard.getLayout = function getLayout(page: ReactElement) {
return <LayoutAuthenticated>{page}</LayoutAuthenticated>
}
export default Dashboard
export default Dashboard

View File

@ -19,8 +19,11 @@ import { useAppDispatch, useAppSelector } from '../stores/hooks';
import Link from 'next/link';
import {toast, ToastContainer} from "react-toastify";
import { getPexelsImage, getPexelsVideo } from '../helpers/pexels'
import { useTranslation } from 'react-i18next';
import LanguageSwitcher from '../components/LanguageSwitcher';
export default function Login() {
const { t } = useTranslation('common');
const router = useRouter();
const dispatch = useAppDispatch();
const textColor = useAppSelector((state) => state.style.linkColor);
@ -111,8 +114,9 @@ export default function Login() {
backgroundRepeat: 'no-repeat',
}}>
<div className="flex justify-center w-full bg-blue-300/20">
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">Photo
by {image?.photographer} on Pexels</a>
<a className="text-[8px]" href={image?.photographer_url} target="_blank" rel="noreferrer">
{t('pages.login.pexels.photoCredit', { photographer: image?.photographer })}
</a>
</div>
</div>
)
@ -128,7 +132,7 @@ export default function Login() {
muted
>
<source src={video.video_files[0]?.link} type='video/mp4'/>
Your browser does not support the video tag.
{t('pages.login.pexels.videoUnsupported')}
</video>
<div className='flex justify-center w-full bg-blue-300/20 z-10'>
<a
@ -137,7 +141,7 @@ export default function Login() {
target='_blank'
rel='noreferrer'
>
Video by {video.user.name} on Pexels
{t('pages.login.pexels.videoCredit', { name: video.user.name })}
</a>
</div>
</div>)
@ -156,10 +160,13 @@ export default function Login() {
backgroundRepeat: 'no-repeat',
} : {}}>
<Head>
<title>{getPageTitle('Login')}</title>
<title>{getPageTitle(t('pages.login.pageTitle'))}</title>
</Head>
<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`}>
{contentType === 'image' && contentPosition !== 'background' ? imageBlock(illustrationImage) : null}
{contentType === 'video' && contentPosition !== 'background' ? videoBlock(illustrationVideo) : null}
@ -172,18 +179,18 @@ export default function Login() {
<div className='flex flex-row text-gray-500 justify-between'>
<div>
<p className='mb-2'>Use{' '}
<p className='mb-2'>{t('pages.login.sampleCredentialsAdmin', { email: '', password: '' }).split('/')[0]}
<code className={`cursor-pointer ${textColor} `}
data-password="94e48f87"
onClick={(e) => setLogin(e.target)}>matiasarcuri11@gmail.com</code>{' / '}
<code className={`${textColor}`}>94e48f87</code>{' / '}
to login as Admin</p>
<p>Use <code
{t('pages.login.sampleCredentialsAdmin', { email: '', password: '' }).split('/')[1]}</p>
<p>{t('pages.login.sampleCredentialsUser', { email: '', password: '' }).split('/')[0]}<code
className={`cursor-pointer ${textColor} `}
data-password="d52135bedc81"
onClick={(e) => setLogin(e.target)}>client@hello.com</code>{' / '}
<code className={`${textColor}`}>d52135bedc81</code>{' / '}
to login as User</p>
{t('pages.login.sampleCredentialsUser', { email: '', password: '' }).split('/')[1]}</p>
</div>
<div>
<BaseIcon
@ -205,15 +212,15 @@ export default function Login() {
>
<Form>
<FormField
label='Login'
help='Please enter your login'>
label={t('pages.login.form.loginLabel')}
help={t('pages.login.form.loginHelp')}>
<Field name='email' />
</FormField>
<div className='relative'>
<FormField
label='Password'
help='Please enter your password'>
label={t('pages.login.form.passwordLabel')}
help={t('pages.login.form.passwordHelp')}>
<Field name='password' type={showPassword ? 'text' : 'password'} />
</FormField>
<div
@ -229,12 +236,12 @@ export default function Login() {
</div>
<div className={'flex justify-between'}>
<FormCheckRadio type='checkbox' label='Remember'>
<FormCheckRadio type='checkbox' label={t('pages.login.form.remember')}>
<Field type='checkbox' name='remember' />
</FormCheckRadio>
<Link className={`${textColor} text-blue-600`} href={'/forgot'}>
Forgot password?
{t('pages.login.form.forgotPassword')}
</Link>
</div>
@ -244,7 +251,7 @@ export default function Login() {
<BaseButton
className={'w-full'}
type='submit'
label={isFetching ? 'Loading...' : 'Login'}
label={isFetching ? t('pages.login.form.loading') : t('pages.login.form.loginButton')}
color='info'
disabled={isFetching}
/>
@ -255,7 +262,7 @@ export default function Login() {
<BaseButtons>
<BaseButton
className={'w-full'}
label="Ingresar con Google"
label={t('pages.login.form.loginWithGoogle', { defaultValue: 'Ingresar con Google' })}
color="white"
icon={mdiGoogle}
onClick={handleGoogleLogin}
@ -264,9 +271,9 @@ export default function Login() {
<br />
<p className={'text-center'}>
Dont have an account yet?{' '}
{t('pages.login.form.noAccountYet')}{' '}
<Link className={`${textColor}`} href={'/register'}>
New Account
{t('pages.login.form.newAccount')}
</Link>
</p>
</Form>
@ -276,9 +283,9 @@ export default function Login() {
</div>
</SectionFullScreen>
<div className='bg-black text-white flex flex-col text-center justify-center md:flex-row'>
<p className='py-6 text-sm'>© 2026 <span>{title}</span>. © All rights reserved</p>
<p className='py-6 text-sm'>{t('pages.login.footer.copyright', { year: 2026, title: title })}</p>
<Link className='py-6 ml-4 text-sm' href='/privacy-policy/'>
Privacy Policy
{t('pages.login.footer.privacy')}
</Link>
</div>
<ToastContainer />
@ -288,4 +295,4 @@ export default function Login() {
Login.getLayout = function getLayout(page: ReactElement) {
return <LayoutGuest>{page}</LayoutGuest>;
};
};