feat(company): Show manager contacts modal for employees instead of edit form
Some checks failed
NordaBiz Tests / Unit & Integration Tests (push) Has been cancelled
NordaBiz Tests / E2E Tests (Playwright) (push) Has been cancelled
NordaBiz Tests / Smoke Tests (Production) (push) Has been cancelled
NordaBiz Tests / Send Failure Notification (push) Has been cancelled

Employees clicking "Edytuj profil" now see a modal with their company's
management team contacts instead of being sent to an edit form they can't use.
Managers and admins continue to access the edit form directly. Direct URL
access to /firma/edytuj is also guarded with redirect + flash message.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-09 20:24:53 +01:00
parent 3fb1a23546
commit 193ad2b3aa
4 changed files with 160 additions and 7 deletions

View File

@ -235,8 +235,26 @@ def company_detail(company_id):
# Check if current user can enrich company data (user with company edit rights)
can_enrich = False
can_edit_profile = False
company_managers = []
if current_user.is_authenticated:
can_enrich = current_user.can_edit_company(company.id)
can_edit_profile = current_user.can_manage_company(company.id)
# If user is a member but not manager, load managers for contact modal
if not can_edit_profile:
is_company_member = any(
assoc.company_id == company.id
for assoc in (current_user.company_associations or [])
) or current_user.company_id == company.id
if is_company_member:
company_managers = db.query(User).join(UserCompany).filter(
UserCompany.company_id == company.id,
UserCompany.role == 'MANAGER'
).all()
for m in company_managers:
_ = m.name, m.email # force-load before session close
return render_template('company_detail.html',
company=company,
@ -255,6 +273,8 @@ def company_detail(company_id):
it_audit=it_audit,
pkd_codes=pkd_codes,
can_enrich=can_enrich,
can_edit_profile=can_edit_profile,
company_managers=company_managers,
is_admin=current_user.is_authenticated and current_user.is_admin
)
finally:
@ -618,6 +638,17 @@ def dashboard():
for uc in user_companies:
_ = uc.company.name if uc.company else None
# Managers map for companies where user is EMPLOYEE (for edit permission modal)
company_managers_map = {}
employee_company_ids = [uc.company_id for uc in user_companies if uc.role == 'EMPLOYEE']
if employee_company_ids:
managers = db.query(User, UserCompany.company_id).join(UserCompany).filter(
UserCompany.company_id.in_(employee_company_ids),
UserCompany.role == 'MANAGER'
).all()
for mgr, cid in managers:
company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''})
# Widget 1: Upcoming events (3 nearest future events)
upcoming_events = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today()
@ -674,7 +705,8 @@ def dashboard():
recent_announcements=recent_announcements,
recent_forum_topics=recent_forum_topics,
recent_classifieds=recent_classifieds,
new_companies=new_companies
new_companies=new_companies,
company_managers_map=company_managers_map
)
finally:
db.close()

View File

@ -28,8 +28,9 @@ def company_edit(company_id=None):
"""Display the company profile edit form."""
target_company_id = company_id or current_user.company_id
if not target_company_id or not current_user.can_edit_company(target_company_id):
flash('Nie masz uprawnień do edycji profilu firmy.', 'error')
if not target_company_id or not current_user.can_manage_company(target_company_id):
flash('Edycja profilu firmy jest dostępna tylko dla kadry zarządzającej. '
'Skontaktuj się z osobą zarządzającą Twoją firmą.', 'warning')
return redirect(url_for('public.dashboard'))
db = SessionLocal()
@ -81,8 +82,9 @@ def company_edit_save(company_id=None):
"""Save company profile edits."""
target_company_id = company_id or current_user.company_id
if not target_company_id or not current_user.can_edit_company(target_company_id):
flash('Nie masz uprawnień do edycji profilu firmy.', 'error')
if not target_company_id or not current_user.can_manage_company(target_company_id):
flash('Edycja profilu firmy jest dostępna tylko dla kadry zarządzającej. '
'Skontaktuj się z osobą zarządzającą Twoją firmą.', 'warning')
return redirect(url_for('public.dashboard'))
db = SessionLocal()

View File

@ -624,7 +624,7 @@
<!-- AI Enrichment Button + Edit Profile -->
<div style="margin: var(--spacing-md) 0; display: flex; gap: var(--spacing-sm); flex-wrap: wrap; align-items: center;">
{% if can_enrich %}
{% if can_edit_profile %}
<a href="{{ url_for('company_edit', company_id=company.id) }}" class="ai-enrich-btn" style="text-decoration: none;">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
@ -632,6 +632,14 @@
</svg>
Edytuj profil
</a>
{% elif company_managers %}
<button type="button" class="ai-enrich-btn" onclick="document.getElementById('editPermissionModal').classList.add('active')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
</svg>
Edytuj profil
</button>
{% endif %}
<button
id="aiEnrichBtn"
@ -3595,6 +3603,44 @@
{% endif %}
#}
<!-- Edit Permission Modal (for employees) -->
{% if company_managers is defined and company_managers %}
<div class="modal-overlay" id="editPermissionModal">
<div class="modal" style="max-width: 480px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
<div style="text-align: center; margin-bottom: var(--spacing-lg);">
<div style="font-size: 3em; margin-bottom: var(--spacing-md);">🔒</div>
<h3 style="margin-bottom: var(--spacing-sm);">Edycja wymaga uprawnień</h3>
<p style="color: var(--text-secondary);">
Edycja profilu firmy jest dostępna dla kadry zarządzającej.
Skontaktuj się z odpowiednią osobą:
</p>
</div>
<div style="margin-bottom: var(--spacing-lg);">
{% for mgr in company_managers %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: var(--background); border-radius: var(--radius); margin-bottom: var(--spacing-xs);">
<svg width="20" height="20" fill="none" stroke="var(--primary)" stroke-width="2" viewBox="0 0 24 24">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>
<div>
<div style="font-weight: 600;">{{ mgr.name or 'Brak imienia' }}</div>
{% if mgr.email %}
<a href="mailto:{{ mgr.email }}" style="font-size: 0.85rem; color: var(--primary);">{{ mgr.email }}</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
<div style="text-align: center;">
<button type="button" class="btn btn-primary"
onclick="document.getElementById('editPermissionModal').classList.remove('active')">
Rozumiem
</button>
</div>
</div>
</div>
{% endif %}
<!-- Universal Confirm Modal -->
<div class="modal-overlay" id="confirmModal">
<div class="modal" style="max-width: 420px; background: var(--surface); border-radius: var(--radius-lg); padding: var(--spacing-xl);">
@ -3646,6 +3692,8 @@
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
<style>
.modal-overlay#editPermissionModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#editPermissionModal.active { display: flex; }
.modal-overlay#confirmModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#confirmModal.active { display: flex; }
.toast { padding: 12px 20px; border-radius: var(--radius); background: var(--surface); border-left: 4px solid var(--primary); box-shadow: 0 4px 12px rgba(0,0,0,0.15); display: flex; align-items: center; gap: 10px; animation: toastIn 0.3s ease; }
@ -3679,6 +3727,12 @@ document.getElementById('confirmModalOk').addEventListener('click', () => closeC
document.getElementById('confirmModalCancel').addEventListener('click', () => closeConfirm(false));
document.getElementById('confirmModal').addEventListener('click', e => { if (e.target.id === 'confirmModal') closeConfirm(false); });
// Edit Permission Modal - close on backdrop click
(function() {
var epm = document.getElementById('editPermissionModal');
if (epm) epm.addEventListener('click', function(e) { if (e.target === epm) epm.classList.remove('active'); });
})();
function showToast(message, type = 'info', duration = 4000) {
const container = document.getElementById('toastContainer');
const icons = { success: '✓', error: '✕', warning: '⚠', info: '' };

View File

@ -601,11 +601,17 @@
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: -2px; margin-right: 4px;"><path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/><path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/></svg>
Zobacz profil
</a>
{% if current_user.can_edit_company(uc.company_id) %}
{% if uc.role == 'MANAGER' or current_user.is_admin %}
<a href="{{ url_for('public.company_edit', company_id=uc.company_id) }}" class="btn btn-sm btn-primary" style="padding: 6px 14px;">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: -2px; margin-right: 4px;"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Edytuj profil
</a>
{% elif uc.role == 'EMPLOYEE' %}
<button type="button" class="btn btn-sm btn-primary" style="padding: 6px 14px;"
onclick="showEditPermissionModal({{ uc.company_id }})">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="vertical-align: -2px; margin-right: 4px;"><path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
Edytuj profil
</button>
{% endif %}
</div>
</div>
@ -1014,4 +1020,63 @@
</div>
</div>
</div>
<!-- Edit Permission Modal (for employees) -->
<div class="modal-overlay" id="editPermissionModal">
<div class="modal" style="max-width: 480px; background: var(--surface, white); border-radius: var(--radius-lg, 12px); padding: var(--spacing-xl, 24px);">
<div style="text-align: center; margin-bottom: var(--spacing-lg, 20px);">
<div style="font-size: 3em; margin-bottom: var(--spacing-md, 16px);">🔒</div>
<h3 style="margin-bottom: var(--spacing-sm, 8px);">Edycja wymaga uprawnień</h3>
<p style="color: var(--text-secondary, #666);">
Edycja profilu firmy jest dostępna dla kadry zarządzającej.
Skontaktuj się z odpowiednią osobą:
</p>
</div>
<div id="editPermissionManagersList" style="margin-bottom: var(--spacing-lg, 20px);"></div>
<div style="text-align: center;">
<button type="button" class="btn btn-primary"
onclick="document.getElementById('editPermissionModal').classList.remove('active')">
Rozumiem
</button>
</div>
</div>
</div>
<style>
.modal-overlay#editPermissionModal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); z-index: 1050; align-items: center; justify-content: center; }
.modal-overlay#editPermissionModal.active { display: flex; }
</style>
<script>
const companyManagersMap = {{ company_managers_map | tojson }};
function showEditPermissionModal(companyId) {
const managers = companyManagersMap[companyId] || [];
const list = document.getElementById('editPermissionManagersList');
if (managers.length === 0) {
list.innerHTML = '<div style="text-align: center; padding: 12px; color: var(--text-secondary, #666);">' +
'Twoja firma nie ma przypisanej kadry zarządzającej.<br>' +
'Skontaktuj się z biurem izby: <a href="mailto:biuro@nordabiznes.pl" style="color: var(--primary, #0066cc);">biuro@nordabiznes.pl</a>' +
'</div>';
} else {
list.innerHTML = managers.map(function(m) {
return '<div style="display: flex; align-items: center; gap: 8px; padding: 8px 12px; background: var(--background, #f9fafb); border-radius: 8px; margin-bottom: 4px;">' +
'<svg width="20" height="20" fill="none" stroke="var(--primary, #0066cc)" stroke-width="2" viewBox="0 0 24 24">' +
'<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>' +
'</svg>' +
'<div>' +
'<div style="font-weight: 600;">' + (m.name || 'Brak imienia') + '</div>' +
(m.email ? '<a href="mailto:' + m.email + '" style="font-size: 0.85rem; color: var(--primary, #0066cc);">' + m.email + '</a>' : '') +
'</div></div>';
}).join('');
}
document.getElementById('editPermissionModal').classList.add('active');
}
// Close on backdrop click
document.getElementById('editPermissionModal').addEventListener('click', function(e) {
if (e.target === this) this.classList.remove('active');
});
</script>
{% endblock %}