feat(calendar): add guest form and updated attendee list in event template
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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-31 14:03:24 +02:00
parent 2a9f6fbf1f
commit 18de6aa42d

View File

@ -548,7 +548,7 @@
{% if event.is_external %}
<div>
<strong>Interesuje Cię to wydarzenie?</strong>
<p class="text-muted" style="margin: 0;">{{ event.attendee_count }} osób zainteresowanych z Izby</p>
<p class="text-muted" style="margin: 0;">{{ event.total_attendee_count }} osób zainteresowanych z Izby</p>
</div>
<button id="rsvp-btn" class="btn {% if user_attending %}btn-secondary attending{% else %}btn-primary{% endif %}" onclick="toggleRSVP()">
{% if user_attending %}Nie interesuje mnie{% else %}Zainteresowany{% endif %}
@ -556,13 +556,63 @@
{% else %}
<div>
<strong>Chcesz wziąć udział?</strong>
<p class="text-muted" style="margin: 0;">{{ event.attendee_count }} osób już się zapisało{% if event.max_attendees %} (limit: {{ event.max_attendees }}){% endif %}</p>
<p class="text-muted" style="margin: 0;">{{ event.total_attendee_count }} osób już się zapisało{% if event.max_attendees %} (limit: {{ event.max_attendees }}){% endif %}</p>
</div>
<button id="rsvp-btn" class="btn {% if user_attending %}btn-secondary attending{% else %}btn-primary{% endif %}" onclick="toggleRSVP()">
{% if user_attending %}Wypisz się{% else %}Wezmę udział{% endif %}
</button>
{% endif %}
</div>
{# --- Guest management section --- #}
{% if not event.is_external %}
<div class="guest-section" style="margin-top: 16px;">
<div id="user-guests-list">
{% if user_guests %}
<p style="margin: 0 0 8px; font-weight: 600; font-size: 0.9em; color: var(--text-secondary);">
Twoi goście ({{ user_guests|length }}/5):
</p>
{% for guest in user_guests %}
<div class="guest-item" data-guest-id="{{ guest.id }}" style="display: flex; align-items: center; gap: 8px; padding: 6px 10px; background: var(--surface); border-radius: var(--radius); margin-bottom: 4px; font-size: 0.9em;">
<span class="guest-display" style="flex: 1;">
{{ guest.display_name }}{% if guest.organization %} <span style="color: var(--text-secondary);">({{ guest.organization }})</span>{% endif %}
</span>
<button onclick="editGuest({{ guest.id }}, '{{ guest.first_name|default('', true)|e }}', '{{ guest.last_name|default('', true)|e }}', '{{ guest.organization|default('', true)|e }}')" style="background: none; border: none; cursor: pointer; color: var(--primary); font-size: 0.85em; padding: 2px 6px;">edytuj</button>
<button onclick="deleteGuest({{ guest.id }})" style="background: none; border: none; cursor: pointer; color: var(--error); font-size: 1.1em; padding: 2px 6px;" title="Usuń gościa">&times;</button>
</div>
{% endfor %}
{% endif %}
</div>
{% if user_guests|length < 5 %}
<button id="add-guest-btn" onclick="toggleGuestForm()" class="btn btn-outline" style="margin-top: 8px; font-size: 0.9em;">
+ Dodaj osobę towarzyszącą
</button>
{% endif %}
<div id="guest-form" style="display: none; margin-top: 12px; padding: 16px; background: var(--surface); border-radius: var(--radius); border: 1px solid var(--border);">
<input type="hidden" id="guest-edit-id" value="">
<div style="display: flex; flex-direction: column; gap: 10px;">
<div>
<label for="guest-first-name" style="font-size: 0.85em; color: var(--text-secondary);">Imię</label>
<input type="text" id="guest-first-name" maxlength="100" class="form-control" style="margin-top: 2px;">
</div>
<div>
<label for="guest-last-name" style="font-size: 0.85em; color: var(--text-secondary);">Nazwisko</label>
<input type="text" id="guest-last-name" maxlength="100" class="form-control" style="margin-top: 2px;">
</div>
<div>
<label for="guest-org" style="font-size: 0.85em; color: var(--text-secondary);">Firma / organizacja</label>
<input type="text" id="guest-org" maxlength="255" class="form-control" style="margin-top: 2px;">
</div>
<div style="display: flex; gap: 8px;">
<button id="guest-submit-btn" onclick="submitGuest()" class="btn btn-primary" style="font-size: 0.9em;">Dodaj</button>
<button onclick="cancelGuestForm()" class="btn btn-outline" style="font-size: 0.9em;">Anuluj</button>
</div>
<p id="guest-form-error" style="display: none; color: var(--error); font-size: 0.85em; margin: 0;"></p>
</div>
</div>
</div>
{% endif %}
{% elif event.access_level == 'rada_only' %}
<div class="rsvp-section" style="background: #fef3c7; border: 1px solid #fde68a;">
<svg width="24" height="24" fill="none" stroke="#92400e" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink: 0;">
@ -581,13 +631,14 @@
{% endif %}
</div>
{% if event.attendees and event.can_user_see_attendees(current_user) %}
{% if (event.attendees or event.guests) and event.can_user_see_attendees(current_user) %}
<div class="attendees-section">
<h2>{{ 'Zainteresowani' if event.is_external else 'Uczestnicy' }} ({{ event.attendee_count }})</h2>
<h2>{{ 'Zainteresowani' if event.is_external else 'Uczestnicy' }} ({{ event.total_attendee_count }})</h2>
<div class="attendees-list">
{# --- Regular attendees with their guests --- #}
{% set ns = namespace(shown_hosts=[]) %}
{% for attendee in event.attendees|sort(attribute='user.name') %}
<div class="attendee-badge">
{# Person badge - always clickable: person profile if person_id, user profile otherwise #}
{% if attendee.user.person_id %}
<a href="{{ url_for('public.person_detail', person_id=attendee.user.person_id) }}" class="attendee-name verified">
{% else %}
@ -600,7 +651,6 @@
{{ attendee.user.name or attendee.user.email.split('@')[0] }}
</a>
{# Company badge - always link to company profile #}
{% if attendee.user.company %}
<a href="{{ url_for('public.company_detail_by_slug', slug=attendee.user.company.slug) }}" class="attendee-company">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
@ -610,6 +660,46 @@
</a>
{% endif %}
</div>
{# Guests of this attendee #}
{% for guest in event.guests if guest.host_user_id == attendee.user.id %}
<div class="attendee-badge" style="margin-left: 28px; font-size: 0.9em;">
<span class="attendee-name" style="color: var(--text-secondary);">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="width: 14px; height: 14px;">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
gość: {{ guest.display_name }}{% if guest.organization %} ({{ guest.organization }}){% endif %}
</span>
</div>
{% endfor %}
{% if ns.shown_hosts.append(attendee.user.id) %}{% endif %}
{% endfor %}
{# --- Hosts who are NOT attending but have guests --- #}
{% for guest in event.guests %}
{% if guest.host_user_id not in ns.shown_hosts %}
{% if ns.shown_hosts.append(guest.host_user_id) %}{% endif %}
<div class="attendee-badge" style="opacity: 0.7;">
<span class="attendee-name" style="color: var(--text-secondary);">
<svg fill="none" stroke="currentColor" 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"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
{{ guest.host.name or 'Użytkownik' }} <em style="font-size: 0.85em;">(nie uczestniczy)</em>
</span>
</div>
{% for g in event.guests if g.host_user_id == guest.host_user_id %}
<div class="attendee-badge" style="margin-left: 28px; font-size: 0.9em;">
<span class="attendee-name" style="color: var(--text-secondary);">
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="width: 14px; height: 14px;">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
</svg>
gość: {{ g.display_name }}{% if g.organization %} ({{ g.organization }}){% endif %}
</span>
</div>
{% endfor %}
{% endif %}
{% endfor %}
</div>
</div>
@ -678,6 +768,112 @@ async function toggleRSVP() {
btn.disabled = false;
}
/* --- Guest management --- */
function toggleGuestForm() {
const form = document.getElementById('guest-form');
const btn = document.getElementById('add-guest-btn');
if (form.style.display === 'none') {
document.getElementById('guest-edit-id').value = '';
document.getElementById('guest-first-name').value = '';
document.getElementById('guest-last-name').value = '';
document.getElementById('guest-org').value = '';
document.getElementById('guest-submit-btn').textContent = 'Dodaj';
document.getElementById('guest-form-error').style.display = 'none';
form.style.display = 'block';
btn.style.display = 'none';
document.getElementById('guest-first-name').focus();
} else {
cancelGuestForm();
}
}
function cancelGuestForm() {
document.getElementById('guest-form').style.display = 'none';
const btn = document.getElementById('add-guest-btn');
if (btn) btn.style.display = '';
}
function editGuest(guestId, firstName, lastName, org) {
document.getElementById('guest-edit-id').value = guestId;
document.getElementById('guest-first-name').value = firstName;
document.getElementById('guest-last-name').value = lastName;
document.getElementById('guest-org').value = org;
document.getElementById('guest-submit-btn').textContent = 'Zapisz';
document.getElementById('guest-form-error').style.display = 'none';
document.getElementById('guest-form').style.display = 'block';
const btn = document.getElementById('add-guest-btn');
if (btn) btn.style.display = 'none';
document.getElementById('guest-first-name').focus();
}
async function submitGuest() {
const editId = document.getElementById('guest-edit-id').value;
const firstName = document.getElementById('guest-first-name').value.trim();
const lastName = document.getElementById('guest-last-name').value.trim();
const org = document.getElementById('guest-org').value.trim();
const errEl = document.getElementById('guest-form-error');
if (!firstName && !lastName && !org) {
errEl.textContent = 'Podaj przynajmniej imię, nazwisko lub firmę';
errEl.style.display = 'block';
return;
}
errEl.style.display = 'none';
const eventId = {{ event.id }};
const url = editId
? `/kalendarz/${eventId}/guests/${editId}`
: `/kalendarz/${eventId}/guests`;
const method = editId ? 'PATCH' : 'POST';
try {
const resp = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ first_name: firstName, last_name: lastName, organization: org })
});
const data = await resp.json();
if (data.success) {
showToast(editId ? 'Dane gościa zaktualizowane' : 'Dodano osobę towarzyszącą', 'success');
setTimeout(() => location.reload(), 800);
} else {
errEl.textContent = data.error || 'Wystąpił błąd';
errEl.style.display = 'block';
}
} catch (e) {
errEl.textContent = 'Błąd połączenia';
errEl.style.display = 'block';
}
}
async function deleteGuest(guestId) {
if (typeof nordaConfirm === 'function') {
nordaConfirm('Czy na pewno chcesz usunąć tę osobę towarzyszącą?', async () => {
await doDeleteGuest(guestId);
});
} else {
await doDeleteGuest(guestId);
}
}
async function doDeleteGuest(guestId) {
try {
const resp = await fetch(`/kalendarz/{{ event.id }}/guests/${guestId}`, {
method: 'DELETE',
headers: { 'X-CSRFToken': csrfToken }
});
const data = await resp.json();
if (data.success) {
showToast('Usunięto osobę towarzyszącą', 'info');
setTimeout(() => location.reload(), 800);
} else {
showToast(data.error || 'Wystąpił błąd', 'error');
}
} catch (e) {
showToast('Błąd połączenia', 'error');
}
}
/* --- Add to Calendar functions --- */
function stripHtml(html) {
const tmp = document.createElement('div');