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:
@@ -35,7 +35,7 @@ import { useSearchParams } from 'react-router-dom';
|
||||
import { odooApi, type Patient, type Appointment, type Sale, type Visita, type Doctor, type EstadoCuenta, type PatientWallet, type ReceiptData } from '../services/odoo';
|
||||
import { exportToExcel } from '../lib/exporter';
|
||||
import { useAuth } from '../lib/auth';
|
||||
import { useGlosario } from '../lib/glosario';
|
||||
import { useI18n, useBusinessTerm } from '../lib/i18n';
|
||||
import { useSucursales } from '../lib/sucursales';
|
||||
|
||||
const fmtMoney = (n: number) =>
|
||||
@@ -126,7 +126,8 @@ const CRITICAL_ALERTS: { key: keyof Patient; label: string }[] = [
|
||||
|
||||
const Pacientes: FC = () => {
|
||||
const { brand, can } = useAuth();
|
||||
const { term } = useGlosario();
|
||||
const term = useBusinessTerm();
|
||||
const { t } = useI18n();
|
||||
const { labelFor: branchLabel } = useSucursales();
|
||||
const [patients, setPatients] = useState<Patient[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
@@ -432,14 +433,14 @@ const Pacientes: FC = () => {
|
||||
const TableHeader = () => (
|
||||
<thead className="bg-theme-bg">
|
||||
<tr>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Nombre</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Expediente</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden sm:table-cell">Teléfono</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Edad</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">Última visita</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">{t('patients.colName')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">{t('patients.colRecord')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden sm:table-cell">{t('patients.colPhone')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">{t('patients.colAge')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase hidden md:table-cell">{t('patients.colLastVisit')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">{term('visit_plural')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Puntos</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">Acciones</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">{t('patients.colPoints')}</th>
|
||||
<th className="text-left p-3 text-xs font-medium text-theme-muted uppercase">{t('patients.colActions')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
);
|
||||
@@ -528,16 +529,16 @@ const Pacientes: FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout title={term('patient_plural')} subtitle="Directorio de pacientes">
|
||||
<PageHeader title={term('patient_plural')} subtitle="Busca, crea y gestiona pacientes">
|
||||
<Layout title={term('patient_plural')} subtitle={t('patients.subtitle')}>
|
||||
<PageHeader title={term('patient_plural')} subtitle={t('patients.pageSubtitle')}>
|
||||
<Button variant="outline" onClick={exportPatients} loading={exporting} disabled={patients.length === 0 || !can('pacientes.exportar')}
|
||||
title={can('pacientes.exportar') ? undefined : 'Sin permiso para exportar'}>
|
||||
title={can('pacientes.exportar') ? undefined : t('patients.noExportPerm')}>
|
||||
<Download size={16} className="mr-2" />
|
||||
Exportar CSV
|
||||
{t('patients.exportCsv')}
|
||||
</Button>
|
||||
<Button onClick={openCreate}>
|
||||
<Plus size={16} className="mr-2" />
|
||||
Nuevo {term('patient_singular').toLowerCase()}
|
||||
{t('patients.newWord')} {term('patient_singular').toLowerCase()}
|
||||
</Button>
|
||||
</PageHeader>
|
||||
|
||||
@@ -547,7 +548,7 @@ const Pacientes: FC = () => {
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Search size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-theme-muted pointer-events-none" />
|
||||
<Input
|
||||
placeholder="Buscar por nombre, apellidos o teléfono..."
|
||||
placeholder={t('patients.searchPlaceholder')}
|
||||
value={searchInput}
|
||||
onChange={(e) => setSearchInput(e.target.value)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') setSearch(searchInput.trim()); if (e.key === 'Escape') { setSearchInput(''); setSearch(''); } }}
|
||||
@@ -558,8 +559,8 @@ const Pacientes: FC = () => {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { setSearchInput(''); setSearch(''); }}
|
||||
title="Limpiar búsqueda"
|
||||
aria-label="Limpiar búsqueda"
|
||||
title={t('patients.clearSearch')}
|
||||
aria-label={t('patients.clearSearch')}
|
||||
className="absolute right-2.5 top-1/2 -translate-y-1/2 p-1 rounded-lg text-theme-muted hover:text-theme-heading hover:bg-theme-bg"
|
||||
>
|
||||
<X size={14} />
|
||||
@@ -568,7 +569,7 @@ const Pacientes: FC = () => {
|
||||
</div>
|
||||
<Select
|
||||
options={[
|
||||
{ value: '', label: 'Todos los médicos' },
|
||||
{ value: '', label: t('patients.allDoctors') },
|
||||
...doctors.map((d) => ({
|
||||
value: String(d.id),
|
||||
label: d.patient_count !== undefined ? `${d.name} (${d.patient_count})` : d.name,
|
||||
@@ -584,9 +585,9 @@ const Pacientes: FC = () => {
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4 sm:mb-5">
|
||||
{([
|
||||
{ key: 'vip', label: 'VIP', active: onlyVip, set: setOnlyVip },
|
||||
{ key: 'recent', label: 'Visitas últimos 6 meses', active: onlyRecent, set: (v: boolean) => { setOnlyRecent(v); if (v) setOnlyStale(false); } },
|
||||
{ key: 'stale', label: 'Sin visita +6 meses', active: onlyStale, set: (v: boolean) => { setOnlyStale(v); if (v) setOnlyRecent(false); } },
|
||||
{ key: 'incomplete', label: 'Expediente incompleto', active: onlyIncomplete, set: setOnlyIncomplete },
|
||||
{ key: 'recent', label: t('patients.filterRecent'), active: onlyRecent, set: (v: boolean) => { setOnlyRecent(v); if (v) setOnlyStale(false); } },
|
||||
{ key: 'stale', label: t('patients.filterStale'), active: onlyStale, set: (v: boolean) => { setOnlyStale(v); if (v) setOnlyRecent(false); } },
|
||||
{ key: 'incomplete', label: t('patients.filterIncomplete'), active: onlyIncomplete, set: setOnlyIncomplete },
|
||||
] as const).map((chip) => (
|
||||
<button
|
||||
key={chip.key}
|
||||
@@ -607,7 +608,7 @@ const Pacientes: FC = () => {
|
||||
onClick={() => { setOnlyVip(false); setOnlyRecent(false); setOnlyStale(false); setOnlyIncomplete(false); setDoctorFilter(''); }}
|
||||
className="text-xs text-theme-muted underline underline-offset-2 hover:text-theme-heading"
|
||||
>
|
||||
Quitar filtros
|
||||
{t('patients.clearFilters')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -618,9 +619,9 @@ const Pacientes: FC = () => {
|
||||
<Skeleton count={6} className="h-12 w-full" />
|
||||
) : patients.length === 0 ? (
|
||||
<EmptyState
|
||||
title="Sin pacientes"
|
||||
subtitle="No se encontraron pacientes."
|
||||
actionLabel={`Nuevo ${term('patient_singular').toLowerCase()}`}
|
||||
title={t('patients.emptyTitle')}
|
||||
subtitle={t('patients.emptySub')}
|
||||
actionLabel={`${t('patients.newWord')} ${term('patient_singular').toLowerCase()}`}
|
||||
onAction={openCreate}
|
||||
/>
|
||||
) : (
|
||||
@@ -639,7 +640,7 @@ const Pacientes: FC = () => {
|
||||
<td className="p-3 text-sm">
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium ${completionBadgeClasses(p.expediente_completion)}`}
|
||||
title={p.expediente_missing && p.expediente_missing.length > 0 ? `Falta: ${p.expediente_missing.join(', ')}` : 'Expediente completo'}
|
||||
title={p.expediente_missing && p.expediente_missing.length > 0 ? t('patients.missingRecord', { list: p.expediente_missing.join(', ') }) : t('patients.complete')}
|
||||
>
|
||||
{p.expediente_completion ?? 0}%
|
||||
</span>
|
||||
@@ -1003,11 +1004,11 @@ const Pacientes: FC = () => {
|
||||
|
||||
<div className="flex gap-2 border-b border-theme-border pb-2 overflow-x-auto">
|
||||
{([
|
||||
{ key: 'info', label: 'Información general' },
|
||||
{ key: 'clinical', label: 'Historia clínica' },
|
||||
{ key: 'timeline', label: 'Historial' },
|
||||
{ key: 'docs', label: 'Documentos' },
|
||||
{ key: 'formularios', label: 'Formularios' },
|
||||
{ key: 'info', label: t('patients.tabInfo') },
|
||||
{ key: 'clinical', label: t('patients.tabClinical') },
|
||||
{ key: 'timeline', label: t('patients.tabTimeline') },
|
||||
{ key: 'docs', label: t('patients.tabDocs') },
|
||||
{ key: 'formularios', label: t('patients.tabForms') },
|
||||
] as const).map(({ key, label }) => (
|
||||
<button
|
||||
key={key}
|
||||
@@ -1106,7 +1107,7 @@ const Pacientes: FC = () => {
|
||||
<InfoRow label="Contacto de emergencia" value={selected?.emergency_contact} icon={AlertCircle} />
|
||||
<InfoRow label="Tel. emergencia" value={selected?.emergency_phone} icon={Phone} />
|
||||
<InfoRow label="Recomendado por" value={selected?.referred_by} icon={Users} />
|
||||
<InfoRow label="Médico asignado" value={selected?.primary_doctor || 'Sin asignar'} icon={Stethoscope} />
|
||||
<InfoRow label="Médico asignado" value={selected?.primary_doctor || t('patients.unassigned')} icon={Stethoscope} />
|
||||
<InfoRow label="Puntos" value={`${selected?.wallet_points ?? 0} pts`} icon={Heart} />
|
||||
</div>
|
||||
{selected?.address_notes && (
|
||||
@@ -1191,7 +1192,7 @@ const Pacientes: FC = () => {
|
||||
<>
|
||||
<section>
|
||||
<h4 className="text-sm font-semibold text-theme-heading mb-3 flex items-center">
|
||||
<FileText size={16} className="mr-2 text-theme-muted" /> Expediente escaneado
|
||||
<FileText size={16} className="mr-2 text-theme-muted" /> {t('patients.scannedRecord')}
|
||||
</h4>
|
||||
<PatientAdjuntos patientId={selected.id} kind="expediente" />
|
||||
</section>
|
||||
@@ -1348,10 +1349,10 @@ const Pacientes: FC = () => {
|
||||
<table className="w-full text-sm">
|
||||
<thead className="bg-theme-bg">
|
||||
<tr>
|
||||
<th className="text-left p-2.5 text-xs font-medium text-theme-muted">Servicio</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">Cant.</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">Precio</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">Subtotal</th>
|
||||
<th className="text-left p-2.5 text-xs font-medium text-theme-muted">{t('patients.colService')}</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">{t('patients.colQty')}</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">{t('patients.colPrice')}</th>
|
||||
<th className="text-right p-2.5 text-xs font-medium text-theme-muted">{t('common.subtotal')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-theme-border">
|
||||
|
||||
Reference in New Issue
Block a user