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
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:
parent
2a9f6fbf1f
commit
18de6aa42d
@ -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">×</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');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user