diff --git a/README.md b/README.md index 0a3d1df..dc51859 100644 --- a/README.md +++ b/README.md @@ -65,8 +65,11 @@ Dieselbe Komponente wird an drei Stellen benutzt: ein gesetzter Kundenfilter wird übernommen (`GET /api/entries?...&customer_id=`). - **Kunden**: Umschalter *Karten / Liste* mit Ort, Projektanzahl, Stunden, Umsatz erbracht/offen und Team. -`src/components/projects/ProjectTable.tsx` ist die Listenansicht der Projekte (Umschalter *Karten / Liste*): flach über alle -Kunden statt nach Kunde gruppiert, sortierbar nach Projekt, Kunde, Typ, gebuchten Stunden, Budget, Verbrauch und offenem Betrag. +`src/components/projects/ProjectTable.tsx` ist die Listenansicht der Projekte (Umschalter *Karten / Liste*): +sortierbar nach Projekt, Kunde, Typ, gebuchten Stunden, Budget, Verbrauch und offenem Betrag. +Die Kachelansicht läuft als ein durchgehendes Raster, nach Kunde und Projekt sortiert, mit dem Kundennamen auf der Kachel. +Eine Überschrift je Kunde gab es früher, sie zwang aber jede Gruppe in eine neue Zeile - bei einem Projekt je Kunde blieb der +Rest der Zeile leer. Ein Klick auf eine Zeile öffnet dieselbe Detailansicht wie eine Kachel, hier unterhalb der Tabelle. Beträge sieht nur, wer mindestens Vorgesetzter ist. diff --git a/src/components/projects/Projects.tsx b/src/components/projects/Projects.tsx index 75a730b..c2da29a 100644 --- a/src/components/projects/Projects.tsx +++ b/src/components/projects/Projects.tsx @@ -50,14 +50,16 @@ export const Projects: React.FC = () => { ); }, [list, search, customerFilter, status]); - const groups = useMemo(() => { - const map = new Map(); - for (const p of filtered) { - if (!map.has(p.customer_id)) map.set(p.customer_id, { customer: p.customer, projects: [] }); - map.get(p.customer_id)!.projects.push(p); - } - return [...map.values()].sort((a, b) => a.customer.name.localeCompare(b.customer.name, 'de')); - }, [filtered]); + // Kacheln laufen in einem durchgehenden Raster, nach Kunde und Projekt + // sortiert. Eine Ueberschrift je Kunde wuerde jede Gruppe in eine neue Zeile + // zwingen - bei einem Projekt pro Kunde bliebe der Rest der Zeile leer. + const ordered = useMemo( + () => + [...filtered].sort( + (a, b) => a.customer.name.localeCompare(b.customer.name, 'de') || a.name.localeCompare(b.name, 'de') + ), + [filtered] + ); const selected = list.find((p) => p.id === selectedId) ?? null; const customerOptions = useMemo(() => [...new Map(list.map((p) => [p.customer_id, p.customer])).values()].sort((a, b) => a.name.localeCompare(b.name, 'de')), [list]); @@ -72,8 +74,7 @@ export const Projects: React.FC = () => { if (projects.error && !projects.data) return ; if (!projects.data) return ; - // Die Detailansicht steht in der Kachelansicht unter ihrer Kundengruppe, - // in der Liste unter der Tabelle - der Inhalt ist derselbe. + // Die Detailansicht steht in beiden Ansichten unter der Uebersicht. const detail = selected && ( { )} - {groups.length === 0 ? ( + {ordered.length === 0 ? ( { )} ) : ( -
- {groups.map((group, gi) => ( -
-
- -

{group.customer.name}

- {group.projects.length} {group.projects.length === 1 ? 'Projekt' : 'Projekte'} -
-
- {group.projects - .sort((a, b) => a.name.localeCompare(b.name, 'de')) - .map((p, i) => ( - navigate(p.id === selectedId ? 'projekte' : `projekte/${p.id}`)} - /> - ))} -
- {selected && selected.customer_id === group.customer.id && ( -
- {detail} -
- )} -
- ))} +
+
+ {ordered.map((p, i) => ( + navigate(p.id === selectedId ? 'projekte' : `projekte/${p.id}`)} + /> + ))} +
+ {selected && ( +
+ {detail} +
+ )}
)} @@ -236,7 +226,12 @@ const ProjectCard: React.FC<{
-

{p.code}

+ {/* Ohne Kundengruppen steht der Kunde auf der Kachel selbst. */} +

+ {p.code} + · + {p.customer.name} +

{p.display_name}