Files
fuel_track/app/static/js/stations.js
T
2026-07-13 15:17:14 +02:00

176 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(()=>{
const F=FuelTrack;
let searchController=null,searchSequence=0;
const normalize=value=>String(value??'').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,' ').trim();
const matches=(haystack,query)=>{const source=normalize(haystack),terms=normalize(query).split(/\s+/).filter(Boolean);return terms.every(term=>source.includes(term))};
F.bindStationLiveSearch=()=>{
const form=F.qs('#station-search-form');
if(!form||form.dataset.liveBound)return;
form.dataset.liveBound='1';
const run=F.debounce(async()=>{
const seq=++searchSequence;
searchController?.abort();
searchController=new AbortController();
const p=new URLSearchParams(new FormData(form));p.delete('page');
const url=`${form.action}?${p}`;
try{
const r=await fetch(url,{headers:{'X-Requested-With':'XMLHttpRequest'},signal:searchController.signal});
if(!r.ok)throw new Error('Nie udało się wyszukać stacji.');
const doc=new DOMParser().parseFromString(await r.text(),'text/html');
if(seq!==searchSequence)return;
const incoming=F.qs('main',doc),current=F.qs('main');
if(!incoming||!current)return;
bootstrap.Modal.getInstance(F.qs('#stationPointsModal'))?.hide();
current.replaceWith(incoming);
document.querySelectorAll('.modal-backdrop').forEach(x=>x.remove());
document.body.classList.remove('modal-open');
document.body.style.removeProperty('padding-right');
history.replaceState({},'',url);
F.initPage();
}catch(e){if(e.name!=='AbortError')F.notify(e.message,'danger')}
},300);
F.qs('[data-live-search]',form)?.addEventListener('input',run);
F.qsa('[data-live-sort]',form).forEach(x=>x.addEventListener('change',run));
};
F.bindFavoriteStations=()=>{
F.qsa('.favorite-station-form').forEach(form=>{
if(form.dataset.favoriteSearchBound)return;
form.dataset.favoriteSearchBound='1';
const input=F.qs('.station-modal-search',form),tbody=F.qs('tbody',form);
const pagination=F.qs('.favorite-pagination',form),summary=F.qs('.favorite-pagination-summary',form);
if(!input||!tbody)return;
const pageSize=Math.max(1,Number(form.dataset.pageSize||10));
let currentPage=1;
let empty=F.qs('.favorite-search-empty',tbody);
if(!empty){
empty=document.createElement('tr');empty.className='favorite-search-empty d-none';
empty.innerHTML='<td colspan="3" class="text-center text-body-secondary py-4">Brak pasujących stacji.</td>';
tbody.append(empty);
}
const rows=()=>F.qsa('tr[data-station-name]',tbody);
const filteredRows=()=>rows().filter(row=>matches(row.dataset.stationName||row.textContent,input.value));
const renderPagination=(totalPages,totalItems)=>{
if(summary)summary.textContent=totalItems?`Wyniki: ${totalItems} · strona ${currentPage} z ${totalPages}`:'Brak wyników';
if(!pagination)return;
pagination.innerHTML='';
if(totalPages<=1)return;
const add=(label,page,disabled=false,active=false)=>{
const li=document.createElement('li');li.className=`page-item${disabled?' disabled':''}${active?' active':''}`;
const button=document.createElement('button');button.type='button';button.className='page-link';button.textContent=label;
button.disabled=disabled;button.addEventListener('click',()=>{currentPage=page;render()});
li.append(button);pagination.append(li);
};
add('',Math.max(1,currentPage-1),currentPage===1);
const first=Math.max(1,currentPage-2),last=Math.min(totalPages,first+4);
for(let page=Math.max(1,last-4);page<=last;page++)add(String(page),page,false,page===currentPage);
add('',Math.min(totalPages,currentPage+1),currentPage===totalPages);
};
const render=()=>{
const matching=filteredRows();
const totalPages=Math.max(1,Math.ceil(matching.length/pageSize));
currentPage=Math.min(currentPage,totalPages);
const visibleSet=new Set(matching.slice((currentPage-1)*pageSize,currentPage*pageSize));
rows().forEach(row=>{
const show=visibleSet.has(row);
row.hidden=!show;row.classList.toggle('d-none',!show);
});
empty.hidden=matching.length!==0;empty.classList.toggle('d-none',matching.length!==0);
renderPagination(totalPages,matching.length);
};
input.addEventListener('input',()=>{currentPage=1;render()});
input.addEventListener('search',()=>{currentPage=1;render()});
const modal=form.closest('.modal');
modal?.addEventListener('shown.bs.modal',()=>{input.focus();render()});
modal?.addEventListener('hidden.bs.modal',()=>{input.value='';currentPage=1;render()});
form.addEventListener('change',event=>{
if(!event.target.matches('input[name="station_ids"]'))return;
const checked=F.qsa('input[name="station_ids"]:checked',form).length;
const max=Number(form.dataset.maxFavorites||25);
if(checked>max){event.target.checked=false;F.notify(`Możesz wybrać maksymalnie ${max} stacji.`,'danger')}
});
render();
});
};
F.bindStationFavoriteToggles=()=>{
F.qsa('.station-favorite-toggle').forEach(button=>{
if(button.dataset.bound)return;
button.dataset.bound='1';
button.addEventListener('click',async()=>{
const original=button.textContent;
button.disabled=true;
button.textContent='Zapisywanie…';
try{
const r=await fetch(button.dataset.url,{method:'POST',headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}});
const d=await r.json();
if(!r.ok||!d.ok)throw new Error(d.message||d.error||'Nie udało się zmienić ulubionych.');
const favorite=Boolean(d.data&&d.data.favorite);
button.textContent=favorite?'Usuń z ulubionych':'Dodaj do ulubionych';
F.notify(d.message||'Zapisano');
}catch(e){button.textContent=original;F.notify(e.message,'danger')}
finally{button.disabled=false}
});
});
};
// Zachowany jako no-op: formularz tankowania używa natywnego selecta z optgroup.
F.bindStationProposalSelect=()=>{};
F.bindStationPoints=()=>{
const modalEl=F.qs('#stationPointsModal');
if(!modalEl)return;
const search=F.qs('#stationPointsSearch',modalEl);
if(search&&!search.dataset.bound){
search.dataset.bound='1';
search.addEventListener('input',F.debounce(()=>F.loadStationPoints?.(),250));
}
};
F.loadStationPoints=async()=>{
const modalEl=F.qs('#stationPointsModal');
if(!modalEl||!modalEl.dataset.stationId)return;
const rows=F.qs('#stationPointsRows',modalEl),empty=F.qs('#stationPointsEmpty',modalEl),loading=F.qs('#stationPointsLoading',modalEl),count=F.qs('#stationPointsCount',modalEl),search=F.qs('#stationPointsSearch',modalEl);
const esc=v=>String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[c]));
loading.classList.remove('d-none');empty.classList.add('d-none');rows.innerHTML='';
try{
const r=await fetch(`/api/stations/${modalEl.dataset.stationId}/points?q=${encodeURIComponent(search.value.trim())}&per_page=100`,{headers:{Accept:'application/json','X-Requested-With':'XMLHttpRequest'}}),d=await r.json();
if(!r.ok||!d.ok)throw new Error(d.message||'Nie udało się pobrać punktów');
count.textContent=`Liczba punktów: ${d.data.total}`;
rows.innerHTML=d.data.items.map(x=>`<tr><td><strong>${esc(x.name)}</strong>${x.dkn?`<div class="small text-body-secondary">DKN: ${esc(x.dkn)}</div>`:''}</td><td>${esc(x.address)}</td><td>${esc(x.region||'—')}</td><td>${x.has_petrol?'<span class="badge text-bg-secondary me-1">benzyna</span>':''}${x.has_diesel?'<span class="badge text-bg-secondary me-1">diesel</span>':''}${x.has_lpg?'<span class="badge text-bg-secondary">LPG</span>':''}</td></tr>`).join('');
empty.classList.toggle('d-none',d.data.items.length>0);
}catch(e){empty.textContent=e.message;empty.classList.remove('d-none')}
finally{loading.classList.add('d-none')}
};
if(!document.documentElement.dataset.stationPointsDelegated){
document.documentElement.dataset.stationPointsDelegated='1';
document.addEventListener('click',e=>{
const b=e.target.closest('.station-points-open');
if(!b)return;
const modalEl=F.qs('#stationPointsModal');
if(!modalEl)return;
modalEl.dataset.stationId=b.dataset.stationId;
F.qs('#stationPointsTitle',modalEl).textContent=`Punkty: ${b.dataset.stationName}`;
F.qs('#stationPointsSearch',modalEl).value='';
F.qs('#stationPointsCount',modalEl).textContent='';
bootstrap.Modal.getOrCreateInstance(modalEl).show();
F.loadStationPoints();
});
}
F.bindStationInlineCompany=()=>{
F.qsa('.station-inline-company-select').forEach(select=>{
if(select.dataset.bound)return;
select.dataset.bound='1';
select.addEventListener('change',()=>{
const url=new URL(location.href);
url.searchParams.set('company_id',select.value);
url.searchParams.delete('page');
F.replaceMain(url.toString(),{push:true}).catch(e=>F.notify(e.message,'danger'));
});
});
};
})();