Die Schnellerfassung braucht ein Feld und einen Knopf; wer beim Erfassen erst ein Formular ausfüllen muss, erfasst nichts. Der Fokus bleibt danach im Feld, damit mehrere Sachen hintereinander hineingehen. Die Filter der Aufgabenliste stehen in der Adresse, nicht im Zustand der Komponente — so lässt sich eine gefilterte Liste als Lesezeichen ablegen, und der Zurück-Knopf tut, was er soll. Die Startseite zeigt jetzt, was überfällig, heute oder morgen fällig ist, statt nur die Bereiche aufzuzählen. Für die Auswahlfelder bewusst das native <select>: es bedient sich auf dem Handy mit dem Systemdialog und ist ohne Zusatzcode tastaturbedienbar. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
116 lines
4.4 KiB
TypeScript
116 lines
4.4 KiB
TypeScript
import type { Metadata } from 'next'
|
|
import Link from 'next/link'
|
|
import { ProjektFormular } from '@/app/(app)/projekte/projekt-formular'
|
|
import { Fortschrittsbalken } from '@/components/fortschrittsbalken'
|
|
import { Card, CardInhalt, CardKopf, CardTitel } from '@/components/ui/card'
|
|
import { Kennzeichen } from '@/components/ui/kennzeichen'
|
|
import { PROJEKT_STATUS_NAME, aufwandSumme, fortschrittBerechnen } from '@/lib/aufgabe'
|
|
import { alsDatum, alsDauer } from '@/lib/zeit'
|
|
import { bereicheLaden } from '@/server/abfragen/aufgaben'
|
|
import { verlangeAnmeldung } from '@/server/berechtigung'
|
|
import { db } from '@/server/db'
|
|
|
|
export const metadata: Metadata = { title: 'Projekte' }
|
|
|
|
export default async function ProjekteSeite() {
|
|
await verlangeAnmeldung('/projekte')
|
|
|
|
const [projekte, bereiche] = await Promise.all([
|
|
db.projekt.findMany({
|
|
orderBy: [{ status: 'asc' }, { name: 'asc' }],
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
beschreibung: true,
|
|
status: true,
|
|
zielAm: true,
|
|
bereich: { select: { id: true, name: true, farbe: true } },
|
|
aufgaben: { select: { status: true, aufwandMinuten: true } },
|
|
},
|
|
}),
|
|
bereicheLaden(),
|
|
])
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<header className="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-2xl font-semibold tracking-tight">Projekte</h1>
|
|
<p className="text-gedaempft-vordergrund mt-1 text-sm">
|
|
Eine Klammer um mehrere Aufgaben. Aufwand und Fortschritt ergeben sich aus
|
|
dem, was drinsteckt.
|
|
</p>
|
|
</div>
|
|
<ProjektFormular bereiche={bereiche} einklappbar />
|
|
</header>
|
|
|
|
{projekte.length === 0 ? (
|
|
<Card>
|
|
<CardInhalt className="text-gedaempft-vordergrund py-10 text-center text-sm">
|
|
Noch kein Projekt angelegt.
|
|
</CardInhalt>
|
|
</Card>
|
|
) : (
|
|
<ul className="grid gap-4 sm:grid-cols-2">
|
|
{projekte.map((projekt) => {
|
|
const fortschritt = fortschrittBerechnen(projekt.aufgaben)
|
|
const offenerAufwand = aufwandSumme(
|
|
projekt.aufgaben.filter(
|
|
(a) => a.status !== 'ERLEDIGT' && a.status !== 'VERWORFEN',
|
|
),
|
|
)
|
|
|
|
return (
|
|
<li key={projekt.id}>
|
|
<Card className="h-full">
|
|
<CardKopf>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{projekt.bereich && (
|
|
<span
|
|
aria-hidden
|
|
className="size-3 shrink-0 rounded-full ring-1 ring-black/10"
|
|
style={{ backgroundColor: projekt.bereich.farbe }}
|
|
/>
|
|
)}
|
|
<CardTitel className="text-base">
|
|
<Link
|
|
href={`/projekte/${projekt.id}`}
|
|
className="hover:underline hover:underline-offset-4"
|
|
>
|
|
{projekt.name}
|
|
</Link>
|
|
</CardTitel>
|
|
{projekt.status !== 'AKTIV' && (
|
|
<Kennzeichen>{PROJEKT_STATUS_NAME[projekt.status]}</Kennzeichen>
|
|
)}
|
|
</div>
|
|
{projekt.beschreibung && (
|
|
<p className="text-gedaempft-vordergrund line-clamp-2 text-sm">
|
|
{projekt.beschreibung}
|
|
</p>
|
|
)}
|
|
</CardKopf>
|
|
|
|
<CardInhalt className="flex flex-col gap-3">
|
|
<Fortschrittsbalken
|
|
prozent={fortschritt.prozent}
|
|
beschriftung={`Fortschritt von ${projekt.name}`}
|
|
/>
|
|
<p className="text-gedaempft-vordergrund text-xs">
|
|
{fortschritt.gesamt === 0
|
|
? 'Noch keine Aufgaben zugeordnet.'
|
|
: `${fortschritt.erledigt} von ${fortschritt.gesamt} erledigt (${fortschritt.prozent} %)`}
|
|
{offenerAufwand > 0 && ` · noch ${alsDauer(offenerAufwand)}`}
|
|
{projekt.zielAm && ` · Ziel ${alsDatum(projekt.zielAm)}`}
|
|
</p>
|
|
</CardInhalt>
|
|
</Card>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|