|
|
|
@@ -14,8 +14,10 @@
|
|
|
|
|
<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">
|
|
|
|
|
<labal for="title" class="form-label fw-semibold">Titel des Terminplans</label>
|
|
|
|
|
<input type="text" id="title" name="title" class="form-control form-control-lg" placeholder="z.B. Elternsprechtag Klasse 10a" value="{{ title or '' }}">
|
|
|
|
|
<div class="col-12">
|
|
|
|
|
<label for="title" class="form-label fw-semibold">Titel des Terminplans</label>
|
|
|
|
|
<input type="text" id="title" name="title" class="form-control form-control-lg" placeholder="z.B. Elternsprechtag Klasse 10a" value="{{ title or '' }}">
|
|
|
|
|
</div>
|
|
|
|
|
<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">
|
|
|
|
@@ -30,39 +32,49 @@
|
|
|
|
|
<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 class="form-text mb-0">Die Start-, End- und Pausenzeiten werden auf alle erzeugten Tage angewandt.</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>
|
|
|
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
|
|
|
<label for="default_day_start" class="form-label fw-semibold">Standard-Start</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 class="col-12 col-sm-6 col-lg-3">
|
|
|
|
|
<label for="default_day_end" class="form-label fw-semibold">Standard-Ende</label>
|
|
|
|
|
<input type="time" id="default_day_end" class="form-control" value="16:00">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
|
|
|
<label for="default_pause_start" class="form-label fw-semibold text-danger">Pause von</label>
|
|
|
|
|
<input type="time" id="default_pause_start" class="form-control border-danger-subtle" value="12:00">
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-12 col-sm-6 col-lg-3">
|
|
|
|
|
<label for="default_pause_end" class="form-label fw-semibold text-danger">Pause bis</label>
|
|
|
|
|
<input type="time" id="default_pause_end" class="form-control border-danger-subtle" value="12:45">
|
|
|
|
|
</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" ></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>
|
|
|
|
|
<label for="time_frame" class="form-label fw-semibold">Gespeichertes Zeitfenster (Übertragung ans Backend)</label>
|
|
|
|
|
<textarea id="time_frame" name="time_frame" class="form-control font-monospace" rows="5" placeholder="Wird automatisch aus den Tagen erzeugt" readonly></textarea>
|
|
|
|
|
<div class="form-text">Pausen teilen den Tag automatisch in zwei buchbare Blöcke, sodass die Pause sicher blockiert ist.</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">
|
|
|
|
|
<label for="slot_length" class="form-label fw-semibold">Slot-Länge in Minuten</label>
|
|
|
|
|
<input type="number" id="slot_length" name="slot_length" class="form-control form-control-lg" min="1" value="45">
|
|
|
|
|
</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" >
|
|
|
|
|
<label class="form-label fw-semibold text-primary">Automatisch berechnete Slots</label>
|
|
|
|
|
<div id="slots_amounts_display" class="form-control form-control-lg bg-primary-subtle text-primary fw-bold d-flex align-items-center">0</div>
|
|
|
|
|
<input type="hidden" id="slots_amounts" name="slots_amounts" value="0">
|
|
|
|
|
<div class="form-text">Wird aus der gebuchten Zeit (abzüglich Pausen) und der Slot-Länge berechnet.</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
@@ -76,7 +88,6 @@
|
|
|
|
|
<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>
|
|
|
|
|
|
|
|
|
|
{% endif %}
|
|
|
|
|
<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 %}>
|
|
|
|
@@ -96,7 +107,7 @@
|
|
|
|
|
<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 %}
|
|
|
|
|
{% if mail_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.
|
|
|
|
@@ -121,30 +132,31 @@
|
|
|
|
|
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');
|
|
|
|
|
const defaultPauseStartInput = document.getElementById('default_pause_start');
|
|
|
|
|
const defaultPauseEndInput = document.getElementById('default_pause_end');
|
|
|
|
|
|
|
|
|
|
const slotLengthInput = document.getElementById('slot_length');
|
|
|
|
|
const slotsAmountsInput = document.getElementById('slots_amounts');
|
|
|
|
|
const slotsAmountsDisplay = document.getElementById('slots_amounts_display'); // Neu: Anzeige-Element
|
|
|
|
|
|
|
|
|
|
if (!startDateInput || !endDateInput || !buildButton || !daysContainer || !timeFrameTextarea || !defaultStartInput || !defaultEndInput) {
|
|
|
|
|
if (!startDateInput || !endDateInput || !buildButton || !daysContainer || !timeFrameTextarea) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const weekdayFormatter = new Intl.DateTimeFormat('de-DE', {
|
|
|
|
|
weekday: 'long',
|
|
|
|
|
weekday: 'short',
|
|
|
|
|
day: '2-digit',
|
|
|
|
|
month: '2-digit',
|
|
|
|
|
year: 'numeric',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
function parseDate(value) {
|
|
|
|
|
if (!value) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!value) return null;
|
|
|
|
|
const parts = value.split('-').map(Number);
|
|
|
|
|
if (parts.length !== 3 || parts.some(Number.isNaN)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (parts.length !== 3 || parts.some(Number.isNaN)) return null;
|
|
|
|
|
return new Date(parts[0], parts[1] - 1, parts[2]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -165,16 +177,92 @@
|
|
|
|
|
return copy;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function timeToMinutes(timeStr) {
|
|
|
|
|
if (!timeStr) return 0;
|
|
|
|
|
const parts = timeStr.split(':').map(Number);
|
|
|
|
|
return parts[0] * 60 + parts[1];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateSlotsDisplay(totalSlots) {
|
|
|
|
|
if (slotsAmountsInput) slotsAmountsInput.value = totalSlots;
|
|
|
|
|
if (slotsAmountsDisplay) slotsAmountsDisplay.innerText = totalSlots + " Slots gesamt";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Berechnet die Slots basierend auf den konfigurierten Zeiten & Pausen
|
|
|
|
|
function calculateSlots() {
|
|
|
|
|
if (!slotLengthInput || !slotsAmountsInput) return;
|
|
|
|
|
|
|
|
|
|
const slotLength = parseInt(slotLengthInput.value, 10);
|
|
|
|
|
if (isNaN(slotLength) || slotLength <= 0) {
|
|
|
|
|
updateSlotsDisplay(0);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let totalSlots = 0;
|
|
|
|
|
const rows = Array.from(daysContainer.querySelectorAll('[data-day-row]'));
|
|
|
|
|
|
|
|
|
|
rows.forEach(row => {
|
|
|
|
|
const startTime = row.querySelector('[data-time-start]')?.value;
|
|
|
|
|
const endTime = row.querySelector('[data-time-end]')?.value;
|
|
|
|
|
const pauseStart = row.querySelector('[data-time-pause-start]')?.value;
|
|
|
|
|
const pauseEnd = row.querySelector('[data-time-pause-end]')?.value;
|
|
|
|
|
|
|
|
|
|
if (!startTime || !endTime) return;
|
|
|
|
|
|
|
|
|
|
const startMins = timeToMinutes(startTime);
|
|
|
|
|
const endMins = timeToMinutes(endTime);
|
|
|
|
|
const pauseStartMins = timeToMinutes(pauseStart);
|
|
|
|
|
const pauseEndMins = timeToMinutes(pauseEnd);
|
|
|
|
|
|
|
|
|
|
if (endMins <= startMins) return; // Ungültige Zeit
|
|
|
|
|
|
|
|
|
|
// Wenn eine gültige Pause innerhalb der Start/Endzeit existiert
|
|
|
|
|
if (pauseStartMins > 0 && pauseEndMins > 0 && pauseStartMins < pauseEndMins && pauseStartMins > startMins && pauseStartMins < endMins) {
|
|
|
|
|
// Segment 1 (Vor der Pause)
|
|
|
|
|
const segment1 = pauseStartMins - startMins;
|
|
|
|
|
totalSlots += Math.floor(segment1 / slotLength);
|
|
|
|
|
|
|
|
|
|
// Segment 2 (Nach der Pause)
|
|
|
|
|
const effectivePauseEnd = Math.min(pauseEndMins, endMins);
|
|
|
|
|
const segment2 = endMins - effectivePauseEnd;
|
|
|
|
|
if (segment2 > 0) {
|
|
|
|
|
totalSlots += Math.floor(segment2 / slotLength);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Ohne Pause oder ungültige Pause -> Komplett durchrechnen
|
|
|
|
|
totalSlots += Math.floor((endMins - startMins) / slotLength);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
updateSlotsDisplay(totalSlots);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Synchronisiert das Textfeld und fügt den Break-Cut hinzu
|
|
|
|
|
function syncTextarea() {
|
|
|
|
|
const rows = Array.from(daysContainer.querySelectorAll('[data-day-row]'));
|
|
|
|
|
const lines = rows.map(function (row) {
|
|
|
|
|
const lines = [];
|
|
|
|
|
|
|
|
|
|
rows.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 || '';
|
|
|
|
|
return `${dayValue} ${startTime}-${endTime}`.trim();
|
|
|
|
|
}).filter(Boolean);
|
|
|
|
|
const pauseStart = row.querySelector('[data-time-pause-start]')?.value || '';
|
|
|
|
|
const pauseEnd = row.querySelector('[data-time-pause-end]')?.value || '';
|
|
|
|
|
|
|
|
|
|
if (!dayValue || !startTime || !endTime) return;
|
|
|
|
|
|
|
|
|
|
// Hat der Nutzer eine Pause eingetragen, die innerhalb des Tagesfensters liegt?
|
|
|
|
|
if (pauseStart && pauseEnd && pauseStart < pauseEnd && pauseStart > startTime && pauseStart < endTime) {
|
|
|
|
|
// Zeile am Pausenbeginn aufsplitten -> 2 Blöcke = automatische Pause
|
|
|
|
|
lines.push(`${dayValue} ${startTime}-${pauseStart}`);
|
|
|
|
|
lines.push(`${dayValue} ${pauseEnd}-${endTime}`);
|
|
|
|
|
} else {
|
|
|
|
|
lines.push(`${dayValue} ${startTime}-${endTime}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
timeFrameTextarea.value = lines.join('\n');
|
|
|
|
|
calculateSlots(); // Nach jedem Update neu berechnen
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function renderRows() {
|
|
|
|
@@ -197,40 +285,57 @@
|
|
|
|
|
|
|
|
|
|
endDateInput.setCustomValidity('');
|
|
|
|
|
|
|
|
|
|
// Bisherige Werte sichern
|
|
|
|
|
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 || '';
|
|
|
|
|
const pauseStart = row.querySelector('[data-time-pause-start]')?.value || '';
|
|
|
|
|
const pauseEnd = row.querySelector('[data-time-pause-end]')?.value || '';
|
|
|
|
|
|
|
|
|
|
if (dayValue) {
|
|
|
|
|
existingValues.set(dayValue, { startTime, endTime });
|
|
|
|
|
existingValues.set(dayValue, { startTime, endTime, pauseStart, pauseEnd });
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const defaultStart = defaultStartInput.value || '08:00';
|
|
|
|
|
const defaultEnd = defaultEndInput.value || '12:00';
|
|
|
|
|
const defaultEnd = defaultEndInput.value || '16:00';
|
|
|
|
|
const defaultPauseStart = defaultPauseStartInput.value || '';
|
|
|
|
|
const defaultPauseEnd = defaultPauseEndInput.value || '';
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
const rowStart = preserved.startTime !== undefined ? preserved.startTime : defaultStart;
|
|
|
|
|
const rowEnd = preserved.endTime !== undefined ? preserved.endTime : defaultEnd;
|
|
|
|
|
const rowPauseStart = preserved.pauseStart !== undefined ? preserved.pauseStart : defaultPauseStart;
|
|
|
|
|
const rowPauseEnd = preserved.pauseEnd !== undefined ? preserved.pauseEnd : defaultPauseEnd;
|
|
|
|
|
|
|
|
|
|
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">
|
|
|
|
|
<div class="col-12 col-lg-4">
|
|
|
|
|
<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 class="col-6 col-lg-2">
|
|
|
|
|
<label class="form-label mb-1 text-muted small">Von</label>
|
|
|
|
|
<input type="time" class="form-control form-control-sm" 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 class="col-6 col-lg-2">
|
|
|
|
|
<label class="form-label mb-1 text-muted small">Bis</label>
|
|
|
|
|
<input type="time" class="form-control form-control-sm" value="${rowEnd}" data-time-end>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-lg-2">
|
|
|
|
|
<label class="form-label mb-1 text-danger small">Pause ab</label>
|
|
|
|
|
<input type="time" class="form-control form-control-sm border-danger-subtle" value="${rowPauseStart}" data-time-pause-start>
|
|
|
|
|
</div>
|
|
|
|
|
<div class="col-6 col-lg-2">
|
|
|
|
|
<label class="form-label mb-1 text-danger small">Pause bis</label>
|
|
|
|
|
<input type="time" class="form-control form-control-sm border-danger-subtle" value="${rowPauseEnd}" data-time-pause-end>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
@@ -246,17 +351,25 @@
|
|
|
|
|
syncTextarea();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Event Listeners Registration
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
defaultStartInput.addEventListener('change', renderRows);
|
|
|
|
|
defaultEndInput.addEventListener('change', renderRows);
|
|
|
|
|
defaultPauseStartInput.addEventListener('change', renderRows);
|
|
|
|
|
defaultPauseEndInput.addEventListener('change', renderRows);
|
|
|
|
|
|
|
|
|
|
// Slot Berechnung auf Ändeurng der Länge antriggern
|
|
|
|
|
if(slotLengthInput) {
|
|
|
|
|
slotLengthInput.addEventListener('input', calculateSlots);
|
|
|
|
|
slotLengthInput.addEventListener('change', calculateSlots);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (startDateInput.value && endDateInput.value) {
|
|
|
|
|
renderRows();
|
|
|
|
|
}
|
|
|
|
|
})();
|
|
|
|
|
</script>
|
|
|
|
|
{% endblock %}
|
|
|
|
|
{% endblock %}
|