feat: Add monthly grid view for calendar
- Add view toggle (List/Calendar) to calendar toolbar - Implement monthly grid view with CSS Grid layout - Add month navigation (previous/next) - Color-coded event types (meeting, networking, webinar, other) - Highlight today's date and weekends - Polish month names - Responsive design for mobile URL params: ?view=grid&year=2026&month=1 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9eae623d3e
commit
2a4408327b
75
app.py
75
app.py
@ -2318,27 +2318,94 @@ def admin_fees_export():
|
||||
@app.route('/kalendarz')
|
||||
@login_required
|
||||
def calendar_index():
|
||||
"""Kalendarz wydarzeń Norda Biznes"""
|
||||
"""Kalendarz wydarzeń Norda Biznes - widok listy lub siatki miesięcznej"""
|
||||
from datetime import date
|
||||
import calendar as cal_module
|
||||
|
||||
# Polskie nazwy miesięcy
|
||||
POLISH_MONTHS = {
|
||||
1: 'Styczeń', 2: 'Luty', 3: 'Marzec', 4: 'Kwiecień',
|
||||
5: 'Maj', 6: 'Czerwiec', 7: 'Lipiec', 8: 'Sierpień',
|
||||
9: 'Wrzesień', 10: 'Październik', 11: 'Listopad', 12: 'Grudzień'
|
||||
}
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
today = date.today()
|
||||
|
||||
# Nadchodzące wydarzenia
|
||||
# Parametry widoku
|
||||
view_mode = request.args.get('view', 'list') # list lub grid
|
||||
year = request.args.get('year', today.year, type=int)
|
||||
month = request.args.get('month', today.month, type=int)
|
||||
|
||||
# Walidacja miesiąca/roku
|
||||
if month < 1:
|
||||
month = 12
|
||||
year -= 1
|
||||
elif month > 12:
|
||||
month = 1
|
||||
year += 1
|
||||
|
||||
# Oblicz poprzedni/następny miesiąc
|
||||
if month == 1:
|
||||
prev_month, prev_year = 12, year - 1
|
||||
else:
|
||||
prev_month, prev_year = month - 1, year
|
||||
|
||||
if month == 12:
|
||||
next_month, next_year = 1, year + 1
|
||||
else:
|
||||
next_month, next_year = month + 1, year
|
||||
|
||||
# Dane dla widoku siatki
|
||||
month_days = []
|
||||
events_by_day = {}
|
||||
|
||||
if view_mode == 'grid':
|
||||
# Pobierz wydarzenia z danego miesiąca
|
||||
first_day = date(year, month, 1)
|
||||
last_day = date(year, month, cal_module.monthrange(year, month)[1])
|
||||
events = db.query(NordaEvent).filter(
|
||||
NordaEvent.event_date >= first_day,
|
||||
NordaEvent.event_date <= last_day
|
||||
).order_by(NordaEvent.event_date.asc()).all()
|
||||
|
||||
# Przygotuj strukturę kalendarza (poniedziałek = 0)
|
||||
cal = cal_module.Calendar(firstweekday=0)
|
||||
month_days = cal.monthdayscalendar(year, month)
|
||||
|
||||
# Mapuj wydarzenia na dni
|
||||
for event in events:
|
||||
day = event.event_date.day
|
||||
if day not in events_by_day:
|
||||
events_by_day[day] = []
|
||||
events_by_day[day].append(event)
|
||||
|
||||
# Dane dla widoku listy (zawsze potrzebne dla fallback)
|
||||
upcoming = db.query(NordaEvent).filter(
|
||||
NordaEvent.event_date >= today
|
||||
).order_by(NordaEvent.event_date.asc()).all()
|
||||
|
||||
# Przeszłe wydarzenia (ostatnie 5)
|
||||
past = db.query(NordaEvent).filter(
|
||||
NordaEvent.event_date < today
|
||||
).order_by(NordaEvent.event_date.desc()).limit(5).all()
|
||||
|
||||
return render_template('calendar/index.html',
|
||||
# Dane dla widoku listy
|
||||
upcoming_events=upcoming,
|
||||
past_events=past,
|
||||
today=today
|
||||
today=today,
|
||||
# Dane dla widoku siatki
|
||||
view_mode=view_mode,
|
||||
year=year,
|
||||
month=month,
|
||||
month_name=POLISH_MONTHS.get(month, ''),
|
||||
month_days=month_days,
|
||||
events_by_day=events_by_day,
|
||||
prev_month=prev_month,
|
||||
prev_year=prev_year,
|
||||
next_month=next_month,
|
||||
next_year=next_year,
|
||||
)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
.calendar-header {
|
||||
margin-bottom: var(--spacing-xl);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
.calendar-header h1 {
|
||||
@ -13,6 +13,185 @@
|
||||
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);
|
||||
}
|
||||
@ -140,6 +319,34 @@
|
||||
.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; }
|
||||
|
||||
/* 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 %}
|
||||
|
||||
@ -149,6 +356,79 @@
|
||||
<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 }}">Nastepny →</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<a href="{{ url_for('admin_calendar') }}" class="btn btn-secondary btn-sm">Zarzadzaj</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_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">meeting</span> Rada/Spotkanie</span>
|
||||
<span><span class="badge-type networking">networking</span> Networking</span>
|
||||
<span><span class="badge-type webinar">webinar</span> Webinar</span>
|
||||
<span><span class="badge-type other">other</span> Inne</span>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<!-- ================ WIDOK LISTY ================ -->
|
||||
|
||||
<!-- Nadchodzące wydarzenia -->
|
||||
<div class="events-section">
|
||||
<h2>Nadchodzace wydarzenia</h2>
|
||||
@ -223,9 +503,5 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if current_user.is_admin %}
|
||||
<div style="margin-top: var(--spacing-xl);">
|
||||
<a href="{{ url_for('admin_calendar') }}" class="btn btn-secondary">Zarzadzaj wydarzeniami</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user