// portal-canjes.jsx — Canje tracker (patient) + Operator submission queue
const { useState, useMemo } = React;
// ── Helpers ───────────────────────────────────────────────────────────────────
function getProgramById(id) { return (window.LAB_PROGRAMS || []).find(p => p.id === id); }
function getLabById(id) { return (window.LABS || []).find(l => l.id === id); }
function getPatientById(id) { return (window.PATIENTS || []).find(p => p.id === id); }
function programTypeLabel(buyQty, freeQty) { return `${buyQty}+${freeQty}`; }
// ── EnrollModal — browse available programs to join ───────────────────────────
function EnrollModal({ patient, enrollments, onEnroll, onClose }) {
const C = window.C;
const [labFilter, setLabFilter] = useState('all');
const enrolledIds = enrollments.map(e => e.programId);
const available = (window.LAB_PROGRAMS || []).filter(p => !enrolledIds.includes(p.id));
const filtered = labFilter === 'all' ? available : available.filter(p => p.labId === labFilter);
return (
{/* Lab filter chips */}
{['all', 'lafage', 'lancasco', 'denk', 'pharmaetica'].map(id => {
const lab = id === 'all' ? null : getLabById(id);
const active = labFilter === id;
return (
setLabFilter(id)} style={{
fontSize: 11, fontWeight: 700, padding: '4px 12px', borderRadius: 9999,
border: `1.5px solid ${active ? (lab?.color || C.primary) : C.border}`,
background: active ? (lab?.colorLight || C.primarySurface) : C.surface,
color: active ? (lab?.color || C.primary) : C.text2, cursor: 'pointer',
}}>{id === 'all' ? 'Todos los labs' : lab?.name}
);
})}
{filtered.map(prog => {
const lab = getLabById(prog.labId);
return (
{prog.medicine}
{prog.condition}
{programTypeLabel(prog.buyQty, prog.freeQty)}
PROG.
onEnroll(prog)} style={{
width: '100%', padding: '8px', borderRadius: 10, border: 'none',
background: C.primary, color: '#fff', fontSize: 12, fontWeight: 700, cursor: 'pointer',
}}>+ Inscribirme
);
})}
{filtered.length === 0 &&
}
);
}
// ── CanjeCard — single enrolled program ───────────────────────────────────────
function CanjeCard({ enrollment, onRegisterPurchase }) {
const C = window.C;
const prog = getProgramById(enrollment.programId);
const lab = getLabById(prog?.labId);
if (!prog || !lab) return null;
const total = prog.buyQty;
const logged = enrollment.purchasesLogged;
const pending = enrollment.purchasesPending;
const effective = logged + pending; // shown as total purchases
const isReady = enrollment.rewardsPending > 0;
const pct = Math.min((effective / total) * 100, 100);
return (
{/* Header row */}
{prog.medicine}
{programTypeLabel(prog.buyQty, prog.freeQty)}
{pending > 0 && (
{pending} en espera de portal
)}
{/* Program ratio badge */}
{programTypeLabel(prog.buyQty, prog.freeQty)}
CANJE
{/* Progress */}
{isReady ? (
✦
¡Canje disponible!
{enrollment.rewardsPending} caja{enrollment.rewardsPending > 1 ? 's' : ''} gratis lista{enrollment.rewardsPending > 1 ? 's' : ''} para recoger
) : (
{effective} / {total} compras registradas
{total - effective > 0 ? `Falta${total - effective > 1 ? 'n' : ''} ${total - effective}` : 'Completo'}
)}
{/* Condition tag */}
{prog.condition}
{/* Actions */}
{!isReady && (
onRegisterPurchase(enrollment, prog)} style={{
width: '100%', padding: '9px', borderRadius: 10, border: 'none',
background: C.primarySurface, color: C.primary, fontSize: 12, fontWeight: 700, cursor: 'pointer',
}}>
+ Registrar compra reciente
)}
);
}
// ── RegisterPurchaseModal ─────────────────────────────────────────────────────
function RegisterPurchaseModal({ enrollment, prog, patient, onConfirm, onClose }) {
const C = window.C;
const [date, setDate] = useState(new Date().toISOString().split('T')[0]);
const lab = getLabById(prog?.labId);
return (
{prog?.medicine}
Programa {prog?.buyQty}+{prog?.freeQty} · {lab?.name}
¿Cómo funciona? Un operador de la farmacia enviará esta compra al portal de {lab?.name}. Una vez confirmada, tu progreso se actualizará automáticamente.
Cancelar
onConfirm(enrollment, date)} style={{ flex: 2, padding: 11, borderRadius: 12, border: 'none', background: C.primary, color: '#fff', fontWeight: 700, fontSize: 13, cursor: 'pointer' }}>✓ Registrar compra
);
}
// ── CanjesTab — patient-facing view ──────────────────────────────────────────
function CanjesTab({ patient, enrollments, onAddEnrollment, onRegisterPurchase }) {
const C = window.C;
const [showEnroll, setShowEnroll] = useState(false);
const [regTarget, setRegTarget] = useState(null); // { enrollment, prog }
const myEnrollments = enrollments.filter(e => e.customerId === patient.id);
const readyCanjes = myEnrollments.filter(e => e.rewardsPending > 0);
const inProgress = myEnrollments.filter(e => e.rewardsPending === 0);
return (
{/* Ready banner */}
{readyCanjes.length > 0 && (
✦
{readyCanjes.length === 1 ? '¡Tienes 1 canje listo!' : `¡Tienes ${readyCanjes.length} canjes listos!`}
Pasa a Farmacia Santos para recogerlos
)}
{/* Ready canjes */}
{readyCanjes.length > 0 && (
<>
LISTOS PARA RECOGER
{readyCanjes.map(e => (
setRegTarget({ enr, prog })} />
))}
>
)}
{/* In-progress */}
{inProgress.length > 0 && (
<>
0 ? 8 : 0 }}>EN PROGRESO
{inProgress.map(e => (
setRegTarget({ enr, prog })} />
))}
>
)}
{myEnrollments.length === 0 && (
)}
{/* Enroll CTA */}
setShowEnroll(true)} style={{
width: '100%', marginTop: 8, padding: '13px', borderRadius: 14,
border: `2px dashed ${C.primaryLight}`, background: 'transparent',
color: C.primary, fontSize: 13, fontWeight: 700, cursor: 'pointer',
}}>
+ Inscribirme en un programa de canje
{/* Modals */}
{showEnroll && (
{ onAddEnrollment(patient.id, prog); setShowEnroll(false); }} onClose={() => setShowEnroll(false)} />
)}
{regTarget && (
{ onRegisterPurchase(enr.id, date); setRegTarget(null); }}
onClose={() => setRegTarget(null)} />
)}
);
}
// ── OperatorQueue — grouped submission list by lab ────────────────────────────
function OperatorQueue({ submissions, enrollments, onMarkSubmitted }) {
const C = window.C;
const [confirmed, setConfirmed] = useState(null);
const pending = submissions.filter(s => !s.submittedToPortal);
const byLab = {};
pending.forEach(s => {
const prog = getProgramById(s.programId);
if (!prog) return;
if (!byLab[s.labId]) byLab[s.labId] = [];
byLab[s.labId].push({ ...s, prog });
});
const handleMark = (sub) => {
setConfirmed(null);
onMarkSubmitted(sub.id);
};
return (
{/* Summary */}
ENVÍOS PENDIENTES
{pending.length}
en {Object.keys(byLab).length} portal{Object.keys(byLab).length > 1 ? 'es' : ''}
Última actualización: Hoy 5:00 AM
{pending.length === 0 &&
}
{/* Per-lab groups */}
{Object.entries(byLab).map(([labId, items]) => {
const lab = getLabById(labId);
if (!lab) return null;
return (
{/* Lab header */}
{lab.name}
{items.length} envío{items.length > 1 ? 's' : ''} pendiente{items.length > 1 ? 's' : ''}
Abrir portal ↗
{/* Submission rows */}
{items.map(sub => {
const patient = getPatientById(sub.customerId);
const enr = enrollments.find(e => e.id === sub.enrollmentId);
if (!patient) return null;
return (
{/* Avatar */}
{patient.iniciales}
{patient.nombre}
{patient.dni}
{sub.prog.medicine}
Compra: {new Date(sub.date).toLocaleDateString('es-HN', { day: 'numeric', month: 'short' })}
{enr && ` · Progreso: ${enr.purchasesLogged + enr.purchasesPending}/${sub.prog.buyQty}`}
setConfirmed(sub)} style={{
flexShrink: 0, padding: '7px 12px', borderRadius: 10, border: 'none',
background: C.primarySurface, color: C.primary, fontSize: 11, fontWeight: 700, cursor: 'pointer',
}}>
✓ Enviado
);
})}
);
})}
{/* Confirm modal */}
{confirmed && (
setConfirmed(null)} title="Confirmar envío al portal">
{(() => {
const patient = getPatientById(confirmed.customerId);
const lab = getLabById(confirmed.labId);
return (
<>
{confirmed.prog.medicine}
{patient?.nombre} · {patient?.dni}
Portal: {lab?.name} · {new Date(confirmed.date).toLocaleDateString('es-HN')}
¿Confirmás que ya registraste esta compra en el portal de {lab?.name} ?
El progreso del cliente se actualizará.
setConfirmed(null)} style={{ flex: 1, padding: 11, borderRadius: 12, border: `1.5px solid ${window.C.border}`, background: window.C.surface, color: window.C.text2, fontWeight: 700, fontSize: 13, cursor: 'pointer' }}>Cancelar
handleMark(confirmed)} style={{ flex: 2, padding: 11, borderRadius: 12, border: 'none', background: window.C.primary, color: '#fff', fontWeight: 700, fontSize: 13, cursor: 'pointer' }}>✓ Confirmar
>
);
})()}
)}
);
}
Object.assign(window, { CanjesTab, OperatorQueue });