// 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 &&
{CI.cam} }
);
}
// ── Fila con toggle / chevron ───────────────────────────────
function CRow({ icon, title, desc, on, onToggle, lock, chev, danger, onClick }) {
return (
{icon}
{onToggle &&
{ e.stopPropagation(); onToggle(); }} aria-label={title}/>}
{lock && {lock} }
{chev && › }
);
}
// ── 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
×
Cancelar
Guardar cambios
);
}
// ── 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
?
Tu plan actual
:
onChoose(pl)}>Cambiar a {pl.nombre} }
);
})}
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.
Cerrar
);
}
// ── 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)}/>
)}
Cambiar de plan
{!isFree && Administrar pago en Stripe }
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 (
);
}
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} }
setEdit(true)}>Editar perfil
{/* Estadísticas acumuladas */}
137
Diagnósticos consultados
{/* Usos disponibles hoy */}
Plan {(window.PLANES || []).find(p => p.id === plan)?.nombre || "Basic"}
Usos disponibles hoy
{/* Suscripción */}
{/* 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 (
setOpen(o => !o)} aria-label="Notificaciones">
{CI.bell}
{unread > 0 && {unread} }
{open && (
setOpen(false)}/>
Notificaciones
setOpen(false)}>Cerrar
{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 (
setOpen(o => !o)}>
{cynaInitials(profile.nombre)}
{profile.nombre.split(" ")[0]}
▾
{open && (
setOpen(false)}/>
go(onAccount)}>{CI.user}Mi cuenta
go(onAccount)}>{CI.card}Suscripción
go(logout)}>{CI.logout}Cerrar sesión
)}
);
}
Object.assign(window, { CuentaSection, AccountBell, AccountMenu });