fase 3.6: i18n espanol/ingles (530 claves, provider con fallback, interpolacion y pluralizacion, selector de idioma en menu de usuario y configuracion, convivencia con el glosario de negocio, superficies principales y paginas publicas traducidas sin alterar el espanol)
This commit is contained in:
@@ -28,6 +28,8 @@ import { Card, Button, Input, PageHeader, toast } from '../components/ui';
|
||||
import { odooApi, type Doctor, type ClinicSettings, type ImpuestosSettings, BRAND_DEFAULT, CLINIC_SETTINGS_DEFAULT, IMPUESTOS_DEFAULT, RESERVAS_ONLINE_DEFAULT, NOTIFICACIONES_DEFAULT } from '../services/odoo';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { publicarClinicSettings } from '../lib/clinic-settings';
|
||||
import { useI18n } from '../lib/i18n';
|
||||
import { LanguageSelect } from '../components/LanguageSelect';
|
||||
|
||||
const TABS = [
|
||||
{ key: 'clinica', label: 'Clínica' },
|
||||
@@ -67,6 +69,9 @@ const LEGACY_STORAGE_KEY = 'skeen_clinic_settings';
|
||||
|
||||
const Configuracion: FC = () => {
|
||||
const { user, brand, setBrand, can } = useAuth();
|
||||
const { t } = useI18n();
|
||||
// Clave i18n de una pestaña: 'notif_enviadas' -> 'config.tabs.notifEnviadas'.
|
||||
const tabKey = (k: string) => 'config.tabs.' + k.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
|
||||
// Gerencia gestiona catálogos, usuarios y roles; la sección "Clínica" (datos
|
||||
// de contacto, tipo de cambio y comisiones) queda para administración del sistema.
|
||||
const esAdmin = user?.role === 'admin';
|
||||
@@ -321,21 +326,21 @@ const Configuracion: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title="Configuración" subtitle="Ajustes de la clínica">
|
||||
<PageHeader title="Configuración" subtitle="Información y ajustes de la clínica" />
|
||||
<Layout title={t('config.title')} subtitle={t('config.subtitle')}>
|
||||
<PageHeader title={t('config.title')} subtitle={t('config.pageSubtitle')} />
|
||||
|
||||
{/* Tabs de secciones */}
|
||||
<div className="flex flex-wrap gap-1.5 mb-6">
|
||||
{tabsVisibles.map((t) => (
|
||||
{tabsVisibles.map((tabItem) => (
|
||||
<button
|
||||
key={t.key}
|
||||
key={tabItem.key}
|
||||
type="button"
|
||||
onClick={() => setTab(t.key)}
|
||||
onClick={() => setTab(tabItem.key)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-full transition ${
|
||||
tab === t.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
|
||||
tab === tabItem.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
{t(tabKey(tabItem.key))}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -343,7 +348,7 @@ const Configuracion: FC = () => {
|
||||
{tabsVisibles.length === 0 ? (
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<p className="text-sm text-theme-muted">No tienes permiso para ninguna sección de Configuración.</p>
|
||||
<p className="text-sm text-theme-muted">{t('config.noPermission')}</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
) : tab === 'usuarios' ? (
|
||||
@@ -381,11 +386,11 @@ const Configuracion: FC = () => {
|
||||
subcatalogo === s.key ? 'bg-theme-accent text-theme-inverse' : 'bg-theme-accent-bg text-theme-heading hover:bg-theme-bg'
|
||||
}`}
|
||||
>
|
||||
{s.label}
|
||||
{t(`config.tabs.${s.key}`)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<CatalogoPanel kind={subcatalogo as 'diagnosticos' | 'procedimientos'} titulo={subcatalogo === 'diagnosticos' ? 'Diagnósticos' : 'Procedimientos'} />
|
||||
<CatalogoPanel kind={subcatalogo as 'diagnosticos' | 'procedimientos'} titulo={t(`config.tabs.${subcatalogo}`)} />
|
||||
</Card.Body>
|
||||
</Card>
|
||||
) : tab === 'sucursales' ? (
|
||||
@@ -461,10 +466,20 @@ const Configuracion: FC = () => {
|
||||
) : tab === 'webhooks' ? (
|
||||
<WebhooksPanel />
|
||||
) : (
|
||||
<>
|
||||
<Card className="mb-4 sm:mb-6">
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-1">{t('config.idiomaTitle')}</h3>
|
||||
<p className="text-sm text-theme-muted mb-3">{t('config.idiomaBody')}</p>
|
||||
<div className="max-w-xs">
|
||||
<LanguageSelect />
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 sm:gap-6">
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-4">Información de la clínica</h3>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-4">{t('config.clinicInfo')}</h3>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<a
|
||||
href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(settings.direccion)}`}
|
||||
@@ -529,7 +544,7 @@ const Configuracion: FC = () => {
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-4 flex items-center">
|
||||
<Building2 size={20} className="mr-2" />
|
||||
Ajustes
|
||||
{t('config.settings')}
|
||||
</h3>
|
||||
{loading ? (
|
||||
<p className="text-sm text-theme-muted">Cargando...</p>
|
||||
@@ -693,7 +708,7 @@ const Configuracion: FC = () => {
|
||||
{esAdmin && (
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-1">Identidad de la clínica</h3>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-1">{t('config.clinicIdentity')}</h3>
|
||||
<p className="text-sm text-theme-muted mb-4">
|
||||
Nombre, pie de página y logos que aparecen en el inicio de sesión, el menú y los
|
||||
documentos impresos. Dejar un campo vacío vuelve al valor por omisión.
|
||||
@@ -760,7 +775,7 @@ const Configuracion: FC = () => {
|
||||
placeholder="Vacío = color del tema"
|
||||
/>
|
||||
{brandForm[key] && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setBrandForm((f) => ({ ...f, [key]: '' }))} title="Usar el color del tema">
|
||||
<Button variant="ghost" size="sm" onClick={() => setBrandForm((f) => ({ ...f, [key]: '' }))} title={t('config.useThemeColor')}>
|
||||
Limpiar
|
||||
</Button>
|
||||
)}
|
||||
@@ -797,7 +812,7 @@ const Configuracion: FC = () => {
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-1 flex items-center">
|
||||
<ArrowRightLeft size={20} className="mr-2" />
|
||||
Tipo de cambio USD / MXN
|
||||
{t('config.exchangeRate')}
|
||||
</h3>
|
||||
<p className="text-sm text-theme-muted mb-4">
|
||||
Se usa para convertir ventas en dólares. Vigente:
|
||||
@@ -821,7 +836,7 @@ const Configuracion: FC = () => {
|
||||
</div>
|
||||
<Button onClick={saveTc} disabled={tcSaving}>
|
||||
<Save size={16} className="mr-2" />
|
||||
{tcSaving ? 'Guardando...' : 'Actualizar TC'}
|
||||
{tcSaving ? t('common.saving') : t('config.updateRate')}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Body>
|
||||
@@ -831,7 +846,7 @@ const Configuracion: FC = () => {
|
||||
<Card.Body>
|
||||
<h3 className="font-heading text-xl text-theme-heading mb-1 flex items-center">
|
||||
<Percent size={20} className="mr-2" />
|
||||
Comisiones
|
||||
{t('config.commissions')}
|
||||
</h3>
|
||||
<p className="text-sm text-theme-muted mb-4">
|
||||
Porcentaje de comisión sobre las ventas atribuidas a cada médico (visible en su página "Mis ventas").
|
||||
@@ -864,7 +879,7 @@ const Configuracion: FC = () => {
|
||||
</div>
|
||||
<Button onClick={saveCommissionDefault} disabled={commissionDefaultSaving}>
|
||||
<Save size={16} className="mr-2" />
|
||||
{commissionDefaultSaving ? 'Guardando...' : 'Guardar'}
|
||||
{commissionDefaultSaving ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
@@ -910,7 +925,7 @@ const Configuracion: FC = () => {
|
||||
onClick={() => saveDoctorCommission(d.id)}
|
||||
disabled={commissionSavingId === d.id}
|
||||
>
|
||||
{commissionSavingId === d.id ? 'Guardando...' : 'Guardar'}
|
||||
{commissionSavingId === d.id ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
)}
|
||||
</td>
|
||||
@@ -927,6 +942,7 @@ const Configuracion: FC = () => {
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ImpactoModal
|
||||
|
||||
Reference in New Issue
Block a user