fix(projekte): Kacheln als durchgehendes Raster statt Kundengruppen

Jede Kundenueberschrift begann eine neue Rasterzeile. Bei einem Projekt je
Kunde stand dadurch eine Kachel allein in der Zeile und rechts blieb der
Platz leer - auf der Produktion mit fuenf Kunden untereinander sichtbar.

Die Kacheln laufen jetzt in einem Raster, sortiert nach Kunde und Projekt.
Der Kundenname steht dafuer neben dem Projektcode auf der Kachel selbst.
Die Detailansicht steht wie in der Liste unter der Uebersicht.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-18 11:49:36 +02:00
co-authored by Claude Opus 5
parent be37d89d53
commit 3b818bd1c6
2 changed files with 43 additions and 45 deletions
+5 -2
View File
@@ -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.
+23 -28
View File
@@ -50,14 +50,16 @@ export const Projects: React.FC = () => {
);
}, [list, search, customerFilter, status]);
const groups = useMemo(() => {
const map = new Map<number, { customer: Project['customer']; projects: Project[] }>();
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 <ErrorBox message={projects.error} onRetry={projects.reload} />;
if (!projects.data) return <Spinner label="Lade Projekte…" />;
// 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 && (
<ProjectDetail
key={selected.id}
@@ -135,7 +136,7 @@ export const Projects: React.FC = () => {
)}
</div>
{groups.length === 0 ? (
{ordered.length === 0 ? (
<EmptyState
icon="folder"
title={list.length ? 'Keine Projekte zum Filter' : canEdit ? 'Noch keine Projekte' : 'Dir ist noch kein Projekt zugewiesen'}
@@ -161,22 +162,13 @@ export const Projects: React.FC = () => {
)}
</div>
) : (
<div className="space-y-6">
{groups.map((group, gi) => (
<section key={group.customer.id}>
<div className="mb-2 flex items-center gap-2 px-1">
<span className="h-3 w-3 rounded-full" style={{ background: colorFor(group.customer.id, group.customer.color) }} />
<h2 className="text-sm text-ink-900">{group.customer.name}</h2>
<span className="text-xs font-bold text-ink-400">{group.projects.length} {group.projects.length === 1 ? 'Projekt' : 'Projekte'}</span>
</div>
<div className="space-y-3">
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
{group.projects
.sort((a, b) => a.name.localeCompare(b.name, 'de'))
.map((p, i) => (
{ordered.map((p, i) => (
<ProjectCard
key={p.id}
project={p}
index={gi + i}
index={i}
selected={p.id === selectedId}
showAmounts={canEdit}
hoursPerDay={settings.hours_per_day}
@@ -185,13 +177,11 @@ export const Projects: React.FC = () => {
/>
))}
</div>
{selected && selected.customer_id === group.customer.id && (
<div ref={detailRef} className="mt-3 scroll-mt-4">
{selected && (
<div ref={detailRef} className="scroll-mt-4">
{detail}
</div>
)}
</section>
))}
</div>
)}
@@ -236,7 +226,12 @@ const ProjectCard: React.FC<{
<span aria-hidden className="absolute inset-x-0 top-0 h-1" style={{ background: colorFor(p.customer_id, p.customer.color) }} />
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<p className="tabular text-[11px] font-bold text-ink-400">{p.code}</p>
{/* Ohne Kundengruppen steht der Kunde auf der Kachel selbst. */}
<p className="tabular flex items-center gap-1.5 text-[11px] font-bold text-ink-400">
<span className="shrink-0">{p.code}</span>
<span className="text-ink-300">·</span>
<span className="truncate font-semibold">{p.customer.name}</span>
</p>
<h3 className="truncate text-base text-ink-900">{p.display_name}</h3>
</div>
<div className="flex shrink-0 flex-col items-end gap-1">