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.
330 lines
12 KiB
330 lines
12 KiB
<h1 class="mb-4">Meine Einträge</h1>
|
|
|
|
<!-- 🔢 Zusammenfassung -->
|
|
<div class="container my-4 rounded border shadow-sm py-3 px-4">
|
|
<div class="row row-cols-1 row-cols-md-2 row-cols-lg-3 g-4">
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>🚗 Fahrtkosten (Kilometergeld)</h5>
|
|
<% @total_kilometer_costs_by_year.each do |year, sum| %>
|
|
<p><strong><%= year %>:</strong>
|
|
<%= number_to_currency(sum, unit: "€", separator: ",", delimiter: ".", precision: 2) %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>💶 Fortbildungskosten</h5>
|
|
<% @fortbildungskosten_by_year.each do |year, sum| %>
|
|
<p><strong><%= year %>:</strong>
|
|
<%= number_to_currency(sum, unit: "€", separator: ",", delimiter: ".", precision: 2) %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>🧠 Selbsterfahrungskosten</h5>
|
|
<% @selbsterfahrungskosten_by_year.each do |year, sum| %>
|
|
<p><strong><%= year %>:</strong>
|
|
<%= number_to_currency(sum, unit: "€", separator: ",", delimiter: ".", precision: 2) %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>👨🏫 Supervision</h5>
|
|
<% @selbstsupervision_by_year.each do |year, sum| %>
|
|
<p><strong><%= year %>:</strong>
|
|
<%= number_to_currency(sum, unit: "€", separator: ",", delimiter: ".", precision: 2) %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>📊 Gesamtkosten</h5>
|
|
<% @allekosten_by_year.each do |year, sum| %>
|
|
<p><strong><%= year %>:</strong>
|
|
<%= number_to_currency(sum, unit: "€", separator: ",", delimiter: ".", precision: 2) %>
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
|
|
<div class="col rounded border shadow-sm p-3">
|
|
<h5>🕒 Gesamtzeit</h5>
|
|
<p><strong>Total: <%= @total_minutes / 60 %>h <%= @total_minutes % 60 %> min</strong></p>
|
|
<p>Propädeutikum: <%= @total_minutes_praktikum_prop / 60 %>h <%= @total_minutes % 60 %> min</p>
|
|
<p>Fachspezifikum: <%= @total_minutes_praktikum_fach / 60 %>h <%= @total_minutes % 60 %> min</p>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<!-- 📊 Übersicht je Kombination -->
|
|
<div class="mb-4 rounded border shadow-sm p-3">
|
|
<h4>📊 Übersicht je Kombination</h4>
|
|
<div class="table-responsive">
|
|
<table class="table table-bordered table-sm table-striped">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Typ</th>
|
|
<th>Art</th>
|
|
<th>Verbleibend</th>
|
|
<th>Soll (h)</th>
|
|
<th>Wöchentlich</th>
|
|
<th>Vorauss. Ende</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<% ["propädeutikum", "fachspezifikum"].each do |typ| %>
|
|
<% ["Praktikum", "Selbsterfahrung", "Supervision"].each do |art| %>
|
|
<% remaining = @remaining_minutes_matrix.dig(typ, art) %>
|
|
<% soll = current_user.required_hours_matrix.dig(typ, art) %>
|
|
<% weekly = current_user.weekly_target_matrix.dig(typ, art) %>
|
|
<% ende = @estimated_end_by_typ_art.dig(typ, art) %>
|
|
|
|
<% if remaining.present? || soll.present? || weekly.present? %>
|
|
<tr>
|
|
<td><%= typ.capitalize %></td>
|
|
<td><%= art.capitalize %></td>
|
|
<td><%= remaining ? "#{remaining / 60} h #{remaining % 60} min" : "—" %></td>
|
|
<td><%= soll || "—" %> h</td>
|
|
<td><%= weekly || "—" %> h/Woche</td>
|
|
<td><%= ende.present? ? ende.strftime("%d.%m.%Y") : "—" %></td>
|
|
</tr>
|
|
<% end %>
|
|
<% end %>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="container my-4 rounded border shadow-sm p-3">
|
|
<h4 class="mb-4">⏱ Timer</h4>
|
|
<% if @running_entry.present? %>
|
|
<div class="alert alert-info d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<strong>Laufender Eintrag:</strong>
|
|
<%= @running_entry.praktikums_typ %> – <%= @running_entry.entry_art %><br>
|
|
Gestartet: <%= l(@running_entry.start_time, format: :short) %><br>
|
|
Dauer: <span id="live-timer">Berechne …</span>
|
|
</div>
|
|
<div>
|
|
<%= form_with url: stop_timer_entry_path(@running_entry), method: :post, local: true do |f| %>
|
|
<div class="form-check mb-2">
|
|
<%= f.check_box :lunch_break, class: 'form-check-input', id: 'lunch_break' %>
|
|
<%= f.label :lunch_break, '30 Min Mittagspause abziehen', class: 'form-check-label' %>
|
|
</div>
|
|
<%= f.submit '⏹️ Stoppen', class: "btn btn-danger" %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="mb-4 row">
|
|
|
|
|
|
<%= form_with url: start_timer_entries_path, method: :post, local: true do %>
|
|
<div class="row g-2 align-items-end">
|
|
<div class="col-md-4">
|
|
<%= label_tag :typ, 'Typ' %>
|
|
<%= select_tag :typ,
|
|
options_for_select(
|
|
current_user.praepedeutikum_abgeschlossen? ?
|
|
Entry::PRAKTIKUMSTYPEN.reject { |typ| typ == 'propädeutikum' } :
|
|
Entry::PRAKTIKUMSTYPEN
|
|
),
|
|
class: "form-select" %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= label_tag :art, 'Art' %>
|
|
<%= select_tag :art, options_for_select(Entry::ENTRY_ARTEN), class: "form-select" %>
|
|
</div>
|
|
|
|
<div class="col-md-4">
|
|
<%= submit_tag '▶️ Start', class: "btn btn-success w-100", disabled: @running_entry.present? %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<%= link_to '➕ Neuer Eintrag', new_entry_path, class: 'btn btn-outline-primary mt-3 w-100 w-md-auto mb-3' %>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 📋 Tabelle aller Einträge -->
|
|
<div class="mb-4 rounded border shadow-sm p-3">
|
|
<h4 class="mb-3">📋 Einträge</h4>
|
|
|
|
<div class="mb-3">
|
|
<input type="text" id="entryFilter" class="form-control" placeholder="🔍 Filter nach Datum, Typ, Art …">
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-striped table-hover table-bordered" id="entriesTable">
|
|
<thead>
|
|
<tr>
|
|
<th data-sort-index="0" class="sortable">Datum</th>
|
|
<th data-sort-index="1" class="sortable">Zeit</th>
|
|
<th data-sort-index="2" class="sortable">Typ</th>
|
|
<th data-sort-index="3" class="sortable">Art</th>
|
|
<th data-sort-index="4" class="sortable">Beschreibung</th>
|
|
<th data-sort-index="5" class="sortable">Kilometer</th>
|
|
<th data-sort-index="6" class="sortable">Pauschale</th>
|
|
<th data-sort-index="7" class="sortable">Kosten</th>
|
|
<th data-sort-index="8" class="sortable">Zählt als Fortbildung</th>
|
|
<th>Aktionen</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<% @entries.each do |entry| %>
|
|
<tr>
|
|
<td><%= entry.date.strftime('%d.%m.%Y') %></td>
|
|
<td><%= entry.hours.to_i %>h <%= entry.minutes.to_i %>min</td>
|
|
<td>
|
|
<%= ["Fortbildung", "Semesterkosten"].include?(entry.entry_art) ? entry.entry_art : entry.praktikums_typ.capitalize %>
|
|
</td>
|
|
<td>
|
|
<%= ["Fortbildung", "Semesterkosten"].include?(entry.entry_art) ? entry.beschreibung : entry.entry_art.capitalize %>
|
|
</td>
|
|
<td>
|
|
<%= entry.beschreibung %>
|
|
</td>
|
|
|
|
<td><%= entry.distance_km.to_f %> km</td>
|
|
|
|
<td>
|
|
<%= number_to_currency(entry.kilometer_pauschale, unit: "€", separator: ",", delimiter: ".") %>
|
|
</td>
|
|
|
|
<td>
|
|
<%= number_to_currency(entry.kosten, unit: "€", separator: ",", delimiter: ".") %>
|
|
</td>
|
|
<td class="text-center">
|
|
<%= check_box_tag "fortbildung_#{entry.id}", '1', entry.zaehlt_als_fortbildung, disabled: true %>
|
|
</td>
|
|
<td class="text-end">
|
|
<div class="d-flex justify-content-between">
|
|
<%= link_to 'Bearbeiten', edit_entry_path(entry), class: 'btn btn-sm btn-outline-primary' %>
|
|
<%= link_to 'Löschen', entry_path(entry), class: 'btn btn-sm btn-outline-danger open-delete-modal' %>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<%= link_to "Export als CSV", export_csv_entries_path(format: :csv), class: "btn btn-outline-secondary mt-3 w-100 w-md-auto" %>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- JS für Modal -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
document.querySelectorAll('.open-delete-modal').forEach(button => {
|
|
button.addEventListener('click', function (e) {
|
|
e.preventDefault();
|
|
const targetUrl = this.getAttribute('href');
|
|
document.getElementById('modal-delete-form').setAttribute('action', targetUrl);
|
|
const modal = new bootstrap.Modal(document.getElementById('deleteConfirmModal'));
|
|
modal.show();
|
|
});
|
|
});
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const filterInput = document.getElementById("entryFilter");
|
|
const table = document.getElementById("entriesTable");
|
|
const tableRows = table?.querySelectorAll("tbody tr");
|
|
|
|
if (filterInput && table && tableRows) {
|
|
filterInput.addEventListener("keyup", function () {
|
|
const query = this.value.toLowerCase();
|
|
|
|
tableRows.forEach(row => {
|
|
const text = row.textContent.toLowerCase();
|
|
row.style.display = text.includes(query) ? "" : "none";
|
|
});
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
const table = document.getElementById("entriesTable");
|
|
const headers = table.querySelectorAll("th.sortable");
|
|
let sortDirection = 1;
|
|
|
|
headers.forEach((header, index) => {
|
|
header.style.cursor = 'pointer';
|
|
header.addEventListener("click", () => {
|
|
const tbody = table.querySelector("tbody");
|
|
const rows = Array.from(tbody.querySelectorAll("tr"));
|
|
|
|
const isNumeric = header.dataset.sort === 'km' || header.dataset.sort === 'pauschale';
|
|
const isDate = header.dataset.sort === 'date';
|
|
|
|
rows.sort((a, b) => {
|
|
const cellA = a.children[index].textContent.trim();
|
|
const cellB = b.children[index].textContent.trim();
|
|
|
|
if (isDate) {
|
|
return sortDirection * (new Date(cellA.split('.').reverse().join('-')) - new Date(cellB.split('.').reverse().join('-')));
|
|
}
|
|
|
|
if (isNumeric) {
|
|
const numA = parseFloat(cellA.replace(/[^\d,.-]/g, "").replace(",", "."));
|
|
const numB = parseFloat(cellB.replace(/[^\d,.-]/g, "").replace(",", "."));
|
|
return sortDirection * (numA - numB);
|
|
}
|
|
|
|
return sortDirection * cellA.localeCompare(cellB);
|
|
});
|
|
|
|
rows.forEach(row => tbody.appendChild(row));
|
|
sortDirection *= -1; // Toggle asc/desc
|
|
});
|
|
});
|
|
});
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const timerElement = document.getElementById('live-timer');
|
|
if (!timerElement) return;
|
|
|
|
<% if @running_entry&.start_time.present? %>
|
|
const startedAt = new Date("<%= @running_entry.start_time.iso8601 %>");
|
|
<% end %>
|
|
|
|
if (!startedAt) return;
|
|
const updateTimer = () => {
|
|
const now = new Date();
|
|
const diffMs = now - startedAt;
|
|
const totalMinutes = Math.floor(diffMs / 60000);
|
|
const hours = Math.floor(totalMinutes / 60);
|
|
const minutes = totalMinutes % 60;
|
|
timerElement.textContent = `${hours}h ${minutes}min (${totalMinutes}min)`;
|
|
};
|
|
|
|
updateTimer();
|
|
setInterval(updateTimer, 60000); // alle Minute aktualisieren
|
|
});
|
|
</script>
|