|
|
|
@ -30,7 +30,7 @@ |
|
|
|
id="showAllCombinations" |
|
|
|
> |
|
|
|
<label class="form-check-label" for="showAllCombinations"> |
|
|
|
Alle anzeigen |
|
|
|
Erledigte anzeigen |
|
|
|
</label> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -58,82 +58,69 @@ |
|
|
|
<% Entry::PRAKTIKUMSTYPEN.each do |typ| %> |
|
|
|
<% Entry::ENTRY_ARTEN.each do |art| %> |
|
|
|
|
|
|
|
<% remaining = @remaining_minutes_matrix.dig(typ, art) %> |
|
|
|
<% spent = @spent_minutes_matrix.dig(typ, art) %> |
|
|
|
<% soll = current_user.required_hours_matrix.dig(typ, art) %> |
|
|
|
<% weekly = current_user.weekly_target_matrix.dig(typ, art) %> |
|
|
|
<% remaining = @remaining_minutes_matrix.dig(typ, art).to_i %> |
|
|
|
<% spent = @spent_minutes_matrix.dig(typ, art).to_i %> |
|
|
|
<% soll = current_user.required_hours_for(typ, art).to_f %> |
|
|
|
<% weekly = current_user.weekly_target_for(typ, art).to_f %> |
|
|
|
<% ende = @estimated_end_by_typ_art.dig(typ, art) %> |
|
|
|
|
|
|
|
<% next if spent.blank? %> |
|
|
|
<% next if soll.blank? %> |
|
|
|
|
|
|
|
<% |
|
|
|
# Standardmäßig zeigen wir nur Einträge, |
|
|
|
# Ohne Sollwert und ohne geleistete Zeit ist die Kombination |
|
|
|
# für die Übersicht nicht relevant. |
|
|
|
next if soll.zero? && spent.zero? |
|
|
|
|
|
|
|
# Standardmäßig nur Anforderungen anzeigen, |
|
|
|
# bei denen tatsächlich noch etwas offen ist. |
|
|
|
has_remaining = remaining.to_i.positive? |
|
|
|
has_remaining = remaining.positive? |
|
|
|
%> |
|
|
|
|
|
|
|
<% if remaining.present? || soll.present? || weekly.present? %> |
|
|
|
<tr |
|
|
|
data-has-remaining="<%= has_remaining %>" |
|
|
|
class="<%= 'd-none' unless has_remaining %>" |
|
|
|
> |
|
|
|
<td><%= typ.capitalize %></td> |
|
|
|
|
|
|
|
<td><%= art.capitalize %></td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% if remaining.present? %> |
|
|
|
<%= remaining / 60 %> h <%= remaining % 60 %> min |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% if spent.present? %> |
|
|
|
<%= spent / 60 %> h <%= spent % 60 %> min |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% if soll.present? %> |
|
|
|
<%= soll %> h |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% if weekly.present? %> |
|
|
|
<%= weekly %> h/Woche |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% actual = @actual_hours_per_week[[typ, art]] %> |
|
|
|
|
|
|
|
<% if actual.present? %> |
|
|
|
<%= actual %> h/Woche |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= ende.present? ? ende.strftime("%d.%m.%Y") : "—" %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% percent = @completion_percent_by_typ_art[[typ, art]] %> |
|
|
|
<%= percent.present? ? "#{percent} %" : "—" %> |
|
|
|
</td> |
|
|
|
</tr> |
|
|
|
<% end %> |
|
|
|
<tr |
|
|
|
data-has-remaining="<%= has_remaining %>" |
|
|
|
class="<%= 'd-none' unless has_remaining %>" |
|
|
|
> |
|
|
|
<td><%= typ.capitalize %></td> |
|
|
|
|
|
|
|
<td><%= art %></td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= remaining / 60 %> h <%= remaining % 60 %> min |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= spent / 60 %> h <%= spent % 60 %> min |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= number_with_precision(soll, precision: 2, strip_insignificant_zeros: true) %> h |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% if weekly.positive? %> |
|
|
|
<%= number_with_precision(weekly, precision: 2, strip_insignificant_zeros: true) %> h/Woche |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% actual = @actual_hours_per_week[[typ, art]] %> |
|
|
|
|
|
|
|
<% if actual.present? %> |
|
|
|
<%= actual %> h/Woche |
|
|
|
<% else %> |
|
|
|
— |
|
|
|
<% end %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= ende.present? ? ende.strftime("%d.%m.%Y") : "—" %> |
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<% percent = @completion_percent_by_typ_art[[typ, art]] %> |
|
|
|
<%= percent.nil? ? "—" : "#{percent} %" %> |
|
|
|
</td> |
|
|
|
</tr> |
|
|
|
|
|
|
|
<% end %> |
|
|
|
<% end %> |
|
|
|
@ -250,6 +237,7 @@ |
|
|
|
<p><strong>Total: <%= @total_minutes / 60 %>h <%= @total_minutes % 60 %> min</strong></p> |
|
|
|
<p>Propädeutikum: <%= @total_minutes_praktikum_prop / 60 %>h <%= @total_minutes_praktikum_prop % 60 %> min</p> |
|
|
|
<p>Fachspezifikum: <%= @total_minutes_praktikum_fach / 60 %>h <%= @total_minutes_praktikum_fach % 60 %> min</p> |
|
|
|
<p>Mediation: <%= @total_minutes_praktikum_mediation / 60 %>h <%= @total_minutes_praktikum_mediation % 60 %> min</p> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -404,12 +392,12 @@ |
|
|
|
|
|
|
|
<td> |
|
|
|
|
|
|
|
<%= entry.praktikums_typ&.capitalize%> |
|
|
|
<%= entry.praktikums_typ&.capitalize%> |
|
|
|
|
|
|
|
</td> |
|
|
|
|
|
|
|
<td> |
|
|
|
<%= entry.entry_art&.capitalize %> |
|
|
|
<%= entry.entry_art&.capitalize %> |
|
|
|
|
|
|
|
</td> |
|
|
|
|
|
|
|
@ -452,9 +440,9 @@ |
|
|
|
</div> |
|
|
|
|
|
|
|
<div |
|
|
|
id="entriesCount" |
|
|
|
class="small text-muted mt-2" |
|
|
|
aria-live="polite" |
|
|
|
id="entriesCount" |
|
|
|
class="small text-muted mt-2" |
|
|
|
aria-live="polite" |
|
|
|
> |
|
|
|
Aktuell angezeigt: 0 · Gesamt: 0 |
|
|
|
</div> |
|
|
|
@ -495,12 +483,12 @@ |
|
|
|
const entriesCount = document.getElementById("entriesCount"); |
|
|
|
|
|
|
|
if ( |
|
|
|
!tbody || |
|
|
|
!textFilter || |
|
|
|
!minYearFilter || |
|
|
|
!maxYearFilter || |
|
|
|
!columnToggleMenu || |
|
|
|
!entriesCount |
|
|
|
!tbody || |
|
|
|
!textFilter || |
|
|
|
!minYearFilter || |
|
|
|
!maxYearFilter || |
|
|
|
!columnToggleMenu || |
|
|
|
!entriesCount |
|
|
|
) return; |
|
|
|
|
|
|
|
const headers = Array.from(table.querySelectorAll("thead th")); |
|
|
|
@ -633,34 +621,34 @@ |
|
|
|
}).length; |
|
|
|
|
|
|
|
entriesCount.textContent = |
|
|
|
`Aktuell angezeigt: ${visibleRows} · Gesamt: ${rows.length}`; |
|
|
|
`Aktuell angezeigt: ${visibleRows} · Gesamt: ${rows.length}`; |
|
|
|
}; |
|
|
|
|
|
|
|
const applyFilters = () => { |
|
|
|
const query = textFilter.value.trim().toLowerCase(); |
|
|
|
const minYear = minYearFilter.value |
|
|
|
? Number(minYearFilter.value) |
|
|
|
: null; |
|
|
|
? Number(minYearFilter.value) |
|
|
|
: null; |
|
|
|
const maxYear = maxYearFilter.value |
|
|
|
? Number(maxYearFilter.value) |
|
|
|
: null; |
|
|
|
? Number(maxYearFilter.value) |
|
|
|
: null; |
|
|
|
|
|
|
|
getRows().forEach(row => { |
|
|
|
const rowYear = Number(row.dataset.year); |
|
|
|
|
|
|
|
const matchesQuery = |
|
|
|
!query || getVisibleRowText(row).includes(query); |
|
|
|
!query || getVisibleRowText(row).includes(query); |
|
|
|
|
|
|
|
const matchesMinYear = |
|
|
|
minYear === null || rowYear >= minYear; |
|
|
|
minYear === null || rowYear >= minYear; |
|
|
|
|
|
|
|
const matchesMaxYear = |
|
|
|
maxYear === null || rowYear <= maxYear; |
|
|
|
maxYear === null || rowYear <= maxYear; |
|
|
|
|
|
|
|
row.style.display = |
|
|
|
matchesQuery && matchesMinYear && matchesMaxYear |
|
|
|
? "" |
|
|
|
: "none"; |
|
|
|
matchesQuery && matchesMinYear && matchesMaxYear |
|
|
|
? "" |
|
|
|
: "none"; |
|
|
|
}); |
|
|
|
|
|
|
|
updateEntriesCount(); |
|
|
|
@ -775,52 +763,19 @@ |
|
|
|
})(); |
|
|
|
</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 %>"); |
|
|
|
const startedAt = |
|
|
|
<% if @running_entry&.start_time.present? %> |
|
|
|
new Date("<%= @running_entry.start_time.iso8601 %>"); |
|
|
|
<% else %> |
|
|
|
null; |
|
|
|
<% end %> |
|
|
|
|
|
|
|
if (!startedAt) return; |
|
|
|
|
|
|
|
const updateTimer = () => { |
|
|
|
const now = new Date(); |
|
|
|
const diffMs = now - startedAt; |
|
|
|
@ -842,9 +797,6 @@ |
|
|
|
const table = document.getElementById("combinationOverviewTable"); |
|
|
|
|
|
|
|
if (!checkbox || !table) return; |
|
|
|
if (checkbox.dataset.initialized === "true") return; |
|
|
|
|
|
|
|
checkbox.dataset.initialized = "true"; |
|
|
|
|
|
|
|
const updateVisibility = () => { |
|
|
|
const showAll = checkbox.checked; |
|
|
|
@ -862,7 +814,9 @@ |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
checkbox.addEventListener("change", updateVisibility); |
|
|
|
// Assignment statt addEventListener verhindert doppelte Handler |
|
|
|
// und funktioniert auch nach einem Turbo-Cache/Restore sauber. |
|
|
|
checkbox.onchange = updateVisibility; |
|
|
|
|
|
|
|
updateVisibility(); |
|
|
|
}; |
|
|
|
|