feat: add blue parrot static html kpis dashboard connected to windsor data
This commit is contained in:
535
src/app/dashboard/kpis/kpis/Blue Parrot-kpis2.html
Normal file
535
src/app/dashboard/kpis/kpis/Blue Parrot-kpis2.html
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
|
||||||
|
<title>Blue Parrot · Panel de creatividades</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;800&display=swap">
|
||||||
|
<style>
|
||||||
|
/* Layout: cabecera de marca con pestañas; portada ejecutiva (indicadores con tendencia, lectura del mes, avisos, próximas piezas) y vistas de trabajo por debajo. */
|
||||||
|
:root{
|
||||||
|
--bg:#F4F6FB; --surface:#FFFFFF; --fg:#16203A; --muted:#55607A; --line:#D6DDEC;
|
||||||
|
--blue:#2A4D9C; --blue-soft:#E3EAF8; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#FFFFFF;
|
||||||
|
--series:#2A4D9C; --series-soft:#B9C8EA;
|
||||||
|
--ok:#1E7A46; --ok-bg:#DDF2E5; --warn:#8A5A00; --warn-bg:#FCEFC7; --bad:#A6321F; --bad-bg:#F9E0DB; --info:#2A4D9C; --info-bg:#E3EAF8; --done:#3D4660; --done-bg:#E4E7EF;
|
||||||
|
--font:Poppins,"Segoe UI",system-ui,sans-serif;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#0F1524; --surface:#182036; --fg:#EEF1F8; --muted:#A3ADC4; --line:#2C3754;
|
||||||
|
--blue:#8FB0F2; --blue-soft:#22305A; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#0F1524;
|
||||||
|
--series:#8FB0F2; --series-soft:#3A4F85;
|
||||||
|
--ok:#7FD6A1; --ok-bg:#163526; --warn:#F2CB63; --warn-bg:#3A2E0E; --bad:#F29C8C; --bad-bg:#40201A; --info:#8FB0F2; --info-bg:#22305A; --done:#C2C9DA; --done-bg:#2A3148;
|
||||||
|
color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#0F1524; --surface:#182036; --fg:#EEF1F8; --muted:#A3ADC4; --line:#2C3754;
|
||||||
|
--blue:#8FB0F2; --blue-soft:#22305A; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#0F1524;
|
||||||
|
--series:#8FB0F2; --series-soft:#3A4F85;
|
||||||
|
--ok:#7FD6A1; --ok-bg:#163526; --warn:#F2CB63; --warn-bg:#3A2E0E; --bad:#F29C8C; --bad-bg:#40201A; --info:#8FB0F2; --info-bg:#22305A; --done:#C2C9DA; --done-bg:#2A3148;
|
||||||
|
color-scheme:dark }
|
||||||
|
body{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:15px;line-height:1.5}
|
||||||
|
.wrap{max-width:1120px;margin:0 auto;padding-inline:20px;padding-block:28px 72px;display:flex;flex-direction:column;gap:24px}
|
||||||
|
header{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:flex-end;justify-content:space-between}
|
||||||
|
.eyebrow,.lab,label{font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
h1{margin:0;font-size:30px;font-weight:800;line-height:1.15;text-wrap:balance}
|
||||||
|
h1 span{color:var(--blue)}
|
||||||
|
h2{margin:0;font-size:18px;font-weight:600;text-wrap:balance}
|
||||||
|
h2 small{font-weight:400;font-size:13px;color:var(--muted);margin-left:8px}
|
||||||
|
h3{margin:0;font-size:14px;font-weight:600}
|
||||||
|
.pack{font-size:13px;color:var(--muted);max-width:420px}
|
||||||
|
.note{font-size:13px;color:var(--muted);margin:0}
|
||||||
|
.nav{display:flex;flex-wrap:wrap;gap:8px;border-bottom:1px solid var(--line);padding-bottom:12px;align-items:center}
|
||||||
|
.tab,.chip,.btn{font:inherit;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--fg)}
|
||||||
|
.tab{font-size:14px;font-weight:600;padding:9px 18px;border-radius:99px}
|
||||||
|
.tab[aria-pressed="true"]{background:var(--yellow);border-color:var(--yellow);color:var(--on-yellow)}
|
||||||
|
.nav .grow{flex:1}
|
||||||
|
.chip{font-size:13px;font-weight:500;padding:7px 14px;border-radius:99px}
|
||||||
|
.chip[aria-pressed="true"]{background:var(--blue);color:var(--on-blue);border-color:var(--blue)}
|
||||||
|
.btn{font-size:13px;font-weight:600;padding:8px 14px;border-radius:10px}
|
||||||
|
.btn.primary{background:var(--blue);border-color:var(--blue);color:var(--on-blue)}
|
||||||
|
.btn.current{border-color:var(--blue);color:var(--blue)}
|
||||||
|
.btn:disabled{opacity:.5;cursor:default}
|
||||||
|
:is(.tab,.chip,.btn,textarea,input,.row,.card,.cal a,.hit):focus-visible{outline:3px solid var(--yellow);outline-offset:2px}
|
||||||
|
.view,.sec{display:flex;flex-direction:column}
|
||||||
|
.view{gap:30px} .sec{gap:12px}
|
||||||
|
.fbar{display:flex;flex-direction:column;gap:10px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 16px}
|
||||||
|
.fbar input[type=date]{font:inherit;font-size:13px;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:6px 10px}
|
||||||
|
.fbar label{align-self:center}
|
||||||
|
tr.tot td{font-weight:600;background:var(--blue-soft)}
|
||||||
|
.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
.sep{width:1px;height:22px;background:var(--line);margin-inline:4px}
|
||||||
|
.empty{background:var(--surface);border:1px dashed var(--line);border-radius:14px;padding:26px;color:var(--muted)}
|
||||||
|
.empty b{color:var(--fg)}
|
||||||
|
/* tiles */
|
||||||
|
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
|
||||||
|
.tiles.t3{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
|
||||||
|
.tile{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||||
|
.tile b{font-size:26px;font-weight:800;line-height:1.15}
|
||||||
|
.tile small{color:var(--muted);font-size:13px}
|
||||||
|
.tile svg{width:100%;height:36px;display:block;margin-top:4px}
|
||||||
|
.dl{font-size:12px;font-weight:600;padding:2px 8px;border-radius:99px;white-space:nowrap;background:var(--done-bg);color:var(--done)}
|
||||||
|
.dl.good{background:var(--ok-bg);color:var(--ok)} .dl.poor{background:var(--bad-bg);color:var(--bad)}
|
||||||
|
.bar{height:8px;border-radius:99px;background:var(--blue-soft);overflow:hidden}
|
||||||
|
.bar i{display:block;height:100%;background:var(--yellow);border-radius:99px}
|
||||||
|
.two{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:22px}
|
||||||
|
.lines{display:flex;flex-direction:column;gap:8px}
|
||||||
|
.ln{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px 14px;display:flex;gap:12px;align-items:flex-start;min-width:0}
|
||||||
|
.ln p{margin:0;font-size:14px;min-width:0;overflow-wrap:anywhere}
|
||||||
|
.ln p small{display:block;color:var(--muted);font-size:13px}
|
||||||
|
.tag{flex:none;font-size:12px;font-weight:600;padding:3px 10px;border-radius:99px;white-space:nowrap}
|
||||||
|
.tag.good{background:var(--ok-bg);color:var(--ok)} .tag.poor{background:var(--bad-bg);color:var(--bad)} .tag.att{background:var(--warn-bg);color:var(--warn)} .tag.inf{background:var(--info-bg);color:var(--info)}
|
||||||
|
/* piezas */
|
||||||
|
.list{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.item{background:var(--surface);border:1px solid var(--line);border-radius:14px;overflow:hidden}
|
||||||
|
.row{all:unset;box-sizing:border-box;width:100%;cursor:pointer;display:grid;grid-template-columns:64px 92px minmax(0,1fr) auto;gap:14px;align-items:center;padding:14px 18px}
|
||||||
|
.row .d{font-weight:600;font-variant-numeric:tabular-nums;color:var(--muted);font-size:13px}
|
||||||
|
.fmt{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--blue)}
|
||||||
|
.t{font-weight:600;min-width:0;overflow-wrap:anywhere}
|
||||||
|
.t small{display:block;font-weight:400;color:var(--muted);font-size:13px}
|
||||||
|
.st{font-size:12px;font-weight:600;padding:5px 12px;border-radius:99px;white-space:nowrap}
|
||||||
|
.propuesta{background:var(--info-bg);color:var(--info)} .diseno,.revision{background:var(--warn-bg);color:var(--warn)}
|
||||||
|
.cambios{background:var(--bad-bg);color:var(--bad)} .aprobada{background:var(--ok-bg);color:var(--ok)} .publicada{background:var(--done-bg);color:var(--done)}
|
||||||
|
.thumbs{display:flex;gap:10px;overflow-x:auto;padding:0 18px 16px}
|
||||||
|
.thumbs img{height:170px;width:auto;border-radius:10px;border:1px solid var(--line);flex:none}
|
||||||
|
.detail{border-top:1px solid var(--line);padding:18px;display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:22px}
|
||||||
|
.detail > div{min-width:0;display:flex;flex-direction:column;gap:10px}
|
||||||
|
textarea,input[type=text]{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 12px;width:100%;box-sizing:border-box;resize:vertical}
|
||||||
|
.visual{font-size:14px;background:var(--blue-soft);border-radius:10px;padding:10px 12px}
|
||||||
|
.acts{display:flex;flex-wrap:wrap;gap:8px}
|
||||||
|
.cm{font-size:14px;background:var(--bg);border-radius:10px;padding:8px 12px;overflow-wrap:anywhere}
|
||||||
|
.cm time{display:block;font-size:12px;color:var(--muted)}
|
||||||
|
#msg{font-size:13px;color:var(--bad)}
|
||||||
|
.item{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr)}
|
||||||
|
.item .row,.item .detail{grid-column:1 / -1}
|
||||||
|
.item .thumbs{max-width:440px;box-sizing:border-box}
|
||||||
|
@media (max-width:760px){.item{grid-template-columns:minmax(0,1fr)}.item .thumbs{max-width:100%}}
|
||||||
|
.pcopy{margin:0;padding:0 18px 16px;font-size:14px;white-space:pre-line;overflow-wrap:anywhere;color:var(--fg)}
|
||||||
|
.hl{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px}
|
||||||
|
.hp{display:grid;grid-template-columns:110px minmax(0,1fr);gap:14px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:12px;align-items:start}
|
||||||
|
.hp img,.hp .noimg{width:110px;aspect-ratio:4/5;object-fit:cover;border-radius:10px;border:1px solid var(--line);background:var(--blue-soft);display:block}
|
||||||
|
.hp div{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.hp p,.hp summary{margin:0;font-size:13px;line-height:1.5;white-space:pre-line;overflow-wrap:anywhere}
|
||||||
|
.hp summary{cursor:pointer;list-style:none} .hp details[open] summary{display:none}
|
||||||
|
.hp small{font-size:12px;color:var(--muted)}
|
||||||
|
.calw{overflow-x:auto}
|
||||||
|
.cal{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));gap:6px;min-width:860px}
|
||||||
|
.cal .hd{font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;padding:0 4px}
|
||||||
|
.cal .day{background:var(--surface);border:1px solid var(--line);border-radius:10px;min-height:96px;padding:6px;display:flex;flex-direction:column;gap:4px}
|
||||||
|
.cal .day.off{background:transparent;border-style:dashed}
|
||||||
|
.cal .num{font-size:12px;font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.cal a{font-size:12px;line-height:1.3;padding:4px 6px;border-radius:6px;text-decoration:none;cursor:pointer;overflow-wrap:anywhere}
|
||||||
|
.cal a b{font-weight:600}
|
||||||
|
/* publicado */
|
||||||
|
.gal{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px}
|
||||||
|
.card{display:flex;flex-direction:column;gap:6px;min-width:0;text-decoration:none;color:var(--fg);position:relative}
|
||||||
|
.card img,.card .noimg{width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:12px;border:1px solid var(--line);background:var(--blue-soft);display:block}
|
||||||
|
.card b{font-size:13px;font-weight:600;line-height:1.3;overflow-wrap:anywhere}
|
||||||
|
.card small{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.card .fmt{font-size:11px}
|
||||||
|
.card .top{position:absolute;left:8px;top:8px;background:var(--yellow);color:var(--on-yellow);font-size:11px;font-weight:600;padding:3px 8px;border-radius:99px}
|
||||||
|
.best{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:12px}
|
||||||
|
.bp{display:flex;gap:12px;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px;text-decoration:none;color:var(--fg);min-width:0}
|
||||||
|
.bp img{width:64px;height:80px;object-fit:cover;border-radius:8px;flex:none}
|
||||||
|
.bp div{min-width:0;display:flex;flex-direction:column;gap:2px}
|
||||||
|
.bp b{font-size:13px;line-height:1.3;overflow-wrap:anywhere}
|
||||||
|
.bp small{font-size:12px;color:var(--muted)}
|
||||||
|
/* tablas */
|
||||||
|
.tw{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:14px}
|
||||||
|
table{border-collapse:collapse;width:100%;font-size:14px}
|
||||||
|
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
|
||||||
|
td.wrapc{white-space:normal;min-width:200px}
|
||||||
|
tbody tr:last-child td{border-bottom:0}
|
||||||
|
th{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
.n{text-align:right;font-variant-numeric:tabular-nums}
|
||||||
|
a{color:var(--blue)}
|
||||||
|
/* gráficos */
|
||||||
|
.charts{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
|
||||||
|
.ch{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 16px 10px;display:flex;flex-direction:column;gap:2px;min-width:0;position:relative}
|
||||||
|
.ch .cap{font-size:12px;color:var(--muted)}
|
||||||
|
.ch svg{width:100%;height:auto;display:block;overflow:visible}
|
||||||
|
.ch text{font-family:var(--font);font-size:10px;fill:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.ch .end{fill:var(--fg);font-weight:600;font-size:11px}
|
||||||
|
.hit{fill:transparent;cursor:crosshair;outline:none}
|
||||||
|
.hit:hover,.hit:focus{fill:var(--blue-soft);fill-opacity:.55}
|
||||||
|
#tip{position:fixed;z-index:10;pointer-events:none;background:var(--fg);color:var(--bg);font-size:12px;padding:6px 10px;border-radius:8px;max-width:220px}
|
||||||
|
#tip b{display:block;font-size:13px}
|
||||||
|
.fun{display:flex;flex-direction:column;gap:8px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px}
|
||||||
|
.fr{display:grid;grid-template-columns:150px minmax(0,1fr) 110px;gap:12px;align-items:center;font-size:13px}
|
||||||
|
.fr .tr{height:14px;background:transparent}
|
||||||
|
.fr .tr i{display:block;height:100%;min-width:3px;background:var(--series);border-radius:0 4px 4px 0}
|
||||||
|
.fr .v{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
|
||||||
|
.fr small{color:var(--muted);font-weight:400}
|
||||||
|
/* preguntar */
|
||||||
|
.ask{display:flex;flex-direction:column;gap:12px;max-width:760px}
|
||||||
|
.ans{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;white-space:pre-wrap;overflow-wrap:anywhere;font-size:15px;line-height:1.6}
|
||||||
|
@media (max-width:760px){
|
||||||
|
.two,.detail{grid-template-columns:minmax(0,1fr)}
|
||||||
|
.row{grid-template-columns:52px minmax(0,1fr) auto;row-gap:4px}
|
||||||
|
.row .fmt{grid-column:2;grid-row:1} .row .t{grid-column:1 / -1;grid-row:2} .row .st{grid-row:1;grid-column:3}
|
||||||
|
.fr{grid-template-columns:104px minmax(0,1fr) 84px}
|
||||||
|
h1{font-size:24px}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion:no-preference){ .bar i{transition:width .4s} }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<div class="eyebrow">4Ventures · Panel de cliente</div>
|
||||||
|
<h1><span>Blue Parrot School</span></h1>
|
||||||
|
</div>
|
||||||
|
<div class="pack">Paquete PREMIUM: 16 publicaciones y 20 stories al mes, más 2 reels por semana. <span id="fresh"></span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav" aria-label="Vistas del panel">
|
||||||
|
<button class="tab" type="button" data-view="resumen" aria-pressed="true">Resumen</button>
|
||||||
|
<button class="tab" type="button" data-view="creatividades" aria-pressed="false">Creatividades</button>
|
||||||
|
<button class="tab" type="button" data-view="publicado" aria-pressed="false">Publicado</button>
|
||||||
|
<button class="tab" type="button" data-view="canales" aria-pressed="false">KPIs</button>
|
||||||
|
<button class="tab" type="button" data-view="preguntar" aria-pressed="false" id="tab-ask" hidden>Preguntar</button>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<button class="btn" type="button" id="csv" hidden>Exportar CSV</button>
|
||||||
|
</nav>
|
||||||
|
<div id="msg" hidden></div>
|
||||||
|
|
||||||
|
<div class="view" id="v-resumen">
|
||||||
|
<div id="r-body" class="empty"><b>Cargando el resumen…</b><br>Aquí aparecen los indicadores del mes con su tendencia, lo que mejora y empeora, los avisos y las próximas publicaciones.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-creatividades" hidden>
|
||||||
|
<section class="sec">
|
||||||
|
<h2>Histórico mensual<small>enero de 2026 a enero de 2027</small></h2>
|
||||||
|
<div class="filters" id="meses" aria-label="Mes"></div>
|
||||||
|
</section>
|
||||||
|
<section class="tiles" id="quota" aria-label="Cupo del mes"></section>
|
||||||
|
<section class="sec" id="psec">
|
||||||
|
<h2 id="ptit">Propuestas</h2>
|
||||||
|
<div class="filters" id="filters"></div>
|
||||||
|
<div id="list" class="list"><div class="empty"><b>Cargando las piezas…</b><br>Aquí aparecerán las propuestas de temas y copys, con su estado de validación.</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="sec" id="hist"></section>
|
||||||
|
<p class="note">Las plantillas de diseño están en el lienzo «Blue Parrot School · Plantillas Instagram».</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-publicado" hidden>
|
||||||
|
<div id="kpi" class="empty"><b>Cargando los datos de Instagram…</b><br>Aquí se verá el rendimiento de cada pieza publicada en @blueparrotschool.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-canales" hidden>
|
||||||
|
<div id="wk" class="empty"><b>Cargando los KPIs…</b><br>Aquí se verán todos los canales con filtro de fechas, comparación entre periodos y evolución mensual.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-preguntar" hidden>
|
||||||
|
<section class="ask">
|
||||||
|
<h2>Pregunta a los datos</h2>
|
||||||
|
<p class="note">Claude responde con los datos de este panel. Cada pregunta usa tu cuenta de Claude.</p>
|
||||||
|
<div class="filters" id="sugs"></div>
|
||||||
|
<label for="q">Tu pregunta</label>
|
||||||
|
<textarea id="q" rows="3" placeholder="Por ejemplo: ¿qué canal trae los leads más baratos?"></textarea>
|
||||||
|
<div class="acts"><button class="btn primary" type="button" id="ask">Preguntar</button><button class="btn" type="button" id="stop" hidden>Parar</button></div>
|
||||||
|
<div id="ans" class="ans" hidden></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tip" hidden></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var ESTADOS=[["propuesta","Propuesta"],["diseno","En diseño"],["revision","Revisión interna"],["cambios","Cambios pedidos"],["aprobada","Aprobada"],["publicada","Publicada"]];
|
||||||
|
var FORMATOS=[["carrusel","Carrusel"],["post","Post"],["story","Story"],["reel","Reel"]];
|
||||||
|
var CUPO=[["Publicaciones",["carrusel","post"],16],["Stories",["story"],20],["Reels",["reel"],8]];
|
||||||
|
var TIPOS={C:"Carrusel",R:"Reel",I:"Imagen"};
|
||||||
|
var VIEWS=["resumen","creatividades","publicado","canales","preguntar"];
|
||||||
|
var piezas=[], piezasOk=false, open=null, fF="", fE="", pView="lista", pMes=new Date().toISOString().slice(0,7), hist={}, db=null, kData=null, wData=null, kOrd="f", kView="g", view="resumen";
|
||||||
|
var $=function(id){return document.getElementById(id)};
|
||||||
|
function esc(s){return String(s==null?"":s).replace(/[&<>"']/g,function(c){return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]})}
|
||||||
|
function lab(list,k){for(var i=0;i<list.length;i++)if(list[i][0]===k)return list[i][1];return k}
|
||||||
|
function fecha(f){if(!f)return "—";var p=String(f).split("-");return p.length===3?p[2]+"/"+p[1]:f}
|
||||||
|
function num(n){return Math.round(Number(n||0)).toLocaleString("de-DE")}
|
||||||
|
function fmt(v,f){if(v==null)return "—";if(f==="txt")return esc(v);var o={minimumFractionDigits:2,maximumFractionDigits:2};
|
||||||
|
if(f==="eur")return Number(v).toLocaleString("de-DE",o)+" €";if(f==="pct")return Number(v).toLocaleString("de-DE",o)+" %";
|
||||||
|
if(f==="dec")return Number(v).toLocaleString("de-DE",{minimumFractionDigits:1,maximumFractionDigits:1});return num(v)}
|
||||||
|
function compact(v,f){if(f==="eur"&&v>=1000)return num(v)+" €";return fmt(v,f)}
|
||||||
|
function tick(v,f){return f==="eur"?num(v)+" €":f==="pct"?num(v)+" %":num(v)}
|
||||||
|
function pct(a,b){if(a==null||b==null||!a)return null;return Math.round((b-a)/a*100)}
|
||||||
|
function dTxt(p){return p==null?"—":(p>0?"+":"")+p+" %"}
|
||||||
|
function dCls(p,up){if(p==null||!up||p===0)return "";return (p>0)===(up>0)?"good":"poor"}
|
||||||
|
function med(a){if(!a.length)return 0;var b=a.slice().sort(function(x,y){return x-y}),m=b.length>>1;return b.length%2?b[m]:Math.round((b[m-1]+b[m])/2)}
|
||||||
|
function fail(){var m=$("msg");m.hidden=false;m.textContent="No se pudo guardar el cambio. Vuelve a intentarlo en unos segundos."}
|
||||||
|
|
||||||
|
/* ---------- gráficos ---------- */
|
||||||
|
function nice(m){if(m<=0)return 1;var p=Math.pow(10,Math.floor(Math.log10(m))),x=m/p;return (x<=1?1:x<=2?2:x<=5?5:10)*p}
|
||||||
|
function spark(v){var W=200,H=36,mx=Math.max.apply(null,v)||1,n=v.length;
|
||||||
|
var pts=v.map(function(y,i){return [4+i*(W-12)/(n-1),H-4-(y/mx)*(H-10)]});
|
||||||
|
var d=pts.map(function(p,i){return (i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)}).join(" ");
|
||||||
|
return '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="none" aria-hidden="true"><path d="'+d+'" fill="none" stroke="var(--series)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg>'}
|
||||||
|
function chart(o){ /* o:{k,v,f,tipo,labels,cap} */
|
||||||
|
var W=320,H=150,L=40,R=8,T=18,B=22,v=o.v,n=v.length,mx=nice(Math.max.apply(null,v)),pw=W-L-R,ph=H-T-B,bw=pw/n;
|
||||||
|
var y=function(x){return T+ph-(x/mx)*ph},s='<svg viewBox="0 0 '+W+' '+H+'" role="img" aria-label="'+esc(o.k)+'">';
|
||||||
|
[0,.5,1].forEach(function(g){var yy=y(mx*g);s+='<line x1="'+L+'" x2="'+(W-R)+'" y1="'+yy+'" y2="'+yy+'" stroke="var(--line)" stroke-width="1"/>';s+='<text x="'+(L-6)+'" y="'+(yy+3.5)+'" text-anchor="end">'+tick(mx*g,o.f)+'</text>'});
|
||||||
|
if(o.tipo==="col"){var w=Math.min(24,bw-2);v.forEach(function(x,i){var h=Math.max(0,(x/mx)*ph),cx=L+i*bw+(bw-w)/2,r=Math.min(4,h);
|
||||||
|
if(h>0)s+='<path d="M'+cx+' '+(T+ph)+'V'+(T+ph-h+r)+'q0 -'+r+' '+r+' -'+r+'h'+(w-2*r)+'q'+r+' 0 '+r+' '+r+'V'+(T+ph)+'z" fill="var(--series'+(o.hi&&!o.hi[i]?"-soft":"")+')"/>'})}
|
||||||
|
else{var pts=v.map(function(x,i){return [L+i*bw+bw/2,y(x)]}),d=pts.map(function(p,i){return (i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)}).join(" "),e=pts[n-1];
|
||||||
|
s+='<path d="'+d+'L'+e[0]+' '+(T+ph)+'L'+pts[0][0]+' '+(T+ph)+'z" fill="var(--series)" fill-opacity=".1"/><path d="'+d+'" fill="none" stroke="var(--series)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/><circle cx="'+e[0]+'" cy="'+e[1]+'" r="4" fill="var(--series)" stroke="var(--surface)" stroke-width="2"/>';
|
||||||
|
s+='<text class="end" text-anchor="end" x="'+(W-R)+'" y="'+Math.max(11,e[1]-9)+'">'+fmt(v[n-1],o.f)+'</text>'}
|
||||||
|
var lb=o.labels||[],ix=n<=8?v.map(function(_,i){return i}):[0,Math.floor((n-1)/2),n-1];ix.forEach(function(i){if(lb[i]!=null)s+='<text x="'+(L+i*bw+bw/2)+'" y="'+(H-6)+'" text-anchor="middle">'+esc(lb[i])+'</text>'});
|
||||||
|
v.forEach(function(x,i){s+='<rect class="hit" tabindex="0" x="'+(L+i*bw)+'" y="'+T+'" width="'+bw+'" height="'+ph+'" data-tip="'+esc((o.tipl?o.tipl[i]:lb[i])||"")+'|'+esc(fmt(x,o.f))+'"><title>'+esc((o.tipl?o.tipl[i]:lb[i])||"")+': '+esc(fmt(x,o.f))+'</title></rect>'});
|
||||||
|
return '<div class="ch"><h3>'+esc(o.k)+'</h3><div class="cap">'+esc(o.cap||"")+'</div>'+s+'</svg></div>'}
|
||||||
|
function tipShow(el,ev){var t=$("tip"),p=(el.dataset.tip||"").split("|");t.innerHTML='<b>'+esc(p[1]||"")+'</b>'+esc(p[0]||"");t.hidden=false;
|
||||||
|
var r=el.getBoundingClientRect(),x=ev&&ev.clientX!=null?ev.clientX:r.left+r.width/2,yy=r.top;
|
||||||
|
t.style.left=Math.max(8,Math.min(window.innerWidth-t.offsetWidth-8,x-t.offsetWidth/2))+"px";t.style.top=Math.max(8,yy-t.offsetHeight-6)+"px"}
|
||||||
|
document.addEventListener("mousemove",function(ev){var h=ev.target.closest&&ev.target.closest(".hit");if(h)tipShow(h,ev);else $("tip").hidden=true});
|
||||||
|
document.addEventListener("focusin",function(ev){var h=ev.target.closest&&ev.target.closest(".hit");if(h)tipShow(h);else $("tip").hidden=true});
|
||||||
|
|
||||||
|
/* ---------- datos diarios ---------- */
|
||||||
|
var K=null,KC=null,kf={p:"m1",a:"",b:"",c:"prev",ca:"",cb:""};
|
||||||
|
var MN=["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],MNL=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"];
|
||||||
|
var SRC=[
|
||||||
|
{id:"meta",t:"Meta Ads",fuente:"Cuenta publicitaria BPS",res:"Leads",mt:[{k:"Inversión",f:"eur",a:"spend",up:0},{k:"Impresiones",a:"imp",up:0},{k:"Clics en el enlace",a:"clk",up:0},{k:"Leads",a:"res",up:1},{k:"Coste por lead",f:"eur",n:"spend",d:"res",up:-1}],ch:[3,0,4]},
|
||||||
|
{id:"gads",t:"Google Ads",fuente:"Cuenta BPS",res:"Conversiones",mt:[{k:"Inversión",f:"eur",a:"spend",up:0},{k:"Impresiones",a:"imp",up:0},{k:"Clics",a:"clk",up:0},{k:"Conversiones",a:"res",up:1},{k:"Coste por conversión",f:"eur",n:"spend",d:"res",up:-1}],ch:[3,0,4]},
|
||||||
|
{id:"ig",t:"Instagram",fuente:"@blueparrotschool",nota:"Las visualizaciones incluyen las que vienen de anuncios, así que se mueven con la inversión en Meta Ads.",mt:[{k:"Visualizaciones",a:"views",up:1},{k:"Interacciones",a:"inter",up:1},{k:"Me gusta",a:"likes",up:1},{k:"Comentarios",a:"com",up:1},{k:"Guardados",a:"saves",up:1},{k:"Compartidos",a:"shares",up:1},{k:"Toques en enlaces del perfil",a:"taps",up:1}],ch:[0,1,4]},
|
||||||
|
{id:"fb",t:"Facebook",fuente:"Página Blue Parrot School",mt:[{k:"Impresiones de la página",a:"imp",up:1},{k:"Interacciones con publicaciones",a:"eng",up:1},{k:"Visitas a la página",a:"views",up:1},{k:"Nuevos seguidores",a:"fol",up:1}],ch:[0,1]},
|
||||||
|
{id:"li",t:"LinkedIn",fuente:"Página Blue Parrot School",mt:[{k:"Impresiones",a:"imp",up:1},{k:"Clics",a:"clk",up:1},{k:"Interacciones",a:"eng",up:1},{k:"Nuevos seguidores orgánicos",a:"fol",up:1}],ch:[0,3]},
|
||||||
|
{id:"gsc",t:"Búsqueda orgánica en Google",fuente:"Search Console · blueparrotschool.com",nota:"Search Console publica los datos con unos tres días de retraso.",mt:[{k:"Clics",a:"clk",up:1},{k:"Impresiones",a:"imp",up:1},{k:"CTR",f:"pct",n:"clk",d:"imp",x:100,up:1},{k:"Posición media",f:"dec",n:"posw",d:"imp",up:-1}],ch:[0,1,3]},
|
||||||
|
{id:"ga4",t:"Web",fuente:"Google Analytics 4 · propiedad B2C BPS",nota:"Esta propiedad registra muchas menos sesiones que los clics que da Search Console, así que puede no estar midiendo toda la web. Los usuarios son la suma de los usuarios diarios.",mt:[{k:"Sesiones",a:"ses",up:0},{k:"Usuarios",a:"usr",up:0},{k:"Usuarios nuevos",a:"new",up:0},{k:"Eventos clave",a:"key",up:0}],ch:[0,3]},
|
||||||
|
{id:"gbp",t:"Google Business Profile",fuente:"Fichas de Madrid y Málaga",mt:[{k:"Impresiones",a:"imp",up:1},{k:"Llamadas",a:"call",up:1},{k:"Clics a la web",a:"web",up:1},{k:"Solicitudes de ruta",a:"dir",up:1}],ch:[1,2,3]}];
|
||||||
|
function T0(){return Date.parse(K.start+"T00:00:00Z")}
|
||||||
|
function dIdx(s){return Math.round((Date.parse(s+"T00:00:00Z")-T0())/864e5)}
|
||||||
|
function dStr(i){return new Date(T0()+i*864e5).toISOString().slice(0,10)}
|
||||||
|
function ymd(y,m,d){return new Date(Date.UTC(y,m,d)).toISOString().slice(0,10)}
|
||||||
|
function fd(s){var p=s.split("-");return p[2]+"/"+p[1]+"/"+p[0]}
|
||||||
|
function ok(s){return /^\d{4}-\d{2}-\d{2}$/.test(s||"")}
|
||||||
|
function sums(M,a,b){var i0=Math.max(0,dIdx(a)),i1=Math.min(K.n-1,dIdx(b)),o={_any:false};for(var k in M){var s=0,arr=M[k];for(var i=i0;i<=i1;i++){var v=arr[i];if(v!=null){s+=v;o._any=true}}o[k]=s}return o}
|
||||||
|
function val(m,S){if(!S||!S._any)return null;if(m.n)return S[m.d]?S[m.n]/S[m.d]*(m.x||1):null;return S[m.a]}
|
||||||
|
function months(){var out=[],s=K.start.split("-"),y=+s[0],m=+s[1]-1;for(;;){var a=ymd(y,m,1),b=ymd(y,m+1,0);if(a>K.end)break;out.push({a:a,b:b>K.end?K.end:b,lab:MN[m]+" "+String(y).slice(2),full:MNL[m]+" de "+y,part:b>K.end});m++;if(m>11){m=0;y++}}return out}
|
||||||
|
function range(){var E=K.end.split("-"),y=+E[0],m=+E[1]-1,p=kf.p;
|
||||||
|
if(p==="c"&&ok(kf.a)&&ok(kf.b)&&kf.a<=kf.b)return [kf.a<K.start?K.start:kf.a,kf.b>K.end?K.end:kf.b];
|
||||||
|
if(p==="m0")return [ymd(y,m,1),K.end];if(p==="y")return [ymd(y,0,1),K.end];
|
||||||
|
var n=p==="m3"?3:p==="m6"?6:p==="m12"?12:1;return [ymd(y,m-n,1),ymd(y,m,0)]}
|
||||||
|
function cmpRange(a,b){if(kf.c==="none")return null;if(kf.c==="c")return ok(kf.ca)&&ok(kf.cb)&&kf.ca<=kf.cb?[kf.ca,kf.cb]:null;
|
||||||
|
var A=a.split("-").map(Number),B=b.split("-").map(Number);
|
||||||
|
if(kf.c==="yoy")return [ymd(A[0]-1,A[1]-1,A[2]),ymd(B[0]-1,B[1]-1,B[2])];
|
||||||
|
if(A[2]===1&&ymd(B[0],B[1],0)===b){var n=(B[0]-A[0])*12+B[1]-A[1]+1;return [ymd(A[0],A[1]-1-n,1),ymd(A[0],A[1]-1,0)]}
|
||||||
|
var d=dIdx(b)-dIdx(a)+1;return [dStr(dIdx(a)-d),dStr(dIdx(a)-1)]}
|
||||||
|
function weekly(get){var out=[],e=dIdx(K.end);for(var w=12;w>=0;w--){out.push(get(dStr(e-w*7-6),dStr(e-w*7))||0)}return out}
|
||||||
|
|
||||||
|
/* ---------- resumen ---------- */
|
||||||
|
function renderResumen(){
|
||||||
|
if(!K)return;
|
||||||
|
var E=K.end.split("-"),y=+E[0],m=+E[1]-1,a=ymd(y,m-1,1),b=ymd(y,m,0),pa=ymd(y,m-2,1),pb=ymd(y,m-1,0),PM=new Date(Date.UTC(y,m-1,1)),h="";
|
||||||
|
var S=function(id,x,z){return sums(K.src[id].m,x,z)},tl=function(k,get,f,up,sub){var v=get(a,b),p=pct(get(pa,pb),v);
|
||||||
|
return '<div class="tile"><div class="lab">'+k+'</div><div><b>'+compact(v,f)+'</b> '+(p==null?"":'<span class="dl '+dCls(p,up)+'">'+dTxt(p)+'</span>')+'</div><small>'+sub+'</small>'+spark(weekly(get))+'</div>'};
|
||||||
|
var cpl=val(SRC[0].mt[4],S("meta",a,b)),cpa=val(SRC[1].mt[4],S("gads",a,b));
|
||||||
|
h+='<section class="sec"><h2>'+MNL[PM.getUTCMonth()].charAt(0).toUpperCase()+MNL[PM.getUTCMonth()].slice(1)+' de '+PM.getUTCFullYear()+'<small>variación frente al mes anterior · tendencia de 13 semanas</small></h2><div class="tiles t3">'+
|
||||||
|
tl("Inversión publicitaria",function(x,z){return S("meta",x,z).spend+S("gads",x,z).spend},"eur",0,"Meta Ads y Google Ads")+
|
||||||
|
tl("Leads de Meta Ads",function(x,z){return S("meta",x,z).res},"num",1,cpl==null?"":"a "+fmt(cpl,"eur")+" por lead")+
|
||||||
|
tl("Conversiones de Google Ads",function(x,z){return S("gads",x,z).res},"num",1,cpa==null?"":"a "+fmt(cpa,"eur")+" por conversión")+
|
||||||
|
tl("Clics orgánicos en Google",function(x,z){return S("gsc",x,z).clk},"num",1,"Search Console")+
|
||||||
|
tl("Acciones en Google Business",function(x,z){var s=S("gbp",x,z);return s.call+s.web+s.dir},"num",1,"llamadas, web y rutas")+
|
||||||
|
tl("Visualizaciones en Instagram",function(x,z){return S("ig",x,z).views},"num",1,"orgánico y anuncios")+'</div></section>';
|
||||||
|
var ins=[];SRC.forEach(function(s){var c=sums(K.src[s.id].m,a,b),o=sums(K.src[s.id].m,pa,pb);s.mt.forEach(function(mt){var v=val(mt,c),w=val(mt,o),p=pct(w,v);if(p==null||!mt.up||Math.abs(p)<15||w<5)return;
|
||||||
|
ins.push({p:p,good:(p>0)===(mt.up>0),t:s.t+" · "+mt.k,x:fmt(v,mt.f)+", "+dTxt(p)+" frente al mes anterior ("+fmt(w,mt.f)+")"})})});
|
||||||
|
ins.sort(function(x,z){return Math.abs(z.p)-Math.abs(x.p)});
|
||||||
|
var line=function(x,c,l){return '<div class="ln"><span class="tag '+c+'">'+l+'</span><p>'+esc(x.t)+'<small>'+esc(x.x)+'</small></p></div>'},up=ins.filter(function(x){return x.good}).slice(0,4),dn=ins.filter(function(x){return !x.good}).slice(0,4);
|
||||||
|
h+='<div class="two"><section class="sec"><h2>Lo que mejora</h2><div class="lines">'+(up.map(function(x){return line(x,"good","▲ Mejora")}).join("")||'<p class="note">Ningún indicador mejora más de un 15 %.</p>')+'</div></section>'+
|
||||||
|
'<section class="sec"><h2>Lo que empeora</h2><div class="lines">'+(dn.map(function(x){return line(x,"poor","▼ Empeora")}).join("")||'<p class="note">Ningún indicador empeora más de un 15 %.</p>')+'</div></section></div>';
|
||||||
|
var av=((wData&&wData.avisos)||[]).slice();
|
||||||
|
if(piezasOk){var mh=new Date().toISOString().slice(0,7);CUPO.forEach(function(c){var n=piezas.filter(function(p){return c[1].indexOf(p.formato)>=0&&String(p.fecha).slice(0,7)===mh}).length;if(n<c[2])av.unshift({n:"att",t:c[0]+": "+n+" de "+c[2]+" planificadas este mes",d:"Faltan "+(c[2]-n)+" para cubrir el paquete contratado."})});
|
||||||
|
var pend=piezas.filter(function(p){return p.estado==="revision"||p.estado==="cambios"}).length;if(pend)av.unshift({n:"att",t:pend===1?"1 pieza espera revisión o cambios":pend+" piezas esperan revisión o cambios",d:"Están en la pestaña Creatividades."})}
|
||||||
|
h+='<div class="two"><section class="sec"><h2>Avisos</h2><div class="lines">'+(av.map(function(x){return '<div class="ln"><span class="tag '+(x.n==="att"?"att":"inf")+'">'+(x.n==="att"?"! Atención":"i Nota")+'</span><p>'+esc(x.t)+'<small>'+esc(x.d||"")+'</small></p></div>'}).join("")||'<p class="note">Sin avisos.</p>')+'</div></section>';
|
||||||
|
var hoy=new Date().toISOString().slice(0,10),prox=piezas.filter(function(p){return p.fecha>=hoy&&p.estado!=="publicada"}).slice(0,5);
|
||||||
|
h+='<section class="sec"><h2>Próximas publicaciones</h2><div class="lines">'+(prox.map(function(p){return '<div class="ln"><span class="tag inf">'+esc(fecha(p.fecha))+'</span><p>'+esc(p.titulo)+'<small>'+esc(lab(FORMATOS,p.formato))+' · '+esc(lab(ESTADOS,p.estado))+'</small></p></div>'}).join("")||'<p class="note">'+(piezasOk?"No hay piezas planificadas a partir de hoy.":"Cargando las piezas…")+'</p>')+'</div></section></div>';
|
||||||
|
$("r-body").className="view";$("r-body").innerHTML=h;
|
||||||
|
$("fresh").textContent="Datos hasta el "+fd(K.end)+".";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- creatividades ---------- */
|
||||||
|
var MESES=(function(){var o=[],y=2026,m=0;for(var i=0;i<13;i++){o.push(y+"-"+(m<9?"0":"")+(m+1));m++;if(m>11){m=0;y++}}return o})();
|
||||||
|
var MNOM=["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
||||||
|
function mesNom(k){return MNOM[+k.slice(5)-1]+" de "+k.slice(0,4)}
|
||||||
|
function pm(){return piezas.filter(function(p){return String(p.fecha).slice(0,7)===pMes})}
|
||||||
|
function loadHist(){if(!db||hist[pMes]!==undefined)return;var k=pMes;hist[k]=null;db.doc("hist/"+k).get().then(function(s){hist[k]=s.exists?(s.data().posts||[]):[];if(k===pMes)renderHist()},function(){hist[k]=[];if(k===pMes)renderHist()})}
|
||||||
|
function renderHist(){
|
||||||
|
var el=$("hist"),ps=hist[pMes],hoy=new Date().toISOString().slice(0,7);
|
||||||
|
if(pMes>hoy){el.innerHTML="";return}
|
||||||
|
var h='<h2>Publicado en '+mesNom(pMes).toLowerCase()+'<small>texto e imagen de cada pieza en Instagram</small></h2>';
|
||||||
|
if(ps==null)h+='<div class="empty">Cargando lo publicado…</div>';
|
||||||
|
else if(!ps.length)h+='<div class="empty">No hay piezas publicadas guardadas de este mes.</div>';
|
||||||
|
else h+='<div class="hl">'+ps.map(function(p){var cap=p.cap||"Sin texto.",long=cap.length>260;
|
||||||
|
return '<article class="hp">'+(p.img?'<img src="'+esc(p.img)+'" alt="" loading="lazy">':'<span class="noimg"></span>')+'<div><span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span>'+
|
||||||
|
(long?'<details><summary>'+esc(cap.slice(0,220))+'… <u>ver todo</u></summary><p>'+esc(cap)+'</p></details>':'<p>'+esc(cap)+'</p>')+
|
||||||
|
'<small>Alcance '+num(p.r)+' · '+num(p.v)+' visualizaciones · '+num(p.l)+' me gusta · '+num(p.g)+' guardados · <a href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">Ver en Instagram</a></small></div></article>'}).join("")+'</div><p class="note">Se muestra la primera imagen de cada carrusel y la portada de cada reel. Las stories no se conservan en Instagram.</p>';
|
||||||
|
el.innerHTML=h;
|
||||||
|
}
|
||||||
|
function renderQuota(){
|
||||||
|
$("quota").innerHTML=CUPO.map(function(c){
|
||||||
|
var x=pm().filter(function(p){return c[1].indexOf(p.formato)>=0}),ok=x.filter(function(p){return p.estado==="aprobada"||p.estado==="publicada"}).length;
|
||||||
|
return '<div class="tile"><div class="lab">'+c[0]+'</div><div><b>'+x.length+'</b> <small>de '+c[2]+' planificadas</small></div><div class="bar"><i style="width:'+Math.min(100,Math.round(x.length/c[2]*100))+'%"></i></div><small>'+ok+' aprobadas o publicadas</small></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
function chip(g,k,t,cur){return '<button class="chip" type="button" data-g="'+g+'" data-k="'+k+'" aria-pressed="'+(cur===k)+'">'+t+'</button>'}
|
||||||
|
function renderFilters(){
|
||||||
|
$("meses").innerHTML=MESES.map(function(k){return chip("mes",k,MNOM[+k.slice(5)-1].slice(0,3)+" "+k.slice(2,4),pMes)}).join("");$("ptit").textContent="Propuestas de "+mesNom(pMes).toLowerCase();
|
||||||
|
$("filters").innerHTML=chip("pv","lista","Lista",pView)+chip("pv","cal","Calendario",pView)+'<span class="sep"></span>'+chip("f","","Todos los formatos",fF)+FORMATOS.map(function(f){return chip("f",f[0],f[1],fF)}).join("")+'<span class="sep"></span>'+chip("e","","Todos los estados",fE)+ESTADOS.map(function(e){return chip("e",e[0],e[1],fE)}).join("");
|
||||||
|
}
|
||||||
|
function renderCal(rows){
|
||||||
|
var base=pMes,y=+base.slice(0,4),m=+base.slice(5,7),first=new Date(Date.UTC(y,m-1,1)),off=(first.getUTCDay()+6)%7,dim=new Date(Date.UTC(y,m,0)).getUTCDate();
|
||||||
|
var h='<div class="calw"><div class="cal">'+["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"].map(function(x){return '<div class="hd">'+x+'</div>'}).join("");
|
||||||
|
for(var i=0;i<off;i++)h+='<div class="day off"></div>';
|
||||||
|
for(var dd=1;dd<=dim;dd++){var key=base+"-"+(dd<10?"0":"")+dd,ps=rows.filter(function(p){return p.fecha===key});
|
||||||
|
h+='<div class="day"><span class="num">'+dd+'</span>'+ps.map(function(p){return '<a class="'+esc(p.estado)+'" tabindex="0" role="button" data-goto="'+esc(p.id)+'" title="'+esc(lab(ESTADOS,p.estado))+'"><b>'+esc(lab(FORMATOS,p.formato))+'</b> '+esc(p.titulo)+'</a>'}).join("")+'</div>'}
|
||||||
|
return h+'</div></div><div class="filters"><span class="lab">Estados</span>'+ESTADOS.map(function(e){return '<span class="st '+e[0]+'">'+e[1]+'</span>'}).join("")+'</div><p class="note">Pulsa una pieza para abrirla en la lista.</p>';
|
||||||
|
}
|
||||||
|
function renderList(){
|
||||||
|
var all=pm(),rows=all.filter(function(p){return (!fF||p.formato===fF)&&(!fE||p.estado===fE)});
|
||||||
|
if(!all.length){$("list").innerHTML='<div class="empty"><b>No hay propuestas guardadas de este mes.</b><br>'+(pMes<"2026-10"?"El panel empezó a registrar propuestas en octubre de 2026. Debajo tienes lo que se publicó.":"Pide a Claude que proponga los temas y copys del mes y aparecerán aquí.")+'</div>';return}
|
||||||
|
if(!rows.length){$("list").innerHTML='<div class="empty">Ninguna pieza coincide con estos filtros.</div>';return}
|
||||||
|
if(pView==="cal"){$("list").innerHTML=renderCal(rows);return}
|
||||||
|
$("list").innerHTML=rows.map(function(p){
|
||||||
|
var h='<div class="item" id="it-'+esc(p.id)+'"><button class="row" type="button" data-open="'+esc(p.id)+'" aria-expanded="'+(open===p.id)+'"><span class="d">'+esc(fecha(p.fecha))+'</span><span class="fmt">'+esc(lab(FORMATOS,p.formato))+'</span><span class="t">'+esc(p.titulo)+'<small>'+esc(p.pilar||"")+'</small></span><span class="st '+esc(p.estado)+'">'+esc(lab(ESTADOS,p.estado))+'</span></button>';
|
||||||
|
if((p.imgs||[]).length){h+='<div class="thumbs">'+p.imgs.map(function(i,k){return '<img src="/_blob/'+esc(i)+'" alt="'+esc(p.titulo)+', imagen '+(k+1)+'" loading="lazy">'}).join("")+'</div>'}
|
||||||
|
else if((p.imgsd||[]).length){h+='<div class="thumbs">'+p.imgsd.map(function(i,k){return '<img src="'+esc(i)+'" alt="'+esc(p.titulo)+', slide '+(k+1)+'" loading="lazy">'}).join("")+'</div>'}
|
||||||
|
else if(p.img){h+='<div class="thumbs"><img src="'+esc(p.img)+'" alt="'+esc(p.titulo)+'" loading="lazy"></div>'}
|
||||||
|
if(open!==p.id&&p.copy){h+='<p class="pcopy">'+esc(p.copy)+'</p>'}
|
||||||
|
if(open===p.id){
|
||||||
|
var cms=(p.comentarios||[]).map(function(c){return '<div class="cm"><time>'+esc(c.t||"")+'</time>'+esc(c.texto)+'</div>'}).join("");
|
||||||
|
h+='<div class="detail"><div><label for="copy-'+esc(p.id)+'">Copy</label><textarea id="copy-'+esc(p.id)+'" rows="8">'+esc(p.copy||"")+'</textarea><div class="acts"><button class="btn primary" type="button" data-save="'+esc(p.id)+'">Guardar copy</button><button class="btn" type="button" data-copy="'+esc(p.id)+'">Copiar texto</button></div><div class="lab">Idea visual</div><div class="visual">'+esc(p.visual||"Sin definir")+'</div></div>'+
|
||||||
|
'<div><div class="lab">Estado</div><div class="acts">'+ESTADOS.map(function(e){return '<button class="btn'+(p.estado===e[0]?" current":"")+'" type="button" data-id="'+esc(p.id)+'" data-estado="'+e[0]+'">'+e[1]+'</button>'}).join("")+'</div>'+
|
||||||
|
'<div class="lab">Comentarios</div>'+(cms||'<div class="note">Sin comentarios.</div>')+'<label for="cm-'+esc(p.id)+'">Añadir comentario</label><input type="text" id="cm-'+esc(p.id)+'" placeholder="Por ejemplo: cambiar el titular de la portada"><div class="acts"><button class="btn" type="button" data-cm="'+esc(p.id)+'">Añadir</button></div></div></div>';
|
||||||
|
}
|
||||||
|
return h+'</div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
function renderPiezas(){var past=pMes<"2026-10";$("quota").hidden=past;$("psec").hidden=past;renderQuota();renderFilters();renderList();renderHist();loadHist()}
|
||||||
|
function find(id){for(var i=0;i<piezas.length;i++)if(piezas[i].id===id)return piezas[i]}
|
||||||
|
function upd(id,data){if(!db)return;$("msg").hidden=true;db.doc("piezas/"+id).update(data).catch(fail)}
|
||||||
|
|
||||||
|
/* ---------- publicado ---------- */
|
||||||
|
function renderKpi(d){
|
||||||
|
kData=d;var ps=(d.posts||[]).slice(),mr=med(ps.map(function(p){return p.r}));
|
||||||
|
var h='<div class="tiles"><div class="tile"><div class="lab">Seguidores</div><div><b>'+num(d.seguidores)+'</b></div><small>a '+esc(fecha(d.actualizado))+'</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Piezas publicadas</div><div><b>'+ps.length+'</b></div><small>del '+esc(fecha(d.desde))+' al '+esc(fecha(d.hasta))+'</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Alcance mediano por pieza</div><div><b>'+num(mr)+'</b></div><small>cuentas alcanzadas</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Guardados por pieza</div><div><b>'+(ps.reduce(function(a,p){return a+p.g},0)/Math.max(1,ps.length)).toLocaleString("de-DE",{maximumFractionDigits:1})+'</b></div><small>media del periodo</small></div></div>';
|
||||||
|
var best=ps.slice().sort(function(a,b){return (b.g*40+b.r)-(a.g*40+a.r)}).filter(function(p){return p.c<50}).slice(0,4);
|
||||||
|
h+='<section class="sec"><h2>Las que mejor funcionan<small>por guardados y alcance, sin contar sorteos</small></h2><div class="best">'+best.map(function(p){return '<a class="bp" href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+(p.i?'<img src="/_blob/'+esc(p.i)+'" alt="" loading="lazy">':"")+'<div><span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span><b>'+esc(p.n)+'</b><small>'+num(p.g)+' guardados · alcance '+num(p.r)+'</small></div></a>'}).join("")+'</div></section>';
|
||||||
|
var DN=["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"],by=DN.map(function(){return []});ps.forEach(function(p){var x=new Date(p.f+"T12:00:00Z");by[(x.getUTCDay()+6)%7].push(p.r)});
|
||||||
|
var mes={};ps.forEach(function(p){var k=p.f.slice(0,7);(mes[k]=mes[k]||[]).push(p.r)});var mk=Object.keys(mes).sort(),MN=["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"];
|
||||||
|
h+='<section class="sec"><h2>Patrones</h2><div class="charts">'+
|
||||||
|
chart({k:"Alcance mediano por día de publicación",cap:"cuentas alcanzadas",v:by.map(med),f:"num",tipo:"col",labels:DN,tipl:DN.map(function(x,i){return x+" · "+by[i].length+" piezas"})})+
|
||||||
|
chart({k:"Alcance mediano por mes",cap:"cuentas alcanzadas",v:mk.map(function(k){return med(mes[k])}),f:"num",tipo:"col",labels:mk.map(function(k){return MN[+k.slice(5)-1]}),tipl:mk.map(function(k){return MN[+k.slice(5)-1]+" · "+mes[k].length+" piezas"})})+
|
||||||
|
chart({k:"Piezas publicadas por mes",cap:"feed y reels",v:mk.map(function(k){return mes[k].length}),f:"num",tipo:"col",labels:mk.map(function(k){return MN[+k.slice(5)-1]})})+'</div><p class="note">Los días con pocas piezas dan medianas poco fiables; pasa el cursor para ver cuántas hay.</p></section>';
|
||||||
|
h+='<section class="sec"><h2>Por formato</h2><div class="tw"><table><thead><tr><th>Formato</th><th class="n">Piezas</th><th class="n">Alcance mediano</th><th class="n">Visualizaciones medianas</th><th class="n">Guardados por pieza</th></tr></thead><tbody>'+
|
||||||
|
["C","R","I"].map(function(t){var x=ps.filter(function(p){return p.t===t});if(!x.length)return "";var g=x.reduce(function(a,p){return a+p.g},0);
|
||||||
|
return '<tr><td>'+TIPOS[t]+'</td><td class="n">'+x.length+'</td><td class="n">'+num(med(x.map(function(p){return p.r})))+'</td><td class="n">'+num(med(x.map(function(p){return p.v})))+'</td><td class="n">'+(g/x.length).toLocaleString("de-DE",{maximumFractionDigits:1})+'</td></tr>'}).join("")+'</tbody></table></div></section>';
|
||||||
|
var cols=[["r","Alcance"],["v","Visualizaciones"],["l","Me gusta"],["c","Comentarios"],["g","Guardados"],["s","Compartidos"]];
|
||||||
|
ps.sort(function(a,b){return kOrd==="f"?(a.f<b.f?1:-1):(b[kOrd]-a[kOrd])});
|
||||||
|
h+='<section class="sec"><h2>Todas las piezas</h2><div class="filters">'+chip("kv","g","Galería",kView)+chip("kv","t","Tabla",kView)+'<span class="sep"></span><span class="lab">Ordenar por</span>'+chip("ord","f","Fecha",kOrd)+cols.map(function(c){return chip("ord",c[0],c[1],kOrd)}).join("")+'</div>';
|
||||||
|
if(kView==="g") h+='<div class="gal">'+ps.map(function(p){return '<a class="card" href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+(p.i?'<img src="/_blob/'+esc(p.i)+'" alt="" loading="lazy">':'<span class="noimg"></span>')+(p.r>=mr*1.5?'<span class="top">Destaca</span>':"")+'<span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span><b>'+esc(p.n)+'</b><small>Alcance '+num(p.r)+' · '+num(p.g)+' guardados</small></a>'}).join("")+'</div>';
|
||||||
|
else h+='<div class="tw"><table><thead><tr><th>Fecha</th><th>Formato</th><th>Pieza</th>'+cols.map(function(c){return '<th class="n">'+c[1]+'</th>'}).join("")+'</tr></thead><tbody>'+
|
||||||
|
ps.map(function(p){return '<tr><td>'+esc(fecha(p.f))+'</td><td>'+TIPOS[p.t]+'</td><td class="wrapc"><a href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+esc(p.n)+'</a></td>'+cols.map(function(c){return '<td class="n">'+num(p[c[0]])+'</td>'}).join("")+'</tr>'}).join("")+'</tbody></table></div>';
|
||||||
|
h+='<p class="note">«Destaca» marca las piezas con un alcance al menos un 50 % superior a la mediana. Las stories no aparecen porque Instagram solo conserva sus métricas 24 horas.</p></section>';
|
||||||
|
$("kpi").className="view";$("kpi").innerHTML=h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- canales ---------- */
|
||||||
|
function renderWk(d){wData=d;renderResumen()}
|
||||||
|
function renderK(){
|
||||||
|
if(!K)return;
|
||||||
|
var r=range(),a=r[0],b=r[1],c=cmpRange(a,b),ms=months(),i0=dIdx(a),i1=dIdx(b);
|
||||||
|
var P=[["m0","Este mes"],["m1","Mes pasado"],["m3","Últimos 3 meses"],["m6","Últimos 6 meses"],["m12","Últimos 12 meses"],["y","Este año"],["c","Personalizado"]],C=[["prev","Periodo anterior"],["yoy","Mismo periodo del año anterior"],["c","Otras fechas"],["none","Sin comparar"]];
|
||||||
|
var h='<section class="fbar" aria-label="Filtros de fecha"><div class="filters"><span class="lab">Periodo</span>'+P.map(function(x){return chip("kp",x[0],x[1],kf.p)}).join("")+'</div>';
|
||||||
|
if(kf.p==="c")h+='<div class="filters"><label for="kf-a">Desde</label><input type="date" id="kf-a" data-kf="a" value="'+esc(kf.a||a)+'" min="'+K.start+'" max="'+K.end+'"><label for="kf-b">Hasta</label><input type="date" id="kf-b" data-kf="b" value="'+esc(kf.b||b)+'" min="'+K.start+'" max="'+K.end+'"></div>';
|
||||||
|
h+='<div class="filters"><span class="lab">Comparar con</span>'+C.map(function(x){return chip("kc",x[0],x[1],kf.c)}).join("")+'</div>';
|
||||||
|
if(kf.c==="c")h+='<div class="filters"><label for="kf-ca">Desde</label><input type="date" id="kf-ca" data-kf="ca" value="'+esc(kf.ca)+'" min="'+K.start+'" max="'+K.end+'"><label for="kf-cb">Hasta</label><input type="date" id="kf-cb" data-kf="cb" value="'+esc(kf.cb)+'" min="'+K.start+'" max="'+K.end+'"></div>';
|
||||||
|
h+='<p class="note"><b>'+fd(a)+' – '+fd(b)+'</b>'+(c?' comparado con <b>'+fd(c[0])+' – '+fd(c[1])+'</b>':kf.c==="c"?' · elige las dos fechas de comparación':'')+'. En los gráficos, los meses del periodo van en color intenso.</p></section>';
|
||||||
|
SRC.forEach(function(s){
|
||||||
|
var D=K.src[s.id],cur=sums(D.m,a,b),old=c?sums(D.m,c[0],c[1]):null;
|
||||||
|
h+='<section class="sec"><h2>'+esc(s.t)+'<small>'+esc(s.fuente||"")+' · datos del '+fd(D.from)+' al '+fd(D.to)+'</small></h2>';
|
||||||
|
if(!cur._any){h+='<div class="empty">Sin datos de este canal en el periodo elegido.</div></section>';return}
|
||||||
|
h+='<div class="tiles">'+s.mt.map(function(m){var v=val(m,cur),w=old?val(m,old):null,p=pct(w,v);
|
||||||
|
return '<div class="tile"><div class="lab">'+esc(m.k)+'</div><div><b>'+compact(v,m.f)+'</b> '+(p==null?"":'<span class="dl '+dCls(p,m.up)+'">'+dTxt(p)+'</span>')+'</div><small>'+(c?(w==null?"sin datos en la comparación":"antes "+fmt(w,m.f)):"")+'</small></div>'}).join("")+'</div>';
|
||||||
|
var mm=ms.map(function(x){return {x:x,S:sums(D.m,x.a,x.b)}}).filter(function(o){return o.S._any});
|
||||||
|
h+='<div class="charts">'+s.ch.map(function(ci){var m=s.mt[ci];return chart({k:m.k,cap:"por mes",v:mm.map(function(o){return val(m,o.S)||0}),f:m.f||"num",tipo:"col",labels:mm.map(function(o){return o.x.lab}),tipl:mm.map(function(o){return o.x.full+(o.x.part?" (parcial)":"")}),hi:mm.map(function(o){return o.x.a<=b&&o.x.b>=a})})}).join("")+'</div>';
|
||||||
|
var inr=mm.filter(function(o){return o.x.a<=b&&o.x.b>=a});
|
||||||
|
h+='<div class="lab">Mes a mes</div><div class="tw"><table><thead><tr><th>Mes</th>'+s.mt.map(function(m){return '<th class="n">'+esc(m.k)+'</th>'}).join("")+'</tr></thead><tbody>'+
|
||||||
|
inr.map(function(o){var x=o.x,pa=x.a<a?a:x.a,pb=x.b>b?b:x.b,S2=(pa!==x.a||pb!==x.b)?sums(D.m,pa,pb):o.S,part=pa!==x.a||pb!==ymd(+x.a.slice(0,4),+x.a.slice(5,7),0);
|
||||||
|
return '<tr><td>'+esc(x.full)+(part?' <span class="dl">parcial</span>':'')+'</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,S2),m.f)+'</td>'}).join("")+'</tr>'}).join("")+
|
||||||
|
'<tr class="tot"><td>Total del periodo</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,cur),m.f)+'</td>'}).join("")+'</tr>'+
|
||||||
|
(old?'<tr><td>Periodo comparado</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,old),m.f)+'</td>'}).join("")+'</tr>':"")+'</tbody></table></div>';
|
||||||
|
if(D.loc){h+='<div class="lab">Por academia</div><div class="tw"><table><thead><tr><th>Academia</th>'+s.mt.map(function(m){return '<th class="n">'+esc(m.k)+'</th>'}).join("")+'</tr></thead><tbody>'+Object.keys(D.loc).map(function(n){var S3=sums(D.loc[n],a,b);return '<tr><td>'+esc(n)+'</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,S3),m.f)+'</td>'}).join("")+'</tr>'}).join("")+'</tbody></table></div>'}
|
||||||
|
if(KC&&KC[s.id]){var ag={};KC[s.id].rows.forEach(function(x){if(x[0]<i0||x[0]>i1)return;var o=ag[x[1]]||(ag[x[1]]=[0,0,0]);o[0]+=x[2];o[1]+=x[3];o[2]+=x[4]});
|
||||||
|
var rows=Object.keys(ag).map(function(k){return [KC[s.id].camps[k]].concat(ag[k])}).sort(function(x,z){return z[1]-x[1]});
|
||||||
|
if(rows.length)h+='<div class="lab">Campañas del periodo</div><div class="tw"><table><thead><tr><th>Campaña</th><th class="n">Inversión</th><th class="n">'+esc(s.mt[2].k)+'</th><th class="n">'+esc(s.res)+'</th><th class="n">Coste por resultado</th></tr></thead><tbody>'+rows.map(function(x){return '<tr><td class="wrapc">'+esc(x[0])+'</td><td class="n">'+fmt(x[1],"eur")+'</td><td class="n">'+num(x[2])+'</td><td class="n">'+num(x[3])+'</td><td class="n">'+(x[3]?fmt(x[1]/x[3],"eur"):"—")+'</td></tr>'}).join("")+'</tbody></table></div>'}
|
||||||
|
if(s.nota)h+='<p class="note">'+esc(s.nota)+'</p>';
|
||||||
|
h+='</section>';
|
||||||
|
});
|
||||||
|
var act=document.activeElement,keep=act&&act.dataset&&act.dataset.kf?act.id:null;
|
||||||
|
$("wk").className="view";$("wk").innerHTML=h;if(keep&&$(keep))$(keep).focus();
|
||||||
|
}
|
||||||
|
document.addEventListener("change",function(ev){var k=ev.target.dataset&&ev.target.dataset.kf;if(k){kf[k]=ev.target.value;renderK()}});
|
||||||
|
|
||||||
|
/* ---------- navegación ---------- */
|
||||||
|
function show(v){view=VIEWS.indexOf(v)>=0?v:"resumen";VIEWS.forEach(function(x){$("v-"+x).hidden=x!==view});
|
||||||
|
var t=document.querySelectorAll(".tab");for(var i=0;i<t.length;i++)t[i].setAttribute("aria-pressed",String(t[i].dataset.view===view))}
|
||||||
|
window.addEventListener("hashchange",function(){show(location.hash.slice(1))});
|
||||||
|
document.addEventListener("keydown",function(ev){if((ev.key==="Enter"||ev.key===" ")&&ev.target.dataset&&ev.target.dataset.goto){ev.preventDefault();ev.target.click()}});
|
||||||
|
document.addEventListener("click",function(ev){
|
||||||
|
var g=ev.target.closest("[data-goto]");if(g){pView="lista";fF="";fE="";open=g.dataset.goto;renderPiezas();var el=$("it-"+open);if(el)el.scrollIntoView({block:"start"});return}
|
||||||
|
var b=ev.target.closest("button");if(!b)return;var d=b.dataset;
|
||||||
|
if(d.view){show(d.view);try{location.hash=d.view}catch(e){}window.scrollTo(0,0)}
|
||||||
|
else if(d.g==="f"){fF=d.k;renderPiezas()} else if(d.g==="e"){fE=d.k;renderPiezas()} else if(d.g==="pv"){pView=d.k;renderPiezas()} else if(d.g==="mes"){pMes=d.k;open=null;renderPiezas()}
|
||||||
|
else if(d.g==="kp"){kf.p=d.k;if(d.k==="c"&&K){var rr=range();kf.a=kf.a||rr[0];kf.b=kf.b||rr[1]}renderK()} else if(d.g==="kc"){kf.c=d.k;renderK()}
|
||||||
|
else if(d.g==="kv"&&kData){kView=d.k;renderKpi(kData)} else if(d.g==="ord"&&kData){kOrd=d.k;renderKpi(kData)}
|
||||||
|
else if(d.g==="sug"){$("q").value=b.textContent;$("q").focus()}
|
||||||
|
else if(d.open){open=open===d.open?null:d.open;renderList()}
|
||||||
|
else if(d.estado){upd(d.id,{estado:d.estado})}
|
||||||
|
else if(d.save){upd(d.save,{copy:$("copy-"+d.save).value})}
|
||||||
|
else if(d.copy){var ta=$("copy-"+d.copy);navigator.clipboard.writeText(ta.value).then(function(){b.textContent="Copiado"},function(){ta.select()})}
|
||||||
|
else if(d.cm){var i=$("cm-"+d.cm),v=i.value.trim();if(!v)return;var p=find(d.cm);upd(d.cm,{comentarios:(p.comentarios||[]).concat([{t:new Date().toLocaleDateString("es-ES"),texto:v}])})}
|
||||||
|
});
|
||||||
|
show(location.hash.slice(1));renderQuota();renderFilters();
|
||||||
|
|
||||||
|
/* ---------- exportar ---------- */
|
||||||
|
function csvOf(rows){return ""+rows.map(function(r){return r.map(function(c){c=c==null?"":String(c);return /[";\n]/.test(c)?'"'+c.replace(/"/g,'""')+'"':c}).join(";")}).join("\n")}
|
||||||
|
function csvNum(v){return v==null?"":String(v).replace(".",",")}
|
||||||
|
function exportRows(){
|
||||||
|
if(view==="creatividades")return ["piezas-"+pMes+".csv",[["Fecha","Formato","Pilar","Título","Estado","Copy","Idea visual"]].concat(pm().map(function(p){return [p.fecha,lab(FORMATOS,p.formato),p.pilar,p.titulo,lab(ESTADOS,p.estado),p.copy,p.visual]}))];
|
||||||
|
if(view==="publicado"&&kData)return ["instagram-publicado.csv",[["Fecha","Formato","Pieza","Enlace","Alcance","Visualizaciones","Me gusta","Comentarios","Guardados","Compartidos"]].concat(kData.posts.map(function(p){return [p.f,TIPOS[p.t],p.n,"https://www.instagram.com/"+p.u+"/",p.r,p.v,p.l,p.c,p.g,p.s]}))];
|
||||||
|
if(!K)return null;var r=range(),ms=months().filter(function(x){return x.a<=r[1]&&x.b>=r[0]}),rows=[["Canal","Indicador"].concat(ms.map(function(x){return x.lab})).concat(["Total "+fd(r[0])+" - "+fd(r[1])])];
|
||||||
|
SRC.forEach(function(s){var D=K.src[s.id];s.mt.forEach(function(m){rows.push([s.t,m.k].concat(ms.map(function(x){var v=val(m,sums(D.m,x.a<r[0]?r[0]:x.a,x.b>r[1]?r[1]:x.b));return csvNum(v==null?null:Math.round(v*100)/100)})).concat([csvNum((function(v){return v==null?null:Math.round(v*100)/100})(val(m,sums(D.m,r[0],r[1]))))]))})});return ["kpis-por-canal.csv",rows]}
|
||||||
|
window.claude.use("downloads").then(function(dl){if(!dl)return;var b=$("csv");b.hidden=false;
|
||||||
|
b.addEventListener("click",function(){var x=exportRows();if(!x)return;dl.save({filename:"blue-parrot-"+x[0],data:csvOf(x[1])}).catch(function(e){if(e&&(e.code==="unavailable"||e.code==="not_granted"))b.hidden=true})})});
|
||||||
|
|
||||||
|
/* ---------- preguntar ---------- */
|
||||||
|
window.claude.use("sample").then(function(sample){if(!sample)return;$("tab-ask").hidden=false;
|
||||||
|
$("sugs").innerHTML=["¿Qué canal trae los leads más baratos?","¿Qué tipo de contenido debería publicar más en Instagram?","Resume septiembre en cinco líneas para el cliente","¿Qué tres acciones priorizarías en octubre?"].map(function(s){return '<button class="chip" type="button" data-g="sug">'+esc(s)+'</button>'}).join("");
|
||||||
|
var ctl=null;$("stop").addEventListener("click",function(){if(ctl)ctl.abort()});
|
||||||
|
$("ask").addEventListener("click",function(){var q=$("q").value.trim();if(!q)return;var out=$("ans"),ask=$("ask"),stop=$("stop");
|
||||||
|
var ms12=K?months().slice(-13):[],ctx={kpis_mensuales:K&&{meses:ms12.map(function(x){return x.lab+(x.part?" (parcial)":"")}),canales:SRC.map(function(s){return {canal:s.t,nota:s.nota,indicadores:s.mt.map(function(m){return {k:m.k,unidad:m.f||"num",v:ms12.map(function(x){var v=val(m,sums(K.src[s.id].m,x.a,x.b));return v==null?null:Math.round(v*100)/100})}})}})},
|
||||||
|
instagram:kData&&{seguidores:kData.seguidores,piezas:kData.posts.map(function(p){return {fecha:p.f,formato:TIPOS[p.t],titulo:p.n,alcance:p.r,visualizaciones:p.v,megusta:p.l,comentarios:p.c,guardados:p.g,compartidos:p.s}})},
|
||||||
|
piezas_octubre:piezas.map(function(p){return {fecha:p.fecha,formato:p.formato,titulo:p.titulo,estado:p.estado}}),paquete:"16 publicaciones, 20 stories y 8 reels al mes"};
|
||||||
|
var prompt="Eres analista de marketing de la agencia 4Ventures y respondes sobre su cliente Blue Parrot School (escuela de idiomas en Madrid, Málaga y online). Responde en español, en texto plano sin Markdown, breve y con cifras concretas de los datos. Si el dato no está, dilo. No inventes.\n\nDATOS (JSON):\n"+JSON.stringify(ctx)+"\n\nPREGUNTA: "+q;
|
||||||
|
ctl=new AbortController();out.hidden=false;out.textContent="Pensando…";ask.disabled=true;stop.hidden=false;
|
||||||
|
sample(prompt,{signal:ctl.signal,onText:function(u){out.textContent=u.text}}).then(function(r){out.textContent=r.text+(r.truncated?"\n\n(Respuesta cortada: pide menos de una vez.)":"")},
|
||||||
|
function(e){var c=e&&e.code;if(c==="cancelled"){out.textContent=(e.text||"")||"Pregunta cancelada.";return}
|
||||||
|
if(c==="not_granted"||c==="sampling_disabled"||c==="not_declared"||c==="capability_disabled"){out.textContent="Claude no está disponible en esta vista.";return}
|
||||||
|
out.textContent=(e&&e.text?e.text+"\n\n":"")+(c==="rate_limited"?"Has alcanzado el límite de uso. Prueba más tarde.":c==="refused"?"Claude no ha podido responder a esa pregunta. Prueba a formularla de otra forma.":"No se pudo completar la respuesta. Vuelve a intentarlo.")})
|
||||||
|
.then(function(){ask.disabled=false;stop.hidden=true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- datos ---------- */
|
||||||
|
window.claude.use("db").then(function(x){
|
||||||
|
db=x;
|
||||||
|
if(!db){var m='<div class="empty"><b>No se pueden cargar los datos en esta vista.</b><br>Abre el panel con tu sesión de Claude iniciada.</div>';$("list").innerHTML=m;$("r-body").innerHTML=m;$("r-body").className="";$("kpi").innerHTML=m;$("kpi").className="";$("wk").innerHTML=m;$("wk").className="";return}
|
||||||
|
db.doc("kpi/instagram").onSnapshot(function(s){ if(s.exists) renderKpi(s.data()); else $("kpi").innerHTML="<b>Sin datos de Instagram todavía.</b><br>Pide a Claude que actualice las métricas desde Windsor." },function(){});
|
||||||
|
db.doc("kpi/windsor").onSnapshot(function(s){ if(s.exists) renderWk(s.data()) },function(){});
|
||||||
|
db.doc("kpi/diario").onSnapshot(function(s){ if(s.exists){K=s.data();renderK();renderResumen()} else { $("wk").innerHTML="<b>Sin KPIs todavía.</b><br>Pide a Claude que los actualice desde Windsor.";$("r-body").innerHTML=$("wk").innerHTML } },function(){});
|
||||||
|
db.doc("kpi/campanas").onSnapshot(function(s){ if(s.exists){KC=s.data();renderK()} },function(){});
|
||||||
|
db.collection("piezas").orderBy("fecha").onSnapshot(function(s){
|
||||||
|
var a=document.activeElement, keep=a&&a.id&&/^(copy|cm)-/.test(a.id)?{id:a.id,v:a.value}:null;
|
||||||
|
piezas=s.docs.map(function(d){var o=Object.assign({},d.data());o.id=d.id;return o});piezasOk=true;
|
||||||
|
renderPiezas();renderResumen();
|
||||||
|
if(keep&&$(keep.id)){$(keep.id).value=keep.v;$(keep.id).focus()}
|
||||||
|
},function(){$("list").innerHTML='<div class="empty"><b>Se ha perdido la conexión con los datos.</b><br>Recarga la página.</div>'});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body></html>
|
||||||
107
src/app/dashboard/kpis/kpis/route.ts
Normal file
107
src/app/dashboard/kpis/kpis/route.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import { htmlTemplate } from './template';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { createClient } from '@/lib/supabase/server';
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
const supabase = await createClient();
|
||||||
|
const tenant_id = '8bec977a-6032-4759-aa7f-b5042081f450';
|
||||||
|
|
||||||
|
const { data: adDaily } = await supabase
|
||||||
|
.from('ad_daily')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenant_id)
|
||||||
|
.order('day', { ascending: true });
|
||||||
|
|
||||||
|
const { data: orgDaily } = await supabase
|
||||||
|
.from('organic_daily')
|
||||||
|
.select('*')
|
||||||
|
.eq('tenant_id', tenant_id)
|
||||||
|
.order('day', { ascending: true });
|
||||||
|
|
||||||
|
const { data: creativePipeline } = await supabase
|
||||||
|
.from('creative_pipeline')
|
||||||
|
.select('*')
|
||||||
|
.order('created_at', { ascending: true });
|
||||||
|
|
||||||
|
// Map to K format
|
||||||
|
const startDay = new Date();
|
||||||
|
startDay.setFullYear(startDay.getFullYear() - 1);
|
||||||
|
const startStr = startDay.toISOString().split('T')[0];
|
||||||
|
const endStr = new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const daysCount = Math.round((new Date(endStr).getTime() - new Date(startStr).getTime()) / 864e5) + 1;
|
||||||
|
const K: any = {
|
||||||
|
start: startStr,
|
||||||
|
end: endStr,
|
||||||
|
n: daysCount,
|
||||||
|
src: {
|
||||||
|
meta: { m: { spend: Array(daysCount).fill(null), imp: Array(daysCount).fill(null), clk: Array(daysCount).fill(null), res: Array(daysCount).fill(null) }, from: startStr, to: endStr },
|
||||||
|
gads: { m: { spend: Array(daysCount).fill(null), imp: Array(daysCount).fill(null), clk: Array(daysCount).fill(null), res: Array(daysCount).fill(null) }, from: startStr, to: endStr },
|
||||||
|
gsc: { m: { clk: Array(daysCount).fill(null), imp: Array(daysCount).fill(null), posw: Array(daysCount).fill(null) }, from: startStr, to: endStr },
|
||||||
|
ga4: { m: { ses: Array(daysCount).fill(null), usr: Array(daysCount).fill(null), new: Array(daysCount).fill(null), key: Array(daysCount).fill(null) }, from: startStr, to: endStr },
|
||||||
|
gbp: { m: { imp: Array(daysCount).fill(null), call: Array(daysCount).fill(null), web: Array(daysCount).fill(null), dir: Array(daysCount).fill(null) }, from: startStr, to: endStr },
|
||||||
|
ig: { m: { views: Array(daysCount).fill(null), inter: Array(daysCount).fill(null), likes: Array(daysCount).fill(null), com: Array(daysCount).fill(null), saves: Array(daysCount).fill(null), shares: Array(daysCount).fill(null), taps: Array(daysCount).fill(null) }, from: startStr, to: endStr }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIdx = (d: string) => Math.round((new Date(d).getTime() - new Date(startStr).getTime()) / 864e5);
|
||||||
|
|
||||||
|
if (adDaily) {
|
||||||
|
for (const row of adDaily) {
|
||||||
|
if (row.day < startStr || row.day > endStr) continue;
|
||||||
|
const idx = getIdx(row.day);
|
||||||
|
const plat = row.platform === 'meta' ? 'meta' : row.platform === 'google' ? 'gads' : null;
|
||||||
|
if (!plat) continue;
|
||||||
|
K.src[plat].m.spend[idx] = row.spend;
|
||||||
|
K.src[plat].m.imp[idx] = row.impressions;
|
||||||
|
K.src[plat].m.clk[idx] = row.clicks;
|
||||||
|
K.src[plat].m.res[idx] = row.leads || 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orgDaily) {
|
||||||
|
for (const row of orgDaily) {
|
||||||
|
if (row.day < startStr || row.day > endStr) continue;
|
||||||
|
const idx = getIdx(row.day);
|
||||||
|
K.src.ga4.m.ses[idx] = row.ga4_sessions;
|
||||||
|
K.src.ga4.m.usr[idx] = row.ga4_users;
|
||||||
|
K.src.ga4.m.new[idx] = row.ga4_new_users;
|
||||||
|
K.src.ga4.m.key[idx] = row.ga4_key_events;
|
||||||
|
K.src.gsc.m.clk[idx] = row.gsc_clicks;
|
||||||
|
K.src.gsc.m.imp[idx] = row.gsc_impressions;
|
||||||
|
K.src.gsc.m.posw[idx] = row.gsc_position_w;
|
||||||
|
K.src.gbp.m.imp[idx] = row.gbp_impressions;
|
||||||
|
K.src.gbp.m.call[idx] = row.gbp_calls;
|
||||||
|
K.src.gbp.m.web[idx] = row.gbp_website_clicks;
|
||||||
|
K.src.gbp.m.dir[idx] = row.gbp_directions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const kData = { seguidores: 0, posts: [], actualizado: endStr, desde: startStr, hasta: endStr };
|
||||||
|
|
||||||
|
// Inject data
|
||||||
|
const dataPayload = { K, KC: {}, kData, piezas: creativePipeline || [] };
|
||||||
|
|
||||||
|
let htmlStr = htmlTemplate.replace(
|
||||||
|
/\/\* ---------- datos ---------- \*\/[\s\S]*?\}\)\(\);/g,
|
||||||
|
`/* ---------- datos ---------- */
|
||||||
|
window.__INJECTED_DATA__ = ${JSON.stringify(dataPayload).replace(/</g, '\\u003c')};
|
||||||
|
K = window.__INJECTED_DATA__.K;
|
||||||
|
KC = window.__INJECTED_DATA__.KC;
|
||||||
|
kData = window.__INJECTED_DATA__.kData;
|
||||||
|
piezas = window.__INJECTED_DATA__.piezas;
|
||||||
|
piezasOk = true;
|
||||||
|
|
||||||
|
if (!K) { $("wk").innerHTML="<b>Sin KPIs todavía.</b>"; $("r-body").innerHTML=$("wk").innerHTML; }
|
||||||
|
else { renderK(); renderWk(); renderResumen(); }
|
||||||
|
if (kData) { renderKpi(kData); }
|
||||||
|
renderPiezas();
|
||||||
|
})();`
|
||||||
|
);
|
||||||
|
|
||||||
|
return new NextResponse(htmlStr, {
|
||||||
|
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
||||||
|
});
|
||||||
|
}
|
||||||
536
src/app/dashboard/kpis/kpis/template.ts
Normal file
536
src/app/dashboard/kpis/kpis/template.ts
Normal file
@@ -0,0 +1,536 @@
|
|||||||
|
export const htmlTemplate = `
|
||||||
|
<!doctype html><html><head><meta charset=utf8><meta name=viewport content="width=device-width,initial-scale=1,viewport-fit=cover"><style>:root{color-scheme:light;box-sizing:border-box;padding-top:env(safe-area-inset-top,0px);padding-bottom:env(safe-area-inset-bottom,0px)}html{scroll-padding-top:env(safe-area-inset-top,0px)}body{margin:0;padding:0;font:14px -apple-system,BlinkMacSystemFont,sans-serif;background:#faf9f5;color:#141413}img{max-width:100%}[hidden]:not([hidden=until-found i]){display:none!important}</style></head><body>
|
||||||
|
<title>Blue Parrot · Panel de creatividades</title>
|
||||||
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;800&display=swap">
|
||||||
|
<style>
|
||||||
|
/* Layout: cabecera de marca con pestañas; portada ejecutiva (indicadores con tendencia, lectura del mes, avisos, próximas piezas) y vistas de trabajo por debajo. */
|
||||||
|
:root{
|
||||||
|
--bg:#F4F6FB; --surface:#FFFFFF; --fg:#16203A; --muted:#55607A; --line:#D6DDEC;
|
||||||
|
--blue:#2A4D9C; --blue-soft:#E3EAF8; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#FFFFFF;
|
||||||
|
--series:#2A4D9C; --series-soft:#B9C8EA;
|
||||||
|
--ok:#1E7A46; --ok-bg:#DDF2E5; --warn:#8A5A00; --warn-bg:#FCEFC7; --bad:#A6321F; --bad-bg:#F9E0DB; --info:#2A4D9C; --info-bg:#E3EAF8; --done:#3D4660; --done-bg:#E4E7EF;
|
||||||
|
--font:Poppins,"Segoe UI",system-ui,sans-serif;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
|
||||||
|
--bg:#0F1524; --surface:#182036; --fg:#EEF1F8; --muted:#A3ADC4; --line:#2C3754;
|
||||||
|
--blue:#8FB0F2; --blue-soft:#22305A; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#0F1524;
|
||||||
|
--series:#8FB0F2; --series-soft:#3A4F85;
|
||||||
|
--ok:#7FD6A1; --ok-bg:#163526; --warn:#F2CB63; --warn-bg:#3A2E0E; --bad:#F29C8C; --bad-bg:#40201A; --info:#8FB0F2; --info-bg:#22305A; --done:#C2C9DA; --done-bg:#2A3148;
|
||||||
|
color-scheme:dark } }
|
||||||
|
:root[data-theme="dark"]{
|
||||||
|
--bg:#0F1524; --surface:#182036; --fg:#EEF1F8; --muted:#A3ADC4; --line:#2C3754;
|
||||||
|
--blue:#8FB0F2; --blue-soft:#22305A; --yellow:#FBC02D; --on-yellow:#1A1A1A; --on-blue:#0F1524;
|
||||||
|
--series:#8FB0F2; --series-soft:#3A4F85;
|
||||||
|
--ok:#7FD6A1; --ok-bg:#163526; --warn:#F2CB63; --warn-bg:#3A2E0E; --bad:#F29C8C; --bad-bg:#40201A; --info:#8FB0F2; --info-bg:#22305A; --done:#C2C9DA; --done-bg:#2A3148;
|
||||||
|
color-scheme:dark }
|
||||||
|
body{background:var(--bg);color:var(--fg);font-family:var(--font);font-size:15px;line-height:1.5}
|
||||||
|
.wrap{max-width:1120px;margin:0 auto;padding-inline:20px;padding-block:28px 72px;display:flex;flex-direction:column;gap:24px}
|
||||||
|
header{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:flex-end;justify-content:space-between}
|
||||||
|
.eyebrow,.lab,label{font-size:12px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
h1{margin:0;font-size:30px;font-weight:800;line-height:1.15;text-wrap:balance}
|
||||||
|
h1 span{color:var(--blue)}
|
||||||
|
h2{margin:0;font-size:18px;font-weight:600;text-wrap:balance}
|
||||||
|
h2 small{font-weight:400;font-size:13px;color:var(--muted);margin-left:8px}
|
||||||
|
h3{margin:0;font-size:14px;font-weight:600}
|
||||||
|
.pack{font-size:13px;color:var(--muted);max-width:420px}
|
||||||
|
.note{font-size:13px;color:var(--muted);margin:0}
|
||||||
|
.nav{display:flex;flex-wrap:wrap;gap:8px;border-bottom:1px solid var(--line);padding-bottom:12px;align-items:center}
|
||||||
|
.tab,.chip,.btn{font:inherit;cursor:pointer;border:1px solid var(--line);background:var(--surface);color:var(--fg)}
|
||||||
|
.tab{font-size:14px;font-weight:600;padding:9px 18px;border-radius:99px}
|
||||||
|
.tab[aria-pressed="true"]{background:var(--yellow);border-color:var(--yellow);color:var(--on-yellow)}
|
||||||
|
.nav .grow{flex:1}
|
||||||
|
.chip{font-size:13px;font-weight:500;padding:7px 14px;border-radius:99px}
|
||||||
|
.chip[aria-pressed="true"]{background:var(--blue);color:var(--on-blue);border-color:var(--blue)}
|
||||||
|
.btn{font-size:13px;font-weight:600;padding:8px 14px;border-radius:10px}
|
||||||
|
.btn.primary{background:var(--blue);border-color:var(--blue);color:var(--on-blue)}
|
||||||
|
.btn.current{border-color:var(--blue);color:var(--blue)}
|
||||||
|
.btn:disabled{opacity:.5;cursor:default}
|
||||||
|
:is(.tab,.chip,.btn,textarea,input,.row,.card,.cal a,.hit):focus-visible{outline:3px solid var(--yellow);outline-offset:2px}
|
||||||
|
.view,.sec{display:flex;flex-direction:column}
|
||||||
|
.view{gap:30px} .sec{gap:12px}
|
||||||
|
.fbar{display:flex;flex-direction:column;gap:10px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 16px}
|
||||||
|
.fbar input[type=date]{font:inherit;font-size:13px;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:6px 10px}
|
||||||
|
.fbar label{align-self:center}
|
||||||
|
tr.tot td{font-weight:600;background:var(--blue-soft)}
|
||||||
|
.filters{display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||||||
|
.sep{width:1px;height:22px;background:var(--line);margin-inline:4px}
|
||||||
|
.empty{background:var(--surface);border:1px dashed var(--line);border-radius:14px;padding:26px;color:var(--muted)}
|
||||||
|
.empty b{color:var(--fg)}
|
||||||
|
/* tiles */
|
||||||
|
.tiles{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:14px}
|
||||||
|
.tiles.t3{grid-template-columns:repeat(auto-fit,minmax(300px,1fr))}
|
||||||
|
.tile{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;display:flex;flex-direction:column;gap:6px;min-width:0}
|
||||||
|
.tile b{font-size:26px;font-weight:800;line-height:1.15}
|
||||||
|
.tile small{color:var(--muted);font-size:13px}
|
||||||
|
.tile svg{width:100%;height:36px;display:block;margin-top:4px}
|
||||||
|
.dl{font-size:12px;font-weight:600;padding:2px 8px;border-radius:99px;white-space:nowrap;background:var(--done-bg);color:var(--done)}
|
||||||
|
.dl.good{background:var(--ok-bg);color:var(--ok)} .dl.poor{background:var(--bad-bg);color:var(--bad)}
|
||||||
|
.bar{height:8px;border-radius:99px;background:var(--blue-soft);overflow:hidden}
|
||||||
|
.bar i{display:block;height:100%;background:var(--yellow);border-radius:99px}
|
||||||
|
.two{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(0,1fr);gap:22px}
|
||||||
|
.lines{display:flex;flex-direction:column;gap:8px}
|
||||||
|
.ln{background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:12px 14px;display:flex;gap:12px;align-items:flex-start;min-width:0}
|
||||||
|
.ln p{margin:0;font-size:14px;min-width:0;overflow-wrap:anywhere}
|
||||||
|
.ln p small{display:block;color:var(--muted);font-size:13px}
|
||||||
|
.tag{flex:none;font-size:12px;font-weight:600;padding:3px 10px;border-radius:99px;white-space:nowrap}
|
||||||
|
.tag.good{background:var(--ok-bg);color:var(--ok)} .tag.poor{background:var(--bad-bg);color:var(--bad)} .tag.att{background:var(--warn-bg);color:var(--warn)} .tag.inf{background:var(--info-bg);color:var(--info)}
|
||||||
|
/* piezas */
|
||||||
|
.list{display:flex;flex-direction:column;gap:10px}
|
||||||
|
.item{background:var(--surface);border:1px solid var(--line);border-radius:14px;overflow:hidden}
|
||||||
|
.row{all:unset;box-sizing:border-box;width:100%;cursor:pointer;display:grid;grid-template-columns:64px 92px minmax(0,1fr) auto;gap:14px;align-items:center;padding:14px 18px}
|
||||||
|
.row .d{font-weight:600;font-variant-numeric:tabular-nums;color:var(--muted);font-size:13px}
|
||||||
|
.fmt{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--blue)}
|
||||||
|
.t{font-weight:600;min-width:0;overflow-wrap:anywhere}
|
||||||
|
.t small{display:block;font-weight:400;color:var(--muted);font-size:13px}
|
||||||
|
.st{font-size:12px;font-weight:600;padding:5px 12px;border-radius:99px;white-space:nowrap}
|
||||||
|
.propuesta{background:var(--info-bg);color:var(--info)} .diseno,.revision{background:var(--warn-bg);color:var(--warn)}
|
||||||
|
.cambios{background:var(--bad-bg);color:var(--bad)} .aprobada{background:var(--ok-bg);color:var(--ok)} .publicada{background:var(--done-bg);color:var(--done)}
|
||||||
|
.thumbs{display:flex;gap:10px;overflow-x:auto;padding:0 18px 16px}
|
||||||
|
.thumbs img{height:170px;width:auto;border-radius:10px;border:1px solid var(--line);flex:none}
|
||||||
|
.detail{border-top:1px solid var(--line);padding:18px;display:grid;grid-template-columns:minmax(0,1.4fr) minmax(0,1fr);gap:22px}
|
||||||
|
.detail > div{min-width:0;display:flex;flex-direction:column;gap:10px}
|
||||||
|
textarea,input[type=text]{font:inherit;color:var(--fg);background:var(--bg);border:1px solid var(--line);border-radius:10px;padding:10px 12px;width:100%;box-sizing:border-box;resize:vertical}
|
||||||
|
.visual{font-size:14px;background:var(--blue-soft);border-radius:10px;padding:10px 12px}
|
||||||
|
.acts{display:flex;flex-wrap:wrap;gap:8px}
|
||||||
|
.cm{font-size:14px;background:var(--bg);border-radius:10px;padding:8px 12px;overflow-wrap:anywhere}
|
||||||
|
.cm time{display:block;font-size:12px;color:var(--muted)}
|
||||||
|
#msg{font-size:13px;color:var(--bad)}
|
||||||
|
.item{display:grid;grid-template-columns:minmax(0,auto) minmax(0,1fr)}
|
||||||
|
.item .row,.item .detail{grid-column:1 / -1}
|
||||||
|
.item .thumbs{max-width:440px;box-sizing:border-box}
|
||||||
|
@media (max-width:760px){.item{grid-template-columns:minmax(0,1fr)}.item .thumbs{max-width:100%}}
|
||||||
|
.pcopy{margin:0;padding:0 18px 16px;font-size:14px;white-space:pre-line;overflow-wrap:anywhere;color:var(--fg)}
|
||||||
|
.hl{display:grid;grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:14px}
|
||||||
|
.hp{display:grid;grid-template-columns:110px minmax(0,1fr);gap:14px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:12px;align-items:start}
|
||||||
|
.hp img,.hp .noimg{width:110px;aspect-ratio:4/5;object-fit:cover;border-radius:10px;border:1px solid var(--line);background:var(--blue-soft);display:block}
|
||||||
|
.hp div{min-width:0;display:flex;flex-direction:column;gap:6px}
|
||||||
|
.hp p,.hp summary{margin:0;font-size:13px;line-height:1.5;white-space:pre-line;overflow-wrap:anywhere}
|
||||||
|
.hp summary{cursor:pointer;list-style:none} .hp details[open] summary{display:none}
|
||||||
|
.hp small{font-size:12px;color:var(--muted)}
|
||||||
|
.calw{overflow-x:auto}
|
||||||
|
.cal{display:grid;grid-template-columns:repeat(7,minmax(120px,1fr));gap:6px;min-width:860px}
|
||||||
|
.cal .hd{font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.08em;padding:0 4px}
|
||||||
|
.cal .day{background:var(--surface);border:1px solid var(--line);border-radius:10px;min-height:96px;padding:6px;display:flex;flex-direction:column;gap:4px}
|
||||||
|
.cal .day.off{background:transparent;border-style:dashed}
|
||||||
|
.cal .num{font-size:12px;font-weight:600;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.cal a{font-size:12px;line-height:1.3;padding:4px 6px;border-radius:6px;text-decoration:none;cursor:pointer;overflow-wrap:anywhere}
|
||||||
|
.cal a b{font-weight:600}
|
||||||
|
/* publicado */
|
||||||
|
.gal{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:16px}
|
||||||
|
.card{display:flex;flex-direction:column;gap:6px;min-width:0;text-decoration:none;color:var(--fg);position:relative}
|
||||||
|
.card img,.card .noimg{width:100%;aspect-ratio:4/5;object-fit:cover;border-radius:12px;border:1px solid var(--line);background:var(--blue-soft);display:block}
|
||||||
|
.card b{font-size:13px;font-weight:600;line-height:1.3;overflow-wrap:anywhere}
|
||||||
|
.card small{font-size:12px;color:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.card .fmt{font-size:11px}
|
||||||
|
.card .top{position:absolute;left:8px;top:8px;background:var(--yellow);color:var(--on-yellow);font-size:11px;font-weight:600;padding:3px 8px;border-radius:99px}
|
||||||
|
.best{display:grid;grid-template-columns:repeat(auto-fit,minmax(250px,1fr));gap:12px}
|
||||||
|
.bp{display:flex;gap:12px;background:var(--surface);border:1px solid var(--line);border-radius:12px;padding:10px;text-decoration:none;color:var(--fg);min-width:0}
|
||||||
|
.bp img{width:64px;height:80px;object-fit:cover;border-radius:8px;flex:none}
|
||||||
|
.bp div{min-width:0;display:flex;flex-direction:column;gap:2px}
|
||||||
|
.bp b{font-size:13px;line-height:1.3;overflow-wrap:anywhere}
|
||||||
|
.bp small{font-size:12px;color:var(--muted)}
|
||||||
|
/* tablas */
|
||||||
|
.tw{overflow-x:auto;background:var(--surface);border:1px solid var(--line);border-radius:14px}
|
||||||
|
table{border-collapse:collapse;width:100%;font-size:14px}
|
||||||
|
th,td{padding:10px 14px;text-align:left;border-bottom:1px solid var(--line);white-space:nowrap}
|
||||||
|
td.wrapc{white-space:normal;min-width:200px}
|
||||||
|
tbody tr:last-child td{border-bottom:0}
|
||||||
|
th{font-size:12px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
||||||
|
.n{text-align:right;font-variant-numeric:tabular-nums}
|
||||||
|
a{color:var(--blue)}
|
||||||
|
/* gráficos */
|
||||||
|
.charts{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:14px}
|
||||||
|
.ch{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:14px 16px 10px;display:flex;flex-direction:column;gap:2px;min-width:0;position:relative}
|
||||||
|
.ch .cap{font-size:12px;color:var(--muted)}
|
||||||
|
.ch svg{width:100%;height:auto;display:block;overflow:visible}
|
||||||
|
.ch text{font-family:var(--font);font-size:10px;fill:var(--muted);font-variant-numeric:tabular-nums}
|
||||||
|
.ch .end{fill:var(--fg);font-weight:600;font-size:11px}
|
||||||
|
.hit{fill:transparent;cursor:crosshair;outline:none}
|
||||||
|
.hit:hover,.hit:focus{fill:var(--blue-soft);fill-opacity:.55}
|
||||||
|
#tip{position:fixed;z-index:10;pointer-events:none;background:var(--fg);color:var(--bg);font-size:12px;padding:6px 10px;border-radius:8px;max-width:220px}
|
||||||
|
#tip b{display:block;font-size:13px}
|
||||||
|
.fun{display:flex;flex-direction:column;gap:8px;background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px}
|
||||||
|
.fr{display:grid;grid-template-columns:150px minmax(0,1fr) 110px;gap:12px;align-items:center;font-size:13px}
|
||||||
|
.fr .tr{height:14px;background:transparent}
|
||||||
|
.fr .tr i{display:block;height:100%;min-width:3px;background:var(--series);border-radius:0 4px 4px 0}
|
||||||
|
.fr .v{text-align:right;font-variant-numeric:tabular-nums;font-weight:600}
|
||||||
|
.fr small{color:var(--muted);font-weight:400}
|
||||||
|
/* preguntar */
|
||||||
|
.ask{display:flex;flex-direction:column;gap:12px;max-width:760px}
|
||||||
|
.ans{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:16px 18px;white-space:pre-wrap;overflow-wrap:anywhere;font-size:15px;line-height:1.6}
|
||||||
|
@media (max-width:760px){
|
||||||
|
.two,.detail{grid-template-columns:minmax(0,1fr)}
|
||||||
|
.row{grid-template-columns:52px minmax(0,1fr) auto;row-gap:4px}
|
||||||
|
.row .fmt{grid-column:2;grid-row:1} .row .t{grid-column:1 / -1;grid-row:2} .row .st{grid-row:1;grid-column:3}
|
||||||
|
.fr{grid-template-columns:104px minmax(0,1fr) 84px}
|
||||||
|
h1{font-size:24px}
|
||||||
|
}
|
||||||
|
@media (prefers-reduced-motion:no-preference){ .bar i{transition:width .4s} }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<header>
|
||||||
|
<div>
|
||||||
|
<div class="eyebrow">4Ventures · Panel de cliente</div>
|
||||||
|
<h1><span>Blue Parrot School</span></h1>
|
||||||
|
</div>
|
||||||
|
<div class="pack">Paquete PREMIUM: 16 publicaciones y 20 stories al mes, más 2 reels por semana. <span id="fresh"></span></div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<nav class="nav" aria-label="Vistas del panel">
|
||||||
|
<button class="tab" type="button" data-view="resumen" aria-pressed="true">Resumen</button>
|
||||||
|
<button class="tab" type="button" data-view="creatividades" aria-pressed="false">Creatividades</button>
|
||||||
|
<button class="tab" type="button" data-view="publicado" aria-pressed="false">Publicado</button>
|
||||||
|
<button class="tab" type="button" data-view="canales" aria-pressed="false">KPIs</button>
|
||||||
|
<button class="tab" type="button" data-view="preguntar" aria-pressed="false" id="tab-ask" hidden>Preguntar</button>
|
||||||
|
<span class="grow"></span>
|
||||||
|
<button class="btn" type="button" id="csv" hidden>Exportar CSV</button>
|
||||||
|
</nav>
|
||||||
|
<div id="msg" hidden></div>
|
||||||
|
|
||||||
|
<div class="view" id="v-resumen">
|
||||||
|
<div id="r-body" class="empty"><b>Cargando el resumen…</b><br>Aquí aparecen los indicadores del mes con su tendencia, lo que mejora y empeora, los avisos y las próximas publicaciones.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-creatividades" hidden>
|
||||||
|
<section class="sec">
|
||||||
|
<h2>Histórico mensual<small>enero de 2026 a enero de 2027</small></h2>
|
||||||
|
<div class="filters" id="meses" aria-label="Mes"></div>
|
||||||
|
</section>
|
||||||
|
<section class="tiles" id="quota" aria-label="Cupo del mes"></section>
|
||||||
|
<section class="sec" id="psec">
|
||||||
|
<h2 id="ptit">Propuestas</h2>
|
||||||
|
<div class="filters" id="filters"></div>
|
||||||
|
<div id="list" class="list"><div class="empty"><b>Cargando las piezas…</b><br>Aquí aparecerán las propuestas de temas y copys, con su estado de validación.</div></div>
|
||||||
|
</section>
|
||||||
|
<section class="sec" id="hist"></section>
|
||||||
|
<p class="note">Las plantillas de diseño están en el lienzo «Blue Parrot School · Plantillas Instagram».</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-publicado" hidden>
|
||||||
|
<div id="kpi" class="empty"><b>Cargando los datos de Instagram…</b><br>Aquí se verá el rendimiento de cada pieza publicada en @blueparrotschool.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-canales" hidden>
|
||||||
|
<div id="wk" class="empty"><b>Cargando los KPIs…</b><br>Aquí se verán todos los canales con filtro de fechas, comparación entre periodos y evolución mensual.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view" id="v-preguntar" hidden>
|
||||||
|
<section class="ask">
|
||||||
|
<h2>Pregunta a los datos</h2>
|
||||||
|
<p class="note">Claude responde con los datos de este panel. Cada pregunta usa tu cuenta de Claude.</p>
|
||||||
|
<div class="filters" id="sugs"></div>
|
||||||
|
<label for="q">Tu pregunta</label>
|
||||||
|
<textarea id="q" rows="3" placeholder="Por ejemplo: ¿qué canal trae los leads más baratos?"></textarea>
|
||||||
|
<div class="acts"><button class="btn primary" type="button" id="ask">Preguntar</button><button class="btn" type="button" id="stop" hidden>Parar</button></div>
|
||||||
|
<div id="ans" class="ans" hidden></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="tip" hidden></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var ESTADOS=[["propuesta","Propuesta"],["diseno","En diseño"],["revision","Revisión interna"],["cambios","Cambios pedidos"],["aprobada","Aprobada"],["publicada","Publicada"]];
|
||||||
|
var FORMATOS=[["carrusel","Carrusel"],["post","Post"],["story","Story"],["reel","Reel"]];
|
||||||
|
var CUPO=[["Publicaciones",["carrusel","post"],16],["Stories",["story"],20],["Reels",["reel"],8]];
|
||||||
|
var TIPOS={C:"Carrusel",R:"Reel",I:"Imagen"};
|
||||||
|
var VIEWS=["resumen","creatividades","publicado","canales","preguntar"];
|
||||||
|
var piezas=[], piezasOk=false, open=null, fF="", fE="", pView="lista", pMes=new Date().toISOString().slice(0,7), hist={}, db=null, kData=null, wData=null, kOrd="f", kView="g", view="resumen";
|
||||||
|
var \$=function(id){return document.getElementById(id)};
|
||||||
|
function esc(s){return String(s==null?"":s).replace(/[&<>"']/g,function(c){return {"&":"&","<":"<",">":">",'"':""","'":"'"}[c]})}
|
||||||
|
function lab(list,k){for(var i=0;i<list.length;i++)if(list[i][0]===k)return list[i][1];return k}
|
||||||
|
function fecha(f){if(!f)return "—";var p=String(f).split("-");return p.length===3?p[2]+"/"+p[1]:f}
|
||||||
|
function num(n){return Math.round(Number(n||0)).toLocaleString("de-DE")}
|
||||||
|
function fmt(v,f){if(v==null)return "—";if(f==="txt")return esc(v);var o={minimumFractionDigits:2,maximumFractionDigits:2};
|
||||||
|
if(f==="eur")return Number(v).toLocaleString("de-DE",o)+" €";if(f==="pct")return Number(v).toLocaleString("de-DE",o)+" %";
|
||||||
|
if(f==="dec")return Number(v).toLocaleString("de-DE",{minimumFractionDigits:1,maximumFractionDigits:1});return num(v)}
|
||||||
|
function compact(v,f){if(f==="eur"&&v>=1000)return num(v)+" €";return fmt(v,f)}
|
||||||
|
function tick(v,f){return f==="eur"?num(v)+" €":f==="pct"?num(v)+" %":num(v)}
|
||||||
|
function pct(a,b){if(a==null||b==null||!a)return null;return Math.round((b-a)/a*100)}
|
||||||
|
function dTxt(p){return p==null?"—":(p>0?"+":"")+p+" %"}
|
||||||
|
function dCls(p,up){if(p==null||!up||p===0)return "";return (p>0)===(up>0)?"good":"poor"}
|
||||||
|
function med(a){if(!a.length)return 0;var b=a.slice().sort(function(x,y){return x-y}),m=b.length>>1;return b.length%2?b[m]:Math.round((b[m-1]+b[m])/2)}
|
||||||
|
function fail(){var m=\$("msg");m.hidden=false;m.textContent="No se pudo guardar el cambio. Vuelve a intentarlo en unos segundos."}
|
||||||
|
|
||||||
|
/* ---------- gráficos ---------- */
|
||||||
|
function nice(m){if(m<=0)return 1;var p=Math.pow(10,Math.floor(Math.log10(m))),x=m/p;return (x<=1?1:x<=2?2:x<=5?5:10)*p}
|
||||||
|
function spark(v){var W=200,H=36,mx=Math.max.apply(null,v)||1,n=v.length;
|
||||||
|
var pts=v.map(function(y,i){return [4+i*(W-12)/(n-1),H-4-(y/mx)*(H-10)]});
|
||||||
|
var d=pts.map(function(p,i){return (i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)}).join(" ");
|
||||||
|
return '<svg viewBox="0 0 '+W+' '+H+'" preserveAspectRatio="none" aria-hidden="true"><path d="'+d+'" fill="none" stroke="var(--series)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke"/></svg>'}
|
||||||
|
function chart(o){ /* o:{k,v,f,tipo,labels,cap} */
|
||||||
|
var W=320,H=150,L=40,R=8,T=18,B=22,v=o.v,n=v.length,mx=nice(Math.max.apply(null,v)),pw=W-L-R,ph=H-T-B,bw=pw/n;
|
||||||
|
var y=function(x){return T+ph-(x/mx)*ph},s='<svg viewBox="0 0 '+W+' '+H+'" role="img" aria-label="'+esc(o.k)+'">';
|
||||||
|
[0,.5,1].forEach(function(g){var yy=y(mx*g);s+='<line x1="'+L+'" x2="'+(W-R)+'" y1="'+yy+'" y2="'+yy+'" stroke="var(--line)" stroke-width="1"/>';s+='<text x="'+(L-6)+'" y="'+(yy+3.5)+'" text-anchor="end">'+tick(mx*g,o.f)+'</text>'});
|
||||||
|
if(o.tipo==="col"){var w=Math.min(24,bw-2);v.forEach(function(x,i){var h=Math.max(0,(x/mx)*ph),cx=L+i*bw+(bw-w)/2,r=Math.min(4,h);
|
||||||
|
if(h>0)s+='<path d="M'+cx+' '+(T+ph)+'V'+(T+ph-h+r)+'q0 -'+r+' '+r+' -'+r+'h'+(w-2*r)+'q'+r+' 0 '+r+' '+r+'V'+(T+ph)+'z" fill="var(--series'+(o.hi&&!o.hi[i]?"-soft":"")+')"/>'})}
|
||||||
|
else{var pts=v.map(function(x,i){return [L+i*bw+bw/2,y(x)]}),d=pts.map(function(p,i){return (i?"L":"M")+p[0].toFixed(1)+" "+p[1].toFixed(1)}).join(" "),e=pts[n-1];
|
||||||
|
s+='<path d="'+d+'L'+e[0]+' '+(T+ph)+'L'+pts[0][0]+' '+(T+ph)+'z" fill="var(--series)" fill-opacity=".1"/><path d="'+d+'" fill="none" stroke="var(--series)" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/><circle cx="'+e[0]+'" cy="'+e[1]+'" r="4" fill="var(--series)" stroke="var(--surface)" stroke-width="2"/>';
|
||||||
|
s+='<text class="end" text-anchor="end" x="'+(W-R)+'" y="'+Math.max(11,e[1]-9)+'">'+fmt(v[n-1],o.f)+'</text>'}
|
||||||
|
var lb=o.labels||[],ix=n<=8?v.map(function(_,i){return i}):[0,Math.floor((n-1)/2),n-1];ix.forEach(function(i){if(lb[i]!=null)s+='<text x="'+(L+i*bw+bw/2)+'" y="'+(H-6)+'" text-anchor="middle">'+esc(lb[i])+'</text>'});
|
||||||
|
v.forEach(function(x,i){s+='<rect class="hit" tabindex="0" x="'+(L+i*bw)+'" y="'+T+'" width="'+bw+'" height="'+ph+'" data-tip="'+esc((o.tipl?o.tipl[i]:lb[i])||"")+'|'+esc(fmt(x,o.f))+'"><title>'+esc((o.tipl?o.tipl[i]:lb[i])||"")+': '+esc(fmt(x,o.f))+'</title></rect>'});
|
||||||
|
return '<div class="ch"><h3>'+esc(o.k)+'</h3><div class="cap">'+esc(o.cap||"")+'</div>'+s+'</svg></div>'}
|
||||||
|
function tipShow(el,ev){var t=\$("tip"),p=(el.dataset.tip||"").split("|");t.innerHTML='<b>'+esc(p[1]||"")+'</b>'+esc(p[0]||"");t.hidden=false;
|
||||||
|
var r=el.getBoundingClientRect(),x=ev&&ev.clientX!=null?ev.clientX:r.left+r.width/2,yy=r.top;
|
||||||
|
t.style.left=Math.max(8,Math.min(window.innerWidth-t.offsetWidth-8,x-t.offsetWidth/2))+"px";t.style.top=Math.max(8,yy-t.offsetHeight-6)+"px"}
|
||||||
|
document.addEventListener("mousemove",function(ev){var h=ev.target.closest&&ev.target.closest(".hit");if(h)tipShow(h,ev);else \$("tip").hidden=true});
|
||||||
|
document.addEventListener("focusin",function(ev){var h=ev.target.closest&&ev.target.closest(".hit");if(h)tipShow(h);else \$("tip").hidden=true});
|
||||||
|
|
||||||
|
/* ---------- datos diarios ---------- */
|
||||||
|
var K=null,KC=null,kf={p:"m1",a:"",b:"",c:"prev",ca:"",cb:""};
|
||||||
|
var MN=["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"],MNL=["enero","febrero","marzo","abril","mayo","junio","julio","agosto","septiembre","octubre","noviembre","diciembre"];
|
||||||
|
var SRC=[
|
||||||
|
{id:"meta",t:"Meta Ads",fuente:"Cuenta publicitaria BPS",res:"Leads",mt:[{k:"Inversión",f:"eur",a:"spend",up:0},{k:"Impresiones",a:"imp",up:0},{k:"Clics en el enlace",a:"clk",up:0},{k:"Leads",a:"res",up:1},{k:"Coste por lead",f:"eur",n:"spend",d:"res",up:-1}],ch:[3,0,4]},
|
||||||
|
{id:"gads",t:"Google Ads",fuente:"Cuenta BPS",res:"Conversiones",mt:[{k:"Inversión",f:"eur",a:"spend",up:0},{k:"Impresiones",a:"imp",up:0},{k:"Clics",a:"clk",up:0},{k:"Conversiones",a:"res",up:1},{k:"Coste por conversión",f:"eur",n:"spend",d:"res",up:-1}],ch:[3,0,4]},
|
||||||
|
{id:"ig",t:"Instagram",fuente:"@blueparrotschool",nota:"Las visualizaciones incluyen las que vienen de anuncios, así que se mueven con la inversión en Meta Ads.",mt:[{k:"Visualizaciones",a:"views",up:1},{k:"Interacciones",a:"inter",up:1},{k:"Me gusta",a:"likes",up:1},{k:"Comentarios",a:"com",up:1},{k:"Guardados",a:"saves",up:1},{k:"Compartidos",a:"shares",up:1},{k:"Toques en enlaces del perfil",a:"taps",up:1}],ch:[0,1,4]},
|
||||||
|
{id:"fb",t:"Facebook",fuente:"Página Blue Parrot School",mt:[{k:"Impresiones de la página",a:"imp",up:1},{k:"Interacciones con publicaciones",a:"eng",up:1},{k:"Visitas a la página",a:"views",up:1},{k:"Nuevos seguidores",a:"fol",up:1}],ch:[0,1]},
|
||||||
|
{id:"li",t:"LinkedIn",fuente:"Página Blue Parrot School",mt:[{k:"Impresiones",a:"imp",up:1},{k:"Clics",a:"clk",up:1},{k:"Interacciones",a:"eng",up:1},{k:"Nuevos seguidores orgánicos",a:"fol",up:1}],ch:[0,3]},
|
||||||
|
{id:"gsc",t:"Búsqueda orgánica en Google",fuente:"Search Console · blueparrotschool.com",nota:"Search Console publica los datos con unos tres días de retraso.",mt:[{k:"Clics",a:"clk",up:1},{k:"Impresiones",a:"imp",up:1},{k:"CTR",f:"pct",n:"clk",d:"imp",x:100,up:1},{k:"Posición media",f:"dec",n:"posw",d:"imp",up:-1}],ch:[0,1,3]},
|
||||||
|
{id:"ga4",t:"Web",fuente:"Google Analytics 4 · propiedad B2C BPS",nota:"Esta propiedad registra muchas menos sesiones que los clics que da Search Console, así que puede no estar midiendo toda la web. Los usuarios son la suma de los usuarios diarios.",mt:[{k:"Sesiones",a:"ses",up:0},{k:"Usuarios",a:"usr",up:0},{k:"Usuarios nuevos",a:"new",up:0},{k:"Eventos clave",a:"key",up:0}],ch:[0,3]},
|
||||||
|
{id:"gbp",t:"Google Business Profile",fuente:"Fichas de Madrid y Málaga",mt:[{k:"Impresiones",a:"imp",up:1},{k:"Llamadas",a:"call",up:1},{k:"Clics a la web",a:"web",up:1},{k:"Solicitudes de ruta",a:"dir",up:1}],ch:[1,2,3]}];
|
||||||
|
function T0(){return Date.parse(K.start+"T00:00:00Z")}
|
||||||
|
function dIdx(s){return Math.round((Date.parse(s+"T00:00:00Z")-T0())/864e5)}
|
||||||
|
function dStr(i){return new Date(T0()+i*864e5).toISOString().slice(0,10)}
|
||||||
|
function ymd(y,m,d){return new Date(Date.UTC(y,m,d)).toISOString().slice(0,10)}
|
||||||
|
function fd(s){var p=s.split("-");return p[2]+"/"+p[1]+"/"+p[0]}
|
||||||
|
function ok(s){return /^\\d{4}-\\d{2}-\\d{2}\$/.test(s||"")}
|
||||||
|
function sums(M,a,b){var i0=Math.max(0,dIdx(a)),i1=Math.min(K.n-1,dIdx(b)),o={_any:false};for(var k in M){var s=0,arr=M[k];for(var i=i0;i<=i1;i++){var v=arr[i];if(v!=null){s+=v;o._any=true}}o[k]=s}return o}
|
||||||
|
function val(m,S){if(!S||!S._any)return null;if(m.n)return S[m.d]?S[m.n]/S[m.d]*(m.x||1):null;return S[m.a]}
|
||||||
|
function months(){var out=[],s=K.start.split("-"),y=+s[0],m=+s[1]-1;for(;;){var a=ymd(y,m,1),b=ymd(y,m+1,0);if(a>K.end)break;out.push({a:a,b:b>K.end?K.end:b,lab:MN[m]+" "+String(y).slice(2),full:MNL[m]+" de "+y,part:b>K.end});m++;if(m>11){m=0;y++}}return out}
|
||||||
|
function range(){var E=K.end.split("-"),y=+E[0],m=+E[1]-1,p=kf.p;
|
||||||
|
if(p==="c"&&ok(kf.a)&&ok(kf.b)&&kf.a<=kf.b)return [kf.a<K.start?K.start:kf.a,kf.b>K.end?K.end:kf.b];
|
||||||
|
if(p==="m0")return [ymd(y,m,1),K.end];if(p==="y")return [ymd(y,0,1),K.end];
|
||||||
|
var n=p==="m3"?3:p==="m6"?6:p==="m12"?12:1;return [ymd(y,m-n,1),ymd(y,m,0)]}
|
||||||
|
function cmpRange(a,b){if(kf.c==="none")return null;if(kf.c==="c")return ok(kf.ca)&&ok(kf.cb)&&kf.ca<=kf.cb?[kf.ca,kf.cb]:null;
|
||||||
|
var A=a.split("-").map(Number),B=b.split("-").map(Number);
|
||||||
|
if(kf.c==="yoy")return [ymd(A[0]-1,A[1]-1,A[2]),ymd(B[0]-1,B[1]-1,B[2])];
|
||||||
|
if(A[2]===1&&ymd(B[0],B[1],0)===b){var n=(B[0]-A[0])*12+B[1]-A[1]+1;return [ymd(A[0],A[1]-1-n,1),ymd(A[0],A[1]-1,0)]}
|
||||||
|
var d=dIdx(b)-dIdx(a)+1;return [dStr(dIdx(a)-d),dStr(dIdx(a)-1)]}
|
||||||
|
function weekly(get){var out=[],e=dIdx(K.end);for(var w=12;w>=0;w--){out.push(get(dStr(e-w*7-6),dStr(e-w*7))||0)}return out}
|
||||||
|
|
||||||
|
/* ---------- resumen ---------- */
|
||||||
|
function renderResumen(){
|
||||||
|
if(!K)return;
|
||||||
|
var E=K.end.split("-"),y=+E[0],m=+E[1]-1,a=ymd(y,m-1,1),b=ymd(y,m,0),pa=ymd(y,m-2,1),pb=ymd(y,m-1,0),PM=new Date(Date.UTC(y,m-1,1)),h="";
|
||||||
|
var S=function(id,x,z){return sums(K.src[id].m,x,z)},tl=function(k,get,f,up,sub){var v=get(a,b),p=pct(get(pa,pb),v);
|
||||||
|
return '<div class="tile"><div class="lab">'+k+'</div><div><b>'+compact(v,f)+'</b> '+(p==null?"":'<span class="dl '+dCls(p,up)+'">'+dTxt(p)+'</span>')+'</div><small>'+sub+'</small>'+spark(weekly(get))+'</div>'};
|
||||||
|
var cpl=val(SRC[0].mt[4],S("meta",a,b)),cpa=val(SRC[1].mt[4],S("gads",a,b));
|
||||||
|
h+='<section class="sec"><h2>'+MNL[PM.getUTCMonth()].charAt(0).toUpperCase()+MNL[PM.getUTCMonth()].slice(1)+' de '+PM.getUTCFullYear()+'<small>variación frente al mes anterior · tendencia de 13 semanas</small></h2><div class="tiles t3">'+
|
||||||
|
tl("Inversión publicitaria",function(x,z){return S("meta",x,z).spend+S("gads",x,z).spend},"eur",0,"Meta Ads y Google Ads")+
|
||||||
|
tl("Leads de Meta Ads",function(x,z){return S("meta",x,z).res},"num",1,cpl==null?"":"a "+fmt(cpl,"eur")+" por lead")+
|
||||||
|
tl("Conversiones de Google Ads",function(x,z){return S("gads",x,z).res},"num",1,cpa==null?"":"a "+fmt(cpa,"eur")+" por conversión")+
|
||||||
|
tl("Clics orgánicos en Google",function(x,z){return S("gsc",x,z).clk},"num",1,"Search Console")+
|
||||||
|
tl("Acciones en Google Business",function(x,z){var s=S("gbp",x,z);return s.call+s.web+s.dir},"num",1,"llamadas, web y rutas")+
|
||||||
|
tl("Visualizaciones en Instagram",function(x,z){return S("ig",x,z).views},"num",1,"orgánico y anuncios")+'</div></section>';
|
||||||
|
var ins=[];SRC.forEach(function(s){var c=sums(K.src[s.id].m,a,b),o=sums(K.src[s.id].m,pa,pb);s.mt.forEach(function(mt){var v=val(mt,c),w=val(mt,o),p=pct(w,v);if(p==null||!mt.up||Math.abs(p)<15||w<5)return;
|
||||||
|
ins.push({p:p,good:(p>0)===(mt.up>0),t:s.t+" · "+mt.k,x:fmt(v,mt.f)+", "+dTxt(p)+" frente al mes anterior ("+fmt(w,mt.f)+")"})})});
|
||||||
|
ins.sort(function(x,z){return Math.abs(z.p)-Math.abs(x.p)});
|
||||||
|
var line=function(x,c,l){return '<div class="ln"><span class="tag '+c+'">'+l+'</span><p>'+esc(x.t)+'<small>'+esc(x.x)+'</small></p></div>'},up=ins.filter(function(x){return x.good}).slice(0,4),dn=ins.filter(function(x){return !x.good}).slice(0,4);
|
||||||
|
h+='<div class="two"><section class="sec"><h2>Lo que mejora</h2><div class="lines">'+(up.map(function(x){return line(x,"good","▲ Mejora")}).join("")||'<p class="note">Ningún indicador mejora más de un 15 %.</p>')+'</div></section>'+
|
||||||
|
'<section class="sec"><h2>Lo que empeora</h2><div class="lines">'+(dn.map(function(x){return line(x,"poor","▼ Empeora")}).join("")||'<p class="note">Ningún indicador empeora más de un 15 %.</p>')+'</div></section></div>';
|
||||||
|
var av=((wData&&wData.avisos)||[]).slice();
|
||||||
|
if(piezasOk){var mh=new Date().toISOString().slice(0,7);CUPO.forEach(function(c){var n=piezas.filter(function(p){return c[1].indexOf(p.formato)>=0&&String(p.fecha).slice(0,7)===mh}).length;if(n<c[2])av.unshift({n:"att",t:c[0]+": "+n+" de "+c[2]+" planificadas este mes",d:"Faltan "+(c[2]-n)+" para cubrir el paquete contratado."})});
|
||||||
|
var pend=piezas.filter(function(p){return p.estado==="revision"||p.estado==="cambios"}).length;if(pend)av.unshift({n:"att",t:pend===1?"1 pieza espera revisión o cambios":pend+" piezas esperan revisión o cambios",d:"Están en la pestaña Creatividades."})}
|
||||||
|
h+='<div class="two"><section class="sec"><h2>Avisos</h2><div class="lines">'+(av.map(function(x){return '<div class="ln"><span class="tag '+(x.n==="att"?"att":"inf")+'">'+(x.n==="att"?"! Atención":"i Nota")+'</span><p>'+esc(x.t)+'<small>'+esc(x.d||"")+'</small></p></div>'}).join("")||'<p class="note">Sin avisos.</p>')+'</div></section>';
|
||||||
|
var hoy=new Date().toISOString().slice(0,10),prox=piezas.filter(function(p){return p.fecha>=hoy&&p.estado!=="publicada"}).slice(0,5);
|
||||||
|
h+='<section class="sec"><h2>Próximas publicaciones</h2><div class="lines">'+(prox.map(function(p){return '<div class="ln"><span class="tag inf">'+esc(fecha(p.fecha))+'</span><p>'+esc(p.titulo)+'<small>'+esc(lab(FORMATOS,p.formato))+' · '+esc(lab(ESTADOS,p.estado))+'</small></p></div>'}).join("")||'<p class="note">'+(piezasOk?"No hay piezas planificadas a partir de hoy.":"Cargando las piezas…")+'</p>')+'</div></section></div>';
|
||||||
|
\$("r-body").className="view";\$("r-body").innerHTML=h;
|
||||||
|
\$("fresh").textContent="Datos hasta el "+fd(K.end)+".";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- creatividades ---------- */
|
||||||
|
var MESES=(function(){var o=[],y=2026,m=0;for(var i=0;i<13;i++){o.push(y+"-"+(m<9?"0":"")+(m+1));m++;if(m>11){m=0;y++}}return o})();
|
||||||
|
var MNOM=["Enero","Febrero","Marzo","Abril","Mayo","Junio","Julio","Agosto","Septiembre","Octubre","Noviembre","Diciembre"];
|
||||||
|
function mesNom(k){return MNOM[+k.slice(5)-1]+" de "+k.slice(0,4)}
|
||||||
|
function pm(){return piezas.filter(function(p){return String(p.fecha).slice(0,7)===pMes})}
|
||||||
|
function loadHist(){if(!db||hist[pMes]!==undefined)return;var k=pMes;hist[k]=null;db.doc("hist/"+k).get().then(function(s){hist[k]=s.exists?(s.data().posts||[]):[];if(k===pMes)renderHist()},function(){hist[k]=[];if(k===pMes)renderHist()})}
|
||||||
|
function renderHist(){
|
||||||
|
var el=\$("hist"),ps=hist[pMes],hoy=new Date().toISOString().slice(0,7);
|
||||||
|
if(pMes>hoy){el.innerHTML="";return}
|
||||||
|
var h='<h2>Publicado en '+mesNom(pMes).toLowerCase()+'<small>texto e imagen de cada pieza en Instagram</small></h2>';
|
||||||
|
if(ps==null)h+='<div class="empty">Cargando lo publicado…</div>';
|
||||||
|
else if(!ps.length)h+='<div class="empty">No hay piezas publicadas guardadas de este mes.</div>';
|
||||||
|
else h+='<div class="hl">'+ps.map(function(p){var cap=p.cap||"Sin texto.",long=cap.length>260;
|
||||||
|
return '<article class="hp">'+(p.img?'<img src="'+esc(p.img)+'" alt="" loading="lazy">':'<span class="noimg"></span>')+'<div><span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span>'+
|
||||||
|
(long?'<details><summary>'+esc(cap.slice(0,220))+'… <u>ver todo</u></summary><p>'+esc(cap)+'</p></details>':'<p>'+esc(cap)+'</p>')+
|
||||||
|
'<small>Alcance '+num(p.r)+' · '+num(p.v)+' visualizaciones · '+num(p.l)+' me gusta · '+num(p.g)+' guardados · <a href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">Ver en Instagram</a></small></div></article>'}).join("")+'</div><p class="note">Se muestra la primera imagen de cada carrusel y la portada de cada reel. Las stories no se conservan en Instagram.</p>';
|
||||||
|
el.innerHTML=h;
|
||||||
|
}
|
||||||
|
function renderQuota(){
|
||||||
|
\$("quota").innerHTML=CUPO.map(function(c){
|
||||||
|
var x=pm().filter(function(p){return c[1].indexOf(p.formato)>=0}),ok=x.filter(function(p){return p.estado==="aprobada"||p.estado==="publicada"}).length;
|
||||||
|
return '<div class="tile"><div class="lab">'+c[0]+'</div><div><b>'+x.length+'</b> <small>de '+c[2]+' planificadas</small></div><div class="bar"><i style="width:'+Math.min(100,Math.round(x.length/c[2]*100))+'%"></i></div><small>'+ok+' aprobadas o publicadas</small></div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
function chip(g,k,t,cur){return '<button class="chip" type="button" data-g="'+g+'" data-k="'+k+'" aria-pressed="'+(cur===k)+'">'+t+'</button>'}
|
||||||
|
function renderFilters(){
|
||||||
|
\$("meses").innerHTML=MESES.map(function(k){return chip("mes",k,MNOM[+k.slice(5)-1].slice(0,3)+" "+k.slice(2,4),pMes)}).join("");\$("ptit").textContent="Propuestas de "+mesNom(pMes).toLowerCase();
|
||||||
|
\$("filters").innerHTML=chip("pv","lista","Lista",pView)+chip("pv","cal","Calendario",pView)+'<span class="sep"></span>'+chip("f","","Todos los formatos",fF)+FORMATOS.map(function(f){return chip("f",f[0],f[1],fF)}).join("")+'<span class="sep"></span>'+chip("e","","Todos los estados",fE)+ESTADOS.map(function(e){return chip("e",e[0],e[1],fE)}).join("");
|
||||||
|
}
|
||||||
|
function renderCal(rows){
|
||||||
|
var base=pMes,y=+base.slice(0,4),m=+base.slice(5,7),first=new Date(Date.UTC(y,m-1,1)),off=(first.getUTCDay()+6)%7,dim=new Date(Date.UTC(y,m,0)).getUTCDate();
|
||||||
|
var h='<div class="calw"><div class="cal">'+["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"].map(function(x){return '<div class="hd">'+x+'</div>'}).join("");
|
||||||
|
for(var i=0;i<off;i++)h+='<div class="day off"></div>';
|
||||||
|
for(var dd=1;dd<=dim;dd++){var key=base+"-"+(dd<10?"0":"")+dd,ps=rows.filter(function(p){return p.fecha===key});
|
||||||
|
h+='<div class="day"><span class="num">'+dd+'</span>'+ps.map(function(p){return '<a class="'+esc(p.estado)+'" tabindex="0" role="button" data-goto="'+esc(p.id)+'" title="'+esc(lab(ESTADOS,p.estado))+'"><b>'+esc(lab(FORMATOS,p.formato))+'</b> '+esc(p.titulo)+'</a>'}).join("")+'</div>'}
|
||||||
|
return h+'</div></div><div class="filters"><span class="lab">Estados</span>'+ESTADOS.map(function(e){return '<span class="st '+e[0]+'">'+e[1]+'</span>'}).join("")+'</div><p class="note">Pulsa una pieza para abrirla en la lista.</p>';
|
||||||
|
}
|
||||||
|
function renderList(){
|
||||||
|
var all=pm(),rows=all.filter(function(p){return (!fF||p.formato===fF)&&(!fE||p.estado===fE)});
|
||||||
|
if(!all.length){\$("list").innerHTML='<div class="empty"><b>No hay propuestas guardadas de este mes.</b><br>'+(pMes<"2026-10"?"El panel empezó a registrar propuestas en octubre de 2026. Debajo tienes lo que se publicó.":"Pide a Claude que proponga los temas y copys del mes y aparecerán aquí.")+'</div>';return}
|
||||||
|
if(!rows.length){\$("list").innerHTML='<div class="empty">Ninguna pieza coincide con estos filtros.</div>';return}
|
||||||
|
if(pView==="cal"){\$("list").innerHTML=renderCal(rows);return}
|
||||||
|
\$("list").innerHTML=rows.map(function(p){
|
||||||
|
var h='<div class="item" id="it-'+esc(p.id)+'"><button class="row" type="button" data-open="'+esc(p.id)+'" aria-expanded="'+(open===p.id)+'"><span class="d">'+esc(fecha(p.fecha))+'</span><span class="fmt">'+esc(lab(FORMATOS,p.formato))+'</span><span class="t">'+esc(p.titulo)+'<small>'+esc(p.pilar||"")+'</small></span><span class="st '+esc(p.estado)+'">'+esc(lab(ESTADOS,p.estado))+'</span></button>';
|
||||||
|
if((p.imgs||[]).length){h+='<div class="thumbs">'+p.imgs.map(function(i,k){return '<img src="/_blob/'+esc(i)+'" alt="'+esc(p.titulo)+', imagen '+(k+1)+'" loading="lazy">'}).join("")+'</div>'}
|
||||||
|
else if((p.imgsd||[]).length){h+='<div class="thumbs">'+p.imgsd.map(function(i,k){return '<img src="'+esc(i)+'" alt="'+esc(p.titulo)+', slide '+(k+1)+'" loading="lazy">'}).join("")+'</div>'}
|
||||||
|
else if(p.img){h+='<div class="thumbs"><img src="'+esc(p.img)+'" alt="'+esc(p.titulo)+'" loading="lazy"></div>'}
|
||||||
|
if(open!==p.id&&p.copy){h+='<p class="pcopy">'+esc(p.copy)+'</p>'}
|
||||||
|
if(open===p.id){
|
||||||
|
var cms=(p.comentarios||[]).map(function(c){return '<div class="cm"><time>'+esc(c.t||"")+'</time>'+esc(c.texto)+'</div>'}).join("");
|
||||||
|
h+='<div class="detail"><div><label for="copy-'+esc(p.id)+'">Copy</label><textarea id="copy-'+esc(p.id)+'" rows="8">'+esc(p.copy||"")+'</textarea><div class="acts"><button class="btn primary" type="button" data-save="'+esc(p.id)+'">Guardar copy</button><button class="btn" type="button" data-copy="'+esc(p.id)+'">Copiar texto</button></div><div class="lab">Idea visual</div><div class="visual">'+esc(p.visual||"Sin definir")+'</div></div>'+
|
||||||
|
'<div><div class="lab">Estado</div><div class="acts">'+ESTADOS.map(function(e){return '<button class="btn'+(p.estado===e[0]?" current":"")+'" type="button" data-id="'+esc(p.id)+'" data-estado="'+e[0]+'">'+e[1]+'</button>'}).join("")+'</div>'+
|
||||||
|
'<div class="lab">Comentarios</div>'+(cms||'<div class="note">Sin comentarios.</div>')+'<label for="cm-'+esc(p.id)+'">Añadir comentario</label><input type="text" id="cm-'+esc(p.id)+'" placeholder="Por ejemplo: cambiar el titular de la portada"><div class="acts"><button class="btn" type="button" data-cm="'+esc(p.id)+'">Añadir</button></div></div></div>';
|
||||||
|
}
|
||||||
|
return h+'</div>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
function renderPiezas(){var past=pMes<"2026-10";\$("quota").hidden=past;\$("psec").hidden=past;renderQuota();renderFilters();renderList();renderHist();loadHist()}
|
||||||
|
function find(id){for(var i=0;i<piezas.length;i++)if(piezas[i].id===id)return piezas[i]}
|
||||||
|
function upd(id,data){if(!db)return;\$("msg").hidden=true;db.doc("piezas/"+id).update(data).catch(fail)}
|
||||||
|
|
||||||
|
/* ---------- publicado ---------- */
|
||||||
|
function renderKpi(d){
|
||||||
|
kData=d;var ps=(d.posts||[]).slice(),mr=med(ps.map(function(p){return p.r}));
|
||||||
|
var h='<div class="tiles"><div class="tile"><div class="lab">Seguidores</div><div><b>'+num(d.seguidores)+'</b></div><small>a '+esc(fecha(d.actualizado))+'</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Piezas publicadas</div><div><b>'+ps.length+'</b></div><small>del '+esc(fecha(d.desde))+' al '+esc(fecha(d.hasta))+'</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Alcance mediano por pieza</div><div><b>'+num(mr)+'</b></div><small>cuentas alcanzadas</small></div>'+
|
||||||
|
'<div class="tile"><div class="lab">Guardados por pieza</div><div><b>'+(ps.reduce(function(a,p){return a+p.g},0)/Math.max(1,ps.length)).toLocaleString("de-DE",{maximumFractionDigits:1})+'</b></div><small>media del periodo</small></div></div>';
|
||||||
|
var best=ps.slice().sort(function(a,b){return (b.g*40+b.r)-(a.g*40+a.r)}).filter(function(p){return p.c<50}).slice(0,4);
|
||||||
|
h+='<section class="sec"><h2>Las que mejor funcionan<small>por guardados y alcance, sin contar sorteos</small></h2><div class="best">'+best.map(function(p){return '<a class="bp" href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+(p.i?'<img src="/_blob/'+esc(p.i)+'" alt="" loading="lazy">':"")+'<div><span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span><b>'+esc(p.n)+'</b><small>'+num(p.g)+' guardados · alcance '+num(p.r)+'</small></div></a>'}).join("")+'</div></section>';
|
||||||
|
var DN=["Lun","Mar","Mié","Jue","Vie","Sáb","Dom"],by=DN.map(function(){return []});ps.forEach(function(p){var x=new Date(p.f+"T12:00:00Z");by[(x.getUTCDay()+6)%7].push(p.r)});
|
||||||
|
var mes={};ps.forEach(function(p){var k=p.f.slice(0,7);(mes[k]=mes[k]||[]).push(p.r)});var mk=Object.keys(mes).sort(),MN=["ene","feb","mar","abr","may","jun","jul","ago","sep","oct","nov","dic"];
|
||||||
|
h+='<section class="sec"><h2>Patrones</h2><div class="charts">'+
|
||||||
|
chart({k:"Alcance mediano por día de publicación",cap:"cuentas alcanzadas",v:by.map(med),f:"num",tipo:"col",labels:DN,tipl:DN.map(function(x,i){return x+" · "+by[i].length+" piezas"})})+
|
||||||
|
chart({k:"Alcance mediano por mes",cap:"cuentas alcanzadas",v:mk.map(function(k){return med(mes[k])}),f:"num",tipo:"col",labels:mk.map(function(k){return MN[+k.slice(5)-1]}),tipl:mk.map(function(k){return MN[+k.slice(5)-1]+" · "+mes[k].length+" piezas"})})+
|
||||||
|
chart({k:"Piezas publicadas por mes",cap:"feed y reels",v:mk.map(function(k){return mes[k].length}),f:"num",tipo:"col",labels:mk.map(function(k){return MN[+k.slice(5)-1]})})+'</div><p class="note">Los días con pocas piezas dan medianas poco fiables; pasa el cursor para ver cuántas hay.</p></section>';
|
||||||
|
h+='<section class="sec"><h2>Por formato</h2><div class="tw"><table><thead><tr><th>Formato</th><th class="n">Piezas</th><th class="n">Alcance mediano</th><th class="n">Visualizaciones medianas</th><th class="n">Guardados por pieza</th></tr></thead><tbody>'+
|
||||||
|
["C","R","I"].map(function(t){var x=ps.filter(function(p){return p.t===t});if(!x.length)return "";var g=x.reduce(function(a,p){return a+p.g},0);
|
||||||
|
return '<tr><td>'+TIPOS[t]+'</td><td class="n">'+x.length+'</td><td class="n">'+num(med(x.map(function(p){return p.r})))+'</td><td class="n">'+num(med(x.map(function(p){return p.v})))+'</td><td class="n">'+(g/x.length).toLocaleString("de-DE",{maximumFractionDigits:1})+'</td></tr>'}).join("")+'</tbody></table></div></section>';
|
||||||
|
var cols=[["r","Alcance"],["v","Visualizaciones"],["l","Me gusta"],["c","Comentarios"],["g","Guardados"],["s","Compartidos"]];
|
||||||
|
ps.sort(function(a,b){return kOrd==="f"?(a.f<b.f?1:-1):(b[kOrd]-a[kOrd])});
|
||||||
|
h+='<section class="sec"><h2>Todas las piezas</h2><div class="filters">'+chip("kv","g","Galería",kView)+chip("kv","t","Tabla",kView)+'<span class="sep"></span><span class="lab">Ordenar por</span>'+chip("ord","f","Fecha",kOrd)+cols.map(function(c){return chip("ord",c[0],c[1],kOrd)}).join("")+'</div>';
|
||||||
|
if(kView==="g") h+='<div class="gal">'+ps.map(function(p){return '<a class="card" href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+(p.i?'<img src="/_blob/'+esc(p.i)+'" alt="" loading="lazy">':'<span class="noimg"></span>')+(p.r>=mr*1.5?'<span class="top">Destaca</span>':"")+'<span class="fmt">'+TIPOS[p.t]+' · '+esc(fecha(p.f))+'</span><b>'+esc(p.n)+'</b><small>Alcance '+num(p.r)+' · '+num(p.g)+' guardados</small></a>'}).join("")+'</div>';
|
||||||
|
else h+='<div class="tw"><table><thead><tr><th>Fecha</th><th>Formato</th><th>Pieza</th>'+cols.map(function(c){return '<th class="n">'+c[1]+'</th>'}).join("")+'</tr></thead><tbody>'+
|
||||||
|
ps.map(function(p){return '<tr><td>'+esc(fecha(p.f))+'</td><td>'+TIPOS[p.t]+'</td><td class="wrapc"><a href="https://www.instagram.com/'+esc(p.u)+'/" target="_blank" rel="noopener">'+esc(p.n)+'</a></td>'+cols.map(function(c){return '<td class="n">'+num(p[c[0]])+'</td>'}).join("")+'</tr>'}).join("")+'</tbody></table></div>';
|
||||||
|
h+='<p class="note">«Destaca» marca las piezas con un alcance al menos un 50 % superior a la mediana. Las stories no aparecen porque Instagram solo conserva sus métricas 24 horas.</p></section>';
|
||||||
|
\$("kpi").className="view";\$("kpi").innerHTML=h;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- canales ---------- */
|
||||||
|
function renderWk(d){wData=d;renderResumen()}
|
||||||
|
function renderK(){
|
||||||
|
if(!K)return;
|
||||||
|
var r=range(),a=r[0],b=r[1],c=cmpRange(a,b),ms=months(),i0=dIdx(a),i1=dIdx(b);
|
||||||
|
var P=[["m0","Este mes"],["m1","Mes pasado"],["m3","Últimos 3 meses"],["m6","Últimos 6 meses"],["m12","Últimos 12 meses"],["y","Este año"],["c","Personalizado"]],C=[["prev","Periodo anterior"],["yoy","Mismo periodo del año anterior"],["c","Otras fechas"],["none","Sin comparar"]];
|
||||||
|
var h='<section class="fbar" aria-label="Filtros de fecha"><div class="filters"><span class="lab">Periodo</span>'+P.map(function(x){return chip("kp",x[0],x[1],kf.p)}).join("")+'</div>';
|
||||||
|
if(kf.p==="c")h+='<div class="filters"><label for="kf-a">Desde</label><input type="date" id="kf-a" data-kf="a" value="'+esc(kf.a||a)+'" min="'+K.start+'" max="'+K.end+'"><label for="kf-b">Hasta</label><input type="date" id="kf-b" data-kf="b" value="'+esc(kf.b||b)+'" min="'+K.start+'" max="'+K.end+'"></div>';
|
||||||
|
h+='<div class="filters"><span class="lab">Comparar con</span>'+C.map(function(x){return chip("kc",x[0],x[1],kf.c)}).join("")+'</div>';
|
||||||
|
if(kf.c==="c")h+='<div class="filters"><label for="kf-ca">Desde</label><input type="date" id="kf-ca" data-kf="ca" value="'+esc(kf.ca)+'" min="'+K.start+'" max="'+K.end+'"><label for="kf-cb">Hasta</label><input type="date" id="kf-cb" data-kf="cb" value="'+esc(kf.cb)+'" min="'+K.start+'" max="'+K.end+'"></div>';
|
||||||
|
h+='<p class="note"><b>'+fd(a)+' – '+fd(b)+'</b>'+(c?' comparado con <b>'+fd(c[0])+' – '+fd(c[1])+'</b>':kf.c==="c"?' · elige las dos fechas de comparación':'')+'. En los gráficos, los meses del periodo van en color intenso.</p></section>';
|
||||||
|
SRC.forEach(function(s){
|
||||||
|
var D=K.src[s.id],cur=sums(D.m,a,b),old=c?sums(D.m,c[0],c[1]):null;
|
||||||
|
h+='<section class="sec"><h2>'+esc(s.t)+'<small>'+esc(s.fuente||"")+' · datos del '+fd(D.from)+' al '+fd(D.to)+'</small></h2>';
|
||||||
|
if(!cur._any){h+='<div class="empty">Sin datos de este canal en el periodo elegido.</div></section>';return}
|
||||||
|
h+='<div class="tiles">'+s.mt.map(function(m){var v=val(m,cur),w=old?val(m,old):null,p=pct(w,v);
|
||||||
|
return '<div class="tile"><div class="lab">'+esc(m.k)+'</div><div><b>'+compact(v,m.f)+'</b> '+(p==null?"":'<span class="dl '+dCls(p,m.up)+'">'+dTxt(p)+'</span>')+'</div><small>'+(c?(w==null?"sin datos en la comparación":"antes "+fmt(w,m.f)):"")+'</small></div>'}).join("")+'</div>';
|
||||||
|
var mm=ms.map(function(x){return {x:x,S:sums(D.m,x.a,x.b)}}).filter(function(o){return o.S._any});
|
||||||
|
h+='<div class="charts">'+s.ch.map(function(ci){var m=s.mt[ci];return chart({k:m.k,cap:"por mes",v:mm.map(function(o){return val(m,o.S)||0}),f:m.f||"num",tipo:"col",labels:mm.map(function(o){return o.x.lab}),tipl:mm.map(function(o){return o.x.full+(o.x.part?" (parcial)":"")}),hi:mm.map(function(o){return o.x.a<=b&&o.x.b>=a})})}).join("")+'</div>';
|
||||||
|
var inr=mm.filter(function(o){return o.x.a<=b&&o.x.b>=a});
|
||||||
|
h+='<div class="lab">Mes a mes</div><div class="tw"><table><thead><tr><th>Mes</th>'+s.mt.map(function(m){return '<th class="n">'+esc(m.k)+'</th>'}).join("")+'</tr></thead><tbody>'+
|
||||||
|
inr.map(function(o){var x=o.x,pa=x.a<a?a:x.a,pb=x.b>b?b:x.b,S2=(pa!==x.a||pb!==x.b)?sums(D.m,pa,pb):o.S,part=pa!==x.a||pb!==ymd(+x.a.slice(0,4),+x.a.slice(5,7),0);
|
||||||
|
return '<tr><td>'+esc(x.full)+(part?' <span class="dl">parcial</span>':'')+'</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,S2),m.f)+'</td>'}).join("")+'</tr>'}).join("")+
|
||||||
|
'<tr class="tot"><td>Total del periodo</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,cur),m.f)+'</td>'}).join("")+'</tr>'+
|
||||||
|
(old?'<tr><td>Periodo comparado</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,old),m.f)+'</td>'}).join("")+'</tr>':"")+'</tbody></table></div>';
|
||||||
|
if(D.loc){h+='<div class="lab">Por academia</div><div class="tw"><table><thead><tr><th>Academia</th>'+s.mt.map(function(m){return '<th class="n">'+esc(m.k)+'</th>'}).join("")+'</tr></thead><tbody>'+Object.keys(D.loc).map(function(n){var S3=sums(D.loc[n],a,b);return '<tr><td>'+esc(n)+'</td>'+s.mt.map(function(m){return '<td class="n">'+fmt(val(m,S3),m.f)+'</td>'}).join("")+'</tr>'}).join("")+'</tbody></table></div>'}
|
||||||
|
if(KC&&KC[s.id]){var ag={};KC[s.id].rows.forEach(function(x){if(x[0]<i0||x[0]>i1)return;var o=ag[x[1]]||(ag[x[1]]=[0,0,0]);o[0]+=x[2];o[1]+=x[3];o[2]+=x[4]});
|
||||||
|
var rows=Object.keys(ag).map(function(k){return [KC[s.id].camps[k]].concat(ag[k])}).sort(function(x,z){return z[1]-x[1]});
|
||||||
|
if(rows.length)h+='<div class="lab">Campañas del periodo</div><div class="tw"><table><thead><tr><th>Campaña</th><th class="n">Inversión</th><th class="n">'+esc(s.mt[2].k)+'</th><th class="n">'+esc(s.res)+'</th><th class="n">Coste por resultado</th></tr></thead><tbody>'+rows.map(function(x){return '<tr><td class="wrapc">'+esc(x[0])+'</td><td class="n">'+fmt(x[1],"eur")+'</td><td class="n">'+num(x[2])+'</td><td class="n">'+num(x[3])+'</td><td class="n">'+(x[3]?fmt(x[1]/x[3],"eur"):"—")+'</td></tr>'}).join("")+'</tbody></table></div>'}
|
||||||
|
if(s.nota)h+='<p class="note">'+esc(s.nota)+'</p>';
|
||||||
|
h+='</section>';
|
||||||
|
});
|
||||||
|
var act=document.activeElement,keep=act&&act.dataset&&act.dataset.kf?act.id:null;
|
||||||
|
\$("wk").className="view";\$("wk").innerHTML=h;if(keep&&\$(keep))\$(keep).focus();
|
||||||
|
}
|
||||||
|
document.addEventListener("change",function(ev){var k=ev.target.dataset&&ev.target.dataset.kf;if(k){kf[k]=ev.target.value;renderK()}});
|
||||||
|
|
||||||
|
/* ---------- navegación ---------- */
|
||||||
|
function show(v){view=VIEWS.indexOf(v)>=0?v:"resumen";VIEWS.forEach(function(x){\$("v-"+x).hidden=x!==view});
|
||||||
|
var t=document.querySelectorAll(".tab");for(var i=0;i<t.length;i++)t[i].setAttribute("aria-pressed",String(t[i].dataset.view===view))}
|
||||||
|
window.addEventListener("hashchange",function(){show(location.hash.slice(1))});
|
||||||
|
document.addEventListener("keydown",function(ev){if((ev.key==="Enter"||ev.key===" ")&&ev.target.dataset&&ev.target.dataset.goto){ev.preventDefault();ev.target.click()}});
|
||||||
|
document.addEventListener("click",function(ev){
|
||||||
|
var g=ev.target.closest("[data-goto]");if(g){pView="lista";fF="";fE="";open=g.dataset.goto;renderPiezas();var el=\$("it-"+open);if(el)el.scrollIntoView({block:"start"});return}
|
||||||
|
var b=ev.target.closest("button");if(!b)return;var d=b.dataset;
|
||||||
|
if(d.view){show(d.view);try{location.hash=d.view}catch(e){}window.scrollTo(0,0)}
|
||||||
|
else if(d.g==="f"){fF=d.k;renderPiezas()} else if(d.g==="e"){fE=d.k;renderPiezas()} else if(d.g==="pv"){pView=d.k;renderPiezas()} else if(d.g==="mes"){pMes=d.k;open=null;renderPiezas()}
|
||||||
|
else if(d.g==="kp"){kf.p=d.k;if(d.k==="c"&&K){var rr=range();kf.a=kf.a||rr[0];kf.b=kf.b||rr[1]}renderK()} else if(d.g==="kc"){kf.c=d.k;renderK()}
|
||||||
|
else if(d.g==="kv"&&kData){kView=d.k;renderKpi(kData)} else if(d.g==="ord"&&kData){kOrd=d.k;renderKpi(kData)}
|
||||||
|
else if(d.g==="sug"){\$("q").value=b.textContent;\$("q").focus()}
|
||||||
|
else if(d.open){open=open===d.open?null:d.open;renderList()}
|
||||||
|
else if(d.estado){upd(d.id,{estado:d.estado})}
|
||||||
|
else if(d.save){upd(d.save,{copy:\$("copy-"+d.save).value})}
|
||||||
|
else if(d.copy){var ta=\$("copy-"+d.copy);navigator.clipboard.writeText(ta.value).then(function(){b.textContent="Copiado"},function(){ta.select()})}
|
||||||
|
else if(d.cm){var i=\$("cm-"+d.cm),v=i.value.trim();if(!v)return;var p=find(d.cm);upd(d.cm,{comentarios:(p.comentarios||[]).concat([{t:new Date().toLocaleDateString("es-ES"),texto:v}])})}
|
||||||
|
});
|
||||||
|
show(location.hash.slice(1));renderQuota();renderFilters();
|
||||||
|
|
||||||
|
/* ---------- exportar ---------- */
|
||||||
|
function csvOf(rows){return ""+rows.map(function(r){return r.map(function(c){c=c==null?"":String(c);return /[";\\n]/.test(c)?'"'+c.replace(/"/g,'""')+'"':c}).join(";")}).join("\\n")}
|
||||||
|
function csvNum(v){return v==null?"":String(v).replace(".",",")}
|
||||||
|
function exportRows(){
|
||||||
|
if(view==="creatividades")return ["piezas-"+pMes+".csv",[["Fecha","Formato","Pilar","Título","Estado","Copy","Idea visual"]].concat(pm().map(function(p){return [p.fecha,lab(FORMATOS,p.formato),p.pilar,p.titulo,lab(ESTADOS,p.estado),p.copy,p.visual]}))];
|
||||||
|
if(view==="publicado"&&kData)return ["instagram-publicado.csv",[["Fecha","Formato","Pieza","Enlace","Alcance","Visualizaciones","Me gusta","Comentarios","Guardados","Compartidos"]].concat(kData.posts.map(function(p){return [p.f,TIPOS[p.t],p.n,"https://www.instagram.com/"+p.u+"/",p.r,p.v,p.l,p.c,p.g,p.s]}))];
|
||||||
|
if(!K)return null;var r=range(),ms=months().filter(function(x){return x.a<=r[1]&&x.b>=r[0]}),rows=[["Canal","Indicador"].concat(ms.map(function(x){return x.lab})).concat(["Total "+fd(r[0])+" - "+fd(r[1])])];
|
||||||
|
SRC.forEach(function(s){var D=K.src[s.id];s.mt.forEach(function(m){rows.push([s.t,m.k].concat(ms.map(function(x){var v=val(m,sums(D.m,x.a<r[0]?r[0]:x.a,x.b>r[1]?r[1]:x.b));return csvNum(v==null?null:Math.round(v*100)/100)})).concat([csvNum((function(v){return v==null?null:Math.round(v*100)/100})(val(m,sums(D.m,r[0],r[1]))))]))})});return ["kpis-por-canal.csv",rows]}
|
||||||
|
window.claude.use("downloads").then(function(dl){if(!dl)return;var b=\$("csv");b.hidden=false;
|
||||||
|
b.addEventListener("click",function(){var x=exportRows();if(!x)return;dl.save({filename:"blue-parrot-"+x[0],data:csvOf(x[1])}).catch(function(e){if(e&&(e.code==="unavailable"||e.code==="not_granted"))b.hidden=true})})});
|
||||||
|
|
||||||
|
/* ---------- preguntar ---------- */
|
||||||
|
window.claude.use("sample").then(function(sample){if(!sample)return;\$("tab-ask").hidden=false;
|
||||||
|
\$("sugs").innerHTML=["¿Qué canal trae los leads más baratos?","¿Qué tipo de contenido debería publicar más en Instagram?","Resume septiembre en cinco líneas para el cliente","¿Qué tres acciones priorizarías en octubre?"].map(function(s){return '<button class="chip" type="button" data-g="sug">'+esc(s)+'</button>'}).join("");
|
||||||
|
var ctl=null;\$("stop").addEventListener("click",function(){if(ctl)ctl.abort()});
|
||||||
|
\$("ask").addEventListener("click",function(){var q=\$("q").value.trim();if(!q)return;var out=\$("ans"),ask=\$("ask"),stop=\$("stop");
|
||||||
|
var ms12=K?months().slice(-13):[],ctx={kpis_mensuales:K&&{meses:ms12.map(function(x){return x.lab+(x.part?" (parcial)":"")}),canales:SRC.map(function(s){return {canal:s.t,nota:s.nota,indicadores:s.mt.map(function(m){return {k:m.k,unidad:m.f||"num",v:ms12.map(function(x){var v=val(m,sums(K.src[s.id].m,x.a,x.b));return v==null?null:Math.round(v*100)/100})}})}})},
|
||||||
|
instagram:kData&&{seguidores:kData.seguidores,piezas:kData.posts.map(function(p){return {fecha:p.f,formato:TIPOS[p.t],titulo:p.n,alcance:p.r,visualizaciones:p.v,megusta:p.l,comentarios:p.c,guardados:p.g,compartidos:p.s}})},
|
||||||
|
piezas_octubre:piezas.map(function(p){return {fecha:p.fecha,formato:p.formato,titulo:p.titulo,estado:p.estado}}),paquete:"16 publicaciones, 20 stories y 8 reels al mes"};
|
||||||
|
var prompt="Eres analista de marketing de la agencia 4Ventures y respondes sobre su cliente Blue Parrot School (escuela de idiomas en Madrid, Málaga y online). Responde en español, en texto plano sin Markdown, breve y con cifras concretas de los datos. Si el dato no está, dilo. No inventes.\\n\\nDATOS (JSON):\\n"+JSON.stringify(ctx)+"\\n\\nPREGUNTA: "+q;
|
||||||
|
ctl=new AbortController();out.hidden=false;out.textContent="Pensando…";ask.disabled=true;stop.hidden=false;
|
||||||
|
sample(prompt,{signal:ctl.signal,onText:function(u){out.textContent=u.text}}).then(function(r){out.textContent=r.text+(r.truncated?"\\n\\n(Respuesta cortada: pide menos de una vez.)":"")},
|
||||||
|
function(e){var c=e&&e.code;if(c==="cancelled"){out.textContent=(e.text||"")||"Pregunta cancelada.";return}
|
||||||
|
if(c==="not_granted"||c==="sampling_disabled"||c==="not_declared"||c==="capability_disabled"){out.textContent="Claude no está disponible en esta vista.";return}
|
||||||
|
out.textContent=(e&&e.text?e.text+"\\n\\n":"")+(c==="rate_limited"?"Has alcanzado el límite de uso. Prueba más tarde.":c==="refused"?"Claude no ha podido responder a esa pregunta. Prueba a formularla de otra forma.":"No se pudo completar la respuesta. Vuelve a intentarlo.")})
|
||||||
|
.then(function(){ask.disabled=false;stop.hidden=true});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ---------- datos ---------- */
|
||||||
|
window.claude.use("db").then(function(x){
|
||||||
|
db=x;
|
||||||
|
if(!db){var m='<div class="empty"><b>No se pueden cargar los datos en esta vista.</b><br>Abre el panel con tu sesión de Claude iniciada.</div>';\$("list").innerHTML=m;\$("r-body").innerHTML=m;\$("r-body").className="";\$("kpi").innerHTML=m;\$("kpi").className="";\$("wk").innerHTML=m;\$("wk").className="";return}
|
||||||
|
db.doc("kpi/instagram").onSnapshot(function(s){ if(s.exists) renderKpi(s.data()); else \$("kpi").innerHTML="<b>Sin datos de Instagram todavía.</b><br>Pide a Claude que actualice las métricas desde Windsor." },function(){});
|
||||||
|
db.doc("kpi/windsor").onSnapshot(function(s){ if(s.exists) renderWk(s.data()) },function(){});
|
||||||
|
db.doc("kpi/diario").onSnapshot(function(s){ if(s.exists){K=s.data();renderK();renderResumen()} else { \$("wk").innerHTML="<b>Sin KPIs todavía.</b><br>Pide a Claude que los actualice desde Windsor.";\$("r-body").innerHTML=\$("wk").innerHTML } },function(){});
|
||||||
|
db.doc("kpi/campanas").onSnapshot(function(s){ if(s.exists){KC=s.data();renderK()} },function(){});
|
||||||
|
db.collection("piezas").orderBy("fecha").onSnapshot(function(s){
|
||||||
|
var a=document.activeElement, keep=a&&a.id&&/^(copy|cm)-/.test(a.id)?{id:a.id,v:a.value}:null;
|
||||||
|
piezas=s.docs.map(function(d){var o=Object.assign({},d.data());o.id=d.id;return o});piezasOk=true;
|
||||||
|
renderPiezas();renderResumen();
|
||||||
|
if(keep&&\$(keep.id)){\$(keep.id).value=keep.v;\$(keep.id).focus()}
|
||||||
|
},function(){\$("list").innerHTML='<div class="empty"><b>Se ha perdido la conexión con los datos.</b><br>Recarga la página.</div>'});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body></html>`;
|
||||||
Reference in New Issue
Block a user