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
- norda_events: kolumna event_date_end (NULLABLE, check constraint >= event_date) - NordaEvent: property is_multi_day, date_range_display; is_past uwzględnia koniec - Admin (new/edit): pole "Data zakończenia" w formularzu - Calendar grid: wydarzenie wielodniowe wyświetla się na każdym dniu zakresu - Upcoming/past filter: używa COALESCE(end, date) — 2-dniowe zostaje w Upcoming do swojego ostatniego dnia - event.html: "Termin" + zakres dla wielodniowych; ICS/Google end date z dateEnd - Lekki markdown dla opisów: tylko **bold** → <strong> (audyt: tylko event #60) Zero wpływu na 42 istniejące wydarzenia (NULL == stare zachowanie). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1110 lines
46 KiB
HTML
Executable File
1110 lines
46 KiB
HTML
Executable File
{% extends "base.html" %}
|
||
|
||
{% block title %}{{ event.title }} - Norda Biznes Partner{% endblock %}
|
||
|
||
{% block extra_css %}
|
||
<style>
|
||
.guest-type-btn.active { background: var(--primary) !important; color: white !important; }
|
||
.guest-type-btn:not(.active):hover { background: var(--border) !important; }
|
||
|
||
.guest-form-group { display: flex; flex-direction: column; }
|
||
.guest-form-label { font-size: 0.8em; font-weight: 500; color: var(--text-secondary); margin-bottom: 4px; }
|
||
.guest-form-input {
|
||
width: 100%; padding: 8px 12px;
|
||
border: 1px solid var(--border); border-radius: var(--radius);
|
||
font-size: 0.9em; transition: var(--transition);
|
||
background: var(--background);
|
||
}
|
||
.guest-form-input:focus { outline: none; border-color: var(--primary); box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1); }
|
||
|
||
@media (max-width: 480px) {
|
||
#guest-form div[style*="grid-template-columns"] { grid-template-columns: 1fr !important; }
|
||
}
|
||
|
||
.event-header {
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.event-header h1 {
|
||
font-size: var(--font-size-3xl);
|
||
color: var(--text-primary);
|
||
margin-bottom: var(--spacing-sm);
|
||
}
|
||
|
||
.event-detail {
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-xl);
|
||
box-shadow: var(--shadow);
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.event-info-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: var(--spacing-lg);
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.info-item svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
color: var(--primary);
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.info-label {
|
||
font-size: var(--font-size-sm);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.info-value {
|
||
font-weight: 500;
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.event-description {
|
||
margin-bottom: var(--spacing-xl);
|
||
line-height: 1.7;
|
||
color: var(--text-primary);
|
||
font-size: 1.05rem;
|
||
}
|
||
|
||
.event-description p {
|
||
margin-bottom: var(--spacing-md);
|
||
}
|
||
|
||
.event-description p:last-child {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.event-description h3 {
|
||
font-size: var(--font-size-lg);
|
||
margin: var(--spacing-lg) 0 var(--spacing-sm);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.event-description ul {
|
||
margin: 0 0 var(--spacing-md) var(--spacing-lg);
|
||
padding: 0;
|
||
}
|
||
|
||
.event-description li {
|
||
margin-bottom: var(--spacing-xs);
|
||
}
|
||
|
||
.event-callout {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: var(--spacing-md);
|
||
padding: var(--spacing-lg);
|
||
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
|
||
border: 1px solid #bbf7d0;
|
||
border-radius: var(--radius-lg);
|
||
margin-top: var(--spacing-lg);
|
||
white-space: normal;
|
||
}
|
||
|
||
.event-callout svg {
|
||
width: 22px;
|
||
height: 22px;
|
||
color: #16a34a;
|
||
flex-shrink: 0;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
.event-callout strong {
|
||
color: #15803d;
|
||
}
|
||
|
||
.event-callout small {
|
||
display: block;
|
||
margin-top: var(--spacing-xs);
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.rsvp-section {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-lg);
|
||
padding: var(--spacing-lg);
|
||
background: var(--background);
|
||
border-radius: var(--radius);
|
||
}
|
||
|
||
.attendees-section {
|
||
background: var(--surface);
|
||
border-radius: var(--radius-lg);
|
||
padding: var(--spacing-xl);
|
||
box-shadow: var(--shadow);
|
||
}
|
||
|
||
.attendees-section h2 {
|
||
font-size: var(--font-size-xl);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.attendees-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
|
||
.attendee-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-sm);
|
||
padding: var(--spacing-xs);
|
||
background: var(--background);
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-sm);
|
||
}
|
||
|
||
/* Unverified person - blue */
|
||
.attendee-name {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--spacing-xs);
|
||
padding: var(--spacing-xs) var(--spacing-sm);
|
||
background: #dbeafe;
|
||
color: #1e40af;
|
||
border-radius: var(--radius);
|
||
font-weight: 500;
|
||
font-size: var(--font-size-sm);
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
/* Verified person - green */
|
||
.attendee-name.verified {
|
||
background: #dcfce7;
|
||
color: #166534;
|
||
}
|
||
|
||
a.attendee-name:hover {
|
||
background: #bfdbfe;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
a.attendee-name.verified:hover {
|
||
background: #bbf7d0;
|
||
}
|
||
|
||
.attendee-name svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.attendee-company {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--spacing-xs);
|
||
padding: var(--spacing-xs) var(--spacing-sm);
|
||
background: #fee2e2;
|
||
color: #991b1b;
|
||
border-radius: var(--radius);
|
||
font-weight: 500;
|
||
font-size: var(--font-size-sm);
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.attendee-company:hover {
|
||
background: #fecaca;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.attendee-company svg {
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.back-link {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--spacing-xs);
|
||
color: var(--text-secondary);
|
||
text-decoration: none;
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.back-link:hover {
|
||
color: var(--primary);
|
||
}
|
||
|
||
#rsvp-btn.attending {
|
||
background: var(--success);
|
||
}
|
||
|
||
.event-logo {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
margin-bottom: var(--spacing-lg);
|
||
}
|
||
|
||
.event-logo img {
|
||
height: 48px;
|
||
width: auto;
|
||
}
|
||
|
||
/* Pill badge links — spójne z NordaGPT chat */
|
||
.person-link, .company-link {
|
||
display: inline-block;
|
||
padding: 2px 10px;
|
||
margin: 1px 2px;
|
||
border-radius: 12px;
|
||
text-decoration: none;
|
||
font-weight: 600;
|
||
font-size: 0.95em;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.person-link {
|
||
background: #ecfdf5;
|
||
color: #047857;
|
||
}
|
||
.person-link:hover {
|
||
background: #d1fae5;
|
||
color: #065f46;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(4, 120, 87, 0.2);
|
||
}
|
||
|
||
.company-link {
|
||
background: #fff7ed;
|
||
color: #c2410c;
|
||
}
|
||
.company-link:hover {
|
||
background: #ffedd5;
|
||
color: #9a3412;
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 2px 4px rgba(194, 65, 12, 0.2);
|
||
}
|
||
|
||
.speaker-link {
|
||
color: var(--primary);
|
||
text-decoration: none;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.speaker-link:hover {
|
||
text-decoration: underline;
|
||
}
|
||
|
||
.calendar-add {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--spacing-md);
|
||
padding: var(--spacing-md) var(--spacing-lg);
|
||
background: linear-gradient(135deg, #f0f9ff, #eff6ff);
|
||
border: 1px solid #bfdbfe;
|
||
border-radius: var(--radius-lg);
|
||
margin-bottom: var(--spacing-xl);
|
||
}
|
||
|
||
.calendar-add-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
background: var(--primary);
|
||
border-radius: var(--radius);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.calendar-add-icon svg {
|
||
width: 20px;
|
||
height: 20px;
|
||
color: white;
|
||
}
|
||
|
||
.calendar-add-label {
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 600;
|
||
color: #1e40af;
|
||
}
|
||
|
||
.calendar-add-buttons {
|
||
display: flex;
|
||
gap: var(--spacing-sm);
|
||
margin-left: auto;
|
||
}
|
||
|
||
.calendar-add-btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius);
|
||
font-size: var(--font-size-sm);
|
||
font-weight: 500;
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
border: none;
|
||
cursor: pointer;
|
||
color: white;
|
||
}
|
||
|
||
.calendar-add-btn.google {
|
||
background: #4285f4;
|
||
}
|
||
|
||
.calendar-add-btn.google:hover {
|
||
background: #3367d6;
|
||
}
|
||
|
||
.calendar-add-btn.outlook {
|
||
background: #0078d4;
|
||
}
|
||
|
||
.calendar-add-btn.outlook:hover {
|
||
background: #106ebe;
|
||
}
|
||
|
||
.calendar-add-btn svg {
|
||
width: 16px;
|
||
height: 16px;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.calendar-add {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: var(--spacing-sm);
|
||
}
|
||
.calendar-add-buttons {
|
||
margin-left: 0;
|
||
}
|
||
.calendar-add-icon {
|
||
display: none;
|
||
}
|
||
}
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block content %}
|
||
<a href="{{ url_for('calendar.calendar_index') }}" 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>
|
||
Powrót do kalendarza
|
||
</a>
|
||
|
||
{% if current_user.is_authenticated and current_user.can_access_admin_panel() %}
|
||
<div class="admin-event-bar" style="display: flex; gap: 8px; flex-wrap: wrap; padding: 10px 16px; background: #f0f4ff; border: 1px solid #c7d2fe; border-radius: var(--radius-lg); margin-bottom: var(--spacing-lg);">
|
||
<a href="{{ url_for('admin.admin_calendar_edit', event_id=event.id) }}" class="btn btn-primary" style="font-size: 0.85em;">
|
||
<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 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/>
|
||
<path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||
</svg>
|
||
Edytuj wydarzenie
|
||
</a>
|
||
{% if event.is_paid %}
|
||
<a href="{{ url_for('admin.admin_event_payments', event_id=event.id) }}" class="btn btn-outline" style="font-size: 0.85em; background: white;">
|
||
<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="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>
|
||
</svg>
|
||
Zarządzaj płatnościami
|
||
</a>
|
||
{% endif %}
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div class="event-header">
|
||
{% 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">
|
||
<img src="{{ url_for('static', filename='img/norda-logo.svg') }}" alt="Norda Biznes">
|
||
</div>
|
||
{% endif %}
|
||
<h1>{{ event.title }}
|
||
{% 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>
|
||
{% 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>
|
||
{% endif %}
|
||
</h1>
|
||
</div>
|
||
|
||
<div class="event-detail">
|
||
<div class="event-info-grid">
|
||
<div class="info-item">
|
||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
</svg>
|
||
<div>
|
||
<div class="info-label">{% if event.is_multi_day %}Termin{% else %}Data{% endif %}</div>
|
||
<div class="info-value">{{ event.date_range_display }}</div>
|
||
</div>
|
||
</div>
|
||
|
||
{% if event.time_start %}
|
||
<div class="info-item">
|
||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<circle cx="12" cy="12" r="10"></circle>
|
||
<polyline points="12 6 12 12 16 14"></polyline>
|
||
</svg>
|
||
<div>
|
||
<div class="info-label">Godzina</div>
|
||
<div class="info-value">{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if event.location %}
|
||
<div class="info-item">
|
||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
|
||
<circle cx="12" cy="10" r="3"></circle>
|
||
</svg>
|
||
<div>
|
||
<div class="info-label">Miejsce</div>
|
||
<div class="info-value">
|
||
{% if event.location_url %}
|
||
<a href="{{ event.location_url }}" target="_blank" style="color:var(--primary);">{{ event.location }}</a>
|
||
{% elif event.location and event.location not in ['Do ustalenia', 'Online'] and 'do ustalenia' not in event.location|lower %}
|
||
<a href="https://www.google.com/maps/search/{{ event.location|urlencode }}" target="_blank" style="color:var(--primary);">{{ event.location }}</a>
|
||
{% else %}
|
||
{{ event.location }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if event.speaker_name %}
|
||
<div class="info-item">
|
||
<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>
|
||
<div>
|
||
<div class="info-label">Prelegent</div>
|
||
<div class="info-value">
|
||
{% if speaker_user_id %}
|
||
<a href="{{ url_for('public.user_profile', user_id=speaker_user_id) }}" class="person-link">{{ event.speaker_name }}</a>
|
||
{% elif speaker_company_slug %}
|
||
<a href="{{ url_for('public.company_detail_by_slug', slug=speaker_company_slug) }}" class="company-link">{{ event.speaker_name }}</a>
|
||
{% else %}
|
||
{{ event.speaker_name }}
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
|
||
{% if event.time_start and not event.is_past %}
|
||
<div class="calendar-add">
|
||
<div class="calendar-add-icon">
|
||
<svg fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
|
||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"></rect>
|
||
<line x1="16" y1="2" x2="16" y2="6"></line>
|
||
<line x1="8" y1="2" x2="8" y2="6"></line>
|
||
<line x1="3" y1="10" x2="21" y2="10"></line>
|
||
</svg>
|
||
</div>
|
||
<div class="calendar-add-label">Dodaj do kalendarza</div>
|
||
<div class="calendar-add-buttons">
|
||
<a href="#" class="calendar-add-btn google" onclick="addToGoogleCalendar(); return false;">
|
||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.94-.49-7-3.85-7-7.93s3.05-7.44 7-7.93v2.02C8.16 6.57 6 9.03 6 12s2.16 5.43 5 5.91v2.02zm2 0V17.9c2.84-.48 5-2.94 5-5.9s-2.16-5.43-5-5.91V4.07c3.94.49 7 3.85 7 7.93s-3.06 7.44-7 7.93z"/></svg>
|
||
Google
|
||
</a>
|
||
<a href="#" class="calendar-add-btn outlook" onclick="downloadICS(); return false;">
|
||
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm0-12H5V6h14v2z"/></svg>
|
||
Outlook / ICS
|
||
</a>
|
||
</div>
|
||
</div>
|
||
{% 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 %}
|
||
<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;">
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if enriched_description %}
|
||
<div class="event-description">
|
||
{{ enriched_description }}
|
||
</div>
|
||
{% elif event.description %}
|
||
<div class="event-description">
|
||
{{ event.description|safe }}
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if event.attachment_filename %}
|
||
<div style="display: flex; align-items: center; gap: var(--spacing-md); padding: var(--spacing-lg); background: linear-gradient(135deg, #fef3c7, #fefce8); border: 1px solid #fde68a; border-radius: var(--radius-lg); margin-bottom: var(--spacing-xl);">
|
||
<svg width="24" height="24" fill="none" stroke="#92400e" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink: 0;">
|
||
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
|
||
<polyline points="14 2 14 8 20 8"></polyline>
|
||
<line x1="16" y1="13" x2="8" y2="13"></line>
|
||
<line x1="16" y1="17" x2="8" y2="17"></line>
|
||
</svg>
|
||
<div style="flex: 1;">
|
||
<div style="font-weight: 600; color: #92400e;">Załącznik</div>
|
||
<a href="{{ url_for('static', filename=event.attachment_path.replace('static/', '')) }}" target="_blank" style="color: #b45309; text-decoration: underline;">{{ event.attachment_filename }}</a>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
|
||
{% if not event.is_past %}
|
||
{% if event.can_user_attend(current_user) %}
|
||
<div class="rsvp-section">
|
||
{% if event.is_external %}
|
||
<div>
|
||
<strong>Interesuje Cię to wydarzenie?</strong>
|
||
<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 %}
|
||
</button>
|
||
{% else %}
|
||
<div>
|
||
<strong>Chcesz wziąć udział?</strong>
|
||
<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>
|
||
{% if event.is_paid and not user_attending %}
|
||
<p style="margin: 4px 0 0; font-size: 0.85em; color: var(--warning-text, #92400e); background: #fef3c7; padding: 6px 10px; border-radius: var(--radius-sm); display: inline-block;">
|
||
Wydarzenie płatne: <strong>{{ "%.0f"|format(event.price_member) }} zł</strong> (członkowie) / <strong>{{ "%.0f"|format(event.price_guest) }} zł</strong> (goście)
|
||
</p>
|
||
{% endif %}
|
||
</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 %}
|
||
{% if event.is_paid and user_attending %}
|
||
{% set my_amount = (user_attending.payment_amount or 0)|float %}
|
||
{% set guests_amount = user_guests|map(attribute='payment_amount')|select('ne', none)|map('float')|sum if user_guests else 0 %}
|
||
{% set total_to_pay = my_amount + guests_amount %}
|
||
<div style="margin-top: 8px; width: 100%;">
|
||
{% if user_attending.payment_status == 'paid' and not user_guests %}
|
||
<span style="background: #dcfce7; color: #166534; padding: 6px 12px; border-radius: var(--radius-sm); font-size: 0.85em; font-weight: 600; display: inline-block;">
|
||
✓ Płatność potwierdzona
|
||
</span>
|
||
{% elif user_attending.payment_status == 'exempt' and not user_guests %}
|
||
<span style="background: #f3f4f6; color: #6b7280; padding: 6px 12px; border-radius: var(--radius-sm); font-size: 0.85em; font-weight: 600; display: inline-block;">
|
||
Zwolniony z opłaty
|
||
</span>
|
||
{% else %}
|
||
<div style="background: #fef3c7; color: #92400e; padding: 10px 14px; border-radius: var(--radius-sm); font-size: 0.85em; display: inline-block;">
|
||
<strong>Do wpłaty łącznie: {{ "%.0f"|format(total_to_pay) }} zł</strong>
|
||
<div style="font-size: 0.9em; margin-top: 4px; color: #a16207;">
|
||
Ty: {{ "%.0f"|format(my_amount) }} zł
|
||
{% if user_guests %}
|
||
+ {{ user_guests|length }} {{ 'osoba' if user_guests|length == 1 else 'osoby' if user_guests|length < 5 else 'osób' }}: {{ "%.0f"|format(guests_amount) }} zł
|
||
{% endif %}
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
</div>
|
||
{% 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ą / gościa
|
||
</button>
|
||
{% endif %}
|
||
|
||
<div id="guest-form" style="display: none; margin-top: 12px; padding: 20px; background: var(--surface); border-radius: var(--radius-lg); border: 1px solid var(--border); box-shadow: var(--shadow);">
|
||
<input type="hidden" id="guest-edit-id" value="">
|
||
<input type="hidden" id="guest-type" value="external">
|
||
|
||
{% if event.is_paid and current_user.company_id %}
|
||
<div id="guest-type-selector" style="display: flex; gap: 0; margin-bottom: 16px; border-radius: var(--radius); overflow: hidden; border: 1px solid var(--border);">
|
||
<button type="button" class="guest-type-btn active" data-type="member" onclick="selectGuestType('member')" style="flex: 1; padding: 10px 16px; border: none; cursor: pointer; font-size: 0.85em; font-weight: 600; transition: var(--transition); background: var(--primary); color: white;">
|
||
Osoba z firmy{% if event.price_member %} ({{ "%.0f"|format(event.price_member) }} zł){% endif %}
|
||
</button>
|
||
<button type="button" class="guest-type-btn" data-type="external" onclick="selectGuestType('external')" style="flex: 1; padding: 10px 16px; border: none; border-left: 1px solid var(--border); cursor: pointer; font-size: 0.85em; font-weight: 600; transition: var(--transition); background: var(--background); color: var(--text-secondary);">
|
||
Gość spoza Izby{% if event.price_guest %} ({{ "%.0f"|format(event.price_guest) }} zł){% endif %}
|
||
</button>
|
||
</div>
|
||
|
||
<div id="colleague-picker" class="guest-form-group" style="margin-bottom: 12px;">
|
||
<label for="colleague-select" class="guest-form-label">Wybierz osobę z firmy</label>
|
||
<select id="colleague-select" class="form-control guest-form-input" onchange="onColleagueSelect()">
|
||
<option value="">— wpisz dane ręcznie —</option>
|
||
</select>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px;">
|
||
<div class="guest-form-group">
|
||
<label for="guest-first-name" class="guest-form-label">Imię</label>
|
||
<input type="text" id="guest-first-name" maxlength="100" class="form-control guest-form-input" placeholder="np. Jan">
|
||
</div>
|
||
<div class="guest-form-group">
|
||
<label for="guest-last-name" class="guest-form-label">Nazwisko</label>
|
||
<input type="text" id="guest-last-name" maxlength="100" class="form-control guest-form-input" placeholder="np. Kowalski">
|
||
</div>
|
||
</div>
|
||
<div class="guest-form-group" style="margin-top: 12px;">
|
||
<label for="guest-org" class="guest-form-label">Firma / organizacja</label>
|
||
<input type="text" id="guest-org" maxlength="255" class="form-control guest-form-input" placeholder="np. Nazwa firmy Sp. z o.o.">
|
||
</div>
|
||
<div style="display: flex; gap: 8px; margin-top: 16px;">
|
||
<button id="guest-submit-btn" onclick="submitGuest()" class="btn btn-primary" style="font-size: 0.9em;">Dodaj osobę</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: 8px 0 0;"></p>
|
||
</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;">
|
||
<path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
|
||
</svg>
|
||
<div>
|
||
<strong style="color: #92400e;">Wydarzenie tylko dla Rady Izby</strong>
|
||
<p class="text-muted" style="margin: 0; color: #a16207;">Zapisy są dostępne wyłącznie dla członków Rady Izby NORDA.</p>
|
||
</div>
|
||
</div>
|
||
{% endif %}
|
||
{% else %}
|
||
<div class="rsvp-section" style="background: var(--border);">
|
||
<p class="text-muted" style="margin: 0;">To wydarzenie już się odbyło.{% if event.can_user_see_attendees(current_user) %} {{ event.total_attendee_count }} osób brało udział.{% endif %}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
{% 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.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">
|
||
{% if attendee.user.person_id %}
|
||
<a href="{{ url_for('public.person_detail', person_id=attendee.user.person_id) }}" class="attendee-name verified">
|
||
{% else %}
|
||
<a href="{{ url_for('public.user_profile', user_id=attendee.user.id) }}" class="attendee-name verified">
|
||
{% endif %}
|
||
<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>
|
||
{{ attendee.user.name or attendee.user.email.split('@')[0] }}
|
||
</a>
|
||
|
||
{% 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">
|
||
<path d="M19 21V5a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v5m-4 0h4"></path>
|
||
</svg>
|
||
{{ attendee.user.company.name }}
|
||
</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>
|
||
{% endif %}
|
||
|
||
<div id="toastContainer" style="position: fixed; top: 80px; right: 20px; z-index: 1100; display: flex; flex-direction: column; gap: 10px;"></div>
|
||
|
||
<style>
|
||
.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; }
|
||
.toast.success { border-left-color: var(--success); }
|
||
.toast.error { border-left-color: var(--error); }
|
||
@keyframes toastIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||
@keyframes toastOut { from { opacity: 1; } to { opacity: 0; } }
|
||
</style>
|
||
{% endblock %}
|
||
|
||
{% block extra_js %}
|
||
const csrfToken = '{{ csrf_token() }}';
|
||
|
||
function showToast(message, type = 'info', duration = 4000) {
|
||
const container = document.getElementById('toastContainer');
|
||
const icons = { success: '✓', error: '✕', warning: '⚠', info: 'ℹ' };
|
||
const toast = document.createElement('div');
|
||
toast.className = `toast ${type}`;
|
||
toast.innerHTML = `<span style="font-size:1.2em">${icons[type]||'ℹ'}</span><span>${message}</span>`;
|
||
container.appendChild(toast);
|
||
setTimeout(() => { toast.style.animation = 'toastOut 0.3s ease forwards'; setTimeout(() => toast.remove(), 300); }, duration);
|
||
}
|
||
|
||
async function toggleRSVP() {
|
||
const btn = document.getElementById('rsvp-btn');
|
||
btn.disabled = true;
|
||
|
||
try {
|
||
const response = await fetch('{{ url_for("calendar.calendar_rsvp", event_id=event.id) }}', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-CSRFToken': csrfToken
|
||
}
|
||
});
|
||
|
||
const data = await response.json();
|
||
const isExt = {{ 'true' if event.is_external else 'false' }};
|
||
if (data.success) {
|
||
if (data.action === 'added') {
|
||
btn.textContent = isExt ? 'Nie interesuje mnie' : 'Wypisz się';
|
||
btn.classList.remove('btn-primary');
|
||
btn.classList.add('btn-secondary', 'attending');
|
||
showToast(isExt ? 'Oznaczono jako zainteresowany!' : 'Zapisano na wydarzenie!', 'success');
|
||
} else {
|
||
btn.textContent = isExt ? 'Zainteresowany' : 'Wezmę udział';
|
||
btn.classList.remove('btn-secondary', 'attending');
|
||
btn.classList.add('btn-primary');
|
||
showToast(isExt ? 'Usunięto zainteresowanie' : 'Wypisano z wydarzenia', 'info');
|
||
}
|
||
// Refresh page to update attendees list
|
||
setTimeout(() => location.reload(), 1000);
|
||
} else {
|
||
showToast(data.error || 'Wystąpił błąd', 'error');
|
||
}
|
||
} catch (error) {
|
||
showToast('Błąd połączenia', 'error');
|
||
}
|
||
|
||
btn.disabled = false;
|
||
}
|
||
|
||
/* --- Guest management --- */
|
||
let colleaguesLoaded = false;
|
||
|
||
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';
|
||
// Reset guest type to member if paid event with company
|
||
const typeSelector = document.getElementById('guest-type-selector');
|
||
if (typeSelector) {
|
||
selectGuestType('member');
|
||
loadColleagues();
|
||
}
|
||
const cs = document.getElementById('colleague-select');
|
||
if (cs) cs.value = '';
|
||
form.style.display = 'block';
|
||
btn.style.display = 'none';
|
||
document.getElementById('guest-first-name').focus();
|
||
} else {
|
||
cancelGuestForm();
|
||
}
|
||
}
|
||
|
||
function selectGuestType(type) {
|
||
document.getElementById('guest-type').value = type;
|
||
document.querySelectorAll('.guest-type-btn').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.type === type);
|
||
});
|
||
const picker = document.getElementById('colleague-picker');
|
||
if (picker) {
|
||
picker.style.display = type === 'member' ? 'block' : 'none';
|
||
}
|
||
}
|
||
|
||
async function loadColleagues() {
|
||
if (colleaguesLoaded) return;
|
||
const select = document.getElementById('colleague-select');
|
||
if (!select) return;
|
||
try {
|
||
const resp = await fetch('/kalendarz/{{ event.id }}/company-colleagues');
|
||
const data = await resp.json();
|
||
data.forEach(c => {
|
||
const opt = document.createElement('option');
|
||
opt.value = JSON.stringify(c);
|
||
opt.textContent = c.name + (c.already_registered ? ' (już zapisany/a)' : '');
|
||
if (c.already_registered) opt.disabled = true;
|
||
select.appendChild(opt);
|
||
});
|
||
colleaguesLoaded = true;
|
||
} catch(e) {}
|
||
}
|
||
|
||
function onColleagueSelect() {
|
||
const select = document.getElementById('colleague-select');
|
||
if (!select.value) return;
|
||
try {
|
||
const c = JSON.parse(select.value);
|
||
document.getElementById('guest-first-name').value = c.first_name || '';
|
||
document.getElementById('guest-last-name').value = c.last_name || '';
|
||
document.getElementById('guest-org').value = '{{ (active_company.name if active_company else (current_user.company.name if current_user.company else ""))|e }}';
|
||
} catch(e) {}
|
||
}
|
||
|
||
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, guest_type: document.getElementById('guest-type').value || 'external' })
|
||
});
|
||
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) {
|
||
nordaConfirm('Czy na pewno chcesz usunąć tę osobę towarzyszącą?', function() {
|
||
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');
|
||
tmp.innerHTML = html;
|
||
return tmp.textContent || tmp.innerText || '';
|
||
}
|
||
|
||
function foldIcsLine(line) {
|
||
// ICS spec: lines max 75 octets, continuation lines start with space
|
||
const parts = [];
|
||
while (line.length > 75) {
|
||
parts.push(line.substring(0, 75));
|
||
line = ' ' + line.substring(75);
|
||
}
|
||
parts.push(line);
|
||
return parts.join('\r\n');
|
||
}
|
||
|
||
const _evt = {
|
||
title: {{ event.title|tojson }},
|
||
date: '{{ event.event_date.strftime("%Y%m%d") }}',
|
||
dateEnd: '{{ event.event_date_end.strftime("%Y%m%d") if event.event_date_end else "" }}',
|
||
start: '{{ event.time_start.strftime("%H%M") if event.time_start else "0000" }}',
|
||
end: '{{ event.time_end.strftime("%H%M") if event.time_end else "" }}',
|
||
location: {{ (event.location or '')|tojson }},
|
||
speaker: {{ (event.speaker_name or '')|tojson }},
|
||
description: {{ (event.description or '')|tojson }},
|
||
organizerName: {{ (event.external_source if event.is_external and event.external_source else event.organizer_name or 'Norda Biznes')|tojson }},
|
||
organizerEmail: {{ (event.organizer_email or 'biuro@norda-biznes.info')|tojson }},
|
||
url: window.location.href,
|
||
};
|
||
|
||
function addToGoogleCalendar() {
|
||
const start = _evt.date + 'T' + _evt.start + '00';
|
||
const endDate = _evt.dateEnd || _evt.date;
|
||
const end = _evt.end ? (endDate + 'T' + _evt.end + '00') : '';
|
||
const details = [
|
||
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
|
||
_evt.description,
|
||
'',
|
||
'Szczegóły: ' + _evt.url,
|
||
].filter(Boolean).join('\n');
|
||
const params = new URLSearchParams({
|
||
action: 'TEMPLATE',
|
||
text: _evt.title,
|
||
dates: start + '/' + (end || start),
|
||
location: _evt.location,
|
||
details: details,
|
||
ctz: 'Europe/Warsaw',
|
||
});
|
||
window.open('https://calendar.google.com/calendar/render?' + params, '_blank');
|
||
}
|
||
|
||
function downloadICS() {
|
||
const start = _evt.date + 'T' + _evt.start + '00';
|
||
const endDate = _evt.dateEnd || _evt.date;
|
||
const end = _evt.end ? (endDate + 'T' + _evt.end + '00') : (endDate + 'T' + _evt.start + '00');
|
||
const uid = 'nordabiz-event-{{ event.id }}@nordabiznes.pl';
|
||
const now = new Date().toISOString().replace(/[-:]/g,'').split('.')[0] + 'Z';
|
||
const plainDesc = stripHtml(_evt.description);
|
||
const descParts = [
|
||
_evt.speaker ? 'Prowadzący: ' + _evt.speaker : '',
|
||
plainDesc,
|
||
'',
|
||
'Szczegóły: ' + _evt.url,
|
||
].filter(Boolean);
|
||
const desc = descParts.join('\\n');
|
||
const lines = [
|
||
'BEGIN:VCALENDAR',
|
||
'VERSION:2.0',
|
||
'PRODID:-//NordaBiznes//PL',
|
||
'BEGIN:VEVENT',
|
||
'UID:' + uid,
|
||
'DTSTAMP:' + now,
|
||
'DTSTART;TZID=Europe/Warsaw:' + start,
|
||
'DTEND;TZID=Europe/Warsaw:' + end,
|
||
'SUMMARY:' + _evt.title,
|
||
'LOCATION:' + _evt.location,
|
||
'DESCRIPTION:' + desc,
|
||
'URL:' + _evt.url,
|
||
'ORGANIZER;CN=' + _evt.organizerName + ':mailto:' + _evt.organizerEmail,
|
||
'BEGIN:VALARM',
|
||
'TRIGGER:-P1D',
|
||
'ACTION:DISPLAY',
|
||
'DESCRIPTION:Jutro: ' + _evt.title,
|
||
'END:VALARM',
|
||
'END:VEVENT',
|
||
'END:VCALENDAR',
|
||
];
|
||
const ics = lines.map(foldIcsLine).join('\r\n');
|
||
const blob = new Blob([ics], { type: 'text/calendar;charset=utf-8' });
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
a.download = _evt.title.substring(0, 50).replace(/[^a-zA-Z0-9ąćęłńóśźżĄĆĘŁŃÓŚŹŻ ]/g, '') + '.ics';
|
||
a.click();
|
||
URL.revokeObjectURL(a.href);
|
||
}
|
||
{% endblock %}
|