Files
planer/src/app/(app)/projekte/projekt-formular.tsx
T
Florian WildeandClaude Opus 5 a4a889fcb8 feat(aufgaben): Inbox, Aufgabenliste, Ideen und Projekte
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>
2026-09-10 16:17:44 +02:00

235 lines
6.6 KiB
TypeScript

'use client'
import { useState, useTransition } from 'react'
import { useRouter } from 'next/navigation'
import { Loader2, Plus, Trash2, X } from 'lucide-react'
import { toast } from 'sonner'
import { Button } from '@/components/ui/button'
import { Card, CardInhalt, CardKopf, CardTitel } from '@/components/ui/card'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Select } from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea'
import { PROJEKT_STATUS_NAME } from '@/lib/aufgabe'
import {
projektAendern,
projektAnlegen,
projektLoeschen,
} from '@/server/aktionen/projekte'
export type ProjektWerte = {
id: string
name: string
beschreibung: string | null
status: 'AKTIV' | 'PAUSIERT' | 'ABGESCHLOSSEN' | 'VERWORFEN'
bereichId: string | null
zielWert: string
}
const STATUS = ['AKTIV', 'PAUSIERT', 'ABGESCHLOSSEN', 'VERWORFEN'] as const
type Eigenschaften = {
projekt?: ProjektWerte
bereiche: { id: string; name: string }[]
/** Beim Anlegen: Formular erst auf Klick zeigen. */
einklappbar?: boolean
onFertig?: () => void
}
export function ProjektFormular({
projekt,
bereiche,
einklappbar = false,
onFertig,
}: Eigenschaften) {
const router = useRouter()
const [laeuft, starte] = useTransition()
const [felder, setFelder] = useState<Record<string, string>>({})
const [offen, setOffen] = useState(!einklappbar)
const [loeschenBestaetigt, setLoeschenBestaetigt] = useState(false)
function absenden(ereignis: React.FormEvent<HTMLFormElement>) {
ereignis.preventDefault()
const formular = ereignis.currentTarget
const daten = new FormData(formular)
starte(async () => {
const antwort = projekt
? await projektAendern(projekt.id, daten)
: await projektAnlegen(daten)
if (antwort.ok) {
setFelder({})
toast.success(antwort.meldung)
if (!projekt) {
formular.reset()
if (einklappbar) setOffen(false)
}
router.refresh()
onFertig?.()
} else {
setFelder(antwort.felder ?? {})
toast.error(antwort.fehler)
}
})
}
function loeschen() {
if (!projekt) return
if (!loeschenBestaetigt) {
setLoeschenBestaetigt(true)
return
}
const daten = new FormData()
daten.set('id', projekt.id)
starte(async () => {
const antwort = await projektLoeschen(daten)
if (antwort.ok) {
toast.success(antwort.meldung)
router.push('/projekte')
router.refresh()
} else {
toast.error(antwort.fehler)
}
})
}
if (einklappbar && !offen) {
return (
<Button onClick={() => setOffen(true)}>
<Plus aria-hidden />
Neues Projekt
</Button>
)
}
const kennung = projekt?.id ?? 'neu'
const formular = (
<form onSubmit={absenden} className="flex flex-col gap-4">
<div className="flex flex-col gap-2">
<Label htmlFor={`name-${kennung}`}>Name</Label>
<Input
id={`name-${kennung}`}
name="name"
defaultValue={projekt?.name ?? ''}
required
maxLength={80}
aria-invalid={Boolean(felder.name)}
/>
{felder.name && <p className="text-gefahr text-xs">{felder.name}</p>}
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={`beschreibung-${kennung}`}>Beschreibung</Label>
<Textarea
id={`beschreibung-${kennung}`}
name="beschreibung"
defaultValue={projekt?.beschreibung ?? ''}
maxLength={2000}
placeholder="Worum geht es, und woran merkst du, dass es fertig ist?"
/>
</div>
<div className="grid gap-4 sm:grid-cols-3">
<div className="flex flex-col gap-2">
<Label htmlFor={`status-${kennung}`}>Status</Label>
<Select
id={`status-${kennung}`}
name="status"
defaultValue={projekt?.status ?? 'AKTIV'}
>
{STATUS.map((status) => (
<option key={status} value={status}>
{PROJEKT_STATUS_NAME[status]}
</option>
))}
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={`bereich-${kennung}`}>Bereich</Label>
<Select
id={`bereich-${kennung}`}
name="bereichId"
defaultValue={projekt?.bereichId ?? ''}
>
<option value="">Kein Bereich</option>
{bereiche.map((bereich) => (
<option key={bereich.id} value={bereich.id}>
{bereich.name}
</option>
))}
</Select>
</div>
<div className="flex flex-col gap-2">
<Label htmlFor={`ziel-${kennung}`}>Zieltermin</Label>
<Input
id={`ziel-${kennung}`}
name="zielAm"
type="date"
defaultValue={projekt?.zielWert ?? ''}
/>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button type="submit" disabled={laeuft}>
{laeuft && <Loader2 className="animate-spin" aria-hidden />}
{projekt ? 'Speichern' : 'Projekt anlegen'}
</Button>
{(onFertig ?? einklappbar) && (
<Button
type="button"
art="leise"
disabled={laeuft}
onClick={() => {
if (einklappbar) setOffen(false)
onFertig?.()
}}
>
Abbrechen
</Button>
)}
{projekt && (
<Button
type="button"
art="gefahr"
className="ml-auto"
disabled={laeuft}
onClick={loeschen}
onBlur={() => setLoeschenBestaetigt(false)}
>
<Trash2 aria-hidden />
{loeschenBestaetigt ? 'Wirklich löschen?' : 'Löschen'}
</Button>
)}
</div>
{projekt && loeschenBestaetigt && (
<p className="text-gedaempft-vordergrund text-xs">
Die enthaltenen Aufgaben bleiben erhalten und verlieren nur ihre Zuordnung.
</p>
)}
</form>
)
if (!einklappbar) return formular
return (
<Card className="w-full">
<CardKopf className="flex-row items-center justify-between">
<CardTitel className="text-base">Neues Projekt</CardTitel>
<Button art="leise" groesse="quadrat" onClick={() => setOffen(false)}>
<X aria-hidden />
<span className="sr-only">Schließen</span>
</Button>
</CardKopf>
<CardInhalt>{formular}</CardInhalt>
</Card>
)
}