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.
- package/index.html +28 -14
- 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.
|
|
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
|
-
/*
|
|
698
|
-
|
|
699
|
-
|
|
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
|
|
702
|
-
|
|
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="
|
|
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"
|
|
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,'&').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
|
-
|
|
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();
|
|
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();
|
|
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;
|