feat(calendar): add external events support for KIG/ARP integration
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
External events from partner organizations (ARP, KIG, etc.) can now be added to the calendar with distinct visual treatment: - Grey badge "ZEWNĘTRZNE" and muted date box in list view - Grey color in grid view with border accent - "Jestem zainteresowany" instead of "Zapisz się" (no commitment) - Prominent "Przejdź do rejestracji" button linking to external organizer - "Zainteresowani" section instead of "Uczestnicy" - Toggle filter "Pokaż zewnętrzne" with localStorage persistence - Admin form checkbox to mark events as external New fields: is_external, external_url, external_source on NordaEvent. Migration: 086_external_events.sql Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a703991e62
commit
7a3955d0fa
@ -943,6 +943,8 @@ def admin_calendar_new():
|
|||||||
if event_type == 'rada' and access_level != 'rada_only':
|
if event_type == 'rada' and access_level != 'rada_only':
|
||||||
access_level = 'rada_only'
|
access_level = 'rada_only'
|
||||||
|
|
||||||
|
is_external = request.form.get('is_external') == 'on'
|
||||||
|
|
||||||
event = NordaEvent(
|
event = NordaEvent(
|
||||||
title=request.form.get('title', '').strip(),
|
title=request.form.get('title', '').strip(),
|
||||||
description=sanitize_html(request.form.get('description', '').strip()),
|
description=sanitize_html(request.form.get('description', '').strip()),
|
||||||
@ -951,10 +953,13 @@ def admin_calendar_new():
|
|||||||
time_end=request.form.get('time_end') or None,
|
time_end=request.form.get('time_end') or None,
|
||||||
location=request.form.get('location', '').strip() or None,
|
location=request.form.get('location', '').strip() or None,
|
||||||
event_type=event_type,
|
event_type=event_type,
|
||||||
max_attendees=request.form.get('max_attendees', type=int) or None,
|
max_attendees=None if is_external else (request.form.get('max_attendees', type=int) or None),
|
||||||
access_level=access_level,
|
access_level=access_level,
|
||||||
created_by=current_user.id,
|
created_by=current_user.id,
|
||||||
source='manual'
|
source='manual',
|
||||||
|
is_external=is_external,
|
||||||
|
external_url=request.form.get('external_url', '').strip() or None if is_external else None,
|
||||||
|
external_source=request.form.get('external_source', '').strip() or None if is_external else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Handle file attachment
|
# Handle file attachment
|
||||||
|
|||||||
@ -328,19 +328,22 @@ def rsvp(event_id):
|
|||||||
EventAttendee.user_id == current_user.id
|
EventAttendee.user_id == current_user.id
|
||||||
).first()
|
).first()
|
||||||
|
|
||||||
|
is_ext = getattr(event, 'is_external', False) or False
|
||||||
|
msg_added = 'Oznaczono jako zainteresowany' if is_ext else 'Zapisano na wydarzenie'
|
||||||
|
msg_removed = 'Usunięto zainteresowanie' if is_ext else 'Wypisano z wydarzenia'
|
||||||
|
|
||||||
if existing:
|
if existing:
|
||||||
# Wypisz
|
|
||||||
db.delete(existing)
|
db.delete(existing)
|
||||||
db.commit()
|
db.commit()
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'action': 'removed',
|
'action': 'removed',
|
||||||
'message': 'Wypisano z wydarzenia',
|
'message': msg_removed,
|
||||||
'attendee_count': event.attendee_count
|
'attendee_count': event.attendee_count
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
# Zapisz
|
# Skip max_attendees check for external events
|
||||||
if event.max_attendees and event.attendee_count >= event.max_attendees:
|
if not is_ext and event.max_attendees and event.attendee_count >= event.max_attendees:
|
||||||
return jsonify({'success': False, 'error': 'Brak wolnych miejsc'}), 400
|
return jsonify({'success': False, 'error': 'Brak wolnych miejsc'}), 400
|
||||||
|
|
||||||
attendee = EventAttendee(
|
attendee = EventAttendee(
|
||||||
@ -353,7 +356,7 @@ def rsvp(event_id):
|
|||||||
return jsonify({
|
return jsonify({
|
||||||
'success': True,
|
'success': True,
|
||||||
'action': 'added',
|
'action': 'added',
|
||||||
'message': 'Zapisano na wydarzenie',
|
'message': msg_added,
|
||||||
'attendee_count': event.attendee_count
|
'attendee_count': event.attendee_count
|
||||||
})
|
})
|
||||||
finally:
|
finally:
|
||||||
|
|||||||
@ -2174,6 +2174,11 @@ class NordaEvent(Base):
|
|||||||
organizer_name = Column(String(255), default='Norda Biznes')
|
organizer_name = Column(String(255), default='Norda Biznes')
|
||||||
organizer_email = Column(String(255), default='biuro@norda-biznes.info')
|
organizer_email = Column(String(255), default='biuro@norda-biznes.info')
|
||||||
|
|
||||||
|
# External event (ARP, KIG, etc.)
|
||||||
|
is_external = Column(Boolean, default=False)
|
||||||
|
external_url = Column(String(1000)) # Registration link at external organizer
|
||||||
|
external_source = Column(String(255)) # Source name (e.g. "Agencja Rozwoju Pomorza")
|
||||||
|
|
||||||
# Attachment
|
# Attachment
|
||||||
attachment_filename = Column(String(255)) # Original filename
|
attachment_filename = Column(String(255)) # Original filename
|
||||||
attachment_path = Column(String(1000)) # Server path (static/uploads/events/...)
|
attachment_path = Column(String(1000)) # Server path (static/uploads/events/...)
|
||||||
|
|||||||
9
database/migrations/086_external_events.sql
Normal file
9
database/migrations/086_external_events.sql
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
-- Migration 086: Add external event fields to norda_events
|
||||||
|
-- For KIG integration: external events from ARP, KIG, partner organizations
|
||||||
|
|
||||||
|
ALTER TABLE norda_events ADD COLUMN IF NOT EXISTS is_external BOOLEAN DEFAULT FALSE;
|
||||||
|
ALTER TABLE norda_events ADD COLUMN IF NOT EXISTS external_url VARCHAR(1000);
|
||||||
|
ALTER TABLE norda_events ADD COLUMN IF NOT EXISTS external_source VARCHAR(255);
|
||||||
|
|
||||||
|
-- Grant permissions
|
||||||
|
GRANT ALL ON TABLE norda_events TO nordabiz_app;
|
||||||
@ -106,6 +106,27 @@
|
|||||||
<form method="POST" action="{{ url_for('admin.admin_calendar_new') }}" enctype="multipart/form-data">
|
<form method="POST" action="{{ url_for('admin.admin_calendar_new') }}" enctype="multipart/form-data">
|
||||||
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
<div class="form-group" style="background: var(--bg-secondary); padding: var(--spacing-md); border-radius: var(--radius); border: 1px solid var(--border);">
|
||||||
|
<label style="display: flex; align-items: center; gap: var(--spacing-sm); cursor: pointer; margin-bottom: 0;">
|
||||||
|
<input type="checkbox" id="is_external" name="is_external" style="width: auto;">
|
||||||
|
<span>Wydarzenie zewnętrzne</span>
|
||||||
|
</label>
|
||||||
|
<div class="form-hint">Zaznacz dla wydarzeń organizowanych przez podmioty zewnętrzne (ARP, KIG, urzędy). Użytkownicy zobaczą przycisk "Jestem zainteresowany" zamiast "Zapisz się".</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="external-fields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="external_source">Organizator / Źródło *</label>
|
||||||
|
<input type="text" id="external_source" name="external_source" maxlength="255" placeholder="np. Agencja Rozwoju Pomorza">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="external_url">Link do rejestracji *</label>
|
||||||
|
<input type="url" id="external_url" name="external_url" placeholder="https://brokereksportowy.pl/...">
|
||||||
|
<div class="form-hint">Link do strony zewnętrznej, gdzie użytkownicy mogą się zarejestrować</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="title">Tytul wydarzenia *</label>
|
<label for="title">Tytul wydarzenia *</label>
|
||||||
<input type="text" id="title" name="title" required maxlength="255" placeholder="np. Spotkanie czlonkow Norda Biznes">
|
<input type="text" id="title" name="title" required maxlength="255" placeholder="np. Spotkanie czlonkow Norda Biznes">
|
||||||
@ -197,3 +218,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_js %}
|
||||||
|
(function() {
|
||||||
|
const cb = document.getElementById('is_external');
|
||||||
|
const extFields = document.getElementById('external-fields');
|
||||||
|
const maxAtt = document.getElementById('max_attendees');
|
||||||
|
const maxAttGroup = maxAtt ? maxAtt.closest('.form-group') : null;
|
||||||
|
|
||||||
|
function toggle() {
|
||||||
|
const isExt = cb.checked;
|
||||||
|
extFields.style.display = isExt ? 'block' : 'none';
|
||||||
|
if (maxAttGroup) maxAttGroup.style.display = isExt ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.addEventListener('change', toggle);
|
||||||
|
toggle();
|
||||||
|
})();
|
||||||
|
{% endblock %}
|
||||||
|
|||||||
@ -383,13 +383,20 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="event-header">
|
<div class="event-header">
|
||||||
{% if event.is_featured %}
|
{% if event.is_external %}
|
||||||
|
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) var(--spacing-md); background: #f1f5f9; border: 1px solid #cbd5e1; border-radius: var(--radius); margin-bottom: var(--spacing-md); font-size: var(--font-size-sm); color: #475569;">
|
||||||
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/></svg>
|
||||||
|
Wydarzenie zewnętrzne{% if event.external_source %} · {{ event.external_source }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% elif event.is_featured %}
|
||||||
<div class="event-logo">
|
<div class="event-logo">
|
||||||
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes">
|
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<h1>{{ event.title }}
|
<h1>{{ event.title }}
|
||||||
{% if event.access_level == 'admin_only' %}
|
{% if event.is_external %}
|
||||||
|
<span style="display:inline-block;background:#94a3b8;color:#fff;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">ZEWNĘTRZNE</span>
|
||||||
|
{% elif event.access_level == 'admin_only' %}
|
||||||
<span style="display:inline-block;background:#ef4444;color:#fff;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">UKRYTE</span>
|
<span style="display:inline-block;background:#ef4444;color:#fff;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">UKRYTE</span>
|
||||||
{% elif event.access_level == 'rada_only' %}
|
{% elif event.access_level == 'rada_only' %}
|
||||||
<span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">IZBA</span>
|
<span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:12px;padding:2px 8px;border-radius:4px;font-weight:600;vertical-align:middle;">IZBA</span>
|
||||||
@ -492,6 +499,18 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if event.is_external and event.external_url %}
|
||||||
|
<div style="padding: var(--spacing-lg); background: linear-gradient(135deg, #eff6ff, #f0f9ff); border: 1px solid #93c5fd; border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl); text-align: center;">
|
||||||
|
<div style="font-weight: 600; color: #1e40af; margin-bottom: var(--spacing-sm);">Rejestracja u organizatora</div>
|
||||||
|
<a href="{{ event.external_url }}" target="_blank" class="btn btn-primary" style="font-size: var(--font-size-lg); padding: var(--spacing-sm) var(--spacing-xl);">
|
||||||
|
Przejdź do rejestracji →
|
||||||
|
</a>
|
||||||
|
{% if event.external_source %}
|
||||||
|
<div style="margin-top: var(--spacing-sm); font-size: var(--font-size-sm); color: var(--text-secondary);">{{ event.external_source }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if event.image_url %}
|
{% if event.image_url %}
|
||||||
<div style="margin-bottom: var(--spacing-xl); border-radius: var(--radius-lg); overflow: hidden;">
|
<div style="margin-bottom: var(--spacing-xl); border-radius: var(--radius-lg); overflow: hidden;">
|
||||||
<img src="{{ event.image_url }}" alt="{{ event.title }}" style="width: 100%; height: auto; display: block;">
|
<img src="{{ event.image_url }}" alt="{{ event.title }}" style="width: 100%; height: auto; display: block;">
|
||||||
@ -526,6 +545,15 @@
|
|||||||
{% if not event.is_past %}
|
{% if not event.is_past %}
|
||||||
{% if event.can_user_attend(current_user) %}
|
{% if event.can_user_attend(current_user) %}
|
||||||
<div class="rsvp-section">
|
<div class="rsvp-section">
|
||||||
|
{% 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>
|
||||||
|
</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 %}Jestem zainteresowany{% endif %}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
<div>
|
<div>
|
||||||
<strong>Chcesz wziąć udział?</strong>
|
<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.attendee_count }} osób już się zapisało{% if event.max_attendees %} (limit: {{ event.max_attendees }}){% endif %}</p>
|
||||||
@ -533,6 +561,7 @@
|
|||||||
<button id="rsvp-btn" class="btn {% if user_attending %}btn-secondary attending{% else %}btn-primary{% endif %}" onclick="toggleRSVP()">
|
<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 %}
|
{% if user_attending %}Wypisz się{% else %}Wezmę udział{% endif %}
|
||||||
</button>
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% elif event.access_level == 'rada_only' %}
|
{% elif event.access_level == 'rada_only' %}
|
||||||
<div class="rsvp-section" style="background: #fef3c7; border: 1px solid #fde68a;">
|
<div class="rsvp-section" style="background: #fef3c7; border: 1px solid #fde68a;">
|
||||||
@ -554,7 +583,7 @@
|
|||||||
|
|
||||||
{% if event.attendees and event.can_user_see_attendees(current_user) %}
|
{% if event.attendees and event.can_user_see_attendees(current_user) %}
|
||||||
<div class="attendees-section">
|
<div class="attendees-section">
|
||||||
<h2>Uczestnicy ({{ event.attendee_count }})</h2>
|
<h2>{{ 'Zainteresowani' if event.is_external else 'Uczestnicy' }} ({{ event.attendee_count }})</h2>
|
||||||
<div class="attendees-list">
|
<div class="attendees-list">
|
||||||
{% for attendee in event.attendees|sort(attribute='user.name') %}
|
{% for attendee in event.attendees|sort(attribute='user.name') %}
|
||||||
<div class="attendee-badge">
|
<div class="attendee-badge">
|
||||||
@ -624,17 +653,18 @@ async function toggleRSVP() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
const isExt = {{ 'true' if event.is_external else 'false' }};
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
if (data.action === 'added') {
|
if (data.action === 'added') {
|
||||||
btn.textContent = 'Wypisz się';
|
btn.textContent = isExt ? 'Nie interesuje mnie' : 'Wypisz się';
|
||||||
btn.classList.remove('btn-primary');
|
btn.classList.remove('btn-primary');
|
||||||
btn.classList.add('btn-secondary', 'attending');
|
btn.classList.add('btn-secondary', 'attending');
|
||||||
showToast('Zapisano na wydarzenie!', 'success');
|
showToast(isExt ? 'Oznaczono jako zainteresowany!' : 'Zapisano na wydarzenie!', 'success');
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = 'Wezmę udział';
|
btn.textContent = isExt ? 'Jestem zainteresowany' : 'Wezmę udział';
|
||||||
btn.classList.remove('btn-secondary', 'attending');
|
btn.classList.remove('btn-secondary', 'attending');
|
||||||
btn.classList.add('btn-primary');
|
btn.classList.add('btn-primary');
|
||||||
showToast('Wypisano z wydarzenia', 'info');
|
showToast(isExt ? 'Usunięto zainteresowanie' : 'Wypisano z wydarzenia', 'info');
|
||||||
}
|
}
|
||||||
// Refresh page to update attendees list
|
// Refresh page to update attendees list
|
||||||
setTimeout(() => location.reload(), 1000);
|
setTimeout(() => location.reload(), 1000);
|
||||||
|
|||||||
@ -191,6 +191,12 @@
|
|||||||
color: #7c3aed;
|
color: #7c3aed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-event.external {
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border-left: 2px solid #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
/* Widok listy - istniejące style */
|
/* Widok listy - istniejące style */
|
||||||
.events-section {
|
.events-section {
|
||||||
margin-bottom: var(--spacing-2xl);
|
margin-bottom: var(--spacing-2xl);
|
||||||
@ -320,6 +326,34 @@
|
|||||||
.badge-type.webinar { background: #dcfce7; color: #166534; }
|
.badge-type.webinar { background: #dcfce7; color: #166534; }
|
||||||
.badge-type.networking { background: #fef3c7; color: #92400e; }
|
.badge-type.networking { background: #fef3c7; color: #92400e; }
|
||||||
.badge-type.other { background: #f3e8ff; color: #7c3aed; }
|
.badge-type.other { background: #f3e8ff; color: #7c3aed; }
|
||||||
|
.badge-type.external { background: #f1f5f9; color: #475569; }
|
||||||
|
|
||||||
|
.event-card.external-event {
|
||||||
|
border-left: 3px solid #94a3b8;
|
||||||
|
opacity: 0.92;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-card.external-event .event-date-box {
|
||||||
|
background: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.external-source {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-size: var(--font-size-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-toggle input { width: auto; }
|
||||||
|
|
||||||
.rsvp-list-btn { min-width: 110px; transition: var(--transition); }
|
.rsvp-list-btn { min-width: 110px; transition: var(--transition); }
|
||||||
.rsvp-list-btn.rsvp-attending {
|
.rsvp-list-btn.rsvp-attending {
|
||||||
@ -398,6 +432,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<label class="filter-toggle">
|
||||||
|
<input type="checkbox" id="show-external" checked>
|
||||||
|
Pokaż zewnętrzne
|
||||||
|
</label>
|
||||||
|
|
||||||
{% if current_user.can_access_admin_panel() %}
|
{% if current_user.can_access_admin_panel() %}
|
||||||
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
|
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -428,8 +467,9 @@
|
|||||||
{% if day in events_by_day %}
|
{% if day in events_by_day %}
|
||||||
{% for event in events_by_day[day] %}
|
{% for event in events_by_day[day] %}
|
||||||
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}"
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}"
|
||||||
class="calendar-event {{ event.event_type }}"
|
class="calendar-event {{ 'external' if event.is_external else event.event_type }}"
|
||||||
title="{{ event.title }}{% if event.time_start %} - {{ event.time_start.strftime('%H:%M') }}{% endif %}">
|
data-external="{{ 'true' if event.is_external else 'false' }}"
|
||||||
|
title="{{ event.title }}{% if event.time_start %} - {{ event.time_start.strftime('%H:%M') }}{% endif %}{% if event.is_external %} ({{ event.external_source }}){% endif %}">
|
||||||
{% if event.time_start %}{{ event.time_start.strftime('%H:%M') }} {% endif %}{{ event.title[:18] }}{% if event.title|length > 18 %}...{% endif %}
|
{% if event.time_start %}{{ event.time_start.strftime('%H:%M') }} {% endif %}{{ event.title[:18] }}{% if event.title|length > 18 %}...{% endif %}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -447,6 +487,7 @@
|
|||||||
<span><span class="badge-type networking">Networking</span></span>
|
<span><span class="badge-type networking">Networking</span></span>
|
||||||
<span><span class="badge-type webinar">Webinar</span></span>
|
<span><span class="badge-type webinar">Webinar</span></span>
|
||||||
<span><span class="badge-type other">Inne</span></span>
|
<span><span class="badge-type other">Inne</span></span>
|
||||||
|
<span><span class="badge-type external">Zewnętrzne</span></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -458,17 +499,20 @@
|
|||||||
|
|
||||||
{% if upcoming_events %}
|
{% if upcoming_events %}
|
||||||
{% for event in upcoming_events %}
|
{% for event in upcoming_events %}
|
||||||
<div class="event-card {% if event.is_featured %}featured{% endif %}">
|
<div class="event-card {% if event.is_featured %}featured{% endif %}{% if event.is_external %} external-event{% endif %}" data-external="{{ 'true' if event.is_external else 'false' }}">
|
||||||
<div class="event-date-box">
|
<div class="event-date-box">
|
||||||
<div class="day">{{ event.event_date.day }}</div>
|
<div class="day">{{ event.event_date.day }}</div>
|
||||||
<div class="month">{{ pl_months.get(event.event_date.strftime('%b'), event.event_date.strftime('%b')) }}</div>
|
<div class="month">{{ pl_months.get(event.event_date.strftime('%b'), event.event_date.strftime('%b')) }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="event-info">
|
<div class="event-info">
|
||||||
<div class="event-title">
|
<div class="event-title">
|
||||||
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>{% if event.access_level == 'admin_only' %} <span style="display:inline-block;background:#ef4444;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">UKRYTE</span>{% elif event.access_level == 'rada_only' %} <span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">IZBA</span>{% endif %}
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>{% if event.is_external %} <span style="display:inline-block;background:#94a3b8;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">ZEWNĘTRZNE</span>{% elif event.access_level == 'admin_only' %} <span style="display:inline-block;background:#ef4444;color:#fff;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">UKRYTE</span>{% elif event.access_level == 'rada_only' %} <span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:10px;padding:1px 5px;border-radius:3px;font-weight:600;vertical-align:middle;">IZBA</span>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
{% if event.is_external and event.external_source %}
|
||||||
|
<div class="external-source">Źródło: {{ event.external_source }}</div>
|
||||||
|
{% endif %}
|
||||||
<div class="event-meta">
|
<div class="event-meta">
|
||||||
<span class="badge-type {{ event.event_type }}">{{ pl_types.get(event.event_type, event.event_type) }}</span>
|
<span class="badge-type {{ 'external' if event.is_external else event.event_type }}">{{ 'Zewnętrzne' if event.is_external else pl_types.get(event.event_type, event.event_type) }}</span>
|
||||||
{% if event.time_start %}
|
{% if event.time_start %}
|
||||||
<span>{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</span>
|
<span>{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -492,13 +536,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="event-actions">
|
<div class="event-actions">
|
||||||
|
{% if event.is_external %}
|
||||||
|
<span class="attendee-count">{{ event.attendee_count }} zainteresowanych</span>
|
||||||
|
{% if event.can_user_attend(current_user) %}
|
||||||
|
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
|
||||||
|
<button class="btn btn-sm rsvp-list-btn {{ 'rsvp-attending' if is_attending else 'rsvp-not-attending' }}" data-event-id="{{ event.id }}" data-attending="{{ 'true' if is_attending else 'false' }}" data-external="true" onclick="toggleListRSVP(this)" {{ 'onmouseenter=this.textContent="Nie interesuje" onmouseleave=this.textContent="Zainteresowany ✓"'|safe if is_attending }}>
|
||||||
|
{{ 'Zainteresowany ✓' if is_attending else 'Jestem zainteresowany' }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
|
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
|
||||||
{% if event.can_user_attend(current_user) %}
|
{% if event.can_user_attend(current_user) %}
|
||||||
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
|
{% set is_attending = event.attendees|selectattr('user_id','equalto',current_user.id)|list %}
|
||||||
<button class="btn btn-sm rsvp-list-btn {{ 'rsvp-attending' if is_attending else 'rsvp-not-attending' }}" data-event-id="{{ event.id }}" data-attending="{{ 'true' if is_attending else 'false' }}" onclick="toggleListRSVP(this)" {{ 'onmouseenter=this.textContent="Wypisz się" onmouseleave=this.textContent="Zapisano ✓"'|safe if is_attending }}>
|
<button class="btn btn-sm rsvp-list-btn {{ 'rsvp-attending' if is_attending else 'rsvp-not-attending' }}" data-event-id="{{ event.id }}" data-attending="{{ 'true' if is_attending else 'false' }}" data-external="false" onclick="toggleListRSVP(this)" {{ 'onmouseenter=this.textContent="Wypisz się" onmouseleave=this.textContent="Zapisano ✓"'|safe if is_attending }}>
|
||||||
{{ 'Zapisano ✓' if is_attending else 'Zapisz się' }}
|
{{ 'Zapisano ✓' if is_attending else 'Zapisz się' }}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="btn btn-outline btn-sm">Szczegóły</a>
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="btn btn-outline btn-sm">Szczegóły</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -547,6 +601,7 @@
|
|||||||
var csrfToken = '{{ csrf_token() }}';
|
var csrfToken = '{{ csrf_token() }}';
|
||||||
async function toggleListRSVP(btn) {
|
async function toggleListRSVP(btn) {
|
||||||
var eventId = btn.dataset.eventId;
|
var eventId = btn.dataset.eventId;
|
||||||
|
var isExt = btn.dataset.external === 'true';
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
try {
|
try {
|
||||||
var resp = await fetch('/kalendarz/' + eventId + '/rsvp', {
|
var resp = await fetch('/kalendarz/' + eventId + '/rsvp', {
|
||||||
@ -555,15 +610,20 @@ async function toggleListRSVP(btn) {
|
|||||||
});
|
});
|
||||||
var data = await resp.json();
|
var data = await resp.json();
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
var addedText = isExt ? 'Zainteresowany ✓' : 'Zapisano ✓';
|
||||||
|
var removedText = isExt ? 'Jestem zainteresowany' : 'Zapisz się';
|
||||||
|
var hoverText = isExt ? 'Nie interesuje' : 'Wypisz się';
|
||||||
|
var countLabel = isExt ? ' zainteresowanych' : ' uczestników';
|
||||||
|
|
||||||
if (data.action === 'added') {
|
if (data.action === 'added') {
|
||||||
btn.textContent = 'Zapisano ✓';
|
btn.textContent = addedText;
|
||||||
btn.classList.remove('rsvp-not-attending');
|
btn.classList.remove('rsvp-not-attending');
|
||||||
btn.classList.add('rsvp-attending');
|
btn.classList.add('rsvp-attending');
|
||||||
btn.dataset.attending = 'true';
|
btn.dataset.attending = 'true';
|
||||||
btn.onmouseenter = function() { this.textContent = 'Wypisz się'; };
|
btn.onmouseenter = function() { this.textContent = hoverText; };
|
||||||
btn.onmouseleave = function() { this.textContent = 'Zapisano ✓'; };
|
btn.onmouseleave = function() { this.textContent = addedText; };
|
||||||
} else {
|
} else {
|
||||||
btn.textContent = 'Zapisz się';
|
btn.textContent = removedText;
|
||||||
btn.classList.remove('rsvp-attending');
|
btn.classList.remove('rsvp-attending');
|
||||||
btn.classList.add('rsvp-not-attending');
|
btn.classList.add('rsvp-not-attending');
|
||||||
btn.dataset.attending = 'false';
|
btn.dataset.attending = 'false';
|
||||||
@ -571,9 +631,27 @@ async function toggleListRSVP(btn) {
|
|||||||
btn.onmouseleave = null;
|
btn.onmouseleave = null;
|
||||||
}
|
}
|
||||||
var countEl = btn.parentElement.querySelector('.attendee-count');
|
var countEl = btn.parentElement.querySelector('.attendee-count');
|
||||||
if (countEl) countEl.textContent = data.attendee_count + ' uczestników';
|
if (countEl) countEl.textContent = data.attendee_count + countLabel;
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Filter toggle for external events */
|
||||||
|
(function() {
|
||||||
|
var cb = document.getElementById('show-external');
|
||||||
|
var stored = localStorage.getItem('nordabiz_show_external');
|
||||||
|
if (stored === 'false') cb.checked = false;
|
||||||
|
|
||||||
|
function applyFilter() {
|
||||||
|
var show = cb.checked;
|
||||||
|
localStorage.setItem('nordabiz_show_external', show);
|
||||||
|
document.querySelectorAll('[data-external="true"]').forEach(function(el) {
|
||||||
|
el.style.display = show ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
cb.addEventListener('change', applyFilter);
|
||||||
|
applyFilter();
|
||||||
|
})();
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user