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
- Month names: sty, lut, mar... instead of Jan, Feb, Mar - Event types: Spotkanie, Networking, Webinar, Inne - RSVP "Wezmę udział" button directly on event list cards - Grid legend labels also in Polish Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
547 lines
16 KiB
HTML
Executable File
547 lines
16 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Kalendarz - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.calendar-header {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.calendar-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Toolbar z przyciskami widoku */
|
|
.calendar-toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
background: var(--surface);
|
|
padding: var(--spacing-md);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.view-toggle {
|
|
display: flex;
|
|
gap: 2px;
|
|
background: var(--border);
|
|
border-radius: var(--radius);
|
|
padding: 2px;
|
|
}
|
|
|
|
.view-toggle a {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
text-decoration: none;
|
|
color: var(--text-secondary);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.view-toggle a.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.view-toggle a:not(.active):hover {
|
|
background: var(--surface);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.month-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.month-nav a {
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
text-decoration: none;
|
|
color: var(--text-secondary);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-sm);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.month-nav a:hover {
|
|
background: var(--surface-hover, #f3f4f6);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.current-month {
|
|
font-weight: 600;
|
|
font-size: var(--font-size-lg);
|
|
min-width: 160px;
|
|
text-align: center;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* Siatka kalendarza */
|
|
.calendar-grid {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.calendar-header-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
}
|
|
|
|
.day-header {
|
|
padding: var(--spacing-sm);
|
|
text-align: center;
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.day-header.weekend {
|
|
background: #1e40af;
|
|
}
|
|
|
|
.calendar-week {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
}
|
|
|
|
.calendar-day {
|
|
min-height: 110px;
|
|
padding: var(--spacing-xs);
|
|
border: 1px solid var(--border);
|
|
border-top: none;
|
|
background: var(--background);
|
|
vertical-align: top;
|
|
}
|
|
|
|
.calendar-day:not(:last-child) {
|
|
border-right: none;
|
|
}
|
|
|
|
.calendar-day.empty {
|
|
background: #f9fafb;
|
|
}
|
|
|
|
.calendar-day.today {
|
|
background: #eff6ff;
|
|
border-color: var(--primary);
|
|
border-width: 2px;
|
|
}
|
|
|
|
.calendar-day.weekend {
|
|
background: #fafafa;
|
|
}
|
|
|
|
.day-number {
|
|
font-weight: 600;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.calendar-day.today .day-number {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.calendar-event {
|
|
display: block;
|
|
padding: 3px 6px;
|
|
margin-bottom: 3px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: 11px;
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.calendar-event:hover {
|
|
opacity: 0.8;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.calendar-event.meeting {
|
|
background: #dbeafe;
|
|
color: #1e40af;
|
|
}
|
|
|
|
.calendar-event.networking {
|
|
background: #fef3c7;
|
|
color: #92400e;
|
|
}
|
|
|
|
.calendar-event.webinar {
|
|
background: #dcfce7;
|
|
color: #166534;
|
|
}
|
|
|
|
.calendar-event.other {
|
|
background: #f3e8ff;
|
|
color: #7c3aed;
|
|
}
|
|
|
|
/* Widok listy - istniejące style */
|
|
.events-section {
|
|
margin-bottom: var(--spacing-2xl);
|
|
}
|
|
|
|
.events-section h2 {
|
|
font-size: var(--font-size-xl);
|
|
margin-bottom: var(--spacing-lg);
|
|
color: var(--text-primary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.event-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-md);
|
|
box-shadow: var(--shadow);
|
|
display: flex;
|
|
gap: var(--spacing-lg);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.event-card:hover {
|
|
box-shadow: var(--shadow-md);
|
|
}
|
|
|
|
.event-card.featured {
|
|
border-left: 4px solid var(--primary);
|
|
}
|
|
|
|
.event-card.past {
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.event-date-box {
|
|
min-width: 70px;
|
|
text-align: center;
|
|
background: var(--primary);
|
|
color: white;
|
|
border-radius: var(--radius);
|
|
padding: var(--spacing-sm);
|
|
}
|
|
|
|
.event-date-box .day {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
}
|
|
|
|
.event-date-box .month {
|
|
font-size: var(--font-size-sm);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.event-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.event-title {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.event-title a {
|
|
color: inherit;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.event-title a:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
.event-meta {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-bottom: var(--spacing-sm);
|
|
}
|
|
|
|
.event-meta span {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
}
|
|
|
|
.event-description {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.event-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.attendee-count {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
}
|
|
|
|
.badge-type {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
border-radius: var(--radius-sm);
|
|
font-size: var(--font-size-xs);
|
|
font-weight: 500;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-type.meeting { background: #dbeafe; color: #1e40af; }
|
|
.badge-type.webinar { background: #dcfce7; color: #166534; }
|
|
.badge-type.networking { background: #fef3c7; color: #92400e; }
|
|
.badge-type.other { background: #f3e8ff; color: #7c3aed; }
|
|
|
|
.rsvp-list-btn { min-width: 110px; }
|
|
|
|
/* Responsywność */
|
|
@media (max-width: 768px) {
|
|
.calendar-toolbar {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.month-nav {
|
|
justify-content: center;
|
|
}
|
|
|
|
.calendar-day {
|
|
min-height: 80px;
|
|
padding: 2px;
|
|
}
|
|
|
|
.calendar-event {
|
|
font-size: 10px;
|
|
padding: 2px 4px;
|
|
}
|
|
|
|
.day-header {
|
|
font-size: var(--font-size-xs);
|
|
padding: var(--spacing-xs);
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% set pl_months = {'Jan':'sty','Feb':'lut','Mar':'mar','Apr':'kwi','May':'maj','Jun':'cze','Jul':'lip','Aug':'sie','Sep':'wrz','Oct':'paź','Nov':'lis','Dec':'gru'} %}
|
|
{% set pl_types = {'meeting':'Spotkanie','networking':'Networking','webinar':'Webinar','other':'Inne','conference':'Konferencja','workshop':'Warsztaty'} %}
|
|
|
|
{% block content %}
|
|
<div class="calendar-header">
|
|
<h1>Kalendarz wydarzeń</h1>
|
|
<p class="text-muted">Spotkania i wydarzenia Norda Biznes</p>
|
|
</div>
|
|
|
|
<!-- Toolbar z przyciskami widoku -->
|
|
<div class="calendar-toolbar">
|
|
<div class="view-toggle">
|
|
<a href="?view=list" class="{% if view_mode == 'list' %}active{% endif %}">
|
|
Lista
|
|
</a>
|
|
<a href="?view=grid&year={{ year }}&month={{ month }}" class="{% if view_mode == 'grid' %}active{% endif %}">
|
|
Kalendarz
|
|
</a>
|
|
</div>
|
|
|
|
{% if view_mode == 'grid' %}
|
|
<div class="month-nav">
|
|
<a href="?view=grid&year={{ prev_year }}&month={{ prev_month }}">← Poprzedni</a>
|
|
<span class="current-month">{{ month_name }} {{ year }}</span>
|
|
<a href="?view=grid&year={{ next_year }}&month={{ next_month }}">Następny →</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if current_user.can_access_admin_panel() %}
|
|
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary btn-sm">Zarządzaj</a>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if view_mode == 'grid' %}
|
|
<!-- ================ WIDOK SIATKI ================ -->
|
|
<div class="calendar-grid">
|
|
<!-- Nagłówki dni tygodnia -->
|
|
<div class="calendar-header-row">
|
|
<div class="day-header">Pon</div>
|
|
<div class="day-header">Wt</div>
|
|
<div class="day-header">Sr</div>
|
|
<div class="day-header">Czw</div>
|
|
<div class="day-header">Pt</div>
|
|
<div class="day-header weekend">Sob</div>
|
|
<div class="day-header weekend">Nd</div>
|
|
</div>
|
|
|
|
<!-- Tygodnie -->
|
|
{% for week in month_days %}
|
|
<div class="calendar-week">
|
|
{% for day in week %}
|
|
{% set is_weekend = loop.index > 5 %}
|
|
<div class="calendar-day {% if day == 0 %}empty{% endif %}{% if day == today.day and month == today.month and year == today.year %} today{% endif %}{% if is_weekend and day != 0 %} weekend{% endif %}">
|
|
{% if day != 0 %}
|
|
<div class="day-number">{{ day }}</div>
|
|
{% if day in events_by_day %}
|
|
{% for event in events_by_day[day] %}
|
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}"
|
|
class="calendar-event {{ event.event_type }}"
|
|
title="{{ event.title }}{% if event.time_start %} - {{ event.time_start.strftime('%H:%M') }}{% endif %}">
|
|
{% if event.time_start %}{{ event.time_start.strftime('%H:%M') }} {% endif %}{{ event.title[:18] }}{% if event.title|length > 18 %}...{% endif %}
|
|
</a>
|
|
{% endfor %}
|
|
{% endif %}
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Legenda typów wydarzeń -->
|
|
<div style="margin-top: var(--spacing-lg); display: flex; gap: var(--spacing-lg); flex-wrap: wrap; font-size: var(--font-size-sm); color: var(--text-secondary);">
|
|
<span><span class="badge-type meeting">Spotkanie</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 other">Inne</span></span>
|
|
</div>
|
|
|
|
{% else %}
|
|
<!-- ================ WIDOK LISTY ================ -->
|
|
|
|
<!-- Nadchodzące wydarzenia -->
|
|
<div class="events-section">
|
|
<h2>Nadchodzące wydarzenia</h2>
|
|
|
|
{% if upcoming_events %}
|
|
{% for event in upcoming_events %}
|
|
<div class="event-card {% if event.is_featured %}featured{% endif %}">
|
|
<div class="event-date-box">
|
|
<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>
|
|
<div class="event-info">
|
|
<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 %}
|
|
</div>
|
|
<div class="event-meta">
|
|
<span class="badge-type {{ event.event_type }}">{{ pl_types.get(event.event_type, event.event_type) }}</span>
|
|
{% if event.time_start %}
|
|
<span>{{ event.time_start.strftime('%H:%M') }}{% if event.time_end %} - {{ event.time_end.strftime('%H:%M') }}{% endif %}</span>
|
|
{% endif %}
|
|
{% if event.location %}
|
|
<span>{{ event.location }}</span>
|
|
{% endif %}
|
|
{% if event.speaker_name %}
|
|
<span>Prelegent: {{ event.speaker_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if event.description %}
|
|
<div class="event-description">{{ event.description|striptags|truncate(150, True) }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="event-actions">
|
|
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
|
|
{% if event.can_user_attend(current_user) %}
|
|
<button class="btn btn-primary btn-sm rsvp-list-btn" data-event-id="{{ event.id }}" data-attending="{{ 'true' if event.attendees|selectattr('user_id','equalto',current_user.id)|list else 'false' }}" onclick="toggleListRSVP(this)">
|
|
{% if event.attendees|selectattr('user_id','equalto',current_user.id)|list %}Zapisano{% else %}Wezmę udział{% endif %}
|
|
</button>
|
|
{% endif %}
|
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="btn btn-outline btn-sm">Szczegóły</a>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<p>Brak nadchodzących wydarzeń</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Przeszłe wydarzenia -->
|
|
{% if past_events %}
|
|
<div class="events-section">
|
|
<h2>Ostatnie wydarzenia</h2>
|
|
|
|
{% for event in past_events %}
|
|
<div class="event-card past">
|
|
<div class="event-date-box" style="background: var(--secondary);">
|
|
<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>
|
|
<div class="event-info">
|
|
<div class="event-title">
|
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}">{{ event.title }}</a>
|
|
</div>
|
|
<div class="event-meta">
|
|
<span class="badge-type {{ event.event_type }}">{{ pl_types.get(event.event_type, event.event_type) }}</span>
|
|
{% if event.location %}
|
|
<span>{{ event.location }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="event-actions">
|
|
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
var csrfToken = '{{ csrf_token() }}';
|
|
async function toggleListRSVP(btn) {
|
|
var eventId = btn.dataset.eventId;
|
|
btn.disabled = true;
|
|
try {
|
|
var resp = await fetch('/kalendarz/' + eventId + '/rsvp', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-CSRFToken': csrfToken}
|
|
});
|
|
var data = await resp.json();
|
|
if (data.success) {
|
|
if (data.action === 'added') {
|
|
btn.textContent = 'Zapisano';
|
|
btn.classList.remove('btn-primary');
|
|
btn.classList.add('btn-secondary');
|
|
} else {
|
|
btn.textContent = 'Wezmę udział';
|
|
btn.classList.remove('btn-secondary');
|
|
btn.classList.add('btn-primary');
|
|
}
|
|
var countEl = btn.parentElement.querySelector('.attendee-count');
|
|
if (countEl) countEl.textContent = data.attendee_count + ' uczestników';
|
|
}
|
|
} catch(e) {}
|
|
btn.disabled = false;
|
|
}
|
|
{% endblock %}
|