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} />
|
<ZielSection projectId={p.id} project={p} />
|
||||||
|
|
||||||
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<section className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
<Card.Root>
|
<VerlinkungenSection projectId={p.id} project={p} />
|
||||||
<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>
|
|
||||||
|
|
||||||
<Card.Root>
|
<Card.Root>
|
||||||
<Card.Header>
|
<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 }) {
|
function LinkRow({ label, url }: { label: string; url: string | null }) {
|
||||||
return (
|
return (
|
||||||
<p className="flex items-center justify-between">
|
<p className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -104,6 +104,10 @@ export const updateProjectInput = z.object({
|
|||||||
nichtZiele: z.string().max(2000).nullable().optional(),
|
nichtZiele: z.string().max(2000).nullable().optional(),
|
||||||
liefergegenstaende: z.string().max(2000).nullable().optional(),
|
liefergegenstaende: z.string().max(2000).nullable().optional(),
|
||||||
zieldreieckFlex: zieldreieckFlexSchema.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>;
|
export type UpdateProjectInput = z.infer<typeof updateProjectInput>;
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user