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>
302 lines
14 KiB
HTML
Executable File
302 lines
14 KiB
HTML
Executable File
{% extends "base.html" %}
|
|
|
|
{% block title %}Nowe wydarzenie - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.form-container {
|
|
max-width: 700px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.description-preview {
|
|
padding: var(--spacing-md); background: var(--background);
|
|
border: 1px solid var(--border); border-radius: var(--radius);
|
|
font-size: var(--font-size-sm); line-height: 1.6; max-height: 300px; overflow-y: auto;
|
|
}
|
|
.description-preview h3 { font-size: 1em; margin: 12px 0 4px; }
|
|
.description-preview ul, .description-preview ol { padding-left: 20px; margin: 4px 0; }
|
|
.description-preview p { margin: 4px 0; }
|
|
|
|
.form-header {
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.form-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-card {
|
|
background: var(--surface);
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-xl);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.form-group {
|
|
margin-bottom: var(--spacing-lg);
|
|
}
|
|
|
|
.form-group label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: var(--spacing-xs);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.form-group input,
|
|
.form-group select,
|
|
.form-group textarea {
|
|
width: 100%;
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: 1px solid var(--border);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-base);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.form-group input:focus,
|
|
.form-group select:focus,
|
|
.form-group textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
|
|
}
|
|
|
|
.form-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.form-hint {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
margin-top: var(--spacing-xs);
|
|
}
|
|
|
|
.form-actions {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
margin-top: var(--spacing-xl);
|
|
}
|
|
|
|
.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);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="form-container">
|
|
<div style="display: flex; gap: var(--spacing-md); flex-wrap: wrap;">
|
|
{% if event %}
|
|
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" 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 wydarzenia
|
|
</a>
|
|
{% endif %}
|
|
<a href="{{ url_for('admin.admin_calendar') }}" 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>
|
|
Powrot do listy wydarzen
|
|
</a>
|
|
</div>
|
|
|
|
<div class="form-header">
|
|
<h1>{{ 'Edytuj wydarzenie' if event else 'Nowe wydarzenie' }}</h1>
|
|
<p class="text-muted">{{ 'Zmień dane wydarzenia' if event else 'Dodaj spotkanie lub wydarzenie Norda Biznes' }}</p>
|
|
</div>
|
|
|
|
<div class="form-card">
|
|
<form method="POST" action="{{ url_for('admin.admin_calendar_edit', event_id=event.id) if event else url_for('admin.admin_calendar_new') }}" enctype="multipart/form-data">
|
|
<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;"{{ ' checked' if event and event.is_external }}>
|
|
<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" value="{{ event.external_source or '' if event else '' }}">
|
|
</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/..." value="{{ event.external_url or '' if event else '' }}">
|
|
<div class="form-hint">Link do strony zewnętrznej, gdzie użytkownicy mogą się zarejestrować</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="title">Tytul wydarzenia *</label>
|
|
<input type="text" id="title" name="title" required maxlength="255" placeholder="np. Spotkanie czlonkow Norda Biznes" value="{{ event.title if event else '' }}">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label>Opis</label>
|
|
{% if event and event.description %}
|
|
<div class="description-preview">{{ event.description|safe }}</div>
|
|
<div class="form-hint">Opis wydarzenia jest zarządzany przez administratora portalu. Aby zmienić treść opisu, skontaktuj się z administratorem.</div>
|
|
{% else %}
|
|
<textarea id="description" name="description" rows="4" placeholder="Opisz co będzie się działo na wydarzeniu..."></textarea>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="event_type">Typ wydarzenia</label>
|
|
<select id="event_type" name="event_type">
|
|
<option value="meeting"{{ ' selected' if event and event.event_type == 'meeting' }}>Spotkanie</option>
|
|
<option value="webinar"{{ ' selected' if event and event.event_type == 'webinar' }}>Webinar</option>
|
|
<option value="networking"{{ ' selected' if event and event.event_type == 'networking' }}>Networking</option>
|
|
<option value="rada"{{ ' selected' if event and event.event_type == 'rada' }}>Rada Izby</option>
|
|
<option value="other"{{ ' selected' if event and event.event_type == 'other' }}>Inne</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="event_date">Data rozpoczęcia *</label>
|
|
<input type="date" id="event_date" name="event_date" required value="{{ event.event_date.strftime('%Y-%m-%d') if event else '' }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="event_date_end">Data zakończenia (opcjonalna — tylko dla wielodniowych)</label>
|
|
<input type="date" id="event_date_end" name="event_date_end" value="{{ event.event_date_end.strftime('%Y-%m-%d') if event and event.event_date_end else '' }}">
|
|
<div class="form-hint">Pozostaw puste dla wydarzeń jednodniowych. Jeśli wydarzenie trwa 2+ dni (np. targi, konferencja), podaj ostatni dzień.</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="access_level">Poziom dostępu *</label>
|
|
<select id="access_level" name="access_level">
|
|
<option value="members_only"{{ ' selected' if event and event.access_level == 'members_only' }}>Tylko członkowie Izby NORDA</option>
|
|
<option value="rada_only"{{ ' selected' if event and event.access_level == 'rada_only' }}>Tylko Rada Izby</option>
|
|
<option value="public"{{ ' selected' if event and event.access_level == 'public' }}>Publiczne (wszyscy zalogowani)</option>
|
|
</select>
|
|
<div class="form-hint">
|
|
<strong>Członkowie</strong> - wszyscy członkowie Izby NORDA mogą widzieć i zapisać się<br>
|
|
<strong>Rada Izby</strong> - tylko wyznaczeni członkowie Rady Izby<br>
|
|
<strong>Publiczne</strong> - widoczne dla wszystkich zalogowanych użytkowników
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="time_start">Godzina rozpoczecia</label>
|
|
<input type="time" id="time_start" name="time_start" value="{{ event.time_start.strftime('%H:%M') if event and event.time_start else '' }}">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="time_end">Godzina zakonczenia</label>
|
|
<input type="time" id="time_end" name="time_end" value="{{ event.time_end.strftime('%H:%M') if event and event.time_end else '' }}">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="location">Miejsce</label>
|
|
<input type="text" id="location" name="location" maxlength="500" placeholder="np. ul. Tomasza Rogali 11, Wejherowo lub Online" value="{{ event.location or '' if event else '' }}">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="location_url">Link do lokalizacji</label>
|
|
<input type="url" id="location_url" name="location_url" placeholder="np. link do Google Maps lub Zoom" value="{{ event.location_url or '' if event else '' }}">
|
|
<div class="form-hint">Opcjonalny link do mapy lub platformy online</div>
|
|
</div>
|
|
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="speaker_name">Prelegent</label>
|
|
<input type="text" id="speaker_name" name="speaker_name" maxlength="255" placeholder="Imie i nazwisko" value="{{ event.speaker_name or '' if event else '' }}">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="max_attendees">Limit uczestnikow</label>
|
|
<input type="number" id="max_attendees" name="max_attendees" min="1" placeholder="Pozostaw puste = bez limitu" value="{{ event.max_attendees or '' if event else '' }}">
|
|
</div>
|
|
</div>
|
|
|
|
<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_paid" name="is_paid" style="width: auto;"{{ ' checked' if event and event.is_paid }}>
|
|
<span>Wydarzenie płatne</span>
|
|
</label>
|
|
<div class="form-hint">Zaznacz, jeśli uczestnicy muszą wnieść opłatę za udział.</div>
|
|
</div>
|
|
|
|
<div id="paid-fields" style="display: none;">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<label for="price_member">Cena dla członków Izby (zł) *</label>
|
|
<input type="number" id="price_member" name="price_member" min="0" step="1" placeholder="np. 140" value="{{ event.price_member|int if event and event.price_member else '' }}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="price_guest">Cena dla gości (zł) *</label>
|
|
<input type="number" id="price_guest" name="price_guest" min="0" step="1" placeholder="np. 240" value="{{ event.price_guest|int if event and event.price_guest else '' }}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="attachment">Załącznik (PDF, DOCX)</label>
|
|
{% if event and event.attachment_filename %}
|
|
<div class="form-hint" style="margin-bottom: 6px;">Aktualny: <strong>{{ event.attachment_filename }}</strong> — wybierz nowy plik, aby zastąpić</div>
|
|
{% endif %}
|
|
<input type="file" id="attachment" name="attachment" accept=".pdf,.docx,.doc">
|
|
<div class="form-hint">Opcjonalny plik do pobrania przez uczestników (np. zaproszenie, agenda)</div>
|
|
</div>
|
|
|
|
<div class="form-actions">
|
|
<button type="submit" class="btn btn-primary">{{ 'Zapisz zmiany' if event else 'Utworz wydarzenie' }}</button>
|
|
<a href="{{ url_for('admin.admin_calendar') }}" class="btn btn-secondary">Anuluj</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% 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();
|
|
|
|
// Paid event toggle
|
|
const paidCb = document.getElementById('is_paid');
|
|
const paidFields = document.getElementById('paid-fields');
|
|
function togglePaid() {
|
|
paidFields.style.display = paidCb.checked ? 'block' : 'none';
|
|
}
|
|
paidCb.addEventListener('change', togglePaid);
|
|
togglePaid();
|
|
})();
|
|
{% endblock %}
|