// Cuenta — pestaña de cuenta integrada de Cynamonurs (rediseño editorial). // Datos fieles al modelo real de CynamonSup (ver src/Planes.jsx): // Free $0 · Basic $59 MXN/mes · PRO $109 MXN/mes · límites DIARIOS. // Expone a window: CuentaSection, AccountBell, AccountMenu. const { useState: kS, useEffect: kE } = React; // ── Iconos ────────────────────────────────────────────────── const CI = { bell: , mail: , user: , logout: , card: , globe: , cloud: , check: , bcheck: , age: , building: , cam: , refresh: , }; // ── Datos reales del plan (espejo de src/Planes.jsx) ───────── const CYNA_PRICE = { free: { amt: "$0", per: "siempre gratis" }, basic: { amt: "$59", per: "MXN / mes" }, pro: { amt: "$109", per: "MXN / mes" }, }; // Límites DIARIOS por plan (idénticos a la tabla comparativa de Planes). const CYNA_DAILY = { free: { buscar: 3, caso: 1, exportar: 0, calc: 3 }, basic: { buscar: 8, caso: 5, exportar: 8, calc: 30 }, pro: { buscar: Infinity, caso: Infinity, exportar: Infinity, calc: Infinity }, }; const CYNA_USED = { buscar: 3, caso: 2, exportar: 1, calc: 6 }; // uso simulado de hoy const CYNA_LIMIT_ROWS = [ ["buscar", "Buscar diagnósticos"], ["caso", "Crear caso de paciente"], ["exportar", "Exportar PDF / Word"], ["calc", "Calculadoras clínicas"], ]; const CYNA_NOTIFS = [ { id: 1, unread: true, title: "Tu suscripción se renueva en 5 días", desc: "Se cobrará automáticamente el 1 jul. Te lo recordamos por correo.", time: "Hace 2 h" }, { id: 2, unread: true, title: "Nuevas escalas en Herramientas", desc: "Añadimos Glasgow pediátrica y Silverman a tus calculadoras.", time: "Ayer" }, { id: 3, unread: false, title: "Copia de seguridad completada", desc: "Tus casos y planes se respaldaron correctamente en la nube.", time: "Hace 2 días" }, ]; // ── Store de perfil (localStorage + evento para sincronizar nav) ── const ProfileStore = { key: "cyna_profile", defaults: { nombre: "María García", role: "Enfermera · Cuidados intensivos", edad: "34", sexo: "Femenino", tipo: "Pública", institucion: "Hospital General Dr. Salvador", photo: null, }, get() { try { return { ...this.defaults, ...JSON.parse(localStorage.getItem(this.key) || "{}") }; } catch (e) { return { ...this.defaults }; } }, set(patch) { const next = { ...this.get(), ...patch }; try { localStorage.setItem(this.key, JSON.stringify(next)); } catch (e) {} window.dispatchEvent(new CustomEvent("cyna-profile", { detail: next })); return next; }, }; function useProfile() { const [p, setP] = kS(ProfileStore.get()); kE(() => { const fn = (e) => setP(e.detail || ProfileStore.get()); window.addEventListener("cyna-profile", fn); return () => window.removeEventListener("cyna-profile", fn); }, []); return p; } // ── Store de plan ──────────────────────────────────────────── const PlanState = { key: "cyna_plan", get() { try { return localStorage.getItem(this.key) || "basic"; } catch (e) { return "basic"; } }, set(id) { try { localStorage.setItem(this.key, id); } catch (e) {} window.dispatchEvent(new CustomEvent("cyna-plan", { detail: id })); }, }; function usePlan() { const [p, setP] = kS(PlanState.get()); kE(() => { const fn = (e) => setP(e.detail || PlanState.get()); window.addEventListener("cyna-plan", fn); return () => window.removeEventListener("cyna-plan", fn); }, []); return p; } const cynaInitials = (name) => (name || "").trim().split(/\s+/).slice(0, 2).map(s => s[0] || "").join("").toUpperCase() || "MG"; // ── Avatar con subir foto (input creado al vuelo) ──────────── function AvatarUpload({ src, label, onPick, size = 84, showCam = true }) { const pick = () => { const inp = document.createElement("input"); inp.type = "file"; inp.accept = "image/*"; inp.onchange = (e) => { const f = e.target.files && e.target.files[0]; if (!f) return; const r = new FileReader(); r.onload = () => onPick(r.result); r.readAsDataURL(f); }; inp.click(); }; return (
{src ? "" : label}
{showCam && }
); } // ── Fila con toggle / chevron ─────────────────────────────── function CRow({ icon, title, desc, on, onToggle, lock, chev, danger, onClick }) { return (
{icon}
{title}
{desc &&
{desc}
}
{onToggle &&
); } // ── Modal Editar perfil ───────────────────────────────────── function EditProfileModal({ onClose }) { const cur = ProfileStore.get(); const [f, setF] = kS(cur); const up = (k) => (e) => setF(s => ({ ...s, [k]: e.target.value })); const save = () => { ProfileStore.set(f); onClose(); }; return (
e.stopPropagation()}>

Editar perfil

setF(s => ({ ...s, photo: d }))}/>
JPG o PNG · máx. 5 MB {f.photo && }
{["Pública", "Privada"].map(t => ( ))}
); } // ── Modal Cambiar de plan (usa los planes reales) ─────────── function ChangePlanModal({ current, onClose, onChoose }) { const planes = window.PLANES || []; return (
e.stopPropagation()}>

Cambiar de plan

{planes.map(pl => { const isCur = pl.id === current; return (
{isCur ? Plan actual : (pl.destacado ? Recomendado : null)}
{pl.nombre}
{pl.precio}{pl.periodo}

{pl.lema}

    {pl.bullets.map((b, i) =>
  • {CI.bcheck}{b}
  • )}
{isCur ? : }
); })}
Stripe Al cambiar de plan te llevaremos a Stripe para confirmar el pago de forma segura. Puedes cambiar o cancelar cuando quieras; el ajuste se aplica en tu próximo ciclo.
); } // ── Tarjeta de suscripción (según plan actual) ────────────── function SubCard({ plan, onChangePlan }) { const [autopay, setAutopay] = kS(true); const [preavisar, setPreavisar] = kS(true); const planObj = (window.PLANES || []).find(p => p.id === plan) || { nombre: "Basic" }; const price = CYNA_PRICE[plan] || CYNA_PRICE.basic; const isFree = plan === "free"; return (
CynamonSup · {isFree ? "Con anuncios" : "Sin anuncios"}
Plan {planObj.nombre}
{isFree ? "Gratis" : "Activo"}
Precio
{price.amt} {price.per}
Siguiente cobro
{isFree ? "Sin cobros" : "1 jul 2026"}
Renueva
{isFree ? "—" : "Cada mes"}
{!isFree && (
setAutopay(v => !v)}/> setPreavisar(v => !v)}/>
)}
{!isFree && }
Stripe{isFree ? "Sube a Basic o PRO para guardar en la nube y quitar anuncios." : "Tus pagos se procesan de forma segura. Puedes cancelar cuando quieras."}
); } // ── Sección de límites diarios (según plan) ───────────────── function LimitsCard({ plan }) { const daily = CYNA_DAILY[plan] || CYNA_DAILY.basic; return (
{CYNA_LIMIT_ROWS.map(([k, name]) => { const max = daily[k]; if (max === 0) { return (
{name} No incluido en Free
); } if (!isFinite(max)) { return (
{name} Ilimitado
); } const used = Math.min(CYNA_USED[k], max); const pct = Math.round((used / max) * 100); return (
{name} {used} / {max} usados
= 80 ? "warn" : ""} style={{ width: pct + "%" }}/>
); })}
{CI.refresh}Los límites son diarios y se reinician cada día. {plan !== "pro" ? "¿Necesitas más? Sube a PRO para uso ilimitado." : "Tienes uso ilimitado."}
); } // ── SECCIÓN CUENTA (pestaña dentro de la app) ─────────────── function CuentaSection() { const profile = useProfile(); const plan = usePlan(); const [edit, setEdit] = kS(false); const [changePlan, setChangePlan] = kS(false); const [toast, setToast] = kS(null); const [emailNotif, setEmailNotif] = kS(true); kE(() => { if (!toast) return; const t = setTimeout(() => setToast(null), 3200); return () => clearTimeout(t); }, [toast]); const choosePlan = (pl) => { PlanState.set(pl.id); setChangePlan(false); setToast(`Plan actualizado a ${pl.nombre}. El cobro lo procesa Stripe.`); }; const logout = () => (window.__cynaLogout ? window.__cynaLogout() : null); return (
{/* Banner de perfil con foto editable */}
ProfileStore.set({ photo: d })}/>

{profile.nombre}

{profile.role}

{CI.age} {profile.edad} años {CI.user} {profile.sexo} {CI.building} Institución {profile.tipo.toLowerCase()} {profile.institucion && {profile.institucion}}
{/* Estadísticas acumuladas */}
Tu actividad

Estadísticas

24
Casos guardados
137
Diagnósticos consultados
9 días
Racha activa
{/* Usos disponibles hoy */}
Plan {(window.PLANES || []).find(p => p.id === plan)?.nombre || "Basic"}

Usos disponibles hoy

{/* Suscripción */}
CynamonSup

Suscripción

setChangePlan(true)}/>
{/* Configuración */}
Datos y preferencias

Configuración

setEmailNotif(v => !v)}/>
{edit && setEdit(false)}/>} {changePlan && setChangePlan(false)} onChoose={choosePlan}/>} {toast &&
{CI.check}{toast}
}
); } // ── Widget NAV: campana de notificaciones ─────────────────── function AccountBell() { const [open, setOpen] = kS(false); const unread = CYNA_NOTIFS.filter(n => n.unread).length; return (
{open && (
setOpen(false)}/>
Notificaciones
{CYNA_NOTIFS.map(n => (
{n.title}
{n.desc}
{n.time}
))}
{CI.mail}Estos avisos también llegan a tu correo electrónico.
)}
); } // ── Widget NAV: avatar + menú de usuario ──────────────────── function AccountMenu({ onAccount }) { const profile = useProfile(); const [open, setOpen] = kS(false); const go = (fn) => { setOpen(false); fn && fn(); }; const logout = () => (window.__cynaLogout ? window.__cynaLogout() : null); return (
{open && (
setOpen(false)}/>
)}
); } Object.assign(window, { CuentaSection, AccountBell, AccountMenu });