Browse Source

add filter query

main
Christoph Marzell 2 weeks ago
parent
commit
f9df450433
  1. 360
      app/views/entries/index.html.erb

360
app/views/entries/index.html.erb

@ -249,62 +249,107 @@
<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 class="row g-3 mb-3 align-items-end">
<div class="col-12 col-lg-4">
<label for="entryFilter" class="form-label">Suche</label>
<input type="text" id="entryFilter" class="form-control" placeholder="🔍 Filter nach Datum, Typ, Art …">
</div>
<div class="col-6 col-md-3 col-lg-2">
<label for="minYearFilter" class="form-label">Min. Jahr</label>
<select id="minYearFilter" class="form-select">
<option value="">Alle</option>
</select>
</div>
<div class="col-6 col-md-3 col-lg-2">
<label for="maxYearFilter" class="form-label">Max. Jahr</label>
<select id="maxYearFilter" class="form-select">
<option value="">Alle</option>
</select>
</div>
<div class="col-12 col-md-6 col-lg-4">
<label class="form-label d-block">Spalten</label>
<div class="dropdown" data-bs-auto-close="outside">
<button
class="btn btn-outline-secondary dropdown-toggle w-100 text-start"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Spalten ein-/ausblenden
</button>
<div class="dropdown-menu p-3 shadow-sm" id="columnToggleMenu" style="min-width: 320px;">
<!-- wird per JS befüllt -->
</div>
</div>
</div>
</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>
<th data-sort-index="0" data-sort="date" class="sortable" data-column-key="datum">Datum</th>
<th data-sort-index="1" data-sort="numeric" class="sortable" data-column-key="zeit">Zeit</th>
<th data-sort-index="2" data-sort="text" class="sortable" data-column-key="typ">Typ</th>
<th data-sort-index="3" data-sort="text" class="sortable" data-column-key="art">Art</th>
<th data-sort-index="4" data-sort="text" class="sortable" data-column-key="beschreibung">Beschreibung</th>
<th data-sort-index="5" data-sort="numeric" class="sortable" data-column-key="kilometer">Kilometer</th>
<th data-sort-index="6" data-sort="numeric" class="sortable" data-column-key="pauschale">Pauschale</th>
<th data-sort-index="7" data-sort="numeric" class="sortable" data-column-key="kosten">Kosten</th>
<th data-sort-index="8" data-sort="boolean" class="sortable" data-column-key="fortbildung">Zählt als Fortbildung</th>
<th data-column-key="aktionen">Aktionen</th>
</tr>
</thead>
<tbody>
<% @entries.each do |entry| %>
<tr>
<td>
<% duration_in_minutes = entry.hours.to_i * 60 + entry.minutes.to_i %>
<tr data-year="<%= entry.date.year %>">
<td data-sort-value="<%= entry.date.iso8601 %>">
<%= entry.date.strftime('%d.%m.%Y') %>
</td>
<td>
<% if entry.hours.to_i > 0 || entry.minutes.to_i > 0 %>
<td data-sort-value="<%= duration_in_minutes %>">
<% if duration_in_minutes > 0 %>
<%= formatted_duration(entry) %>
<% else %>
<% end %>
</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>
<td data-sort-value="<%= entry.distance_km.to_f %>">
<%= entry.distance_km.to_f %> km
</td>
<td>
<td data-sort-value="<%= entry.kilometer_pauschale.to_f %>">
<%= number_to_currency(entry.kilometer_pauschale, unit: "€", separator: ",", delimiter: ".") %>
</td>
<td>
<td data-sort-value="<%= entry.kosten.to_f %>">
<%= number_to_currency(entry.kosten, unit: "€", separator: ",", delimiter: ".") %>
</td>
<td class="text-center">
<td class="text-center" data-sort-value="<%= entry.zaehlt_als_fortbildung ? 1 : 0 %>">
<%= 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">
<div class="d-flex justify-content-between gap-2">
<%= 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>
@ -314,10 +359,9 @@
</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 -->
@ -337,21 +381,265 @@
</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();
console.log(query);
tableRows.forEach(row => {
const text = row.textContent.toLowerCase();
row.style.display = text.includes(query) ? "" : "none";
(() => {
const initEntriesTable = () => {
const table = document.getElementById("entriesTable");
if (!table || table.dataset.enhanced === "true") return;
table.dataset.enhanced = "true";
const tbody = table.querySelector("tbody");
const textFilter = document.getElementById("entryFilter");
const minYearFilter = document.getElementById("minYearFilter");
const maxYearFilter = document.getElementById("maxYearFilter");
const columnToggleMenu = document.getElementById("columnToggleMenu");
if (!tbody || !textFilter || !minYearFilter || !maxYearFilter || !columnToggleMenu) return;
const headers = Array.from(table.querySelectorAll("thead th"));
const sortableHeaders = headers.filter(header => header.classList.contains("sortable"));
const storageKey = "entriesTableColumnVisibility";
let currentSortIndex = null;
let currentSortDirection = 1;
const getRows = () => Array.from(tbody.querySelectorAll("tr"));
const loadColumnVisibility = () => {
try {
return JSON.parse(localStorage.getItem(storageKey)) || {};
} catch (_) {
return {};
}
};
const saveColumnVisibility = (state) => {
try {
localStorage.setItem(storageKey, JSON.stringify(state));
} catch (_) {
// localStorage evtl. nicht verfügbar – dann einfach ohne Persistenz weiter
}
};
const buildYearOptions = () => {
const years = [...new Set(
getRows()
.map(row => Number(row.dataset.year))
.filter(year => Number.isFinite(year))
)].sort((a, b) => a - b);
[minYearFilter, maxYearFilter].forEach(select => {
const currentValue = select.value;
select.innerHTML = '<option value="">Alle</option>';
years.forEach(year => {
const option = document.createElement("option");
option.value = year;
option.textContent = year;
if (String(year) === currentValue) option.selected = true;
select.appendChild(option);
});
});
};
const buildColumnToggleMenu = () => {
const savedVisibility = loadColumnVisibility();
columnToggleMenu.innerHTML = "";
headers.forEach((header, index) => {
const key = header.dataset.columnKey || `col_${index}`;
header.dataset.columnKey = key;
const wrapper = document.createElement("div");
wrapper.className = "form-check mb-2";
const input = document.createElement("input");
input.type = "checkbox";
input.className = "form-check-input";
input.id = `toggle-column-${key}`;
input.dataset.columnKey = key;
input.dataset.columnIndex = index;
input.checked = savedVisibility[key] !== false;
const label = document.createElement("label");
label.className = "form-check-label";
label.setAttribute("for", input.id);
label.textContent = header.textContent.trim();
input.addEventListener("change", () => {
const state = getColumnVisibilityState();
const checkedCount = Object.values(state).filter(Boolean).length;
if (checkedCount === 0) {
input.checked = true;
return;
}
applyColumnVisibility();
saveColumnVisibility(getColumnVisibilityState());
applyFilters();
});
wrapper.appendChild(input);
wrapper.appendChild(label);
columnToggleMenu.appendChild(wrapper);
});
};
const getColumnVisibilityState = () => {
const state = {};
columnToggleMenu.querySelectorAll('input[type="checkbox"]').forEach(input => {
state[input.dataset.columnKey] = input.checked;
});
return state;
};
const applyColumnVisibility = () => {
const state = getColumnVisibilityState();
headers.forEach((header, index) => {
const key = header.dataset.columnKey;
const visible = state[key] !== false;
header.style.display = visible ? "" : "none";
getRows().forEach(row => {
if (row.children[index]) {
row.children[index].style.display = visible ? "" : "none";
}
});
});
};
const getVisibleRowText = (row) => {
return Array.from(row.children)
.filter(cell => cell.style.display !== "none")
.map(cell => cell.textContent.toLowerCase())
.join(" ");
};
const applyFilters = () => {
const query = textFilter.value.trim().toLowerCase();
const minYear = minYearFilter.value ? Number(minYearFilter.value) : null;
const maxYear = maxYearFilter.value ? Number(maxYearFilter.value) : null;
getRows().forEach(row => {
const rowYear = Number(row.dataset.year);
const matchesQuery = !query || getVisibleRowText(row).includes(query);
const matchesMinYear = !minYear || rowYear >= minYear;
const matchesMaxYear = !maxYear || rowYear <= maxYear;
row.style.display = (matchesQuery && matchesMinYear && matchesMaxYear) ? "" : "none";
});
};
const parseNumeric = (value) => {
if (value === null || value === undefined || value === "") return null;
const cleaned = String(value).replace(/[^\d,.-]/g, "").replace(",", ".");
const number = parseFloat(cleaned);
return Number.isNaN(number) ? null : number;
};
const getComparableValue = (cell, sortType) => {
const explicitValue = cell.dataset.sortValue;
if (explicitValue !== undefined) {
if (sortType === "date") {
const time = Date.parse(explicitValue);
return Number.isNaN(time) ? null : time;
}
if (["numeric", "boolean"].includes(sortType)) {
const num = Number(explicitValue);
return Number.isNaN(num) ? null : num;
}
return String(explicitValue).toLowerCase();
}
const text = cell.textContent.trim();
if (!text || text === "—") return null;
if (sortType === "date") {
const parts = text.split(".");
if (parts.length === 3) {
const isoLike = `${parts[2]}-${parts[1]}-${parts[0]}`;
const time = Date.parse(isoLike);
return Number.isNaN(time) ? null : time;
}
}
if (sortType === "numeric") {
return parseNumeric(text);
}
if (sortType === "boolean") {
const checkbox = cell.querySelector('input[type="checkbox"]');
if (checkbox) return checkbox.checked ? 1 : 0;
return null;
}
return text.toLowerCase();
};
const compareRows = (rowA, rowB, index, sortType, direction) => {
const cellA = rowA.children[index];
const cellB = rowB.children[index];
const valueA = getComparableValue(cellA, sortType);
const valueB = getComparableValue(cellB, sortType);
const emptyA = valueA === null || valueA === "";
const emptyB = valueB === null || valueB === "";
if (emptyA && emptyB) return 0;
if (emptyA) return 1;
if (emptyB) return -1;
if (sortType === "text") {
return direction * String(valueA).localeCompare(String(valueB), "de", { sensitivity: "base" });
}
return direction * (valueA - valueB);
};
const applySorting = (header) => {
const index = Number(header.dataset.sortIndex);
const sortType = header.dataset.sort || "text";
if (currentSortIndex === index) {
currentSortDirection *= -1;
} else {
currentSortIndex = index;
currentSortDirection = 1;
}
const sortedRows = getRows().sort((a, b) =>
compareRows(a, b, index, sortType, currentSortDirection)
);
sortedRows.forEach(row => tbody.appendChild(row));
applyFilters();
};
buildYearOptions();
buildColumnToggleMenu();
applyColumnVisibility();
applyFilters();
textFilter.addEventListener("input", applyFilters);
minYearFilter.addEventListener("change", applyFilters);
maxYearFilter.addEventListener("change", applyFilters);
sortableHeaders.forEach(header => {
header.style.cursor = "pointer";
header.addEventListener("click", () => applySorting(header));
});
}
});
};
document.addEventListener("DOMContentLoaded", initEntriesTable);
document.addEventListener("turbo:load", initEntriesTable);
})();
</script>
<script>
document.addEventListener("DOMContentLoaded", () => {

Loading…
Cancel
Save