259 lines
13 KiB
HTML
259 lines
13 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Terminplan konfigurieren - Inventarsystem{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container py-4">
|
|
<div class="row justify-content-center">
|
|
<div class="col-12 col-lg-10 col-xl-8">
|
|
<div class="card border-0 shadow-lg rounded-4 overflow-hidden">
|
|
<div class="card-header text-white" style="background: linear-gradient(135deg, #0f4c5c, #16697a);">
|
|
<h1 class="h3 mb-1 fw-bold">Terminplan konfigurieren</h1>
|
|
<p class="mb-0 opacity-75">Erstellen Sie einen neuen Plan und verschicken Sie anschließend den Buchungslink.</p>
|
|
</div>
|
|
<div class="card-body p-4 p-md-5 bg-white">
|
|
<form method="post" action="{{ url_for('terminplaner.configure') }}" class="vstack gap-3">
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-6">
|
|
<label for="start_date" class="form-label fw-semibold">Startdatum</label>
|
|
<input type="date" id="start_date" name="start_date" class="form-control form-control-lg" required>
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<label for="end_date" class="form-label fw-semibold">Enddatum</label>
|
|
<input type="date" id="end_date" name="end_date" class="form-control form-control-lg" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border rounded-4 p-3 p-md-4 bg-light-subtle">
|
|
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2 mb-2">
|
|
<div>
|
|
<label class="form-label fw-semibold mb-0">Zeitfenster pro Tag</label>
|
|
<div class="form-text mb-0">Sobald Start- und Enddatum gesetzt sind, wird für jeden Tag automatisch ein eigener Eintrag erzeugt.</div>
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" id="build_time_frame">Tage aus Zeitraum erzeugen</button>
|
|
</div>
|
|
|
|
<div class="row g-3 mb-3">
|
|
<div class="col-12 col-md-6 col-xl-4">
|
|
<label for="default_day_start" class="form-label fw-semibold">Standard-Startzeit</label>
|
|
<input type="time" id="default_day_start" class="form-control" value="08:00">
|
|
</div>
|
|
<div class="col-12 col-md-6 col-xl-4">
|
|
<label for="default_day_end" class="form-label fw-semibold">Standard-Endzeit</label>
|
|
<input type="time" id="default_day_end" class="form-control" value="12:00">
|
|
</div>
|
|
</div>
|
|
|
|
<div id="time_frame_days" class="vstack gap-2"></div>
|
|
|
|
<div class="mt-3">
|
|
<label for="time_frame" class="form-label fw-semibold">Gespeichertes Zeitfenster</label>
|
|
<textarea id="time_frame" name="time_frame" class="form-control font-monospace" rows="5" placeholder="Wird automatisch aus den Tagen erzeugt" required></textarea>
|
|
<div class="form-text">Das Formular überträgt die erzeugten Tageszeilen an das Backend. Sie können die Liste hier bei Bedarf noch anpassen.</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-12 col-md-6">
|
|
<label for="slots_amounts" class="form-label fw-semibold">Anzahl Slots</label>
|
|
<input type="number" id="slots_amounts" name="slots_amounts" class="form-control" min="1" value="1" required>
|
|
</div>
|
|
<div class="col-12 col-md-6">
|
|
<label for="slot_lenght" class="form-label fw-semibold">Slot-Länge in Minuten</label>
|
|
<input type="number" id="slot_lenght" name="slot_lenght" class="form-control" min="1" value="45" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label for="mail" class="form-label fw-semibold">Empfänger-Mails</label>
|
|
<input type="text" id="mail" name="mail" class="form-control" placeholder="adresse1@schule.de, adresse2@schule.de">
|
|
</div>
|
|
|
|
<div>
|
|
<label for="note" class="form-label fw-semibold">Notiz</label>
|
|
<textarea id="note" name="note" class="form-control" rows="4" placeholder="Optionaler Einführungstext für die Mail"></textarea>
|
|
</div>
|
|
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="add_to_calendar" name="add_to_calendar" {% if add_to_calendar %}checked{% endif %}>
|
|
<label class="form-check-label fw-semibold" for="add_to_calendar">Kalendereintrag für den Nutzer erzeugen</label>
|
|
<div class="form-text">Erzeugt zusätzlich eine .ics-Datei, die in gängige Kalender importiert werden kann.</div>
|
|
</div>
|
|
|
|
<div class="d-flex flex-column flex-sm-row gap-2 pt-2">
|
|
<button type="submit" class="btn btn-primary btn-lg">Terminplan erzeugen</button>
|
|
<a class="btn btn-outline-secondary btn-lg" href="{{ url_for('terminplaner.main') }}">Zur Übersicht</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
{% if generated_link %}
|
|
<div class="alert alert-success mt-4 shadow-sm rounded-4">
|
|
<div class="fw-bold mb-1">Buchungslink erstellt</div>
|
|
<div class="mb-2">
|
|
{% if email_service_enabled %}
|
|
Teilen Sie diesen Link mit den Teilnehmenden oder versenden Sie ihn direkt per E-Mail.
|
|
{% else %}
|
|
Der E-Mail-Service ist deaktiviert. Teilen Sie diesen Link manuell mit den Teilnehmenden.
|
|
{% endif %}
|
|
</div>
|
|
<a href="{{ generated_link }}" class="d-inline-block text-break">{{ generated_link }}</a>
|
|
{% if calendar_link %}
|
|
<div class="mt-3">
|
|
<div class="fw-semibold mb-1">Kalendereintrag</div>
|
|
<a href="{{ calendar_link }}" class="btn btn-outline-primary btn-sm">.ics herunterladen</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
(function () {
|
|
const startDateInput = document.getElementById('start_date');
|
|
const endDateInput = document.getElementById('end_date');
|
|
const buildButton = document.getElementById('build_time_frame');
|
|
const daysContainer = document.getElementById('time_frame_days');
|
|
const timeFrameTextarea = document.getElementById('time_frame');
|
|
const defaultStartInput = document.getElementById('default_day_start');
|
|
const defaultEndInput = document.getElementById('default_day_end');
|
|
|
|
if (!startDateInput || !endDateInput || !buildButton || !daysContainer || !timeFrameTextarea || !defaultStartInput || !defaultEndInput) {
|
|
return;
|
|
}
|
|
|
|
const weekdayFormatter = new Intl.DateTimeFormat('de-DE', {
|
|
weekday: 'long',
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
});
|
|
|
|
function parseDate(value) {
|
|
if (!value) {
|
|
return null;
|
|
}
|
|
|
|
const parts = value.split('-').map(Number);
|
|
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
|
return null;
|
|
}
|
|
|
|
return new Date(parts[0], parts[1] - 1, parts[2]);
|
|
}
|
|
|
|
function formatDateForRow(date) {
|
|
return weekdayFormatter.format(date);
|
|
}
|
|
|
|
function formatDateForValue(date) {
|
|
const year = date.getFullYear();
|
|
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
const day = String(date.getDate()).padStart(2, '0');
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
function addDays(date, days) {
|
|
const copy = new Date(date);
|
|
copy.setDate(copy.getDate() + days);
|
|
return copy;
|
|
}
|
|
|
|
function syncTextarea() {
|
|
const rows = Array.from(daysContainer.querySelectorAll('[data-day-row]'));
|
|
const lines = rows.map(function (row) {
|
|
const dayValue = row.getAttribute('data-day-value') || '';
|
|
const startTime = row.querySelector('[data-time-start]')?.value || '';
|
|
const endTime = row.querySelector('[data-time-end]')?.value || '';
|
|
return `${dayValue} ${startTime}-${endTime}`.trim();
|
|
}).filter(Boolean);
|
|
|
|
timeFrameTextarea.value = lines.join('\n');
|
|
}
|
|
|
|
function renderRows() {
|
|
const startDate = parseDate(startDateInput.value);
|
|
const endDate = parseDate(endDateInput.value);
|
|
|
|
if (!startDate || !endDate) {
|
|
daysContainer.innerHTML = '<div class="text-muted small">Bitte Start- und Enddatum auswählen, damit die Tageszeilen erzeugt werden.</div>';
|
|
timeFrameTextarea.value = '';
|
|
endDateInput.setCustomValidity('');
|
|
return;
|
|
}
|
|
|
|
if (endDate < startDate) {
|
|
daysContainer.innerHTML = '<div class="text-danger small">Das Enddatum muss nach dem Startdatum liegen.</div>';
|
|
timeFrameTextarea.value = '';
|
|
endDateInput.setCustomValidity('Das Enddatum muss nach dem Startdatum liegen.');
|
|
return;
|
|
}
|
|
|
|
endDateInput.setCustomValidity('');
|
|
|
|
const existingValues = new Map();
|
|
Array.from(daysContainer.querySelectorAll('[data-day-row]')).forEach(function (row) {
|
|
const dayValue = row.getAttribute('data-day-value');
|
|
const startTime = row.querySelector('[data-time-start]')?.value || '';
|
|
const endTime = row.querySelector('[data-time-end]')?.value || '';
|
|
if (dayValue) {
|
|
existingValues.set(dayValue, { startTime, endTime });
|
|
}
|
|
});
|
|
|
|
const defaultStart = defaultStartInput.value || '08:00';
|
|
const defaultEnd = defaultEndInput.value || '12:00';
|
|
const rows = [];
|
|
|
|
for (let current = startDate; current <= endDate; current = addDays(current, 1)) {
|
|
const dayValue = formatDateForValue(current);
|
|
const preserved = existingValues.get(dayValue) || {};
|
|
const rowStart = preserved.startTime || defaultStart;
|
|
const rowEnd = preserved.endTime || defaultEnd;
|
|
|
|
rows.push(`
|
|
<div class="border rounded-3 bg-white p-3" data-day-row data-day-value="${dayValue}">
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-12 col-lg-5">
|
|
<label class="form-label fw-semibold mb-1">${formatDateForRow(current)}</label>
|
|
<div class="text-muted small">${dayValue}</div>
|
|
</div>
|
|
<div class="col-6 col-lg-3">
|
|
<label class="form-label mb-1">Von</label>
|
|
<input type="time" class="form-control" value="${rowStart}" data-time-start>
|
|
</div>
|
|
<div class="col-6 col-lg-3">
|
|
<label class="form-label mb-1">Bis</label>
|
|
<input type="time" class="form-control" value="${rowEnd}" data-time-end>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
daysContainer.innerHTML = rows.join('');
|
|
daysContainer.querySelectorAll('input[type="time"]').forEach(function (input) {
|
|
input.addEventListener('input', syncTextarea);
|
|
input.addEventListener('change', syncTextarea);
|
|
});
|
|
|
|
syncTextarea();
|
|
}
|
|
|
|
buildButton.addEventListener('click', renderRows);
|
|
startDateInput.addEventListener('input', renderRows);
|
|
startDateInput.addEventListener('change', renderRows);
|
|
endDateInput.addEventListener('input', renderRows);
|
|
endDateInput.addEventListener('change', renderRows);
|
|
defaultStartInput.addEventListener('change', syncTextarea);
|
|
defaultEndInput.addEventListener('change', syncTextarea);
|
|
|
|
if (startDateInput.value && endDateInput.value) {
|
|
renderRows();
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|