buhanka-finance-app 1.78.0 → 1.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.html +32 -13
- package/package.json +1 -1
package/index.html
CHANGED
|
@@ -309,7 +309,7 @@ tr.subtotal>td:first-child{color:var(--accent);box-shadow:inset 3px 0 0 var(--ac
|
|
|
309
309
|
|
|
310
310
|
<script>
|
|
311
311
|
/* ============================================================ БУХАНКА · Финансы v2 ============================================================ */
|
|
312
|
-
const APP_VERSION='11.
|
|
312
|
+
const APP_VERSION='11.3';
|
|
313
313
|
const SCHEMA=9; /* версия структуры данных; растёт при доработках */
|
|
314
314
|
const STORE_KEY='buhanka_finance_v9';/* НЕ МЕНЯТЬ НИКОГДА — иначе данные «слетят» при обновлении файла */
|
|
315
315
|
const LEGACY_KEYS=['buhanka_finance_v8','buhanka_finance_v7','buhanka_finance_v6','buhanka_finance_v5'];
|
|
@@ -1381,8 +1381,14 @@ function runChecks(P){
|
|
|
1381
1381
|
dblTr.push(`${o.date} ${o.account} → ${o.account2} на ${fmt(Math.abs(o.amount))} (и ${hit.date})`); } });
|
|
1382
1382
|
if(dblTr.length) add('warn','Возможные дубли переводов',`Один и тот же перевод, похоже, заведён дважды — обычно так бывает, когда загружены выписки обоих счетов: ${dblTr.join('; ')}. Лишнюю операцию удалите в «Операциях».`,'ops');
|
|
1383
1383
|
// 8. Налоги: большая задолженность
|
|
1384
|
-
|
|
1385
|
-
|
|
1384
|
+
/* историю не считаем: за исторические месяцы платежи в приложение не заводились,
|
|
1385
|
+
иначе «долг» копит всё начисленное с начала года */
|
|
1386
|
+
let taxLiab=0, taxFrom=-1;
|
|
1387
|
+
for(let i=0;i<=idx;i++){ const mo=MONTHS[i]; if(isHist(mo))continue; if(taxFrom<0)taxFrom=i;
|
|
1388
|
+
const fc=factByCatOf('Общий',mo); taxLiab+=Math.abs(fc['Налоги']||0)-expenseCatSum(mo,'Налоги'); }
|
|
1389
|
+
if(Math.abs(taxLiab)>1) add('warn','Налоги: есть задолженность/переплата',
|
|
1390
|
+
`Начислено минус уплачено с ${taxFrom<0?'начала года':MSHORT[taxFrom]} — ${fmt(taxLiab)} ₽. Месяцы, помеченные историческими, не считаем: платежи по ним в приложении не заводились. Детали на вкладке «Налоги».`,'taxes');
|
|
1391
|
+
else if(taxFrom>=0) add('ok','Налоги: начислено = уплачено',`С ${MSHORT[taxFrom]} расхождений нет.`,'taxes');
|
|
1386
1392
|
return out;
|
|
1387
1393
|
}
|
|
1388
1394
|
function checksSummary(P){ const r=runChecks(P); return {err:r.filter(x=>x.level==='err').length, warn:r.filter(x=>x.level==='warn').length, ok:r.filter(x=>x.level==='ok').length, list:r}; }
|
|
@@ -3013,6 +3019,7 @@ function setCloud(status,savedAt,err){
|
|
|
3013
3019
|
async function cloudGet(){ const r=await fetch('/api/state',{cache:'no-store'}); return r.json(); }
|
|
3014
3020
|
/* после восстановления копии пишем поверх облака осознанно: иначе первое же сохранение
|
|
3015
3021
|
упрётся в «расхождение версий», хотя расхождение создали мы сами */
|
|
3022
|
+
function cloudRemember(c){ try{ if(c&&c.ok&&c.state) CLOUD.lastVol=stVol(c.state); }catch(e){} }
|
|
3016
3023
|
async function cloudRebase(){ if(!CLOUD.on){ ST._base=Number(ST._ts||0); return; }
|
|
3017
3024
|
try{ const c=await cloudGet(); ST._base=Number((c&&c.ts)||0); }catch(e){}
|
|
3018
3025
|
CLOUD.conflict=false; CONFLICT=null; }
|
|
@@ -3049,32 +3056,42 @@ async function cloudSync(){
|
|
|
3049
3056
|
/* пишем только поверх той версии, на которой основаны наши правки */
|
|
3050
3057
|
let cur=null; try{ cur=await cloudGet(); }catch(e){}
|
|
3051
3058
|
if(cur&&cur.ok&&cur.state){
|
|
3059
|
+
cloudRemember(cur);
|
|
3052
3060
|
const cloudTs=Number(cur.ts||0), base=Number(ST._base||0);
|
|
3053
3061
|
if(cloudTs!==base){
|
|
3054
3062
|
CLOUD.busy=false; CLOUD.pending=false;
|
|
3055
3063
|
cloudConflict(cur);
|
|
3056
3064
|
return;
|
|
3057
3065
|
}
|
|
3066
|
+
/* даже когда версии совпали, не пишем поверх заметно более полного облака:
|
|
3067
|
+
так терялись данные после восстановления старой локальной копии */
|
|
3068
|
+
if(!CLOUD.allowPoorer && volPoorer(stVol(cur.state),stVol(ST))){
|
|
3069
|
+
CLOUD.busy=false; CLOUD.pending=false;
|
|
3070
|
+
cloudConflict(cur,'poorer');
|
|
3071
|
+
return;
|
|
3072
|
+
}
|
|
3058
3073
|
}
|
|
3059
3074
|
const d=await cloudPut();
|
|
3060
|
-
if(d&&d.ok){ ST._base=Number(ST._ts||0); saveLocal(); setCloud('synced',d.savedAt); }
|
|
3075
|
+
if(d&&d.ok){ ST._base=Number(ST._ts||0); CLOUD.allowPoorer=false; CLOUD.lastVol=stVol(ST); saveLocal(); setCloud('synced',d.savedAt); }
|
|
3061
3076
|
else setCloud('error',null,(d&&d.reason)||'отказ сервера');
|
|
3062
3077
|
}catch(e){ setCloud('error',null,'нет связи'); }
|
|
3063
3078
|
CLOUD.busy=false;
|
|
3064
3079
|
if(CLOUD.pending){ CLOUD.pending=false; cloudSchedule(); }
|
|
3065
3080
|
}
|
|
3066
3081
|
/* в облаке версия, которой нет у нас: показываем обе и даём выбрать */
|
|
3067
|
-
let CONFLICT=null;
|
|
3068
|
-
function cloudConflict(cur){
|
|
3069
|
-
CLOUD.conflict=true; CONFLICT=cur; setCloud('conflict');
|
|
3082
|
+
let CONFLICT=null, CONFLICT_REASON='';
|
|
3083
|
+
function cloudConflict(cur,reason){
|
|
3084
|
+
CLOUD.conflict=true; CONFLICT=cur; CONFLICT_REASON=reason||''; setCloud('conflict');
|
|
3070
3085
|
const mine=stVol(ST), theirs=stVol(cur.state);
|
|
3071
3086
|
const when=d=>{ try{ const x=new Date(d); const p=n=>String(n).padStart(2,'0');
|
|
3072
3087
|
return p(x.getDate())+'.'+p(x.getMonth()+1)+' '+p(x.getHours())+':'+p(x.getMinutes()); }catch(e){ return '—'; } };
|
|
3073
3088
|
const cmp=(k,a,b)=>`<tr><td>${k}</td><td class="num">${fmt(a)}</td>
|
|
3074
3089
|
<td class="num ${b<a?'neg':b>a?'pos':''}">${fmt(b)}</td></tr>`;
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3090
|
+
const poorer=reason==='poorer';
|
|
3091
|
+
openModal(poorer?'Похоже, здесь старая копия':'В облаке другая версия',
|
|
3092
|
+
`<p class="hint" style="margin:0 0 12px">${poorer
|
|
3093
|
+
? 'Эта вкладка собирается записать в облако заметно меньше данных, чем там уже есть — скорее всего здесь открыта старая или только что восстановленная копия. Автосохранение приостановлено.'
|
|
3094
|
+
: 'Кто-то (скорее всего вы на другом компьютере или в другой вкладке) сохранил данные после того, как эта вкладка загрузилась. Чтобы ничего не потерять, автосохранение приостановлено.'}</p>
|
|
3078
3095
|
<div class="tbl-wrap"><table class="gt"><thead><tr><th>Что</th>
|
|
3079
3096
|
<th class="num">Здесь (${when(ST._ts)})</th><th class="num">В облаке (${when(cur.ts)})</th></tr></thead><tbody>
|
|
3080
3097
|
${cmp('Точки',mine.points,theirs.points)}
|
|
@@ -3092,7 +3109,7 @@ function cloudConflict(cur){
|
|
|
3092
3109
|
()=>conflictTake());
|
|
3093
3110
|
const mf=document.querySelector('#modalRoot .mf'); if(mf)mf.remove(); /* решение только двумя явными кнопками */
|
|
3094
3111
|
}
|
|
3095
|
-
function cloudTagClick(){ if(CLOUD.conflict&&CONFLICT){ cloudConflict(CONFLICT); return; } cloudSync(); }
|
|
3112
|
+
function cloudTagClick(){ if(CLOUD.conflict&&CONFLICT){ cloudConflict(CONFLICT,CONFLICT_REASON); return; } cloudSync(); }
|
|
3096
3113
|
function conflictTake(){
|
|
3097
3114
|
const cur=CONFLICT; closeModal(); if(!cur)return;
|
|
3098
3115
|
cloudAdopt(cur,'Перед переходом на облачную версию');
|
|
@@ -3105,16 +3122,18 @@ async function conflictKeep(){
|
|
|
3105
3122
|
if(volPoorer(theirs,mine) && !confirm(
|
|
3106
3123
|
'Ваша версия заметно беднее облачной:\n'+
|
|
3107
3124
|
'операции '+mine.ops+' против '+theirs.ops+', ячейки '+mine.cells+' против '+theirs.cells+
|
|
3108
|
-
'.\n\nВсё равно перезаписать облако?')){ cloudConflict(cur); return; }
|
|
3125
|
+
'.\n\nВсё равно перезаписать облако?')){ cloudConflict(cur,CONFLICT_REASON); return; }
|
|
3109
3126
|
bkCreate('Облачная версия перед перезаписью',true,true);
|
|
3110
3127
|
try{ localStorage.setItem('buhanka_cloud_overwritten',JSON.stringify({at:Date.now(),state:cur.state})); }catch(e){}
|
|
3111
|
-
CLOUD.conflict=false; CONFLICT=null;
|
|
3128
|
+
CLOUD.conflict=false; CONFLICT=null; CONFLICT_REASON='';
|
|
3129
|
+
CLOUD.allowPoorer=true; /* явное решение пользователя — барьер по объёму снимаем один раз */
|
|
3112
3130
|
ST._base=Number(cur.ts||0); /* пишем поверх осознанно */
|
|
3113
3131
|
save();
|
|
3114
3132
|
toast('Облако перезаписано вашей версией');
|
|
3115
3133
|
}
|
|
3116
3134
|
function cloudFlush(){
|
|
3117
3135
|
if(!CLOUD.on||CLOUD.conflict||CLOUD.status==='synced') return; /* при расхождении вслепую не пишем */
|
|
3136
|
+
if(!CLOUD.allowPoorer&&CLOUD.lastVol&&volPoorer(CLOUD.lastVol,stVol(ST))) return; /* и не обедняем облако */
|
|
3118
3137
|
try{ navigator.sendBeacon('/api/state', new Blob([JSON.stringify({state:ST,ts:Number(ST._ts||Date.now())})],{type:'application/json'})); }catch(e){}
|
|
3119
3138
|
}
|
|
3120
3139
|
/* вернулись на вкладку — проверяем облако сразу, не дожидаясь минутной паузы */
|