nordabiz/templates/calendar/index.html
Maciej Pienczyn 4181a2e760 refactor: Migrate access control from is_admin to role-based system
Replace ~170 manual `if not current_user.is_admin` checks with:
- @role_required(SystemRole.ADMIN) for user management, security, ZOPK
- @role_required(SystemRole.OFFICE_MANAGER) for content management
- current_user.can_access_admin_panel() for admin UI access
- current_user.can_moderate_forum() for forum moderation
- current_user.can_edit_company(id) for company permissions

Add @office_manager_required decorator shortcut.
Add SQL migration to sync existing users' role field.

Role hierarchy: UNAFFILIATED(10) < MEMBER(20) < EMPLOYEE(30) < MANAGER(40) < OFFICE_MANAGER(50) < ADMIN(100)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-01 21:05:22 +01:00

508 lines
14 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; }
/* 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 %}
{% 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 }}">&larr; Poprzedni</a>
<span class="current-month">{{ month_name }} {{ year }}</span>
<a href="?view=grid&year={{ next_year }}&month={{ next_month }}">Następny &rarr;</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">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>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">{{ 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 }}">{{ 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[:150] }}{% if event.description|length > 150 %}...{% endif %}</div>
{% endif %}
</div>
<div class="event-actions">
<span class="attendee-count">{{ event.attendee_count }} uczestników</span>
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" class="btn btn-primary btn-sm">Szczegoly</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">{{ 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 }}">{{ 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 %}