uprawnienia do stacji i usuwanie firm

This commit is contained in:
Mateusz Gruszczyński
2026-07-13 15:17:14 +02:00
parent 5f6e4770c1
commit 6671ef5bcc
4 changed files with 137 additions and 19 deletions
+27
View File
@@ -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/<int:company_id>')
@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/<int:company_id>')
@role_required('boss','admin')
def companies_update(company_id):
+43 -18
View File
@@ -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='<td colspan="3" class="text-center text-body-secondary py-4">Brak pasujących stacji.</td>';
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);
};
input.addEventListener('input',filter);
input.addEventListener('search',filter);
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();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;
+15 -1
View File
@@ -55,9 +55,23 @@
<div class="col-md-4"><label class="form-label">Podział faktury po dniu</label><input class="form-control" type="number" min="1" max="28" name="invoice_split_day" value="{{ selected_company.invoice_split_day }}"></div>
<div class="col-12 d-flex justify-content-between align-items-center"><div class="form-check"><input class="form-check-input" name="active" type="checkbox" id="company-active" {% if selected_company.active %}checked{% endif %}><label class="form-check-label" for="company-active">Firma aktywna</label></div><button class="btn btn-primary">Zapisz ustawienia</button></div>
</form>
{% if current_user.role == 'admin' %}
<hr class="my-4">
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div><strong class="text-danger">Usunięcie firmy</strong><div class="small text-body-secondary">Możliwe tylko wtedy, gdy firma nie ma użytkowników, pojazdów ani kart paliwowych.</div></div>
<form action="/api/companies/{{ selected_company.id }}" data-method="DELETE" class="ajax-form" onsubmit="return confirm('Usunąć firmę {{ selected_company.name|e }}? Tej operacji nie można cofnąć.')">
<button class="btn btn-outline-danger">Usuń firmę</button>
</form>
</div>
{% endif %}
</div>
</div>
<div class="card mb-4"><div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3">
<div><h2 class="h5 mb-1">Dostęp do stacji</h2>{% if selected_company.allowed_stations %}<p class="text-danger mb-0">Aktywna lista zamknięta: {{ selected_company.allowed_stations|length }} dozwolonych stacji.</p>{% else %}<p class="text-body-secondary mb-0">Wszystkie aktywne stacje są dozwolone.</p>{% endif %}</div>
<a class="btn btn-secondary ajax-nav-link" href="{{ url_for('main.stations', company_id=selected_company.id) }}">Konfiguruj dostęp</a>
</div></div>
<div class="card mb-4"><div class="card-header"><strong>Karty paliwowe</strong></div><div class="card-body">
<form action="/api/fuel-cards" method="post" class="ajax-form row g-3 mb-4"><input type="hidden" name="company_id" value="{{selected_company.id}}"><div class="col-md-4"><label class="form-label">Nazwa karty</label><input class="form-control" name="name" placeholder="Np. UTA flota główna" required></div><div class="col-md-3"><label class="form-label">Operator karty</label><input class="form-control" name="provider" placeholder="UTA, DKV, Orlen" required></div><div class="col-md-3"><label class="form-label">Opis</label><input class="form-control" name="description" placeholder="Opcjonalnie"></div><div class="col-md-2 d-flex align-items-end"><button class="btn btn-primary w-100">Dodaj kartę</button></div></form>
<div class="table-responsive"><table class="table table-hover align-middle mb-0"><thead><tr><th>Karta</th><th>Operator karty</th><th>Opis</th><th>Status</th></tr></thead><tbody>{% for card in fuel_cards %}<tr><td><strong>{{card.name}}</strong></td><td>{{card.provider}}</td><td>{{card.description or '—'}}</td><td><span class="badge {% if card.active %}text-bg-success{% else %}text-bg-secondary{% endif %}">{{'aktywna' if card.active else 'wyłączona'}}</span></td></tr>{% else %}<tr><td colspan="4" class="text-center text-body-secondary py-4">Brak kart paliwowych.</td></tr>{% endfor %}</tbody></table></div>
@@ -65,7 +79,7 @@
<div class="card"><div class="card-body d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-3"><div><h2 class="h5 mb-1">Ulubione stacje firmy</h2><p class="text-body-secondary mb-0">Do 25 stacji. Pusta lista oznacza 10 największych sieci w Polsce.</p></div><button class="btn btn-secondary" data-bs-toggle="modal" data-bs-target="#companyFavoritesModal">Edytuj ulubione</button></div></div>
<div class="modal fade" id="companyFavoritesModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><form action="/api/companies/{{selected_company.id}}/favorite-stations" data-method="PUT" class="ajax-form favorite-station-form" data-max-favorites="25"><div class="modal-header"><h2 class="modal-title fs-5">Ulubione stacje: {{selected_company.name}}</h2><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p class="text-body-secondary">Ulubione wpływają tylko na kolejność propozycji i nie blokują pozostałych stacji.</p><input class="form-control mb-3 station-modal-search" placeholder="Szukaj po nazwie sieci, firmy lub punktu"><div class="table-responsive"><table class="table table-hover align-middle"><thead><tr><th></th><th>Nazwa</th><th class="text-end">Punkty</th></tr></thead><tbody>{% for s in stations %}<tr data-station-name="{{(s.display_name ~ ' ' ~ s.company_name ~ ' ' ~ (s.station_name_list|join(' ')))|lower}}"><td><input class="form-check-input" type="checkbox" name="station_ids" value="{{s.id}}" {% if s in selected_company.favorite_stations %}checked{% endif %}></td><td><strong>{{s.display_name}}</strong>{% if s.company_name != s.display_name %}<div class="small text-body-secondary">{{s.company_name}}</div>{% endif %}</td><td class="text-end">{{s.station_count}}</td></tr>{% endfor %}</tbody></table></div></div><div class="modal-footer"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button class="btn btn-primary">Zapisz</button></div></form></div></div></div>
<div class="modal fade" id="companyFavoritesModal" tabindex="-1"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><form action="/api/companies/{{selected_company.id}}/favorite-stations" data-method="PUT" class="ajax-form favorite-station-form" data-max-favorites="25" data-page-size="10"><div class="modal-header"><h2 class="modal-title fs-5">Ulubione stacje: {{selected_company.name}}</h2><button class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><p class="text-body-secondary">Ulubione wpływają tylko na kolejność propozycji i nie blokują pozostałych stacji.</p><input class="form-control mb-3 station-modal-search" type="search" placeholder="Szukaj po nazwie sieci, firmy lub punktu"><div class="table-responsive"><table class="table table-hover align-middle mb-2"><thead><tr><th></th><th>Nazwa</th><th class="text-end">Punkty</th></tr></thead><tbody>{% for s in stations %}<tr data-station-name="{{(s.display_name ~ ' ' ~ s.company_name ~ ' ' ~ (s.station_name_list|join(' ')))|lower}}"><td><input class="form-check-input" type="checkbox" name="station_ids" value="{{s.id}}" {% if s in selected_company.favorite_stations %}checked{% endif %}></td><td><strong>{{s.display_name}}</strong>{% if s.company_name != s.display_name %}<div class="small text-body-secondary">{{s.company_name}}</div>{% endif %}</td><td class="text-end">{{s.station_count}}</td></tr>{% endfor %}</tbody></table></div><div class="d-flex flex-column flex-sm-row justify-content-between align-items-sm-center gap-2"><small class="text-body-secondary favorite-pagination-summary"></small><nav aria-label="Strony stacji"><ul class="pagination pagination-sm mb-0 favorite-pagination"></ul></nav></div></div><div class="modal-footer position-sticky bottom-0 bg-body border-top"><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Anuluj</button><button type="submit" class="btn btn-primary">Zapisz ulubione</button></div></form></div></div></div>
{% else %}<div class="card"><div class="card-body text-center text-body-secondary py-5">Dodaj lub wybierz firmę.</div></div>{% endif %}
</div>
</div>
+52
View File
@@ -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