);
}
/* =====================================================================
HOME
===================================================================== */
/* Guía de bienvenida · se muestra hasta que el cliente la cierra */
const P_GUIDE_KEY = 'trascend_portal_guide_v1';
const P_SEEN_KEY = 'trascend_portal_seen_v1';
/* Onboarding del cliente · no es un texto para leer: son cuatro pasos que se
marcan solos con el estado real de su cuenta. Cuando los cuatro están
hechos, la tarjeta se apaga y no vuelve. */
function PortalGuide({ user, setRoute }) {
const cid = user.clientId;
const kSeen = P_SEEN_KEY + ':' + cid;
const kOff = P_GUIDE_KEY + ':' + cid;
const [dismissed, setDismissed] = React.useState(() => { try { return localStorage.getItem(kOff) === 'off'; } catch (e) { return false; } });
const [seen, setSeen] = React.useState(() => { try { return JSON.parse(localStorage.getItem(kSeen) || '[]'); } catch (e) { return []; } });
const conns = PortalData.getConnections(cid) || [];
const requests = PortalData.getRequests(cid) || [];
const flows = PortalData.getFlows(cid) || [];
const runs = PortalData.getFlowRuns(cid) || [];
const mark = (key, route) => {
const next = seen.includes(key) ? seen : seen.concat(key);
setSeen(next);
try { localStorage.setItem(kSeen, JSON.stringify(next)); } catch (e) {}
setRoute(route);
};
const steps = [
{ k: 'conexiones', r: 'conexiones', t: 'Conectá tus canales',
d: 'Vinculás tu WhatsApp y tus redes para que trabajemos sobre tus cuentas, no sobre las nuestras.',
done: conns.some((c) => c.status === 'connected'),
hint: 'Todo queda a tu nombre y podés revocarlo cuando quieras.' },
{ k: 'operacion', r: 'operacion', t: 'Mirá qué está corriendo',
d: 'En Mi operación ves las automatizaciones activas y las horas que te devolvieron este mes.',
done: seen.includes('operacion'),
hint: flows.length ? flows.length + ' automatización(es) en tu cuenta' : 'Todavía no hay nada instalado' },
{ k: 'peticiones', r: 'peticiones', t: 'Dejá tu primer pedido',
d: 'Lo que necesites queda registrado con su estado. Sin cadenas de mails ni recordarle a nadie.',
done: requests.length > 0,
hint: requests.length ? requests.length + ' pedido(s) registrados' : 'Probalo con cualquier cosa: se responde igual' },
{ k: 'proyectos', r: 'proyectos', t: 'Seguí tu proyecto',
d: 'El avance de lo que nos encargaste, con su alcance acordado y la fecha estimada de entrega.',
done: seen.includes('proyectos'),
hint: runs.length ? runs.length + ' ejecución(es) registradas' : 'Sin ejecuciones todavía' },
];
const doneN = steps.filter((s) => s.done).length;
const complete = doneN === steps.length;
if (dismissed) return null;
const close = () => { setDismissed(true); try { localStorage.setItem(kOff, 'off'); } catch (e) {} };
return (
{complete ? 'Ya está todo listo' : 'Poner tu cuenta en marcha'}
{complete
? 'Completaste los cuatro pasos. Podés cerrar esto: tu portal queda igual.'
: 'Cuatro pasos. Se marcan solos a medida que los hacés.'}
{doneN}/{steps.length}
{steps.map((it, i) => (
))}
);
}
function HomePage({ user, setRoute }) {
const cid = user.clientId;
const deals = PortalData.getDeals(cid);
const invoices = PortalData.getInvoices(cid);
const requests = PortalData.getRequests(cid);
const flows = PortalData.getFlows(cid) || [];
const runs = PortalData.getFlowRuns(cid) || [];
const activeDeals = deals.filter(d=>!['ganado','perdido'].includes(d.stage)).length;
const liveFlows = flows.filter(f=>f.status==='produccion').length;
const savedH = flows.reduce((a,f)=>a+Number(f.hours_saved_month||0),0);
const pendInvoices = invoices.filter(i=>['pendiente','vencida'].includes(i.status)).length;
const openRequests = requests.filter(r=>r.status!=='resuelta').length;
/* Lo último que hizo su operación: la prueba de que corre sin que él mire. */
const lastRuns = runs.slice().sort((a,b)=>String(b.ran_at).localeCompare(String(a.ran_at))).slice(0,4);
const flowName = (id) => (flows.find(f=>f.id===id)||{}).name || 'Automatización';
const recentReqs = requests.slice(0,3);
return (
Hola, {user.contactName.split(' ')[0]} 👋
Resumen del trabajo que estamos haciendo para {user.client.name}.
Todavía no hay ejecucionesCuando tu primera automatización entre en producción, aparecen acá.
: lastRuns.map(r=>(
{r.status==='ok'?'✓':'!'}
{flowName(r.flow_id)}
{r.status==='ok' ? (Number(r.items||0)+' elemento(s) procesados sin error') : (r.error || 'Terminó con error, lo estamos viendo')}
{fmtDT(r.ran_at)}
))
}
Mis solicitudes
{recentReqs.length===0
?
Sin solicitudes aún
: recentReqs.map(r=>(
{fmtDate(r.created_at)}
{r.title}
{r.team_note &&
Trascend: {r.team_note}
}
))
}
);
}
/* =====================================================================
PROYECTOS — el avance de lo que nos encargó
===================================================================== */
function ProyectosPage({ user }) {
const deals = PortalData.getDeals(user.clientId);
return (
Tus proyectos
Alcance y precio se acuerdan por escrito antes de arrancar
{deals.length===0 ?
Sin proyectos registradosCuando arranquemos el primero, su avance aparece acá.
);
}
/* =====================================================================
FACTURAS Y PRESUPUESTOS — lo que se le cobró y lo que se le propuso
===================================================================== */
function FacturasPage({ user }) {
const invoices = PortalData.getInvoices(user.clientId);
const quotes = PortalData.getQuotes(user.clientId);
const pend = invoices.filter(i=>['pendiente','vencida'].includes(i.status));
return (
Facturas
{pend.length > 0 && }
{invoices.length===0 ?
Sin facturas todavíaCuando emitamos la primera, aparece acá con su estado.
);
}
/* =====================================================================
PETICIONES — lo que necesita, registrado y con estado
===================================================================== */
function PeticionesPage({ user }) {
const [reqs, setReqs] = useState(()=>PortalData.getRequests(user.clientId));
const [sf, setSF] = useState(false);
const [toast, toastNode] = useToast();
const [f, setF] = useState({ title:'', type:'consulta', description:'' });
const set = (k) => (e) => setF({ ...f, [k]: e.target.value });
const reload = ()=>setReqs(PortalData.getRequests(user.clientId));
async function send() {
if (f.title.trim().length < 3) { toast('Ponele un título para saber de qué se trata'); return; }
await PortalData.createRequest(user.clientId, user.contactId, f);
setF({ title:'', type:'consulta', description:'' });
setSF(false); reload(); toast('Pedido registrado — te respondemos por acá');
}
return (
Mis solicitudes
Lo que necesites queda registrado con su estado. Sin cadenas de mails ni recordarle a nadie.
{sf && (
)}
{reqs.length===0 ?
Sin solicitudes todavíaProbalo con cualquier cosa: se responde igual.
: reqs.map(r=>(
{fmtDate(r.created_at)}
{r.title}
{r.description &&
{r.description}
}
{r.team_note &&
Trascend: {r.team_note}
}
))}
{toastNode}
);
}
/* =====================================================================
MENSAJES — el historial de la conversación, sin buscar en el mail
===================================================================== */
function MensajesPage({ user }) {
const threads = PortalData.getThreadsWithMessages(user.clientId) || [];
const [open, setOpen] = useState(threads.length ? threads[0].id : null);
const cur = threads.find(t=>t.id===open) || threads[0];
if (!threads.length) return (
Mensajes
Sin conversaciones todavíaAcá vas a ver el historial de lo que hablamos por WhatsApp y correo, sin tener que buscarlo.