From 6671ef5bcc503478378bacce745afe7bbbca59fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Mon, 13 Jul 2026 15:17:14 +0200 Subject: [PATCH] uprawnienia do stacji i usuwanie firm --- app/api.py | 27 +++++++++++++ app/static/js/stations.js | 61 +++++++++++++++++++++--------- app/templates/admin_companies.html | 16 +++++++- tests/test_app.py | 52 +++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 19 deletions(-) diff --git a/app/api.py b/app/api.py index 195fa3a..9539fb2 100644 --- a/app/api.py +++ b/app/api.py @@ -396,6 +396,33 @@ def companies_create(): db.session.add(c);db.session.commit();return response({'id':c.id,'redirect':url_for('main.admin_companies',company_id=c.id)},'Dodano firmę',201) except Exception as exc:db.session.rollback();return fail(str(exc)) +@api.delete('/companies/') +@role_required('admin') +def companies_delete(company_id): + company = CompanySettings.query.get_or_404(company_id) + blockers = [] + users_count = User.query.filter_by(company_id=company.id).count() + vehicles_count = Vehicle.query.filter_by(company_id=company.id).count() + cards_count = FuelCard.query.filter_by(company_id=company.id).count() + if users_count: + blockers.append(f'{users_count} użytkowników') + if vehicles_count: + blockers.append(f'{vehicles_count} pojazdów') + if cards_count: + blockers.append(f'{cards_count} kart paliwowych') + if blockers: + return fail( + 'Nie można usunąć firmy, ponieważ ma przypisane: ' + ', '.join(blockers) + + '. Najpierw przenieś lub usuń te dane albo dezaktywuj firmę.', + 409, + ) + company.favorite_stations.clear() + company.allowed_stations.clear() + db.session.delete(company) + db.session.commit() + socketio.emit('app_changed', {'resource':'company','deleted_id':company_id}) + return response({'redirect':url_for('main.admin_companies')}, 'Usunięto firmę') + @api.put('/companies/') @role_required('boss','admin') def companies_update(company_id): diff --git a/app/static/js/stations.js b/app/static/js/stations.js index efdfb8e..9307240 100644 --- a/app/static/js/stations.js +++ b/app/static/js/stations.js @@ -39,36 +39,61 @@ F.bindFavoriteStations=()=>{ 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='Brak pasujących stacji.'; tbody.append(empty); } - const filter=()=>{ - const rows=F.qsa('tr[data-station-name]',tbody); - let visible=0; - rows.forEach(row=>{ - const searchable=row.dataset.stationName||row.textContent; - const show=matches(searchable,input.value); - row.hidden=!show; - row.classList.toggle('d-none',!show); - if(show)visible++; - }); - empty.hidden=visible!==0; - empty.classList.toggle('d-none',visible!==0); + 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); }; - input.addEventListener('input',filter); - input.addEventListener('search',filter); + 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();filter()}); - modal?.addEventListener('hidden.bs.modal',()=>{input.value='';filter()}); - filter(); + 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; diff --git a/app/templates/admin_companies.html b/app/templates/admin_companies.html index b464fea..76d968b 100644 --- a/app/templates/admin_companies.html +++ b/app/templates/admin_companies.html @@ -55,9 +55,23 @@
+ {% if current_user.role == 'admin' %} +
+
+
Usunięcie firmy
Możliwe tylko wtedy, gdy firma nie ma użytkowników, pojazdów ani kart paliwowych.
+
+ +
+
+ {% endif %} +
+

Dostęp do stacji

{% if selected_company.allowed_stations %}

Aktywna lista zamknięta: {{ selected_company.allowed_stations|length }} dozwolonych stacji.

{% else %}

Wszystkie aktywne stacje są dozwolone.

{% endif %}
+ Konfiguruj dostęp +
+
Karty paliwowe
{% for card in fuel_cards %}{% else %}{% endfor %}
KartaOperator kartyOpisStatus
{{card.name}}{{card.provider}}{{card.description or '—'}}{{'aktywna' if card.active else 'wyłączona'}}
Brak kart paliwowych.
@@ -65,7 +79,7 @@

Ulubione stacje firmy

Do 25 stacji. Pusta lista oznacza 10 największych sieci w Polsce.

- + {% else %}
Dodaj lub wybierz firmę.
{% endif %}
diff --git a/tests/test_app.py b/tests/test_app.py index 2820dab..c58ab2b 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -200,3 +200,55 @@ def test_openapi_contains_password_flow(tmp_path): password_flow = spec['components']['securitySchemes']['oauth2Password']['flows']['password'] assert password_flow['tokenUrl'] == '/api/auth/token' assert '/api/auth/token' in spec['paths'] + + +def test_admin_can_delete_empty_company(tmp_path): + from app.extensions import db + from app.models import CompanySettings + + app = make_app(tmp_path, "delete-company.db") + client = app.test_client() + client.post('/api/auth/login', data={'email':'admin@example.com','password':'admin123!'}) + with app.app_context(): + company = CompanySettings(name='Firma do usunięcia', region='mazowieckie') + db.session.add(company) + db.session.commit() + company_id = company.id + response = client.delete(f'/api/companies/{company_id}') + assert response.status_code == 200 + assert response.json['ok'] is True + with app.app_context(): + assert db.session.get(CompanySettings, company_id) is None + + +def test_company_with_users_cannot_be_deleted(tmp_path): + from app.extensions import db + from app.models import CompanySettings, User + + app = make_app(tmp_path, "delete-company-blocked.db") + client = app.test_client() + client.post('/api/auth/login', data={'email':'admin@example.com','password':'admin123!'}) + with app.app_context(): + company = CompanySettings(name='Firma z użytkownikiem', region='mazowieckie') + db.session.add(company) + db.session.flush() + user = User(name='Pracownik', email='pracownik@example.com', role='user', company_id=company.id) + user.set_password('Haslo123!') + db.session.add(user) + db.session.commit() + company_id = company.id + response = client.delete(f'/api/companies/{company_id}') + assert response.status_code == 409 + assert 'użytkowników' in response.json['message'] + + +def test_company_settings_show_station_access_and_favorite_pagination(tmp_path): + app = make_app(tmp_path, "company-station-ui.db") + client = app.test_client() + client.post('/login', data={'email':'admin@example.com','password':'admin123!'}) + html = client.get('/admin/companies').get_data(as_text=True) + assert 'Konfiguruj dostęp' in html + assert 'data-page-size="10"' in html + assert 'favorite-pagination' in html + assert 'Zapisz ulubione' in html + assert 'Usuń firmę' in html