use crate::components::filter_bar::{FilterBar, FilterZustand}; use crate::components::tool_form::{FormModus, ToolForm}; use crate::models::tool::{Prioritaet, Status, Tool}; use crate::server::api::{csv_importieren, tool_loeschen, tools_laden}; use leptos::html; use leptos::prelude::*; // --------------------------------------------------------------------------- // Sortier-Zustand (rein UI-lokal, kein Serde nötig) // --------------------------------------------------------------------------- #[derive(Clone, Debug, PartialEq, Default)] enum SortSpalte { #[default] Keine, Status, Prioritaet, } #[derive(Clone, Debug, PartialEq, Default)] enum SortRichtung { #[default] Aufsteigend, Absteigend, } impl SortRichtung { fn umkehren(&self) -> Self { match self { SortRichtung::Aufsteigend => SortRichtung::Absteigend, SortRichtung::Absteigend => SortRichtung::Aufsteigend, } } } #[derive(Clone, Debug, PartialEq, Default)] struct SortZustand { spalte: SortSpalte, richtung: SortRichtung, } fn status_rang(s: &Status) -> u8 { match s { Status::Offen => 0, Status::Organisiert => 1, Status::VorOrt => 2, } } fn prioritaet_rang(p: &Prioritaet) -> u8 { match p { Prioritaet::Hoch => 0, Prioritaet::Mittel => 1, Prioritaet::Niedrig => 2, } } #[component] pub fn ToolList() -> impl IntoView { // Aktualisierungs-Zähler: hochzählen löst Resource-Reload aus let version = RwSignal::new(0u32); let neu_laden = move || version.update(|v| *v += 1); // Daten vom Server – wird NUR innerhalb gelesen let tools_resource = Resource::new(move || version.get(), |_| tools_laden()); // Formular-Modus: None = Liste, Some(Neu/Bearbeiten) let bearbeitungs_modus = RwSignal::new(Option::::None); // Filter-Zustand let filter = RwSignal::new(FilterZustand::default()); // Welche Zeile ist gerade aufgeklappt? (ID oder None) let aufgeklappt = RwSignal::new(Option::::None); // Sortier-Zustand let sortierung = RwSignal::new(SortZustand::default()); // NodeRef + Effect für den Modal-Dialog let dialog_ref = NodeRef::::new(); Effect::new(move |_| { if let Some(el) = dialog_ref.get() { if bearbeitungs_modus.get().is_some() { let _ = el.show_modal(); } else { el.close(); } } }); // Tool-Cache: Tabelle rendert immer aus diesem Signal, nie direkt aus der Resource. // → Suspense-Fallback beim Reload lässt die Tabelle sichtbar (kein Scroll-Reset). let tools_cache = RwSignal::new(Vec::::new()); let ist_geladen = RwSignal::new(false); // CSV-Import-Meldungen let csv_fehler = RwSignal::new(Option::::None); let csv_erfolg = RwSignal::new(Option::::None); // Löschen-Aktion let loeschen_aktion = Action::new(|id: &String| { let id = id.clone(); async move { tool_loeschen(id).await } }); let neu_laden_nach_loeschen = neu_laden.clone(); Effect::new(move |_| { if let Some(ergebnis) = loeschen_aktion.value().get() { if ergebnis.is_ok() { neu_laden_nach_loeschen(); } } }); // CSV-Datei einlesen let csv_import = move |ev: leptos::ev::Event| { use wasm_bindgen::JsCast; let input = ev .target() .unwrap() .dyn_into::() .unwrap(); let files = input.files().unwrap(); if files.length() == 0 { return; } let file = files.get(0).unwrap(); let reader = web_sys::FileReader::new().unwrap(); let reader_clone = reader.clone(); let neu_laden_csv = neu_laden.clone(); let onload = wasm_bindgen::closure::Closure::wrap(Box::new(move |_: web_sys::Event| { let inhalt = reader_clone .result() .unwrap() .as_string() .unwrap_or_default(); let inhalt_clone = inhalt.clone(); let neu_laden_inner = neu_laden_csv.clone(); leptos::task::spawn_local(async move { match csv_importieren(inhalt_clone).await { Ok(n) => { csv_erfolg.set(Some(format!("{n} Werkzeuge importiert."))); csv_fehler.set(None); neu_laden_inner(); } Err(e) => { csv_fehler.set(Some(e.to_string())); csv_erfolg.set(None); } } }); }) as Box); reader.set_onload(Some(onload.as_ref().unchecked_ref())); onload.forget(); reader.read_as_text(&file).unwrap(); }; view! {
// ---- Kopfzeile ----

"🔧 Werkzeugliste"

"⬇ CSV Export"
// ---- CSV-Meldungen ---- {move || csv_erfolg.get().map(|m| view! {
{m}
})} {move || csv_fehler.get().map(|e| view! {
{e}
})} // ---- Suspense: nur zum Signal-Update beim (Re-)Laden ---- // Die Tabelle selbst ist NICHT Teil des Suspense-Blocks. // Beim Reload bleibt sie sichtbar (alter Cache-Wert) → kein Scroll-Reset. }.into_any() } else { view! {

"Lade Werkzeuge…"

}.into_any() } }> {move || tools_resource.get().map(|result| { tools_cache.set(result.unwrap_or_default()); ist_geladen.set(true); })}
// ---- Filter + Tabelle (rendert aus Cache, nie aus Resource direkt) ---- {move || { if !ist_geladen.get() { return view! { }.into_any(); } let alle_tools = tools_cache.get(); let f = filter.get(); let mut alle_tags: Vec = alle_tools .iter() .flat_map(|t| t.tags.iter().cloned()) .collect(); alle_tags.sort(); alle_tags.dedup(); let tools: Vec = alle_tools .into_iter() .filter(|t| { if !f.suche.is_empty() { let s = f.suche.to_lowercase(); let treffer = t.name.to_lowercase().contains(&s) || t.tags.iter().any(|tag| tag.to_lowercase().contains(&s)) || t.notizen .as_deref() .unwrap_or("") .to_lowercase() .contains(&s); if !treffer { return false; } } if let Some(ref st) = f.status { if &t.status != st { return false; } } if let Some(ref p) = f.prioritaet { if &t.prioritaet != p { return false; } } if let Some(ref b) = f.beschaffung { if &t.beschaffung.art != b { return false; } } if !f.tags_einschliessen.is_empty() && !f.tags_einschliessen.iter().any(|tag| t.tags.contains(tag)) { return false; } if f.tags_ausschliessen.iter().any(|tag| t.tags.contains(tag)) { return false; } true }) .collect(); // Sortierung anwenden let sort = sortierung.get(); let mut tools = tools; if sort.spalte != SortSpalte::Keine { tools.sort_by(|a, b| { let ord = match sort.spalte { SortSpalte::Status => status_rang(&a.status).cmp(&status_rang(&b.status)), SortSpalte::Prioritaet => prioritaet_rang(&a.prioritaet).cmp(&prioritaet_rang(&b.prioritaet)), SortSpalte::Keine => std::cmp::Ordering::Equal, }; if sort.richtung == SortRichtung::Absteigend { ord.reverse() } else { ord } }); } view! { {if tools.is_empty() { view! {

"Keine Werkzeuge gefunden."

}.into_any() } else { view! {
{tools.into_iter().map(|tool| { // Drei separate ID-Klone für drei verschiedene Closures let id_toggle = tool.id.clone(); let id_arrow = tool.id.clone(); let id_style = tool.id.clone(); let tool_id = tool.id.clone(); let tool_edit = tool.clone(); // Daten für die Detail-Zeile (statisch geklont) let verwendung = tool.verwendung.clone(); let notizen = tool.notizen.clone(); let verantwortlich = tool.verantwortlich.clone(); let erstellt_am = tool.erstellt_am.clone(); view! { <> // --- Hauptzeile --- // --- Detail-Zeile (immer im DOM, per style ein-/ausgeblendet) --- } }).collect::>()}
"Name" "Anzahl" "Beschaffung" "Status" {move || match sortierung.get() { SortZustand { spalte: SortSpalte::Status, richtung: SortRichtung::Aufsteigend } => " ↑", SortZustand { spalte: SortSpalte::Status, richtung: SortRichtung::Absteigend } => " ↓", _ => "", }} "Priorität" {move || match sortierung.get() { SortZustand { spalte: SortSpalte::Prioritaet, richtung: SortRichtung::Aufsteigend } => " ↑", SortZustand { spalte: SortSpalte::Prioritaet, richtung: SortRichtung::Absteigend } => " ↓", _ => "", }} "Tags" "Aktionen"
{tool.name.clone()} {tool.anzahl.to_string()} {tool.beschaffung.to_string()} {tool.status.to_string()} {tool.prioritaet.to_string()} {tool.tags.iter().map(|tag| view! { {tag.clone()} }).collect::>()}
// Verwendung {if !verwendung.is_empty() { view! {
"Verwendung"
    {verwendung.iter().map(|v| view! {
  • {v.clone()}
  • }).collect::>()}
}.into_any() } else { view! { }.into_any() }} // Notizen {notizen.map(|n| view! {
"Notizen"

{n}

})} // Verantwortlich {verantwortlich.map(|v| view! {
"Verantwortlich"

{v}

})} // Erstellt am
"Erstellt am"

{erstellt_am}

}.into_any() }} }.into_any() }} // ---- Modal-Dialog ---- = Klick auf den Backdrop let auf_backdrop = ev.target() .and_then(|t| t.dyn_into::().ok()) .is_some(); if auf_backdrop { bearbeitungs_modus.set(None); } } > {move || { bearbeitungs_modus.get().map(|modus| view! { }) }}
} }