Verlinkungen im Projekt-Detail editierbar machen
ci / quality (push) Failing after 43s
ci / build (push) Has been skipped
deploy / deploy (push) Successful in 51s

Risikoliste und Lessons Learned konnten bisher nur beim Anlegen bzw.
Abschluss gesetzt werden. Jetzt ist die ganze Verlinkungen-Karte über
"Bearbeiten" editierbar (Outline, Plane, Risikoliste, Lessons Learned).
Leeres Feld entfernt den Link, URLs werden serverseitig validiert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
dejanandClaude Opus 4.8 committed 2026-06-06 20:11:44 +02:00
1 parent 390a99bf5e
commit e46d107b2d
2 files changed
+82 -11

No files matched your search

+78 -11
View File
@@ -143,17 +143,7 @@ export function ProjektRoute() {
<ZielSection projectId={p.id} project={p} />
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
<Card.Root>
<Card.Header>
<h2 className="text-lg font-semibold">Verlinkungen</h2>
</Card.Header>
<Card.Body className="space-y-2 text-sm">
<LinkRow label="Outline-Steckbrief" url={p.outlineSteckbriefUrl} />
<LinkRow label="Plane-Board" url={p.planeProjectUrl} />
<LinkRow label="Risikoliste" url={p.risikolisteUrl} />
<LinkRow label="Lessons Learned" url={p.lessonsLearnedUrl} />
</Card.Body>
</Card.Root>
<VerlinkungenSection projectId={p.id} project={p} />
<Card.Root>
<Card.Header>
@@ -1850,6 +1840,83 @@ function AbschlussDialog({
);
}
const LINK_FIELDS = [
{ key: 'outlineSteckbriefUrl', label: 'Outline-Steckbrief', placeholder: 'https://wiki.tool-ctrl.de/doc/…' },
{ key: 'planeProjectUrl', label: 'Plane-Board', placeholder: 'https://project.tool-ctrl.de/…' },
{ key: 'risikolisteUrl', label: 'Risikoliste', placeholder: 'https://… (z. B. Outline / Plane)' },
{ key: 'lessonsLearnedUrl', label: 'Lessons Learned', placeholder: 'https://… (Retro / Doku)' },
] as const;
function VerlinkungenSection({ projectId, project: p }: { projectId: string; project: ProjectDetail }) {
const utils = trpc.useUtils();
const [editing, setEditing] = useState(false);
const [vals, setVals] = useState<Record<string, string>>({});
const update = trpc.project.update.useMutation({
onSuccess: async () => {
toast(voice.toast.saved);
await utils.project.byId.invalidate({ id: projectId });
setEditing(false);
},
});
function startEdit() {
setVals(Object.fromEntries(LINK_FIELDS.map((f) => [f.key, p[f.key] ?? ''])));
setEditing(true);
}
function save() {
update.mutate({
projectId,
...Object.fromEntries(LINK_FIELDS.map((f) => [f.key, (vals[f.key] ?? '').trim() || null])),
});
}
return (
<Card.Root>
<Card.Header className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Verlinkungen</h2>
{!editing && (
<button
type="button"
onClick={startEdit}
className="inline-flex items-center gap-1 text-xs text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]"
>
<Pencil size={13} /> Bearbeiten
</button>
)}
</Card.Header>
<Card.Body className="space-y-2 text-sm">
{editing ? (
<div className="space-y-3">
{LINK_FIELDS.map((f) => (
<label key={f.key} className="block">
<span className="text-sm font-medium">{f.label}</span>
<input
type="url"
value={vals[f.key] ?? ''}
onChange={(e) => setVals((v) => ({ ...v, [f.key]: e.target.value }))}
placeholder={f.placeholder}
className={clsx(inputCls, 'mt-1')}
/>
</label>
))}
<p className="text-xs text-[var(--color-ink-faint)]">Leer lassen entfernt den Link. URLs müssen mit http(s):// beginnen.</p>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={() => setEditing(false)}>
Abbrechen
</Button>
<Button type="button" size="sm" onClick={save} disabled={update.isPending}>
Speichern
</Button>
</div>
</div>
) : (
LINK_FIELDS.map((f) => <LinkRow key={f.key} label={f.label} url={p[f.key]} />)
)}
</Card.Body>
</Card.Root>
);
}
function LinkRow({ label, url }: { label: string; url: string | null }) {
return (
<p className="flex items-center justify-between">
+4
View File
@@ -104,6 +104,10 @@ export const updateProjectInput = z.object({
nichtZiele: z.string().max(2000).nullable().optional(),
liefergegenstaende: z.string().max(2000).nullable().optional(),
zieldreieckFlex: zieldreieckFlexSchema.nullable().optional(),
outlineSteckbriefUrl: z.string().url().nullable().optional(),
planeProjectUrl: z.string().url().nullable().optional(),
risikolisteUrl: z.string().url().nullable().optional(),
lessonsLearnedUrl: z.string().url().nullable().optional(),
});
export type UpdateProjectInput = z.infer<typeof updateProjectInput>;