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.
Files changed (2) hide show
  1. package/index.html +32 -13
  2. 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.1';
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
- let taxLiab=0; for(let i=0;i<=idx;i++){ const mo=MONTHS[i]; const fc=factByCatOf('Общий',mo); taxLiab+=Math.abs(fc['Налоги']||0)-expenseCatSum(mo,'Налоги'); }
1385
- if(Math.abs(taxLiab)>1) add('warn','Налоги: есть задолженность/переплата',`Накопленная разница начислено−уплачено по налогам — ${fmt(taxLiab)} ₽. Детали на вкладке «Налоги».`,'taxes');
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
- openModal('В облаке другая версия',
3076
- `<p class="hint" style="margin:0 0 12px">Кто-то (скорее всего вы на другом компьютере или в другой вкладке) сохранил данные
3077
- после того, как эта вкладка загрузилась. Чтобы ничего не потерять, автосохранение приостановлено.</p>
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
  /* вернулись на вкладку — проверяем облако сразу, не дожидаясь минутной паузы */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buhanka-finance-app",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Buhanka bakery finance dashboard — single-file web app",
5
5
  "license": "MIT",
6
6
  "files": [