{t('config.portal.help')}
+{t('config.portal.linkTitle')}
+{t('config.portal.linkHelp')}
+{t('config.portal.accessHelp')}
+ {cargandoAccesos ? null : accesos.length === 0 ? ( +{t('config.portal.accessNone')}
+ ) : ( +| {t('config.portal.colPatient')} | +{t('config.portal.colLast')} | +{t('config.portal.colSessions')} | ++ |
|---|---|---|---|
| + {a.paciente} + {a.telefono || a.email || ''} + | +{a.ultimo_acceso ? String(a.ultimo_acceso).slice(0, 16) : '—'} | +{a.sesiones_abiertas} | ++ {a.sesiones_abiertas > 0 && ( + + )} + | +
+
{brand.pie}
diff --git a/frontend/src/pages/MiCuenta.tsx b/frontend/src/pages/MiCuenta.tsx new file mode 100644 index 0000000..1dab57f --- /dev/null +++ b/frontend/src/pages/MiCuenta.tsx @@ -0,0 +1,768 @@ +import type { FC, ReactNode } from 'react'; +import { useCallback, useEffect, useState } from 'react'; +import { useNavigate, useParams } from 'react-router-dom'; +import { + AlertTriangle, Calendar, CreditCard, FileSignature, FileText, KeyRound, + Loader2, LogOut, Package, Printer, Sparkles, User, Wallet, +} from 'lucide-react'; +import { Button, Input, toast } from '../components/ui'; +import { useBusinessTerm, useI18n } from '../lib/i18n'; +import { + portalApi, portalToken, setPortalToken, usePortalConfig, usePortalLogout, + type PortalCita, type PortalConfig, type PortalPerfil, +} from '../services/portal'; + +const money = (n: number | null | undefined, moneda = 'MXN') => + n === null || n === undefined ? '—' : `${moneda} ${Number(n).toLocaleString('es-MX', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; + +const fecha = (iso: string | null | undefined) => { + if (!iso) return '—'; + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return String(iso).slice(0, 10); + return d.toLocaleDateString('es-MX', { day: '2-digit', month: 'short', year: 'numeric' }); +}; + +// ============================================================ +// Página pública: cuenta del cliente (portal de autoservicio). +// Fuera del gate del personal. La sesión es propia (token del portal). +// ============================================================ + +const Card: FC<{ children: ReactNode; className?: string }> = ({ children, className = '' }) => ( +{t('portal.unavailableBody')}
+{t('portal.title')}
+{t('portal.login.intro')}
+ setIdentificador(e.target.value)} autoFocus /> + + {!modoPw && usaCodigo && ( +{hint}
} + + {!modoPw && usaCodigo && enviado && ( +{error}
} + + {config.config.permitir_contrasena && ( + + )} +{
{text}
{it.label}
+{it.value}
+{c.servicio || trm('appointment_singular')}
+{fecha(c.fecha)} · {c.hora} · {c.estado_label}
+{t('portal.citas.anticipo')}
+ )} + {(c.puede_cancelar || c.puede_reagendar) && ( +{t('portal.citas.date')}
+ cargarSlots(e.target.value)} + className="w-full rounded-xl border border-theme-border bg-theme-bg px-3 py-2 text-sm text-theme-heading" /> + {fechaNueva && ( + slots.length === 0 ?{t('portal.citas.noSlots')}
: ( +{t('portal.citas.slot')}
+{t('portal.pagos.balance')}
+{money(data.saldo_pendiente, data.moneda)}
+ {config.config.permitir_descargar_estado_cuenta && ( + + )} +{p.folio}
+{fecha(p.fecha)} · {p.metodo_label}
+{money(p.monto, p.moneda)}
+{v.folio}
+{fecha(v.fecha)} · {v.estado}
+{money(v.total, v.moneda)}
+{t('portal.monedero.points')}
+{data.points}
+{t('portal.monedero.mxn')}
+{money(data.equivalent_mxn)}
+{t('portal.monedero.usd')}
+{data.equivalent_usd === null ? '—' : `USD ${data.equivalent_usd}`}
+{m.descripcion || '—'}
+{fecha(m.fecha)}
+= 0 ? 'text-emerald-600' : 'text-rose-600'}`}>{m.puntos > 0 ? '+' : ''}{m.puntos}
+{p.paquete || p.servicio}
+ {p.estado} +{m.plan}
+ {m.estado} +{f.plantilla}
+{f.folio}
+{f.plantilla}
+{f.folio} · {fecha(f.llenado_at)}{f.tiene_firma ? ` · ${t('portal.formularios.signed')}` : ''}
+{r.servicio || r.folio}
+{fecha(r.fecha)}
+{r.contenido}
+{t('portal.perfil.readonly')}
} +{t('portal.perfil.noPhotos')}
: ( +${data.paciente.nombre} · ${data.paciente.patient_id || ''}
${data.paciente.telefono || ''} · ${data.paciente.email || ''}
Generado: ${String(data.generado).slice(0, 16)}
+| Fecha | Folio | Método | Monto |
|---|
{config.brand.nombre}
+ {portalActivo && ( + + )}