▦
Facility Manager – NIFCOZarządzanie infrastrukturą i utrzymaniem obiektu
⌕
♧3
GM
GrzegorzNIFCO (Administrator)
⌄

Pulpit główny

Podsumowanie zadań, przeglądów, raportów i dokumentacji

⚒ Zadania do realizacji

Wszystkie zadania →

▦ Najbliższe przeglądy

Harmonogram →

▤ Ostatnie raporty serwisowe

Wszystkie raporty →

▥ Realizacja zadań

Rejestr zadań
14zadań

♙ Najbliższe wizyty serwisu

☑ Zadania do obchodów dziennych

Wszystkie obchody →

⚡ Szybkie akcje

Zadania i usterki

Rejestr zgłoszeń, napraw i prac do realizacji

NrZadanie / usterkaObszarPriorytetStatusTerminOferta / kosztAkcja

Obchody dzienne

Plan zadań dla techników, kontrola wykonania i dokumentacja zdjęciowa

☑ Zadania zaplanowane na wybrany dzień

GodzinaZadanie / punkt obchoduObszar / lokalizacjaTechnikStatusNotatka / zdjęcieAkcja

Archiwum obchodów

Zadania z wcześniejszych dni
Kalendarz obchodów
● Dzień z zakończonym obchodem. Kliknij zaznaczony dzień, aby wyświetlić jego wpisy.
DataGodzinaZadanieLokalizacjaTechnikStatusNotatkaZdjęcie

Raporty serwisowe

Rejestr wizyt, wykonanych prac i załączników

Data wizytyFirma / zespółZakres pracPowiązane zadanieStatusAkcja

Przeglądy i serwis

Harmonogram przeglądów, protokoły i zalecenia

Rodzaj przegląduObiekt / lokalizacjaTerminWykonawcaStatusAkcja

Materiały i zamówienia

Zapotrzebowania, zamówienia i kontrola materiałów

Nazwa materiału / zakresIlośćPowiązane zadanieWartość nettoStatusAkcja

Magazyn części infrastruktury

Ewidencja części zamiennych i materiałów utrzymania infrastruktury

Nazwa częściIndeks / nr katalogowyKategoriaIlośćStan min.LokalizacjaCena jedn. / dostawcaUwagiAkcja

Dokumentacja

Baza protokołów, raportów, certyfikatów i zdjęć — uporządkowana według folderów

📁 Foldery dokumentacji

Wszystkie dokumenty

0 plików
Nazwa dokumentuKategoriaDataPowiązane zadaniePlikAkcja

Analizy i rozliczenia

Skuteczność, terminowość, koszty i trendy

Dane grupowane według daty oferty lub daty zgłoszenia.

Koszty w czasie

Struktura kosztów według obszaru

Rozliczenie plan vs wykonanie

Potwierdzenie i ocena wykonania zleceń

Weryfikacja techniczna

Po wykonaniu zlecenia wybierz wynik odbioru, wpisz koszt rzeczywisty i dodaj komentarz. Dane są zapisywane w rejestrze zadań.

ZadanieWykonawcaWartość ofertyWynik odbioruKoszt rzeczywistyData odbioruNotatka

Otwarte kwestie i zalecane działania

Ustawienia systemu

Konfiguracja aplikacji, powiadomień i parametrów

⚙ Ogólne

♧ Powiadomienia i alerty

Kopia zapasowa i dane

Dane są zapisywane lokalnie w przeglądarce urządzenia. Kopię warto pobierać regularnie.

Zarządzanie użytkownikami

Użytkownicy, firmy serwisowe, role i dostęp do systemu

UżytkownikFirma / działRolaStatusAkcja
`);win.document.close(); } function deleteStockItem(i){if(confirm("Czy na pewno usunąć tę część z magazynu?")){db.infrastructureStock.splice(i,1);persist()}} function renderMaterials(){$("materialRows").innerHTML=db.materials.map((x,i)=>`${esc(x.name)}${esc(x.qty)}${esc(x.task||"—")}${fmt(x.cost)}${badge(x.status)}`).join("")+db.tasks.filter(x=>x.net>0).map(x=>`${esc(x.task)}
Oferta SPIE ${esc(x.offer)}—${esc(x.ticket)}${fmt(x.net)}${badge(x.status)}`).join("")} let selectedDocFolder=""; function folderPath(id){let out=[],f=db.docFolders.find(x=>x.id===id),guard=0;while(f&&guard++<30){out.unshift(f.name);f=db.docFolders.find(x=>x.id===f.parentId)}return out.join(" / ")} function renderDocFolders(parentId="",depth=0){return db.docFolders.filter(f=>(f.parentId||"")===parentId).map(f=>{let count=db.documents.filter(d=>d.folderId===f.id).length,children=db.docFolders.some(c=>c.parentId===f.id);return `${children?`
${renderDocFolders(f.id,depth+1)}
`:""}`}).join("")} function renderDocuments(){ensureDocFolders();let q=($("docSearch")?.value||"").toLowerCase(),cat=$("docCategory")?.value||"";let tree=$("docFolderTree");if(tree)tree.innerHTML=``+renderDocFolders()+`
`;let list=db.documents.map((x,i)=>({...x,_index:i})).filter(x=>(!selectedDocFolder||x.folderId===selectedDocFolder)&&(!q||x.name.toLowerCase().includes(q))&&(!cat||x.category===cat));$("docCurrentFolder").textContent=selectedDocFolder?folderPath(selectedDocFolder):"Wszystkie dokumenty";$("docCount").textContent=list.length+" plików";$("documentRows").innerHTML=list.map(x=>`${esc(x.name)}${esc(x.category)}${date(x.date)}${esc(x.task||"—")}${x.fileId?``:esc(x.fileName||"Brak pliku")}`).join("")||`
Brak dokumentów w tym folderze.
`} function selectDocFolder(id){selectedDocFolder=id||"";renderDocuments()} function addDocFolder(parentId=""){let name=prompt(parentId?"Podaj nazwę podfolderu:":"Podaj nazwę nowego folderu:");if(!name||!name.trim())return;let clean=name.trim();if(db.docFolders.some(f=>f.name.toLowerCase()===clean.toLowerCase()&&(f.parentId||"")===(parentId||""))){alert("Folder o takiej nazwie już istnieje w tej lokalizacji.");return}let id="DF"+Date.now()+Math.random().toString(36).slice(2,6);db.docFolders.push({id,name:clean,parentId:parentId||""});selectedDocFolder=id;persist()} function analysisDate(t){let raw=t.offerDate||t.createdAt||t.deadline||t.date||"";if(/^\d{2}\.\d{2}\.\d{4}$/.test(raw)){let [d,m,y]=raw.split(".");return new Date(+y,+m-1,+d,12)}if(/^\d{4}-\d{2}-\d{2}$/.test(raw))return new Date(raw+"T12:00:00");return null} function periodKey(d,mode){let y=d.getFullYear(),m=d.getMonth();if(mode==="day")return d.toISOString().slice(0,10);if(mode==="week"){let x=new Date(y,m,d.getDate()),day=(x.getDay()+6)%7;x.setDate(x.getDate()-day+3);let first=new Date(x.getFullYear(),0,4),wk=1+Math.round(((x-first)/86400000-3+((first.getDay()+6)%7))/7);return `${x.getFullYear()}-T${String(wk).padStart(2,"0")}`}if(mode==="month")return `${y}-${String(m+1).padStart(2,"0")}`;if(mode==="quarter")return `${y}-Q${Math.floor(m/3)+1}`;if(mode==="half")return `${y}-H${m<6?1:2}`;return String(y)} function periodLabel(k,mode){if(mode==="day"){let [y,m,d]=k.split("-");return `${d}.${m}`}return k} function analysisDateInYear(t,year){let d=analysisDate(t);return d&&d.getFullYear()===year} function approvalLabel(v){return v||"Nie oceniono"} function isCostApproved(t){return ["Do zamówienia","Zamówione","Do realizacji","W realizacji","Do odbioru","Zakończone"].includes(t.status)||["Wykonano prawidłowo","Wykonano częściowo"].includes(t.approval)} function setApproval(i,key,value){let t=db.tasks[i];t[key]=value;if(key==="approval") {t.approvalDate=new Date().toISOString().slice(0,10);t.approvalLog=t.approvalLog||[];t.approvalLog.push({date:t.approvalDate,value,comment:t.approvalNote||""});if(value==="Wykonano prawidłowo"){t.status="Zakończone";if(t.actualCost===""||t.actualCost===null||t.actualCost===undefined)t.actualCost=+t.net||0;}else if(value==="Wykonano częściowo"){t.status="W realizacji";}else if(value==="Nie wykonano"){t.status="Do realizacji";t.actualCost="";}else if(value==="Do poprawy / reklamacja"){t.status="Do weryfikacji";}}persist()} function renderAnalytics(){ let allTasks=db.tasks||[],tasks=allTasks.filter(isCostApproved),period=$("analysisPeriod")?.value||"week",mode=$("analysisCostMode")?.value||"offer",year=+( $("analysisYear")?.value || new Date().getFullYear()); let yearSel=$("analysisYear");if(yearSel&&!yearSel.options.length){let years=[...new Set(tasks.map(t=>analysisDate(t)?.getFullYear()).filter(Boolean))];years.push(new Date().getFullYear());yearSel.innerHTML=[...new Set(years)].sort((a,b)=>b-a).map(y=>``).join("")} let dated=tasks.filter(t=>analysisDateInYear(t,year)),sum=dated.reduce((a,t)=>a+(+t.net||0),0),actual=dated.reduce((a,t)=>a+(+t.actualCost||0),0),closed=dated.filter(t=>t.approval||t.status==="Zakończone").length,unsettled=dated.filter(t=>t.actualCost===""||t.actualCost===null||t.actualCost===undefined).length; $("analysisPeriodHint").textContent=`Rok ${year}. Koszty obejmują wyłącznie zaakceptowane pozycje (Do zamówienia, Zamówione, Do realizacji, W realizacji, Do odbioru i Zakończone). Oferty oczekujące na decyzję, porównanie lub weryfikację nie obciążają kosztów. Okres według daty oferty lub zgłoszenia.`; $("analyticsKpis").innerHTML=[["Wartość ofert",fmt(sum)],["Koszt rzeczywisty",fmt(actual)],["Odchylenie kosztów",fmt(actual-sum)],["Pozycje w analizie",dated.length],["Odebrane / ocenione",closed],["Nierozliczone",unsettled]].map(x=>`
${x[0]}
${x[1]}
`).join(""); let groups={};dated.forEach(t=>{let d=analysisDate(t);if(!d)return;let k=periodKey(d,period);groups[k]??={offer:0,actual:0,count:0};groups[k].offer+=+t.net||0;groups[k].actual+=+t.actualCost||0;groups[k].count++});let keys=Object.keys(groups).sort();if(period==="day"||period==="week")keys=keys.slice(-16);else if(period==="month")keys=keys.slice(-12);let max=Math.max(1,...keys.map(k=>Math.max(groups[k].offer,groups[k].actual)));$("periodChartTitle").textContent=`Koszty — ${({day:"dni",week:"tygodnie",month:"miesiące",quarter:"kwartały",half:"półrocza",year:"lata"})[period]}`; $("periodChart").innerHTML=keys.length?keys.map(k=>{let g=groups[k],v=mode==="actual"?g.actual:g.offer;return `
${periodLabel(k,period)}
${mode==="both"?`
`:""}
${fmt(v)}
`}).join(""):`
Brak danych z datą w wybranym roku.
`+(mode==="both"?`
● Wartość ofert● Koszt rzeczywisty
`:""); let areas=[...new Set(dated.map(t=>t.area||"Bez obszaru"))].map(a=>[a,dated.filter(t=>t.area===a).reduce((n,t)=>n+(mode==="actual"?(+t.actualCost||0):(+t.net||0)),0)]).sort((a,b)=>b[1]-a[1]),amax=Math.max(1,...areas.map(x=>x[1]));$("areaBars").innerHTML=areas.map(([a,n])=>`
${esc(a)}
${fmt(n)}
`).join("")||"Brak danych"; let delta=actual-sum,dp=sum?delta/sum*100:0;$("costReconciliation").innerHTML=`
Plan / wartość ofert${fmt(sum)}
Koszt rzeczywisty${fmt(actual)}
Różnica${fmt(delta)} (${dp.toFixed(1).replace(".",",")}%)

Koszt rzeczywisty uzupełnia się przy odbiorze zadania. Puste wartości nie są traktowane jako zero w rozliczeniu wykonawcy.

`; $("approvalRows").innerHTML=tasks.map((t,i)=>`${esc(t.task)}
${esc(t.ticket||"")} • ${esc(t.area||"")}${esc(t.spie||t.vendor||"—")}${fmt(t.net)}`).join(""); $("issues").innerHTML=``; } function renderUsers(){$("userRows").innerHTML=db.users.map((x,i)=>`${esc(x.name)}${esc(x.company)}${esc(x.role)}${badge(x.status)} `).join("")} function renderSettings(){$("switches").innerHTML=["Przypomnienia o przeglądach","Zadania po terminie","Nowe raporty serwisowe","Zmiana statusu zamówień"].map((x,i)=>`
${x}
`).join("");$("siteName").value=db.settings.site;$("adminName").value=db.settings.admin} function initFilters(){$("taskStatus").innerHTML=''+statusList.map(x=>``).join("");$("taskArea").innerHTML=''+[...new Set(db.tasks.map(x=>x.area))].map(x=>``).join("")} function modal(title,body){$("modalTitle").textContent=title;$("modalBody").innerHTML=body;$("modal").classList.add("open")}function closeModal(){$("modal").classList.remove("open")} let taskPendingPhotos=[],taskRemovedPhotoIds=[]; function nextTicketNumber(){ const year=new Date().getFullYear(),prefix=`ZGŁ-${year}-`; const nums=db.tasks.map(t=>String(t.ticket||"").match(new RegExp("^ZGŁ-"+year+"-(\\d+)$","i"))).filter(Boolean).map(m=>+m[1]); return prefix+String(Math.max(0,...nums)+1).padStart(4,"0"); } function openTask(x=null){ taskEditingId=x?.id||null; let t=x||{},ticket=t.ticket&&t.ticket!=="—"?t.ticket:nextTicketNumber(); taskPendingPhotos=[];taskRemovedPhotoIds=[]; modal(x?"Edytuj zadanie / usterkę":"Dodaj zadanie / usterkę",`
Numer nadawany automatycznie.
Zdjęcia usterki
Dodaj zdjęcia pokazujące miejsce lub zakres usterki. Możesz wybrać kilka fotografii.
`); $("fPhotos").addEventListener("change",e=>{taskPendingPhotos=[...taskPendingPhotos,...Array.from(e.target.files||[])];e.target.value="";renderTaskPhotoPreview()}); if(x?.id)loadTaskPhotos(x.id).then(renderExistingTaskPhotos); } function renderTaskPhotoPreview(){ let el=$("taskPhotoPreview");if(!el)return; el.innerHTML=taskPendingPhotos.map((f,i)=>`
Podgląd usterki${esc(f.name)}
${Math.max(1,Math.round(f.size/1024))} KB
`).join(""); } function removePendingTaskPhoto(i){taskPendingPhotos.splice(i,1);renderTaskPhotoPreview()} async function loadTaskPhotos(taskId){ if(!taskId)return[];let d=await reportFileDB(); return await new Promise((resolve,reject)=>{let r=d.transaction("files","readonly").objectStore("files").index("reportId").getAll("task_"+taskId);r.onsuccess=()=>resolve((r.result||[]).filter(f=>f.recordType==="taskPhoto"));r.onerror=()=>reject(r.error)}); } function renderExistingTaskPhotos(files){ let el=$("taskExistingPhotos");if(!el)return; el.innerHTML=files.map(f=>`
Zdjęcie usterki${esc(f.name)}
${Math.max(1,Math.round(f.size/1024))} KB
`).join("")||""; } async function downloadTaskPhoto(id){ let d=await reportFileDB(),f=await new Promise((resolve,reject)=>{let r=d.transaction("files","readonly").objectStore("files").get(id);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)}); if(!f)return;let u=URL.createObjectURL(f.blob),a=document.createElement("a");a.href=u;a.download=f.name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1500); } async function removeExistingTaskPhoto(id){ if(!confirm("Usunąć to zdjęcie usterki?"))return; let d=await reportFileDB();await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite");tx.objectStore("files").delete(id);tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)}); taskRemovedPhotoIds.push(id);renderExistingTaskPhotos(await loadTaskPhotos(taskEditingId)); } let taskEditingId=null; async function saveTask(old){ let name=$("fTask").value.trim();if(!name){alert("Wpisz nazwę zadania.");return} let t=db.tasks.find(x=>x.offer===old&&old);if(!t)t={id:"U"+Date.now(),custom:true}; let previousStatus=t.status; Object.assign(t,{task:name,area:$("fArea").value,priority:$("fPriority").value,status:$("fStatus").value,deadline:$("fDeadline").value,net:+$("fCost").value||0,offer:$("fOffer").value||"—",ticket:$("fTicket").value||nextTicketNumber(),details:$("fDetails").value}); if(t.status==="Zakończone"){ t.approval="Wykonano prawidłowo"; t.actualCost=+t.net||0; if(!t.approvalDate)t.approvalDate=new Date().toISOString().slice(0,10); if(previousStatus!=="Zakończone"){t.approvalLog=t.approvalLog||[];t.approvalLog.push({date:t.approvalDate,value:"Wykonano prawidłowo",comment:"Automatyczne potwierdzenie po oznaczeniu zadania jako zakończone."});} } if(!db.tasks.includes(t))db.tasks.unshift(t); try{ if(taskPendingPhotos.length){let d=await reportFileDB();await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite"),st=tx.objectStore("files");taskPendingPhotos.forEach((f,i)=>st.put({id:"task_"+t.id+"_"+Date.now()+"_"+i,reportId:"task_"+t.id,recordType:"taskPhoto",name:f.name,type:f.type||"image/jpeg",size:f.size,blob:f}));tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})} closeModal();persist(); }catch(e){console.error(e);alert("Nie udało się zapisać zdjęć usterki. Sprawdź wolne miejsce w przeglądarce.")} } function editTask(offer){let t=db.tasks.find(x=>x.offer===offer);taskEditingId=t?.id||null;openTask(t)} let reportEditId=null,reportPendingFiles=[]; function openReport(index=null){let x=index===null?{}:db.reports[index];reportEditId=index===null?null:index;reportPendingFiles=[];modal(index===null?"Dodaj raport serwisowy":"Edytuj raport serwisowy",`
Zdjęcia i dokumenty
Możesz dodać kilka zdjęć oraz plików, np. protokół PDF, DOCX lub zestawienie XLSX. Załączniki zostaną zapisane lokalnie w tej przeglądarce.
`);$("rFiles").addEventListener("change",e=>{reportPendingFiles=[...reportPendingFiles,...Array.from(e.target.files||[])];e.target.value="";renderReportFilePreview()});if(index!==null)loadReportAttachments(x.id).then(renderExistingReportAttachments);} function renderReportFilePreview(){let el=$("rAttachPreview");if(!el)return;el.innerHTML=reportPendingFiles.map((f,i)=>`
${f.type.startsWith("image/")?`Podgląd`:"📎"}${esc(f.name)}
${Math.max(1,Math.round(f.size/1024))} KB
`).join("")} function removePendingReportFile(i){reportPendingFiles.splice(i,1);renderReportFilePreview()} let reportDbPromise=null;function reportFileDB(){if(reportDbPromise)return reportDbPromise;reportDbPromise=new Promise((resolve,reject)=>{let req=indexedDB.open("nifco_report_attachments",1);req.onupgradeneeded=()=>{let d=req.result;if(!d.objectStoreNames.contains("files")){let st=d.createObjectStore("files",{keyPath:"id"});st.createIndex("reportId","reportId",{unique:false})}};req.onsuccess=()=>resolve(req.result);req.onerror=()=>reject(req.error)});return reportDbPromise} async function storeReportFiles(reportId,files){if(!files.length)return;let d=await reportFileDB();await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite"),st=tx.objectStore("files");files.forEach((f,i)=>st.put({id:reportId+"_"+Date.now()+"_"+i,reportId,name:f.name,type:f.type||"application/octet-stream",size:f.size,blob:f}));tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})} async function loadReportAttachments(reportId){if(!reportId)return[];try{let d=await reportFileDB();return await new Promise((resolve,reject)=>{let tx=d.transaction("files","readonly"),req=tx.objectStore("files").index("reportId").getAll(reportId);req.onsuccess=()=>resolve(req.result||[]);req.onerror=()=>reject(req.error)})}catch(e){console.error(e);return[]}} function renderExistingReportAttachments(files){let el=$("rExistingAttachments");if(!el)return;el.innerHTML=files.map(f=>`
${f.type.startsWith("image/")?`Zdjęcie`:"📎"}${esc(f.name)}
${Math.max(1,Math.round(f.size/1024))} KB
`).join("")||""} async function downloadReportFile(id){let d=await reportFileDB();let f=await new Promise((resolve,reject)=>{let r=d.transaction("files","readonly").objectStore("files").get(id);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)});if(!f)return;let u=URL.createObjectURL(f.blob),a=document.createElement("a");a.href=u;a.download=f.name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1500)} async function saveReport(){if(!$("rScope").value.trim()){alert("Uzupełnij zakres raportu.");return}let x=reportEditId===null?{id:"R"+Date.now()}:db.reports[reportEditId];Object.assign(x,{date:$("rDate").value,vendor:$("rVendor").value,task:$("rTask").value,status:$("rStatus").value,scope:$("rScope").value});if(reportEditId===null)db.reports.push(x);try{await storeReportFiles(x.id,reportPendingFiles);let files=await loadReportAttachments(x.id);x.attachmentCount=files.length;closeModal();persist()}catch(e){console.error(e);alert("Nie udało się zapisać załączników. Sprawdź wolne miejsce w przeglądarce.")}} async function deleteReport(i){if(!confirm("Usunąć raport wraz z załącznikami?"))return;let x=db.reports[i];if(x?.id){try{let d=await reportFileDB();await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite"),idx=tx.objectStore("files").index("reportId"),r=idx.getAllKeys(x.id);r.onsuccess=()=>r.result.forEach(k=>tx.objectStore("files").delete(k));tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)})}catch(e){console.warn(e)}}db.reports.splice(i,1);persist()} let inspectionPendingFile=null,inspectionEditIndex=-1; function openInspection(i=null){let x=i===null?{}:db.inspections[i];inspectionEditIndex=i===null?-1:i;inspectionPendingFile=null;modal(i===null?"Dodaj przegląd / serwis":"Edytuj przegląd / serwis",`
Dołącz protokół lub raport, np. PDF, DOCX albo XLSX. Plik będzie zapisany lokalnie w tej przeglądarce.
${x.attachmentName?`Aktualny plik: ${esc(x.attachmentName)} `:"Brak załączonego raportu."}
`);$("iFile").addEventListener("change",e=>{inspectionPendingFile=e.target.files?.[0]||null;$('iFileInfo').textContent=inspectionPendingFile?`Wybrano: ${inspectionPendingFile.name} (${Math.max(1,Math.round(inspectionPendingFile.size/1024))} KB)`:"Brak nowego pliku."})} async function saveInspection(i){let previous=i<0?{}:db.inspections[i];let x={...previous,id:previous.id||"I"+Date.now(),title:$("iTitle").value,place:$("iPlace").value,date:$("iDate").value,vendor:$("iVendor").value,status:$("iStatus").value};if(!x.title){alert("Podaj rodzaj przeglądu.");return}try{if(inspectionPendingFile){let d=await reportFileDB();let fileId="inspection_"+x.id;await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite");tx.objectStore("files").put({id:fileId,reportId:fileId,recordType:"inspection",name:inspectionPendingFile.name,type:inspectionPendingFile.type||"application/octet-stream",size:inspectionPendingFile.size,blob:inspectionPendingFile});tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)});x.attachmentId=fileId;x.attachmentName=inspectionPendingFile.name}if(i<0)db.inspections.push(x);else db.inspections[i]=x;closeModal();persist()}catch(e){console.error(e);alert("Nie udało się zapisać raportu przeglądu. Spróbuj ponownie lub sprawdź wolne miejsce w przeglądarce.")}} async function downloadInspectionFile(id){if(!id)return;let d=await reportFileDB();let f=await new Promise((resolve,reject)=>{let r=d.transaction("files","readonly").objectStore("files").get(id);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)});if(!f){alert("Nie znaleziono załączonego pliku.");return}let u=URL.createObjectURL(f.blob),a=document.createElement("a");a.href=u;a.download=f.name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1500)} function editInspection(i){openInspection(i)} function openMaterial(){modal("Nowe zapotrzebowanie materiałowe",`
`)} function saveMaterial(){if(!$("mName").value.trim()){alert("Podaj nazwę materiału.");return}db.materials.push({id:"MAT"+Date.now(),name:$("mName").value,qty:$("mQty").value,cost:+$("mCost").value||0,date:$("mDate").value,vendor:$("mVendor").value,area:$("mArea").value,offerCost:0,includedInTaskCost:$("mIncluded").checked,status:$("mStatus").value,task:$("mTask").value});closeModal();persist()} function openDocument(){ensureDocFolders();let opts=db.docFolders.map(f=>``).join("");modal("Dodaj dokument / załącznik",`
Dokument i załącznik zostaną zapisane lokalnie w tej przeglądarce. `)} async function saveDocument(){let f=$("dFile").files[0],name=$("dName").value.trim();if(!name){alert("Podaj nazwę dokumentu.");return}let item={id:"D"+Date.now(),name,category:$("dCat").value,date:$("dDate").value,task:$("dTask").value,folderId:$("dFolder").value||"",fileName:f?.name||"",size:f?.size||0};try{if(f){let d=await reportFileDB(),fileId="document_"+item.id;await new Promise((resolve,reject)=>{let tx=d.transaction("files","readwrite");tx.objectStore("files").put({id:fileId,reportId:fileId,recordType:"document",name:f.name,type:f.type||"application/octet-stream",size:f.size,blob:f});tx.oncomplete=resolve;tx.onerror=()=>reject(tx.error)});item.fileId=fileId}db.documents.push(item);selectedDocFolder=item.folderId;closeModal();persist()}catch(e){console.error(e);alert("Nie udało się zapisać pliku. Sprawdź wolne miejsce w przeglądarce.")}} async function downloadDocumentFile(id){if(!id)return;let d=await reportFileDB(),f=await new Promise((resolve,reject)=>{let r=d.transaction("files","readonly").objectStore("files").get(id);r.onsuccess=()=>resolve(r.result);r.onerror=()=>reject(r.error)});if(!f){alert("Nie znaleziono pliku. Możliwe, że został usunięty z pamięci przeglądarki.");return}let u=URL.createObjectURL(f.blob),a=document.createElement("a");a.href=u;a.download=f.name;a.click();setTimeout(()=>URL.revokeObjectURL(u),1500)} function openUser(){modal("Dodaj użytkownika",`
`)} function saveUser(){if(!$("uName").value.trim()){alert("Podaj nazwę użytkownika.");return}db.users.push({name:$("uName").value,company:$("uCompany").value,role:$("uRole").value,status:$("uStatus").value});closeModal();persist()} function editUser(i){let x=db.users[i];modal("Edytuj użytkownika",`
`)} function deleteItem(k,i){if(confirm("Czy na pewno usunąć ten wpis?")){db[k].splice(i,1);persist()}} function saveSettings(){db.settings.site=$("siteName").value;db.settings.admin=$("adminName").value;$("userName").textContent=db.settings.admin;persist();alert("Zapisano ustawienia.")} function backup(){let a=document.createElement("a"),u=URL.createObjectURL(new Blob([JSON.stringify(db,null,2)],{type:"application/json"}));a.href=u;a.download="NIFCO_kopia_danych.json";a.click();URL.revokeObjectURL(u)} function restoreBackup(e){let f=e.target.files[0];if(!f)return;let r=new FileReader();r.onload=()=>{try{let x=JSON.parse(r.result);if(!x.tasks)throw 0;db={...db,...x};persist();alert("Kopia danych została wczytana.")}catch(_){alert("Nieprawidłowy plik kopii.")}};r.readAsText(f)} function exportCSV(){let rows=[["Obszar","Zadanie","Oferta","Zgłoszenie","Wartość oferty netto","Koszt rzeczywisty netto","Odchylenie","Status","Odbiór wykonania","Data odbioru","Uwagi","Priorytet","Termin"],...db.tasks.map(x=>[x.area,x.task,x.offer,x.ticket,x.net,x.actualCost,(+x.actualCost||0)-(+x.net||0),x.status,x.approval,x.approvalDate,x.approvalNote,x.priority,x.deadline])];let a=document.createElement("a"),u=URL.createObjectURL(new Blob(["\ufeff"+rows.map(r=>r.map(v=>'"'+String(v??"").replace(/"/g,'""')+'"').join(";")).join("\r\n")],{type:"text/csv;charset=utf-8"}));a.href=u;a.download="NIFCO_analiza_kosztow_i_odbiorow.csv";a.click();setTimeout(()=>URL.revokeObjectURL(u),1000)}["taskSearch","taskStatus","taskPriority","taskArea","reportSearch","docSearch","docCategory"].forEach(id=>$(id).addEventListener("input",render)); $("docSearch").addEventListener("input",renderDocuments);$("docCategory").addEventListener("change",renderDocuments); document.querySelectorAll("[data-tab]").forEach(b=>b.onclick=()=>{document.querySelectorAll("[data-tab]").forEach(x=>x.classList.toggle("active",x===b));renderInspections()}); $("globalSearch").addEventListener("keydown",e=>{if(e.key==="Enter"){go("tasks");$("taskSearch").value=e.target.value;renderTasks()}}); try{load();initFilters();if($("roundDate"))$("roundDate").value=new Date().toISOString().slice(0,10);render();}catch(e){console.error("Błąd uruchamiania panelu:",e);const root=document.querySelector("main")||document.body;root.insertAdjacentHTML("afterbegin",'
Panel napotkał błąd podczas uruchamiania.
Odśwież stronę. Jeśli problem się powtarza, otwórz plik w aktualnej wersji Chrome lub Edge.
');}if(Array.isArray(db.reports))db.reports.forEach(async r=>{if(r.id){try{let f=await loadReportAttachments(r.id);if(r.attachmentCount!==f.length){r.attachmentCount=f.length;persist()}}catch(e){console.warn("Nie udało się odczytać załączników raportu:",e)}}});