feat(company): add team management for company MANAGERs
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

Allow company MANAGERs to add/remove users, change roles,
and manage granular permissions from the company edit page.
New "Zespół" tab with AJAX-based team CRUD.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-02 14:44:05 +02:00
parent c5efd4056c
commit 49555f6a2e
3 changed files with 658 additions and 0 deletions

View File

@ -15,3 +15,4 @@ from . import routes_pej # noqa: E402, F401
from . import routes_announcements # noqa: E402, F401
from . import routes_company_edit # noqa: E402, F401
from . import routes_rss # noqa: E402, F401
from . import routes_team # noqa: E402, F401

View File

@ -0,0 +1,379 @@
"""
Team Management Routes
======================
Routes for company MANAGERs to manage their team members:
add/remove users, change roles, toggle permissions.
"""
import re
import secrets
import string
from datetime import datetime, timedelta
from flask import request, jsonify
from flask_login import login_required, current_user
from werkzeug.security import generate_password_hash
from blueprints.public import bp
from database import (
SessionLocal, User, Company, UserCompany,
UserCompanyPermissions, SystemRole, CompanyRole,
)
from email_service import send_welcome_activation_email
from extensions import limiter
from utils.permissions import can_invite_user_to_company
import logging
import os
logger = logging.getLogger(__name__)
EMAIL_RE = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
PERMISSION_KEYS = [
'can_edit_description', 'can_edit_services', 'can_edit_contacts',
'can_edit_social', 'can_manage_classifieds', 'can_post_forum',
'can_view_analytics',
]
def _require_team_manager(company_id):
"""Check if current user can manage this company's team. Returns (db, company) or raises."""
if not can_invite_user_to_company(current_user, company_id):
return None, None
db = SessionLocal()
company = db.query(Company).filter(Company.id == company_id).first()
if not company:
db.close()
return None, None
return db, company
@bp.route('/firma/<int:company_id>/zespol')
@login_required
def team_list(company_id):
"""List team members with roles and permissions."""
db, company = _require_team_manager(company_id)
if not db:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
members = db.query(UserCompany).filter(
UserCompany.company_id == company_id
).all()
result = []
for uc in members:
user = db.query(User).filter(User.id == uc.user_id).first()
if not user:
continue
perms = db.query(UserCompanyPermissions).filter_by(
user_id=user.id, company_id=company_id
).first()
member_data = {
'id': user.id,
'name': user.name or user.email,
'email': user.email,
'phone': user.phone,
'role': uc.role,
'is_primary': uc.is_primary,
'is_active': user.is_active,
'is_current_user': user.id == current_user.id,
'last_login': user.last_login.isoformat() if user.last_login else None,
'permissions': {},
}
if perms:
for key in PERMISSION_KEYS:
member_data['permissions'][key] = getattr(perms, key, False)
result.append(member_data)
# Sort: MANAGERs first, then by name
role_order = {'MANAGER': 0, 'EMPLOYEE': 1, 'VIEWER': 2, 'NONE': 3}
result.sort(key=lambda m: (role_order.get(m['role'], 9), (m['name'] or '').lower()))
return jsonify({'success': True, 'members': result, 'company_name': company.name})
except Exception as e:
logger.error(f"Error listing team for company {company_id}: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas pobierania zespołu'}), 500
finally:
db.close()
@bp.route('/firma/<int:company_id>/zespol/dodaj', methods=['POST'])
@login_required
@limiter.limit("20 per hour")
def team_add_member(company_id):
"""Add a user to the company team."""
db, company = _require_team_manager(company_id)
if not db:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
data = request.get_json() or {}
email = (data.get('email') or '').strip().lower()
name = (data.get('name') or '').strip()
role_str = (data.get('role') or 'EMPLOYEE').upper()
# Validate
if not email or not EMAIL_RE.match(email):
return jsonify({'success': False, 'error': 'Podaj prawidłowy adres email'}), 400
if not name:
return jsonify({'success': False, 'error': 'Podaj imię i nazwisko'}), 400
if role_str not in ('VIEWER', 'EMPLOYEE', 'MANAGER'):
return jsonify({'success': False, 'error': 'Nieprawidłowa rola'}), 400
# Check if already in this team
existing_user = db.query(User).filter(User.email == email).first()
if existing_user:
existing_uc = db.query(UserCompany).filter_by(
user_id=existing_user.id, company_id=company_id
).first()
if existing_uc:
return jsonify({'success': False, 'error': 'Ta osoba już należy do Twojego zespołu'}), 400
is_new_account = False
if existing_user:
# Existing user — just link to company
user = existing_user
is_primary = user.company_id is None # primary if no company yet
uc = UserCompany(
user_id=user.id,
company_id=company_id,
role=role_str,
is_primary=is_primary,
)
db.add(uc)
if is_primary:
user.company_id = company_id
user.company_role = role_str
user.is_norda_member = company.status == 'active'
else:
# New user — create account
is_new_account = True
password_chars = string.ascii_letters + string.digits + "!@#$%^&*"
generated_password = ''.join(secrets.choice(password_chars) for _ in range(16))
password_hash = generate_password_hash(generated_password, method='pbkdf2:sha256')
user = User(
email=email,
password_hash=password_hash,
name=name,
company_id=company_id,
is_verified=True,
is_active=True,
is_norda_member=company.status == 'active',
)
user.set_role(SystemRole.EMPLOYEE)
user.set_company_role(CompanyRole[role_str])
db.add(user)
db.flush() # get user.id
uc = UserCompany(
user_id=user.id,
company_id=company_id,
role=role_str,
is_primary=True,
)
db.add(uc)
# Create permissions
db.flush()
UserCompanyPermissions.get_or_create(db, user.id, company_id)
db.commit()
# Send activation email for new accounts
if is_new_account:
try:
token = secrets.token_urlsafe(32)
user.reset_token = token
user.reset_token_expires = datetime.now() + timedelta(hours=72)
db.commit()
app_url = os.getenv('APP_URL', 'https://nordabiznes.pl')
reset_url = f"{app_url}/reset-password/{token}"
send_welcome_activation_email(email, name, reset_url)
logger.info(f"Activation email sent to {email} for company {company_id}")
except Exception as mail_err:
logger.error(f"Failed to send activation email to {email}: {mail_err}")
logger.info(
f"Manager {current_user.email} added {email} (role={role_str}) "
f"to company {company_id} (new_account={is_new_account})"
)
return jsonify({
'success': True,
'user_id': user.id,
'is_new_account': is_new_account,
'message': f'{"Konto utworzone i email" if is_new_account else "Użytkownik"} '
f'został dodany do zespołu',
})
except Exception as e:
db.rollback()
logger.error(f"Error adding team member to company {company_id}: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas dodawania osoby'}), 500
finally:
db.close()
@bp.route('/firma/<int:company_id>/zespol/<int:user_id>/rola', methods=['POST'])
@login_required
def team_change_role(company_id, user_id):
"""Change a team member's company role."""
if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz zmienić własnej roli'}), 400
db, company = _require_team_manager(company_id)
if not db:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
data = request.get_json() or {}
role_str = (data.get('role') or '').upper()
if role_str not in ('VIEWER', 'EMPLOYEE', 'MANAGER'):
return jsonify({'success': False, 'error': 'Nieprawidłowa rola'}), 400
uc = db.query(UserCompany).filter_by(
user_id=user_id, company_id=company_id
).first()
if not uc:
return jsonify({'success': False, 'error': 'Użytkownik nie należy do tego zespołu'}), 404
uc.role = role_str
# Sync user.company_role if this is their primary company
user = db.query(User).filter(User.id == user_id).first()
if user and user.company_id == company_id:
user.set_company_role(CompanyRole[role_str])
# Update permissions when role changes
perms = UserCompanyPermissions.get_or_create(db, user_id, company_id)
if role_str == 'MANAGER':
perms.can_edit_contacts = True
perms.can_edit_social = True
perms.can_view_analytics = True
elif role_str in ('EMPLOYEE', 'VIEWER'):
perms.can_edit_contacts = False
perms.can_edit_social = False
perms.can_view_analytics = False
perms.granted_by_id = current_user.id
perms.updated_at = datetime.now()
db.commit()
logger.info(f"Manager {current_user.email} changed role of user {user_id} to {role_str} in company {company_id}")
return jsonify({'success': True, 'message': f'Rola zmieniona na {role_str}'})
except Exception as e:
db.rollback()
logger.error(f"Error changing role for user {user_id} in company {company_id}: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas zmiany roli'}), 500
finally:
db.close()
@bp.route('/firma/<int:company_id>/zespol/<int:user_id>/uprawnienia', methods=['POST'])
@login_required
def team_toggle_permission(company_id, user_id):
"""Toggle a specific permission for a team member."""
db, company = _require_team_manager(company_id)
if not db:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
data = request.get_json() or {}
perm_key = data.get('permission')
perm_value = bool(data.get('value'))
if perm_key not in PERMISSION_KEYS:
return jsonify({'success': False, 'error': 'Nieprawidłowe uprawnienie'}), 400
# Only toggle permissions for EMPLOYEE role
uc = db.query(UserCompany).filter_by(
user_id=user_id, company_id=company_id
).first()
if not uc:
return jsonify({'success': False, 'error': 'Użytkownik nie należy do tego zespołu'}), 404
if uc.role != 'EMPLOYEE':
return jsonify({'success': False, 'error': 'Uprawnienia można zmieniać tylko pracownikom'}), 400
perms = UserCompanyPermissions.get_or_create(db, user_id, company_id)
setattr(perms, perm_key, perm_value)
perms.granted_by_id = current_user.id
perms.updated_at = datetime.now()
db.commit()
logger.info(f"Manager {current_user.email} set {perm_key}={perm_value} for user {user_id} in company {company_id}")
return jsonify({'success': True, 'message': 'Uprawnienie zaktualizowane'})
except Exception as e:
db.rollback()
logger.error(f"Error toggling permission for user {user_id}: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas zmiany uprawnienia'}), 500
finally:
db.close()
@bp.route('/firma/<int:company_id>/zespol/<int:user_id>/usun', methods=['POST'])
@login_required
def team_remove_member(company_id, user_id):
"""Remove a user from the company team (does not delete the account)."""
if user_id == current_user.id:
return jsonify({'success': False, 'error': 'Nie możesz usunąć siebie z zespołu'}), 400
db, company = _require_team_manager(company_id)
if not db:
return jsonify({'success': False, 'error': 'Brak uprawnień'}), 403
try:
uc = db.query(UserCompany).filter_by(
user_id=user_id, company_id=company_id
).first()
if not uc:
return jsonify({'success': False, 'error': 'Użytkownik nie należy do tego zespołu'}), 404
user = db.query(User).filter(User.id == user_id).first()
user_name = (user.name or user.email) if user else '?'
# Delete permissions
db.query(UserCompanyPermissions).filter_by(
user_id=user_id, company_id=company_id
).delete()
# Delete company association
db.delete(uc)
# Clear primary company link if this was user's primary
if user and user.company_id == company_id:
user.company_id = None
user.set_company_role(CompanyRole.NONE)
# Check if user has other company associations
other_uc = db.query(UserCompany).filter(
UserCompany.user_id == user_id,
UserCompany.company_id != company_id
).first()
if not other_uc:
user.is_norda_member = False
db.commit()
logger.info(f"Manager {current_user.email} removed user {user_id} ({user_name}) from company {company_id}")
return jsonify({'success': True, 'message': f'{user_name} został usunięty z zespołu'})
except Exception as e:
db.rollback()
logger.error(f"Error removing user {user_id} from company {company_id}: {e}")
return jsonify({'success': False, 'error': 'Błąd podczas usuwania osoby'}), 500
finally:
db.close()

View File

@ -292,6 +292,44 @@
.ce-tab[data-tab="social"].active svg { color: #ec4899; }
.ce-tab[data-tab="visibility"].active { color: #8b5cf6; border-bottom-color: #8b5cf6; }
.ce-tab[data-tab="visibility"].active svg { color: #8b5cf6; }
.ce-tab[data-tab="team"].active { color: #0ea5e9; border-bottom-color: #0ea5e9; }
.ce-tab[data-tab="team"].active svg { color: #0ea5e9; }
/* Team management */
.team-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-lg); flex-wrap: wrap; gap: var(--spacing-sm); }
.team-header h3 { margin: 0; font-size: var(--font-size-lg); color: var(--text-primary); }
.team-add-form { background: var(--surface-alt, #f8fafc); border-radius: var(--radius); padding: var(--spacing-lg); margin-bottom: var(--spacing-lg); border: 1px solid var(--border-light, #e2e8f0); }
.team-add-form .form-row { display: grid; grid-template-columns: 1fr 1fr auto; gap: var(--spacing-md); align-items: end; }
.team-add-form .form-row-bottom { display: grid; grid-template-columns: 1fr auto; gap: var(--spacing-md); align-items: end; margin-top: var(--spacing-md); }
.team-add-form label { display: block; font-size: var(--font-size-sm); font-weight: 500; color: var(--text-secondary); margin-bottom: 4px; }
.team-member-card { display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); border-bottom: 1px solid var(--border-light, #e2e8f0); flex-wrap: wrap; }
.team-member-card:last-child { border-bottom: none; }
.team-member-avatar { width: 42px; height: 42px; border-radius: 50%; background: var(--primary, #2E4872); color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 600; font-size: 15px; flex-shrink: 0; }
.team-member-info { flex: 1; min-width: 150px; }
.team-member-name { font-weight: 600; color: var(--text-primary); }
.team-member-email { font-size: var(--font-size-sm); color: var(--text-secondary); }
.team-member-actions { display: flex; align-items: center; gap: var(--spacing-sm); flex-wrap: wrap; }
.team-role-select { padding: 6px 10px; border-radius: var(--radius); border: 1px solid var(--border-light, #e2e8f0); font-size: var(--font-size-sm); background: var(--surface, #fff); }
.team-role-badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 12px; font-weight: 600; }
.team-role-badge.manager { background: #dbeafe; color: #1e40af; }
.team-role-badge.employee { background: #dcfce7; color: #166534; }
.team-role-badge.viewer { background: #f3e8ff; color: #6b21a8; }
.team-btn-remove { padding: 6px 12px; border-radius: var(--radius); border: 1px solid #fecaca; background: #fff; color: #dc2626; font-size: var(--font-size-sm); cursor: pointer; transition: all 0.2s; }
.team-btn-remove:hover { background: #fef2f2; border-color: #dc2626; }
.team-perms-toggle { margin-top: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: var(--surface-alt, #f8fafc); border-radius: var(--radius); }
.team-perms-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--spacing-xs); }
.team-perm-item { display: flex; align-items: center; gap: 8px; font-size: var(--font-size-sm); padding: 4px 0; }
.team-perm-item input[type="checkbox"] { accent-color: var(--primary, #2E4872); width: 16px; height: 16px; }
.team-empty { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
.team-loading { text-align: center; padding: var(--spacing-xl); color: var(--text-secondary); }
.team-you-badge { font-size: 11px; background: #e0f2fe; color: #0369a1; padding: 1px 6px; border-radius: 8px; margin-left: 6px; }
@media (max-width: 768px) {
.team-add-form .form-row { grid-template-columns: 1fr; }
.team-add-form .form-row-bottom { grid-template-columns: 1fr; }
.team-member-card { padding: var(--spacing-md); }
.team-member-actions { width: 100%; justify-content: flex-end; }
.team-perms-grid { grid-template-columns: 1fr; }
}
/* Tab content */
.ce-tab-content { display: none; padding: var(--spacing-xl); }
@ -751,6 +789,12 @@
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/></svg>
<span class="tab-label">Widoczność</span>
</button>
{% if current_user.can_manage_company(company.id) %}
<button type="button" class="ce-tab" data-tab="team">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
<span class="tab-label">Zespół</span>
</button>
{% endif %}
</div>
<form method="POST" action="{{ url_for('public.company_edit_save', company_id=company.id) }}" id="companyEditForm" enctype="multipart/form-data">
@ -1135,6 +1179,51 @@
<a href="{{ url_for('public.company_detail', company_id=company.id) }}" class="btn btn-outline">Anuluj</a>
</div>
</form>
{% if current_user.can_manage_company(company.id) %}
<!-- TAB 6: Zespół (outside form — uses AJAX) -->
<div class="ce-tab-content" id="tab-team">
<div class="team-header">
<h3>Zespół firmy</h3>
<button type="button" class="btn btn-primary btn-sm" onclick="toggleAddForm()">+ Dodaj osobę</button>
</div>
<div class="team-add-form" id="teamAddForm" style="display:none;">
<div class="form-row">
<div>
<label for="teamEmail">Email</label>
<input type="email" id="teamEmail" class="form-input" placeholder="jan@example.com">
</div>
<div>
<label for="teamName">Imię i nazwisko</label>
<input type="text" id="teamName" class="form-input" placeholder="Jan Kowalski">
</div>
</div>
<div class="form-row-bottom">
<div>
<label for="teamRole">Rola</label>
<select id="teamRole" class="form-input">
<option value="EMPLOYEE">Pracownik — może edytować dane firmy</option>
<option value="VIEWER">Obserwator — może przeglądać dashboard</option>
<option value="MANAGER">Kadra zarządzająca — pełna kontrola</option>
</select>
</div>
<div>
<button type="button" class="btn btn-primary" onclick="addTeamMember()">Dodaj</button>
<button type="button" class="btn btn-outline" onclick="toggleAddForm()" style="margin-left:4px;">Anuluj</button>
</div>
</div>
<p style="font-size:var(--font-size-sm); color:var(--text-secondary); margin:var(--spacing-sm) 0 0;">
Nowy użytkownik otrzyma email z linkiem do ustawienia hasła.
Istniejący użytkownik portalu zostanie od razu dodany do zespołu.
</p>
</div>
<div id="teamMembersList">
<div class="team-loading">Ładowanie zespołu...</div>
</div>
</div>
{% endif %}
</div>
<div class="ce-preview" id="livePreview">
@ -1578,4 +1667,193 @@ if (previewOverlay) {
if (e.target === this) closeMobilePreview();
});
}
// Toast notifications
function showToast(message, type, duration) {
type = type || 'info';
duration = duration || 4000;
var toast = document.createElement('div');
toast.className = 'flash flash-' + type;
toast.innerHTML = '<span>' + message + '</span><button class="flash-close" onclick="this.parentElement.remove()">&times;</button>';
toast.style.cssText = 'position:fixed;top:20px;right:20px;z-index:9999;min-width:280px;max-width:480px;animation:fadeIn 0.3s;';
document.body.appendChild(toast);
setTimeout(function() { if (toast.parentElement) toast.remove(); }, duration);
}
{% if current_user.can_manage_company(company.id) %}
// ===== Team Management =====
var TEAM_COMPANY_ID = {{ company.id }};
var teamLoaded = false;
var CSRF_TOKEN = '{{ csrf_token() }}';
var PERM_LABELS = {
'can_edit_description': 'Edycja opisu',
'can_edit_services': 'Edycja usług',
'can_edit_contacts': 'Edycja kontaktów',
'can_edit_social': 'Edycja social media',
'can_manage_classifieds': 'Zarządzanie ogłoszeniami',
'can_post_forum': 'Posty na forum',
'can_view_analytics': 'Podgląd statystyk'
};
// Hook into tab switcher — lazy load team on first click
document.addEventListener('click', function(e) {
var tab = e.target.closest('.ce-tab[data-tab="team"]');
if (tab && !teamLoaded) {
teamLoaded = true;
loadTeamMembers();
}
});
function toggleAddForm() {
var form = document.getElementById('teamAddForm');
form.style.display = form.style.display === 'none' ? 'block' : 'none';
if (form.style.display === 'block') document.getElementById('teamEmail').focus();
}
function loadTeamMembers() {
var list = document.getElementById('teamMembersList');
list.innerHTML = '<div class="team-loading">Ładowanie...</div>';
fetch('/firma/' + TEAM_COMPANY_ID + '/zespol')
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) { list.innerHTML = '<div class="team-empty">Błąd: ' + (data.error || '') + '</div>'; return; }
if (!data.members.length) { list.innerHTML = '<div class="team-empty">Brak członków zespołu. Dodaj pierwszą osobę.</div>'; return; }
var html = '';
data.members.forEach(function(m) {
var initials = (m.name || '?').split(' ').map(function(w) { return w[0]; }).join('').substring(0, 2).toUpperCase();
var roleClass = m.role === 'MANAGER' ? 'manager' : (m.role === 'EMPLOYEE' ? 'employee' : 'viewer');
var roleLabel = m.role === 'MANAGER' ? 'Kadra zarządzająca' : (m.role === 'EMPLOYEE' ? 'Pracownik' : 'Obserwator');
html += '<div class="team-member-card" data-user-id="' + m.id + '">';
html += '<div class="team-member-avatar">' + initials + '</div>';
html += '<div class="team-member-info">';
html += '<div class="team-member-name">' + escapeHtml(m.name || m.email);
if (m.is_current_user) html += '<span class="team-you-badge">Ty</span>';
html += '</div>';
html += '<div class="team-member-email">' + escapeHtml(m.email) + '</div>';
html += '</div>';
html += '<div class="team-member-actions">';
if (!m.is_current_user) {
html += '<select class="team-role-select" onchange="changeRole(' + m.id + ', this.value)">';
html += '<option value="VIEWER"' + (m.role === 'VIEWER' ? ' selected' : '') + '>Obserwator</option>';
html += '<option value="EMPLOYEE"' + (m.role === 'EMPLOYEE' ? ' selected' : '') + '>Pracownik</option>';
html += '<option value="MANAGER"' + (m.role === 'MANAGER' ? ' selected' : '') + '>Kadra zarządzająca</option>';
html += '</select>';
html += '<button type="button" class="team-btn-remove" onclick="removeTeamMember(' + m.id + ', \'' + escapeHtml(m.name || m.email).replace(/'/g, "\\'") + '\')">Usuń</button>';
} else {
html += '<span class="team-role-badge ' + roleClass + '">' + roleLabel + '</span>';
}
html += '</div>';
// Permissions for EMPLOYEE
if (m.role === 'EMPLOYEE' && !m.is_current_user && m.permissions) {
html += '<div class="team-perms-toggle" style="width:100%;">';
html += '<div style="font-size:var(--font-size-sm);font-weight:500;margin-bottom:6px;color:var(--text-secondary);">Uprawnienia:</div>';
html += '<div class="team-perms-grid">';
Object.keys(PERM_LABELS).forEach(function(key) {
var checked = m.permissions[key] ? ' checked' : '';
html += '<label class="team-perm-item">';
html += '<input type="checkbox"' + checked + ' onchange="togglePermission(' + m.id + ', \'' + key + '\', this.checked)">';
html += PERM_LABELS[key];
html += '</label>';
});
html += '</div></div>';
}
html += '</div>';
});
list.innerHTML = html;
})
.catch(function() { list.innerHTML = '<div class="team-empty">Nie udało się załadować zespołu.</div>'; });
}
function addTeamMember() {
var email = document.getElementById('teamEmail').value.trim();
var name = document.getElementById('teamName').value.trim();
var role = document.getElementById('teamRole').value;
if (!email) { showToast('Podaj adres email', 'error'); return; }
if (!name) { showToast('Podaj imię i nazwisko', 'error'); return; }
fetch('/firma/' + TEAM_COMPANY_ID + '/zespol/dodaj', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
body: JSON.stringify({ email: email, name: name, role: role })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showToast(data.message, 'success');
document.getElementById('teamEmail').value = '';
document.getElementById('teamName').value = '';
document.getElementById('teamAddForm').style.display = 'none';
loadTeamMembers();
} else {
showToast(data.error || 'Błąd', 'error');
}
})
.catch(function() { showToast('Błąd połączenia', 'error'); });
}
function changeRole(userId, newRole) {
fetch('/firma/' + TEAM_COMPANY_ID + '/zespol/' + userId + '/rola', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
body: JSON.stringify({ role: newRole })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
showToast(data.message, 'success');
loadTeamMembers(); // Re-render to update permissions section
} else {
showToast(data.error || 'Błąd', 'error');
loadTeamMembers(); // Revert select
}
})
.catch(function() { showToast('Błąd połączenia', 'error'); });
}
function togglePermission(userId, permKey, value) {
fetch('/firma/' + TEAM_COMPANY_ID + '/zespol/' + userId + '/uprawnienia', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN },
body: JSON.stringify({ permission: permKey, value: value })
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (!data.success) { showToast(data.error || 'Błąd', 'error'); loadTeamMembers(); }
})
.catch(function() { showToast('Błąd połączenia', 'error'); });
}
function removeTeamMember(userId, userName) {
nordaConfirm(
'Usunąć ' + userName + ' z zespołu?',
'Konto użytkownika nie zostanie usunięte — tylko powiązanie z firmą.',
function() {
fetch('/firma/' + TEAM_COMPANY_ID + '/zespol/' + userId + '/usun', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': CSRF_TOKEN }
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) { showToast(data.message, 'success'); loadTeamMembers(); }
else showToast(data.error || 'Błąd', 'error');
})
.catch(function() { showToast('Błąd połączenia', 'error'); });
}
);
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
{% endif %}
{% endblock %}