feat(admin): Add user-company assignment UI from companies panel
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
- Add "Users" button and modal in companies table to view/assign/unassign users - New endpoint POST /admin/companies/<id>/unassign-user to detach user from company - New endpoint GET /admin/users/list-all for user dropdown in assignment modal - Modal shows assigned users with "Unpin" button and dropdown for adding new ones Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
95c46444c4
commit
aa49c18f7a
@ -407,6 +407,27 @@ def admin_user_assign_company(user_id):
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/users/list-all')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_users_list_all():
|
||||
"""Get all users as JSON (for dropdowns)"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
users = db.query(User).order_by(User.name, User.email).all()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'users': [{
|
||||
'id': u.id,
|
||||
'name': u.name,
|
||||
'email': u.email,
|
||||
'company_id': u.company_id
|
||||
} for u in users]
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/users/<int:user_id>/delete', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.ADMIN)
|
||||
|
||||
@ -487,6 +487,39 @@ def admin_company_people(company_id):
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/companies/<int:company_id>/unassign-user', methods=['POST'])
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
def admin_company_unassign_user(company_id):
|
||||
"""Unassign a user from a company"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
company = db.query(Company).filter(Company.id == company_id).first()
|
||||
if not company:
|
||||
return jsonify({'success': False, 'error': 'Firma nie istnieje'}), 404
|
||||
|
||||
data = request.get_json() or {}
|
||||
user_id = data.get('user_id')
|
||||
if not user_id:
|
||||
return jsonify({'success': False, 'error': 'Nie podano ID użytkownika'}), 400
|
||||
|
||||
user = db.query(User).filter(User.id == user_id, User.company_id == company_id).first()
|
||||
if not user:
|
||||
return jsonify({'success': False, 'error': 'Użytkownik nie jest przypisany do tej firmy'}), 404
|
||||
|
||||
user.company_id = None
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Admin {current_user.email} unassigned user {user.email} from company {company.name}")
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'Użytkownik {user.email} odpięty od {company.name}'
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
@bp.route('/companies/<int:company_id>/users')
|
||||
@login_required
|
||||
@role_required(SystemRole.OFFICE_MANAGER)
|
||||
|
||||
@ -550,7 +550,12 @@
|
||||
<path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" onclick="openPeopleModal({{ company.id }}, '{{ company.name|e }}')" title="Osoby powiązane">
|
||||
<button class="btn-icon" onclick="openUsersModal({{ company.id }}, '{{ company.name|e }}')" title="Użytkownicy">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" onclick="openPeopleModal({{ company.id }}, '{{ company.name|e }}')" title="Osoby KRS">
|
||||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||||
<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"/>
|
||||
</svg>
|
||||
@ -705,6 +710,28 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Users Modal -->
|
||||
<div id="usersModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header" id="usersModalTitle">Użytkownicy firmy</div>
|
||||
<div class="modal-body">
|
||||
<div id="usersList" class="people-list">
|
||||
<div class="empty-state">Ładowanie...</div>
|
||||
</div>
|
||||
<div style="margin-top: var(--spacing-md); padding-top: var(--spacing-md); border-top: 1px solid var(--border);">
|
||||
<label class="form-label">Przypisz użytkownika</label>
|
||||
<div style="display: flex; gap: var(--spacing-sm);">
|
||||
<select id="assignUserSelect" class="form-control" style="flex: 1;"></select>
|
||||
<button class="btn btn-primary" onclick="assignUserToCompany()">Przypisz</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-secondary" onclick="closeUsersModal()">Zamknij</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Confirm Modal -->
|
||||
<div id="confirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
@ -951,6 +978,130 @@
|
||||
document.getElementById('peopleModal').classList.remove('active');
|
||||
}
|
||||
|
||||
// Users Modal
|
||||
let usersModalCompanyId = null;
|
||||
|
||||
async function openUsersModal(companyId, companyName) {
|
||||
usersModalCompanyId = companyId;
|
||||
document.getElementById('usersModalTitle').textContent = `Użytkownicy - ${companyName}`;
|
||||
document.getElementById('usersList').innerHTML = '<div class="empty-state">Ładowanie...</div>';
|
||||
document.getElementById('usersModal').classList.add('active');
|
||||
await loadCompanyUsers(companyId);
|
||||
await loadAvailableUsers();
|
||||
}
|
||||
|
||||
function closeUsersModal() {
|
||||
usersModalCompanyId = null;
|
||||
document.getElementById('usersModal').classList.remove('active');
|
||||
}
|
||||
|
||||
async function loadCompanyUsers(companyId) {
|
||||
try {
|
||||
const response = await fetch(`/admin/companies/${companyId}/users`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
if (data.users.length === 0) {
|
||||
document.getElementById('usersList').innerHTML = '<div class="empty-state">Brak przypisanych użytkowników</div>';
|
||||
} else {
|
||||
let html = '';
|
||||
data.users.forEach(u => {
|
||||
html += `
|
||||
<div class="people-item">
|
||||
<div class="people-info">
|
||||
<div class="people-name">${u.name || u.email}</div>
|
||||
<div class="people-role">${u.email} · ${u.role || 'user'}</div>
|
||||
</div>
|
||||
<button class="btn btn-secondary" style="padding: 4px 8px; font-size: var(--font-size-xs);" onclick="unassignUser(${companyId}, ${u.id}, '${u.email}')">Odepnij</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
document.getElementById('usersList').innerHTML = html;
|
||||
}
|
||||
} else {
|
||||
document.getElementById('usersList').innerHTML = '<div class="empty-state">Błąd pobierania danych</div>';
|
||||
}
|
||||
} catch (error) {
|
||||
document.getElementById('usersList').innerHTML = '<div class="empty-state">Błąd połączenia</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAvailableUsers() {
|
||||
try {
|
||||
const response = await fetch('/admin/users/list-all');
|
||||
const data = await response.json();
|
||||
|
||||
const select = document.getElementById('assignUserSelect');
|
||||
select.innerHTML = '<option value="">-- Wybierz użytkownika --</option>';
|
||||
|
||||
if (data.success) {
|
||||
data.users.filter(u => !u.company_id).forEach(u => {
|
||||
const option = document.createElement('option');
|
||||
option.value = u.id;
|
||||
option.textContent = `${u.name || u.email} (${u.email})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading users:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function assignUserToCompany() {
|
||||
if (!usersModalCompanyId) return;
|
||||
const userId = document.getElementById('assignUserSelect').value;
|
||||
if (!userId) {
|
||||
showToast('Wybierz użytkownika', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/companies/${usersModalCompanyId}/assign-user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ user_id: parseInt(userId) })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
await loadCompanyUsers(usersModalCompanyId);
|
||||
await loadAvailableUsers();
|
||||
} else {
|
||||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Błąd połączenia', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function unassignUser(companyId, userId, userEmail) {
|
||||
try {
|
||||
const response = await fetch(`/admin/companies/${companyId}/unassign-user`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRFToken': csrfToken
|
||||
},
|
||||
body: JSON.stringify({ user_id: userId })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
if (data.success) {
|
||||
showToast(data.message, 'success');
|
||||
await loadCompanyUsers(companyId);
|
||||
await loadAvailableUsers();
|
||||
} else {
|
||||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
showToast('Błąd połączenia', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Delete (Archive) Company
|
||||
function deleteCompany(companyId, companyName) {
|
||||
document.getElementById('confirmIcon').className = 'modal-icon warning';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user