Verlinkungen im Projekt-Detail editierbar machen
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:
1 parent
390a99bf5e
commit
e46d107b2d
2 files changed
+82
-11
No files matched your search
@@ -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">
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user