Some smal fixes for the Terminplaner
This commit is contained in:
@@ -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,48 @@
|
||||
<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" 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 for="slots_amounts" class="form-label fw-semibold text-primary">Automatisch berechnete Slots</label>
|
||||
<input type="number" id="slots_amounts" name="slots_amounts" class="form-control bg-light text-primary fw-bold" min="0" value="0" readonly>
|
||||
<div class="form-text">Wird aus der gebuchten Zeit (abzüglich Pausen) und der Slot-Länge berechnet.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +87,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 +106,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 +131,30 @@
|
||||
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');
|
||||
|
||||
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 +175,87 @@
|
||||
return copy;
|
||||
}
|
||||
|
||||
function timeToMinutes(timeStr) {
|
||||
if (!timeStr) return 0;
|
||||
const parts = timeStr.split(':').map(Number);
|
||||
return parts[0] * 60 + parts[1];
|
||||
}
|
||||
|
||||
// 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) {
|
||||
slotsAmountsInput.value = 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);
|
||||
}
|
||||
});
|
||||
|
||||
slotsAmountsInput.value = 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 +278,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 +344,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 %}
|
||||
Reference in New Issue
Block a user