UI: alle 8 Dialoge auf barrierearme Modal-Basis umgestellt
Bisher waren alle Overlay-Dialoge plain divs ohne role/Fokus-Trap/Escape. Jetzt setzen sie auf <Modal> auf (role=dialog, aria-modal, Fokus-Trap + -Rueckgabe, Escape, Body-Scroll-Lock, konsistenter X-Button, Scroll bei langem Inhalt). - Modal: X-Schliessen-Button + max-h-[90vh]/overflow-y-auto ergaenzt - BlockerDialog, ManualDialog (Timeline) - leads: CSV-Import + Neuer Lead - bereich: Neues Projekt - projekt: Gate-Review + Projekt abschliessen - lead: Zu Deal machen Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
30006e3b32
commit
f600c9f187
7 files changed
+489
-566
No files matched your search
@@ -3,6 +3,7 @@ import { toast } from 'sonner';
|
||||
import { trpc } from '../../lib/trpc';
|
||||
import { voice } from '../../lib/voice';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Modal } from '../ui/Modal';
|
||||
|
||||
export function BlockerDialog({ onClose }: { onClose: () => void }) {
|
||||
const [title, setTitle] = useState('');
|
||||
@@ -23,49 +24,40 @@ export function BlockerDialog({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
|
||||
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()}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Neuer Blocker</h2>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Was klemmt?</span>
|
||||
<input
|
||||
required
|
||||
ref={(el) => {
|
||||
if (el && !title) el.focus();
|
||||
}}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
maxLength={200}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Mehr Kontext (optional)</span>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={1000}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Erstellen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Modal open onClose={onClose} title="Neuer Blocker">
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Was klemmt?</span>
|
||||
<input
|
||||
required
|
||||
ref={(el) => {
|
||||
if (el && !title) el.focus();
|
||||
}}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
maxLength={200}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Mehr Kontext (optional)</span>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
rows={3}
|
||||
maxLength={1000}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Erstellen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { Pencil, Trash2, Plus, X } from 'lucide-react';
|
||||
import { Pencil, Trash2, Plus } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { clsx } from 'clsx';
|
||||
import { trpc } from '../../lib/trpc';
|
||||
import { Card } from '../ui/Card';
|
||||
import { Button } from '../ui/Button';
|
||||
import { Modal } from '../ui/Modal';
|
||||
import { formatDuration, formatDurationCompact } from '../../lib/format-duration';
|
||||
import { TIME_CATEGORIES, type TimeCategory } from '../../lib/time-categories';
|
||||
|
||||
@@ -333,97 +334,78 @@ export function ManualDialog({
|
||||
}
|
||||
|
||||
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="flex items-center justify-between">
|
||||
<h2 className="text-xl font-semibold">Block nachtragen</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Schließen"
|
||||
className="rounded-[var(--radius-sm)] p-1 text-[var(--color-ink-muted)] hover:bg-black/5"
|
||||
>
|
||||
<X size={16} />
|
||||
</button>
|
||||
</div>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Start</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={startedAt}
|
||||
onChange={(e) => setStartedAt(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Ende</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={endedAt}
|
||||
onChange={(e) => setEndedAt(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<Modal open onClose={onClose} title="Block nachtragen">
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Bezug auf ToDo (optional)</span>
|
||||
<select
|
||||
value={todoId}
|
||||
onChange={(e) => setTodoId(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
>
|
||||
<option value="">— kein ToDo —</option>
|
||||
{todos.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Kategorie</span>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as TimeCategory)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
>
|
||||
{TIME_CATEGORIES.map((c) => (
|
||||
<option key={c.value} value={c.value}>
|
||||
{c.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Beschreibung (optional)</span>
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Start</span>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={200}
|
||||
type="datetime-local"
|
||||
required
|
||||
value={startedAt}
|
||||
onChange={(e) => setStartedAt(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Ende</span>
|
||||
<input
|
||||
type="datetime-local"
|
||||
required
|
||||
value={endedAt}
|
||||
onChange={(e) => setEndedAt(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Bezug auf ToDo (optional)</span>
|
||||
<select
|
||||
value={todoId}
|
||||
onChange={(e) => setTodoId(e.target.value)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
>
|
||||
<option value="">— kein ToDo —</option>
|
||||
{todos.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
{t.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Kategorie</span>
|
||||
<select
|
||||
value={category}
|
||||
onChange={(e) => setCategory(e.target.value as TimeCategory)}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
>
|
||||
{TIME_CATEGORIES.map((c) => (
|
||||
<option key={c.value} value={c.value}>
|
||||
{c.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-xs text-[var(--color-ink-muted)]">Beschreibung (optional)</span>
|
||||
<input
|
||||
type="text"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={200}
|
||||
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useId, useRef, type ReactNode } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { X } from 'lucide-react';
|
||||
|
||||
type Size = 'sm' | 'md' | 'lg';
|
||||
|
||||
@@ -94,10 +95,18 @@ export function Modal({
|
||||
aria-modal="true"
|
||||
aria-labelledby={title != null ? titleId : undefined}
|
||||
tabIndex={-1}
|
||||
className={`w-full ${maxWidth[size]} rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)] focus:outline-none`}
|
||||
className={`relative max-h-[90vh] w-full ${maxWidth[size]} overflow-y-auto rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)] focus:outline-none`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
aria-label="Schließen"
|
||||
className="absolute right-4 top-4 rounded-[var(--radius-sm)] p-1 text-[var(--color-ink-muted)] hover:bg-black/5 hover:text-[var(--color-ink)]"
|
||||
>
|
||||
<X size={18} />
|
||||
</button>
|
||||
{title != null && (
|
||||
<h2 id={titleId} className="text-xl font-semibold">
|
||||
<h2 id={titleId} className="pr-8 text-xl font-semibold">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { trpc } from '../lib/trpc';
|
||||
import { voice } from '../lib/voice';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Modal } from '../components/ui/Modal';
|
||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||
import { toast } from 'sonner';
|
||||
import { clsx } from 'clsx';
|
||||
@@ -220,95 +221,86 @@ function ProjectCreateDialog({
|
||||
});
|
||||
}
|
||||
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()}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Neues Projekt</h2>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<Field label="Name">
|
||||
<input
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Beschreibung (optional)">
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={2000}
|
||||
rows={3}
|
||||
placeholder="Worum geht's? Kurz beschreiben."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Logo (optional)">
|
||||
<LogoUpload value={logoUrl} name={name} onChange={setLogoUrl} />
|
||||
</Field>
|
||||
<Field label="Bereich">
|
||||
<select
|
||||
required
|
||||
value={areaId}
|
||||
onChange={(e) => setAreaId(e.target.value)}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{areas.length ? '— Bereich wählen —' : 'Bereiche werden geladen…'}
|
||||
<Modal open onClose={onClose} title="Neues Projekt">
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<Field label="Name">
|
||||
<input
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Beschreibung (optional)">
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
maxLength={2000}
|
||||
rows={3}
|
||||
placeholder="Worum geht's? Kurz beschreiben."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Logo (optional)">
|
||||
<LogoUpload value={logoUrl} name={name} onChange={setLogoUrl} />
|
||||
</Field>
|
||||
<Field label="Bereich">
|
||||
<select
|
||||
required
|
||||
value={areaId}
|
||||
onChange={(e) => setAreaId(e.target.value)}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="" disabled>
|
||||
{areas.length ? '— Bereich wählen —' : 'Bereiche werden geladen…'}
|
||||
</option>
|
||||
{areas.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
{areas.map((a) => (
|
||||
<option key={a.id} value={a.id}>
|
||||
{a.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Projektart">
|
||||
<select
|
||||
value={kind}
|
||||
onChange={(e) => setKind(e.target.value as 'kundenprojekt' | 'internes_projekt')}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="kundenprojekt">Kundenprojekt</option>
|
||||
<option value="internes_projekt">Internes Projekt</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Outline-Steckbrief-URL">
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={steckbrief}
|
||||
onChange={(e) => setSteckbrief(e.target.value)}
|
||||
placeholder="https://outline.tool-ctrl.de/..."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Plane-Board-URL">
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={plane}
|
||||
onChange={(e) => setPlane(e.target.value)}
|
||||
placeholder="https://plane.tool-ctrl.de/..."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={create.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Projektart">
|
||||
<select
|
||||
value={kind}
|
||||
onChange={(e) => setKind(e.target.value as 'kundenprojekt' | 'internes_projekt')}
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="kundenprojekt">Kundenprojekt</option>
|
||||
<option value="internes_projekt">Internes Projekt</option>
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Outline-Steckbrief-URL">
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={steckbrief}
|
||||
onChange={(e) => setSteckbrief(e.target.value)}
|
||||
placeholder="https://outline.tool-ctrl.de/..."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Plane-Board-URL">
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={plane}
|
||||
onChange={(e) => setPlane(e.target.value)}
|
||||
placeholder="https://plane.tool-ctrl.de/..."
|
||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={create.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||
import { ErrorState } from '../components/ui/ErrorState';
|
||||
import { Modal } from '../components/ui/Modal';
|
||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||
import {
|
||||
LEAD_STATUS_META,
|
||||
@@ -357,57 +358,48 @@ 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="w-full max-w-sm rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
<Modal open onClose={onClose} title="Zu Deal machen" size="sm">
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Legt einen Pipeline-Deal in Stufe „Lead“ an und verknüpft ihn.
|
||||
</p>
|
||||
<form
|
||||
className="mt-4 space-y-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
promote.mutate({ leadId, valueEur: Math.round(euro * 100), probability });
|
||||
}}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Zu Deal machen</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Legt einen Pipeline-Deal in Stufe „Lead“ an und verknüpft ihn.
|
||||
</p>
|
||||
<form
|
||||
className="mt-4 space-y-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
promote.mutate({ leadId, valueEur: Math.round(euro * 100), probability });
|
||||
}}
|
||||
>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Wert (EUR)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={euro}
|
||||
onChange={(e) => setEuro(Number(e.target.value))}
|
||||
className={clsx(inputCls, 'mt-1')}
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Wahrscheinlichkeit: {probability}%</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={probability}
|
||||
onChange={(e) => setProbability(Number(e.target.value))}
|
||||
className="mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={promote.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Wert (EUR)</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
value={euro}
|
||||
onChange={(e) => setEuro(Number(e.target.value))}
|
||||
className={clsx(inputCls, 'mt-1')}
|
||||
/>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Wahrscheinlichkeit: {probability}%</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={100}
|
||||
value={probability}
|
||||
onChange={(e) => setProbability(Number(e.target.value))}
|
||||
className="mt-1 w-full"
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={promote.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+155
-178
@@ -6,6 +6,7 @@ import { trpc } from '../lib/trpc';
|
||||
import { voice } from '../lib/voice';
|
||||
import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { Modal } from '../components/ui/Modal';
|
||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||
import { LEAD_STATUS_META, LEAD_STATUS_ORDER, type LeadStatus } from '../lib/lead-meta';
|
||||
|
||||
@@ -107,90 +108,79 @@ function LeadImportDialog({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
|
||||
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()}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Leads aus CSV importieren</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Spalten: Quelle, Vorname, Nachname, Position, Firma, Straße, PLZ, Ort, Telefon, Mobil,
|
||||
Fax, E-Mail, Website, Branche, Notizen. Je Zeile entsteht ein Lead mit primärem Kontakt;
|
||||
Notizen werden als erster Austausch-Log-Eintrag gespeichert. Duplikate (gleicher
|
||||
Firmenname) werden übersprungen.
|
||||
</p>
|
||||
<Modal open onClose={onClose} title="Leads aus CSV importieren">
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Spalten: Quelle, Vorname, Nachname, Position, Firma, Straße, PLZ, Ort, Telefon, Mobil, Fax,
|
||||
E-Mail, Website, Branche, Notizen. Je Zeile entsteht ein Lead mit primärem Kontakt; Notizen
|
||||
werden als erster Austausch-Log-Eintrag gespeichert. Duplikate (gleicher Firmenname) werden
|
||||
übersprungen.
|
||||
</p>
|
||||
|
||||
<div className="mt-4 space-y-3">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Projekt zuordnen (optional)</span>
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
disabled={!!result}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— kein Projekt —</option>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">CSV-Datei</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
onChange={onFile}
|
||||
className="mt-1 block w-full text-sm file:mr-3 file:rounded-[var(--radius-md)] file:border file:border-[var(--color-border)] file:bg-white file:px-3 file:py-1.5 file:text-sm"
|
||||
/>
|
||||
</label>
|
||||
{fileName && !result && (
|
||||
<p className="text-xs text-[var(--color-ink-muted)]">
|
||||
Bereit: {fileName} ({Math.max(0, csv.split('\n').filter((l) => l.trim()).length - 1)}{' '}
|
||||
Datenzeilen)
|
||||
<div className="mt-4 space-y-3">
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Projekt zuordnen (optional)</span>
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
disabled={!!result}
|
||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="">— kein Projekt —</option>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">CSV-Datei</span>
|
||||
<input
|
||||
type="file"
|
||||
accept=".csv,text/csv"
|
||||
onChange={onFile}
|
||||
className="mt-1 block w-full text-sm file:mr-3 file:rounded-[var(--radius-md)] file:border file:border-[var(--color-border)] file:bg-white file:px-3 file:py-1.5 file:text-sm"
|
||||
/>
|
||||
</label>
|
||||
{fileName && !result && (
|
||||
<p className="text-xs text-[var(--color-ink-muted)]">
|
||||
Bereit: {fileName} ({Math.max(0, csv.split('\n').filter((l) => l.trim()).length - 1)}{' '}
|
||||
Datenzeilen)
|
||||
</p>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
|
||||
<p className="font-medium text-[var(--color-ampel-green-fg)]">
|
||||
{result.created} Leads importiert
|
||||
</p>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
|
||||
<p className="font-medium text-[var(--color-ampel-green-fg)]">
|
||||
{result.created} Leads importiert
|
||||
</p>
|
||||
<ul className="mt-1 space-y-0.5 text-xs text-[var(--color-ink-muted)]">
|
||||
<li>{result.contacts} Kontakte angelegt</li>
|
||||
<li>{result.touchpoints} Notizen als Log-Eintrag</li>
|
||||
{result.skippedDuplicate > 0 && (
|
||||
<li>{result.skippedDuplicate} Duplikate übersprungen</li>
|
||||
)}
|
||||
{result.skippedEmpty > 0 && (
|
||||
<li>{result.skippedEmpty} leere Zeilen übersprungen</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
{result ? 'Schließen' : 'Abbrechen'}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => importMut.mutate({ csv, projectId: projectId || undefined })}
|
||||
disabled={!csv || importMut.isPending}
|
||||
>
|
||||
{importMut.isPending ? 'Importiere…' : 'Importieren'}
|
||||
</Button>
|
||||
)}
|
||||
<ul className="mt-1 space-y-0.5 text-xs text-[var(--color-ink-muted)]">
|
||||
<li>{result.contacts} Kontakte angelegt</li>
|
||||
<li>{result.touchpoints} Notizen als Log-Eintrag</li>
|
||||
{result.skippedDuplicate > 0 && (
|
||||
<li>{result.skippedDuplicate} Duplikate übersprungen</li>
|
||||
)}
|
||||
{result.skippedEmpty > 0 && <li>{result.skippedEmpty} leere Zeilen übersprungen</li>}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
{result ? 'Schließen' : 'Abbrechen'}
|
||||
</Button>
|
||||
{!result && (
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => importMut.mutate({ csv, projectId: projectId || undefined })}
|
||||
disabled={!csv || importMut.isPending}
|
||||
>
|
||||
{importMut.isPending ? 'Importiere…' : 'Importieren'}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,121 +272,108 @@ function LeadCreateDialog({ onClose }: { onClose: () => void }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Neuer Lead</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Nur die Firma ist Pflicht — alles Weitere füllst du im Dossier.
|
||||
</p>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<Field label="Firma">
|
||||
<Modal open onClose={onClose} title="Neuer Lead">
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Nur die Firma ist Pflicht — alles Weitere füllst du im Dossier.
|
||||
</p>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<Field label="Firma">
|
||||
<input
|
||||
required
|
||||
value={firma}
|
||||
onChange={(e) => setFirma(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Branche">
|
||||
<input
|
||||
required
|
||||
value={firma}
|
||||
onChange={(e) => setFirma(e.target.value)}
|
||||
value={branche}
|
||||
onChange={(e) => setBranche(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Branche">
|
||||
<input
|
||||
value={branche}
|
||||
onChange={(e) => setBranche(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Nische / Sub-Segment">
|
||||
<input
|
||||
value={subSegment}
|
||||
onChange={(e) => setSubSegment(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label="Website">
|
||||
<Field label="Nische / Sub-Segment">
|
||||
<input
|
||||
value={website}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
value={subSegment}
|
||||
onChange={(e) => setSubSegment(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Quelle (wie gefunden)">
|
||||
<input
|
||||
value={quelle}
|
||||
onChange={(e) => setQuelle(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Projekt (optional)">
|
||||
</div>
|
||||
<Field label="Website">
|
||||
<input
|
||||
value={website}
|
||||
onChange={(e) => setWebsite(e.target.value)}
|
||||
className={inputCls}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Quelle (wie gefunden)">
|
||||
<input value={quelle} onChange={(e) => setQuelle(e.target.value)} className={inputCls} />
|
||||
</Field>
|
||||
<Field label="Projekt (optional)">
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
className={inputCls}
|
||||
>
|
||||
<option value="">— kein Projekt —</option>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Owner">
|
||||
<select
|
||||
value={projectId}
|
||||
onChange={(e) => setProjectId(e.target.value)}
|
||||
value={ownerUserId}
|
||||
onChange={(e) => setOwnerUserId(e.target.value)}
|
||||
className={inputCls}
|
||||
>
|
||||
<option value="">— kein Projekt —</option>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
<option value="">— niemand —</option>
|
||||
{(users.data ?? []).map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<Field label="Owner">
|
||||
<select
|
||||
value={ownerUserId}
|
||||
onChange={(e) => setOwnerUserId(e.target.value)}
|
||||
className={inputCls}
|
||||
>
|
||||
<option value="">— niemand —</option>
|
||||
{(users.data ?? []).map((u) => (
|
||||
<option key={u.id} value={u.id}>
|
||||
{u.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Status">
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value as LeadStatus)}
|
||||
className={inputCls}
|
||||
>
|
||||
{LEAD_STATUS_ORDER.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{LEAD_STATUS_META[s].label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
</div>
|
||||
<Field label={`Score: ${score}/10`}>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={10}
|
||||
value={score}
|
||||
onChange={(e) => setScore(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
<Field label="Status">
|
||||
<select
|
||||
value={status}
|
||||
onChange={(e) => setStatus(e.target.value as LeadStatus)}
|
||||
className={inputCls}
|
||||
>
|
||||
{LEAD_STATUS_ORDER.map((s) => (
|
||||
<option key={s} value={s}>
|
||||
{LEAD_STATUS_META[s].label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={create.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Field label={`Score: ${score}/10`}>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={10}
|
||||
value={score}
|
||||
onChange={(e) => setScore(Number(e.target.value))}
|
||||
className="w-full"
|
||||
/>
|
||||
</Field>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={create.isPending}>
|
||||
Anlegen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+95
-116
@@ -8,6 +8,7 @@ import { Card } from '../components/ui/Card';
|
||||
import { Button } from '../components/ui/Button';
|
||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||
import { ErrorState } from '../components/ui/ErrorState';
|
||||
import { Modal } from '../components/ui/Modal';
|
||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||
import { useCurrentUser } from '../hooks/useCurrentUser';
|
||||
import { voice } from '../lib/voice';
|
||||
@@ -704,65 +705,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()}
|
||||
<Modal open onClose={onClose} title={`Gate-Review: ${PHASE_META[phase].label}`}>
|
||||
<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,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
<form
|
||||
className="mt-4 space-y-3"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
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)}
|
||||
/>
|
||||
Ergebnis erreicht
|
||||
</label>
|
||||
<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={erreicht}
|
||||
onChange={(e) => setErreicht(e.target.checked)}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={advance}
|
||||
onChange={(e) => setAdvance(e.target.checked)}
|
||||
/>
|
||||
Folgephase jetzt freigeben
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={add.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Ergebnis erreicht
|
||||
</label>
|
||||
<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)} />
|
||||
Folgephase jetzt freigeben
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={add.isPending}>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2320,65 +2308,56 @@ function AbschlussDialog({
|
||||
{ key: 'retro', label: 'Retrospektive / Lessons Learned durchgeführt' },
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="max-h-[90vh] w-full max-w-md overflow-y-auto rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h2 className="text-xl font-semibold">Projekt abschließen</h2>
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Ein Projekt endet bewusst (Leitfaden Phase 4).
|
||||
</p>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<div className="space-y-1.5 rounded-[var(--radius-md)] border border-[var(--color-border)] p-3">
|
||||
{checkItems.map((it) => (
|
||||
<label key={it.key} className="flex items-start gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checks[it.key]}
|
||||
onChange={(e) => setChecks((c) => ({ ...c, [it.key]: e.target.checked }))}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
{it.label}
|
||||
</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>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Lessons-Learned-URL (Outline)</span>
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://outline.tool-ctrl.de/lessons-learned/..."
|
||||
className={clsx(inputCls, 'mt-1')}
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Abschließen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<Modal open onClose={onClose} title="Projekt abschließen">
|
||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||
Ein Projekt endet bewusst (Leitfaden Phase 4).
|
||||
</p>
|
||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||
<div className="space-y-1.5 rounded-[var(--radius-md)] border border-[var(--color-border)] p-3">
|
||||
{checkItems.map((it) => (
|
||||
<label key={it.key} className="flex items-start gap-2 text-sm">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checks[it.key]}
|
||||
onChange={(e) => setChecks((c) => ({ ...c, [it.key]: e.target.checked }))}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
{it.label}
|
||||
</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>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium">Lessons-Learned-URL (Outline)</span>
|
||||
<input
|
||||
required
|
||||
type="url"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
placeholder="https://outline.tool-ctrl.de/lessons-learned/..."
|
||||
className={clsx(inputCls, 'mt-1')}
|
||||
/>
|
||||
</label>
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button type="button" variant="ghost" onClick={onClose}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button type="submit" disabled={mutation.isPending}>
|
||||
Abschließen
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user