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>
154 lines
4.9 KiB
TypeScript
154 lines
4.9 KiB
TypeScript
import type { Metadata } from 'next'
|
|
import Link from 'next/link'
|
|
import { notFound } from 'next/navigation'
|
|
import { ArrowLeft } from 'lucide-react'
|
|
import { NeueAufgabe } from '@/app/(app)/aufgaben/neue-aufgabe'
|
|
import { ProjektFormular } from '@/app/(app)/projekte/projekt-formular'
|
|
import { AufgabenListe } from '@/components/aufgaben/aufgaben-liste'
|
|
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, alsTagesschluessel } from '@/lib/zeit'
|
|
import {
|
|
aufgabenLaden,
|
|
bereicheLaden,
|
|
projekteZurAuswahlLaden,
|
|
} from '@/server/abfragen/aufgaben'
|
|
import { verlangeAnmeldung } from '@/server/berechtigung'
|
|
import { db } from '@/server/db'
|
|
|
|
export async function generateMetadata({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>
|
|
}): Promise<Metadata> {
|
|
const { id } = await params
|
|
const projekt = await db.projekt.findUnique({
|
|
where: { id },
|
|
select: { name: true },
|
|
})
|
|
return { title: projekt?.name ?? 'Projekt' }
|
|
}
|
|
|
|
export default async function ProjektSeite({
|
|
params,
|
|
}: {
|
|
params: Promise<{ id: string }>
|
|
}) {
|
|
const { id } = await params
|
|
await verlangeAnmeldung(`/projekte/${id}`)
|
|
|
|
const projekt = await db.projekt.findUnique({
|
|
where: { id },
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
beschreibung: true,
|
|
status: true,
|
|
zielAm: true,
|
|
bereichId: true,
|
|
bereich: { select: { id: true, name: true, farbe: true } },
|
|
},
|
|
})
|
|
if (!projekt) notFound()
|
|
|
|
const [aufgaben, bereiche, projekte] = await Promise.all([
|
|
// Hier bewusst ohne nurOffen: im Projekt gehört auch das Erledigte ins Bild.
|
|
aufgabenLaden({ projektId: projekt.id }),
|
|
bereicheLaden(),
|
|
projekteZurAuswahlLaden(),
|
|
])
|
|
|
|
const fortschritt = fortschrittBerechnen(aufgaben)
|
|
const offenerAufwand = aufwandSumme(
|
|
aufgaben.filter((a) => a.status !== 'ERLEDIGT' && a.status !== 'VERWORFEN'),
|
|
)
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<Link
|
|
href="/projekte"
|
|
className="text-gedaempft-vordergrund hover:text-vordergrund inline-flex w-fit items-center gap-1 text-sm"
|
|
>
|
|
<ArrowLeft className="size-4" aria-hidden />
|
|
Alle Projekte
|
|
</Link>
|
|
|
|
<header className="flex flex-col gap-3">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{projekt.bereich && (
|
|
<span
|
|
aria-hidden
|
|
className="size-4 shrink-0 rounded-full ring-1 ring-black/10"
|
|
style={{ backgroundColor: projekt.bereich.farbe }}
|
|
/>
|
|
)}
|
|
<h1 className="text-2xl font-semibold tracking-tight">{projekt.name}</h1>
|
|
{projekt.status !== 'AKTIV' && (
|
|
<Kennzeichen>{PROJEKT_STATUS_NAME[projekt.status]}</Kennzeichen>
|
|
)}
|
|
</div>
|
|
|
|
{projekt.beschreibung && (
|
|
<p className="text-gedaempft-vordergrund text-sm whitespace-pre-line">
|
|
{projekt.beschreibung}
|
|
</p>
|
|
)}
|
|
|
|
<Fortschrittsbalken
|
|
prozent={fortschritt.prozent}
|
|
beschriftung={`Fortschritt von ${projekt.name}`}
|
|
/>
|
|
<p className="text-gedaempft-vordergrund text-sm">
|
|
{fortschritt.gesamt === 0
|
|
? 'Noch keine Aufgaben zugeordnet.'
|
|
: `${fortschritt.erledigt} von ${fortschritt.gesamt} erledigt (${fortschritt.prozent} %)`}
|
|
{offenerAufwand > 0 && ` · noch ${alsDauer(offenerAufwand)} geschätzt`}
|
|
{projekt.zielAm && ` · Ziel ${alsDatum(projekt.zielAm)}`}
|
|
</p>
|
|
</header>
|
|
|
|
<div className="flex flex-wrap items-start justify-between gap-4">
|
|
<h2 className="text-lg font-medium">Aufgaben im Projekt</h2>
|
|
<NeueAufgabe
|
|
bereiche={bereiche}
|
|
projekte={projekte}
|
|
vorgabe={{
|
|
projektId: projekt.id,
|
|
...(projekt.bereichId ? { bereichId: projekt.bereichId } : {}),
|
|
typ: 'ARBEITSPAKET',
|
|
}}
|
|
beschriftung="Aufgabe hinzufügen"
|
|
/>
|
|
</div>
|
|
|
|
<AufgabenListe
|
|
aufgaben={aufgaben}
|
|
bereiche={bereiche}
|
|
projekte={projekte}
|
|
leerText="Diesem Projekt ist noch keine Aufgabe zugeordnet."
|
|
/>
|
|
|
|
<Card>
|
|
<CardKopf>
|
|
<CardTitel className="text-base">Projekt bearbeiten</CardTitel>
|
|
</CardKopf>
|
|
<CardInhalt>
|
|
<ProjektFormular
|
|
projekt={{
|
|
id: projekt.id,
|
|
name: projekt.name,
|
|
beschreibung: projekt.beschreibung,
|
|
status: projekt.status,
|
|
bereichId: projekt.bereichId,
|
|
zielWert: projekt.zielAm ? alsTagesschluessel(projekt.zielAm) : '',
|
|
}}
|
|
bereiche={bereiche}
|
|
/>
|
|
</CardInhalt>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|