Feature: Lessons-Learned-Vorlage beim Projekt-Abschluss
Der Abschluss verlangte eine URL + freie Vermerk-Textarea, half aber nicht beim Strukturieren der Retro. Jetzt gefuehrte Felder (Ziel erreicht? / Aufwand vs. Plan / was lief gut / was nicht), gespeichert als JSONB project.lessons_learned (Migration 0018). Anzeige im Projekt-Detail als "Retrospektive"; Altprojekte fallen auf den freien Abschlussvermerk zurueck. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
3e66ba3156
commit
62945201b1
7 files changed
+3443
-20
No files matched your search
@@ -137,6 +137,7 @@ export const projectRouter = router({
|
||||
phase: 'abschluss',
|
||||
lessonsLearnedUrl: input.lessonsLearnedUrl,
|
||||
abschlussvermerk: input.abschlussvermerk ?? p.abschlussvermerk,
|
||||
lessonsLearned: input.lessonsLearned ?? p.lessonsLearned,
|
||||
pflichtMinimumState: { ...p.pflichtMinimumState, retrospektive: true },
|
||||
})
|
||||
.where(eq(project.id, input.projectId))
|
||||
|
||||
@@ -143,7 +143,7 @@ export function ProjektRoute() {
|
||||
{abschlussOpen && (
|
||||
<AbschlussDialog
|
||||
projectId={p.id}
|
||||
abschlussvermerk={p.abschlussvermerk}
|
||||
lessonsLearned={p.lessonsLearned}
|
||||
onClose={() => setAbschlussOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -205,15 +205,29 @@ export function ProjektRoute() {
|
||||
</Card.Root>
|
||||
)}
|
||||
|
||||
{p.abschlussvermerk && (
|
||||
{p.lessonsLearned ? (
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<h2 className="text-lg font-semibold">Abschlussvermerk</h2>
|
||||
<h2 className="text-lg font-semibold">Retrospektive</h2>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<p className="whitespace-pre-wrap text-sm">{p.abschlussvermerk}</p>
|
||||
<Card.Body className="space-y-3 text-sm">
|
||||
<RetroField label="Ziel erreicht?" value={p.lessonsLearned.zielErreicht} />
|
||||
<RetroField label="Aufwand vs. Plan" value={p.lessonsLearned.aufwand} />
|
||||
<RetroField label="Was lief gut?" value={p.lessonsLearned.liefGut} />
|
||||
<RetroField label="Was lief nicht gut?" value={p.lessonsLearned.liefSchlecht} />
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
) : (
|
||||
p.abschlussvermerk && (
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<h2 className="text-lg font-semibold">Abschlussvermerk</h2>
|
||||
</Card.Header>
|
||||
<Card.Body>
|
||||
<p className="whitespace-pre-wrap text-sm">{p.abschlussvermerk}</p>
|
||||
</Card.Body>
|
||||
</Card.Root>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -2305,17 +2319,37 @@ function AddMilestoneForm({ projectId, onDone }: { projectId: string; onDone: ()
|
||||
);
|
||||
}
|
||||
|
||||
function RetroField({ label, value }: { label: string; value: string }) {
|
||||
if (!value?.trim()) return null;
|
||||
return (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-[var(--color-ink-muted)]">{label}</p>
|
||||
<p className="whitespace-pre-wrap">{value}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AbschlussDialog({
|
||||
projectId,
|
||||
abschlussvermerk,
|
||||
lessonsLearned,
|
||||
onClose,
|
||||
}: {
|
||||
projectId: string;
|
||||
abschlussvermerk: string | null;
|
||||
lessonsLearned: {
|
||||
zielErreicht: string;
|
||||
aufwand: string;
|
||||
liefGut: string;
|
||||
liefSchlecht: string;
|
||||
} | null;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const [url, setUrl] = useState('');
|
||||
const [vermerk, setVermerk] = useState(abschlussvermerk ?? '');
|
||||
const [ll, setLl] = useState({
|
||||
zielErreicht: lessonsLearned?.zielErreicht ?? '',
|
||||
aufwand: lessonsLearned?.aufwand ?? '',
|
||||
liefGut: lessonsLearned?.liefGut ?? '',
|
||||
liefSchlecht: lessonsLearned?.liefSchlecht ?? '',
|
||||
});
|
||||
const [checks, setChecks] = useState({ abnahme: false, aufraeumen: false, retro: false });
|
||||
const navigate = useNavigate();
|
||||
const utils = trpc.useUtils();
|
||||
@@ -2330,10 +2364,11 @@ function AbschlussDialog({
|
||||
});
|
||||
function submit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
const hasLl = Object.values(ll).some((v) => v.trim());
|
||||
mutation.mutate({
|
||||
projectId,
|
||||
lessonsLearnedUrl: url,
|
||||
abschlussvermerk: vermerk.trim() || undefined,
|
||||
lessonsLearned: hasLl ? ll : undefined,
|
||||
});
|
||||
}
|
||||
const checkItems: Array<{ key: keyof typeof checks; label: string }> = [
|
||||
@@ -2360,17 +2395,28 @@ function AbschlussDialog({
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Abschlussvermerk</span>
|
||||
<textarea
|
||||
value={vermerk}
|
||||
onChange={(e) => setVermerk(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={2000}
|
||||
placeholder="Ziel erreicht? Aufwand gegen Plan? Wichtigste Erkenntnisse — eine halbe Seite, kein Roman."
|
||||
className={clsx(inputCls, 'mt-1')}
|
||||
/>
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium">Retrospektive</p>
|
||||
{(
|
||||
[
|
||||
['zielErreicht', 'Ziel erreicht?'],
|
||||
['aufwand', 'Aufwand vs. Plan'],
|
||||
['liefGut', 'Was lief gut?'],
|
||||
['liefSchlecht', 'Was lief nicht gut?'],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<label key={key} className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">{label}</span>
|
||||
<textarea
|
||||
value={ll[key]}
|
||||
onChange={(e) => setLl((s) => ({ ...s, [key]: e.target.value }))}
|
||||
rows={2}
|
||||
maxLength={2000}
|
||||
className={clsx(inputCls, 'mt-0.5')}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Lessons-Learned-URL (Outline)</span>
|
||||
<input
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE "project" ADD COLUMN "lessons_learned" jsonb;
|
||||
File diff suppressed because it is too large.
Load diff
@@ -127,6 +127,13 @@
|
||||
"when": 1780826387755,
|
||||
"tag": "0017_rare_oracle",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 18,
|
||||
"version": "7",
|
||||
"when": 1780828837324,
|
||||
"tag": "0018_last_lake",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -166,6 +166,14 @@ export const project = pgTable(
|
||||
lessonsLearnedUrl: text('lessons_learned_url'),
|
||||
// Abschluss (Leitfaden Phase 4): kurzer Abschlussvermerk — Ziel erreicht? Aufwand vs. Plan?
|
||||
abschlussvermerk: text('abschlussvermerk'),
|
||||
// Strukturierte Retrospektive (Lessons-Learned-Vorlage). Ersetzt die freie
|
||||
// Vermerk-Textarea durch geführte Felder; alte Einträge nutzen abschlussvermerk.
|
||||
lessonsLearned: jsonb('lessons_learned').$type<{
|
||||
zielErreicht: string;
|
||||
aufwand: string;
|
||||
liefGut: string;
|
||||
liefSchlecht: string;
|
||||
} | null>(),
|
||||
createdBy: uuid('created_by').references(() => user.id, { onDelete: 'set null' }),
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
|
||||
@@ -44,10 +44,20 @@ export const createProjectInput = z.object({
|
||||
});
|
||||
export type CreateProjectInput = z.infer<typeof createProjectInput>;
|
||||
|
||||
export const lessonsLearnedSchema = z.object({
|
||||
zielErreicht: z.string().max(2000),
|
||||
aufwand: z.string().max(2000),
|
||||
liefGut: z.string().max(2000),
|
||||
liefSchlecht: z.string().max(2000),
|
||||
});
|
||||
export type LessonsLearned = z.infer<typeof lessonsLearnedSchema>;
|
||||
|
||||
export const archiveProjectInput = z.object({
|
||||
projectId: z.string().uuid(),
|
||||
lessonsLearnedUrl: z.string().url(),
|
||||
abschlussvermerk: z.string().max(2000).optional(),
|
||||
// Strukturierte Retrospektive (Lessons-Learned-Vorlage), optional.
|
||||
lessonsLearned: lessonsLearnedSchema.optional(),
|
||||
});
|
||||
export type ArchiveProjectInput = z.infer<typeof archiveProjectInput>;
|
||||
|
||||
|
||||
Reference in new issue
Block a user