You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
258 lines
10 KiB
258 lines
10 KiB
<!-- Zeiteingabe-Karte -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="card-title">⏱️ Zeitberechnung</h5>
|
|
|
|
<div class="mb-3">
|
|
<label for="minute-inputs" class="form-label">Minuteneinträge (z. B. 45, 60, 30):</label>
|
|
<textarea id="minute-inputs" class="form-control" rows="2" placeholder="Trenne mit Komma, Leertaste oder Zeilenumbruch"></textarea>
|
|
</div>
|
|
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
|
<button id="sum-button" class="btn btn-outline-primary">➕ Summieren</button>
|
|
<p class="mb-0 mt-2 mt-md-0">
|
|
<strong>Gesamt:</strong> <span id="total-time">0h 0min</span> (<span id="total-minutes">0min</span>)
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rails-Formular als Karte -->
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<h5 class="card-title">📋 Eintrag erstellen</h5>
|
|
|
|
<%= form_with(model: entry, local: true) do |form| %>
|
|
<% if entry.errors.any? %>
|
|
<div class="alert alert-danger">
|
|
<h5><%= pluralize(entry.errors.count, "Fehler") %> verhinderten das Speichern:</h5>
|
|
<ul>
|
|
<% entry.errors.full_messages.each do |msg| %>
|
|
<li><%= msg %></li>
|
|
<% end %>
|
|
</ul>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="row g-3">
|
|
<div class="col-md-4">
|
|
<%= form.label :date, '📅 Datum', class: 'form-label' %>
|
|
<%= form.date_field :date, class: 'form-control flatpickr', data: {
|
|
enable_time: false, altInput: true,
|
|
altFormat: 'd.m.Y', dateFormat: 'Y-m-d'
|
|
}, value: form.object.date %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= form.label :start_time, '🕒 Beginn', class: 'form-label' %>
|
|
<%= form.time_field :start_time, class: 'form-control', step: 60,
|
|
value: @entry.start_time&.strftime("%H:%M") %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= form.label :end_time, '🕓 Ende', class: 'form-label' %>
|
|
<%= form.time_field :end_time, class: 'form-control', step: 60,
|
|
value: @entry.end_time&.strftime("%H:%M") %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= form.label :hours, '⏳ Stunden', class: 'form-label' %>
|
|
<%= form.number_field :hours, class: 'form-control', min: 0,
|
|
value: form.object.hours || 0 %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= form.label :minutes, '⏱ Minuten', class: 'form-label' %>
|
|
<%= form.number_field :minutes, class: 'form-control', min: 0, max: 59,
|
|
value: form.object.minutes || 0 %>
|
|
</div>
|
|
|
|
<div class="col-md-4 d-flex align-items-end">
|
|
<div class="form-check">
|
|
<%= form.check_box :has_break, { class: 'form-check-input', id: 'entry_has_break' }, true, false %>
|
|
<%= form.label :has_break, '🧘 30 Min. Mittagspause abziehen', class: 'form-check-label ms-2' %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<p><strong>🧮 Berechnet:</strong> <span id="calculated-total-minutes">0 min</span></p>
|
|
</div>
|
|
|
|
<hr>
|
|
|
|
<div class="row g-3 mt-2">
|
|
<div class="col-md-6">
|
|
<%= form.label :praktikums_typ, '🔧 Praktikumstyp', class: 'form-label' %>
|
|
<%= form.select :praktikums_typ,
|
|
current_user.praepedeutikum_abgeschlossen? ?
|
|
Entry::PRAKTIKUMSTYPEN.reject { |typ| typ == 'propädeutikum' } :
|
|
Entry::PRAKTIKUMSTYPEN,
|
|
{}, class: 'form-select' %>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<%= form.label :entry_art, '📚 Art', class: 'form-label' %>
|
|
<%= form.select :entry_art, Entry::ENTRY_ARTEN, {}, class: 'form-select', id: 'entry_art_select' %>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<%= form.label :distance_km, '🚗 Entfernung (km)', class: 'form-label' %>
|
|
<%= form.number_field :distance_km, class: 'form-control', min: 0, value: form.object.distance_km || 0 %>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<%= form.label :beschreibung, '📝 Beschreibung', class: 'form-label' %>
|
|
<%= form.text_field :beschreibung, class: 'form-control' %>
|
|
</div>
|
|
|
|
<div class="col-md-6">
|
|
<%= form.label :kosten, '💸 Kosten', class: 'form-label' %>
|
|
<%= form.number_field :kosten, class: 'form-control', min: 0, step: 0.01 %>
|
|
</div>
|
|
|
|
<div class="col-md-6 d-flex align-items-end">
|
|
<div class="form-check">
|
|
<%= form.check_box :zaehlt_als_fortbildung, class: "form-check-input" %>
|
|
<%= form.label :zaehlt_als_fortbildung, "🎓 Zählt als Fortbildung", class: "form-check-label" %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-4 text-end">
|
|
<%= form.submit '💾 Speichern', class: 'btn btn-success' %>
|
|
<%= link_to '🔙 Zurück', entries_path, class: 'btn btn-outline-secondary ms-2' %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const entryArtSelect = document.getElementById("entry_art_select");
|
|
const fortbildungFields = document.getElementById("fortbildung-fields");
|
|
|
|
if (entryArtSelect && fortbildungFields) {
|
|
entryArtSelect.addEventListener("change", function () {
|
|
if (entryArtSelect.value === "Fortbildung") {
|
|
fortbildungFields.style.display = "block";
|
|
} else {
|
|
fortbildungFields.style.display = "none";
|
|
}
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const textarea = document.getElementById("minute-inputs");
|
|
const totalSpan = document.getElementById("total-time");
|
|
const totalMinutesSpan = document.getElementById("total-minutes");
|
|
const totalMinutesDisplay = document.getElementById("calculated-total-minutes");
|
|
const sumBtn = document.getElementById("sum-button");
|
|
|
|
const startInput = document.getElementById("entry_start_time");
|
|
const endInput = document.getElementById("entry_end_time");
|
|
const hourInput = document.getElementById("entry_hours");
|
|
const minuteInput = document.getElementById("entry_minutes");
|
|
const breakCheckbox = document.getElementById("entry_has_break");
|
|
|
|
let baseTotalMinutes = 0; // <- diese merken wir uns immer ohne Pause-Abzug!
|
|
|
|
function updateDisplayFromBaseMinutes() {
|
|
let total = baseTotalMinutes;
|
|
if (breakCheckbox && breakCheckbox.checked) total = Math.max(0, total - 30);
|
|
|
|
const h = Math.floor(total / 60);
|
|
const m = total % 60;
|
|
|
|
hourInput.value = h;
|
|
minuteInput.value = m;
|
|
|
|
totalSpan.textContent = `${h}h ${m}min`;
|
|
totalMinutesSpan.textContent = `${total} min`;
|
|
totalMinutesDisplay.textContent = `${total} min`;
|
|
}
|
|
|
|
function clearFields(exclude = null) {
|
|
if (exclude !== 'textarea') textarea.value = '';
|
|
if (exclude !== 'time') {
|
|
startInput.value = '';
|
|
endInput.value = '';
|
|
}
|
|
}
|
|
|
|
sumBtn.addEventListener("click", function () {
|
|
const values = textarea.value.split(/[ ,;\n]+/).map(v => parseInt(v.trim())).filter(Number.isFinite);
|
|
baseTotalMinutes = values.reduce((sum, val) => sum + val, 0);
|
|
updateDisplayFromBaseMinutes();
|
|
clearFields('textarea');
|
|
});
|
|
|
|
function calculateFromTimes() {
|
|
const start = startInput.value;
|
|
const end = endInput.value;
|
|
|
|
if (start && end) {
|
|
const [sh, sm] = start.split(':').map(Number);
|
|
const [eh, em] = end.split(':').map(Number);
|
|
|
|
let startDate = new Date(0, 0, 0, sh, sm);
|
|
let endDate = new Date(0, 0, 0, eh, em);
|
|
|
|
let diffMin = (endDate - startDate) / 60000;
|
|
if (diffMin < 0) diffMin += 1440;
|
|
|
|
baseTotalMinutes = diffMin;
|
|
updateDisplayFromBaseMinutes();
|
|
clearFields('time');
|
|
}
|
|
}
|
|
|
|
function normalizeManualMinutes() {
|
|
let minutes = parseInt(minuteInput.value, 10);
|
|
if (!isNaN(minutes) && minutes >= 60) {
|
|
const extra = Math.floor(minutes / 60);
|
|
const remain = minutes % 60;
|
|
const currentHours = parseInt(hourInput.value, 10) || 0;
|
|
hourInput.value = currentHours + extra;
|
|
minuteInput.value = remain;
|
|
}
|
|
baseTotalMinutes = (parseInt(hourInput.value, 10) || 0) * 60 + (parseInt(minuteInput.value, 10) || 0);
|
|
updateDisplayFromBaseMinutes();
|
|
clearFields(); // alle Quellen löschen
|
|
}
|
|
|
|
function calculateEndTimeFromStart() {
|
|
if (!startInput.value || baseTotalMinutes === 0) return;
|
|
|
|
const [sh, sm] = startInput.value.split(":").map(Number);
|
|
const startDate = new Date(2000, 0, 1, sh, sm);
|
|
|
|
let minutesToAdd = baseTotalMinutes;
|
|
if (breakCheckbox && breakCheckbox.checked) minutesToAdd = Math.max(0, baseTotalMinutes - 30);
|
|
|
|
const endDate = new Date(startDate.getTime() + minutesToAdd * 60000);
|
|
const hh = String(endDate.getHours()).padStart(2, '0');
|
|
const mm = String(endDate.getMinutes()).padStart(2, '0');
|
|
|
|
endInput.value = `${hh}:${mm}`;
|
|
}
|
|
|
|
startInput.addEventListener("change", function () {
|
|
calculateEndTimeFromStart();
|
|
});
|
|
|
|
// Listeners
|
|
if (startInput && endInput) {
|
|
startInput.addEventListener("change", calculateFromTimes);
|
|
endInput.addEventListener("change", calculateFromTimes);
|
|
}
|
|
|
|
if (hourInput) hourInput.addEventListener("input", normalizeManualMinutes);
|
|
if (minuteInput) minuteInput.addEventListener("blur", normalizeManualMinutes);
|
|
|
|
if (breakCheckbox) {
|
|
breakCheckbox.addEventListener("change", updateDisplayFromBaseMinutes);
|
|
}
|
|
});
|
|
</script>
|