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 profile links to usernames and avatars across forum, classifieds, announcements, company recommendations, board members, and group messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
578 lines
21 KiB
HTML
578 lines
21 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Zarzadzanie grupa - {{ group.display_name }} - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
.manage-container {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
margin-bottom: var(--spacing-lg);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.manage-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.manage-header h1 {
|
|
font-size: var(--font-size-2xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.manage-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
border: 1px solid var(--border-color, #e5e7eb);
|
|
padding: var(--spacing-xl);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.manage-card h2 {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-xs);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
transition: var(--transition);
|
|
font-family: inherit;
|
|
}
|
|
|
|
.form-group textarea {
|
|
resize: vertical;
|
|
min-height: 80px;
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
/* Members table */
|
|
.members-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.members-table th {
|
|
text-align: left;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
border-bottom: 2px solid var(--border-color, #e5e7eb);
|
|
}
|
|
|
|
.members-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border-bottom: 1px solid var(--border-color, #f3f4f6);
|
|
vertical-align: middle;
|
|
}
|
|
|
|
.members-table tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.member-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.member-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-initial {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-info-name {
|
|
font-weight: 500;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.member-info-email {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.role-badge {
|
|
display: inline-block;
|
|
padding: 1px 8px;
|
|
border-radius: 10px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.role-badge.owner {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.role-badge.moderator {
|
|
background: #dbeafe;
|
|
color: #1d4ed8;
|
|
}
|
|
|
|
.role-badge.member {
|
|
background: #f3f4f6;
|
|
color: #6b7280;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 4px 12px;
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
border: 1px solid var(--border);
|
|
background: var(--surface);
|
|
color: var(--text-secondary);
|
|
cursor: pointer;
|
|
transition: var(--transition);
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.btn-action:hover {
|
|
background: var(--background);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-action.danger {
|
|
color: #dc2626;
|
|
border-color: #fecaca;
|
|
}
|
|
|
|
.btn-action.danger:hover {
|
|
background: #fef2f2;
|
|
border-color: #dc2626;
|
|
}
|
|
|
|
.btn-action.moderator-toggle {
|
|
color: var(--primary);
|
|
border-color: #bfdbfe;
|
|
}
|
|
|
|
.btn-action.moderator-toggle:hover {
|
|
background: #eff6ff;
|
|
}
|
|
|
|
/* Add member section */
|
|
.add-member-section {
|
|
margin-top: var(--spacing-lg);
|
|
padding-top: var(--spacing-lg);
|
|
border-top: 1px solid var(--border-color, #e5e7eb);
|
|
}
|
|
|
|
.add-member-section h3 {
|
|
font-size: var(--font-size-base);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.add-member-autocomplete {
|
|
position: relative;
|
|
}
|
|
|
|
.add-member-autocomplete input[type="text"] {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.add-member-autocomplete input[type="text"]:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.add-autocomplete-results {
|
|
position: absolute;
|
|
top: 100%;
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
border-radius: 0 0 var(--radius) var(--radius);
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
z-index: 100;
|
|
box-shadow: var(--shadow-lg);
|
|
display: none;
|
|
}
|
|
|
|
.add-autocomplete-results.visible {
|
|
display: block;
|
|
}
|
|
|
|
.add-autocomplete-item {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
cursor: pointer;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
transition: background 0.15s;
|
|
}
|
|
|
|
.add-autocomplete-item:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.add-autocomplete-item-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
object-fit: cover;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.add-autocomplete-item-initial {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.add-autocomplete-item-name {
|
|
font-weight: 500;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.add-autocomplete-item-company {
|
|
font-size: var(--font-size-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.add-autocomplete-no-results {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
.manage-card {
|
|
padding: var(--spacing-md);
|
|
}
|
|
.members-table th:last-child,
|
|
.members-table td:last-child {
|
|
text-align: right;
|
|
}
|
|
.action-buttons {
|
|
justify-content: flex-end;
|
|
}
|
|
}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="manage-container">
|
|
<a href="{{ url_for('messages.group_view', group_id=group.id) }}" class="back-link">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
|
<path d="M19 12H5M12 19l-7-7 7-7"/>
|
|
</svg>
|
|
Powrot do grupy
|
|
</a>
|
|
|
|
<div class="manage-header">
|
|
<h1>Zarzadzanie grupa</h1>
|
|
</div>
|
|
|
|
{# ===== GROUP INFO EDIT (owner only) ===== #}
|
|
{% if membership.is_owner %}
|
|
<div class="manage-card">
|
|
<h2>Informacje o grupie</h2>
|
|
<form method="POST" action="{{ url_for('messages.group_edit', group_id=group.id) }}">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<div class="form-group">
|
|
<label for="group-name">Nazwa grupy</label>
|
|
<input type="text" id="group-name" name="name" maxlength="100" value="{{ group.name or '' }}" placeholder="np. Komisja ds. PEJ">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="group-description">Opis (opcjonalnie)</label>
|
|
<textarea id="group-description" name="description" maxlength="500" placeholder="Krotki opis grupy...">{{ group.description or '' }}</textarea>
|
|
</div>
|
|
<button type="submit" class="btn btn-primary">Zapisz zmiany</button>
|
|
</form>
|
|
|
|
<div style="margin-top: var(--spacing-xl); padding-top: var(--spacing-lg); border-top: 1px solid var(--border-color, #e5e7eb);">
|
|
<h3 style="color: #dc2626; font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm);">Strefa niebezpieczna</h3>
|
|
<p style="font-size: var(--font-size-xs); color: var(--text-secondary); margin-bottom: var(--spacing-sm);">Usunięcie grupy jest nieodwracalne. Wszystkie wiadomości i załączniki zostaną usunięte.</p>
|
|
<form method="POST" action="{{ url_for('messages.group_delete', group_id=group.id) }}" onsubmit="return nordaConfirm(this, 'Wszystkie wiadomości i załączniki zostaną trwale usunięte.', {title: 'Usunąć grupę?', icon: '🗑️', okText: 'Tak, usuń grupę'});">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<button type="submit" class="btn" style="background: #dc2626; color: white; border: none; font-size: var(--font-size-sm);">Usuń grupę</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# ===== MEMBERS TABLE ===== #}
|
|
<div class="manage-card">
|
|
<h2>Uczestnicy ({{ members|length }})</h2>
|
|
|
|
<table class="members-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Osoba</th>
|
|
<th>Rola</th>
|
|
<th>Akcje</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for m in members %}
|
|
<tr>
|
|
<td>
|
|
<div class="member-cell">
|
|
<a href="{{ url_for('public.user_profile', user_id=m.user_id) }}" style="text-decoration:none;color:inherit;">
|
|
{% if m.user.avatar_path %}
|
|
<img src="{{ url_for('static', filename=m.user.avatar_path) }}" class="member-avatar" alt="">
|
|
{% else %}
|
|
<div class="member-initial">{{ (m.user.name or m.user.email)[0].upper() }}</div>
|
|
{% endif %}
|
|
</a>
|
|
<div>
|
|
<div class="member-info-name"><a href="{{ url_for('public.user_profile', user_id=m.user_id) }}" style="color:inherit;text-decoration:none;" onmouseover="this.style.textDecoration='underline'" onmouseout="this.style.textDecoration='none'">{{ m.user.name or m.user.email.split('@')[0] }}</a></div>
|
|
{% if m.user.privacy_show_email != False %}
|
|
<div class="member-info-email">{{ m.user.email }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if m.is_owner %}
|
|
<span class="role-badge owner">Wlasciciel</span>
|
|
{% elif m.is_moderator %}
|
|
<span class="role-badge moderator">Moderator</span>
|
|
{% else %}
|
|
<span class="role-badge member">Uczestnik</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="action-buttons">
|
|
{% if m.user_id != current_user.id and not m.is_owner %}
|
|
{% if membership.is_owner %}
|
|
{# Owner can toggle moderator and remove #}
|
|
<form method="POST" action="{{ url_for('messages.group_change_role', group_id=group.id) }}" style="display:inline;">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_id" value="{{ m.user_id }}">
|
|
<input type="hidden" name="role" value="{{ 'member' if m.is_moderator else 'moderator' }}">
|
|
<button type="submit" class="btn-action moderator-toggle" title="{% if m.is_moderator %}Odbierz moderatora{% else %}Nadaj moderatora{% endif %}">
|
|
{% if m.is_moderator %}
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 6L6 18M6 6l12 12"/></svg>
|
|
Odbierz mod.
|
|
{% else %}
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg>
|
|
Nadaj mod.
|
|
{% endif %}
|
|
</button>
|
|
</form>
|
|
<form method="POST" action="{{ url_for('messages.group_remove_member', group_id=group.id) }}" style="display:inline;" onsubmit="return nordaConfirm(this, 'Uczestnik zostanie usunięty z grupy.', {title: 'Usunąć uczestnika?', icon: '👤', okText: 'Tak, usuń'});">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_id" value="{{ m.user_id }}">
|
|
<button type="submit" class="btn-action danger">
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
Usun
|
|
</button>
|
|
</form>
|
|
{% elif membership.is_moderator and not m.is_moderator %}
|
|
{# Moderator can remove members only #}
|
|
<form method="POST" action="{{ url_for('messages.group_remove_member', group_id=group.id) }}" style="display:inline;" onsubmit="return nordaConfirm(this, 'Uczestnik zostanie usunięty z grupy.', {title: 'Usunąć uczestnika?', icon: '👤', okText: 'Tak, usuń'});">
|
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
|
<input type="hidden" name="user_id" value="{{ m.user_id }}">
|
|
<button type="submit" class="btn-action danger">
|
|
<svg width="12" height="12" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
|
Usun
|
|
</button>
|
|
</form>
|
|
{% endif %}
|
|
{% elif m.user_id == current_user.id %}
|
|
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);">Ty</span>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
|
|
{# ===== ADD MEMBER ===== #}
|
|
<div class="add-member-section">
|
|
<h3>Dodaj uczestnika</h3>
|
|
<div class="add-member-autocomplete" id="add-member-autocomplete">
|
|
<input type="text" id="add-member-search" placeholder="Wpisz imie, nazwisko lub email..." autocomplete="off">
|
|
<div id="add-autocomplete-results" class="add-autocomplete-results"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
/* Add member autocomplete */
|
|
(function() {
|
|
var availableUsers = [
|
|
{% for user in available_users %}
|
|
{id: {{ user.id }}, name: {{ (user.name or user.email.split('@')[0]) | tojson }}, email: {{ user.email | tojson }}, companyName: {{ (user._company_name or '') | tojson }}, avatarPath: {% if user.avatar_path %}{{ url_for('static', filename=user.avatar_path) | tojson }}{% else %}""{% endif %}}{{ ',' if not loop.last }}
|
|
{% endfor %}
|
|
];
|
|
|
|
var searchInput = document.getElementById('add-member-search');
|
|
var resultsDiv = document.getElementById('add-autocomplete-results');
|
|
var autocompleteDiv = document.getElementById('add-member-autocomplete');
|
|
var groupId = {{ group.id }};
|
|
var csrfToken = '{{ csrf_token() }}';
|
|
|
|
function normalize(str) {
|
|
return str.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
}
|
|
|
|
function filterUsers(query) {
|
|
if (!query) return [];
|
|
var q = normalize(query);
|
|
return availableUsers.filter(function(u) {
|
|
return normalize(u.name).indexOf(q) !== -1 ||
|
|
normalize(u.email).indexOf(q) !== -1 ||
|
|
normalize(u.companyName).indexOf(q) !== -1;
|
|
});
|
|
}
|
|
|
|
function renderResults(matches) {
|
|
if (matches.length === 0) {
|
|
resultsDiv.innerHTML = '<div class="add-autocomplete-no-results">Nie znaleziono</div>';
|
|
resultsDiv.classList.add('visible');
|
|
return;
|
|
}
|
|
resultsDiv.innerHTML = matches.map(function(u) {
|
|
var initial = (u.name || u.email)[0].toUpperCase();
|
|
var avatarHtml = u.avatarPath
|
|
? '<img src="' + u.avatarPath + '" class="add-autocomplete-item-avatar" alt="">'
|
|
: '<div class="add-autocomplete-item-initial">' + initial + '</div>';
|
|
var companyHtml = u.companyName ? '<div class="add-autocomplete-item-company">' + u.companyName + '</div>' : '';
|
|
return '<div class="add-autocomplete-item" data-id="' + u.id + '">' +
|
|
avatarHtml +
|
|
'<div>' +
|
|
'<div class="add-autocomplete-item-name">' + u.name + '</div>' +
|
|
companyHtml +
|
|
'</div>' +
|
|
'</div>';
|
|
}).join('');
|
|
resultsDiv.classList.add('visible');
|
|
}
|
|
|
|
searchInput.addEventListener('input', function() {
|
|
var q = this.value.trim();
|
|
if (q.length === 0) {
|
|
resultsDiv.classList.remove('visible');
|
|
return;
|
|
}
|
|
renderResults(filterUsers(q));
|
|
});
|
|
|
|
searchInput.addEventListener('focus', function() {
|
|
if (this.value.trim().length > 0) {
|
|
renderResults(filterUsers(this.value.trim()));
|
|
}
|
|
});
|
|
|
|
resultsDiv.addEventListener('click', function(e) {
|
|
var item = e.target.closest('.add-autocomplete-item');
|
|
if (item) {
|
|
var userId = item.dataset.id;
|
|
/* Submit add member via hidden form */
|
|
var form = document.createElement('form');
|
|
form.method = 'POST';
|
|
form.action = '{{ url_for("messages.group_add_member", group_id=group.id) }}';
|
|
|
|
var csrfInput = document.createElement('input');
|
|
csrfInput.type = 'hidden';
|
|
csrfInput.name = 'csrf_token';
|
|
csrfInput.value = csrfToken;
|
|
form.appendChild(csrfInput);
|
|
|
|
var userInput = document.createElement('input');
|
|
userInput.type = 'hidden';
|
|
userInput.name = 'user_id';
|
|
userInput.value = userId;
|
|
form.appendChild(userInput);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
}
|
|
});
|
|
|
|
document.addEventListener('click', function(e) {
|
|
if (!autocompleteDiv.contains(e.target)) {
|
|
resultsDiv.classList.remove('visible');
|
|
}
|
|
});
|
|
})();
|
|
{% endblock %}
|