{
e.preventDefault();
const b = $('b-entrar'), err = $('err-entrada');
b.disabled = true; b.textContent = 'Entrant…'; err.hidden = true;
try {
if (INCRUSTAT) { await carrega(); return; }
const r = await fetch('/api/entrar', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ usuari: $('usuari').value, clau: $('clau').value }),
});
const j = await r.json();
if (!j.ok) throw new Error(j.motiu || 'No s\'ha pogut entrar');
$('clau').value = '';
await carrega();
} catch (ex) {
err.textContent = ex.message; err.hidden = false;
} finally {
b.disabled = false; b.textContent = 'Entrar';
}
};
$('b-surt').onclick = async () => {
if (!INCRUSTAT) await fetch('/api/sortir', { method: 'POST' });
D = null; mostra('p-entrada'); $('usuari').focus();
};
$('b-enrere').onclick = () => { pintaHub(); mostra('p-hub'); };
// ---------- carrega ----------
async function carrega() {
if (INCRUSTAT) {
D = INCRUSTAT;
$('qui').textContent = D.usuari ? '· ' + esc(D.usuari) : '';
pintaHub(); mostra('p-hub');
return;
}
const r = await fetch('/api/estat');
if (r.status === 401) { mostra('p-entrada'); $('usuari').focus(); return; }
// Qualsevol altre error del servidor es DIU. Abans queien tots al catch de
// l'arrencada i la pantalla es quedava muda: sense xarxa l'app s'obria i no
// explicava per que no hi havia numeros, i un desplegament mal configurat
// (500) tampoc. "Sense connexio" i "l'app esta trencada" s'assemblen molt
// massa a la pantalla per deixar-ho a l'endevinalla.
if (!r.ok) {
const j = await r.json().catch(() => ({}));
mostra('p-entrada');
$('err-entrada').textContent = j.motiu || j.error || ('El servidor ha respost ' + r.status + '.');
$('err-entrada').hidden = false;
return;
}
D = await r.json();
if (D.error) { mostra('p-entrada'); $('err-entrada').textContent = D.error; $('err-entrada').hidden = false; return; }
$('qui').textContent = D.usuari ? '· ' + esc(D.usuari) : '';
pintaHub(); mostra('p-hub');
}
// ---------- hub ----------
const MODULS = [
{ id: 'dash', nom: 'Dash', que: 'Tot en un cop d\'ull', xifra: (d) => {
const n = d.guaita.vencutsTotal, f = d.alba.finestres.length;
return { gran: f + (f === 1 ? ' finestra' : ' finestres'), petit: n + ' documents vençuts' }; },
crida: (d) => d.alba.finestres.length > 0 },
{ id: 'cumul', nom: 'Cúmul', que: 'Inversió del capital personal', xifra: (d) => d.cumul.hiHa
? { gran: eur(d.cumul.comptes[0].aportat, 2), petit: 'aportat · broker simulat' }
: { gran: 'Sense dades', petit: 'corre npm run ronda' } },
{ id: 'guaita', nom: 'Guaita', que: 'Control de les societats', xifra: (d) => ({
gran: d.guaita.circulant ? eur(d.guaita.circulant.estalviTotal) : eur(null),
petit: 'identificat en cost de finançament' }),
crida: (d) => Boolean(d.guaita.circulant && d.guaita.circulant.estalviTotal > 0) },
{ id: 'alba', nom: 'Alba', que: 'Marques noves al mercat', xifra: (d) => ({
gran: d.alba.finestres.length + ' obertes',
petit: d.alba.finestres.map((g) => g.nom).join(' · ') || 'cap finestra ara' }),
crida: (d) => d.alba.finestres.length > 0 },
];
function pintaHub() {
$('rellotge').textContent = D.avui;
$('tessel-les').innerHTML = MODULS.map((m) => {
const x = m.xifra(D);
return '';
}).join('');
for (const b of $('tessel-les').querySelectorAll('[data-mod]')) {
b.onclick = () => obre(b.dataset.mod);
}
}
function obre(id) {
const m = MODULS.find((x) => x.id === id);
$('v-titol').textContent = m.nom;
$('v-subtitol').textContent = m.que + ' · ' + D.avui;
$('v-cos').innerHTML = VISTES[id](D);
mostra('p-vista');
}
// ---------- vistes ----------
function taula(caps, files, peu) {
return '
' + caps.map((c) => '
' + c + '
').join('')
+ '
' + files.map((f) => '
' + f.map((c) => '
' + c + '
').join('') + '
').join('')
+ '' + (peu ? '
' + peu + '
' : '') + '
';
}
const VISTES = {
dash: (d) => {
const c = d.guaita.circulant;
// LA TIRA DE DALT: cada xifra amb QUE VOL DIR al costat.
//
// Abans hi havia quatre numeros i prou. Un numero sol es llegeix com un
// vol: el propietari va llegir els 105,66 EUR d\'efectiu sobrant del bot
// com si fossin el guany. Aixi que aqui cada tessel\u00b7la porta el seu peu
// dient de que va, i el bot —que no hi era— hi surt primer.
const estr = d.cumul.comptes.find((x) => !x.vara && !x.error) || null;
const vara = d.cumul.comptes.find((x) => x.vara && !x.error) || null;
const r = d.cumul.ultimaRonda;
const cua = d.alba.cua;
// LA TIRA LIDERA AMB ON SON ELS DINERS, i abans no.
//
// Lideraba amb el bot: 1.000 EUR de paper. Al costat, el grup te 19,6 M EUR
// parats a l'estoc i paga 380.826 EUR d'interessos l'any. Un tauler que posa
// primer el compte de proves i amaga els milions es un tauler sobre una
// joguina, i es exactament el que el propietari va dir amb "aquest dash es
// una merda... sabe on esta invertit tot, possibles guanys, imports".
let h = '
'
+ tiraItem('Invertit en estoc', c ? gran(c.estocTotal) : '—', 'diners parats al pati')
+ tiraItem('Deute financer', c ? gran(c.deuteTotal) : '—',
c && c.estocTotal ? 'l\'estoc n\'es el ' + pc(c.estocTotal / c.deuteTotal, 0) : 'del grup')
+ tiraItem('Interessos', c ? gran(c.despesesFinanceres) : '—',
c ? pc(c.pesEnBenefici, 0) + ' del benefici net' : '')
+ tiraItem('Es pot alliberar', c ? gran(c.capitalAlliberat) : '—',
c ? 'baixant l\'estoc a ' + c.diesObjectiu + ' dies' : '')
+ tiraItem('Estalvi si es fa', c ? gran(c.estalviTotal) : '—', 'cada any, en interessos')
+ tiraItem('Documents vençuts', String(d.guaita.vencutsTotal), 'reclamar-los és feina teva')
+ '
';
// ═══ ON ESTA INVERTIT ═══
// Els IMPORTS per societat, que es el que faltava. I el que es veu de seguida:
// l'estalvi NO esta repartit, esta concentrat en qui va carregat.
if (d.guaita.societats && d.guaita.societats.length > 0) {
const amb = d.guaita.societats.filter((x) => x.estoc !== null);
if (amb.length > 0) {
h += '
On està invertitguaita · ' + esc(c ? c.exercici : '') + '
'
+ taula(['Societat', 'Estoc', 'Dies', 'Cost per dia', 'Deute financer', 'Tipus', 'Es pot alliberar'],
amb.map((x) => [
esc(x.nom),
eur(x.estoc, 0),
dec(x.diesEstoc, 0),
x.costPerDiaEstoc ? eur(x.costPerDiaEstoc, 0) + '/dia' : '—',
eur(x.deuteFinancer, 0),
pc(x.tipusImplicit, 1),
x.capitalAlliberat > 0
? '' + eur(x.capitalAlliberat, 0) + '(' + dec(x.diesEstalviats, 0) + ' dies)'
: 'ja per sota de ' + (c ? c.diesObjectiu : 90) + '',
]),
'Estoc i deute a tancament. Cost per dia = quant costa en interessos tenir aquell estoc un dia més.')
+ '
El capital alliberable no està repartit: es concentra a les societats '
+ 'que van per damunt dels ' + (c ? c.diesObjectiu : 90) + ' dies. Les que ja hi són per sota no tenen res a treure '
+ 'per aquesta via.
';
}
}
// EL BOT. No hi era, al tauler, i es el modul que mou diners.
h += '
El bot d\'inversiócúmul · paper
';
if (!estr && !vara) {
h += '
Encara no hi ha cap compte de paper amb ronda apuntada.
' + linia(estr, 'L\'estratègia') + linia(vara, 'Comprar i no tocar-ho') + '
';
// LA COMPARACIO ES L\'UNICA LECTURA QUE VOL DIR ALGUNA COSA. Un compte que
// porta un +8% no diu res sense saber que hauria fet no fer res.
if (estr && vara && estr.guany !== null && vara.guany !== null) {
const dif = estr.patrimoni - vara.patrimoni;
h += '
La comparació és el que importa. '
+ (Math.abs(dif) < 0.005 ? 'Ara mateix van igualades.'
: 'L\'estratègia va ' + (dif > 0 ? 'per davant' : 'per darrere') + ' de comprar i no tocar-ho per '
+ eur(Math.abs(dif), 2) + '.')
+ ' Un compte que puja no diu res sense saber què hauria fet no fer res.
';
}
h += '
L\'efectiu que veus a la taula de Cúmul no és el guany: és el que va '
+ 'quedar sense col\u00b7locar perquè no arriba per una acció sencera més.
';
}
// EL SILENCI NO ES BONA SENYAL. Aixo va estar cinc dies sense dir-se.
if (r && r.dies >= 2) {
h += '
Fa ' + r.dies + ' dies que el bot no gira. '
+ 'Si havia de girar cada matí, mira estat/cumul-ultima-tasca.txt: '
+ 'hi ha l\'hora d\'arrencada i el motiu.
';
}
h += '';
// EL RADAR. El resum d\'on enfocar-se, que es com ell ho va demanar.
if (cua && cua.caps.length > 0) {
h += '
El radaralba · noms, no fets
'
+ taula(['Nom', 'Punts', 'Fonts', 'Titular on ha sortit'],
cua.caps.map((x) => [esc(x.nom), String(x.punts), String(x.fonts),
'' + esc(x.titol || '—') + '']),
'Els ' + cua.caps.length + ' amb més senyal de ' + cua.total + ' noms a la cua.')
+ '
Això són noms, no fets: vol dir que el nom ha sortit en aquell '
+ 'titular, no que la marca entri a Europa. Al PC: npm run alba -- accepta <nom> '
+ 'o descarta <nom>.
';
}
if (c) {
h += '
El cost del deuteguaita · ' + esc(c.exercici) + '
'
+ '
' + pc(c.pesEnBenefici, 0) + ''
+ '
del benefici net del grup se\'n va en interessos: ' + eur(c.despesesFinanceres)
+ ' sobre ' + eur(c.resultat) + ' de resultat.
'
+ '
'
+ '
Dies d\'estoc'
+ eur(c.estalviRotacio) + '/any
baixant l\'estoc a ' + c.diesObjectiu + ' dies
'
+ '
Asimetria de tipus'
+ eur(c.estalviAsimetria) + '/any
alineant els tipus dins del grup
'
+ '
'
+ '
No és una promesa. El tipus implícit és despeses ÷ deute a '
+ 'tancament. Cal el quadre de deute amb els tipus contractuals per confirmar-ho.
';
}
h += '
Finestresalba
' + finestres(d) + '';
h += '
Societatsguaita
' + taulaSocietats(d) + '';
return h;
},
cumul: (d) => {
if (!d.cumul.hiHa) {
// Dos motius diferents per a la mateixa tessel·la buida, i cal dir el bo:
// al desplegament el bot no hi corre, i dir-hi "corre npm run ronda" fa
// creure que falta alguna cosa. (Compte: aixo es DINS del literal de la
// pagina — els apostrofs de les cadenes del client van amb DUES barres.)
if (d.cumul.desplegament) {
return '
El bot d\'inversió corre al teu ordinador; aquí només se\'n pot veure una fotografia. '
+ 'Per pujar-la: npm run preparar-desplegament al PC i torna a importar les variables a Vercel.
';
}
return '
Sense fotografia del bròquer. Corre npm run ronda i apareixerà aquí.
'
+ '
L\'adaptador d\'Alpaca està escrit i no verificat. '
+ 'Comprova\'l amb npm run provar-portal abans de res.
';
}
// EL RESULTAT VA PRIMER, i no es un caprici de disseny. Abans la taula
// ensenyava efectiu i posicions i prou, i el propietari va llegir els 105,66
// EUR d'efectiu sobrant com si fossin el guany: va preguntar si "amb 1000
// EUR ja hem guanyat 105" quan aquell compte perdia 11,57. Una pantalla que
// amaga el numero que la gent busca no evita l'error, el provoca: llegiran
// el que hi hagi mes a prop. Cap color per al signe — a la casa l'estat no
// es codifica amb color — nomes el signe i el percentatge al costat.
const resultat = (c) => c.guany === null ? '—'
: (c.guany > 0 ? '+' : '') + eur(c.guany, 2) + ' (' + (c.guanyPct > 0 ? '+' : '') + pc(c.guanyPct, 2) + ')';
const files = d.cumul.comptes.map((c) => c.error
? [esc(c.broker), '' + esc(c.error) + '', '', '', '', '', '']
: [esc(c.broker) + (c.vara ? ' · vara de mesurar' : ''),
eur(c.patrimoni, 2), resultat(c), eur(c.efectiu, 2),
Object.entries(c.posicions).map(([s, u]) => u + ' ' + esc(s)).join(', ') || '—',
eur(c.aportat, 2), String(c.operacions)]);
const quan = d.cumul.comptes.map((c) => c.patrimoniData).filter(Boolean)[0];
const p = d.cumul.pla;
const sans = d.cumul.comptes.filter((x) => !x.error);
const costosTotal = sans.reduce((t, x) => t + (x.costos || 0), 0);
let extra = '';
// ═══ ON ESTA INVERTIT ═══
// Quant hi ha col·locat i quant parat, per compte. Ho va demanar aixi:
// "sabe on esta invertit tot". El valor de les posicions no es calcula amb
// preus d'avui: es el patrimoni que va apuntar la ronda menys l'efectiu.
const ambPes = sans.filter((x) => x.valorPosicions !== null);
if (ambPes.length > 0) {
extra += '
On està invertita ' + esc(quan || '') + '
'
+ taula(['Compte', 'Col·locat', 'Pes', 'Parat en efectiu', 'En què'],
ambPes.map((x) => [
esc(x.broker) + (x.vara ? ' · vara' : ''),
eur(x.valorPosicions, 2), pc(x.pesInvertit, 0), eur(x.efectiu, 2),
Object.entries(x.posicions).map(([sb, u]) => u + ' ' + esc(sb)).join(', ')
|| 'res: tot en efectiu',
]),
'El col·locat és el patrimoni de la darrera ronda menys l\'efectiu: la mateixa xifra i la mateixa data, sense inventar cap preu.')
+ '';
}
// ═══ EL PLA: LA REGLA QUE PRODUEIX AQUELLS NUMEROS ═══
if (p) {
extra += '
El placonfig/cumul.json
'
+ '
'
+ '
Què compra' + esc(p.simbols.join(', ')) + ''
+ '
' + (p.estrategia === 'tendencia'
? 'filtre de tendència: dins si el preu és per damunt de la mitjana de ' + p.finestra + ' dies, fora si no'
: 'comprar i mantenir') + '
'
+ '
Quan decideix'
+ (p.estrategia === 'tendencia' ? '1 cop al mes' : 'sempre dins') + ''
+ '
' + (p.estrategia === 'tendencia' ? 'l\'últim dia hàbil. La resta de dies no toca res.' : 'no ven mai') + '
'
+ '
Diners nous'
+ eur(p.aportacio, 0) + '/mes
' + (p.aportacio > 0 ? 'entren el primer dia hàbil' : 'cap aportació configurada') + '
'
+ '
';
// LA MAQUINA APAGADA. La tesi del projecte es "l'aportacio constant mes
// l'interes compost", i amb aportacio 0 la meitat de la tesi no corre.
if (!p.aportacio) {
extra += '
La màquina està a mitges. La tesi de Cúmul és '
+ 'aportació constant + interès compost, i ara mateix no hi entra cap diner nou: '
+ 'només corre la part de l\'interès. Es canvia amb aportacio a '
+ 'config/cumul.json.
';
}
extra += '';
}
// ═══ LA MAQUINA: ARITMETICA, NO PREVISIO ═══
//
// Ell va demanar "possibles guanys". Es el lloc on el software financer
// menteix mes, aixi que va amb tres regles: la taxa es un SUPOSIT declarat i
// no una previsio meva, al costat hi va SEMPRE quant hi has posat tu (que es
// el que separa el compost de la fantasia), i el que el backtest ja sap
// —que el filtre perd contra comprar i no tocar-ho— hi surt al costat.
if (p) {
const compost = (ini, mes, taxa, anys) => {
let v = ini;
for (let m = 0; m < anys * 12; m++) v = v * (1 + taxa / 12) + mes;
return v;
};
const ini = p.efectiuInicial || 0;
const files2 = [];
for (const anys of [10, 20]) {
const posat = ini + p.aportacio * 12 * anys;
files2.push([
'' + anys + ' anys',
eur(posat, 0),
eur(compost(ini, p.aportacio, 0.04, anys), 0),
eur(compost(ini, p.aportacio, 0.08, anys), 0),
eur(compost(ini, p.aportacio, 0.12, anys), 0),
]);
}
extra += '
Què pot arribar a seraritmètica, no previsió
'
+ taula(['Termini', 'Hi has posat tu', 'al 4%/any', 'al 8%/any', 'al 12%/any'], files2,
'Interès compost mensual sobre ' + eur(ini, 0) + ' i ' + eur(p.aportacio, 0) + '/mes. '
+ 'La columna hi has posat tu és la que separa el compost de la fantasia.')
+ '
Això no és una previsió i les taxes no són meves. Són tres supòsits '
+ 'per veure la forma de la corba: ningú no sap quin serà el rendiment. I el backtest d\'aquesta casa '
+ 'diu una cosa incòmoda que va al costat: sobre deu anys reals, el filtre de tendència perd '
+ 'contra comprar i no tocar-ho a les tres sèries provades. El que sí que fa és partir la caiguda '
+ 'màxima per dos. Si el que compres és dormir tranquil i no rendiment, aquest és el motiu de veritat.
'
+ '';
}
// ═══ ELS COSTOS PAGATS ═══
if (costosTotal > 0 || sans.some((x) => x.historial.length > 0)) {
const ops = [];
for (const x of sans) {
for (const o of x.historial) {
ops.push([esc(o.data || '—'), esc(x.broker), esc(o.sentit || '') + ' ' + dec(o.unitats, 2) + ' ' + esc(o.simbol || ''),
eur(o.preu, 4), eur(o.import, 2), eur(o.comissio, 2)]);
}
}
ops.sort((a, b) => String(b[0]).localeCompare(String(a[0])));
extra += '
Què ha costatcomissions pagades
'
+ '
' + eur(costosTotal, 2) + ''
+ '
en comissions, sobre ' + eur(sans.reduce((t, x) => t + (x.aportat || 0), 0), 0) + ' aportats. '
+ 'Amb un mínim d\'ordre fix, com menys rota una estratègia menys pesa això.
'
+ (ops.length > 0
? taula(['Data', 'Compte', 'Operació', 'Preu', 'Import', 'Comissió'], ops.slice(0, 10),
'Les ' + Math.min(10, ops.length) + ' últimes operacions dels comptes de paper.')
: '')
+ '';
}
return taula(['Compte', 'Patrimoni', 'Resultat', 'Efectiu', 'Posicions', 'Aportat', 'Ops'], files,
(quan ? 'Patrimoni i resultat a data ' + esc(quan) + ', amb els preus que va fer servir la ronda d\'aquell dia. '
: 'Sense cap ronda apuntada al diari no hi ha patrimoni: no se\'n inventa cap amb preus vells. ')
+ 'L\'efectiu NO és el guany: és el que va quedar sense col·locar. Amb accions senceres sempre en sobra.')
+ '
Encara és el bròquer simulat. '
+ 'Les xifres surten del fitxer d\'estat que Cúmul escriu: aquesta pantalla el llegeix i no pot moure res.
'
// Al desplegament les xifres son una FOTOGRAFIA pujada pel propietari, i
// el dia que es va fer es part de la dada: sense ell, una cartera vella
// es faria passar per la d avui.
+ (d.cumul.desplegament
? '
Fotografia del bot al teu PC' + (d.cumul.generat ? ', pujada el ' + esc(d.cumul.generat) + '' : '')
+ '. Es refresca tornant a importar les variables (npm run preparar-desplegament).
'
: '')
+ extra;
},
guaita: (d) => {
// LES LIQUIDACIONS VAN PRIMER, i la taula de societats despres.
//
// Es l'ordre que va demanar el propietari: "modul de control de les
// societats... tambe hi ha unes liquidacions entre empreses que tindrem de
// vigilar i contrastar o be demanar que son". La taula de ratios es una foto
// anual de fitxes que encara son propostes; les liquidacions son diner que es
// mou cada mes i que ningu esta contrastant. El que es mou va abans.
let h = '
Liquidacions entre societats'
+ 'vigilar · contrastar · preguntar
'
+ liquidacions(d) + '';
h += '
Els números de cada societat
'
+ taulaSocietats(d) + '';
if (d.guaita.vencuts.length) {
h += '
Les trucades d\'aquesta setmana'
+ '' + d.guaita.vencutsTotal + ' vençuts en total
' + d.alba.novetats14dies + ' novetats en 14 dies · '
+ d.alba.pendentsDeValidar + ' evidències pendents de validar'
+ (d.alba.senseGrup.length ? ' · ' + d.alba.senseGrup.length + ' marques sense grup conegut, que és la primera pregunta a fer' : '')
+ '
';
}
// LES LIQUIDACIONS ENTRE SOCIETATS.
//
// El panell que el propietari va demanar amb "unes liquidacions entre empreses
// que tindrem de vigilar i contrastar o be demanar que son". Les tres coses, en
// aquest ordre: quant hi ha viu, que no s\'ha pogut donar per bo, i que cal
// preguntar.
//
// Les xifres surten de documents/liquidacions/, que NO es al repositori i per
// tant NO existeix al desplegament. Aixo vol dir que al mobil aquest panell surt
// buit — i el text de quan surt buit ho ha de dir aixi, perque no sembli que
// falta res quan el que passa es que la confidencialitat funciona.
function liquidacions(d) {
const L = d.guaita.liquidacions;
if (!L || !L.hiHa) {
return '
Les liquidacions entre societats es llegeixen de '
+ 'documents/liquidacions/, a la carpeta del projecte al teu ordinador. '
+ 'No es pugen enlloc, ni aquí: són els saldos reals entre les societats.'
+ (L && L.motiu ? ' Ara mateix: ' + esc(L.motiu) + '.' : '')
+ '
'
+ '
Deixa-hi els fulls que ja es fan cada mes —«Liquidació @DDMMAAAA.xlsx» i '
+ '«saldos totes empreses a DD.MM.AAAA.xlsx»— i tornaran a sortir aquí. '
+ 'Al terminal: npm run guaita -- liquidacions.
';
}
const v = L.viu;
let h = '
'
+ tiraItem('Saldo viu sense liquidar', v ? gran(v.importSenseLiquidar) : '—',
v ? v.quantesSenseLiquidar + ' parelles que el full del mes no toca' : '')
+ tiraItem('Finançament entre elles', v ? gran(v.financerViu) : '—', 'préstecs i crèdits, no es liquiden cada mes')
+ tiraItem('Per preguntar', String(L.preguntesTotal), 'coses que ZENIT no pot saber sol')
+ tiraItem('Mesos llegits', String(L.mesos.length), L.mesos.length ? L.mesos[0] + ' a ' + L.mesos[L.mesos.length - 1] : '')
+ '
';
if (v && v.darreraLiquidacio) {
h += '
Segons el mateix full: ' + esc(v.darreraLiquidacio.text) + '
';
}
// ═══ QUE HI HA VIU ═══
// La foto de totes les parelles a la vegada, amb la columna que importa: si
// el full d\'aquell mes la toca o la deixa alla.
if (v && v.parelles.length) {
h += '
Què hi ha viu'
+ 'foto a ' + esc(v.data) + '
'
+ taula(['Parella', 'Operacions', 'Finançament', 'Saldo total', 'Al full del mes'],
v.parelles.map((p) => [
esc(p.text),
eur(p.operatiu, 2),
p.financer ? eur(p.financer, 2) : '—',
eur(p.total, 2),
p.liquidat === null ? 'no hi surt'
: p.senseLiquidar ? ' no es toca'
: eur(p.liquidat, 2),
]),
'Positiu = a cobrar, negatiu = a pagar, des de la primera societat de cada parella (ho declara el full). '
+ 'Les operacions es liquiden cada mes; el finançament no, i per això no surt com a pendent.')
+ '';
}
// ═══ EL QUE NO S\'HA POGUT DONAR PER BO ═══
if (L.perMirar.length) {
h += '
El que no s\'ha pogut donar per bo'
+ '' + L.perMirar.length + (L.perMirar.length === 1 ? ' bloc' : ' blocs') + '
'
+ taula(['Mes', 'Parella', '', 'Un costat diu', 'L\'altre diu', 'Es va transferir'],
L.perMirar.map((p) => [
esc(p.data),
esc(p.etiqueta) + (p.anotacio ? ' anotat: ' + esc(p.anotacio) + '' : ''),
p.estat === 'no-quadra'
? ' no quadra'
: p.estat === 'nomes-un-costat'
? ' només un costat'
: ' a mirar',
esc(p.nomEsquerra) + ': ' + (p.netEsquerra === null ? 'no consta' : eur(p.netEsquerra, 2)),
esc(p.nomDreta) + ': ' + (p.netDreta === null ? 'no consta' : eur(p.netDreta, 2)),
eur(p.liquidat, 2)
+ (p.segons === 'esquerra' ? ' la xifra de ' + esc(p.nomEsquerra) + ''
: p.segons === 'dreta' ? ' la xifra de ' + esc(p.nomDreta) + '' : ''),
]),
'No quadra vol dir que s\'ha mirat i els dos costats no diuen el mateix. '
+ 'Només un costat vol dir que no s\'ha pogut mirar, que és una altra cosa: '
+ 'l\'import es va treure d\'un sol joc de llibres.')
+ '';
}
// ═══ PER PREGUNTAR ═══
// Es el resultat del panell, no la lletra petita. Cada pregunta va amb el
// motiu: un avis sense el per que es una molestia i s\'acaba ignorant.
if (L.preguntes.length) {
h += '
Per preguntar'
+ '' + L.preguntesTotal + ' en total
'
+ L.preguntes.map((q) => '
' + esc(q.pregunta) + ''
+ '' + esc(q.perQue) + '
').join('')
+ '
';
if (L.preguntesTotal > L.preguntes.length) {
h += '
N\'hi ha ' + (L.preguntesTotal - L.preguntes.length) + ' més. Totes, amb el motiu de cada una: '
+ 'npm run guaita -- liquidacions.
';
}
h += '
Cap d\'aquestes preguntes no és una acusació. Un desquadrament el pot fer '
+ 'una factura de l\'últim dia del mes que l\'una ja té comptabilitzada i l\'altra encara no. '
+ 'El problema no seria que passi: seria no saber quina és.
';
}
// ═══ MES A MES ═══
if (L.perMes.length) {
h += '
Mes a mes
'
+ taula(['Mes', 'Parelles amb saldo', 'Liquidat', 'S\'ha pogut contrastar', 'Per mirar'],
L.perMes.map((m) => [
esc(m.data),
String(m.vives),
eur(m.liquidat, 2),
m.contrastades + ' de ' + m.vives,
m.perMirar ? '' + m.perMirar + '' : '—',
]),
L.forats.length
? 'Falten els fulls de ' + L.forats.map((x) => esc(x)).join(', ') + '. '
+ 'Un forat al mig vol dir que els saldos d\'aquell mes van quedar dins del següent.'
: 'Sense forats: hi ha el full de tots els mesos de la sèrie.')
+ '';
}
if (L.errors) {
h += '
' + L.errors + (L.errors === 1 ? ' full no s\'ha pogut llegir' : ' fulls no s\'han pogut llegir')
+ '. El motiu surt a npm run guaita -- liquidacions.
';
}
return h;
}
function taulaSocietats(d) {
const glif = (r) => r.vencuts > 30 ? 'mira' : r.vencuts > 0 ? 'mig' : 'ok';
const f = d.guaita.societats
? d.guaita.societats.map((s) => [
esc(s.nom),
s.vendes ? eur(s.vendes) + (s.vendesAnt ? '' + eur(s.vendesAnt) + '' : '') : '—',
s.resultat ? eur(s.resultat) + (s.resultatAnt ? '' + eur(s.resultatAnt) + '' : '') : '—',
s.margeNet !== null && s.margeNet !== undefined ? pc(s.margeNet) : '—',
s.autonomia !== null && s.autonomia !== undefined ? pc(s.autonomia, 0) : '—',
s.diesEstoc ? String(Math.round(s.diesEstoc)) : '—',
s.liquiditat !== null && s.liquiditat !== undefined ? dec(s.liquiditat) : '—',
'',
])
: d.guaita.resum.map((r) => [esc(r.nom), '—', '—', '—', '—', '—', '—', '']);
// Una taula sencera de guions NO explica res, i era el que sortia quan les
// fitxes no es llegien: semblava que l'app estigues trencada. Si no hi ha
// fitxes es diu, i es diu QUE falta i on va.
const peu = d.guaita.societats
? 'Xifra petita = exercici anterior. Les fitxes estan en estat PROPOSTA fins que les validis amb '
+ 'npm run guaita -- valida-fitxa. ● mirar-s\'ho · ◐ atenció · ○ tranquil'
: 'No hi ha cap fitxa comptable, i per aixo totes les xifres surten buides. '
+ 'Falta el fitxer estat/guaita-fitxes.json a la carpeta del projecte. '
+ 'Si creus que hi es, mira que no es digui .json.txt: el Windows amaga '
+ 'les extensions conegudes i el fitxer sembla be quan no ho es. '
+ 'Per veure-ho tot d\'un cop: npm run comprovar.';
return taula(['Societat', 'Vendes', 'Resultat', 'Marge net', 'Autonomia', 'Dies estoc', 'Liquiditat', ''], f, peu);
}
// ---------- arrencada ----------
carrega().catch(() => { mostra('p-entrada'); $('usuari').focus(); });
setInterval(() => { if (D && $('p-entrada').hidden) carrega().catch(() => {}); }, 300_000);
// El treballador de servei: es el que fa que l'app obri sense cobertura i que
// "Afegir a la pantalla d'inici" doni una app de debo. Nomes sobre HTTPS o a
// localhost — el navegador no el registra enlloc mes, i esta be que sigui aixi.
// Si falla no es diu res a la pantalla: l'app funciona igual, nomes sense
// arrencada sense xarxa, i un avis aqui no li serviria de res a qui mira.
if ('serviceWorker' in navigator) {
addEventListener('load', () => navigator.serviceWorker.register('/sw.js').catch(() => {}));
}