AUTOZAPIS AKTYWNY
OBSŁUGA FLOTY
Wersja użytkowa – przygotowana do pracy wieloużytkownikowej na komputerze i telefonie
Administrator lokalny Administrator

KIEROWCY

Lista kierowców, telefon, uprawnienia HDS i ważność badań

Alerty kierowców

Dwuklik – pokaż szczegóły
0

GRAFIK KIEROWCÓW

Stali kierowcy są domyślnie dostępni. Skoczkowie pojawiają się jako dostępni dopiero po potwierdzeniu.
Dostępny / potwierdzony Skoczek – niepotwierdzony Skoczek – brak zgłoszonej dostępności Niedostępny

POJAZDY

Dwuklik w pojazd otwiera parametry techniczne i dokumenty

Alerty pojazdów

Dwuklik – pokaż szczegóły
0

OBSŁUGA FLOTY

Expert Glass | Krysztal · zarządzanie pojazdami i rezerwacjami

Alerty floty

Dwuklik – pokaż szczegóły
0
Status rezerwacji: kontrola konfliktów i blokad Firebase aktywna.

MONTAŻE / MASZYNY

Moduł w przygotowaniu

Docelowo: obsługa montażystów oraz wynajmu maszyn.
`; const w=window.open('','_blank','width=1100,height=900'); if(!w){alert('Przeglądarka zablokowała okno raportu. Zezwól na wyskakujące okna.');return} w.document.open();w.document.write(html);w.document.close(); } function showModuleAlerts(kind){currentAlertReportKind=kind;let rows=kind==='drivers'?getDriverAlerts():(kind==='fleet'?[...getFleetAlerts(),...getVehicleAlerts()]:getVehicleAlerts());if(kind==='fleet'){const seen=new Set();rows=rows.filter(r=>{if(seen.has(r.text))return false;seen.add(r.text);return true})}moduleAlertsTitle.textContent=kind==='drivers'?'Alerty kierowców':(kind==='fleet'?'Alerty floty':'Alerty pojazdów');moduleAlertsBody.innerHTML=rows.length?rows.map(r=>`
⚠ ${r.text}
`).join(''):'
Brak alertów.
';openModal('moduleAlertsModal')} function ensureDriverExtensions(){(data.drivers||[]).forEach(d=>{if(d.phone===undefined)d.phone='';if(d.hdsPermission===undefined)d.hdsPermission=!!d.hdsAuthorized;if(d.driverType===undefined)d.driverType='staly';if(d.experienceNotes===undefined)d.experienceNotes='';if(d.availabilityNotes===undefined)d.availabilityNotes='';});} let driverScheduleWeekStart=getMonday(new Date()); function driverScheduleKey(driverId,date){ return `${driverId}__${isoDate(date)}`; } function getDriverScheduleStatus(d,date){ data.driverSchedule=data.driverSchedule||{}; const saved=data.driverSchedule[driverScheduleKey(d.id,date)]; if(saved)return saved; const day=date.getDay(); if(day===0||day===6)return 'unavailable'; if(d.driverType==='skoczek')return 'none'; return 'available'; } function cycleDriverScheduleStatus(d,date){ data.driverSchedule=data.driverSchedule||{}; const key=driverScheduleKey(d.id,date); const cur=getDriverScheduleStatus(d,date); if(d.driverType==='skoczek'){ // brak -> niepotwierdzony -> dostępny -> niedostępny -> brak if(cur==='none')data.driverSchedule[key]='pending'; else if(cur==='pending')data.driverSchedule[key]='confirmed'; else if(cur==='confirmed')data.driverSchedule[key]='unavailable'; else delete data.driverSchedule[key]; }else{ // stali: zielony dostępny -> czerwony niedostępny -> zielony if(cur==='available')data.driverSchedule[key]='unavailable'; else delete data.driverSchedule[key]; } try{ localStorage.setItem('fleetCalendar_driverSchedule_backup',JSON.stringify(data.driverSchedule||{})); }catch(e){} save(); renderDriverSchedule(); } function printDriverSchedule(){ const days=['PON','WT','ŚR','CZW','PT','SOB','ND']; const list=(data.drivers||[]).filter(d=>d.active!==false).slice().sort((a,b)=>{ const at=a.driverType==='skoczek'?1:0, bt=b.driverType==='skoczek'?1:0; return at-bt||String(a.name||'').localeCompare(String(b.name||''),'pl'); }); const escPrint=s=>String(s??'').replace(/[&<>"']/g,ch=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch])); let rows=''; list.forEach(d=>{ let cells=''; for(let i=0;i<7;i++){ const day=addDays(driverScheduleWeekStart,i); const st=getDriverScheduleStatus(d,day); let label='', cls=''; if(d.driverType==='skoczek'){ if(st==='confirmed'){label='✓DOSTĘPNY';cls='ok'} else if(st==='pending'){label='!NIEPOTW.';cls='pending'} else if(st==='unavailable'){label='✕NIEDOSTĘPNY';cls='no'} else {label='?BRAK POTW.';cls='neutral'} }else{ if(st==='unavailable'){label='✕NIEDOSTĘPNY';cls='no'} else {label='✓DOSTĘPNY';cls='ok'} } cells+=`${label}`; } const badge=d.driverType==='skoczek' ? 'SKOCZEK' : 'STAŁY'; const permsPrint=Array.isArray(d.permissions)?d.permissions.join(', '):String(d.permissions||''); rows+=`
${escPrint(d.name||'')}${badge}
tel. ${escPrint(d.phone||'—')}
upr.: ${escPrint(permsPrint||'—')}
${cells}`; }); let heads=''; for(let i=0;i<7;i++){ const day=addDays(driverScheduleWeekStart,i); heads+=`${days[i]}
${fmtDate(day)}`; } const from=fmtDate(driverScheduleWeekStart); const to=fmtDate(addDays(driverScheduleWeekStart,6)); const year=driverScheduleWeekStart.getFullYear(); const printHtml=` Grafik kierowców ${from}-${to}

TYGODNIOWY GRAFIK KIEROWCÓW

${from} – ${to}.${year}
${heads}${rows}
Kierowca
✓ Dostępny / potwierdzony ! Skoczek – niepotwierdzony ? Skoczek – brak potwierdzenia ✕ Niedostępny
Wydruk kolorowy – przy zapisie do PDF kolory statusów są zachowywane.
`; const w=window.open('','_blank','width=1200,height=800'); if(!w){ alert('Przeglądarka zablokowała okno wydruku. Zezwól na wyskakujące okna dla tego pliku.'); return; } w.document.open(); w.document.write(printHtml); w.document.close(); } function renderDriverSchedule(){ const el=document.getElementById('driverSchedule'); const label=document.getElementById('driverScheduleLabel'); if(!el||!label)return; const days=['PON','WT','ŚR','CZW','PT','SOB','ND']; label.textContent=`${fmtDate(driverScheduleWeekStart)} – ${fmtDate(addDays(driverScheduleWeekStart,6))}.${driverScheduleWeekStart.getFullYear()}`; el.innerHTML='
Kierowca
'; for(let i=0;i<7;i++){ const d=addDays(driverScheduleWeekStart,i); el.innerHTML+=`
${days[i]}
${fmtDate(d)}
`; } const list=(data.drivers||[]).filter(d=>d.active!==false).slice().sort((a,b)=>{ const at=a.driverType==='skoczek'?1:0, bt=b.driverType==='skoczek'?1:0; return at-bt||String(a.name||'').localeCompare(String(b.name||''),'pl'); }); list.forEach(d=>{ const nm=document.createElement('div'); nm.className='ds-name'; const permsScreen=Array.isArray(d.permissions)?d.permissions.join(', '):String(d.permissions||''); nm.innerHTML=`
${d.name||''}${d.driverType==='skoczek'?'SKOCZEK':'STAŁY'}
tel. ${d.phone||'—'}
upr.: ${permsScreen||'—'}
`; el.appendChild(nm); for(let i=0;i<7;i++){ const day=addDays(driverScheduleWeekStart,i); const st=getDriverScheduleStatus(d,day); const c=document.createElement('div'); if(d.driverType==='skoczek'){ c.className=`ds-cell ${st==='pending'?'jumper-pending':st==='confirmed'?'jumper-confirmed':st==='unavailable'?'staff-unavailable':'jumper-none'}`; c.textContent=st==='pending'?'NIEPOTWIERDZONY':st==='confirmed'?'DOSTĘPNY':st==='unavailable'?'NIEDOSTĘPNY':'BRAK POTWIERDZENIA'; c.title='Kliknij: brak → niepotwierdzony → dostępny → niedostępny → wyczyść'; }else{ c.className=`ds-cell ${st==='unavailable'?'staff-unavailable':'staff-available'}`; c.textContent=st==='unavailable'?'NIEDOSTĘPNY':'DOSTĘPNY'; c.title='Kliknij, aby zmienić dostępność'; } c.onclick=()=>cycleDriverScheduleStatus(d,day); el.appendChild(c); } }); } function renderDriversModule(){ ensureDriverExtensions(); renderDriverAlerts(); const grid=document.getElementById('driversGrid'); if(!grid)return; const list=data.drivers||[]; if(!list.length){ grid.innerHTML='
Brak danych kierowców. Pola pozostaną puste do czasu importu aktualnej listy.
'; renderDriverSchedule(); return; } grid.innerHTML=''; list.forEach(d=>{ const perms=d.permissions||d.uprawnienia||[]; const permHtml=Array.isArray(perms)?perms.map(x=>`${x}`).join(''):String(perms||''); const tile=document.createElement('div'); tile.className=`driver-tile ${d.driverType==='skoczek'?'jumper':''}`; tile.ondblclick=()=>openDriverEditor(d.id); tile.innerHTML=`

${d.name||''}

${d.driverType==='skoczek'?'KIEROWCA SKOCZEK':'KIEROWCA STAŁY'}
Telefon
${d.phone||'—'}
Uprawnienia
${permHtml||'—'}
Uprawnienia HDS
${d.hdsPermission||d.hdsAuthorized?'TAK':'NIE'}
Badania lekarskie
${d.medical||d.medicalExam||''}
Psychotesty
${d.psych||d.psychExam||''}
Prawo jazdy ważne do
${d.license||''}
${d.experienceNotes?`
Doświadczenie: ${d.experienceNotes}
`:''}${d.availabilityNotes?`
Dostępność: ${d.availabilityNotes}
`:''}`; grid.appendChild(tile); }); renderDriverSchedule(); } function reportValue(v){return (v===undefined||v===null||v==='')?'brak danych':String(v)} function reportInsuranceRows(v){ ensureVehicleExtensions(v); const rows=[]; ['OC','AC','NNW'].forEach(type=>{ const x=v.insurance?.[type]||{}; if(x.insurer||x.validFrom||x.validTo||x.policyNumber||x.coverage){ rows.push(`${type}${reportValue(x.insurer)}${reportValue(x.validFrom)} - ${reportValue(x.validTo)}${reportValue(x.policyNumber)}${reportValue(x.coverage)}`); } }); return rows.length?rows.join(''):`Brak danych ubezpieczeniowych.`; } function reportTireRows(v){ ensureVehicleExtensions(v); const s=v.tireData?.summer||{}, w=v.tireData?.winter||{}; const row=(name,t)=>`${name}${reportValue(t.manufacturer)}${reportValue(t.size)}${reportValue(t.supplier)}${reportValue(t.wear)}${reportValue(t.quantity)}`; return row('Letnie',s)+row('Zimowe',w); } function reportServiceRows(v){ ensureVehicleExtensions(v); const rows=Array.isArray(v.warrantyServices)?v.warrantyServices:[]; if(!rows.length)return `Brak wpisanego harmonogramu serwisowego.`; return rows.map((r,i)=>`${i+1}${reportValue(r.type||r['Rodzaj przeglądu']||r['Przebieg']||r.dueKm)}${reportValue(r.dueKm||r['Przebieg']||r.dueDate)}${reportValue(r.notes||r['Uwagi']||'')}`).join(''); } function vehicleWarrantyTitle(v){ if(normalizeReg(v.reg)==='KN9941K')return 'FORD PROTECT +2 LATA / LIMIT 160 000 KM'; return v.manufacturerWarranty||v.warrantyNotes||'GWARANCJA / SERWIS - DANE W BAZIE'; } function generateVehicleReport(id){ const v=vehicle(id); if(!v)return; ensureVehicleExtensions(v); const photo=v.photo?photoSrc(v.photo,v.reg):''; const title=`${v.brand||''} ${v.model||''}`.trim(); const genericRows=[ ['Nr rejestracyjny',v.reg],['Marka / model',title],['Nr flotowy',v.fleetNo], ['VIN',v.vin],['Rocznik',v.year],['Pierwsza rejestracja',v.firstRegistration], ['Właściciel / finansujący',v.registeredOwner],['Użytkownik',v.company], ['Kategoria / zabudowa',v.vehicleCategory||v.bodyType],['DMC',v.dmc], ['Masa własna / eksploatacyjna',v.operatingWeight],['Ładowność',v.payload], ['Silnik',v.engineCapacity],['Moc',v.enginePower],['Paliwo',v.fuelType], ['Skrzynia biegów',v.transmission],['Napęd',v.drive],['Liczba miejsc',v.seats], ['Wymiary całkowite',v.overallDimensions],['Przestrzeń ładunkowa',v.cargo], ['Zbiornik paliwa',v.fuelTank],['Klasa emisji',v.emissionClass], ['Przyczepa hamowana',v.towBraked],['Przyczepa niehamowana',v.towUnbraked], ['Hak',v.hook],['Aktualny przebieg',v.currentMileage?`${v.currentMileage} km`:''], ['Przegląd techniczny',v.inspection||v.technicalInspectionDate] ]; const baseRows=genericRows.map((r,i)=>`${r[0]}${reportValue(r[1])}${i%2===0&&genericRows[i+1]?`${genericRows[i+1][0]}${reportValue(genericRows[i+1][1])}`:''}`).filter((_,i)=>i%2===0).join(''); const fordExtra=normalizeReg(v.reg)==='KN9941K'?`

Serwis / gwarancja Ford

Kontakt serwisowy${reportValue(v.serviceContact)}
Ford Pomoc${reportValue(v.roadsideAssistance)}
Wybrana gwarancjaFord Protect +2 lata / limit 160 000 km - 9 100 zł brutto
Zasady serwisowe${reportValue(v.servicePolicy)}
Gwarancja perforacyjna${reportValue(v.corrosionWarranty)}
Ford Pass${reportValue(v.fordPassNote)}
`:''; const html=`Raport ${v.reg}

RAPORT POJAZDU - ${title.toUpperCase()}

${v.reg||''} | ${v.company||''}
${photo?``:'brak zdjęcia'}

Podstawowe dane pojazdu

${baseRows}

Opony

${reportTireRows(v)}
RodzajProducentRozmiarDostawcaZużycieIlość

Ubezpieczenia

${reportInsuranceRows(v)}
TypUbezpieczycielOkresPolisaZakres

PLAN SERWISOWY - ${v.reg||''}

${reportServiceRows(v)}
Lp.RodzajPrzebieg / terminUwagi
${fordExtra} ${v.manufacturerWarranty||v.warrantyNotes?`

Gwarancja

Informacja${reportValue(v.manufacturerWarranty||v.warrantyNotes)}
`:''} ${v.documentNotes?`

Uwagi z dokumentów

${v.documentNotes}
`:''}
Raport wygenerowany z bieżących danych OBSŁUGA FLOTY. Dane pól pustych oznaczono jako „brak danych”.
`; const w=window.open('','_blank','width=1100,height=900'); if(!w){alert('Przeglądarka zablokowała okno raportu. Zezwól na wyskakujące okna.');return} w.document.open();w.document.write(html);w.document.close(); } const NAVIFLEET_URL_STORAGE='OBSLUGA_FLOTY_NAVIFLEET_SOURCE_URL'; const NAVIFLEET_DEFAULT_URL='https://gps.navifleet.pl/positions/data/549e21c1e87c651bfd2b53b251d7af18'; let navifleetLastRows=[]; function nfNormalizeReg(v){ let s=String(v||'').toUpperCase().trim(); // NaviFleet poprzedza rejestracje numerem flotowym, np. (04) AA 566HV. s=s.replace(/^\s*\(\s*\d+\s*\)\s*/, ''); return s.replace(/[^A-Z0-9]/g,''); } function nfNumber(v){ if(v===null||v===undefined||v==='')return null; if(typeof v==='number')return Number.isFinite(v)?v:null; const s=String(v).replace(/\s/g,'').replace(',','.'); const m=s.match(/-?\d+(?:\.\d+)?/); return m?Number(m[0]):null; } function nfExtractVehicles(payload){ if(Array.isArray(payload))return payload; if(Array.isArray(payload?.vehicles))return payload.vehicles; if(Array.isArray(payload?.data))return payload.data; if(Array.isArray(payload?.items))return payload.items; if(payload && typeof payload==='object'){ const vals=Object.values(payload); if(vals.length && vals.every(x=>x&&typeof x==='object'))return vals; } return []; } async function nfLoadPayload(){ const pasted=document.getElementById('navifleetJsonPaste').value.trim(); if(pasted){ try{return JSON.parse(pasted)}catch(e){throw new Error('Wklejony JSON jest nieprawidłowy.')} } const url=document.getElementById('navifleetSourceUrl').value.trim()||NAVIFLEET_DEFAULT_URL; document.getElementById('navifleetSourceUrl').value=url; localStorage.setItem(NAVIFLEET_URL_STORAGE,url); let res; try{res=await fetch(url,{cache:'no-store'})} catch(e){throw new Error('Przeglądarka zablokowała pobranie danych. Otwórz link NaviFleet w nowej karcie, skopiuj JSON i wklej go w pole poniżej.')} if(!res.ok)throw new Error('NaviFleet zwrócił HTTP '+res.status+'.'); try{return await res.json()}catch(e){throw new Error('Odpowiedź NaviFleet nie jest prawidłowym JSON-em.')} } function nfBuildRows(payload){ const arr=nfExtractVehicles(payload); if(!arr.length)throw new Error('Nie znaleziono listy pojazdów w odpowiedzi NaviFleet.'); const appByReg=new Map((data.vehicles||[]).map(v=>[nfNormalizeReg(v.reg),v])); const rows=[]; for(const n of arr){ // NaviFleet /positions/data zwraca dane pojazdu wewnątrz obiektu `device`. // Obsługujemy jednocześnie starszy/płaski format, aby zachować kompatybilność. const dev=(n&&typeof n.device==='object'&&n.device)?n.device:n; const regRaw=dev.license ?? dev.registration ?? dev.reg ?? dev.plate ?? dev.name ?? n.license ?? n.registration ?? n.reg ?? n.plate ?? n.name ?? ''; const reg=nfNormalizeReg(regRaw); if(!reg)continue; const app=appByReg.get(reg); const mileage=nfNumber(dev.mileage ?? n.mileage); const nfData=Array.isArray(n?.data)?n.data[0]:n?.data; const totalDistance=nfNumber(nfData?.total_distance ?? dev.total_distance ?? n.total_distance); const current=nfNumber(app?.currentMileage); let status='Brak pojazdu w aplikacji'; let cls='navifleet-muted'; if(app){ if(mileage===null){status='Brak mileage w NaviFleet';cls='navifleet-warn'} else if(current===null){status='Do uzupełnienia';cls='navifleet-ok'} else if(mileage>current){status='Do aktualizacji';cls='navifleet-ok'} else if(mileage===current){status='Bez zmian';cls='navifleet-muted'} else {status='NaviFleet ma niższy przebieg – pomijam';cls='navifleet-bad'} } rows.push({regRaw:String(regRaw),reg,app,mileage,totalDistance,current,status,cls}); } return rows; } function nfRenderPreview(rows){ navifleetLastRows=rows; const matched=rows.filter(r=>r.app).length; const updates=rows.filter(r=>r.app&&r.mileage!==null&&(r.current===null||r.mileage>r.current)).length; document.getElementById('navifleetStatus').textContent=`NaviFleet: ${rows.length} pozycji • dopasowano ${matched} • do aktualizacji ${updates}`; document.getElementById('navifleetPreview').innerHTML=`
${rows.map(r=>``).join('')}
`; } async function nfPreview(){ const status=document.getElementById('navifleetStatus'); status.textContent='Pobieranie danych NaviFleet…'; const payload=await nfLoadPayload(); const rows=nfBuildRows(payload); nfRenderPreview(rows); return rows; } async function nfApply(){ try{ const rows=await nfPreview(); let changed=0; const now=new Date().toISOString(); rows.forEach(r=>{ if(!r.app||r.mileage===null)return; if(r.current!==null && r.mileage<=r.current)return; ensureVehicleExtensions(r.app); const km=Math.round(r.mileage); r.app.currentMileage=String(km); r.app.mileageHistory=Array.isArray(r.app.mileageHistory)?r.app.mileageHistory:[]; const last=r.app.mileageHistory[r.app.mileageHistory.length-1]; if(!last || Number(last.mileage)!==km){ r.app.mileageHistory.push({date:now.slice(0,10),mileage:km,source:'NaviFleet',syncedAt:now}); } r.app.navifleetLastSync=now; changed++; }); if(changed){ save(); try{if(window.FleetCloud?.configured && cloudReady){await window.FleetCloud.pushState(data,currentUserRole);}}catch(e){console.warn('NaviFleet: natychmiastowy zapis Firebase:',e)} nfRenderPreview(nfBuildRows(await nfLoadPayload())); document.getElementById('navifleetStatus').textContent=`Zaktualizowano ${changed} przebiegów. Dane zapisano w aplikacji i zsynchronizowano ze wspólną bazą Firebase.`; }else{ document.getElementById('navifleetStatus').textContent='Brak przebiegów wymagających aktualizacji.'; } }catch(e){ console.error('NaviFleet sync:',e); document.getElementById('navifleetStatus').textContent='Błąd: '+(e?.message||e); } } function openNavifleetSync(){ document.getElementById('navifleetSourceUrl').value=localStorage.getItem(NAVIFLEET_URL_STORAGE)||NAVIFLEET_DEFAULT_URL; document.getElementById('navifleetJsonPaste').value=''; document.getElementById('navifleetPreview').innerHTML=''; document.getElementById('navifleetStatus').textContent='Źródło NaviFleet jest zapisane na stałe. Dopasowanie odbywa się po numerze rejestracyjnym; aktualizujemy tylko przebieg i nigdy nie obniżamy istniejącej wartości.'; openModal('navifleetModal'); } function renderVehiclesModule(){renderVehicleAlerts();const grid=document.getElementById('vehiclesGrid');if(!grid)return;grid.innerHTML='';sortVehicles();data.vehicles.forEach(v=>{const isExpert=String(v.company||'').toUpperCase()==='EXPERT GLASS'||(!v.company&&String(v.bodyType||'').trim().toLowerCase()==='ciężarówka otwarta');const tile=document.createElement('div');tile.className=`fleet-tile ${vehicleClass(v)} ${bodyShapeClass(v)}`;tile.ondblclick=()=>showDetails(v.id);tile.innerHTML=`${tileShapeSvg(v)}

${v.reg||''}

${v.brand||''} ${v.model||''}
Nr flotowy: ${v.fleetNo||''}
${v.bodyType||''}${v.payload?`
Ładowność: ${v.payload}`:''}
${v.photo?`${v.reg||''}`:''}${canEditVehicles()?``:''}
`;grid.appendChild(tile)})} function renderModules(){renderDriversModule();renderVehiclesModule();renderDriverSchedule()} function switchModule(id){document.querySelectorAll('.module').forEach(x=>x.classList.toggle('active',x.id===id));document.querySelectorAll('.module-btn').forEach(x=>x.classList.toggle('active',x.dataset.module===id));if(id==='driversModule')renderDriversModule();if(id==='vehiclesModule')renderVehiclesModule();if(id==='calendarModule')renderAll()} document.getElementById('driverAlertBox').ondblclick=()=>showModuleAlerts('drivers'); document.getElementById('vehicleAlertBox').ondblclick=()=>showModuleAlerts('vehicles'); document.getElementById('fleetAlertBox').ondblclick=()=>showModuleAlerts('fleet'); document.getElementById('printModuleAlertsBtn').onclick=()=>printAlertReport(currentAlertReportKind==='drivers'?'drivers':'fleet'); document.getElementById('printFleetAlertsBtn').onclick=()=>printAlertReport('fleet'); document.querySelectorAll('.module-btn').forEach(b=>b.addEventListener('click',()=>switchModule(b.dataset.module))); document.getElementById('navifleetSyncBtn').onclick=openNavifleetSync; document.getElementById('navifleetTestBtn').onclick=async()=>{ try{ document.getElementById('navifleetStatus').textContent='Testowanie połączenia…'; const payload=await nfLoadPayload(); const rows=nfBuildRows(payload); document.getElementById('navifleetStatus').textContent=`Połączenie OK. Odczytano ${rows.length} pojazdów.`; }catch(e){ document.getElementById('navifleetStatus').textContent='Błąd: '+(e?.message||e); } }; document.getElementById('navifleetPreviewBtn').onclick=async()=>{ try{await nfPreview()}catch(e){document.getElementById('navifleetStatus').textContent='Błąd: '+(e?.message||e)} }; document.getElementById('navifleetApplyBtn').onclick=nfApply; function renderAll(){sortVehicles();populateSelects();renderWeek();renderSummary();renderAlerts();renderCalendar();renderModules()} function openModal(id){const m=document.getElementById(id);m.classList.add('open');if(id!=='vehicleModal'&&id!=='driverModal')setTimeout(()=>{if(m.classList.contains('open'))restoreModalDraft(m)},0)}function closeModals(){document.querySelectorAll('.modal').forEach(m=>m.classList.remove('open'));insuranceEditorVehicleId=null} document.querySelectorAll('[data-close]').forEach(b=>b.onclick=closeModals);document.querySelectorAll('.modal').forEach(m=>m.addEventListener('click',e=>{if(e.target===m)closeModals()})); function backToVehicleDetails(){ const id=activeVehicleId; try{ const im=document.getElementById('insuranceModal'); if(im?.classList.contains('open') && insuranceEditorVehicleId){ const v=vehicle(insuranceEditorVehicleId); if(v){syncInsuranceEditorToVehicle(v);quietPersist();} } }catch(e){console.warn('Synchronizacja danych przed powrotem do karty pojazdu:',e)} closeModals(); insuranceEditorVehicleId=null; if(id)setTimeout(()=>showDetails(id),0); } document.querySelectorAll('.back-to-details').forEach(b=>b.onclick=backToVehicleDetails); document.getElementById('backFromDetails').onclick=()=>{closeModals();switchModule('vehicles')}; document.getElementById('backFromVehicleEdit').onclick=()=>{ const id=editingVehicleId||activeVehicleId; closeModals(); if(id)setTimeout(()=>showDetails(id),0); }; document.getElementById('editVehicleFromDetails').onclick=(e)=>{ e.preventDefault(); e.stopPropagation(); try{ const id=activeVehicleId; if(!id){alert('Nie udało się ustalić pojazdu do edycji. Zamknij szczegóły i otwórz pojazd ponownie.');return} closeModals(); setTimeout(()=>{openVehicleEditor(id)},50); }catch(err){ console.error('Błąd przycisku Edytuj pojazd w szczegółach',err); alert('Nie udało się otworzyć edycji pojazdu: '+(err?.message||err)); } }; function localDate(d){const z=new Date(d.getTime()-d.getTimezoneOffset()*60000);return z.toISOString().slice(0,10)} function fillHourSelects(){['bStartHour','bEndHour','xStartHour','xEndHour'].forEach(id=>{const el=document.getElementById(id);el.innerHTML='';for(let h=0;h<24;h++){const o=document.createElement('option');o.value=String(h).padStart(2,'0');o.textContent=String(h).padStart(2,'0')+':00';el.appendChild(o)}})} function setDateHour(dateId,hourId,d){document.getElementById(dateId).value=localDate(d);document.getElementById(hourId).value=String(d.getHours()).padStart(2,'0')} function composeDateTime(dateId,hourId){const d=document.getElementById(dateId).value,h=document.getElementById(hourId).value;return d&&h!==''?`${d}T${h}:00`:''} fillHourSelects(); document.getElementById('addBtn').onclick=()=>{document.getElementById('bError').textContent='';let st=new Date();st.setMinutes(0,0,0);let en=new Date(st);en.setHours(en.getHours()+2);setDateHour('bStartDate','bStartHour',st);setDateHour('bEndDate','bEndHour',en);bOwner.value='';bPurpose.value='Dostawa towaru';bCountry.value='PL';bNotes.value='';openModal('bookingModal')} document.getElementById('blockBtn').onclick=()=>{document.getElementById('xError').textContent='';let st=new Date();st.setMinutes(0,0,0);let en=new Date(st);en.setHours(en.getHours()+3);setDateHour('xStartDate','xStartHour',st);setDateHour('xEndDate','xEndHour',en);xNotes.value='';openModal('blockModal')} function conflict(vehicleId,start,end){return data.events.find(e=>e.vehicleId===vehicleId&&e.status!=='cancelled'&&overlap(e.start,e.end,start,end))} function countryLabel(code){return({PL:'Polska',CZ:'Czechy',SK:'Słowacja',AT:'Austria',DE:'Niemcy'})[code]||code} function vignetteWarning(v,country,start,end){if(country==='PL')return '';const label=countryLabel(country);const val=(v.vignettes||{})[country];if(!val||String(val).trim()==='-' )return `ALERT: BRAK WAŻNEJ WINIETY – ${label}.\nW danych pojazdu ${v.reg} nie ma ważnej winiety / opłaty drogowej dla tego kraju.`;if(String(val).trim().toUpperCase()==='BOX'){return country==='CZ'?'BOX (sprawdź ważność BOXa - Czechy max 12 miesięcy od ostatniego wjazdu)':'BOX – sprawdź ważność urządzenia przed wyjazdem.'}if(!isDateValue(val))return `ALERT: SPRAWDŹ OPŁATĘ DROGOWĄ – ${label}.\nWartość w danych pojazdu: ${val}`;const startDay=new Date(start);startDay.setHours(0,0,0,0);const endDay=new Date(end);endDay.setHours(0,0,0,0);const valid=new Date(val+'T23:59:59');if(valid{const dt=v.insurance[type]&&v.insurance[type].validTo;if(!isDateValue(dt))return;const expiry=new Date(dt+'T23:59:59');if(expiry{const obj={id:'e'+Date.now(),vehicleId:bVehicle.value,status:bStatus.value,start:composeDateTime('bStartDate','bStartHour'),end:composeDateTime('bEndDate','bEndHour'),driverId:bDriver.value,owner:bOwner.value,purpose:bPurpose.value,country:bCountry.value,notes:bNotes.value,createdBy:(currentUserProfile&¤tUserProfile.uid)||'',createdByName:(currentUserProfile&¤tUserProfile.displayName)||''};if(!obj.start||!obj.end||new Date(obj.end)<=new Date(obj.start)){bError.textContent='Podaj prawidłowy przedział czasu.';return}const c=conflict(obj.vehicleId,obj.start,obj.end);if(c){const d=driver(c.driverId);bError.textContent=`Konflikt: pojazd jest już zajęty ${fmtDT(c.start)} – ${fmtDT(c.end)}${d?' przez '+d.name:''}.`;return}const v=vehicle(obj.vehicleId);const warns=[];const vw=vignetteWarning(v,obj.country,obj.start,obj.end);if(vw)warns.push(vw);const iw=insuranceBookingWarning(v,obj.start,obj.end);if(iw)warns.push(iw);if(warns.length){const msg=warns.join('\n\n');bError.textContent=msg;if(!window.confirm(msg+'\n\nCzy mimo to zatwierdzić rezerwację?'))return}saveBooking.disabled=true;bError.textContent='Sprawdzanie dostępności w bazie…';try{if(cloudReady&&window.FleetCloud?.createReservationAtomic)await window.FleetCloud.createReservationAtomic(obj);data.events.push(obj);save();bError.textContent='';closeModals()}catch(e){bError.textContent=e?.code==='fleet/reservation-conflict'?'Konflikt: pojazd został już zarezerwowany na innym urządzeniu.':('Nie udało się zapisać rezerwacji: '+(e.message||e))}finally{saveBooking.disabled=false}} document.getElementById('saveBlock').onclick=async()=>{const obj={id:'e'+Date.now(),vehicleId:xVehicle.value,status:xStatus.value,start:composeDateTime('xStartDate','xStartHour'),end:composeDateTime('xEndDate','xEndHour'),driverId:'',owner:'',purpose:statusLabel(xStatus.value),notes:xNotes.value,createdBy:(currentUserProfile&¤tUserProfile.uid)||'',createdByName:(currentUserProfile&¤tUserProfile.displayName)||''};if(!obj.start||!obj.end||new Date(obj.end)<=new Date(obj.start)){xError.textContent='Podaj prawidłowy przedział czasu.';return}const c=conflict(obj.vehicleId,obj.start,obj.end);if(c){xError.textContent=`Nie można zablokować – istnieje już wpis ${fmtDT(c.start)} – ${fmtDT(c.end)}.`;return}saveBlock.disabled=true;xError.textContent='Sprawdzanie dostępności w bazie…';try{if(cloudReady&&window.FleetCloud?.createReservationAtomic)await window.FleetCloud.createReservationAtomic(obj);data.events.push(obj);save();xError.textContent='';closeModals()}catch(e){xError.textContent=e?.code==='fleet/reservation-conflict'?'Nie można zablokować pojazdu — termin został właśnie zajęty na innym urządzeniu.':('Nie udało się zapisać blokady: '+(e.message||e))}finally{saveBlock.disabled=false}} let activeVehicleId=''; const VEHICLE_AUTHORITATIVE_20260922={ 'AA566HV':{firstRegistration:'2024-03-27',engineCapacity:'1968 cm³',oc:{insurer:'Wustermot',validTo:'2027-04-23'},summer:{manufacturer:'Giti',size:'235/65 R16C',quantity:'4'}}, 'AA681HV':{firstRegistration:'2024-03-27',engineCapacity:'1968 cm³',oc:{insurer:'Wustermot',validTo:'2027-04-23'},summer:{manufacturer:'Giti',size:'235/65 R16C',quantity:'4'}}, 'ZA434JS':{engineCapacity:'1968 cm³',oc:{validTo:'2027-04-23'},summer:{manufacturer:'Giti (2 szt.) + Michelin (2 szt.)',size:'235/65 R16C',quantity:'4'}}, 'ZA926JX':{oc:{insurer:'Wustermot',validTo:'2027-04-23'},summer:{manufacturer:'Kleber (2 szt.) + Michelin (2 szt.)',size:'215/75 R16C',quantity:'4'}}, 'ZA185IN':{firstRegistration:'2025-09-02',engineCapacity:'1968 cm³',summer:{manufacturer:'Kormoran (2 szt.) + Sailum (2 szt.)',size:'235/65 R16C',quantity:'4'}}, 'AA638VN':{firstRegistration:'2025-09-02',engineCapacity:'1968 cm³',summer:{manufacturer:'Kormoran',size:'235/65 R16C',quantity:'4'}}, 'AA498ZX':{firstRegistration:'2025-10-01',engineCapacity:'2184 cm³',summer:{manufacturer:'NEXEN',size:'215/75 R16C',quantity:'4'}}, 'AA515ZX':{firstRegistration:'2025-10-30',engineCapacity:'2184 cm³',summer:{manufacturer:'NEXEN',size:'215/75 R16C',quantity:'4'}}, 'AB569AS':{firstRegistration:'2025-10-30',engineCapacity:'2184 cm³',summer:{manufacturer:'NEXEN',size:'215/75 R16C',quantity:'4'}}, 'KN9941K':{summer:{manufacturer:'GoodYear',size:'195/75 R16C',quantity:'4'}}, 'KN2190K':{inspection:'2027-11-10',summer:{manufacturer:'Continental',size:'245/70 R17.5',quantity:'6'}}, 'KN8417J':{inspection:'2027-09-16',currentMileage:'155639',summer:{manufacturer:'Continental',size:'265/70 R17.5',quantity:'6'}}, 'KN5390G':{summer:{manufacturer:'KUMHO (4 szt.) + Sailum (2 szt.)',size:'285/70 R19.5',quantity:'6'}}, 'KN9275J':{inspection:'2027-09-16',summer:{manufacturer:'GoodYear (4 szt.) + AEOLUS (2 szt.)',size:'315/80 R22.5',quantity:'6'}} }; function enforceVehicleAuthoritative20260922(v){ if(!v)return false; const u=VEHICLE_AUTHORITATIVE_20260922[normalizeReg(v.reg)]; if(!u)return false; let ch=false; if(!v.tireData)v.tireData={summer:{},winter:{}}; if(!v.tireData.summer)v.tireData.summer={}; if(!v.tireData.winter)v.tireData.winter={}; if(!v.insurance)v.insurance={}; if(!v.insurance.OC)v.insurance.OC={}; if(u.firstRegistration!==undefined&&v.firstRegistration!==u.firstRegistration){v.firstRegistration=u.firstRegistration;ch=true} if(u.engineCapacity!==undefined&&v.engineCapacity!==u.engineCapacity){v.engineCapacity=u.engineCapacity;ch=true} if(u.currentMileage!==undefined&&String(v.currentMileage||'')!==String(u.currentMileage)){v.currentMileage=u.currentMileage;ch=true} if(u.inspection!==undefined&&!v.inspection&&!v.technicalInspectionDate){v.inspection=u.inspection;v.technicalInspectionDate=u.inspection;ch=true} if(u.oc){ if(u.oc.insurer!==undefined&&v.insurance.OC.insurer!==u.oc.insurer){v.insurance.OC.insurer=u.oc.insurer;ch=true} if(u.oc.validTo!==undefined&&v.insurance.OC.validTo!==u.oc.validTo){v.insurance.OC.validTo=u.oc.validTo;ch=true} } if(u.summer){ const d={manufacturer:u.summer.manufacturer,size:u.summer.size,supplier:'',wear:'',quantity:u.summer.quantity}; const c=v.tireData.summer||{}; if(c.manufacturer!==d.manufacturer||c.size!==d.size||String(c.quantity||'')!==String(d.quantity)||c.supplier!==''||c.wear!==''){ v.tireData.summer=d;ch=true; } if(v.tires!==u.summer.size){v.tires=u.summer.size;ch=true} } return ch; } function enforceAllVehicleAuthoritative20260922(){ let ch=false;(data.vehicles||[]).forEach(v=>{if(enforceVehicleAuthoritative20260922(v))ch=true});return ch; } function cleanupInsuranceContaminationV10(){ data.meta=data.meta||{}; let changed=false; (data.vehicles||[]).forEach(v=>{ ensureVehicleExtensions(v); const fleet=String(v.fleetNo??'').trim(); const reg=normalizeReg(v.reg); // Zachowujemy wyłącznie dane OC, które użytkownik podał jednoznacznie. const known={ 'AA566HV':{insurer:'Wustermot',validTo:'2027-04-23'}, 'AA681HV':{insurer:'Wustermot',validTo:'2027-04-23'}, 'ZA434JS':{insurer:'',validTo:'2027-04-23'}, 'ZA926JX':{insurer:'Wustermot',validTo:'2027-04-23'} }; if(known[reg]){ v.insurance.OC={ ...(v.insurance.OC||{}), insurer:known[reg].insurer, validTo:known[reg].validTo }; changed=true; } // Dokładny fałszywy zestaw, który rozlał się po pojazdach. ['OC','AC','NNW'].forEach(type=>{ const x=v.insurance?.[type]||{}; const insurer=String(x.insurer||'').trim().toLowerCase(); const policy=String(x.policyNumber||'').trim(); const validTo=String(x.validTo||'').trim(); const bad = (insurer==='allianz' && policy==='888' && (validTo==='2027-10-01' || validTo==='2027-09-01')) || (policy==='888' && (validTo==='2027-10-01' || validTo==='2027-09-01')); if(bad){ // Nie kasujemy potwierdzonego OC z listy known. if(type==='OC' && known[reg]){ v.insurance.OC.policyNumber=''; v.insurance.OC.phone=''; v.insurance.OC.validFrom=''; }else{ v.insurance[type]={insurer:'',policyNumber:'',phone:'',validFrom:'',validTo:'',coverage:''}; } changed=true; } }); }); return changed; } function cleanupCopiedInsuranceV9(){ const seededFleet=new Set(['4','6','7','9']); let changed=false; (data.vehicles||[]).forEach(v=>{ ensureVehicleExtensions(v); if(seededFleet.has(String(v.fleetNo??'').trim()))return; const oc=v.insurance?.OC||{}, ac=v.insurance?.AC||{}; const copiedOC=String(oc.insurer||'').toLowerCase()==='allianz' && String(oc.policyNumber||'')==='888' && String(oc.validTo||'')==='2027-10-01'; const copiedAC=String(ac.insurer||'').toLowerCase()==='allianz' && String(ac.policyNumber||'')==='888' && String(ac.validTo||'')==='2027-09-01'; if(copiedOC){v.insurance.OC={insurer:'',policyNumber:'',phone:'',validFrom:'',validTo:'',coverage:''};changed=true;} if(copiedAC){v.insurance.AC={insurer:'',policyNumber:'',phone:'',validFrom:'',validTo:'',coverage:''};changed=true;} }); return changed; } function enforceInsuranceAuthoritativeV8(){ const byFleet={ '4':{insurer:'Wustermot',validTo:'2027-04-23'}, '6':{insurer:'Wustermot',validTo:'2027-04-23'}, '7':{validTo:'2027-04-23'}, '9':{insurer:'Wustermot',validTo:'2027-04-23'} }; let changed=false; (data.vehicles||[]).forEach(v=>{ ensureVehicleExtensions(v); const fix=byFleet[String(v.fleetNo??'').trim()]; if(!fix)return; v.insurance=v.insurance||{};v.insurance.OC=v.insurance.OC||{}; Object.entries(fix).forEach(([k,val])=>{ if(!String(v.insurance.OC[k]||'').trim()){v.insurance.OC[k]=val;changed=true;} }); }); return changed; } function ensureVehicleExtensions(v){ if(!v)return; if(v.currentMileage===undefined)v.currentMileage=''; if(!v.assignedEquipment)v.assignedEquipment={straps:false,mats:false,separators:false,tools:false}; if(!v.tireData)v.tireData={summer:{manufacturer:'',size:v.tires||'',supplier:'',wear:'',quantity:''},winter:{manufacturer:'',size:'',supplier:'',wear:'',quantity:''}};['summer','winter'].forEach(k=>{v.tireData[k]=v.tireData[k]||{};if(v.tireData[k].manufacturer===undefined)v.tireData[k].manufacturer='';if(v.tireData[k].size===undefined)v.tireData[k].size='';if(v.tireData[k].supplier===undefined)v.tireData[k].supplier='';if(v.tireData[k].wear===undefined)v.tireData[k].wear='';if(v.tireData[k].quantity===undefined)v.tireData[k].quantity=''}); if(!v.insurance)v.insurance={OC:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''},AC:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''},NNW:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''}};['OC','AC','NNW'].forEach(k=>{v.insurance[k]=v.insurance[k]||{};if(v.insurance[k].insurer===undefined)v.insurance[k].insurer='';if(v.insurance[k].phone===undefined)v.insurance[k].phone='';if(v.insurance[k].validFrom===undefined)v.insurance[k].validFrom='';if(v.insurance[k].validTo===undefined)v.insurance[k].validTo='';if(v.insurance[k].policyNumber===undefined)v.insurance[k].policyNumber='';if(v.insurance[k].coverage===undefined)v.insurance[k].coverage=''}); if(!v.warrantyServices)v.warrantyServices=[]; if(!v.serviceHistory)v.serviceHistory=[];if(!v.mileageHistory)v.mileageHistory=[];if(!v.documents)v.documents=[];if(!v.operatingCosts)v.operatingCosts=[]; } function equipHtml(v){ensureVehicleExtensions(v);const names=[['straps','Pasy transportowe'],['mats','Maty'],['separators','Przekładki'],['tools','Narzędzia']];const on=names.filter(([k])=>v.assignedEquipment[k]).map(([,n])=>`${n}`);return on.length?`
${on.join('')}
`:'—'} function vehicleUsageStats(v){const now=new Date(),from=new Date(now.getFullYear(),now.getMonth(),1),to=new Date(now.getFullYear(),now.getMonth()+1,1);const ev=(data.events||[]).filter(e=>e.vehicleId===v.id&&e.status!=='cancelled'&&new Date(e.end)>from&&new Date(e.start){const s=Math.max(new Date(e.start).getTime(),from.getTime()),en=Math.min(new Date(e.end).getTime(),to.getTime());hours+=Math.max(0,en-s)/3600000});return {count:ev.length,hours:Math.round(hours*10)/10}} function serviceCostSum(v){ensureVehicleExtensions(v);let total=0;(v.serviceHistory||[]).forEach(r=>{Object.entries(r||{}).forEach(([k,val])=>{const h=normalizeHeader(k);if(h.includes('koszt')||h.includes('kwota')){const n=parseFloat(String(val||'').replace(/\s/g,'').replace(',','.').replace(/[^0-9.-]/g,''));if(Number.isFinite(n))total+=n}})});return Math.round(total*100)/100} function vehicleOpsSummary(v){const u=vehicleUsageStats(v),cost=serviceCostSum(v),failures=(data.events||[]).filter(e=>e.vehicleId===v.id&&e.status==='failure').length;return `

Eksploatacja i wykorzystanie

Rezerwacje w bieżącym miesiącu
${u.count}
Czas zajęcia w bieżącym miesiącu
${u.hours} h
Zarejestrowane awarie
${failures}
Koszty z historii serwisów
${cost?cost.toLocaleString('pl-PL',{minimumFractionDigits:2,maximumFractionDigits:2})+' zł':'—'}
Wpisy przebiegu
${(v.mileageHistory||[]).length}
Dokumenty pojazdu
${(v.documents||[]).length||'—'}
`} function uiDate(value){ const s=String(value||'').trim(); const m=s.match(/^(\d{4})-(\d{2})-(\d{2})$/); return m?`${m[3]}.${m[2]}.${m[1]}`:(s||'—'); } function parseDateOnly(value){ const s=String(value||'').trim(); const m=s.match(/^(\d{4})-(\d{2})-(\d{2})$/); if(!m)return null; return new Date(Number(m[1]),Number(m[2])-1,Number(m[3]),12,0,0,0); } function expiryStatus(value){ const d=parseDateOnly(value); if(!d)return {cls:'gray',label:'brak daty'}; const now=new Date(); const today=new Date(now.getFullYear(),now.getMonth(),now.getDate(),12,0,0,0); const diff=Math.ceil((d-today)/(24*60*60*1000)); if(diff<0)return {cls:'red',label:'termin przekroczony'}; if(diff<=30)return {cls:'yellow',label:`${diff} dni do końca`}; return {cls:'green',label:'ważne'}; } function statusValueHtml(value){ const st=expiryStatus(value); return `
${uiDate(value)}
`; } let insuranceEditorVehicleId=null; function syncInsuranceEditorToVehicle(v){ if(!v)return; ensureVehicleExtensions(v); const modal=document.getElementById('insuranceModal'); if(!modal?.classList.contains('open'))return; if(String(insuranceEditorVehicleId||'')!==String(v.id||''))return; const map=[ ['OC','insOCName','insOCPolicy','insOCPhone','insOCFrom','insOCValid'], ['AC','insACName','insACPolicy','insACPhone','insACFrom','insACValid'], ['NNW','insNNWName','insNNWPolicy','insNNWPhone','insNNWFrom','insNNWValid'] ]; map.forEach(([type,n,p,ph,fr,to])=>{ const nameEl=document.getElementById(n), policyEl=document.getElementById(p), phoneEl=document.getElementById(ph), fromEl=document.getElementById(fr), toEl=document.getElementById(to); if(!toEl)return; v.insurance[type]={ ...v.insurance[type], insurer:nameEl?.value?.trim?.()||'', policyNumber:policyEl?.value?.trim?.()||'', phone:phoneEl?.value?.trim?.()||'', validFrom:fromEl?.value||'', validTo:toEl?.value||'' }; }); } function ocValidTo(v){ ensureVehicleExtensions(v); /* V8: funkcja tylko ODCZYTUJE dane pojazdu. Nie wolno tu kopiować pól z ostatnio otwartego formularza ubezpieczenia do innego samochodu. */ return String(v?.insurance?.OC?.validTo||'').trim(); } function vignetteStatus(value){ const s=String(value||'').trim(); if(!s)return {cls:'orange',label:'brak danych',display:'brak danych'}; if(s.toUpperCase()==='BOX')return {cls:'green',label:'BOX / opłata aktywna',display:'BOX'}; const base=expiryStatus(s); return {cls:base.cls==='gray'?'orange':base.cls,label:base.label,display:uiDate(s)}; } function vignetteUi(v){ const countries=['SK','CZ','AT','DE']; const source=v?.vignettes||{}; const present=countries.filter(c=>String(source[c]||'').trim()); if(!present.length)return {main:'Winiety',sub:'',lamp:''}; const statuses=present.map(c=>vignetteStatus(source[c]).cls); let overall='green'; if(statuses.includes('red'))overall='red'; else if(statuses.includes('yellow'))overall='yellow'; else if(statuses.includes('orange'))overall='orange'; return { main:'Winiety', sub:'', lamp:`` }; } function techBox(label,value){ return `
${label}
${value||'—'}
`; } function showTechnicalInsideDetails(id){ const v=vehicle(id);if(!v)return; activeVehicleId=id;ensureVehicleExtensions(v); const d=driver(v.driverId); const vins=['SK','CZ','AT','DE'].map(k=>{const st=vignetteStatus(v?.vignettes?.[k]);return `${k}: ${st.display}`}).join('
'); const body=document.getElementById('dBody'); document.getElementById('dTitle').textContent=`SZCZEGÓŁY – ${v.reg}`; body.innerHTML=`

Dane techniczne pojazdu

Dane techniczne

${techBox('Nr rejestracyjny',v.reg)} ${techBox('Nr flotowy',v.fleetNo)} ${techBox('Marka / model',`${v.brand||''} ${v.model||''}`.trim())} ${techBox('Rodzaj zabudowy',v.bodyType)} ${techBox('Rocznik',v.year)} ${techBox('VIN',v.vin)} ${techBox('Pierwsza rejestracja',uiDate(v.firstRegistration))} ${techBox('Kategoria',v.vehicleCategory)} ${techBox('DMC',v.dmc)} ${techBox('Masa własna / eksploatacyjna',v.operatingWeight)} ${techBox('Ładowność',v.payload)} ${techBox('Wymiary całkowite',v.overallDimensions)} ${techBox('Przestrzeń ładunkowa',v.cargo)} ${techBox('Liczba miejsc',v.seats)} ${techBox('Pojemność silnika',v.engineCapacity)} ${techBox('Moc silnika',v.enginePower)} ${techBox('Paliwo',v.fuelType)} ${techBox('Skrzynia biegów',v.transmission)} ${techBox('Napęd',v.drive)} ${techBox('Klasa emisji',v.emissionClass)} ${techBox('Zbiornik paliwa',v.fuelTank)} ${techBox('Opony',v.tires)} ${techBox('Przyczepa hamowana',v.towBraked)} ${techBox('Przyczepa niehamowana',v.towUnbraked)} ${techBox('Hak',v.hook)} ${techBox('HDS',v.hds)} ${techBox('Zasięg HDS',v.armReach)} ${techBox('Udźwig HDS',v.hdsLift)}

Dokumenty i eksploatacja

${techBox('Aktualny przebieg',v.currentMileage?`${v.currentMileage} km`:'—')} ${techBox('Przegląd techniczny',uiDate(v.inspection||v.technicalInspectionDate))} ${techBox('Tachograf',uiDate(v.tachograph))} ${techBox('UDT',uiDate(v.udt))} ${techBox('Winiety / opłaty',vins)} ${techBox('Właściciel / finansujący',v.registeredOwner)} ${techBox('Użytkownik / firma',v.company)} ${techBox('Przypisany kierowca',d?.name||'—')}

Dodatkowe informacje

${techBox('Gwarancja producenta',v.manufacturerWarranty)} ${techBox('Umowa leasingu',v.leasingAgreement)} ${techBox('Sprzedawca / dealer',v.purchaseDealer)} ${techBox('Data zakupu',v.purchaseDate)} ${techBox('Cena netto',v.purchasePriceNet)} ${techBox('Cena brutto',v.purchasePriceGross)} ${techBox('Wyposażenie',v.equipment)} ${techBox('Uwagi',v.purchaseNotes||v.documentNotes)}
`; document.getElementById('techBackBtn').onclick=()=>showDetails(id); } function showEquipmentInsideDetails(id){ const v=vehicle(id);if(!v)return; activeVehicleId=id;ensureVehicleExtensions(v); const eq=v.assignedEquipment||{}; const body=document.getElementById('dBody'); document.getElementById('dTitle').textContent=`WYPOSAŻENIE – ${v.reg}`; body.innerHTML=`

Wyposażenie pojazdu

Pasy${Number(eq.strapsQty||0)} szt.
Maty${Number(eq.matsQty||0)} szt.
Separatory${Number(eq.separatorsQty||0)} szt.
Inne${eq.otherName?`${eq.otherName} — ${Number(eq.otherQty||0)} szt.`:'—'}
${v.equipment?`
Uwagi: ${v.equipment}
`:''}
`; document.getElementById('equipmentBackBtn').onclick=()=>showDetails(id); document.getElementById('editEquipmentBtn').onclick=()=>openEquipmentEditor(id); } function showDetails(id){ const v=vehicle(id);if(!v)return; activeVehicleId=id;enforceVehicleAuthoritative20260922(v);ensureVehicleExtensions(v); const uu=VEHICLE_AUTHORITATIVE_20260922[normalizeReg(v.reg)]; if(uu){if(uu.firstRegistration!==undefined)v.firstRegistration=uu.firstRegistration;if(uu.engineCapacity!==undefined)v.engineCapacity=uu.engineCapacity;if(uu.currentMileage!==undefined)v.currentMileage=uu.currentMileage;if(uu.inspection!==undefined){v.inspection=uu.inspection;v.technicalInspectionDate=uu.inspection}} const vign=vignetteUi(v); const inspection=v.inspection||v.technicalInspectionDate||''; const body=document.getElementById('dBody'); document.getElementById('dTitle').textContent='Szczegóły pojazdu'; body.innerHTML=`
Aktywny
${v.photo?`${v.reg||''}`:`
Brak zdjęcia pojazdu
`}
${v.reg||'—'}
${v.brand||''} ${v.model||''}
${v.bodyType||'Pojazd'}
▣
Nr rejestracyjny
${v.reg||'—'}
🔧
Ważność przeglądu
${statusValueHtml(inspection)}
#
Nr flotowy
${v.fleetNo||'—'}
🛡
Ważność OC
${statusValueHtml(ocValidTo(v))}
▤
Rodzaj zabudowy
${v.bodyType||'—'}
▥
Winiety
${vign.lamp||''}${vign.main}
▣
Rocznik
${v.year||'—'}
↔
Wymiary gabarytowe
${v.overallDimensions||'—'}
▥
VIN
${v.vin||'—'}
□
Przestrzeń ładunkowa
${v.cargo||'—'}
◴
Przebieg
${v.currentMileage?`${v.currentMileage} km`:'—'}
⌁
Hak
${v.hook?'Tak':'—'}
${v.hook||''}
△
Ładowność
${v.payload||'—'}
`; document.getElementById('modTechnical').onclick=()=>showTechnicalInsideDetails(id); document.getElementById('modTires').onclick=()=>showTires(id); document.getElementById('modServices').onclick=()=>showServices(id); document.getElementById('modInsurance').onclick=()=>showInsurance(id); document.getElementById('modEquipment').onclick=()=>showEquipmentInsideDetails(id); document.getElementById('modPackage').onclick=()=>showServicePackage(id); document.getElementById('modReport').onclick=()=>generateVehicleReport(id); openModal('detailsModal'); } let editingEquipmentVehicleId=null; function openEquipmentEditor(id){ const v=vehicle(id);if(!v)return; ensureVehicleExtensions(v); editingEquipmentVehicleId=id; activeVehicleId=id; const eq=v.assignedEquipment||{}; document.getElementById('equipmentEditorTitle').textContent=`Edytuj wyposażenie – ${v.reg}`; document.getElementById('eqStrapsQty').value=Number(eq.strapsQty||0); document.getElementById('eqMatsQty').value=Number(eq.matsQty||0); document.getElementById('eqSeparatorsQty').value=Number(eq.separatorsQty||0); document.getElementById('eqOtherName').value=eq.otherName||''; document.getElementById('eqOtherQty').value=Number(eq.otherQty||0); document.getElementById('eqNotes').value=v.equipment||''; document.getElementById('eqError').textContent=''; closeModals(); openModal('equipmentEditorModal'); } document.getElementById('backFromEquipmentEditor').onclick=()=>{ const id=editingEquipmentVehicleId||activeVehicleId; closeModals(); if(id)setTimeout(()=>showEquipmentInsideDetails(id),0); }; document.getElementById('saveEquipmentBtn').onclick=()=>{ try{ const id=editingEquipmentVehicleId||activeVehicleId; const v=vehicle(id); if(!v)throw new Error('Nie znaleziono pojazdu.'); ensureVehicleExtensions(v); const strapsQty=Math.max(0,parseInt(document.getElementById('eqStrapsQty').value||'0',10)||0); const matsQty=Math.max(0,parseInt(document.getElementById('eqMatsQty').value||'0',10)||0); const separatorsQty=Math.max(0,parseInt(document.getElementById('eqSeparatorsQty').value||'0',10)||0); const otherName=document.getElementById('eqOtherName').value.trim(); const otherQty=Math.max(0,parseInt(document.getElementById('eqOtherQty').value||'0',10)||0); const notes=document.getElementById('eqNotes').value.trim(); v.assignedEquipment={ ...(v.assignedEquipment||{}), straps:strapsQty>0, mats:matsQty>0, separators:separatorsQty>0, strapsQty, matsQty, separatorsQty, otherName, otherQty }; v.equipment=notes; save(); editingEquipmentVehicleId=null; closeModals(); setTimeout(()=>showEquipmentInsideDetails(id),0); }catch(e){ console.error('Błąd zapisu wyposażenia',e); document.getElementById('eqError').textContent='Nie udało się zapisać wyposażenia: '+(e?.message||e); } }; function showHds(id){const v=vehicle(id);if(!v)return;activeVehicleId=id;hdsTitle.textContent=`HDS – ${v.reg}`;hdsBody.innerHTML=`
HDS
${String(v.hds||'').toUpperCase()==='TAK'?'TAK':'NIE'}
Max. udźwig
${v.hdsLift||'—'}
Max. długość ramienia
${v.armReach||'—'}
Dodatkowe wyposażenie / informacje
${v.equipment||'—'}
`;openModal('hdsModal')} async function editVehicleOperationalData(id){ const v=vehicle(id);if(!v)return;ensureVehicleExtensions(v); const mileage=prompt('Aktualny przebieg [km]:',v.currentMileage||'');if(mileage===null)return; const newMileage=String(mileage).replace(/[^0-9]/g,''); const history=Array.isArray(v.mileageHistory)?[...v.mileageHistory]:[]; if(newMileage&&newMileage!==String(v.currentMileage||''))history.push({date:new Date().toISOString().slice(0,10),mileage:newMileage,source:'Ręczny wpis'}); if(window.FleetCloud?.configured){ if(!cloudReady){alert('Brak połączenia z Firebase. Zmiana nie została zapisana.');return} clearTimeout(cloudSyncTimer); try{await window.FleetCloud.updateVehicleMileage(id,newMileage,history)} catch(e){alert('Nie zapisano przebiegu w Firebase: '+(e.message||e));return} } v.currentMileage=newMileage;v.mileageHistory=history; const labels=[['straps','Pasy transportowe'],['mats','Maty'],['separators','Przekładki'],['tools','Narzędzia']]; labels.forEach(([k,label])=>{v.assignedEquipment[k]=confirm(`${label}: zaznacz OK jeśli wyposażenie jest przypisane do pojazdu.`)}); save();showDetails(id); } function tireWearValue(x){const n=parseFloat(String(x||'').replace(',','.'));return Number.isFinite(n)?n:null} function showTires(id){ const v=vehicle(id);if(!v)return;activeVehicleId=id;ensureVehicleExtensions(v); /* V6: źródłem przypisania jest numer flotowy widoczny na kafelku. Firebase może zmieniać/odtwarzać techniczne ID dokumentu, ale nr flotowy jest stały. */ const FIX_BY_FLEET={ '4':['Giti','235/65 R16C','4'], '6':['Giti','235/65 R16C','4'], '7':['Giti (2 szt.) + Michelin (2 szt.)','235/65 R16C','4'], '9':['Kleber (2 szt.) + Michelin (2 szt.)','215/75 R16C','4'], '11':['Kormoran (2 szt.) + Sailum (2 szt.)','235/65 R16C','4'], '12':['Kormoran','235/65 R16C','4'], '14':['NEXEN','215/75 R16C','4'], '16':['NEXEN','215/75 R16C','4'], '17':['NEXEN','215/75 R16C','4'], '18':['GoodYear','195/75 R16C','4'], '50':['Continental','245/70 R17.5','6'], '52':['Continental','265/70 R17.5','6'], '54':['KUMHO (4 szt.) + Sailum (2 szt.)','285/70 R19.5','6'], '53':['GoodYear (4 szt.) + AEOLUS (2 szt.)','315/80 R22.5','6'] }; const fleet=String(v.fleetNo??'').trim(); const f=FIX_BY_FLEET[fleet]; let summer; if(f){ summer={manufacturer:f[0],size:f[1],supplier:'',wear:'',quantity:f[2]}; v.tireData.summer={...summer}; v.tires=f[1]; try{localStorage.setItem(STORAGE,JSON.stringify(data))}catch(e){} try{if(typeof scheduleCloudSync==='function')scheduleCloudSync()}catch(e){} }else{ summer=v.tireData.summer||{manufacturer:'',size:'',supplier:'',wear:'',quantity:''}; } const winter=v.tireData.winter||{manufacturer:'',size:'',supplier:'',wear:'',quantity:''}; tiresTitle.textContent=`OPONY – ${v.reg}`; tiresBody.innerHTML=`
Przycisk otworzy nową wiadomość do pmajchrzak@expertglass.pl z uzupełnionym numerem rejestracyjnym i flotowym.
`; openModal('tiresModal'); /* V7: przeglądarka potrafi odtworzyć poprzednie wartości pól formularza po ich wyrenderowaniu. Dlatego właściwe dane wpisujemy także bezpośrednio do gotowych inputów po otwarciu modala. */ if(f){ const forceTireInputs=()=>{ const vals={ tSummerMaker:f[0],tSummerSize:f[1],tSummerSupplier:'', tSummerWear:'',tSummerQty:f[2] }; Object.entries(vals).forEach(([k,val])=>{const el=document.getElementById(k);if(el){el.setAttribute('autocomplete','off');el.value=val}}); }; forceTireInputs(); requestAnimationFrame(forceTireInputs); setTimeout(forceTireInputs,50); setTimeout(forceTireInputs,250); } } function requestTirePurchase(id){const v=vehicle(id);if(!v)return;ensureVehicleExtensions(v);const kind=prompt('Rodzaj opon: wpisz letnie lub zimowe','letnie')||'letnie';const td=kind.toLowerCase().startsWith('z')?v.tireData.winter:v.tireData.summer;const size=td.size||prompt('Rozmiar opon:','R../../..')||'R../../..';data.tirePurchaseRequests=data.tirePurchaseRequests||[];data.tirePurchaseRequests.push({date:new Date().toISOString().slice(0,10),vehicleId:v.id,reg:v.reg||'',fleetNo:v.fleetNo||'',kind,size,quantity:td.quantity||'',status:'Zgłoszono zakup'});localStorage.setItem(STORAGE,JSON.stringify(data));const subject=`Zakup nowych opon – ${v.reg}`;const body=`Dzień dobry, proszę o zakup nowych opon do samochodu: Nr rejestracyjny: ${v.reg||''} Nr flotowy: ${v.fleetNo||''} Rodzaj opon: ${kind} Rozmiar opon: ${size} Proszę o informację zwrotną po złożeniu zamówienia. Z poważaniem,`;window.location.href=`mailto:pmajchrzak@expertglass.pl?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`} function tableFromRows(rows){if(!rows||!rows.length)return '
Brak danych.
';const cols=[...new Set(rows.flatMap(r=>Object.keys(r||{})))].filter(Boolean);return `
${cols.map(c=>``).join('')}${rows.map(r=>`${cols.map(c=>``).join('')}`).join('')}
`} function showServices(id){const v=vehicle(id);if(!v)return;activeVehicleId=id;ensureVehicleExtensions(v);serviceTitle.textContent=`SERWISY – ${v.reg}`;const planned=(data.events||[]).filter(e=>e.vehicleId===id&&e.status==='service'&&new Date(e.end)>=new Date()).sort((a,b)=>new Date(a.start)-new Date(b.start));serviceBody.innerHTML=`

Przeglądy gwarancyjne

${v.warrantyServiceNote?`
${esc(v.warrantyServiceNote)}
`:''}${tableFromRows(v.warrantyServices)}

Serwisy zaplanowane w kalendarzu rezerwacji

${planned.length?planned.map(e=>`
${fmtDT(e.start)} – ${fmtDT(e.end)}${e.notes?`
${esc(e.notes)}`:''}
`).join(''):'
Brak zaplanowanych serwisów.
'}
`;openModal('serviceModal')} function showServiceHistory(id){const v=vehicle(id);if(!v)return;activeVehicleId=id;ensureVehicleExtensions(v);serviceHistoryTitle.textContent=`Historia serwisów – ${v.reg}`;serviceHistoryBody.innerHTML=tableFromRows(v.serviceHistory);openModal('serviceHistoryModal');const b=document.querySelector('#serviceHistoryModal .back-to-details');if(b)b.onclick=()=>{closeModals();showServices(id)}} function sanitizeVehicleInsuranceV11(v){ if(!v)return false; ensureVehicleExtensions(v); let changed=false; const reg=normalizeReg(v.reg); const known={ 'AA566HV':{insurer:'Wustermot',validTo:'2027-04-23'}, 'AA681HV':{insurer:'Wustermot',validTo:'2027-04-23'}, 'ZA434JS':{validTo:'2027-04-23'}, 'ZA926JX':{insurer:'Wustermot',validTo:'2027-04-23'} }; ['OC','AC','NNW'].forEach(type=>{ const x=v.insurance[type]||{}; const insurer=String(x.insurer||'').trim().toLowerCase(); const policy=String(x.policyNumber||'').trim(); const to=String(x.validTo||'').trim(); const contaminated=(policy==='888' && (to==='2027-10-01'||to==='2027-09-01')) || (insurer==='allianz' && policy==='888'); if(contaminated){ v.insurance[type]={insurer:'',policyNumber:'',phone:'',validFrom:'',validTo:'',coverage:''}; changed=true; } }); if(known[reg]){ const k=known[reg]; if(k.insurer!==undefined && v.insurance.OC.insurer!==k.insurer){v.insurance.OC.insurer=k.insurer;changed=true} if(k.validTo!==undefined && v.insurance.OC.validTo!==k.validTo){v.insurance.OC.validTo=k.validTo;changed=true} } return changed; } function showInsurance(id){ const v=vehicle(id);if(!v)return; activeVehicleId=id; insuranceEditorVehicleId=id; ensureVehicleExtensions(v); /* Odczyt polis bez automatycznego nadpisywania danych użytkownika. */ insuranceTitle.textContent=`UBEZPIECZENIA – ${v.reg}`; const ins=v.insurance; insuranceBody.innerHTML=`${['OC','AC','NNW'].map(k=>``).join('')}`; openModal('insuranceModal'); /* V11: tak jak skuteczny FIX opon — po wyrenderowaniu wymuszamy w DOM dokładnie dane TEGO pojazdu, żeby przeglądarka nie przywróciła starego formularza. */ const forceInsuranceInputs=()=>{ if(String(insuranceEditorVehicleId||'')!==String(id))return; ['OC','AC','NNW'].forEach(k=>{ const x=v.insurance[k]||{}; const vals={ [`ins${k}Name`]:x.insurer||'', [`ins${k}Policy`]:x.policyNumber||'', [`ins${k}Phone`]:x.phone||'', [`ins${k}From`]:x.validFrom||'', [`ins${k}Valid`]:x.validTo||'' }; Object.entries(vals).forEach(([eid,val])=>{ const el=document.getElementById(eid); if(el){el.setAttribute('autocomplete','off');el.value=val} }); }); }; forceInsuranceInputs(); requestAnimationFrame(forceInsuranceInputs); setTimeout(forceInsuranceInputs,50); setTimeout(forceInsuranceInputs,250); } function showServicePackage(id){const v=vehicle(id);if(!v)return;activeVehicleId=id;const p=getServicePackage(v);servicePackageTitle.textContent=`PAKIET SERWISOWY – ${v.reg}`;if(!p){servicePackageBody.innerHTML='
Brak przypisanego pakietu serwisowego.
';openModal('servicePackageModal');return}const yn=x=>x?'TAK':'NIE';const km=parseInt(String(v.currentMileage||'').replace(/\D/g,''),10);const left=Number.isFinite(km)?p.maxKm-km:null;servicePackageBody.innerHTML=`

${p.name}

Dostawca
${p.provider}
Okres obowiązywania
${p.validFrom} – ${p.validTo}
Limit całkowity
${p.maxKm.toLocaleString('pl-PL')} km${Number.isFinite(km)?` · aktualnie ${km.toLocaleString('pl-PL')} km · ${left>=0?`pozostało ${left.toLocaleString('pl-PL')} km`:`przekroczono o ${Math.abs(left).toLocaleString('pl-PL')} km`}`:''}
Zakładany przebieg roczny
${p.annualKm.toLocaleString('pl-PL')} km
Dopuszczalna odchyłka
${p.allowedDeviation}
Koszt miesięczny
${p.monthlyTotal} ${p.currency}/mies. (ComfortSuper ${p.monthlyComfort} + przedłużona gwarancja ${p.monthlyWarranty})
Przedłużona gwarancja
${p.extendedWarranty} · wartość ${p.extendedWarrantyPrice.toLocaleString('pl-PL')} EUR

Zakres

Przeglądy / obsługa producenta
${yn(p.scope.maintenance)}
Naprawy wynikające z normalnego zużycia
${yn(p.scope.wearRepairs)}
Części i robocizna
${yn(p.scope.partsLabor)}
Pomoc drogowa / holowanie przy awarii technicznej
${yn(p.scope.roadside)}
Opony
${yn(p.scope.tires)}
Szkody komunikacyjne / kolizje
${yn(p.scope.trafficDamage)}
Rozbite szyby
${yn(p.scope.brokenGlass)}

Najważniejsze warunki

Assistance: ${p.roadsideArea}

Serwis: ${p.serviceRule}

Naprawy za granicą: ${p.foreignRepairRule}

MAN ServiceCare: ${p.serviceCare}

Uwaga: ComfortSuper nie zastępuje OC/AC. Szkody komunikacyjne, rozbite szyby i opony nie są objęte podstawowym zakresem tego pakietu.
`;openModal('servicePackageModal')} function showCoverage(id,type){const v=vehicle(id);if(!v)return;activeVehicleId=id;ensureVehicleExtensions(v);coverageTitle.textContent=`Zakres ubezpieczenia ${type} – ${v.reg}`;coverageBody.textContent=v.insurance[type].coverage||'Zakres ubezpieczenia nie został jeszcze uzupełniony.';openModal('coverageModal');const b=document.querySelector('#coverageModal .back-to-details');if(b)b.onclick=()=>{closeModals();showInsurance(id)}} function saveTireData(){const v=vehicle(activeVehicleId);if(!v)return;ensureVehicleExtensions(v);v.tireData={summer:{manufacturer:tSummerMaker.value.trim(),size:tSummerSize.value.trim(),supplier:tSummerSupplier.value.trim(),wear:tSummerWear.value,quantity:tSummerQty.value},winter:{manufacturer:tWinterMaker.value.trim(),size:tWinterSize.value.trim(),supplier:tWinterSupplier.value.trim(),wear:tWinterWear.value,quantity:tWinterQty.value}};v.tires=v.tireData.summer.size||v.tireData.winter.size||v.tires||'';if(data.autosaveDrafts)delete data.autosaveDrafts.tiresModal;save();closeModals();renderVehiclesModule()} async function saveInsuranceData(){ const id=insuranceEditorVehicleId; const v=vehicle(id);if(!v)return; ensureVehicleExtensions(v); syncInsuranceEditorToVehicle(v); const insuranceSnapshot=JSON.parse(JSON.stringify(v.insurance)); const button=document.getElementById('saveInsurance'); if(button){button.disabled=true;button.textContent='Zapisywanie polis…'} try{ // Zapis wyłącznie pola insurance danego pojazdu. Bez nadpisywania innych // pojazdów, rezerwacji, grafików i dokumentów w Firebase. if(window.FleetCloud?.configured){ if(!cloudReady)throw new Error('Brak aktywnego połączenia z Firebase.'); clearTimeout(cloudSyncTimer); await window.FleetCloud.updateVehicleInsurances([{id,insurance:insuranceSnapshot}]); } v.insurance=insuranceSnapshot; if(data.autosaveDrafts)delete data.autosaveDrafts.insuranceModal; localStorage.setItem(STORAGE,JSON.stringify(data)); renderAll();renderAlerts();renderVehicleAlerts(); closeModals();insuranceEditorVehicleId=null;activeVehicleId=id; setTimeout(()=>showDetails(id),0); setAutosaveStatus('saved','POLISY ZAPISANE I ZSYNCHRONIZOWANE'); }catch(err){ console.error('Zapis ubezpieczeń:',err); setAutosaveStatus('error','BŁĄD ZAPISU POLIS'); alert('Nie udało się zapisać polis w Firebase. Formularz pozostaje otwarty. '+(err?.message||err)); }finally{ if(button){button.disabled=false;button.textContent='Zapisz ubezpieczenia'} } } function normalizeHeader(s){return String(s||'').trim().toLowerCase()} function rowHasReg(row,reg){const nr=normalizeReg(reg);return Object.entries(row||{}).some(([k,v])=>{const hk=normalizeHeader(k);return (hk.includes('rejestr')||hk.includes('samoch')||hk.includes('pojazd')||hk==='nr rej'||hk==='rej')&&normalizeReg(v)===nr})} async function importRepairWorkbook(file){if(typeof XLSX==='undefined'){alert('Brak biblioteki XLSX. Połącz komputer z internetem i odśwież stronę.');return}const wb=XLSX.read(await file.arrayBuffer(),{type:'array'});(data.vehicles||[]).forEach(v=>{ensureVehicleExtensions(v);const sheetName=wb.SheetNames.find(n=>normalizeReg(n)===normalizeReg(v.reg));if(sheetName){v.warrantyServices=XLSX.utils.sheet_to_json(wb.Sheets[sheetName],{defval:''}).filter(r=>Object.values(r).some(x=>String(x).trim()!==''))}});const allName=wb.SheetNames.find(n=>normalizeHeader(n)==='wszystkie naprawy');if(allName){const rows=XLSX.utils.sheet_to_json(wb.Sheets[allName],{defval:''});(data.vehicles||[]).forEach(v=>{const incoming=rows.filter(r=>rowHasReg(r,v.reg));const current=Array.isArray(v.serviceHistory)?v.serviceHistory:[];const keyOf=r=>String((r&&r['Nr zlecenia naprawy'])||'').trim()||[r&&r['Data zgłoszenia'],r&&r['Rodzaj naprawy/szkody'],r&&r['Serwis']].map(x=>String(x||'').trim()).join('|');const merged=new Map();current.forEach(r=>merged.set(keyOf(r),r));incoming.forEach(r=>merged.set(keyOf(r),Object.assign({},merged.get(keyOf(r))||{},r)));v.serviceHistory=Array.from(merged.values())})}save();alert('Dane serwisowe zostały zaktualizowane z pliku Excel.');if(activeVehicleId)showServices(activeVehicleId)} let editingDriverId=null,editingVehicleId=null; function openDriverEditor(id=null){ editingDriverId=id;const d=id?driver(id):null;driverModalTitle.textContent=d?'Edytuj kierowcę':'Dodaj kierowcę'; ndName.value=d?.name||'';ndPhone.value=d?.phone||'';const perms=d?(d.permissions||d.uprawnienia||[]):[];ndPermissions.value=Array.isArray(perms)?perms.join(', '):String(perms||'');ndHdsPermission.checked=!!(d?.hdsPermission||d?.hdsAuthorized);ndDriverType.value=d?.driverType||'staly';ndExperienceNotes.value=d?.experienceNotes||'';ndAvailabilityNotes.value=d?.availabilityNotes||'';ndMedical.value=d?.medical||d?.medicalExam||'';ndPsych.value=d?.psych||d?.psychExam||'';ndLicense.value=d?.license||'';ndError.textContent='';openModal('driverModal'); } document.getElementById('addDriverBtn').onclick=()=>openDriverEditor(null); document.getElementById('saveDriver').onclick=()=>{const name=ndName.value.trim();if(!name){ndError.textContent='Podaj imię i nazwisko.';return}const payload={name,phone:ndPhone.value.trim(),permissions:ndPermissions.value.split(',').map(x=>x.trim()).filter(Boolean),hdsPermission:!!ndHdsPermission.checked,driverType:ndDriverType.value||'staly',experienceNotes:ndExperienceNotes.value.trim(),availabilityNotes:ndAvailabilityNotes.value.trim(),medical:ndMedical.value,psych:ndPsych.value,license:ndLicense.value,active:true};if(editingDriverId){const d=driver(editingDriverId);Object.assign(d,payload)}else{data.drivers.push({id:'d'+Date.now(),...payload})}if(data.autosaveDrafts)delete data.autosaveDrafts.driverModal;save();editingDriverId=null;closeModals();renderDriversModule();populateSelects();}; function detectVehicleKindBody(v){const bt=String(v?.bodyType||'').toLowerCase();const kind=bt.includes('samochód osobowy')?'Samochód osobowy':(bt.includes('cięż')?'Ciężarówka':'Bus');return {kind,body:bt.includes('otwart')?'otwarta':'zamknięta'}} function openVehicleEditor(id=null){ // Formularz zawsze wypełniaj wyłącznie danymi pojazdu wskazanego identyfikatorem. // Stary szkic innego pojazdu nie może nadpisać formularza ani trafić do autozapisu. if(data.autosaveDrafts)delete data.autosaveDrafts.vehicleModal; editingVehicleId=id; const v=id?vehicle(id):null; if(id&&!v){alert('Nie znaleziono pojazdu w aktualnej bazie.');return} const kb=detectVehicleKindBody(v); const set=(elId,val)=>{const el=document.getElementById(elId);if(el)el.value=(val??'')}; const check=(elId,val)=>{const el=document.getElementById(elId);if(el)el.checked=!!val}; const title=document.getElementById('vehicleModalTitle'); if(title)title.textContent=v?'Edytuj pojazd':'Dodaj pojazd'; set('nvFleet',v?.fleetNo); set('nvReg',v?.reg); set('nvBrand',v?.brand); set('nvModel',v?.model); set('nvVehicleKind',kb.kind); set('nvBody',kb.body); set('nvYear',v?.year); set('nvVin',v?.vin); set('nvInspection',v?.inspection||v?.technicalInspectionDate); set('nvTachograph',v?.tachograph); set('nvUdt',v?.udt); set('nvVignetteSK',v?.vignettes?.SK); set('nvVignetteCZ',v?.vignettes?.CZ); set('nvVignetteAT',v?.vignettes?.AT); set('nvVignetteDE',v?.vignettes?.DE); set('nvDmc',v?.dmc); set('nvPayload',v?.payload); set('nvCargo',v?.cargo); set('nvHook',v?.hook); set('nvHds',(String(v?.hds||'').toUpperCase().startsWith('TAK')||v?.hds===true)?'TAK':'NIE'); set('nvHdsLift',v?.hdsLift); set('nvArmReach',v?.armReach); set('nvTires',v?.tires); set('nvMileage',v?.currentMileage); set('nvEquipment',v?.equipment); const eq=v?.assignedEquipment||{}; check('nvEqStraps',eq.straps); check('nvEqMats',eq.mats); check('nvEqSeparators',eq.separators); check('nvEqTools',eq.tools); const pf=document.getElementById('nvPhotoFile');if(pf)pf.value=''; const er=document.getElementById('nvError');if(er)er.textContent=''; openModal('vehicleModal'); } window.openVehicleEditor=openVehicleEditor; document.getElementById('addVehicleBtn').onclick=()=>openVehicleEditor(null) async function fileToDataUrl(file){return new Promise((resolve,reject)=>{const r=new FileReader();r.onerror=reject;r.onload=()=>{const src=r.result;if(!file.type?.startsWith('image/')){resolve(src);return}const img=new Image();img.onerror=()=>resolve(src);img.onload=()=>{try{const max=1280,scale=Math.min(1,max/Math.max(img.width,img.height));const w=Math.max(1,Math.round(img.width*scale)),h=Math.max(1,Math.round(img.height*scale));const c=document.createElement('canvas');c.width=w;c.height=h;const ctx=c.getContext('2d');ctx.drawImage(img,0,0,w,h);resolve(c.toDataURL('image/jpeg',0.78))}catch(e){resolve(src)}};img.src=src};r.readAsDataURL(file)})} document.getElementById('saveVehicle').onclick=async()=>{const reg=nvReg.value.trim();if(!reg){nvError.textContent='Podaj numer rejestracyjny.';return}const dupe=data.vehicles.find(v=>normalizeReg(v.reg)===normalizeReg(reg)&&v.id!==editingVehicleId);if(dupe){nvError.textContent='Pojazd o tym numerze rejestracyjnym już istnieje.';return}let photo=editingVehicleId?(vehicle(editingVehicleId)?.photo||''):'';const pf=nvPhotoFile.files&&nvPhotoFile.files[0];if(pf)photo=await fileToDataUrl(pf);const kind=nvVehicleKind.value,body=nvBody.value;const payload={reg,brand:nvBrand.value.trim(),model:nvModel.value.trim(),year:nvYear.value.trim(),vin:nvVin.value.trim(),inspection:nvInspection.value,tachograph:nvTachograph.value,udt:nvUdt.value,vignettes:{...(editingVehicleId?(vehicle(editingVehicleId)?.vignettes||{}):{}),SK:document.getElementById('nvVignetteSK').value,CZ:document.getElementById('nvVignetteCZ').value,AT:document.getElementById('nvVignetteAT').value,DE:document.getElementById('nvVignetteDE').value},cargo:nvCargo.value.trim(),payload:nvPayload.value.trim(),dmc:nvDmc.value.trim(),hook:nvHook.value.trim(),hds:nvHds.value,armReach:nvArmReach.value.trim(),hdsLift:nvHdsLift.value.trim(),tires:nvTires.value.trim(),currentMileage:nvMileage.value.trim(),assignedEquipment:{straps:nvEqStraps.checked,mats:nvEqMats.checked,separators:nvEqSeparators.checked,tools:nvEqTools.checked},equipment:nvEquipment.value.trim(),photo,fleetNo:nvFleet.value.trim(),bodyType:kind==='Samochód osobowy'?'Samochód osobowy':`${kind} ${body==='otwarta'?'otwarta':'blaszak'}`};const saveButton=document.getElementById('saveVehicle'); if(saveButton){saveButton.disabled=true;saveButton.textContent='Zapisywanie…'} try{ if(editingVehicleId&&window.FleetCloud?.configured){ if(!cloudReady||!window.FleetCloud?.updateVehicleDocuments)throw new Error('Brak połączenia z Firebase. Zmiany nie zostały zapisane.'); clearTimeout(cloudSyncTimer); await window.FleetCloud.updateVehicleDocuments(editingVehicleId,{ inspection:payload.inspection,tachograph:payload.tachograph,udt:payload.udt,vignettes:payload.vignettes }); } if(editingVehicleId){const v=vehicle(editingVehicleId);Object.assign(v,payload);v.technicalInspectionDate=payload.inspection;ensureVehicleExtensions(v)}else{data.vehicles.push({id:'v'+Date.now(),...payload,technicalInspectionDate:payload.inspection,driverId:'',tireData:{summer:{manufacturer:'',size:nvTires.value.trim(),supplier:'',wear:'',quantity:''},winter:{manufacturer:'',size:'',supplier:'',wear:'',quantity:''}},insurance:{OC:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''},AC:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''},NNW:{insurer:'',phone:'',validFrom:'',validTo:'',policyNumber:'',coverage:''}},warrantyServices:[],serviceHistory:[],mileageHistory:[],documents:[],operatingCosts:[]})}sortVehicles();if(data.autosaveDrafts)delete data.autosaveDrafts.vehicleModal;save();editingVehicleId=null;closeModals();renderVehiclesModule();populateSelects();renderAlerts();renderVehicleAlerts(); setAutosaveStatus('saved','DOKUMENTY POJAZDU ZAPISANE I ZSYNCHRONIZOWANE'); }catch(err){console.error('Zapis dokumentów pojazdu:',err);nvError.textContent='Nie udało się zapisać danych: '+(err?.message||err);setAutosaveStatus('error','BŁĄD ZAPISU DOKUMENTÓW');} finally{if(saveButton){saveButton.disabled=false;saveButton.textContent='Zapisz'}}} document.getElementById('driverSchedulePrev').onclick=()=>{driverScheduleWeekStart=addDays(driverScheduleWeekStart,-7);renderDriverSchedule()}; document.getElementById('driverScheduleNext').onclick=()=>{driverScheduleWeekStart=addDays(driverScheduleWeekStart,7);renderDriverSchedule()}; document.getElementById('driverScheduleToday').onclick=()=>{driverScheduleWeekStart=getMonday(new Date());renderDriverSchedule()}; document.getElementById('driverSchedulePrint').onclick=()=>printDriverSchedule(); document.getElementById('prevWeek').onclick=()=>{weekStart=addDays(weekStart,-7);renderAll()};document.getElementById('nextWeek').onclick=()=>{weekStart=addDays(weekStart,7);renderAll()};document.getElementById('todayBtn').onclick=()=>{weekStart=getMonday(new Date());renderAll()};['vehicleFilter','statusFilter','driverFilter','onlyAvailable'].forEach(id=>document.getElementById(id).onchange=renderCalendar) function excelSafeValue(v){return v===undefined||v===null?'':v} function downloadWorkbook(wb,filename){if(typeof XLSX==='undefined'){alert('Brak biblioteki XLSX. Połącz komputer z internetem i odśwież stronę.');return}XLSX.writeFile(wb,filename)} function reportRepairs(){if(!isAdmin())return;const rows=[];(data.vehicles||[]).forEach(v=>{ensureVehicleExtensions(v);(v.serviceHistory||[]).forEach(r=>rows.push({'Nr rejestracyjny':v.reg||'','Nr flotowy':v.fleetNo||'',...r}))});const wb=XLSX.utils.book_new();const ws=XLSX.utils.json_to_sheet(rows.length?rows:[{'Nr rejestracyjny':'','Nr flotowy':'','Informacja':'Brak danych napraw'}]);XLSX.utils.book_append_sheet(wb,ws,'Naprawy');downloadWorkbook(wb,'Raport_naprawy.xlsx')} function reportTires(){if(!isAdmin())return;const rows=[];(data.vehicles||[]).forEach(v=>{ensureVehicleExtensions(v);[['Letnie',v.tireData.summer],['Zimowe',v.tireData.winter]].forEach(([type,t])=>rows.push({'Nr rejestracyjny':v.reg||'','Nr flotowy':v.fleetNo||'','Rodzaj opon':type,'Producent':excelSafeValue(t.manufacturer),'Rozmiar':excelSafeValue(t.size),'Dostawca':excelSafeValue(t.supplier),'Zużycie %':excelSafeValue(t.wear),'Ilość':excelSafeValue(t.quantity)}))});const purchases=(data.tirePurchaseRequests||[]).map(x=>({'Data zgłoszenia':x.date||'','Nr rejestracyjny':x.reg||'','Nr flotowy':x.fleetNo||'','Rodzaj opon':x.kind||'','Rozmiar':x.size||'','Ilość':x.quantity||'','Status':x.status||''}));const wb=XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet(rows),'Opony');XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet(purchases.length?purchases:[{'Informacja':'Brak zgłoszeń zakupu opon'}]),'Lista zakupów');downloadWorkbook(wb,'Raport_opony.xlsx')} function reportInsurance(){if(!isAdmin())return;const rows=(data.vehicles||[]).map(v=>{ensureVehicleExtensions(v);return {'Nr rejestracyjny':v.reg||'','Nr flotowy':v.fleetNo||'','OC - ubezpieczyciel':v.insurance?.OC?.insurer||'','OC - nr polisy':v.insurance?.OC?.policyNumber||'','OC - telefon szkody':v.insurance?.OC?.phone||'','OC ważne od':v.insurance?.OC?.validFrom||'','OC ważne do':v.insurance?.OC?.validTo||'','AC - ubezpieczyciel':v.insurance?.AC?.insurer||'','AC - nr polisy':v.insurance?.AC?.policyNumber||'','AC - telefon szkody':v.insurance?.AC?.phone||'','AC ważne od':v.insurance?.AC?.validFrom||'','AC ważne do':v.insurance?.AC?.validTo||'','NNW - ubezpieczyciel':v.insurance?.NNW?.insurer||'','NNW - nr polisy':v.insurance?.NNW?.policyNumber||'','NNW - telefon szkody':v.insurance?.NNW?.phone||'','NNW ważne od':v.insurance?.NNW?.validFrom||'','NNW ważne do':v.insurance?.NNW?.validTo||''}});const wb=XLSX.utils.book_new();XLSX.utils.book_append_sheet(wb,XLSX.utils.json_to_sheet(rows),'Ubezpieczenia');downloadWorkbook(wb,'Raport_ubezpieczenia.xlsx')} const ROLE_LABELS={admin:'Administrator',logistics:'Logistyka',fleet:'Flota',user:'Użytkownik',driver:'Kierowca',viewer:'Podgląd'}; const ROLE_PERMISSIONS={ admin:{modules:['driversModule','vehiclesModule','calendarModule','machinesModule'],book:true,block:true,editDrivers:true,editVehicles:true,reports:true,users:false,import:true}, logistics:{modules:['driversModule','vehiclesModule','calendarModule'],book:true,block:true,editDrivers:false,editVehicles:false,reports:false,users:false,import:false}, fleet:{modules:['driversModule','vehiclesModule','calendarModule'],book:false,block:true,editDrivers:true,editVehicles:true,reports:false,users:false,import:true}, user:{modules:['vehiclesModule','calendarModule'],book:true,block:false,editDrivers:false,editVehicles:false,reports:false,users:false,import:false}, driver:{modules:['vehiclesModule','calendarModule'],book:false,block:false,editDrivers:false,editVehicles:false,reports:false,users:false,import:false}, viewer:{modules:['driversModule','vehiclesModule','calendarModule'],book:false,block:false,editDrivers:false,editVehicles:false,reports:false,users:false,import:false} }; function perms(){return ROLE_PERMISSIONS[currentUserRole]||ROLE_PERMISSIONS.viewer} function canEditVehicles(){return !!perms().editVehicles} function canEditDrivers(){return !!perms().editDrivers} function initAdminControls(){applyRoleUI()} function applyRoleUI(){ const p=perms(); document.querySelectorAll('.admin-only').forEach(el=>el.style.display=p.reports||p.users?'':'none'); if(document.getElementById('reportsBtn'))reportsBtn.style.display=p.reports?'':'none'; if(document.getElementById('usersBtn'))usersBtn.style.display=p.users?'':'none'; if(document.getElementById('importBtn'))importBtn.style.display=p.import?'':'none'; if(document.getElementById('importJsonInsuranceBtn'))importJsonInsuranceBtn.style.display=p.import?'':'none'; if(document.getElementById('sourceBtn'))sourceBtn.style.display=p.import?'':'none'; if(document.getElementById('exportBtn'))exportBtn.style.display=isAdmin()?'':'none'; if(document.getElementById('addBtn'))addBtn.style.display=p.book?'':'none'; if(document.getElementById('blockBtn'))blockBtn.style.display=p.block?'':'none'; if(document.getElementById('addDriverBtn'))addDriverBtn.style.display=p.editDrivers?'':'none'; if(document.getElementById('addVehicleBtn'))addVehicleBtn.style.display=p.editVehicles?'':'none'; document.querySelectorAll('.module-btn').forEach(b=>{b.style.display=p.modules.includes(b.dataset.module)?'':'none'}); document.getElementById('currentUserName').textContent=currentUserProfile.displayName||currentUserProfile.email||'Użytkownik'; document.getElementById('currentUserRole').textContent=cloudReady?'Tryb wspólny • bez logowania':(ROLE_LABELS[currentUserRole]||currentUserRole); renderModules(); } let cloudSyncTimer=null;let cloudHydrating=false;let cloudReady=false; function scheduleCloudSync(){if(!cloudReady||cloudHydrating||!window.FleetCloud||!window.FleetCloud.configured)return;clearTimeout(cloudSyncTimer);cloudSyncTimer=setTimeout(async()=>{try{await window.FleetCloud.pushState(data,currentUserRole)}catch(e){console.warn('Synchronizacja Firebase:',e)}},700)} let unsubscribeCloud=null; function ensureKNS3456NRestored(){ const reg='KNS3456N'; const existing=(data.vehicles||[]).find(v=>normalizeReg(v.reg)===reg); if(existing){ if(existing.active===false)existing.active=true; return false; } data.vehicles=data.vehicles||[]; data.vehicles.push({ id:'f13', reg:'KNS3456N', brand:'IVECO', model:'DAILY 70C18', year:2019, vin:'ZCFC170D805290005', inspection:'2026-01-14', tachograph:'2026-11-18', udt:'', vignettes:{SK:'BOX',CZ:'BOX'}, cargo:'6 x 2,14 m', payload:'3048 kg', dmc:'7200 kg', operatingWeight:'4152 kg', seats:'3', enginePower:'132 kW', engineCapacity:'2998 cm³', towBraked:'3500 kg', towUnbraked:'750 kg', axles:'2', firstRegistration:'2019-06-03', registeredOwner:'EXPERT GLASS Sp. z o.o.', hook:'', hds:'', armReach:'', hdsLift:'', tires:'', equipment:'', photo:'assets/vehicles/f13.jpeg', driverId:'', fleetNo:'13', bodyType:'Ciężarówka otwarta', currentMileage:'241410', active:true }); return true; } /* SERVICE_HISTORY_EMBEDDED_FIX_V16_20260923 */ const EMBEDDED_SERVICE_HISTORY_V16=[{"Lp.":1,"Data zgłoszenia":"2026-06-01","Nr rejestracyjny":"AB569AS","Nr flotowy":17,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Błąd adblue, check enginge","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Toyota Kobos","Nr zlecenia naprawy":"N010626","Gwarancja/Sposób zapłaty":"Naprawa gwarancyna ","Data zakończenia naprawy":"2026-06-11","Kwota":null,"Nr faktury":null},{"Lp.":2,"Data zgłoszenia":"2026-06-10","Nr rejestracyjny":"AA515ZX","Nr flotowy":16,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Serwis olejowy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Toyota Kobos","Nr zlecenia naprawy":"N020626","Gwarancja/Sposób zapłaty":"Serwis olejowy - koszt KRYSZTAL","Data zakończenia naprawy":"2026-06-15","Kwota":640.2,"Nr faktury":"100/00042/26/P/117"},{"Lp.":3,"Data zgłoszenia":"2026-06-11","Nr rejestracyjny":"ZA926JX","Nr flotowy":9,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Check engine, odcięcie mocy, brak hamulca ręcznego, problem z zawieszeniem","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N030626","Gwarancja/Sposób zapłaty":"Naprawa KRYSZTAL","Data zakończenia naprawy":"2026-08-21","Kwota":null,"Nr faktury":null},{"Lp.":4,"Data zgłoszenia":"2026-06-12","Nr rejestracyjny":"ZA506HM ","Nr flotowy":null,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Powyżej 80km/h drżenie i wstrząsy całego pojazdu","Osoba zatwierdzająca naprawę.":"Maciej Lisowski","Serwis":"KingSoft","Nr zlecenia naprawy":"N040626","Gwarancja/Sposób zapłaty":"Ponowna naprawa - do kontroli czy to to samo ?","Data zakończenia naprawy":"Naprawione ?!","Kwota":null,"Nr faktury":null},{"Lp.":5,"Data zgłoszenia":"2026-06-12","Nr rejestracyjny":"KN5390G","Nr flotowy":54,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Brak oświetlenia tablicy rejestracyjne i spalona żarówka w świetle przeciwmgielnym","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Firma","Nr zlecenia naprawy":"N050626","Gwarancja/Sposób zapłaty":"Naprawa we własnym zakresie","Data zakończenia naprawy":"2026-08-21","Kwota":null,"Nr faktury":null},{"Lp.":6,"Data zgłoszenia":"2026-06-15","Nr rejestracyjny":"KN9275J","Nr flotowy":53,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Obudowa lusterka lewa strona + lustro","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Firma","Nr zlecenia naprawy":"N060626","Gwarancja/Sposób zapłaty":"Naprawa we własnym zakresie. Części zamówione w InterCars","Data zakończenia naprawy":"2026-06-18","Kwota":95.9,"Nr faktury":"00046058/NSA/26/F + 00046232/NSA/26/F"},{"Lp.":7,"Data zgłoszenia":"2026-06-23","Nr rejestracyjny":"KN2481M","Nr flotowy":51,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Wycieraczki działają/nie działają / błąd na desce ropzdzielczej","Osoba zatwierdzająca naprawę.":null,"Serwis":"Kwiatkowski ?","Nr zlecenia naprawy":"N070626","Gwarancja/Sposób zapłaty":"Expert Glass Sp. z o.o.","Data zakończenia naprawy":"2026-07-21","Kwota":1992.6,"Nr faktury":"A045/06/2026"},{"Lp.":8,"Data zgłoszenia":"2026-06-30","Nr rejestracyjny":"AB569AS","Nr flotowy":17,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Serwis olejowy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Toyota Kobos","Nr zlecenia naprawy":"N080626","Gwarancja/Sposób zapłaty":"Serwis olejowy - koszt KRYSZTAL","Data zakończenia naprawy":"2026-07-01","Kwota":625.85,"Nr faktury":"100/00047/26/P/117"},{"Lp.":9,"Data zgłoszenia":"2026-07-02","Nr rejestracyjny":"KNS2481M","Nr flotowy":51,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Płyty podłogowe do wymiany","Osoba zatwierdzająca naprawę.":"Maciej Lisowski","Serwis":"Firma","Nr zlecenia naprawy":"N010726","Gwarancja/Sposób zapłaty":"Naprawa na koszt firmy","Data zakończenia naprawy":"2026-07-09","Kwota":2425.01,"Nr faktury":"kantowki + listwy podłogowe"},{"Lp.":10,"Data zgłoszenia":"2026-07-08","Nr rejestracyjny":"ZA515JB","Nr flotowy":1,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Chech engine, problem z drzwiami","Osoba zatwierdzająca naprawę.":"Michał Plata","Serwis":"MAN Zylina","Nr zlecenia naprawy":"N020726","Gwarancja/Sposób zapłaty":"Koszt KRYSZTAL","Data zakończenia naprawy":"2026-08-21","Kwota":null,"Nr faktury":null},{"Lp.":11,"Data zgłoszenia":"2026-07-03","Nr rejestracyjny":"ZA434JS","Nr flotowy":7,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Uszkodzenie lewego progu, drzwi przesuwnych","Osoba zatwierdzająca naprawę.":"Maciej Lisowski","Serwis":"Car Special","Nr zlecenia naprawy":"N030726","Gwarancja/Sposób zapłaty":"Naprawa z AC","Data zakończenia naprawy":null,"Kwota":null,"Nr faktury":null},{"Lp.":12,"Data zgłoszenia":"2026-07-13","Nr rejestracyjny":"AA638VN","Nr flotowy":12,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Pęknięta szyba","Osoba zatwierdzająca naprawę.":"Maciej Lisowski","Serwis":"NS Glass","Nr zlecenia naprawy":"N040726","Gwarancja/Sposób zapłaty":"Gotówka Krysztal","Data zakończenia naprawy":"2026-07-28","Kwota":1950,"Nr faktury":null},{"Lp.":13,"Data zgłoszenia":"2026-07-13","Nr rejestracyjny":"ZA434JS","Nr flotowy":7,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Dziura w oponie","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Gloss","Nr zlecenia naprawy":"N050726","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-07-13","Kwota":null,"Nr faktury":null},{"Lp.":14,"Data zgłoszenia":"2026-07-20","Nr rejestracyjny":"AA515ZX","Nr flotowy":16,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Kontrola układu wspomagania kierownicy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Toyota Kobos","Nr zlecenia naprawy":"N060726","Gwarancja/Sposób zapłaty":"Naprawa gwarancyjna","Data zakończenia naprawy":"2026-07-23","Kwota":null,"Nr faktury":null},{"Lp.":15,"Data zgłoszenia":"2026-07-21","Nr rejestracyjny":"AA498ZX","Nr flotowy":14,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Uszkodzony przegub wewnętrzny prawy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"Toyota","Nr zlecenia naprawy":"N070726","Gwarancja/Sposób zapłaty":"Holowanie i naprawa gwarancyjna","Data zakończenia naprawy":"2026-07-29","Kwota":null,"Nr faktury":null},{"Lp.":16,"Data zgłoszenia":"2026-07-23","Nr rejestracyjny":"AA681HV","Nr flotowy":6,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Wymiana klocków hamulcowych","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"MAN Zylina","Nr zlecenia naprawy":"N080726","Gwarancja/Sposób zapłaty":"Serwis gwarancyjny","Data zakończenia naprawy":"2026-07-24","Kwota":null,"Nr faktury":null},{"Lp.":17,"Data zgłoszenia":"2026-07-27","Nr rejestracyjny":"KNS3456N","Nr flotowy":13,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Prawdopodobnie urwane zaczepy od lampy lewy przód","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":null,"Nr zlecenia naprawy":"N090726","Gwarancja/Sposób zapłaty":"Expert Glass Sp. z o.o.","Data zakończenia naprawy":null,"Kwota":null,"Nr faktury":null},{"Lp.":18,"Data zgłoszenia":"2026-08-03","Nr rejestracyjny":"ZA506HM ","Nr flotowy":null,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Wymiana alternatora, Pasek klinowy, filtry + oleje","Osoba zatwierdzająca naprawę.":"Michał Plata","Serwis":"ZCARS Żaneta Zajadlik","Nr zlecenia naprawy":"N010826","Gwarancja/Sposób zapłaty":"Expert Glass Sp. z o.o.","Data zakończenia naprawy":"2026-08-03","Kwota":1550,"Nr faktury":"07/2026/00239"},{"Lp.":19,"Data zgłoszenia":"2026-08-13","Nr rejestracyjny":"ZA434JS","Nr flotowy":7,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"wymiana klocków hamulcowych","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N020826","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-08-21","Kwota":"€122,80","Nr faktury":"F/231/08/2026"},{"Lp.":20,"Data zgłoszenia":"2026-08-13","Nr rejestracyjny":"ZA072IO","Nr flotowy":null,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"naprawa koła, wymiana opon","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N030826","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-08-21","Kwota":"€70,85","Nr faktury":"F/230/08/2026"},{"Lp.":21,"Data zgłoszenia":"2026-08-18","Nr rejestracyjny":"S2MS","Nr flotowy":null,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"sprężyny , amortyzatory tył, wymieniane i regulowane spężyny i amortyzatory gwintowane przód, geometria kół","Osoba zatwierdzająca naprawę.":"Piotr Ślaz","Serwis":"KingSoft","Nr zlecenia naprawy":"N040826","Gwarancja/Sposób zapłaty":"Expert Glass Sp. z o.o.","Data zakończenia naprawy":"2026-08-21","Kwota":2600,"Nr faktury":"F/235/08/2026"},{"Lp.":22,"Data zgłoszenia":"2026-08-20","Nr rejestracyjny":"ZA926JX","Nr flotowy":9,"Priorytet":"Średni","Rodzaj naprawy/szkody":"odczyt + kasowanie błędów,uzupeł dod + adaptacja","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N050826","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-08-21","Kwota":"€138,67","Nr faktury":"F/243/08/2026"},{"Lp.":23,"Data zgłoszenia":"2026-09-10","Nr rejestracyjny":"ZA434JS","Nr flotowy":7,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Wymiana oleju","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N010926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-09-10","Kwota":null,"Nr faktury":null},{"Lp.":24,"Data zgłoszenia":"2026-09-10","Nr rejestracyjny":"AA566HV","Nr flotowy":4,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Wymiana oleju, check engine","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N020926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-09-10","Kwota":null,"Nr faktury":null},{"Lp.":25,"Data zgłoszenia":"2026-09-18","Nr rejestracyjny":"ZA926JX","Nr flotowy":9,"Priorytet":"Średni","Rodzaj naprawy/szkody":"Stukanie w w lewym kole, hamulec ręczny","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N030926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":null,"Kwota":null,"Nr faktury":null},{"Lp.":26,"Data zgłoszenia":"2026-09-18","Nr rejestracyjny":"AA638VN","Nr flotowy":12,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Uszkodzona opona","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"GLOSS","Nr zlecenia naprawy":"N040926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":"2026-09-18","Kwota":null,"Nr faktury":null},{"Lp.":27,"Data zgłoszenia":"2026-09-23","Nr rejestracyjny":"ZA072IO","Nr flotowy":null,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Serwis olejowy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingsSoft","Nr zlecenia naprawy":"N050926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":null,"Kwota":null,"Nr faktury":null},{"Lp.":28,"Data zgłoszenia":"2026-09-23","Nr rejestracyjny":"ZA072IO","Nr flotowy":null,"Priorytet":"Wysoki","Rodzaj naprawy/szkody":"Wymiana lampy","Osoba zatwierdzająca naprawę.":"Mateusz Gwiżdż","Serwis":"KingSoft","Nr zlecenia naprawy":"N060926","Gwarancja/Sposób zapłaty":"Krysztal s.r.o.","Data zakończenia naprawy":null,"Kwota":null,"Nr faktury":null}]; function mergeEmbeddedServiceHistoryV16(){ let changed=false; const norm=s=>String(s||'').replace(/\s+/g,'').toUpperCase(); const keyOf=r=>String((r&&r['Nr zlecenia naprawy'])||'').trim()||[r&&r['Data zgłoszenia'],r&&r['Rodzaj naprawy/szkody'],r&&r['Serwis']].map(x=>String(x||'').trim()).join('|'); for(const r of EMBEDDED_SERVICE_HISTORY_V16){ const rr=norm(r['Nr rejestracyjny']); const fleet=String(r['Nr flotowy']??'').trim(); let v=(data.vehicles||[]).find(x=>norm(x.reg)===rr); if(!v && rr==='KN2481M') v=(data.vehicles||[]).find(x=>norm(x.reg)==='KNS2481M'); if(!v && fleet) v=(data.vehicles||[]).find(x=>String(x.fleetNo||'').trim()===fleet); if(!v) continue; ensureVehicleExtensions(v); const cur=Array.isArray(v.serviceHistory)?v.serviceHistory:[]; const k=keyOf(r); const i=cur.findIndex(x=>keyOf(x)===k); if(i>=0) cur[i]=Object.assign({},cur[i],r); else cur.push(Object.assign({},r)); v.serviceHistory=cur; changed=true; } return changed; } /* VEHICLE_DUPLICATE_REPAIR_V17_20260923 */ function dedupeVehiclesByRegistrationV17(){ const list=Array.isArray(data.vehicles)?data.vehicles:[]; const norm=s=>String(s||'').replace(/[^A-Za-z0-9]/g,'').toUpperCase(); const preferredIds={KN5390G:'f54',KNS3456N:'f13'}; const groups=new Map(); for(const v of list){const r=norm(v.reg); if(!r)continue; if(!groups.has(r))groups.set(r,[]); groups.get(r).push(v)} let changed=false; const result=[]; for(const [reg,arr] of groups){ let keep=arr.find(v=>preferredIds[reg]&&v.id===preferredIds[reg]) || arr.find(v=>String(v.fleetNo||'')===({KN5390G:'54',KNS3456N:'13'}[reg]||'')) || arr[0]; for(const other of arr){ if(other===keep)continue; changed=true; // Scal tylko brakujące pola. Nigdy nie nadpisuj poprawnych danych rekordu głównego. for(const [k,val] of Object.entries(other)){ if(k==='id'||k==='reg'||k==='photo'||k==='brand'||k==='model'||k==='fleetNo')continue; if((keep[k]===undefined||keep[k]===null||keep[k]==='') && val!==undefined && val!==null && val!=='') keep[k]=val; } if(Array.isArray(other.serviceHistory)){ keep.serviceHistory=Array.isArray(keep.serviceHistory)?keep.serviceHistory:[]; const key=x=>String(x?.['Nr zlecenia naprawy']||x?.id||'')+'|'+String(x?.['Data zgłoszenia']||x?.date||'')+'|'+String(x?.['Rodzaj naprawy/szkody']||x?.description||''); const seen=new Set(keep.serviceHistory.map(key)); for(const row of other.serviceHistory){if(!seen.has(key(row))){keep.serviceHistory.push(row);seen.add(key(row))}} } } if(reg==='KN5390G'){ keep.id='f54'; keep.reg='KN5390G'; keep.brand='VOLVO'; keep.model='FL 240'; keep.fleetNo='54'; keep.photo='assets/vehicles/kn5390g_volvo_final.png'; keep.bodyType='Ciężarówka otwarta'; } if(reg==='KNS3456N'){ keep.id='f13'; keep.reg='KNS3456N'; keep.brand='IVECO'; keep.model='DAILY 70C18'; keep.fleetNo='13'; keep.photo='assets/vehicles/f13.jpeg'; } result.push(keep); } // zachowaj ewentualne rekordy bez numeru rejestracyjnego for(const v of list){if(!norm(v.reg))result.push(v)} if(changed||result.length!==list.length)data.vehicles=result; return changed||result.length!==list.length; } function mergeProtectedVehicleDocs(targetVehicles, sourceVehicles){ if(!Array.isArray(targetVehicles)||!Array.isArray(sourceVehicles))return false; const norm=v=>String(v||'').toUpperCase().replace(/[^A-Z0-9]/g,''); const byReg=new Map(sourceVehicles.map(v=>[norm(v?.reg),v]).filter(x=>x[0])); let changed=false; const hasVal=v=>v!==undefined&&v!==null&&String(v).trim()!==''; const mergeMissing=(t,src)=>{ if(!src||typeof src!=='object')return; Object.entries(src).forEach(([k,val])=>{ if(val&&typeof val==='object'&&!Array.isArray(val)){ if(!t[k]||typeof t[k]!=='object'||Array.isArray(t[k])){t[k]={};changed=true} mergeMissing(t[k],val); }else if(!hasVal(t[k])&&hasVal(val)){t[k]=val;changed=true} }); }; targetVehicles.forEach(v=>{ const old=byReg.get(norm(v?.reg)); if(!old)return; // Dane dokumentów wpisane ręcznie przez użytkownika są chronione przed pustym rekordem z Firebase. if(!hasVal(v.inspection)&&hasVal(old.inspection)){v.inspection=old.inspection;changed=true} if(old.insurance&&typeof old.insurance==='object'){ if(!v.insurance||typeof v.insurance!=='object'){v.insurance={};changed=true} mergeMissing(v.insurance,old.insurance); } }); return changed; } function recoverProtectedVehicleDocsFromBackup(targetVehicles){ try{ const backup=JSON.parse(localStorage.getItem(STORAGE+'_LAST_GOOD')||'null'); return mergeProtectedVehicleDocs(targetVehicles,backup?.vehicles||[]); }catch(e){console.warn('Odzyskiwanie dokumentów z kopii:',e);return false} } function applyRemoteState(remote){ if(!remote)return; cloudHydrating=true; let documentsChanged=false; try{ const localVehiclesBeforeCloud=Array.isArray(data.vehicles)?data.vehicles:[]; data.vehicles=Array.isArray(remote.vehicles)?remote.vehicles:data.vehicles; // Keep large original photos that were deliberately excluded from Firestore. const localPhotoById=new Map(localVehiclesBeforeCloud.map(v=>[String(v.id),v])); const localPhotoByReg=new Map(localVehiclesBeforeCloud.map(v=>[normalizeReg(v.reg),v])); data.vehicles.forEach(v=>{ const old=localPhotoById.get(String(v.id))||localPhotoByReg.get(normalizeReg(v.reg)); if(old && !v.photo && old.photo)v.photo=old.photo; // Keep every large local attachment omitted from the cloud payload. // Do not overwrite cloud-provided fields or any smaller normal values. function restoreLargeAttachments(target,source){ if(!source||typeof source!=='object'||!target||typeof target!=='object')return; for(const [key,val] of Object.entries(source)){ if(typeof val==='string' && val.length>65000 && (target[key]===undefined||target[key]===null||target[key]===''))target[key]=val; else if(val&&typeof val==='object'&&!Array.isArray(val)){ if(target[key]===undefined||target[key]===null)target[key]={}; if(typeof target[key]==='object')restoreLargeAttachments(target[key],val); } } } if(old)restoreLargeAttachments(v,old); }); if(dedupeAllVehicleDocuments(data.vehicles))documentsChanged=true; // Nie kopiuj przestarzałych lokalnych pól na aktualne rekordy Firebase. // Kopia lokalna jest wyłącznie do ręcznego odzyskiwania, nie do automatycznego nadpisywania. if(dedupeAllVehicleDocuments(data.vehicles))documentsChanged=true; if(dedupeVehiclesByRegistrationV17())documentsChanged=true; // Bez automatycznej restauracji historycznych rekordów. // Firestore jest źródłem prawdy: nie narzucaj starych przebiegów, polis ani dat. data.drivers=Array.isArray(remote.drivers)?remote.drivers:data.drivers; const driversChanged=applyDriverMaster(); if(driversChanged)documentsChanged=true; data.events=Array.isArray(remote.events)?remote.events:data.events; if(Array.isArray(remote.tirePurchaseRequests))data.tirePurchaseRequests=remote.tirePurchaseRequests; if(!data.autosaveDrafts||typeof data.autosaveDrafts!=='object')data.autosaveDrafts={}; if(remote.autosaveDrafts&&typeof remote.autosaveDrafts==='object'){ data.autosaveDrafts={...remote.autosaveDrafts,...data.autosaveDrafts}; } if(!data.driverSchedule||typeof data.driverSchedule!=='object')data.driverSchedule={}; if(remote.driverSchedule&&typeof remote.driverSchedule==='object'){ const remoteSchedule=remote.driverSchedule||{}; const localSchedule=data.driverSchedule||{}; // Nigdy nie kasujemy istniejących wpisów pustym grafikiem z innego urządzenia. // Lokalny wpis ma pierwszeństwo dla tego samego kierowcy/dnia. data.driverSchedule={...remoteSchedule,...DRIVER_SCHEDULE_LOCAL_BACKUP,...localSchedule}; } /* PAWEL_OLSZAK_ETAT_REMOTE_RESTORE */ (function(){ data.driverSchedule=data.driverSchedule||{}; const pawel=(data.drivers||[]).find(d=>d.id==='d11'); if(!pawel)return; ["2026-09-14","2026-09-15","2026-09-16","2026-09-17","2026-09-18"] .forEach(dt=>data.driverSchedule[`${pawel.id}__${dt}`]='available'); ["2026-09-19","2026-09-20"] .forEach(dt=>data.driverSchedule[`${pawel.id}__${dt}`]='unavailable'); })(); /* MAMALA_WRZESIEN_2026_REMOTE_RESTORE */ (function(){ data.driverSchedule=data.driverSchedule||{}; const norm=s=>String(s||'').toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').trim(); const mamala=(data.drivers||[]).find(d=>norm(d.name)===norm('Wojciech Mamala')); if(!mamala)return; ["2026-09-14","2026-09-15","2026-09-16","2026-09-18","2026-09-22","2026-09-23","2026-09-24","2026-09-28","2026-09-30"] .forEach(dt=>data.driverSchedule[`${mamala.id}__${dt}`]='confirmed'); })(); /* TOMASZ_FECKO_WRZESIEN_2026_REMOTE_RESTORE */ (function(){ data.driverSchedule=data.driverSchedule||{}; const tomasz=(data.drivers||[]).find(d=>d.id==='j4'||String(d.name||'').trim().toLowerCase()==='tomasz fecko'); if(!tomasz)return; ["2026-09-20","2026-09-22","2026-09-26","2026-09-28","2026-09-30"] .forEach(dt=>data.driverSchedule[`${tomasz.id}__${dt}`]='confirmed'); })(); if(dedupeVehiclesByRegistrationV17())documentsChanged=true; sortVehicles(); localStorage.setItem(STORAGE,JSON.stringify(data)); renderAll(); }finally{setTimeout(()=>{cloudHydrating=false;if(documentsChanged)scheduleCloudSync()},120)} } async function hydrateFromCloud(){ if(!window.FleetCloud||!window.FleetCloud.configured)return; try{ cloudHydrating=true; const remote=await window.FleetCloud.loadState(); if(remote&&(remote.vehicles.length||remote.drivers.length||remote.events.length)){ applyRemoteState(remote); if((remote.schemaVersion||0)<2 && window.FleetCloud.migrateState){await window.FleetCloud.migrateState(data);console.info('Zaktualizowano strukturę Firestore do wersji 2 i utworzono blokady rezerwacji.')} }else{ if(window.FleetCloud.migrateState)await window.FleetCloud.migrateState(data);else await window.FleetCloud.pushState(data); console.info('Firestore był pusty — zapisano aktualne dane aplikacji.'); } if(window.FleetCloud.subscribeState){ if(unsubscribeCloud)unsubscribeCloud(); unsubscribeCloud=window.FleetCloud.subscribeState(applyRemoteState,(e)=>{ cloudDot.className='cloud-dot local'; console.warn('Utracono synchronizację Firestore:',e); }); } }catch(e){ cloudDot.className='cloud-dot local'; console.warn('Pobranie/zapis danych Firebase:',e); }finally{cloudHydrating=false} } async function refreshUsers(){const box=document.getElementById('usersList');if(!window.FleetCloud||!window.FleetCloud.configured){box.innerHTML='
Firebase nie jest jeszcze skonfigurowany.
';return}box.innerHTML='
Ładowanie…
';try{const rows=await window.FleetCloud.listUsers();box.innerHTML=rows.length?`${rows.map(u=>``).join('')}
UżytkownikE-mailRolaStatus
${u.displayName||''}${u.email||''}${ROLE_LABELS[u.role]||u.role||''}${u.active===false?'Zablokowany':'Aktywny'}
`:'
Brak kont.
'}catch(e){box.innerHTML='
'+(e.message||e)+'
'}} async function openUsers(){openModal('usersModal');const configured=!!(window.FleetCloud&&window.FleetCloud.configured);usersCloudWarning.style.display=configured?'none':'';newUserForm.style.opacity=configured?'1':'.45';createUserBtn.disabled=!configured;migrateCloudBtn.disabled=!configured;refreshUsersBtn.disabled=!configured;if(configured)refreshUsers()} async function createManagedUser(){uError.textContent='';if(!window.FleetCloud||!window.FleetCloud.configured){uError.textContent='Najpierw skonfiguruj Firebase.';return}const email=uEmail.value.trim(),password=uPassword.value,name=uName.value.trim(),role=uRole.value;if(!email||!password||!name){uError.textContent='Uzupełnij imię, e-mail i hasło.';return}if(password.length<8){uError.textContent='Hasło startowe powinno mieć co najmniej 8 znaków.';return}createUserBtn.disabled=true;try{await window.FleetCloud.createManagedUser({email,password,displayName:name,role});uEmail.value='';uPassword.value='';uName.value='';await refreshUsers();alert('Konto zostało utworzone.')}catch(e){uError.textContent=e.message||String(e)}finally{createUserBtn.disabled=false}} async function migrateToCloud(){if(!confirm('Przenieść aktualne dane pojazdów, kierowców i rezerwacji do Firebase?'))return;try{migrateCloudBtn.disabled=true;await window.FleetCloud.pushState(data,'admin',true);alert('Dane zostały zapisane w Firebase.')}catch(e){alert('Migracja nie powiodła się: '+(e.message||e))}finally{migrateCloudBtn.disabled=false}} function enterLocalMode(){cloudReady=false;authGate.classList.add('hidden');cloudDot.className='cloud-dot local';currentUserProfile={uid:'local-admin',displayName:'Administrator lokalny',email:'',role:'admin',active:true};currentUserRole='admin';applyRoleUI()} try{if(mergeEmbeddedServiceHistoryV16()){localStorage.setItem(STORAGE,JSON.stringify(data));}}catch(e){console.warn('Historia serwisów V16:',e)} let authRuntimeStarted=false; async function setupCloudRuntime(){ const fc=window.FleetCloud,fa=window.FleetAuth; if(!fc||!fc.configured||!fa){loginCloudNote.textContent='Brak konfiguracji Firebase. Synchronizacja nie jest dostępna.';authGate.classList.remove('hidden');return} if(authRuntimeStarted)return;authRuntimeStarted=true; authGate.classList.remove('hidden');loginCloudNote.textContent='Zaloguj się kontem Google, aby uzyskać dostęp.'; fa.onAuthStateChanged(async user=>{ if(!user){document.getElementById('logoutBtn').style.setProperty('display','none','important');cloudReady=false;cloudDot.className='cloud-dot local';if(unsubscribeCloud){unsubscribeCloud();unsubscribeCloud=null}authGate.classList.remove('hidden');currentUserProfile=null;return} // Dodatkowa kontrola interfejsu. Dostęp do danych MUSZĄ zabezpieczać reguły Firestore. const allowedAdmins=['mgwizdz.eg@gmail.com','gmati381@gmail.com']; const signedInEmail=(user.email||'').trim().toLowerCase(); const allowedAdmin=allowedAdmins.includes(signedInEmail) || user.uid==='hctGxCY8elXjqpZvHMeLdjgKPNv2'; const googleIdentity=(user.providerData||[]).some(p=>p.providerId==='google.com'); if(!allowedAdmin || !googleIdentity){loginError.textContent='Brak dostępu. Zalogowano: '+(signedInEmail||'(brak e-mail)')+'. Dozwolone konta: '+allowedAdmins.join(', ')+'.';await fa.signOut();return} loginError.textContent='';document.getElementById('logoutBtn').style.setProperty('display','inline-block','important');currentUserProfile={uid:user.uid,displayName:user.displayName||user.email,email:user.email,role:'admin',active:true};currentUserRole='admin';applyRoleUI();cloudReady=true;cloudDot.className='cloud-dot online'; try{await hydrateFromCloud();authGate.classList.add('hidden')}catch(e){loginError.textContent='Błąd synchronizacji: '+(e.message||e);authGate.classList.remove('hidden')} }); fa.getRedirectResult().catch(e=>{loginError.textContent=e.message||String(e)}); } window.addEventListener('fleet-cloud-ready',setupCloudRuntime);if(window.FleetCloud)setupCloudRuntime(); const repairLocksBtn=document.getElementById('repairReservationLocksBtn'); if(repairLocksBtn)repairLocksBtn.onclick=async()=>{ repairLocksBtn.disabled=true; const old=repairLocksBtn.textContent; repairLocksBtn.textContent='SPRAWDZANIE...'; try{ const r=await cleanupStaleReservationLocks(); alert(`Sprawdzono blokady: ${r.checked}\nUsunięto stare blokady: ${r.removed}`); }catch(e){ alert('Nie udało się naprawić blokad.\n\nPowód: '+reservationErrorMessage(e)); }finally{ repairLocksBtn.disabled=false; repairLocksBtn.textContent=old; } }; // Bezpieczne przywracanie polis z eksportu JSON: nigdy nie nadpisuj całego stanu. // Zgodność pojazdów sprawdzana po numerze rejestracyjnym; ID musi zgadzać się, // jeżeli jest obecne w eksporcie. Polisy bez zmian są pomijane. document.getElementById('importJsonInsuranceBtn').onclick=()=>{ if(!isAdmin()){alert('Import polis wymaga uprawnień administratora.');return;} document.getElementById('insuranceJsonFile').click(); }; document.getElementById('insuranceJsonFile').onchange=async event=>{ const input=event.currentTarget, file=input.files&&input.files[0]; if(!file)return; const button=document.getElementById('importJsonInsuranceBtn'); try{ if(!isAdmin())throw new Error('Brak uprawnień administratora.'); if(!cloudReady||!window.FleetCloud?.updateVehicleInsurances)throw new Error('Najpierw zaloguj się i poczekaj na połączenie z Firebase.'); if(file.size>10*1024*1024)throw new Error('Plik JSON jest zbyt duży.'); const imported=JSON.parse(await file.text()); if(!Array.isArray(imported?.vehicles))throw new Error('To nie jest eksport floty JSON (brak listy vehicles).'); // Pobierz najświeższe rekordy z chmury, żeby nie cofnąć zmian z innych urządzeń. const remote=await window.FleetCloud.loadState(); const normalize=s=>String(s||'').replace(/[^A-Za-z0-9]/g,'').toUpperCase(); const byReg=new Map((remote.vehicles||[]).map(v=>[normalize(v.reg),v])); const updates=[], missing=[], conflicts=[]; for(const old of imported.vehicles){ const reg=normalize(old.reg);if(!reg||!old.insurance)continue; const live=byReg.get(reg); if(!live){missing.push(old.reg);continue;} if(old.id&&live.id&&String(old.id)!==String(live.id)){ conflicts.push(old.reg+' (różne ID)');continue; } const next={...(live.insurance||{})};let changed=false; for(const type of ['OC','AC','NNW']){ const policy=old.insurance[type]; if(!policy||typeof policy!=='object'||Array.isArray(policy))continue; // Nie zastępuj poprawnych danych pustymi wpisami z kopii. if(!Object.values(policy).some(v=>typeof v==='string'&&v.trim()))continue; const merged={...(next[type]||{})}; for(const key of ['insurer','policyNumber','phone','validFrom','validTo','coverage']){ if(typeof policy[key]==='string'&&policy[key].trim())merged[key]=policy[key]; } if(JSON.stringify(merged)!==JSON.stringify(next[type]||{})){next[type]=merged;changed=true;} } if(changed)updates.push({id:live.id,reg:live.reg,insurance:next}); } if(conflicts.length)throw new Error('Nie można dopasować pojazdów: '+conflicts.join(', ')+'. Import przerwany bez zapisu.'); if(!updates.length){alert('Brak polis wymagających aktualizacji.'+(missing.length?'\nNieznane pojazdy: '+missing.join(', '):''));return;} const preview=updates.map(x=>x.reg).join(', '); if(!confirm('Zostaną zaktualizowane wyłącznie ubezpieczenia OC/AC/NNW dla '+updates.length+' pojazdów: '+preview+'.\n\nKierowcy, rezerwacje, grafik, dokumenty i pozostałe dane nie zostaną nadpisane.\n\nCzy kontynuować?'))return; button.disabled=true;button.textContent='Zapisywanie polis…'; // Zapis tylko wybranych pól pojazdów w Firestore. await window.FleetCloud.updateVehicleInsurances(updates); // Zsynchronizuj lokalny widok bez uruchamiania pełnego zapisu całej floty. for(const u of updates){const local=(data.vehicles||[]).find(v=>normalize(v.reg)===normalize(u.reg));if(local)local.insurance=u.insurance;} localStorage.setItem(STORAGE,JSON.stringify(data));renderAll(); alert('Przywrócono polisy '+updates.length+' pojazdów w Firebase.'+(missing.length?'\nPominięte nieznane pojazdy: '+missing.join(', '):'')+'\nWykonaj teraz Eksport JSON, aby zachować kopię.'); }catch(err){alert('Import nie został w pełni zakończony: '+(err.message||err)+'\nW razie przerwania ponowny import pominie już zapisane polisy.');} finally{button.disabled=false;button.textContent='Import polis JSON';input.value='';} }; document.getElementById('exportBtn').onclick=()=>{const blob=new Blob([JSON.stringify(data,null,2)],{type:'application/json'});const a=document.createElement('a');a.href=URL.createObjectURL(blob);a.download='flota-rezerwacje.json';a.click();URL.revokeObjectURL(a.href)} document.getElementById('saveTires').onclick=saveTireData; document.getElementById('saveInsurance').onclick=saveInsuranceData; repairExcelFile.onchange=async e=>{const f=e.target.files&&e.target.files[0];if(!f)return;try{await importRepairWorkbook(f)}catch(err){alert('Nie udało się wczytać pliku napraw: '+(err.message||err))}repairExcelFile.value=''}; // Excel schema: POJAZDY, WINIETY, KIEROWCY. Column names flexible PL variants. function val(row,names){for(const n of names){if(row[n]!==undefined&&row[n]!==null&&String(row[n]).trim()!=='')return row[n]}return ''} function excelDate(v){if(!v)return '';if(typeof v==='number'){const d=XLSX.SSF.parse_date_code(v);return `${d.y}-${String(d.m).padStart(2,'0')}-${String(d.d).padStart(2,'0')}`}const s=String(v).trim();const m=s.match(/(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})/);if(m)return `${m[3]}-${m[2].padStart(2,'0')}-${m[1].padStart(2,'0')}`;return s.slice(0,10)} function rowsFromSamochodySheet(wb){const key=wb.SheetNames.find(x=>x.toUpperCase()==='SAMOCHODY');if(!key)return [];const ws=wb.Sheets[key];const raw=XLSX.utils.sheet_to_json(ws,{header:1,defval:''});let out=[];for(let i=0;i{const candidates=rows.filter(r=>normalizeReg(r.reg)===normalizeReg(v.reg));if(!candidates.length)return;let r=candidates[0];if(candidates.length>1){const target=(String(v.brand)+' '+String(v.model)).toUpperCase();r=candidates.find(x=>target.split(/\s+/).some(t=>t.length>=3&&String(x.brand).toUpperCase().includes(t)))||r}v.vin=r.vin||v.vin||'';v.inspection=excelDate(r.inspection)||v.inspection||'';v.tachograph=excelDate(r.tachograph)||'';v.udt=excelDate(r.udt)||'';v.vignettes=v.vignettes||{};const sk=excelDate(r.sk),cz=excelDate(r.cz);if(sk)v.vignettes.SK=sk;if(cz)v.vignettes.CZ=cz;updated++});if(updated){save();return true}return false} function importWorkbook(wb){ if(syncExistingVehiclesFromSamochody(wb))return; const sheet=(name)=>{const key=wb.SheetNames.find(x=>x.toUpperCase()===name);return key?XLSX.utils.sheet_to_json(wb.Sheets[key],{defval:''}):[]}; let ps=sheet('POJAZDY'),ws=sheet('WINIETY'),ds=sheet('KIEROWCY'); if(!ps.length&&wb.SheetNames.length){ps=XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]],{defval:''})} if(!ps.length&&!ds.length)throw new Error('Nie znaleziono danych pojazdów ani kierowców.'); if(ds.length){/* Lista kierowców jest zarządzana w module KIEROWCY; import pojazdów jej nie nadpisuje. */} if(ps.length){ps.forEach(r=>{const reg=String(val(r,['Rejestracja','Nr rejestracyjny','Numer rejestracyjny']));if(!reg)return;const v=data.vehicles.find(x=>normalizeReg(x.reg)===normalizeReg(reg));if(!v)return;const mt=String(val(r,['Marka i typ','Marka i Typ','Marka/typ']));const brand=String(val(r,['Marka']));const model=String(val(r,['Model']));let year=val(r,['Rocznik','Rok']);if(!year&&mt){const m=mt.match(/[’'](\d{2})/);if(m)year=Number(m[1])>=70?1900+Number(m[1]):2000+Number(m[1])}v.brand=brand||v.brand;v.model=model||v.model;v.year=year||v.year;v.vin=String(val(r,['VIN','Nr VIN']))||v.vin;v.inspection=excelDate(val(r,['Przegląd','Przeglad','Ważność przeglądu','Waznosc przegladu','PRZEGLĄD TECHNICZNY']))||v.inspection;v.tachograph=excelDate(val(r,['LEGALIZACJA TACHOGRAFU','Legalizacja tachografu','Tachograf']))||v.tachograph;v.udt=excelDate(val(r,['UDT','Ważność UDT','Waznosc UDT']))||v.udt;v.payload=String(val(r,['Ładowność','Ladownosc','Payload']))||v.payload;v.dmc=String(val(r,['DMC','Dopuszczalna masa całkowita']))||v.dmc;v.cargo=String(val(r,['Przestrzeń ładunkowa','Wymiary przestrzeni ładunkowej','Wymiary']))||v.cargo;v.tires=String(val(r,['Rozmiar opon','Opony','Tires']))||v.tires;v.hook=String(val(r,['HAK','Hak']))||v.hook;v.hds=String(val(r,['HDS','Udźwig HDS','HDS / udźwig']))||v.hds;v.photo=String(val(r,['Zdjęcie','Zdjecie','Photo']))||v.photo;v.bodyType=String(val(r,['Rodzaj zabudowy','Rodzaj zabudowy ']))||v.bodyType;});} sortVehicles(); ws.forEach(r=>{const pid=String(val(r,['Pojazd ID','PojazdID','ID pojazdu'])),reg=String(val(r,['Rejestracja','Nr rejestracyjny'])),country=String(val(r,['Kraj','Country'])),dt=excelDate(val(r,['Ważna do','Wazna do','Data ważności','Data waznosci']));const v=data.vehicles.find(x=>(pid&&x.id===pid)||(reg&&x.reg===reg));if(v&&country&&dt)v.vignettes[country]=dt}); save(); } async function importExcelFile(file){if(typeof XLSX==='undefined')throw new Error('Brak biblioteki XLSX. Połącz komputer z internetem i odśwież stronę.');const wb=XLSX.read(await file.arrayBuffer(),{type:'array'});importWorkbook(wb)} async function importExcelFromUrl(url){if(typeof XLSX==='undefined')throw new Error('Brak biblioteki XLSX. Połącz komputer z internetem i odśwież stronę.');const res=await fetch(url,{cache:'no-store'});if(!res.ok)throw new Error('Nie udało się pobrać pliku z URL.');const buf=await res.arrayBuffer();const wb=XLSX.read(buf,{type:'array'});importWorkbook(wb)} document.getElementById('reportsBtn').onclick=()=>{if(isAdmin())openModal('reportsModal')}; document.getElementById('reportRepairsBtn').onclick=reportRepairs; document.getElementById('reportTiresBtn').onclick=reportTires; document.getElementById('reportInsuranceBtn').onclick=reportInsurance; document.getElementById('importBtn').onclick=()=>excelFile.click();excelFile.onchange=async e=>{const f=e.target.files[0];if(!f)return;try{await importExcelFile(f);alert('Import zakończony.')}catch(err){alert(err.message||'Błąd importu pliku Excel.')}excelFile.value=''} let sourceCfg=JSON.parse(localStorage.getItem(SOURCE_KEY)||'{"url":"","auto":false}'); function openSourceModal(){sUrl.value=sourceCfg.url||'';sAuto.checked=!!sourceCfg.auto;sError.textContent='';openModal('sourceModal')} function saveSourceCfg(){sourceCfg={url:sUrl.value.trim(),auto:sAuto.checked};localStorage.setItem(SOURCE_KEY,JSON.stringify(sourceCfg));closeModals();alert('Ustawienia źródła danych zapisane.')} async function testSourceImport(){const url=sUrl.value.trim();if(!url){sError.textContent='Podaj link do pliku Excel.';return}sError.textContent='Trwa pobieranie i import...';try{await importExcelFromUrl(url);sourceCfg={url,auto:sAuto.checked};localStorage.setItem(SOURCE_KEY,JSON.stringify(sourceCfg));sError.textContent='';closeModals();alert('Import z URL zakończony.')}catch(err){sError.textContent=(err&&err.message)?err.message:'Nie udało się pobrać danych z URL. Sprawdź link lub uprawnienia.'}} document.getElementById('sourceBtn').onclick=openSourceModal; document.getElementById('saveSource').onclick=saveSourceCfg; document.getElementById('testSource').onclick=testSourceImport; if(sourceCfg.url&&sourceCfg.auto){window.addEventListener('load',()=>{importExcelFromUrl(sourceCfg.url).catch(err=>console.warn('Automatyczny import z URL nie powiódł się:',err))})} document.getElementById('usersBtn').onclick=openUsers; document.getElementById('refreshUsersBtn').onclick=refreshUsers; document.getElementById('createUserBtn').onclick=createManagedUser; document.getElementById('migrateCloudBtn').onclick=migrateToCloud; document.getElementById('loginBtn').onclick=async()=>{loginError.textContent='';loginBtn.disabled=true;try{if(!window.FleetAuth)throw new Error('Moduł logowania nie został załadowany.');await window.FleetAuth.signIn()}catch(e){loginError.textContent='Logowanie nie powiodło się: '+(e.message||e)}finally{loginBtn.disabled=false}}; document.getElementById('loginPassword').addEventListener('keydown',e=>{if(e.key==='Enter')loginBtn.click()}); document.getElementById('logoutBtn').onclick=async()=>{if(window.FleetAuth)await window.FleetAuth.signOut();location.reload()}; initAdminControls(); renderAll();
V25 • 29.09 • NOWY WYGLĄD