/* ============ 1. 基础工具 ============ */
function ge(id){return document.getElementById(id);}
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/'/g,''');}
function fmt(n){n=+n||0;var neg=n<-0.004;if(neg)n=-n;var s=n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g,',');return (neg?'-':'')+s;}
function fmtN(n){return (+n||0).toFixed(2);}
function pnum(v){v=parseFloat(String(v==null?'':v).replace(/[,,\s]/g,''));return isNaN(v)?0:v;}
function r2(x){return Math.round((+x+Number.EPSILON)*100)/100;}
function roundTo(x,step){if(!step||step<=0)return r2(x);return r2(Math.round(x/step)*step);}
function uid(){return Date.now().toString(36)+Math.random().toString(36).slice(2,7);}
function pad2(n){return String(n).padStart(2,'0');}
function monthEnd(m){var p=m.split('-');return m+'-'+pad2(new Date(+p[0],+p[1],0).getDate());}
function nextMonth(m,day){var p=m.split('-');var y=+p[0],mo=+p[1]+1;if(mo>12){mo=1;y++;}return y+'-'+pad2(mo)+'-'+pad2(day);}
function nextPeriod(m){var p=m.split('-');var y=+p[0],mo=+p[1]+1;if(mo>12){mo=1;y++;}return y+'-'+pad2(mo);}
function curMonth(){var d=new Date();return d.getFullYear()+'-'+pad2(d.getMonth()+1);}
function monthIdx(m){var p=m.split('-');return +p[0]*12+(+p[1]-1);}
function toast(msg,type){var w=ge('toastWrap');var d=document.createElement('div');d.className='toast '+(type==='ok'?'ok':type==='err'?'err':'');d.innerHTML=(type==='ok'?'✅ ':type==='err'?'❌ ':'💬 ')+esc(msg);w.appendChild(d);setTimeout(function(){d.style.transition='opacity .3s';d.style.opacity='0';setTimeout(function(){d.remove();},320);},3200);}
function openDlg(ico,title,bodyHtml,buttons,keep,wide){ge('dlgIco').textContent=ico;ge('dlgTitle').textContent=title;ge('dlgBody').innerHTML=bodyHtml;ge('dlgBox').style.width=wide?'1000px':'640px';var f=ge('dlgFoot');f.innerHTML='';(buttons||[{t:'关闭'}]).forEach(function(b){var btn=document.createElement('button');btn.className='btn '+(b.cls||'');btn.innerHTML=b.t;btn.onclick=function(){if(b.fn){if(b.fn()===false)return;}if(!b.keep)closeDlg();};f.appendChild(btn);});ge('dlgMask').classList.add('show');}
function closeDlg(){ge('dlgMask').classList.remove('show');}
function askConfirm(msg,fn){openDlg('⚠️','操作确认','
'+esc(msg)+'
',[{t:'取消'},{t:'确认执行',cls:'btn-d',fn:fn}]);}
/* ============ 2. Excel 引擎(纯离线 .xlsx 读写:写入带完整样式与界面一致,读取支持模板导入) ============ */
var MiniZip=(function(){
var CRCT=(function(){var t=[],c,n,k;for(n=0;n<256;n++){c=n;for(k=0;k<8;k++)c=c&1?0xEDB88320^(c>>>1):c>>>1;t[n]=c>>>0;}return t;})();
function crc32(u8){var c=0xFFFFFFFF;for(var i=0;i>>8);return (c^0xFFFFFFFF)>>>0;}
function strU8(s){return new TextEncoder().encode(s);}
function build(files){ // files:[{name,path,data:Uint8Array}]
var parts=[],offset=0,central=[];
function push(u8){parts.push(u8);offset+=u8.length;}
function hdr(n,len){var b=new Uint8Array(n);var dv=new DataView(b.buffer);return {b:b,dv:dv,n:len};}
function u32(v){var b=new Uint8Array(4);new DataView(b.buffer).setUint32(0,v,true);return b;}
function u16(v){var b=new Uint8Array(2);new DataView(b.buffer).setUint16(0,v,true);return b;}
files.forEach(function(f){
var name=strU8(f.path), crc=crc32(f.data), sz=f.data.length;
var loc=new Uint8Array(30+name.length), dv=new DataView(loc.buffer);
dv.setUint32(0,0x04034b50,true);dv.setUint16(4,20,true);dv.setUint16(6,0x0800,true);dv.setUint16(8,0,true);
dv.setUint16(10,0,true);dv.setUint16(12,0x5281,true);dv.setUint32(14,crc,true);
dv.setUint32(18,sz,true);dv.setUint32(22,sz,true);dv.setUint16(26,name.length,true);dv.setUint16(28,0,true);
loc.set(name,30);
var cd=new Uint8Array(46+name.length), cdv=new DataView(cd.buffer);
cdv.setUint32(0,0x02014b50,true);cdv.setUint16(4,20,true);cdv.setUint16(6,20,true);cdv.setUint16(8,0x0800,true);cdv.setUint16(10,0,true);
cdv.setUint16(12,0,true);cdv.setUint16(14,0x5281,true);cdv.setUint32(16,crc,true);
cdv.setUint32(20,sz,true);cdv.setUint32(24,sz,true);cdv.setUint16(28,name.length,true);
cdv.setUint32(42,offset,true);cd.set(name,46);
push(loc);push(f.data);central.push(cd);
});
var cdStart=offset,cdSize=0;
central.forEach(function(c){push(c);cdSize+=c.length;});
var eocd=new Uint8Array(22),ev=new DataView(eocd.buffer);
ev.setUint32(0,0x06054b50,true);ev.setUint16(8,files.length,true);ev.setUint16(10,files.length,true);
ev.setUint32(12,cdSize,true);ev.setUint32(16,cdStart,true);
parts.push(eocd);
var total=offset+22,out=new Uint8Array(total),pos=0; /* offset 已含中央目录,EOCD 必须是文件最后 22 字节,否则 Excel 判定损坏 */
parts.forEach(function(p){out.set(p,pos);pos+=p.length;});
return out;
}
return {build:build};
})();
function xmlEsc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F]/g,'');}
function colName(n){var s='';while(n>0){var m=(n-1)%26;s=String.fromCharCode(65+m)+s;n=Math.floor((n-1)/26);}return s;}
/* ---- 2.1 样式系统(表头/正文/金额/合计 与软件界面一致) ---- */
var XF={DEF:0,HEAD:1,TXT:2,MONEY:3,MTOTAL:4,TOTAL:5,TITLE:6,INT:7,CENTER:8,NOTE:9,AMT:10};
var STYLE_XML=''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
''+
'';
function cellXml(ref,v,st){
if(v==null||v==='')return '';
if(typeof v==='number'){if(!isFinite(v))v=0;return ''+v+'';}
return ''+xmlEsc(v)+'';
}
/* sheet:{name,title,note,header,types:'t文本|m金额|i整数|c居中',widths,rows,totalRow,freeze,rowStyles:'title|h|t|note'} */
function buildSheetXml(s){
var types=s.types||[],rows=s.rows||[],rowStyles=s.rowStyles||null;
var ncol=types.length;
if(s.header&&s.header.length>ncol)ncol=s.header.length;
if(s.widths&&s.widths.length>ncol)ncol=s.widths.length;
rows.forEach(function(r){if(r&&r.length>ncol)ncol=r.length;});
if(ncol<1)ncol=1;
var lead=[];
if(s.title)lead.push({cells:[s.title],st:XF.TITLE});
if(s.note)lead.push({cells:[s.note],st:XF.NOTE});
if(s.header&&s.header.length)lead.push({cells:s.header,st:XF.HEAD});
var freezeAt=lead.length,rowsXml=[],rn=0;
function emitRow(cells,stFn){
rn++;
var cs='';
for(var c=0;c'+cs+'');
}
lead.forEach(function(L){emitRow(L.cells,function(){return s.plain?XF.DEF:L.st;});});
rows.forEach(function(r,ri){
var isTotal=s.totalRow&&ri===rows.length-1,rs=rowStyles?rowStyles[ri]:null;
emitRow(r||[],function(c){
if(s.plain)return types[c]==='a'?XF.AMT:XF.DEF;
var ty=types[c]||'t';
if(rs==='title')return XF.TITLE;
if(rs==='h')return XF.HEAD;
if(rs==='note')return XF.NOTE;
if(rs==='t'||isTotal)return ty==='m'?XF.MTOTAL:XF.TOTAL;
return ty==='m'?XF.MONEY:ty==='i'?XF.INT:ty==='c'?XF.CENTER:XF.TXT;
});
});
var merges=[];
if(!s.plain){
if(s.title)merges.push('A1:'+colName(ncol)+'1');
if(s.note)merges.push('A2:'+colName(ncol)+'2');
}
var sv=''+
(freezeAt>0&&s.freeze!==false&&!s.plain?'':'')+
'';
var colsXml=(s.widths&&s.widths.length)?(''+s.widths.map(function(w,i){return '
';}).join('')+''):'';
return ''+
''+
''+sv+
''+colsXml+
''+rowsXml.join('')+''+
(merges.length?''+merges.map(function(m){return '';}).join('')+'':'')+
'';
}
function buildXlsx(sheets){
var enc=new TextEncoder(),files=[];
var ct=''+
''+
''+
''+
''+
''+
sheets.map(function(s,i){return '';}).join('')+'';
files.push({path:'[Content_Types].xml',data:enc.encode(ct)});
files.push({path:'_rels/.rels',data:enc.encode('')});
var wb=''+
''+
''+sheets.map(function(s,i){return '';}).join('')+'';
files.push({path:'xl/workbook.xml',data:enc.encode(wb)});
files.push({path:'xl/_rels/workbook.xml.rels',data:enc.encode(''+
sheets.map(function(s,i){return '';}).join('')+
'')});
files.push({path:'xl/styles.xml',data:enc.encode(STYLE_XML)});
sheets.forEach(function(s,i){files.push({path:'xl/worksheets/sheet'+(i+1)+'.xml',data:enc.encode(buildSheetXml(s))});});
return MiniZip.build(files);
}
/* ---- 2.2 ZIP 读取(EOCD→中央目录→逐条解压) ---- */
function u16(u8,p){return u8[p]|(u8[p+1]<<8);}
function u32(u8,p){return (u8[p]|(u8[p+1]<<8)|(u8[p+2]<<16)|(u8[p+3]<<24))>>>0;}
function zipEntries(u8){
var i=u8.length-22,eocd=-1,lim=Math.max(0,u8.length-22-65535);
for(;i>=lim;i--){if(u8[i]===0x50&&u8[i+1]===0x4b&&u8[i+2]===0x05&&u8[i+3]===0x06){eocd=i;break;}}
if(eocd<0)throw new Error('不是有效的 .xlsx 文件(ZIP 结构缺失)');
var n=u16(u8,eocd+10),cdOff=u32(u8,eocd+16),p=cdOff,out={};
for(var k=0;ku8.length)throw new Error('Excel 文件数据不完整');
out[name]={method:method,csize:csize,usize:usize,comp:u8.subarray(start,start+csize)};
}
return out;
}
/* 纯 JS 原生 DEFLATE 解压(DecompressionStream 不可用时兜底) */
function inflateRaw(src,usizeHint){
var bitpos=0,out=new Uint8Array(usizeHint>0?usizeHint:1<<16),olen=0;
function ensure(n){if(olen+n>out.length){var cap=out.length*2;while(cap>3;if(byteI>=src.length)throw new Error('压缩数据不完整');var b=(src[byteI]>>(bitpos&7))&1;bitpos++;return b;}
function bits(n){var v=0;for(var i=0;iolen||dist>32768)throw new Error('窗口距离非法');
ensure(len);var from=olen-dist;
for(var i=0;i=29)throw new Error('长度码非法');
var len=LBASE[s]+bits(LEXT[s]);
var d=decode(dh);if(d>=30)throw new Error('距离码非法');
copyWin(DBASE[d]+bits(DEXT[d]),len);
}
}
}
var fixedLit=null,fixedDist=null;
function fixedTables(){
if(fixedLit)return;
var L=new Array(288);for(var i=0;i<288;i++)L[i]=i<144?8:(i<256?9:(i<280?7:8));
fixedLit=construct(L,288);
fixedDist=construct(new Array(30).fill(5),30);
}
var isFinal=0;
do{
isFinal=bit();
var type=bits(2);
if(type===0){
bitpos=(bitpos+7)&~7;
var p0=bitpos>>3;
if(p0+4>src.length)throw new Error('存储块头不完整');
var LEN=u16(src,p0),NLEN=u16(src,p0+2);
if(((~LEN)&0xFFFF)!==NLEN)throw new Error('存储块校验失败');
p0+=4;
if(p0+LEN>src.length)throw new Error('存储块数据不完整');
ensure(LEN);for(var i2=0;i20)lens.push(pv);}
else if(cs===17){var z=3+bits(3);while(z-->0)lens.push(0);}
else{var z2=11+bits(7);while(z2-->0)lens.push(0);}
}
if(lens.length>hlit+hdist)throw new Error('码长超长');
var lh=construct(lens.slice(0,hlit),hlit);
var dh=construct(lens.slice(hlit),hdist);
if(!lh)throw new Error('文字/长度表为空');
inflBlock(lh,dh);
}else throw new Error('未知压缩块类型');
}while(!isFinal);
return out.subarray(0,olen);
}
function unzipEntry(e){
if(e.method===0)return Promise.resolve(e.comp.slice(0,e.usize||e.comp.length));
if(e.method!==8)return Promise.reject(new Error('不支持的压缩方式('+e.method+'),请用 Excel/WPS 另存为标准 .xlsx'));
if(typeof DecompressionStream!=='undefined'){
try{
var ds=new DecompressionStream('deflate-raw');
return new Response(new Blob([e.comp]).stream().pipeThrough(ds)).arrayBuffer()
.then(function(ab){return new Uint8Array(ab);})
.catch(function(){return inflateRaw(e.comp,e.usize);});
}catch(err){return Promise.resolve(inflateRaw(e.comp,e.usize));}
}
return Promise.resolve(inflateRaw(e.comp,e.usize));
}
/* ---- 2.3 工作表解析(sharedStrings / inlineStr / 日期序列号) ---- */
function xmlDecodeEnt(s){
return String(s).replace(/&(?:amp|lt|gt|quot|apos|#x?[0-9A-Fa-f]+);/g,function(m){
if(m==='&')return '&';if(m==='<')return '<';if(m==='>')return '>';if(m==='"')return '"';if(m===''')return "'";
var h=m.substring(2,m.length-1);
try{return String.fromCodePoint(h.charAt(0)==='x'||h.charAt(0)==='X'?parseInt(h.substring(1),16):parseInt(h,10));}catch(e){return '';}
});
}
function colToIdx(ref){
var m=/^([A-Za-z]+)/.exec(ref);if(!m)return 0;
var s=m[1].toUpperCase(),n=0;
for(var i=0;i]*)?>([\s\S]*?)<\/si>/g,m;
while((m=re.exec(xml))){
var s='',tre=/]*)?>([\s\S]*?)<\/t>/g,tm;
while((tm=tre.exec(m[1])))s+=tm[1];
out.push(xmlDecodeEnt(s));
}
return out;
}
function sheetToAOA(xml,shared){
var result=[],m;
var reRow=/]*?)(\/>|>([\s\S]*?)<\/row>)/g;
while((m=reRow.exec(xml))){
var attrs=m[1]||'',inner=m[3]||'';
var rm2=/r="(\d+)"/.exec(attrs);
var rnum=rm2?+rm2[1]:(result.length+1);
var row=[];
var reCell=/]*?)(\/>|>([\s\S]*?)<\/c>)/g,cm;
while((cm=reCell.exec(inner))){
var cAt=cm[1]||'',cIn=cm[3]||'';
var rcm=/r="([A-Za-z]+\d+)"/.exec(cAt);
var ci=rcm?colToIdx(rcm[1]):row.length;
var tm2=/t="([^"]*)"/.exec(cAt);
var ty=tm2?tm2[1]:'';
var v='';
if(ty==='inlineStr'){
var parts=cIn.match(/]*)?>([\s\S]*?)<\/t>/g)||[];
v=parts.map(function(p){return p.replace(/^]*)?>/,'').replace(/<\/t>$/,'');}).join('');
}else{
var vm=/]*)?>([\s\S]*?)<\/v>/.exec(cIn);
if(vm){
v=vm[1];
if(ty==='s')v=shared[+v]||'';
else if(ty==='b')v=(v==='1'?'TRUE':'FALSE');
}
}
v=xmlDecodeEnt(v).replace(/_x000D_/g,'');
if((ty===''||ty==='n'||ty==='str')&&typeof v==='string'&&/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(v))v=parseFloat(v);
while(row.length]*>/g)||[]).forEach(function(tag){
var id=/Id="([^"]+)"/.exec(tag),tg=/Target="([^"]+)"/.exec(tag);
if(id&&tg)rels[id[1]]=tg[1];
});
var out=[];
(wbXml.match(/]*>/g)||[]).forEach(function(tag){
var nm=/name="([^"]*)"/.exec(tag);
var id=/r:id="([^"]+)"/.exec(tag)||/id="([^"]+)"/.exec(tag);
if(nm&&id&&rels[id[1]]!=null){
var t=rels[id[1]].replace(/^\//,'');
if(t.indexOf('xl/')!==0)t='xl/'+t;
out.push({name:xmlDecodeEnt(nm[1]),path:t});
}
});
return out;
}
async function readXlsx(buf){
var u8=new Uint8Array(buf);
var entries=zipEntries(u8);
async function part(path){
var e=entries[path];
if(!e)return null;
var data=await unzipEntry(e);
return new TextDecoder('utf-8').decode(data);
}
var wb=await part('xl/workbook.xml');
if(!wb)throw new Error('缺少 workbook.xml,请确认是标准 .xlsx 文件');
var sharedXml=await part('xl/sharedStrings.xml');
var shared=sharedXml?parseShared(sharedXml):[];
var sheets=parseSheets(wb,(await part('xl/_rels/workbook.xml.rels'))||'');
var map={};
for(var i=0;i=0)return xl.sheet(n[j]);}
}
for(i=0;i1)return a;}
return xl.first();
}
function findHeader(aoa,aliases){
var maxScan=Math.min(aoa.length,8),best=-1,bestMap=null,bestScore=-1;
for(var ri=0;ri=0;});
if(hit&&map[a[0]]==null){map[a[0]]=c;score++;break;}
}
});
if(score>bestScore){bestScore=score;best=ri;bestMap=map;}
}
return {row:best,map:bestMap,score:bestScore};
}
function showImportDlg(o){
var errs=[],list=[];
for(var ri=(o.hd.row<0?0:o.hd.row+1);ri⛔
';
c.innerHTML=html;
}
function stat(ico,label,val,sub,grad){
var g=(grad||'#1d4ed8,#1e40af').split(',');
return '
'+ico+''+esc(label)+'
'+esc(val)+'
'+esc(sub)+'
';
}
function jumpPay(m){nav('pay');setTimeout(function(){var el=ge('payMonth');if(el){el.value=m;pgPayRender();}},30);}
/* ---- 9.2 部门管理 ---- */
function pgDept(c){
var html='
';
c.innerHTML=html;pgEmpRender();
}
function pgEmpRender(){
var list=DB.emps.filter(function(e){
if(empFilter.dept&&e.deptId!==empFilter.dept)return false;
var kw=empFilter.kw.trim();
if(kw&&!(String(e.name||'').includes(kw)||String(e.code||'').includes(kw)))return false;
return true;
});
var html='
工号
姓名
部门
入职日期
基本工资
岗位工资
绩效工资
补贴
固定月薪合计
月专项附加扣除
状态
操作
';
list.forEach(function(e){
var fixed=r2(pnum(e.base)+pnum(e.post)+pnum(e.merit)+pnum(e.allowance));
var ded=e.ded||{};var sp=r2(pnum(ded.child)+pnum(ded.infant)+pnum(ded.elder)+pnum(ded.mortgage)+pnum(ded.rent)+pnum(ded.edu)+pnum(ded.other));
html+='
'+esc(e.code)+'
'+esc(e.name)+'
'+esc(deptName(e.deptId))+'
'+esc(e.hireDate||'')+'
'+
'
'+fmt(e.base)+'
'+fmt(e.post)+'
'+fmt(e.merit)+'
'+fmt(e.allowance)+'
'+
'
'+fmt(fixed)+'
'+fmt(sp)+'
'+
'
'+(e.status==='离职'?'离职':'在职')+'
'+
'
';
});
if(!list.length)html+='
👥无匹配员工
';
html+='
';
ge('empTblWrap').innerHTML=html;
}
function fld(label,cls,id,val,ph,type){return '';}
function editEmp(id){
var e=id?DB.emps.find(function(x){return x.id===id;}):{code:'E'+String(DB.emps.length+1).padStart(3,'0'),name:'',deptId:(DB.depts[0]||{}).id||'',hireDate:'',status:'在职',idcard:'',base:0,post:0,merit:0,allowance:0,socialBase:null,fundBase:null,ded:{child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0},initCum:{months:0,income:0,insFund:0,special:0,tax:0}};
if(!e.ded)e.ded={child:0,infant:0,elder:0,mortgage:0,rent:0,edu:0,other:0};
if(!e.initCum)e.initCum={months:0,income:0,insFund:0,special:0,tax:0};
function moneyF(l,id,v,ph){return fld(l,'',id,v==null?0:v,ph,'num');}
var html='
';
c.innerHTML=html;
}
function numBox(label,id,val){return '';}
function addInsItem(){DB.settings.ins.items.push({key:'x'+Date.now().toString(36),name:'新险种',p:0,c:0,base:'social'});saveDB();pgIns(ge('content'));toast('已增加险种,请设置比例','ok');}
function delInsItem(i){askConfirm('确认删除该险种?',function(){DB.settings.ins.items.splice(i,1);saveDB();pgIns(ge('content'));});}
function saveInsCfg(){
var cfg=DB.settings.ins;
cfg.socialMin=pnum(ge('iSocMin').value);cfg.socialMax=pnum(ge('iSocMax').value);
cfg.fundMin=pnum(ge('iFundMin').value);cfg.fundMax=pnum(ge('iFundMax').value);
cfg.medicalExtra=pnum(ge('iMed').value);cfg.roundTo=parseFloat(ge('iRound').value)||0.01;
saveDB();toast('五险一金方案已保存','ok');
}
/* ---- 9.5 个税设置 ---- */
function pgTax(c){
var t=DB.settings.tax;
var html='
';
c.innerHTML=html;
}
function saveTaxCfg(){DB.settings.tax.basic=pnum(ge('tBasic').value)||5000;saveDB();toast('个税参数已保存','ok');}
function resetTax(){DB.settings.tax=JSON.parse(JSON.stringify(DEF_TAX));saveDB();pgTax(ge('content'));toast('已恢复法定默认税率表','ok');}
/* ---- 9.6 工资核算 ---- */
function pgPay(c){
var html='
💰 工资核算 五险一金与个税自动计算
'+
''+
''+
'
';
c.innerHTML=html;pgPayRender();
}
function pgPayRender(){
var month=ge('payMonth').value,rec=DB.payroll[month],box=ge('payBody');
if(!rec){box.innerHTML='
';
box.innerHTML=html;
}
function payCellEdit(inp,month){
if(isClosed(month))return toast('本月已月末结转,金额已锁定','err');
inp.value=inp.value.replace(/[^0-9.\-]/g,'');
var rec=DB.payroll[month];if(!rec)return;
var i=+inp.dataset.row,k=inp.dataset.k;
if(!rec.inputs)rec.inputs={};
var cur=rec.inputs[rec.rows[i].empId]||{bonus:0,ot:0,otherAdd:0,absence:0,otherDeduct:0};
var v=pnum(inp.value);
if(k==='socialBase'||k==='fundBase'){
/* 缴费基数:留空或 0 = 恢复默认(按应发工资自动取数);填写数字 = 本月手动基数 */
if(v>0)cur[k]=v;else delete cur[k];
}else cur[k]=v;
rec.inputs[rec.rows[i].empId]=cur;
}
function rebuildPayroll(month){
var rec=DB.payroll[month];if(!rec)return;
var prior=priorCumFor(month);
rec.rows=DB.emps.filter(function(e){return e.status!=='离职';}).map(function(e){return buildRow(e,month,(rec.inputs||{})[e.id],prior);});
saveDB();
if(curPage==='pay'&&ge('payMonth')&&ge('payMonth').value===month)pgPayRender();
}
function pgPayRecalc(month){rebuildPayroll(month);pgPayRender();}
function pgPayGen(){
var month=ge('payMonth').value;
if(isClosed(month))return toast(month+' 已月末结转,请先在【月末结转】中取消结转','err');
if(!DB.depts.length)return toast('请先在【部门管理】建立部门','err');
if(!DB.emps.filter(function(e){return e.status!=='离职';}).length)return toast('没有在职员工','err');
var rows=buildPayroll(month);
var inputs={};
rows.forEach(function(r){inputs[r.empId]={bonus:r.bonus,ot:r.ot,otherAdd:r.otherAdd,absence:r.absence,otherDeduct:r.otherDeduct};});
DB.payroll[month]={generatedAt:new Date().toISOString(),rows:rows,inputs:inputs};
saveDB();pgPayRender();toast(month+' 工资表已生成('+rows.length+'人)','ok');
}
function pgPaySave(month){saveDB();toast('已保存','ok');}
function delPayroll(m){
if(isClosed(m))return toast(m+' 已月末结转锁定,请先取消结转','err');
askConfirm('确认删除 '+m+' 的工资表?删除后该月之后的累计预扣税基将不再包含本月。',function(){delete DB.payroll[m];saveDB();pgPayRender();});
}
function rowDetail(month,i){
var r=DB.payroll[month].rows[i];
var insHtml=Object.keys(r.ins).map(function(k){var d=r.ins[k];
return '
';
});
ge('printArea').innerHTML=html;window.print();
}
/* ---- 9.7 凭证中心 ---- */
var vchSel={v1:true,v2:true,v3:true,v4:true};
function vchDefaultMonth(){
return curPeriod();
}
function pgVch(c){
var html='
📑 薪酬凭证自动生成 计提 → 发放 → 缴纳 全链路 · 支持明细/汇总双数据源
'+
''+
'
'+
'
🔗
财务软件对接流程:① 在本页核对分录 → ② 点击「导出凭证 Excel(财务软件标准模板)」 → ③ 打开财务软件,将账期切换到凭证所属月份(发放/缴纳凭证在次月,需切换到次月账期) → ④ 凭证管理 → 📤 批量导入 → 选择导出的文件。导出格式已按《凭证批量导入标准模板》逐列适配;工资计提、五险一金计提按部门自动归集到各部门自定义的费用科目,发放/缴纳凭证使用全局自定义科目,资金科目自动携带现金流编码。 没有个人明细?切换到「按部门汇总生成」页签,点击「部门汇总录入」直接填写各部门汇总金额即可生成同样完整的会计分录(计提/发放/缴纳全套)。
'+
'
';
c.innerHTML=html;pgVchRender();
}
function pgVchRender(){
var month=ge('vchMonth').value,box=ge('vchBody');
var hasDetail=!!(DB.payroll[month]&&DB.payroll[month].rows&&DB.payroll[month].rows.length);
var hasManual=!!(DB.vchManual[month]&&DB.vchManual[month].depts&&DB.vchManual[month].depts.length);
if(!hasDetail&&!hasManual){
box.innerHTML='
📄'+esc(month)+' 尚无可生成凭证的数据
'+
''+
'
';
return;
}
var mode=vchSourceOf(month);
var vs=mode==='manual'?buildVouchersManual(month):buildVouchers(month);
if(!vs){box.innerHTML='
📄当前数据源为空,请切换页签或录入数据
';return;}
var msgs=validateVouchers(vs);
var html='
';
openDlg('🧮','部门汇总数据('+esc(month)+')— 凭证生成',html,[
{t:'取消'},
{t:'💾 保存并生成凭证',cls:'btn-p',fn:function(){return mvSave(month);}}
],false,true);
vchManualPaint();
}
function vchManualPaint(){
var b=ge('mvBody');if(!b)return;
b.innerHTML=vchManualTmp.map(function(r,i){
function tx(k,w){return '';}
function nm(k){return '';}
return '
'+tx('name',110)+'
'+tx('salaryAcc',88)+'
'+tx('socialAcc',88)+'
'+tx('fundAcc',88)+'
'+
'
'+nm('gross')+'
'+nm('pSocial')+'
'+nm('pFund')+'
'+nm('tax')+'
'+nm('cSocial')+'
'+nm('cFund')+'
'+
'
';
}).join('');
mvSums();
}
function mvSum(){var s={gross:0,pSocial:0,pFund:0,tax:0,cSocial:0,cFund:0};vchManualTmp.forEach(function(r){Object.keys(s).forEach(function(k){s[k]=r2(s[k]+pnum(r[k]));});});return s;}
function mvFootHtml(){
var s=mvSum();
return '
';
c.innerHTML=html;
var years=[];
Object.keys(DB.payroll).forEach(function(m){var y=m.substring(0,4);if(years.indexOf(y)<0)years.push(y);});
var py=curPeriod().substring(0,4);
if(years.indexOf(py)<0)years.push(py);
years.sort().reverse();
if(!jzState.year||years.indexOf(jzState.year)<0)jzState.year=py;
ge('jzYear').innerHTML=years.map(function(y){return '';}).join('');
pgJzRender();
}
function pgJzRender(){
var box=ge('jzBody'),cards=ge('jzCards');
if(!box)return;
var months=Object.keys(DB.payroll).filter(function(m){return m.substring(0,4)===jzState.year;}).sort();
/* 本年逐月累计(含未结转月份) */
var acc={gross:0,tax:0,net:0,personal:0,company:0},cum={};
months.forEach(function(m){
var t=payrollTotals(DB.payroll[m].rows);
acc.gross=r2(acc.gross+t.gross);acc.tax=r2(acc.tax+t.tax);acc.net=r2(acc.net+t.net);
acc.personal=r2(acc.personal+t.pTotal);acc.company=r2(acc.company+t.cTotal);
cum[m]={gross:acc.gross,tax:acc.tax,net:acc.net,personal:acc.personal,company:acc.company};
});
var closedN=months.filter(function(m){return isClosed(m);}).length;
var lastCum=months.length?cum[months[months.length-1]]:{gross:0,tax:0,net:0,personal:0,company:0};
cards.innerHTML='
';
months.slice().reverse().forEach(function(m){
var rec=DB.payroll[m],t=payrollTotals(rec.rows),cl=cum[m];
var vs=buildVouchers(m),okV=vs?validateVouchers(vs).length===0:false;
html+='