uprawnienia do stacji i usuwanie firm
This commit is contained in:
+27
@@ -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
@@ -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);
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user