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:
co-authored by
Claude Opus 5
parent
be37d89d53
commit
3b818bd1c6
@@ -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.
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user