chore(format): Prettier auf heute/projekt/lead.tsx anwenden

Diese drei Dateien waren nicht prettier-konform (nie formatiert). Isolierter
Format-Lauf, damit der folgende Logik-Commit nur die echten Aenderungen zeigt.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Raffael SheikhandClaude Opus 4.8 committed 2026-06-07 11:28:19 +02:00
1 parent 00b611fa3b
commit 33a740ca7e
3 files changed
+580 -129

No files matched your search

+11 -5
View File
@@ -219,9 +219,7 @@ function TodoRow({
<span className={dotClass} />
<span
className={
todo.status === 'dropped'
? 'flex-1 line-through text-[var(--color-ink-muted)]'
: 'flex-1'
todo.status === 'dropped' ? 'flex-1 line-through text-[var(--color-ink-muted)]' : 'flex-1'
}
>
{todo.title}
@@ -358,7 +356,10 @@ function CheckInOutBar({
const duration =
checkedInAt && checkedOutAt
? formatDurationCompact(
Math.max(0, Math.round((new Date(checkedOutAt).getTime() - new Date(checkedInAt).getTime()) / 1000)),
Math.max(
0,
Math.round((new Date(checkedOutAt).getTime() - new Date(checkedInAt).getTime()) / 1000),
),
)
: null;
@@ -542,7 +543,12 @@ function EinstempeltView({
todos,
entry,
}: {
todos: Array<{ id: string; title: string; status: 'open' | 'done' | 'dropped'; unplanned?: boolean }>;
todos: Array<{
id: string;
title: string;
status: 'open' | 'done' | 'dropped';
unplanned?: boolean;
}>;
entry: {
status: 'begun' | 'full' | 'absent';
checkedInAt: string | Date | null;
+96 -27
View File
@@ -297,9 +297,7 @@ function Header({ lead, onBack }: { lead: LeadDossier; onBack: () => void }) {
<span className="text-[var(--color-ink-muted)]">Owner</span>
<select
value={lead.ownerUserId ?? ''}
onChange={(e) =>
update.mutate({ id: lead.id, ownerUserId: e.target.value || null })
}
onChange={(e) => update.mutate({ id: lead.id, ownerUserId: e.target.value || null })}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1 text-sm"
>
<option value="">— niemand —</option>
@@ -354,7 +352,10 @@ function PromoteDialog({ leadId, onClose }: { leadId: string; onClose: () => voi
},
});
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30" onClick={onClose}>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={onClose}
>
<div
className="w-full max-w-sm rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
onClick={(e) => e.stopPropagation()}
@@ -522,13 +523,7 @@ function EditableSection({
);
}
function VertrauenslevelControl({
leadId,
value,
}: {
leadId: string;
value: number | null;
}) {
function VertrauenslevelControl({ leadId, value }: { leadId: string; value: number | null }) {
const utils = trpc.useUtils();
const update = trpc.lead.update.useMutation({
onSuccess: async () => {
@@ -541,7 +536,10 @@ function VertrauenslevelControl({
<select
value={value ?? ''}
onChange={(e) =>
update.mutate({ id: leadId, vertrauenslevel: e.target.value ? Number(e.target.value) : null })
update.mutate({
id: leadId,
vertrauenslevel: e.target.value ? Number(e.target.value) : null,
})
}
className="rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1 text-sm"
>
@@ -687,8 +685,18 @@ function AddContactForm({ leadId, onDone }: { leadId: string; onDone: () => void
}}
>
<div className="grid grid-cols-2 gap-2">
<input placeholder="Name *" value={name} onChange={(e) => setName(e.target.value)} className={inputCls} />
<input placeholder="Rolle / Titel" value={rolle} onChange={(e) => setRolle(e.target.value)} className={inputCls} />
<input
placeholder="Name *"
value={name}
onChange={(e) => setName(e.target.value)}
className={inputCls}
/>
<input
placeholder="Rolle / Titel"
value={rolle}
onChange={(e) => setRolle(e.target.value)}
className={inputCls}
/>
<select value={funktion} onChange={(e) => setFunktion(e.target.value)} className={inputCls}>
<option value="">Funktion im Kaufprozess</option>
{Object.entries(CONTACT_FUNKTION_LABEL).map(([v, l]) => (
@@ -697,14 +705,43 @@ function AddContactForm({ leadId, onDone }: { leadId: string; onDone: () => void
</option>
))}
</select>
<input placeholder="E-Mail" value={email} onChange={(e) => setEmail(e.target.value)} className={inputCls} />
<input placeholder="Telefon" value={telefon} onChange={(e) => setTelefon(e.target.value)} className={inputCls} />
<input placeholder="LinkedIn-URL" value={linkedin} onChange={(e) => setLinkedin(e.target.value)} className={inputCls} />
<input placeholder="Sprache / Anrede" value={sprache} onChange={(e) => setSprache(e.target.value)} className={inputCls} />
<input placeholder="Im Buying-Center seit" value={seit} onChange={(e) => setSeit(e.target.value)} className={inputCls} />
<input
placeholder="E-Mail"
value={email}
onChange={(e) => setEmail(e.target.value)}
className={inputCls}
/>
<input
placeholder="Telefon"
value={telefon}
onChange={(e) => setTelefon(e.target.value)}
className={inputCls}
/>
<input
placeholder="LinkedIn-URL"
value={linkedin}
onChange={(e) => setLinkedin(e.target.value)}
className={inputCls}
/>
<input
placeholder="Sprache / Anrede"
value={sprache}
onChange={(e) => setSprache(e.target.value)}
className={inputCls}
/>
<input
placeholder="Im Buying-Center seit"
value={seit}
onChange={(e) => setSeit(e.target.value)}
className={inputCls}
/>
</div>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={isPrimary} onChange={(e) => setIsPrimary(e.target.checked)} />
<input
type="checkbox"
checked={isPrimary}
onChange={(e) => setIsPrimary(e.target.checked)}
/>
Hauptkontakt
</label>
<div className="flex justify-end gap-2">
@@ -832,7 +869,12 @@ function AddTouchpointForm({ leadId, onDone }: { leadId: string; onDone: () => v
}}
>
<div className="grid grid-cols-2 gap-2">
<input type="date" value={datum} onChange={(e) => setDatum(e.target.value)} className={inputCls} />
<input
type="date"
value={datum}
onChange={(e) => setDatum(e.target.value)}
className={inputCls}
/>
<select value={kanal} onChange={(e) => setKanal(e.target.value)} className={inputCls}>
{Object.entries(TOUCHPOINT_KANAL_LABEL).map(([v, l]) => (
<option key={v} value={v}>
@@ -849,10 +891,20 @@ function AddTouchpointForm({ leadId, onDone }: { leadId: string; onDone: () => v
className={inputCls}
/>
<div className="grid grid-cols-2 gap-2">
<input placeholder="Outcome" value={outcome} onChange={(e) => setOutcome(e.target.value)} className={inputCls} />
<input
placeholder="Outcome"
value={outcome}
onChange={(e) => setOutcome(e.target.value)}
className={inputCls}
/>
<label className="flex flex-col text-xs text-[var(--color-ink-muted)]">
Follow-up bis
<input type="date" value={followUp} onChange={(e) => setFollowUp(e.target.value)} className={inputCls} />
<input
type="date"
value={followUp}
onChange={(e) => setFollowUp(e.target.value)}
className={inputCls}
/>
</label>
</div>
<div className="flex justify-end gap-2">
@@ -904,7 +956,9 @@ function OffeneFragenSection({
persist(fragen.map((x, j) => (j === i ? { ...x, done: !x.done } : x)))
}
/>
<span className={clsx('flex-1', f.done && 'text-[var(--color-ink-muted)] line-through')}>
<span
className={clsx('flex-1', f.done && 'text-[var(--color-ink-muted)] line-through')}
>
{f.text}
</span>
<button
@@ -996,8 +1050,18 @@ function AnhaengeSection({
setUrl('');
}}
>
<input placeholder="Bezeichnung" value={label} onChange={(e) => setLabel(e.target.value)} className={inputCls} />
<input placeholder="URL" value={url} onChange={(e) => setUrl(e.target.value)} className={inputCls} />
<input
placeholder="Bezeichnung"
value={label}
onChange={(e) => setLabel(e.target.value)}
className={inputCls}
/>
<input
placeholder="URL"
value={url}
onChange={(e) => setUrl(e.target.value)}
className={inputCls}
/>
<Button type="submit" size="sm" variant="secondary">
<Plus size={14} />
</Button>
@@ -1010,7 +1074,12 @@ function AnhaengeSection({
function ExternalLink({ url, label }: { url: string; label?: string }) {
const href = url.startsWith('http') ? url : `https://${url}`;
return (
<a href={href} target="_blank" rel="noreferrer" className="truncate text-[var(--color-ink)] hover:underline">
<a
href={href}
target="_blank"
rel="noreferrer"
className="truncate text-[var(--color-ink)] hover:underline"
>
{label ?? url}
</a>
);
+473 -97
View File
@@ -367,7 +367,9 @@ function ZielSection({ projectId, project: p }: { projectId: string; project: Pr
const [desc, setDesc] = useState(p.description ?? '');
const [nichtZiele, setNichtZiele] = useState(p.nichtZiele ?? '');
const [liefer, setLiefer] = useState(p.liefergegenstaende ?? '');
const [flex, setFlex] = useState<ZieldreieckFlex | ''>((p.zieldreieckFlex as ZieldreieckFlex) ?? '');
const [flex, setFlex] = useState<ZieldreieckFlex | ''>(
(p.zieldreieckFlex as ZieldreieckFlex) ?? '',
);
const update = trpc.project.update.useMutation({
onSuccess: async () => {
toast(voice.toast.saved);
@@ -424,11 +426,24 @@ function ZielSection({ projectId, project: p }: { projectId: string; project: Pr
<div className="space-y-3">
<label className="block">
<span className="text-sm font-medium">Maxime (roter Faden, ein Satz)</span>
<input value={maxime} onChange={(e) => setMaxime(e.target.value)} maxLength={280} placeholder="z. B. „So schnell wie möglich, mit bestehenden Ressourcen.“" className={clsx(inputCls, 'mt-1')} />
<input
value={maxime}
onChange={(e) => setMaxime(e.target.value)}
maxLength={280}
placeholder="z. B. „So schnell wie möglich, mit bestehenden Ressourcen.“"
className={clsx(inputCls, 'mt-1')}
/>
</label>
<label className="block">
<span className="text-sm font-medium">Ziel (SMART)</span>
<textarea value={ziel} onChange={(e) => setZiel(e.target.value)} rows={2} maxLength={1000} placeholder="z. B. „Ladezeit der Startseite bis 31.07. unter 1 Sekunde, abgenommen vom Kunden.“" className={clsx(inputCls, 'mt-1')} />
<textarea
value={ziel}
onChange={(e) => setZiel(e.target.value)}
rows={2}
maxLength={1000}
placeholder="z. B. „Ladezeit der Startseite bis 31.07. unter 1 Sekunde, abgenommen vom Kunden.“"
className={clsx(inputCls, 'mt-1')}
/>
</label>
{zielHint && (
<p className="flex items-start gap-1.5 text-xs text-[var(--color-ampel-amber-fg)]">
@@ -438,19 +453,45 @@ function ZielSection({ projectId, project: p }: { projectId: string; project: Pr
)}
<label className="block">
<span className="text-sm font-medium">Nicht-Ziele (Scope-Schutz)</span>
<textarea value={nichtZiele} onChange={(e) => setNichtZiele(e.target.value)} rows={2} maxLength={2000} placeholder="Was gehört ausdrücklich NICHT dazu?" className={clsx(inputCls, 'mt-1')} />
<textarea
value={nichtZiele}
onChange={(e) => setNichtZiele(e.target.value)}
rows={2}
maxLength={2000}
placeholder="Was gehört ausdrücklich NICHT dazu?"
className={clsx(inputCls, 'mt-1')}
/>
</label>
<label className="block">
<span className="text-sm font-medium">Liefergegenstände</span>
<textarea value={liefer} onChange={(e) => setLiefer(e.target.value)} rows={2} maxLength={2000} placeholder="Was liegt am Ende konkret vor?" className={clsx(inputCls, 'mt-1')} />
<textarea
value={liefer}
onChange={(e) => setLiefer(e.target.value)}
rows={2}
maxLength={2000}
placeholder="Was liegt am Ende konkret vor?"
className={clsx(inputCls, 'mt-1')}
/>
</label>
<label className="block">
<span className="text-sm font-medium">Hintergrund / Beschreibung</span>
<textarea value={desc} onChange={(e) => setDesc(e.target.value)} rows={3} maxLength={2000} className={clsx(inputCls, 'mt-1')} />
<textarea
value={desc}
onChange={(e) => setDesc(e.target.value)}
rows={3}
maxLength={2000}
className={clsx(inputCls, 'mt-1')}
/>
</label>
<label className="block">
<span className="text-sm font-medium">Zieldreieck: Wenn es eng wird, gibt zuerst nach…</span>
<select value={flex} onChange={(e) => setFlex(e.target.value as ZieldreieckFlex | '')} className={clsx(inputCls, 'mt-1')}>
<span className="text-sm font-medium">
Zieldreieck: Wenn es eng wird, gibt zuerst nach…
</span>
<select
value={flex}
onChange={(e) => setFlex(e.target.value as ZieldreieckFlex | '')}
className={clsx(inputCls, 'mt-1')}
>
<option value="">— nicht festgelegt —</option>
{ZIELDREIECK_FLEX.map((z) => (
<option key={z} value={z}>
@@ -483,7 +524,15 @@ function ZielSection({ projectId, project: p }: { projectId: string; project: Pr
);
}
function ZielRow({ label, value, italic }: { label: string; value: string | null; italic?: boolean }) {
function ZielRow({
label,
value,
italic,
}: {
label: string;
value: string | null;
italic?: boolean;
}) {
return (
<div>
<p className="text-xs font-medium text-[var(--color-ink-muted)]">{label}</p>
@@ -521,7 +570,9 @@ function Zieldreieck({ flex }: { flex: ZieldreieckFlex | null }) {
</span>
);
})}
{!flex && <span className="text-xs text-[var(--color-ink-faint)]">Priorität nicht festgelegt</span>}
{!flex && (
<span className="text-xs text-[var(--color-ink-faint)]">Priorität nicht festgelegt</span>
)}
</div>
</div>
);
@@ -582,7 +633,9 @@ function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectP
);
})}
</ol>
<p className="text-xs text-[var(--color-ink-muted)]">Gate-Ziel: {PHASE_META[phase].gateZiel}</p>
<p className="text-xs text-[var(--color-ink-muted)]">
Gate-Ziel: {PHASE_META[phase].gateZiel}
</p>
{gates.length > 0 && (
<details>
<summary className="cursor-pointer text-xs text-[var(--color-ink-muted)] hover:text-[var(--color-ink)]">
@@ -594,7 +647,9 @@ function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectP
<span
className={clsx(
'mt-0.5 inline-flex h-4 w-4 shrink-0 items-center justify-center rounded-full text-[10px] text-white',
g.ergebnisErreicht ? 'bg-[var(--color-ampel-green-fg)]' : 'bg-[var(--color-ampel-red-fg)]',
g.ergebnisErreicht
? 'bg-[var(--color-ampel-green-fg)]'
: 'bg-[var(--color-ampel-red-fg)]',
)}
>
{g.ergebnisErreicht ? '✓' : '✗'}
@@ -604,7 +659,9 @@ function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectP
<span className="text-xs text-[var(--color-ink-faint)]">
{formatDay(g.reviewedAt)}
</span>
{g.notiz && <span className="block text-xs text-[var(--color-ink-muted)]">{g.notiz}</span>}
{g.notiz && (
<span className="block text-xs text-[var(--color-ink-muted)]">{g.notiz}</span>
)}
</span>
</li>
))}
@@ -612,7 +669,9 @@ function PhaseSection({ projectId, phase }: { projectId: string; phase: ProjectP
</details>
)}
</Card.Body>
{gateOpen && <GateDialog projectId={projectId} phase={phase} onClose={() => setGateOpen(false)} />}
{gateOpen && (
<GateDialog projectId={projectId} phase={phase} onClose={() => setGateOpen(false)} />
)}
</Card.Root>
);
}
@@ -640,24 +699,52 @@ function GateDialog({
},
});
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/30" onClick={onClose}>
<div className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]" onClick={(e) => e.stopPropagation()}>
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
onClick={onClose}
>
<div
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
onClick={(e) => e.stopPropagation()}
>
<h2 className="text-xl font-semibold">Gate-Review: {PHASE_META[phase].label}</h2>
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">Ziel: {PHASE_META[phase].gateZiel}</p>
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
Ziel: {PHASE_META[phase].gateZiel}
</p>
<form
className="mt-4 space-y-3"
onSubmit={(e) => {
e.preventDefault();
add.mutate({ projectId, phase, ergebnisErreicht: erreicht, notiz: notiz.trim() || undefined, advance });
add.mutate({
projectId,
phase,
ergebnisErreicht: erreicht,
notiz: notiz.trim() || undefined,
advance,
});
}}
>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={erreicht} onChange={(e) => setErreicht(e.target.checked)} />
<input
type="checkbox"
checked={erreicht}
onChange={(e) => setErreicht(e.target.checked)}
/>
Ergebnis erreicht
</label>
<textarea value={notiz} onChange={(e) => setNotiz(e.target.value)} rows={3} placeholder="Rückblick / Maßnahmen (bei Gelb/Rot)" className={inputCls} />
<textarea
value={notiz}
onChange={(e) => setNotiz(e.target.value)}
rows={3}
placeholder="Rückblick / Maßnahmen (bei Gelb/Rot)"
className={inputCls}
/>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={advance} onChange={(e) => setAdvance(e.target.checked)} />
<input
type="checkbox"
checked={advance}
onChange={(e) => setAdvance(e.target.checked)}
/>
Folgephase jetzt freigeben
</label>
<div className="flex justify-end gap-2">
@@ -742,7 +829,11 @@ function StatusSection({
<div className="grid grid-cols-2 gap-2">
<label className="text-xs text-[var(--color-ink-muted)]">
Ampel
<select value={neuAmpel} onChange={(e) => setNeuAmpel(e.target.value as 'green' | 'amber' | 'red')} className={inputCls}>
<select
value={neuAmpel}
onChange={(e) => setNeuAmpel(e.target.value as 'green' | 'amber' | 'red')}
className={inputCls}
>
<option value="green">Im Plan</option>
<option value="amber">Gefährdet</option>
<option value="red">Blockiert</option>
@@ -750,7 +841,13 @@ function StatusSection({
</label>
<label className="text-xs text-[var(--color-ink-muted)]">
Trend
<select value={neuTrend} onChange={(e) => setNeuTrend(e.target.value as 'besser' | 'gleich' | 'schlechter' | '')} className={inputCls}>
<select
value={neuTrend}
onChange={(e) =>
setNeuTrend(e.target.value as 'besser' | 'gleich' | 'schlechter' | '')
}
className={inputCls}
>
<option value="">—</option>
<option value="besser">Besser</option>
<option value="gleich">Gleich</option>
@@ -758,9 +855,20 @@ function StatusSection({
</select>
</label>
</div>
<textarea value={text} onChange={(e) => setText(e.target.value)} rows={2} maxLength={1000} placeholder="2-3 Sätze Erläuterung" className={inputCls} />
<textarea
value={text}
onChange={(e) => setText(e.target.value)}
rows={2}
maxLength={1000}
placeholder="2-3 Sätze Erläuterung"
className={inputCls}
/>
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={extern} onChange={(e) => setExtern(e.target.checked)} />
<input
type="checkbox"
checked={extern}
onChange={(e) => setExtern(e.target.checked)}
/>
Nach außen kommunizierbar (Kundenprojekt)
</label>
<div className="flex justify-end gap-2">
@@ -777,12 +885,21 @@ function StatusSection({
{latest ? (
<div className="text-sm">
<div className="flex items-center gap-2">
<span className={clsx('rounded-full px-2 py-0.5 text-xs font-medium', ampelCls[latest.ampel])}>
<span
className={clsx(
'rounded-full px-2 py-0.5 text-xs font-medium',
ampelCls[latest.ampel],
)}
>
{ampelLabel[latest.ampel]}
</span>
{latest.trend && <span className="text-xs text-[var(--color-ink-muted)]">Trend: {latest.trend}</span>}
{latest.trend && (
<span className="text-xs text-[var(--color-ink-muted)]">Trend: {latest.trend}</span>
)}
{latest.externSichtbar && (
<span className="rounded-full bg-black/5 px-2 py-0.5 text-[10px] text-[var(--color-ink-muted)]">extern</span>
<span className="rounded-full bg-black/5 px-2 py-0.5 text-[10px] text-[var(--color-ink-muted)]">
extern
</span>
)}
<span className="ml-auto text-xs text-[var(--color-ink-faint)]">
{formatDay(latest.createdAt)}
@@ -803,7 +920,9 @@ function StatusSection({
) : histQ.isPending ? (
<p className="text-sm text-[var(--color-ink-faint)]">Status wird geladen…</p>
) : histQ.isError ? (
<p className="text-sm text-[var(--color-ink-muted)]">Status konnte nicht geladen werden.</p>
<p className="text-sm text-[var(--color-ink-muted)]">
Status konnte nicht geladen werden.
</p>
) : (
<p className="text-sm text-[var(--color-ink-muted)]">Noch kein Status gesetzt.</p>
)}
@@ -816,11 +935,16 @@ function StatusSection({
<ul className="mt-2 space-y-2 text-sm">
{history.slice(1).map((s) => (
<li key={s.id} className="flex items-start gap-2">
<span className={clsx('mt-0.5 inline-block h-2.5 w-2.5 shrink-0 rounded-full', {
green: 'bg-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-fg)]',
}[s.ampel])} />
<span
className={clsx(
'mt-0.5 inline-block h-2.5 w-2.5 shrink-0 rounded-full',
{
green: 'bg-[var(--color-ampel-green-fg)]',
amber: 'bg-[var(--color-ampel-amber-fg)]',
red: 'bg-[var(--color-ampel-red-fg)]',
}[s.ampel],
)}
/>
<span>
<span className="text-xs text-[var(--color-ink-faint)]">
{formatDay(s.createdAt)}
@@ -874,11 +998,25 @@ function ChangeLogSection({ projectId }: { projectId: string }) {
onSubmit={(e) => {
e.preventDefault();
if (!titel.trim()) return;
add.mutate({ projectId, titel: titel.trim(), auswirkung: auswirkung.trim() || undefined });
add.mutate({
projectId,
titel: titel.trim(),
auswirkung: auswirkung.trim() || undefined,
});
}}
>
<input value={titel} onChange={(e) => setTitel(e.target.value)} placeholder="Was ändert sich?" className={inputCls} />
<input value={auswirkung} onChange={(e) => setAuswirkung(e.target.value)} placeholder="Auswirkung aufs Zieldreieck (Leistung/Zeit/Aufwand)" className={inputCls} />
<input
value={titel}
onChange={(e) => setTitel(e.target.value)}
placeholder="Was ändert sich?"
className={inputCls}
/>
<input
value={auswirkung}
onChange={(e) => setAuswirkung(e.target.value)}
placeholder="Auswirkung aufs Zieldreieck (Leistung/Zeit/Aufwand)"
className={inputCls}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={() => setAdding(false)}>
Abbrechen
@@ -891,7 +1029,8 @@ function ChangeLogSection({ projectId }: { projectId: string }) {
)}
{list.length === 0 && !adding ? (
<p className="text-sm text-[var(--color-ink-muted)]">
Keine Änderungen. Leitfaden: keine stille Erweiterung des Umfangs — jede Änderung bewusst entscheiden.
Keine Änderungen. Leitfaden: keine stille Erweiterung des Umfangs — jede Änderung
bewusst entscheiden.
</p>
) : (
<div className="space-y-2">
@@ -916,7 +1055,11 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className="font-medium">{c.titel}</p>
{c.auswirkung && <p className="mt-0.5 text-xs text-[var(--color-ink-muted)]">Auswirkung: {c.auswirkung}</p>}
{c.auswirkung && (
<p className="mt-0.5 text-xs text-[var(--color-ink-muted)]">
Auswirkung: {c.auswirkung}
</p>
)}
<p className="mt-1 text-xs text-[var(--color-ink-faint)]">
{formatDay(c.createdAt)}
{c.authorName ? ` · ${c.authorName}` : ''}
@@ -931,7 +1074,9 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
<div className="flex shrink-0 items-center gap-1">
<select
value={c.entscheidung}
onChange={(e) => update.mutate({ id: c.id, entscheidung: e.target.value as ChangeEntscheidung })}
onChange={(e) =>
update.mutate({ id: c.id, entscheidung: e.target.value as ChangeEntscheidung })
}
className={clsx('rounded-full border-0 px-2 py-0.5 text-xs font-medium', meta.badge)}
>
{CHANGE_ENTSCHEIDUNGEN.map((en) => (
@@ -940,7 +1085,12 @@ function ChangeRow({ projectId, change: c }: { projectId: string; change: Change
</option>
))}
</select>
<button type="button" onClick={() => remove.mutate({ id: c.id })} aria-label="Entfernen" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]">
<button
type="button"
onClick={() => remove.mutate({ id: c.id })}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
<Trash2 size={14} />
</button>
</div>
@@ -997,7 +1147,10 @@ function StakeholderMatrix({ list }: { list: Stakeholder[] }) {
<div className="grid grid-cols-[auto_repeat(3,1fr)] gap-1 text-xs">
<div />
{cols.map((c) => (
<div key={c} className="px-1 pb-1 text-center font-medium text-[var(--color-ink-muted)]">
<div
key={c}
className="px-1 pb-1 text-center font-medium text-[var(--color-ink-muted)]"
>
Interesse {LEVEL_META[c].label.toLowerCase()}
</div>
))}
@@ -1036,7 +1189,12 @@ function FragmentRow({ r, cols, list }: { r: Level; cols: Level[]; list: Stakeho
key={s.id}
className="inline-flex items-center gap-1 rounded-full bg-black/5 px-1.5 py-0.5 text-[11px]"
>
<span className={clsx('h-1.5 w-1.5 rounded-full', HALTUNG_META[s.haltung as Haltung].dot)} />
<span
className={clsx(
'h-1.5 w-1.5 rounded-full',
HALTUNG_META[s.haltung as Haltung].dot,
)}
/>
{s.name}
</span>
))}
@@ -1094,13 +1252,27 @@ function StakeholderRow({
}}
>
<div className="grid grid-cols-2 gap-2">
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name *" className={inputCls} />
<input value={rolle} onChange={(e) => setRolle(e.target.value)} placeholder="Rolle" className={inputCls} />
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name *"
className={inputCls}
/>
<input
value={rolle}
onChange={(e) => setRolle(e.target.value)}
placeholder="Rolle"
className={inputCls}
/>
<LevelSelect label="Interesse" value={interesse} onChange={setInteresse} />
<LevelSelect label="Einfluss" value={einfluss} onChange={setEinfluss} />
<label className="text-xs text-[var(--color-ink-muted)]">
Haltung
<select value={haltung} onChange={(e) => setHaltung(e.target.value as Haltung)} className={inputCls}>
<select
value={haltung}
onChange={(e) => setHaltung(e.target.value as Haltung)}
className={inputCls}
>
{HALTUNGEN.map((h) => (
<option key={h} value={h}>
{HALTUNG_META[h].label}
@@ -1109,8 +1281,18 @@ function StakeholderRow({
</select>
</label>
</div>
<input value={beduerfnis} onChange={(e) => setBeduerfnis(e.target.value)} placeholder="Bedürfnis / was braucht die Person?" className={inputCls} />
<input value={notiz} onChange={(e) => setNotiz(e.target.value)} placeholder="Notiz" className={inputCls} />
<input
value={beduerfnis}
onChange={(e) => setBeduerfnis(e.target.value)}
placeholder="Bedürfnis / was braucht die Person?"
className={inputCls}
/>
<input
value={notiz}
onChange={(e) => setNotiz(e.target.value)}
placeholder="Notiz"
className={inputCls}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={() => setEditing(false)}>
Abbrechen
@@ -1129,24 +1311,40 @@ function StakeholderRow({
<p className="flex items-center gap-2 font-medium">
<span className={clsx('h-2 w-2 rounded-full', HALTUNG_META[s.haltung as Haltung].dot)} />
{s.name}
{s.rolle && <span className="font-normal text-[var(--color-ink-muted)]">· {s.rolle}</span>}
{s.rolle && (
<span className="font-normal text-[var(--color-ink-muted)]">· {s.rolle}</span>
)}
</p>
<div className="mt-1 flex flex-wrap gap-1 text-xs">
<span className={clsx('rounded-full px-2 py-0.5', LEVEL_META[s.interesse as Level].badge)}>
<span
className={clsx('rounded-full px-2 py-0.5', LEVEL_META[s.interesse as Level].badge)}
>
Interesse {LEVEL_META[s.interesse as Level].label}
</span>
<span className={clsx('rounded-full px-2 py-0.5', LEVEL_META[s.einfluss as Level].badge)}>
Einfluss {LEVEL_META[s.einfluss as Level].label}
</span>
</div>
{s.beduerfnis && <p className="mt-1 text-xs text-[var(--color-ink-muted)]">Braucht: {s.beduerfnis}</p>}
{s.beduerfnis && (
<p className="mt-1 text-xs text-[var(--color-ink-muted)]">Braucht: {s.beduerfnis}</p>
)}
{s.notiz && <p className="mt-0.5 text-xs text-[var(--color-ink-faint)]">{s.notiz}</p>}
</div>
<div className="flex shrink-0 gap-1">
<button type="button" onClick={() => setEditing(true)} aria-label="Bearbeiten" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]">
<button
type="button"
onClick={() => setEditing(true)}
aria-label="Bearbeiten"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]"
>
<Pencil size={14} />
</button>
<button type="button" onClick={() => remove.mutate({ id: s.id })} aria-label="Entfernen" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]">
<button
type="button"
onClick={() => remove.mutate({ id: s.id })}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
<Trash2 size={14} />
</button>
</div>
@@ -1186,13 +1384,27 @@ function AddStakeholderForm({ projectId, onDone }: { projectId: string; onDone:
}}
>
<div className="grid grid-cols-2 gap-2">
<input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name *" className={inputCls} />
<input value={rolle} onChange={(e) => setRolle(e.target.value)} placeholder="Rolle" className={inputCls} />
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Name *"
className={inputCls}
/>
<input
value={rolle}
onChange={(e) => setRolle(e.target.value)}
placeholder="Rolle"
className={inputCls}
/>
<LevelSelect label="Interesse" value={interesse} onChange={setInteresse} />
<LevelSelect label="Einfluss" value={einfluss} onChange={setEinfluss} />
<label className="text-xs text-[var(--color-ink-muted)]">
Haltung
<select value={haltung} onChange={(e) => setHaltung(e.target.value as Haltung)} className={inputCls}>
<select
value={haltung}
onChange={(e) => setHaltung(e.target.value as Haltung)}
className={inputCls}
>
{HALTUNGEN.map((h) => (
<option key={h} value={h}>
{HALTUNG_META[h].label}
@@ -1201,7 +1413,12 @@ function AddStakeholderForm({ projectId, onDone }: { projectId: string; onDone:
</select>
</label>
</div>
<input value={beduerfnis} onChange={(e) => setBeduerfnis(e.target.value)} placeholder="Bedürfnis / was braucht die Person?" className={inputCls} />
<input
value={beduerfnis}
onChange={(e) => setBeduerfnis(e.target.value)}
placeholder="Bedürfnis / was braucht die Person?"
className={inputCls}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={onDone}>
Abbrechen
@@ -1226,7 +1443,11 @@ function LevelSelect({
return (
<label className="text-xs text-[var(--color-ink-muted)]">
{label}
<select value={value} onChange={(e) => onChange(e.target.value as Level)} className={inputCls}>
<select
value={value}
onChange={(e) => onChange(e.target.value as Level)}
className={inputCls}
>
{LEVELS.map((l) => (
<option key={l} value={l}>
{LEVEL_META[l].label}
@@ -1256,7 +1477,13 @@ function LeadsSection({ projectId }: { projectId: string }) {
<Card.Root>
<Card.Header className="flex items-center justify-between">
<h2 className="text-lg font-semibold">
Leads{list.length > 0 && <span className="text-sm font-normal text-[var(--color-ink-muted)]"> ({list.length})</span>}
Leads
{list.length > 0 && (
<span className="text-sm font-normal text-[var(--color-ink-muted)]">
{' '}
({list.length})
</span>
)}
</h2>
<div className="flex items-center gap-3">
<Link
@@ -1290,13 +1517,23 @@ function LeadsSection({ projectId }: { projectId: string }) {
>
<span className="flex items-center gap-2">
<span
className={clsx('h-2 w-2 rounded-full', LEAD_STATUS_META[l.status as LeadStatus].dot)}
className={clsx(
'h-2 w-2 rounded-full',
LEAD_STATUS_META[l.status as LeadStatus].dot,
)}
/>
<span className="font-medium">{l.firma}</span>
{l.branche && <span className="text-xs text-[var(--color-ink-muted)]">{l.branche}</span>}
{l.branche && (
<span className="text-xs text-[var(--color-ink-muted)]">{l.branche}</span>
)}
</span>
<span className="flex items-center gap-2 text-xs text-[var(--color-ink-muted)]">
<span className={clsx('rounded-full px-2 py-0.5', LEAD_STATUS_META[l.status as LeadStatus].badge)}>
<span
className={clsx(
'rounded-full px-2 py-0.5',
LEAD_STATUS_META[l.status as LeadStatus].badge,
)}
>
{LEAD_STATUS_META[l.status as LeadStatus].label}
</span>
<span className="font-mono tabular-nums">{l.score}/10</span>
@@ -1378,7 +1615,13 @@ function RisksSection({ projectId }: { projectId: string }) {
) : (
<div className="space-y-2">
{risiken.map((r) => (
<RiskRow key={r.id} projectId={projectId} risk={r} users={users} userMap={userMap} />
<RiskRow
key={r.id}
projectId={projectId}
risk={r}
users={users}
userMap={userMap}
/>
))}
</div>
)}
@@ -1390,7 +1633,13 @@ function RisksSection({ projectId }: { projectId: string }) {
) : (
<div className="space-y-2">
{chancen.map((r) => (
<RiskRow key={r.id} projectId={projectId} risk={r} users={users} userMap={userMap} />
<RiskRow
key={r.id}
projectId={projectId}
risk={r}
users={users}
userMap={userMap}
/>
))}
</div>
)}
@@ -1457,13 +1706,23 @@ function RiskRow({
});
}}
>
<textarea value={title} onChange={(e) => setTitle(e.target.value)} rows={2} placeholder="Ganzer Satz" className={inputCls} />
<textarea
value={title}
onChange={(e) => setTitle(e.target.value)}
rows={2}
placeholder="Ganzer Satz"
className={inputCls}
/>
<div className="grid grid-cols-2 gap-2">
<LevelSelect label="Wahrscheinlichkeit" value={wahr} onChange={setWahr} />
<LevelSelect label="Auswirkung" value={ausw} onChange={setAusw} />
<label className="text-xs text-[var(--color-ink-muted)]">
Herkunft
<select value={herkunft} onChange={(e) => setHerkunft(e.target.value)} className={inputCls}>
<select
value={herkunft}
onChange={(e) => setHerkunft(e.target.value)}
className={inputCls}
>
<option value="">—</option>
<option value="intern">Intern</option>
<option value="extern">Extern</option>
@@ -1471,7 +1730,11 @@ function RiskRow({
</label>
<label className="text-xs text-[var(--color-ink-muted)]">
Status
<select value={status} onChange={(e) => setStatus(e.target.value as RiskStatus)} className={inputCls}>
<select
value={status}
onChange={(e) => setStatus(e.target.value as RiskStatus)}
className={inputCls}
>
{RISK_STATUSES.map((st) => (
<option key={st} value={st}>
{RISK_STATUS_LABEL[st]}
@@ -1484,7 +1747,11 @@ function RiskRow({
{!isChance && (
<label className="text-xs text-[var(--color-ink-muted)]">
Strategie
<select value={strategie} onChange={(e) => setStrategie(e.target.value)} className={inputCls}>
<select
value={strategie}
onChange={(e) => setStrategie(e.target.value)}
className={inputCls}
>
<option value="">—</option>
{RISK_STRATEGIEN.map((st) => (
<option key={st} value={st}>
@@ -1506,10 +1773,19 @@ function RiskRow({
</select>
</label>
</div>
<input value={massnahme} onChange={(e) => setMassnahme(e.target.value)} placeholder={isChance ? 'Wie nutzen wir die Chance?' : 'Gegenmaßnahme'} className={inputCls} />
<input
value={massnahme}
onChange={(e) => setMassnahme(e.target.value)}
placeholder={isChance ? 'Wie nutzen wir die Chance?' : 'Gegenmaßnahme'}
className={inputCls}
/>
{!isChance && (
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={showstopper} onChange={(e) => setShowstopper(e.target.checked)} />
<input
type="checkbox"
checked={showstopper}
onChange={(e) => setShowstopper(e.target.checked)}
/>
Showstopper (bringt das Projekt zum Stillstand)
</label>
)}
@@ -1529,9 +1805,7 @@ function RiskRow({
<div
className={clsx(
'rounded-[var(--radius-md)] border p-3 text-sm',
r.showstopper
? 'border-[var(--color-ampel-red-fg)]'
: 'border-[var(--color-border)]',
r.showstopper ? 'border-[var(--color-ampel-red-fg)]' : 'border-[var(--color-border)]',
)}
>
<div className="flex items-start justify-between gap-2">
@@ -1544,16 +1818,31 @@ function RiskRow({
{r.title}
</p>
<div className="flex shrink-0 gap-1">
<button type="button" onClick={() => setEditing(true)} aria-label="Bearbeiten" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]">
<button
type="button"
onClick={() => setEditing(true)}
aria-label="Bearbeiten"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]"
>
<Pencil size={14} />
</button>
<button type="button" onClick={() => remove.mutate({ id: r.id })} aria-label="Entfernen" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]">
<button
type="button"
onClick={() => remove.mutate({ id: r.id })}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
<Trash2 size={14} />
</button>
</div>
</div>
<div className="mt-1.5 flex flex-wrap gap-1 text-xs">
<span className={clsx('rounded-full px-2 py-0.5', LEVEL_META[r.wahrscheinlichkeit as Level].badge)}>
<span
className={clsx(
'rounded-full px-2 py-0.5',
LEVEL_META[r.wahrscheinlichkeit as Level].badge,
)}
>
W: {LEVEL_META[r.wahrscheinlichkeit as Level].label}
</span>
<span className={clsx('rounded-full px-2 py-0.5', LEVEL_META[r.auswirkung as Level].badge)}>
@@ -1628,7 +1917,8 @@ function AddRiskForm({
auswirkung: ausw,
showstopper: kind === 'risiko' ? showstopper : false,
massnahme: massnahme.trim() || undefined,
strategie: kind === 'risiko' && strategie !== '' ? (strategie as RiskStrategie) : undefined,
strategie:
kind === 'risiko' && strategie !== '' ? (strategie as RiskStrategie) : undefined,
ownerUserId: owner === '' ? undefined : owner,
});
}}
@@ -1638,7 +1928,11 @@ function AddRiskForm({
value={title}
onChange={(e) => setTitle(e.target.value)}
rows={2}
placeholder={kind === 'risiko' ? 'z. B. „Interne Ressourcen werden abgezogen.“' : 'z. B. „Kunde will weitere Standorte anbinden.“'}
placeholder={
kind === 'risiko'
? 'z. B. „Interne Ressourcen werden abgezogen.“'
: 'z. B. „Kunde will weitere Standorte anbinden.“'
}
className={inputCls}
/>
<div className="grid grid-cols-2 gap-2">
@@ -1649,7 +1943,11 @@ function AddRiskForm({
{kind === 'risiko' && (
<label className="text-xs text-[var(--color-ink-muted)]">
Strategie
<select value={strategie} onChange={(e) => setStrategie(e.target.value)} className={inputCls}>
<select
value={strategie}
onChange={(e) => setStrategie(e.target.value)}
className={inputCls}
>
<option value="">—</option>
{RISK_STRATEGIEN.map((st) => (
<option key={st} value={st}>
@@ -1671,10 +1969,19 @@ function AddRiskForm({
</select>
</label>
</div>
<input value={massnahme} onChange={(e) => setMassnahme(e.target.value)} placeholder={kind === 'risiko' ? 'Gegenmaßnahme (optional)' : 'Wie nutzen? (optional)'} className={inputCls} />
<input
value={massnahme}
onChange={(e) => setMassnahme(e.target.value)}
placeholder={kind === 'risiko' ? 'Gegenmaßnahme (optional)' : 'Wie nutzen? (optional)'}
className={inputCls}
/>
{kind === 'risiko' && (
<label className="flex items-center gap-2 text-sm">
<input type="checkbox" checked={showstopper} onChange={(e) => setShowstopper(e.target.checked)} />
<input
type="checkbox"
checked={showstopper}
onChange={(e) => setShowstopper(e.target.checked)}
/>
Showstopper
</label>
)}
@@ -1780,16 +2087,41 @@ function MilestoneRow({ projectId, milestone: m }: { projectId: string; mileston
}}
>
<div className="grid grid-cols-[1fr_auto_auto] gap-2">
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Titel *" className={inputCls} />
<input type="date" value={due} onChange={(e) => setDue(e.target.value)} className={inputCls} />
<select value={status} onChange={(e) => setStatus(e.target.value as Milestone['status'])} className={inputCls}>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Titel *"
className={inputCls}
/>
<input
type="date"
value={due}
onChange={(e) => setDue(e.target.value)}
className={inputCls}
/>
<select
value={status}
onChange={(e) => setStatus(e.target.value as Milestone['status'])}
className={inputCls}
>
<option value="open">Offen</option>
<option value="done">Erledigt</option>
<option value="overdue">Überfällig</option>
</select>
</div>
<textarea value={ergebnis} onChange={(e) => setErgebnis(e.target.value)} rows={2} placeholder="Ergebnis-Übersicht („Patchnotes“)" className={inputCls} />
<input value={kundennutzen} onChange={(e) => setKundennutzen(e.target.value)} placeholder="Konkreter Kundennutzen" className={inputCls} />
<textarea
value={ergebnis}
onChange={(e) => setErgebnis(e.target.value)}
rows={2}
placeholder="Ergebnis-Übersicht („Patchnotes“)"
className={inputCls}
/>
<input
value={kundennutzen}
onChange={(e) => setKundennutzen(e.target.value)}
placeholder="Konkreter Kundennutzen"
className={inputCls}
/>
<div className="flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={() => setEditing(false)}>
Abbrechen
@@ -1806,11 +2138,17 @@ function MilestoneRow({ projectId, milestone: m }: { projectId: string; mileston
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
<div className="flex items-start justify-between gap-2">
<div className="flex-1">
<p className={clsx('font-medium', m.status === 'done' && 'text-[var(--color-ink-muted)] line-through')}>
<p
className={clsx(
'font-medium',
m.status === 'done' && 'text-[var(--color-ink-muted)] line-through',
)}
>
{m.title}
</p>
<p className="text-xs text-[var(--color-ink-muted)]">
{m.due ?? 'kein Datum'} · {m.status === 'done' ? 'erledigt' : m.status === 'overdue' ? 'überfällig' : 'offen'}
{m.due ?? 'kein Datum'} ·{' '}
{m.status === 'done' ? 'erledigt' : m.status === 'overdue' ? 'überfällig' : 'offen'}
</p>
{warn.length > 0 && (
<p className="mt-1 flex items-start gap-1.5 text-xs text-[var(--color-ampel-amber-fg)]">
@@ -1820,14 +2158,26 @@ function MilestoneRow({ projectId, milestone: m }: { projectId: string; mileston
)}
{m.ergebnis && <p className="mt-1 text-xs">Ergebnis: {m.ergebnis}</p>}
{m.kundennutzen && (
<p className="mt-0.5 text-xs text-[var(--color-ink-muted)]">Kundennutzen: {m.kundennutzen}</p>
<p className="mt-0.5 text-xs text-[var(--color-ink-muted)]">
Kundennutzen: {m.kundennutzen}
</p>
)}
</div>
<div className="flex shrink-0 gap-1">
<button type="button" onClick={() => setEditing(true)} aria-label="Bearbeiten" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]">
<button
type="button"
onClick={() => setEditing(true)}
aria-label="Bearbeiten"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ink)]"
>
<Pencil size={14} />
</button>
<button type="button" onClick={() => remove.mutate({ milestoneId: m.id })} aria-label="Entfernen" className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]">
<button
type="button"
onClick={() => remove.mutate({ milestoneId: m.id })}
aria-label="Entfernen"
className="text-[var(--color-ink-faint)] hover:text-[var(--color-ampel-red-fg)]"
>
<Trash2 size={14} />
</button>
</div>
@@ -1857,8 +2207,18 @@ function AddMilestoneForm({ projectId, onDone }: { projectId: string; onDone: ()
}}
>
<div className="grid grid-cols-[1fr_auto] gap-2">
<input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Titel *" className={inputCls} />
<input type="date" value={due} onChange={(e) => setDue(e.target.value)} className={inputCls} />
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Titel *"
className={inputCls}
/>
<input
type="date"
value={due}
onChange={(e) => setDue(e.target.value)}
className={inputCls}
/>
</div>
{warn.length > 0 && (
<p className="flex items-start gap-1.5 text-xs text-[var(--color-ampel-amber-fg)]">
@@ -1903,7 +2263,11 @@ function AbschlussDialog({
});
function submit(e: React.FormEvent) {
e.preventDefault();
mutation.mutate({ projectId, lessonsLearnedUrl: url, abschlussvermerk: vermerk.trim() || undefined });
mutation.mutate({
projectId,
lessonsLearnedUrl: url,
abschlussvermerk: vermerk.trim() || undefined,
});
}
const checkItems: Array<{ key: keyof typeof checks; label: string }> = [
{ key: 'abnahme', label: 'Abnahme — Liefergegenstände ohne offene Beanstandung abgenommen' },
@@ -1974,13 +2338,23 @@ function AbschlussDialog({
}
const LINK_FIELDS = [
{ key: 'outlineSteckbriefUrl', label: 'Outline-Steckbrief', placeholder: 'https://wiki.tool-ctrl.de/doc/…' },
{
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 }) {
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>>({});
@@ -2032,7 +2406,9 @@ function VerlinkungenSection({ projectId, project: p }: { projectId: string; pro
/>
</label>
))}
<p className="text-xs text-[var(--color-ink-faint)]">Leer lassen entfernt den Link. URLs müssen mit http(s):// beginnen.</p>
<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