buhanka-finance-app 1.69.0 → 1.71.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 +28 -14
  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='10.2';
312
+ const APP_VERSION='10.4';
313
313
  const SCHEMA=8; /* версия структуры данных; растёт при доработках */
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'];
@@ -694,13 +694,13 @@ function empAdjSum(month,empId){ return empAdjBase(month,empId)+mgrBonusOf(empId
694
694
  function empAccrued(month,empId){ let s=0; ST.points.forEach(p=>{ s+=empDaysAt(month,empId,p); }); return s+empAdjSum(month,empId); }
695
695
  function empPaid(month,empId){ return ST.operations.filter(o=>o.month===month&&o.employeeId===empId&&o.type==='Расход').reduce((s,o)=>s+Math.abs(o.amount),0); }
696
696
  const HALF1_LAST=15;
697
- /* к какой половине месяца относится выплата: явная метка o.half, иначе по смыслу —
698
- «аванс» всегда за первую половину; 1–5 числа платят расчёт за вторую половину,
699
- 6–15 — это уже аванс за текущую первую, с 16-го — тоже аванс */
697
+ /* за какую половину платим: выплата должна уменьшать долг того периода, за который сделана.
698
+ 1–5 числа — расчёт за вторую половину прошлого периода; 6–16 — аванс и расчёт за первую;
699
+ с 17-го — уже аванс под вторую. Явная метка o.half всегда сильнее. */
700
+ const HALF2_FROM=17;
700
701
  function opPayHalf(o){ if(o.half===1||o.half===2)return o.half;
701
- const t=((o.category||'')+' '+(o.note||o.comment||'')).toLowerCase();
702
- if(t.indexOf('аванс')>=0)return 1;
703
- return Number(String(o.date||'').slice(8,10))<=5?2:1; }
702
+ const d=Number(String(o.date||'').slice(8,10));
703
+ return (d<=5||d>=HALF2_FROM)?2:1; }
704
704
  function empPaidHalf(month,empId,half){ return (ST.operations||[])
705
705
  .filter(o=>o.month===month&&o.employeeId===empId&&o.type==='Расход'&&opPayHalf(o)===half)
706
706
  .reduce((s,o)=>s+Math.abs(o.amount),0); }
@@ -2347,7 +2347,7 @@ function renderSched(C){
2347
2347
  /* ---- сверка по половинам: где уже рассчитались, а где ещё нет ---- */
2348
2348
  const dcell=(v)=>`<td class="num ${Math.abs(v)<1?'pos':(v>0?'neg':'')}" style="font-weight:600">${Math.abs(v)<1?'ровно':fmt(v)}</td>`;
2349
2349
  html+=`<div class="panel"><div class="hd"><h3>Сверка по половинам · ${m}</h3><div class="spacer"></div>
2350
- <span class="tag" title="Выплаты 1–5 числа считаем расчётом за вторую половину прошлого периода, остальные — за первую. Половину можно поправить в самой операции">разнесение выплат — авто</span></div>
2350
+ <span class="tag" title="1–5 числа — расчёт за вторую половину, 6–16 — за первую, с 17-го — аванс под вторую. Половину можно поправить в самой операции">разнесение выплат — авто</span></div>
2351
2351
  <div class="bd" style="padding:0"><div class="tbl-wrap"><table class="gt" style="font-size:12.5px;min-width:900px">
2352
2352
  <colgroup><col style="width:180px"><col style="width:100px"><col style="width:100px"><col style="width:100px">
2353
2353
  <col style="width:100px"><col style="width:100px"><col style="width:100px"><col style="width:110px"></colgroup>
@@ -2367,7 +2367,8 @@ function renderSched(C){
2367
2367
  <td class="num" style="${bl}">${fmt(a1t)}</td><td class="num">${fmt(p1t)}</td>${dcell(a1t-p1t)}
2368
2368
  <td class="num" style="${bl}">${fmt(a2t)}</td><td class="num">${fmt(p2t)}</td>${dcell(a2t-p2t)}
2369
2369
  <td class="num" style="${bl}">${fmt((a1t+a2t)-(p1t+p2t))}</td></tr></tbody></table></div>
2370
- <div class="bd"><p class="hint" style="margin:0">Половину можно поправить в самой операции, если авто разнесло не туда.</p></div>
2370
+ <div class="bd"><p class="hint" style="margin:0">Δ — сколько ещё должны за эту половину. Минус значит выдали вперёд.
2371
+ Если выплата отнесена не к тому периоду, поправьте «За половину» в самой операции.</p></div>
2371
2372
  </div></div>`;
2372
2373
 
2373
2374
  ST.points.forEach(p=>{ const pemps=emps.filter(e=>(e.points||[]).includes(p));
@@ -2865,13 +2866,15 @@ function esc(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,
2865
2866
  let LAST_EDIT=0, CLOUD_T=null, CLOUD_POLL=0;
2866
2867
  const CLOUD={ on:false, status:'init', savedAt:null, err:'', busy:false, pending:false, conflict:false };
2867
2868
  /* объём состояния — чтобы сравнивать версии и не принимать молча заметно более бедную */
2868
- function stVol(st){ st=st||{}; let cells=0;
2869
+ function stVol(st){ st=st||{}; let cells=0, shifts=0;
2869
2870
  Object.values(st.data||{}).forEach(m=>Object.values(m||{}).forEach(r=>Object.values(r||{}).forEach(c=>{
2870
2871
  if(c&&(Number(c.fact)||Number(c.budget)))cells++; })));
2871
- return {points:(st.points||[]).length, ops:(st.operations||[]).length, cells,
2872
+ Object.values(st.timesheet||{}).forEach(mo=>Object.values(mo||{}).forEach(pt=>Object.values(pt||{}).forEach(e=>{
2873
+ Object.values(e||{}).forEach(v=>{ if(Number(v))shifts++; }); })));
2874
+ return {points:(st.points||[]).length, ops:(st.operations||[]).length, cells, shifts,
2872
2875
  emp:(st.employees||[]).length, loans:(st.loans||[]).length}; }
2873
2876
  function volPoorer(a,b){ /* b заметно беднее a */
2874
- return (b.ops<a.ops*0.7)||(b.cells<a.cells*0.7)||(b.points<a.points)||(b.emp<a.emp*0.7); }
2877
+ return (b.ops<a.ops*0.7)||(b.cells<a.cells*0.7)||(b.points<a.points)||(b.emp<a.emp*0.7)||(b.shifts<a.shifts*0.7); }
2875
2878
  /* принять облачную версию как свою */
2876
2879
  function cloudAdopt(d,label){
2877
2880
  if((ST.operations||[]).length||bkCells(ST)) bkCreate(label,true,true);
@@ -2879,6 +2882,8 @@ function cloudAdopt(d,label){
2879
2882
  ST._ts=Number(d.ts||0); ST._base=Number(d.ts||0);
2880
2883
  saveLocal(); render(); }
2881
2884
  function cloudPossible(){ return location.protocol==='http:'||location.protocol==='https:'; }
2885
+ function it0(path){ const m=String(path||'').match(/(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})/);
2886
+ return m?(m[1]+'T'+m[2]+':'+m[3]+':00Z'):new Date().toISOString(); }
2882
2887
  function hhmm(iso){ try{ const d=new Date(iso), p=x=>String(x).padStart(2,'0'); return p(d.getHours())+':'+p(d.getMinutes()); }catch(e){ return ''; } }
2883
2888
  function setCloud(status,savedAt,err){
2884
2889
  CLOUD.status=status; if(savedAt!==undefined&&savedAt!==null)CLOUD.savedAt=savedAt; CLOUD.err=err||'';
@@ -2896,6 +2901,11 @@ function setCloud(status,savedAt,err){
2896
2901
  el.textContent=m[0]; el.className='cloudtag '+(m[1]||''); el.title=m[2];
2897
2902
  }
2898
2903
  async function cloudGet(){ const r=await fetch('/api/state',{cache:'no-store'}); return r.json(); }
2904
+ /* после восстановления копии пишем поверх облака осознанно: иначе первое же сохранение
2905
+ упрётся в «расхождение версий», хотя расхождение создали мы сами */
2906
+ async function cloudRebase(){ if(!CLOUD.on){ ST._base=Number(ST._ts||0); return; }
2907
+ try{ const c=await cloudGet(); ST._base=Number((c&&c.ts)||0); }catch(e){}
2908
+ CLOUD.conflict=false; CONFLICT=null; }
2899
2909
  async function cloudPut(){
2900
2910
  const r=await fetch('/api/state',{method:'PUT',headers:{'Content-Type':'application/json'},
2901
2911
  body:JSON.stringify({state:ST,ts:Number(ST._ts||Date.now())})});
@@ -2960,6 +2970,7 @@ function cloudConflict(cur){
2960
2970
  ${cmp('Точки',mine.points,theirs.points)}
2961
2971
  ${cmp('Операции',mine.ops,theirs.ops)}
2962
2972
  ${cmp('Заполненные ячейки',mine.cells,theirs.cells)}
2973
+ ${cmp('Смены в табеле',mine.shifts,theirs.shifts)}
2963
2974
  ${cmp('Сотрудники',mine.emp,theirs.emp)}
2964
2975
  ${cmp('Кредиты',mine.loans,theirs.loans)}
2965
2976
  </tbody></table></div>
@@ -3054,7 +3065,9 @@ async function cloudHistRestore(path){
3054
3065
  let d; try{ d=await (await fetch('/api/history?path='+encodeURIComponent(path),{cache:'no-store'})).json(); }catch(e){ toast('Нет связи'); return; }
3055
3066
  if(!d.ok||!d.state){ toast('Снимок недоступен'); return; }
3056
3067
  bkCreate('Перед восстановлением облачного снимка',true,true);
3057
- ST=d.state; migrate(); schemaGuard(); save(); render(); toast('Снимок восстановлен');
3068
+ ST=d.state; migrate(); schemaGuard();
3069
+ await cloudRebase(); save(); render();
3070
+ toast('Восстановлен снимок от '+hhmm(d.savedAt||it0(path)));
3058
3071
  }
3059
3072
 
3060
3073
  /* ============================================================ РЕЗЕРВНЫЕ КОПИИ ============================================================ */
@@ -3096,7 +3109,8 @@ function bkRestore(id){
3096
3109
  if(!confirm('Восстановить состояние от '+b.when+' («'+b.label+'»)?\n\nТекущие данные сначала сохранятся отдельной копией.'))return;
3097
3110
  bkCreate('Перед восстановлением от '+b.when,true,true);
3098
3111
  let next; try{ next=JSON.parse(b.payload); }catch(e){ alert('Копия повреждена'); return; }
3099
- ST=next; migrate(); save(); render(); toast('Восстановлено: '+b.when);
3112
+ ST=next; migrate(); schemaGuard();
3113
+ cloudRebase().then(()=>{ save(); render(); toast('Восстановлено: '+b.when); });
3100
3114
  }
3101
3115
  function bkDownload(id){
3102
3116
  const b=bkRead().find(x=>x.id===id); if(!b)return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "buhanka-finance-app",
3
- "version": "1.69.0",
3
+ "version": "1.71.0",
4
4
  "description": "Buhanka bakery finance dashboard — single-file web app",
5
5
  "license": "MIT",
6
6
  "files": [