v17: файлы для каждого подпункта отдельно по месяцам

This commit is contained in:
Dauren777 2026-06-05 04:02:31 +00:00
parent 26ee5d4356
commit b41bc1d768

View File

@ -222,37 +222,47 @@ function renderDashboard(){
function downloadReport(){
var from=parseInt(document.getElementById("rptFrom").value),to=parseInt(document.getElementById("rptTo").value);
var my=getMy(),csv="№;Мероприятие;Раздел;Дивизион;Статус;Прогресс;Срок;Факт;Отчёты по месяцам (текст);Файлы по месяцам (названия)\n";
var my=getMy(),csv="№;Мероприятие;Подпункт;Раздел;Дивизион;Статус;Прогресс;Срок;Факт;Отчёт (текст);Файлы\n";
my.forEach(function(e){
var rep="",fls="",d=getMD(e.id);
for(var i=from;i<=to;i++){var m=months[i];if(d[m]){if(d[m].report)rep+=M(i)+": "+d[m].report.replace(/"/g,'""')+"; ";if(d[m].files&&d[m].files.length)fls+=M(i)+": "+d[m].files.map(function(f){return f.name}).join(", ")+"; "}}
csv+=e.id+';"'+e.t.replace(/"/g,'""')+'";'+sections[e.sec]+';'+branches[e.b]+';'+statusMap[e.s]+';'+e.p+'%;'+e.due+';'+(e.done||"—")+';"'+rep+'";"'+fls+'"\n';
function addRow(subLabel,subIdx){var rep="",fls="",d=getMD(e.id,subIdx);
for(var i=from;i<=to;i++){var m=months[i];if(d[m]){if(d[m].report)rep+=M(i)+": "+d[m].report.replace(/"/g,'""')+"; ";if(d[m].files&&d[m].files.length)fls+=M(i)+": "+d[m].files.map(function(f){return f.name}).join(", ")+"; "}}
csv+=e.id+';"'+e.t.replace(/"/g,'""')+'";'+(subLabel||"общее")+';'+sections[e.sec]+';'+branches[e.b]+';'+statusMap[e.s]+';'+e.p+'%;'+e.due+';'+(e.done||"—")+';"'+rep+'";"'+fls+'"\n';}
addRow("",-1);
if(e.sub) e.sub.forEach(function(s,i){ addRow(s.l,i); });
});
var blob=new Blob(["\uFEFF"+csv],{type:"text/csv;charset=utf-8"}),a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".csv";a.click()
}
function downloadHTML(){
var from=parseInt(document.getElementById("rptFrom").value),to=parseInt(document.getElementById("rptTo").value);
var my=getMy(),h='<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Сводный отчёт ПБ</title><style>body{font:14px/1.5 Arial,sans-serif;max-width:1100px;margin:0 auto;padding:24px}h1{font-size:22px;margin-bottom:4px}h1 span{color:#00E5FF}h2{font-size:18px;margin:24px 0 8px}.ev{border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:16px;page-break-inside:avoid}.ev h3{font-size:15px;margin:0 0 8px}.meta{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:#666;margin-bottom:8px}.meta strong{color:#111}.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700}.g{background:#D1FAE5;color:#065F46}.a{background:#FEF3C7;color:#92400E}.r{background:#FEE2E2;color:#991B1B}.w{background:#eee;color:#666}.month{background:#f5f5f5;padding:8px 12px;border-radius:4px;margin:6px 0}.month strong{font-size:12px}.files{font-size:12px;color:#555;margin-top:2px}.files li{list-style:"📄 ";margin-left:20px}@media print{.ev{border-color:#999}}</style></head><body>';
var my=getMy(),h='<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><title>Сводный отчёт ПБ</title><style>body{font:14px/1.5 Arial,sans-serif;max-width:1100px;margin:0 auto;padding:24px}h1{font-size:22px;margin-bottom:4px}h1 span{color:#00E5FF}h2{font-size:18px;margin:24px 0 8px}.ev{border:1px solid #ddd;border-radius:8px;padding:16px;margin-bottom:16px}.ev h3{font-size:15px;margin:0 0 8px}.meta{display:flex;gap:16px;flex-wrap:wrap;font-size:12px;color:#666;margin-bottom:8px}.meta strong{color:#111}.badge{display:inline-block;padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700}.g{background:#D1FAE5;color:#065F46}.a{background:#FEF3C7;color:#92400E}.r{background:#FEE2E2;color:#991B1B}.w{background:#eee;color:#666}.month{background:#f5f5f5;padding:8px 12px;border-radius:4px;margin:6px 0}.sub-blk{border-left:3px solid #00E5FF;padding-left:12px;margin:8px 0}.sub-blk strong{font-size:12px}.files{font-size:12px;color:#555;margin-top:2px}ul{list-style:"📄 ";margin-left:20px}@media print{.ev{border-color:#999}}</style></head><body>';
h+='<h1><span>ИИ-Агент</span> ПБ — Сводный отчёт</h1><p style="color:#666">Период: '+M(from)+' — '+M(to)+' · Дивизион: '+branches[curUser.branch]+' · Сформирован: '+new Date().toLocaleDateString()+'</p>';
my.forEach(function(e){
var scls={done:"g",warn:"a",late:"r",wait:"w"}[e.s];
h+='<div class="ev"><h3>'+e.id+'. '+esc(e.t)+'</h3>';
h+='<div class="meta"><span>Раздел: <strong>'+sections[e.sec]+'</strong></span><span>Дивизион: <strong>'+branches[e.b]+'</strong></span><span>Срок: <strong>'+e.due+'</strong></span><span>Факт: <strong>'+(e.done||"—")+'</strong></span><span>Прогресс: <strong>'+e.p+'%</strong></span><span class="badge '+scls+'">'+statusMap[e.s]+'</span></div>';
h+='<div class="meta"><span>Ответственный: <strong>'+esc(e.r)+'</strong></span></div>';
var d=getMD(e.id),has=false;
for(var i=from;i<=to;i++){var m=months[i];if(d[m]&&(d[m].report||(d[m].files&&d[m].files.length))){
has=true;h+='<div class="month"><strong>'+M(i)+'</strong>';
if(d[m].report)h+='<p style="font-size:13px;margin:4px 0">'+esc(d[m].report)+'</p>';
if(d[m].files&&d[m].files.length)h+='<div class="files"><ul>'+d[m].files.map(function(f){return'<li>'+esc(f.name)+(f.desc?' — '+esc(f.desc):'')+' ('+(f.size/1024).toFixed(0)+' КБ)</li>'}).join("")+'</ul></div>';
h+='</div>'}}
if(!has)h+='<p style="font-size:12px;color:#999">Нет отчётов за выбранный период</p>';
// Main event data
h+=renderMonthBlock(e.id,-1,"Общие материалы",from,to);
// Sub-items
if(e.sub) e.sub.forEach(function(s,i){ h+='<div class="sub-blk"><strong>'+s.l+') '+esc(s.t)+'</strong>';h+=renderMonthBlock(e.id,i,"",from,to);h+='</div>'});
h+='<div style="font-size:11px;color:#666;margin-top:8px">🤖 ИИ: '+esc(e.ai)+'</div></div>';
});
h+='</body></html>';
var blob=new Blob(["\uFEFF"+h],{type:"text/html;charset=utf-8"}),a=document.createElement("a");a.href=URL.createObjectURL(blob);a.download="otchet_pb_"+M(from)+"-"+M(to)+".html";a.click()
}
function renderMonthBlock(id,si,label,from,to){
var d=getMD(id,si),has=false,html='';
for(var i=from;i<=to;i++){var m=months[i];if(d[m]&&(d[m].report||(d[m].files&&d[m].files.length))){
has=true;html+='<div class="month"><strong>'+M(i)+(label?' — '+label:'')+'</strong>';
if(d[m].report)html+='<p style="font-size:13px;margin:4px 0">'+esc(d[m].report)+'</p>';
if(d[m].files&&d[m].files.length)html+='<div class="files"><ul>'+d[m].files.map(function(f){return'<li>'+esc(f.name)+(f.desc?' — '+esc(f.desc):'')+' ('+(f.size/1024).toFixed(0)+' КБ)</li>'}).join("")+'</ul></div>';
html+='</div>'}}
if(!has)html='<p style="font-size:12px;color:#999">Нет отчётов</p>';
return html;
}
// ===== MY EVENTS =====
var expandedEvents = {};
function toggleExpand(eid) { expandedEvents[eid] = !expandedEvents[eid]; renderMyEvents(); }
@ -333,19 +343,19 @@ function renderAnalytics(){
}
// ===== EDIT MODAL =====
function openEdit(id,mi){
var editSubIdx = -1; // -1 = main event, 0+ = sub-item
function openEdit(id, mi, si){
if(typeof mi==="number")curMonth=mi;
if(typeof si==="number")editSubIdx=si;
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
var ad=getMD(e.id),sc=getSC(e.id),cm=months[curMonth],cd=ad[cm]||{report:"",files:[]},cfs=cd.files||[],tf=0;
for(var k in ad){if(ad.hasOwnProperty(k))tf+=(ad[k].files||[]).length}
var hasSub = e.sub && e.sub.length;
var cm=months[curMonth];
var sc=getSC(e.id);
var sh="";if(e.sub&&e.sub.length){sh='<div style="font-weight:600;margin-bottom:8px">Подпункты</div><div class="sub-items">';
e.sub.forEach(function(s,i){var ch=sc.indexOf(i)>=0;sh+='<div class="sub-item"><input type="checkbox" id="sc_'+i+'" '+(ch?"checked":"")+'><span class="sub-label">'+s.l+')</span><span class="sub-text">'+esc(s.t)+'</span></div>'});
sh+='</div>'}
var fh="";cfs.forEach(function(f,i){fh+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+',\''+cm+'\','+i+')">📄 '+esc(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+esc(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ · '+f.date+'</span><button class="file-del" onclick="rmF('+e.id+',\''+cm+'\','+i+')">×</button></div>'});
var mh='<div class="month-tabs">';months.forEach(function(m,i){mh+='<span class="month-tab'+(i===curMonth?" active":"")+'" onclick="openEdit('+e.id+','+i+')">'+M(i)+'</span>'});mh+='</div>';
// Main event data
var md=getMD(e.id,-1);
var cd=md[cm]||{report:"",files:[]}, cfs=cd.files||[];
var html='<button class="close" onclick="closeEM()">&times;</button>';
html+='<span class="badge blue">Раздел '+["I","II","III","IV","V"][e.sec]+'</span>';
@ -354,52 +364,104 @@ function openEdit(id,mi){
html+='<div class="field"><label>Статус</label><select id="es"><option value="wait"'+(e.s==="wait"?" selected":"")+'>В процессе</option><option value="warn"'+(e.s==="warn"?" selected":"")+'>На контроле</option><option value="late"'+(e.s==="late"?" selected":"")+'>Просрочено</option><option value="done"'+(e.s==="done"?" selected":"")+'>Исполнено</option></select></div>';
html+='<div class="field"><label>Прогресс (%)</label><input type="range" id="ep" min="0" max="100" value="'+e.p+'" oninput="document.getElementById(\'pv\').textContent=this.value+\'%\'"><span id="pv" style="font-weight:700">'+e.p+'%</span></div>';
html+='<div class="field"><label>Комментарий</label><textarea id="ec" placeholder="Комментарий..."></textarea></div>';
html+=sh;
html+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px"><div style="display:flex;justify-content:space-between;margin-bottom:12px"><span style="font-weight:600">📎 Отчётность по месяцам</span><span style="font-size:12px;color:var(--gray-500)">Файлов: '+tf+'</span></div>';
// Month tabs (shared)
var mh='<div class="month-tabs">';months.forEach(function(m,i){mh+='<span class="month-tab'+(i===curMonth?" active":"")+'" onclick="openEdit('+e.id+','+i+','+editSubIdx+')">'+M(i)+'</span>'});mh+='</div>';
// Sub-items with file sections
var sh='';
if(hasSub){
sh+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:16px"><div style="font-weight:600;margin-bottom:8px">Подпункты</div>';
sh+=mh;
e.sub.forEach(function(s,i){
var ch=sc.indexOf(i)>=0;
var sd=getMD(e.id,i), scd=sd[cm]||{report:"",files:[]}, scfs=scd.files||[];
var isActive = editSubIdx === i;
sh+='<div class="sub-item" style="flex-wrap:wrap;padding:12px 14px;margin-bottom:8px">';
sh+='<input type="checkbox" id="sc_'+i+'" '+(ch?"checked":"")+'><span class="sub-label">'+s.l+')</span><span class="sub-text" style="flex:1">'+esc(s.t)+'</span>';
sh+='<span style="font-size:11px;color:var(--gray-500);margin-right:8px">Файлов: '+(scfs.length)+'</span>';
sh+='<button class="btn btn-sm" onclick="openEdit('+e.id+','+curMonth+','+i+')" style="font-size:11px;background:'+(isActive?'var(--cyan)':'var(--gray-100)')+'">📎</button>';
sh+='</div>';
if(isActive){
sh+='<div style="margin-left:20px;margin-bottom:12px;padding:12px;background:var(--cyan-50);border-radius:8px">';
sh+='<div style="font-weight:600;font-size:13px;margin-bottom:8px">'+s.l+') '+esc(s.t.slice(0,60))+'...</div>';
sh+='<div class="field"><label>Текст отчёта за '+M(curMonth)+'</label><textarea id="mr_s'+i+'" placeholder="Опишите ход исполнения..." style="min-height:60px">'+esc(scd.report||"")+'</textarea></div>';
scfs.forEach(function(f,fi){sh+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+fi+','+i+')">📄 '+esc(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+esc(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ · '+f.date+'</span><button class="file-del" onclick="rmF('+e.id+','+curMonth+','+fi+','+i+')">×</button></div>'});
sh+='<div class="upload-row"><input type="text" id="fd_s'+i+'" placeholder="Описание файла"><input type="file" id="fi_s'+i+'" multiple style="max-width:180px"><button class="btn btn-sm" id="ub_s'+i+'" onclick="uploadFiles('+e.id+','+curMonth+','+i+')">Загрузить</button></div>';
sh+='</div>';
}
});
sh+='</div>';
}
// Main event files section
html+='<div style="border-top:1px solid var(--gray-200);padding-top:16px;margin-top:12px"><div style="display:flex;justify-content:space-between;margin-bottom:12px"><span style="font-weight:600">📎 Общие материалы</span><span style="font-size:12px;color:var(--gray-500)">Файлов: '+cfs.length+'</span></div>';
html+=mh;
html+='<div class="field" style="margin-top:12px"><label>Текст отчёта за '+M(curMonth)+'</label><textarea id="mr" placeholder="Опишите ход исполнения... Можно без файлов." style="min-height:80px">'+esc(cd.report||"")+'</textarea></div>';
html+=fh;
html+='<div class="upload-row"><input type="text" id="fd" placeholder="Описание файла"><input type="file" id="fi" multiple><button class="btn btn-sm" id="ub" onclick="uploadFiles('+e.id+',\''+cm+'\')">Загрузить</button></div>';
cfs.forEach(function(f,i){html+='<div class="file-row"><span class="file-info"><span class="file-name" onclick="dlF('+e.id+','+curMonth+','+i+',-1)">📄 '+esc(f.name)+'</span>'+(f.desc?'<span class="file-desc">'+esc(f.desc)+'</span>':'')+'</span><span class="file-meta">'+(f.size/1024).toFixed(0)+' КБ · '+f.date+'</span><button class="file-del" onclick="rmF('+e.id+','+curMonth+','+i+',-1)">×</button></div>'});
html+='<div class="upload-row"><input type="text" id="fd" placeholder="Описание файла"><input type="file" id="fi" multiple style="max-width:220px"><button class="btn btn-sm" id="ub" onclick="uploadFiles('+e.id+','+curMonth+',-1)">Загрузить</button></div>';
html+='<p style="font-size:11px;color:var(--gray-500);margin-top:6px">Формы завершения: '+esc(e.dname)+'</p></div>';
html+=sh;
html+='<div class="ai-block"><h4>🤖 Вывод ИИ-агента</h4>'+esc(e.ai)+'</div>';
html+='<div style="font-weight:600;margin:8px 0 4px">История:</div><div>';e.h.forEach(function(h){html+='<div class="history-item"><div class="dot"></div>'+esc(h)+'</div>'});html+='</div>';
html+='<div style="margin-top:20px;display:flex;gap:12px"><button class="btn" onclick="saveEdit('+e.id+',\''+cm+'\')">Сохранить</button><button class="btn btn-outline" onclick="closeEM()">Отмена</button></div>';
html+='<div style="margin-top:20px;display:flex;gap:12px"><button class="btn" onclick="saveEdit('+e.id+','+curMonth+')">Сохранить</button><button class="btn btn-outline" onclick="closeEM()">Отмена</button></div>';
document.getElementById("editModalContent").innerHTML=html;
document.getElementById("editModalOverlay").classList.add("open");
}
function saveEdit(id,mk){
function saveEdit(id, mk){
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}if(!e)return;
e.s=document.getElementById("es").value;e.p=parseInt(document.getElementById("ep").value);
var cmt=(document.getElementById("ec").value||"").trim(),mr=document.getElementById("mr");mr=mr?mr.value:"";
if(mk){var ad=getMD(id);if(!ad[mk])ad[mk]={report:"",files:[]};ad[mk].report=mr;setMD(id,ad)}
if(e.sub&&e.sub.length){var cks=[];e.sub.forEach(function(_,i){var el=document.getElementById("sc_"+i);if(el&&el.checked)cks.push(i)});setSC(id,cks)}
var cmt=(document.getElementById("ec").value||"").trim();
// Save main event report
var mr=document.getElementById("mr");if(mr){var ad=getMD(id,-1);if(!ad[mk])ad[mk]={report:"",files:[]};ad[mk].report=mr.value;setMD(id,ad,-1)}
// Save sub-item reports
if(e.sub&&e.sub.length){
var cks=[];
e.sub.forEach(function(_,i){
var el=document.getElementById("sc_"+i);if(el&&el.checked)cks.push(i);
var sr=document.getElementById("mr_s"+i);if(sr){var sd=getMD(id,i);if(!sd[mk])sd[mk]={report:"",files:[]};sd[mk].report=sr.value;setMD(id,sd,i)}
});
setSC(id,cks);
}
var now=new Date().toLocaleDateString();e.h.push(now+" — "+curUser.name+": "+statusMap[e.s]+", "+e.p+"%"+(cmt?" — "+cmt:""));
if(e.s==="done"&&e.done==="\u2014")e.done=now;
saveEvents();closeEM();renderAll();
}
function closeEM(){document.getElementById("editModalOverlay").classList.remove("open")}
// File storage
function getMD(id){var r=localStorage.getItem("sf_"+id);return r?JSON.parse(r):{}}
function setMD(id,o){localStorage.setItem("sf_"+id,JSON.stringify(o))}
// File storage: sf_<id> for main, sf_<id>_s<i> for sub-item i
function getMD(id,si){var k=si>=0?'sf_'+id+'_s'+si:'sf_'+id;var r=localStorage.getItem(k);return r?JSON.parse(r):{}}
function setMD(id,o,si){var k=si>=0?'sf_'+id+'_s'+si:'sf_'+id;localStorage.setItem(k,JSON.stringify(o))}
function getSC(id){var r=localStorage.getItem("ss_"+id);return r?JSON.parse(r):[]}
function setSC(id,a){localStorage.setItem("ss_"+id,JSON.stringify(a))}
function uploadFiles(eid,mk){
var fi=document.getElementById("fi");if(!fi||!fi.files.length)return;
var desc=(document.getElementById("fd").value||"").trim(),btn=document.getElementById("ub");
btn.textContent="Загружается...";btn.disabled=true;
var MAX=4*1024*1024,ad=getMD(eid);if(!ad[mk])ad[mk]={report:"",files:[]};
function countFiles(id){
var total=0,main=getMD(id);
for(var k in main){if(main.hasOwnProperty(k))total+=(main[k].files||[]).length}
var e=null;for(var i=0;i<events.length;i++){if(events[i].id===id){e=events[i];break}}
if(e&&e.sub)for(var j=0;j<e.sub.length;j++){var sd=getMD(id,j);for(var sk in sd){if(sd.hasOwnProperty(sk))total+=(sd[sk].files||[]).length}}
return total;
}
function uploadFiles(eid,mk,si){
var prefix=si>=0?'_s'+si:'',fi=document.getElementById('fi'+prefix);
if(!fi||!fi.files.length)return;
var desc=(document.getElementById('fd'+prefix)||{}).value;desc=(desc||'').trim();
var btn=document.getElementById('ub'+prefix);btn.textContent="Загружается...";btn.disabled=true;
var MAX=4*1024*1024,ad=getMD(eid,si);if(!ad[mk])ad[mk]={report:"",files:[]};
var arr=ad[mk].files,pr=0,sk=0;
function fin(){try{setMD(eid,ad)}catch(e){alert("Хранилище переполнено")}if(sk)alert(sk+" файл(ов) > 4 МБ пропущены");closeEM();openEdit(eid)}
function fin(){try{setMD(eid,ad,si)}catch(e){alert("Хранилище переполнено")}if(sk)alert(sk+" файл(ов) > 4 МБ пропущены");closeEM();openEdit(eid,curMonth,si>=0?si:undefined)}
for(var i=0;i<fi.files.length;i++){(function(f){if(f.size>MAX){sk++;pr++;if(pr===fi.files.length)fin();return}
var r=new FileReader();r.onload=function(ev){arr.push({name:f.name,size:f.size,type:f.type,desc:desc,date:new Date().toLocaleDateString(),data:ev.target.result});pr++;if(pr===fi.files.length)fin()};
r.onerror=function(){pr++;if(pr===fi.files.length)fin()};r.readAsDataURL(f)})(fi.files[i])}
}
function dlF(eid,mk,idx){var ad=getMD(eid),arr=ad[mk]?ad[mk].files:null;if(!arr||!arr[idx]||!arr[idx].data)return;var f=arr[idx],a=document.createElement("a");a.href=f.data;a.download=f.name;document.body.appendChild(a);a.click();document.body.removeChild(a)}
function rmF(eid,mk,idx){var ad=getMD(eid);if(!ad[mk]||!ad[mk].files)return;ad[mk].files.splice(idx,1);setMD(eid,ad);closeEM();openEdit(eid)}
function dlF(eid,mk,idx,si){si=si||-1;var ad=getMD(eid,si),arr=ad[mk]?ad[mk].files:null;if(!arr||!arr[idx]||!arr[idx].data)return;var f=arr[idx],a=document.createElement("a");a.href=f.data;a.download=f.name;document.body.appendChild(a);a.click();document.body.removeChild(a)}
function rmF(eid,mk,idx,si){si=si||-1;var ad=getMD(eid,si);if(!ad[mk]||!ad[mk].files)return;ad[mk].files.splice(idx,1);setMD(eid,ad,si);closeEM();openEdit(eid,curMonth,si>=0?si:undefined)}
// Init
function renderAll(){notifsUpdate();switchTab(document.querySelector(".tab-btn.active").dataset.tab)}