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 { trpc } from '../../lib/trpc';
|
||||||
import { voice } from '../../lib/voice';
|
import { voice } from '../../lib/voice';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
|
import { Modal } from '../ui/Modal';
|
||||||
|
|
||||||
export function BlockerDialog({ onClose }: { onClose: () => void }) {
|
export function BlockerDialog({ onClose }: { onClose: () => void }) {
|
||||||
const [title, setTitle] = useState('');
|
const [title, setTitle] = useState('');
|
||||||
@@ -23,49 +24,40 @@ export function BlockerDialog({ onClose }: { onClose: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Neuer Blocker">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||||
onClick={onClose}
|
<label className="block">
|
||||||
>
|
<span className="text-sm font-medium">Was klemmt?</span>
|
||||||
<div
|
<input
|
||||||
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
required
|
||||||
onClick={(e) => e.stopPropagation()}
|
ref={(el) => {
|
||||||
>
|
if (el && !title) el.focus();
|
||||||
<h2 className="text-xl font-semibold">Neuer Blocker</h2>
|
}}
|
||||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
value={title}
|
||||||
<label className="block">
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
<span className="text-sm font-medium">Was klemmt?</span>
|
maxLength={200}
|
||||||
<input
|
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
required
|
/>
|
||||||
ref={(el) => {
|
</label>
|
||||||
if (el && !title) el.focus();
|
<label className="block">
|
||||||
}}
|
<span className="text-sm font-medium">Mehr Kontext (optional)</span>
|
||||||
value={title}
|
<textarea
|
||||||
onChange={(e) => setTitle(e.target.value)}
|
value={description}
|
||||||
maxLength={200}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
rows={3}
|
||||||
/>
|
maxLength={1000}
|
||||||
</label>
|
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
<label className="block">
|
/>
|
||||||
<span className="text-sm font-medium">Mehr Kontext (optional)</span>
|
</label>
|
||||||
<textarea
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
value={description}
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
Abbrechen
|
||||||
rows={3}
|
</Button>
|
||||||
maxLength={1000}
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
Erstellen
|
||||||
/>
|
</Button>
|
||||||
</label>
|
</div>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
</form>
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
</Modal>
|
||||||
Abbrechen
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={mutation.isPending}>
|
|
||||||
Erstellen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Pencil, Trash2, Plus, X } from 'lucide-react';
|
import { Pencil, Trash2, Plus } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { trpc } from '../../lib/trpc';
|
import { trpc } from '../../lib/trpc';
|
||||||
import { Card } from '../ui/Card';
|
import { Card } from '../ui/Card';
|
||||||
import { Button } from '../ui/Button';
|
import { Button } from '../ui/Button';
|
||||||
|
import { Modal } from '../ui/Modal';
|
||||||
import { formatDuration, formatDurationCompact } from '../../lib/format-duration';
|
import { formatDuration, formatDurationCompact } from '../../lib/format-duration';
|
||||||
import { TIME_CATEGORIES, type TimeCategory } from '../../lib/time-categories';
|
import { TIME_CATEGORIES, type TimeCategory } from '../../lib/time-categories';
|
||||||
|
|
||||||
@@ -333,97 +334,78 @@ export function ManualDialog({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Block nachtragen">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||||
onClick={onClose}
|
<div className="grid grid-cols-2 gap-2">
|
||||||
>
|
|
||||||
<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>
|
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="text-xs text-[var(--color-ink-muted)]">Bezug auf ToDo (optional)</span>
|
<span className="text-xs text-[var(--color-ink-muted)]">Start</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
|
<input
|
||||||
type="text"
|
type="datetime-local"
|
||||||
value={description}
|
required
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
value={startedAt}
|
||||||
maxLength={200}
|
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"
|
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>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<label className="block">
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
<span className="text-xs text-[var(--color-ink-muted)]">Ende</span>
|
||||||
Abbrechen
|
<input
|
||||||
</Button>
|
type="datetime-local"
|
||||||
<Button type="submit" disabled={mutation.isPending}>
|
required
|
||||||
Speichern
|
value={endedAt}
|
||||||
</Button>
|
onChange={(e) => setEndedAt(e.target.value)}
|
||||||
</div>
|
className="mt-0.5 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-2 py-1.5 text-sm"
|
||||||
</form>
|
/>
|
||||||
</div>
|
</label>
|
||||||
</div>
|
</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 { useEffect, useId, useRef, type ReactNode } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
import { X } from 'lucide-react';
|
||||||
|
|
||||||
type Size = 'sm' | 'md' | 'lg';
|
type Size = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
@@ -94,10 +95,18 @@ export function Modal({
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby={title != null ? titleId : undefined}
|
aria-labelledby={title != null ? titleId : undefined}
|
||||||
tabIndex={-1}
|
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 && (
|
{title != null && (
|
||||||
<h2 id={titleId} className="text-xl font-semibold">
|
<h2 id={titleId} className="pr-8 text-xl font-semibold">
|
||||||
{title}
|
{title}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { trpc } from '../lib/trpc';
|
|||||||
import { voice } from '../lib/voice';
|
import { voice } from '../lib/voice';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Modal } from '../components/ui/Modal';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
@@ -220,95 +221,86 @@ function ProjectCreateDialog({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Neues Projekt">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||||
onClick={onClose}
|
<Field label="Name">
|
||||||
>
|
<input
|
||||||
<div
|
required
|
||||||
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
value={name}
|
||||||
onClick={(e) => e.stopPropagation()}
|
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"
|
||||||
<h2 className="text-xl font-semibold">Neues Projekt</h2>
|
/>
|
||||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
</Field>
|
||||||
<Field label="Name">
|
<Field label="Beschreibung (optional)">
|
||||||
<input
|
<textarea
|
||||||
required
|
value={description}
|
||||||
value={name}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
onChange={(e) => setName(e.target.value)}
|
maxLength={2000}
|
||||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
rows={3}
|
||||||
/>
|
placeholder="Worum geht's? Kurz beschreiben."
|
||||||
</Field>
|
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
<Field label="Beschreibung (optional)">
|
/>
|
||||||
<textarea
|
</Field>
|
||||||
value={description}
|
<Field label="Logo (optional)">
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
<LogoUpload value={logoUrl} name={name} onChange={setLogoUrl} />
|
||||||
maxLength={2000}
|
</Field>
|
||||||
rows={3}
|
<Field label="Bereich">
|
||||||
placeholder="Worum geht's? Kurz beschreiben."
|
<select
|
||||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
required
|
||||||
/>
|
value={areaId}
|
||||||
</Field>
|
onChange={(e) => setAreaId(e.target.value)}
|
||||||
<Field label="Logo (optional)">
|
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
<LogoUpload value={logoUrl} name={name} onChange={setLogoUrl} />
|
>
|
||||||
</Field>
|
<option value="" disabled>
|
||||||
<Field label="Bereich">
|
{areas.length ? '— Bereich wählen —' : 'Bereiche werden geladen…'}
|
||||||
<select
|
</option>
|
||||||
required
|
{areas.map((a) => (
|
||||||
value={areaId}
|
<option key={a.id} value={a.id}>
|
||||||
onChange={(e) => setAreaId(e.target.value)}
|
{a.name}
|
||||||
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>
|
</option>
|
||||||
{areas.map((a) => (
|
))}
|
||||||
<option key={a.id} value={a.id}>
|
</select>
|
||||||
{a.name}
|
</Field>
|
||||||
</option>
|
<Field label="Projektart">
|
||||||
))}
|
<select
|
||||||
</select>
|
value={kind}
|
||||||
</Field>
|
onChange={(e) => setKind(e.target.value as 'kundenprojekt' | 'internes_projekt')}
|
||||||
<Field label="Projektart">
|
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
<select
|
>
|
||||||
value={kind}
|
<option value="kundenprojekt">Kundenprojekt</option>
|
||||||
onChange={(e) => setKind(e.target.value as 'kundenprojekt' | 'internes_projekt')}
|
<option value="internes_projekt">Internes Projekt</option>
|
||||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
</select>
|
||||||
>
|
</Field>
|
||||||
<option value="kundenprojekt">Kundenprojekt</option>
|
<Field label="Outline-Steckbrief-URL">
|
||||||
<option value="internes_projekt">Internes Projekt</option>
|
<input
|
||||||
</select>
|
required
|
||||||
</Field>
|
type="url"
|
||||||
<Field label="Outline-Steckbrief-URL">
|
value={steckbrief}
|
||||||
<input
|
onChange={(e) => setSteckbrief(e.target.value)}
|
||||||
required
|
placeholder="https://outline.tool-ctrl.de/..."
|
||||||
type="url"
|
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
value={steckbrief}
|
/>
|
||||||
onChange={(e) => setSteckbrief(e.target.value)}
|
</Field>
|
||||||
placeholder="https://outline.tool-ctrl.de/..."
|
<Field label="Plane-Board-URL">
|
||||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
<input
|
||||||
/>
|
required
|
||||||
</Field>
|
type="url"
|
||||||
<Field label="Plane-Board-URL">
|
value={plane}
|
||||||
<input
|
onChange={(e) => setPlane(e.target.value)}
|
||||||
required
|
placeholder="https://plane.tool-ctrl.de/..."
|
||||||
type="url"
|
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
||||||
value={plane}
|
/>
|
||||||
onChange={(e) => setPlane(e.target.value)}
|
</Field>
|
||||||
placeholder="https://plane.tool-ctrl.de/..."
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
className="w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
/>
|
Abbrechen
|
||||||
</Field>
|
</Button>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<Button type="submit" disabled={create.isPending}>
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
Anlegen
|
||||||
Abbrechen
|
</Button>
|
||||||
</Button>
|
</div>
|
||||||
<Button type="submit" disabled={create.isPending}>
|
</form>
|
||||||
Anlegen
|
</Modal>
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Card } from '../components/ui/Card';
|
|||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
import { ErrorState } from '../components/ui/ErrorState';
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
|
import { Modal } from '../components/ui/Modal';
|
||||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||||
import {
|
import {
|
||||||
LEAD_STATUS_META,
|
LEAD_STATUS_META,
|
||||||
@@ -357,57 +358,48 @@ function PromoteDialog({ leadId, onClose }: { leadId: string; onClose: () => voi
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Zu Deal machen" size="sm">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||||
onClick={onClose}
|
Legt einen Pipeline-Deal in Stufe „Lead“ an und verknüpft ihn.
|
||||||
>
|
</p>
|
||||||
<div
|
<form
|
||||||
className="w-full max-w-sm rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
className="mt-4 space-y-3"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
promote.mutate({ leadId, valueEur: Math.round(euro * 100), probability });
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<h2 className="text-xl font-semibold">Zu Deal machen</h2>
|
<label className="block">
|
||||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
<span className="text-sm font-medium">Wert (EUR)</span>
|
||||||
Legt einen Pipeline-Deal in Stufe „Lead“ an und verknüpft ihn.
|
<input
|
||||||
</p>
|
type="number"
|
||||||
<form
|
min={0}
|
||||||
className="mt-4 space-y-3"
|
value={euro}
|
||||||
onSubmit={(e) => {
|
onChange={(e) => setEuro(Number(e.target.value))}
|
||||||
e.preventDefault();
|
className={clsx(inputCls, 'mt-1')}
|
||||||
promote.mutate({ leadId, valueEur: Math.round(euro * 100), probability });
|
/>
|
||||||
}}
|
</label>
|
||||||
>
|
<label className="block">
|
||||||
<label className="block">
|
<span className="text-sm font-medium">Wahrscheinlichkeit: {probability}%</span>
|
||||||
<span className="text-sm font-medium">Wert (EUR)</span>
|
<input
|
||||||
<input
|
type="range"
|
||||||
type="number"
|
min={0}
|
||||||
min={0}
|
max={100}
|
||||||
value={euro}
|
value={probability}
|
||||||
onChange={(e) => setEuro(Number(e.target.value))}
|
onChange={(e) => setProbability(Number(e.target.value))}
|
||||||
className={clsx(inputCls, 'mt-1')}
|
className="mt-1 w-full"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<span className="text-sm font-medium">Wahrscheinlichkeit: {probability}%</span>
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
<input
|
Abbrechen
|
||||||
type="range"
|
</Button>
|
||||||
min={0}
|
<Button type="submit" disabled={promote.isPending}>
|
||||||
max={100}
|
Anlegen
|
||||||
value={probability}
|
</Button>
|
||||||
onChange={(e) => setProbability(Number(e.target.value))}
|
</div>
|
||||||
className="mt-1 w-full"
|
</form>
|
||||||
/>
|
</Modal>
|
||||||
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+155
-178
@@ -6,6 +6,7 @@ import { trpc } from '../lib/trpc';
|
|||||||
import { voice } from '../lib/voice';
|
import { voice } from '../lib/voice';
|
||||||
import { Card } from '../components/ui/Card';
|
import { Card } from '../components/ui/Card';
|
||||||
import { Button } from '../components/ui/Button';
|
import { Button } from '../components/ui/Button';
|
||||||
|
import { Modal } from '../components/ui/Modal';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
import { LEAD_STATUS_META, LEAD_STATUS_ORDER, type LeadStatus } from '../lib/lead-meta';
|
import { LEAD_STATUS_META, LEAD_STATUS_ORDER, type LeadStatus } from '../lib/lead-meta';
|
||||||
|
|
||||||
@@ -107,90 +108,79 @@ function LeadImportDialog({ onClose }: { onClose: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Leads aus CSV importieren">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||||
onClick={onClose}
|
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
|
||||||
<div
|
werden als erster Austausch-Log-Eintrag gespeichert. Duplikate (gleicher Firmenname) werden
|
||||||
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
übersprungen.
|
||||||
onClick={(e) => e.stopPropagation()}
|
</p>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="mt-4 space-y-3">
|
<div className="mt-4 space-y-3">
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="text-sm font-medium">Projekt zuordnen (optional)</span>
|
<span className="text-sm font-medium">Projekt zuordnen (optional)</span>
|
||||||
<select
|
<select
|
||||||
value={projectId}
|
value={projectId}
|
||||||
onChange={(e) => setProjectId(e.target.value)}
|
onChange={(e) => setProjectId(e.target.value)}
|
||||||
disabled={!!result}
|
disabled={!!result}
|
||||||
className="mt-1 w-full rounded-[var(--radius-md)] border border-[var(--color-border)] bg-white px-3 py-2 text-sm"
|
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>
|
<option value="">— kein Projekt —</option>
|
||||||
{projects.map((p) => (
|
{projects.map((p) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={p.id} value={p.id}>
|
||||||
{p.name}
|
{p.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label className="block">
|
<label className="block">
|
||||||
<span className="text-sm font-medium">CSV-Datei</span>
|
<span className="text-sm font-medium">CSV-Datei</span>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept=".csv,text/csv"
|
accept=".csv,text/csv"
|
||||||
onChange={onFile}
|
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"
|
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>
|
</label>
|
||||||
{fileName && !result && (
|
{fileName && !result && (
|
||||||
<p className="text-xs text-[var(--color-ink-muted)]">
|
<p className="text-xs text-[var(--color-ink-muted)]">
|
||||||
Bereit: {fileName} ({Math.max(0, csv.split('\n').filter((l) => l.trim()).length - 1)}{' '}
|
Bereit: {fileName} ({Math.max(0, csv.split('\n').filter((l) => l.trim()).length - 1)}{' '}
|
||||||
Datenzeilen)
|
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>
|
</p>
|
||||||
)}
|
<ul className="mt-1 space-y-0.5 text-xs text-[var(--color-ink-muted)]">
|
||||||
|
<li>{result.contacts} Kontakte angelegt</li>
|
||||||
{result && (
|
<li>{result.touchpoints} Notizen als Log-Eintrag</li>
|
||||||
<div className="rounded-[var(--radius-md)] border border-[var(--color-border)] p-3 text-sm">
|
{result.skippedDuplicate > 0 && (
|
||||||
<p className="font-medium text-[var(--color-ampel-green-fg)]">
|
<li>{result.skippedDuplicate} Duplikate übersprungen</li>
|
||||||
{result.created} Leads importiert
|
)}
|
||||||
</p>
|
{result.skippedEmpty > 0 && <li>{result.skippedEmpty} leere Zeilen übersprungen</li>}
|
||||||
<ul className="mt-1 space-y-0.5 text-xs text-[var(--color-ink-muted)]">
|
</ul>
|
||||||
<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 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>
|
</div>
|
||||||
</div>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,121 +272,108 @@ function LeadCreateDialog({ onClose }: { onClose: () => void }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Neuer Lead">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||||
onClick={onClose}
|
Nur die Firma ist Pflicht — alles Weitere füllst du im Dossier.
|
||||||
>
|
</p>
|
||||||
<div
|
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||||
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)]"
|
<Field label="Firma">
|
||||||
onClick={(e) => e.stopPropagation()}
|
<input
|
||||||
>
|
required
|
||||||
<h2 className="text-xl font-semibold">Neuer Lead</h2>
|
value={firma}
|
||||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
onChange={(e) => setFirma(e.target.value)}
|
||||||
Nur die Firma ist Pflicht — alles Weitere füllst du im Dossier.
|
className={inputCls}
|
||||||
</p>
|
/>
|
||||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
</Field>
|
||||||
<Field label="Firma">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<Field label="Branche">
|
||||||
<input
|
<input
|
||||||
required
|
value={branche}
|
||||||
value={firma}
|
onChange={(e) => setBranche(e.target.value)}
|
||||||
onChange={(e) => setFirma(e.target.value)}
|
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<Field label="Nische / Sub-Segment">
|
||||||
<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">
|
|
||||||
<input
|
<input
|
||||||
value={website}
|
value={subSegment}
|
||||||
onChange={(e) => setWebsite(e.target.value)}
|
onChange={(e) => setSubSegment(e.target.value)}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Quelle (wie gefunden)">
|
</div>
|
||||||
<input
|
<Field label="Website">
|
||||||
value={quelle}
|
<input
|
||||||
onChange={(e) => setQuelle(e.target.value)}
|
value={website}
|
||||||
className={inputCls}
|
onChange={(e) => setWebsite(e.target.value)}
|
||||||
/>
|
className={inputCls}
|
||||||
</Field>
|
/>
|
||||||
<Field label="Projekt (optional)">
|
</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
|
<select
|
||||||
value={projectId}
|
value={ownerUserId}
|
||||||
onChange={(e) => setProjectId(e.target.value)}
|
onChange={(e) => setOwnerUserId(e.target.value)}
|
||||||
className={inputCls}
|
className={inputCls}
|
||||||
>
|
>
|
||||||
<option value="">— kein Projekt —</option>
|
<option value="">— niemand —</option>
|
||||||
{projects.map((p) => (
|
{(users.data ?? []).map((u) => (
|
||||||
<option key={p.id} value={p.id}>
|
<option key={u.id} value={u.id}>
|
||||||
{p.name}
|
{u.name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Field>
|
</Field>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<Field label="Status">
|
||||||
<Field label="Owner">
|
<select
|
||||||
<select
|
value={status}
|
||||||
value={ownerUserId}
|
onChange={(e) => setStatus(e.target.value as LeadStatus)}
|
||||||
onChange={(e) => setOwnerUserId(e.target.value)}
|
className={inputCls}
|
||||||
className={inputCls}
|
>
|
||||||
>
|
{LEAD_STATUS_ORDER.map((s) => (
|
||||||
<option value="">— niemand —</option>
|
<option key={s} value={s}>
|
||||||
{(users.data ?? []).map((u) => (
|
{LEAD_STATUS_META[s].label}
|
||||||
<option key={u.id} value={u.id}>
|
</option>
|
||||||
{u.name}
|
))}
|
||||||
</option>
|
</select>
|
||||||
))}
|
|
||||||
</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>
|
</Field>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
</div>
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
<Field label={`Score: ${score}/10`}>
|
||||||
Abbrechen
|
<input
|
||||||
</Button>
|
type="range"
|
||||||
<Button type="submit" disabled={create.isPending}>
|
min={0}
|
||||||
Anlegen
|
max={10}
|
||||||
</Button>
|
value={score}
|
||||||
</div>
|
onChange={(e) => setScore(Number(e.target.value))}
|
||||||
</form>
|
className="w-full"
|
||||||
</div>
|
/>
|
||||||
</div>
|
</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 { Button } from '../components/ui/Button';
|
||||||
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
import { SkeletonCard } from '../components/ui/SkeletonCard';
|
||||||
import { ErrorState } from '../components/ui/ErrorState';
|
import { ErrorState } from '../components/ui/ErrorState';
|
||||||
|
import { Modal } from '../components/ui/Modal';
|
||||||
import { useConfirm } from '../components/ui/ConfirmProvider';
|
import { useConfirm } from '../components/ui/ConfirmProvider';
|
||||||
import { useCurrentUser } from '../hooks/useCurrentUser';
|
import { useCurrentUser } from '../hooks/useCurrentUser';
|
||||||
import { voice } from '../lib/voice';
|
import { voice } from '../lib/voice';
|
||||||
@@ -704,65 +705,52 @@ function GateDialog({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title={`Gate-Review: ${PHASE_META[phase].label}`}>
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||||
onClick={onClose}
|
Ziel: {PHASE_META[phase].gateZiel}
|
||||||
>
|
</p>
|
||||||
<div
|
<form
|
||||||
className="w-full max-w-md rounded-[var(--radius-xl)] bg-[var(--color-bg)] p-6 shadow-[var(--shadow-lg)]"
|
className="mt-4 space-y-3"
|
||||||
onClick={(e) => e.stopPropagation()}
|
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>
|
<label className="flex items-center gap-2 text-sm">
|
||||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
<input
|
||||||
Ziel: {PHASE_META[phase].gateZiel}
|
type="checkbox"
|
||||||
</p>
|
checked={erreicht}
|
||||||
<form
|
onChange={(e) => setErreicht(e.target.checked)}
|
||||||
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">
|
Ergebnis erreicht
|
||||||
<input
|
</label>
|
||||||
type="checkbox"
|
<textarea
|
||||||
checked={advance}
|
value={notiz}
|
||||||
onChange={(e) => setAdvance(e.target.checked)}
|
onChange={(e) => setNotiz(e.target.value)}
|
||||||
/>
|
rows={3}
|
||||||
Folgephase jetzt freigeben
|
placeholder="Rückblick / Maßnahmen (bei Gelb/Rot)"
|
||||||
</label>
|
className={inputCls}
|
||||||
<div className="flex justify-end gap-2">
|
/>
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
<label className="flex items-center gap-2 text-sm">
|
||||||
Abbrechen
|
<input type="checkbox" checked={advance} onChange={(e) => setAdvance(e.target.checked)} />
|
||||||
</Button>
|
Folgephase jetzt freigeben
|
||||||
<Button type="submit" disabled={add.isPending}>
|
</label>
|
||||||
Speichern
|
<div className="flex justify-end gap-2">
|
||||||
</Button>
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
</div>
|
Abbrechen
|
||||||
</form>
|
</Button>
|
||||||
</div>
|
<Button type="submit" disabled={add.isPending}>
|
||||||
</div>
|
Speichern
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2320,65 +2308,56 @@ function AbschlussDialog({
|
|||||||
{ key: 'retro', label: 'Retrospektive / Lessons Learned durchgeführt' },
|
{ key: 'retro', label: 'Retrospektive / Lessons Learned durchgeführt' },
|
||||||
];
|
];
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal open onClose={onClose} title="Projekt abschließen">
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/30"
|
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
||||||
onClick={onClose}
|
Ein Projekt endet bewusst (Leitfaden Phase 4).
|
||||||
>
|
</p>
|
||||||
<div
|
<form onSubmit={submit} className="mt-4 space-y-3">
|
||||||
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)]"
|
<div className="space-y-1.5 rounded-[var(--radius-md)] border border-[var(--color-border)] p-3">
|
||||||
onClick={(e) => e.stopPropagation()}
|
{checkItems.map((it) => (
|
||||||
>
|
<label key={it.key} className="flex items-start gap-2 text-sm">
|
||||||
<h2 className="text-xl font-semibold">Projekt abschließen</h2>
|
<input
|
||||||
<p className="mt-1 text-sm text-[var(--color-ink-muted)]">
|
type="checkbox"
|
||||||
Ein Projekt endet bewusst (Leitfaden Phase 4).
|
checked={checks[it.key]}
|
||||||
</p>
|
onChange={(e) => setChecks((c) => ({ ...c, [it.key]: e.target.checked }))}
|
||||||
<form onSubmit={submit} className="mt-4 space-y-3">
|
className="mt-0.5"
|
||||||
<div className="space-y-1.5 rounded-[var(--radius-md)] border border-[var(--color-border)] p-3">
|
/>
|
||||||
{checkItems.map((it) => (
|
{it.label}
|
||||||
<label key={it.key} className="flex items-start gap-2 text-sm">
|
</label>
|
||||||
<input
|
))}
|
||||||
type="checkbox"
|
</div>
|
||||||
checked={checks[it.key]}
|
<label className="block">
|
||||||
onChange={(e) => setChecks((c) => ({ ...c, [it.key]: e.target.checked }))}
|
<span className="text-sm font-medium">Abschlussvermerk</span>
|
||||||
className="mt-0.5"
|
<textarea
|
||||||
/>
|
value={vermerk}
|
||||||
{it.label}
|
onChange={(e) => setVermerk(e.target.value)}
|
||||||
</label>
|
rows={3}
|
||||||
))}
|
maxLength={2000}
|
||||||
</div>
|
placeholder="Ziel erreicht? Aufwand gegen Plan? Wichtigste Erkenntnisse — eine halbe Seite, kein Roman."
|
||||||
<label className="block">
|
className={clsx(inputCls, 'mt-1')}
|
||||||
<span className="text-sm font-medium">Abschlussvermerk</span>
|
/>
|
||||||
<textarea
|
</label>
|
||||||
value={vermerk}
|
<label className="block">
|
||||||
onChange={(e) => setVermerk(e.target.value)}
|
<span className="text-sm font-medium">Lessons-Learned-URL (Outline)</span>
|
||||||
rows={3}
|
<input
|
||||||
maxLength={2000}
|
required
|
||||||
placeholder="Ziel erreicht? Aufwand gegen Plan? Wichtigste Erkenntnisse — eine halbe Seite, kein Roman."
|
type="url"
|
||||||
className={clsx(inputCls, 'mt-1')}
|
value={url}
|
||||||
/>
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
</label>
|
placeholder="https://outline.tool-ctrl.de/lessons-learned/..."
|
||||||
<label className="block">
|
className={clsx(inputCls, 'mt-1')}
|
||||||
<span className="text-sm font-medium">Lessons-Learned-URL (Outline)</span>
|
/>
|
||||||
<input
|
</label>
|
||||||
required
|
<div className="flex justify-end gap-2">
|
||||||
type="url"
|
<Button type="button" variant="ghost" onClick={onClose}>
|
||||||
value={url}
|
Abbrechen
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
</Button>
|
||||||
placeholder="https://outline.tool-ctrl.de/lessons-learned/..."
|
<Button type="submit" disabled={mutation.isPending}>
|
||||||
className={clsx(inputCls, 'mt-1')}
|
Abschließen
|
||||||
/>
|
</Button>
|
||||||
</label>
|
</div>
|
||||||
<div className="flex justify-end gap-2">
|
</form>
|
||||||
<Button type="button" variant="ghost" onClick={onClose}>
|
</Modal>
|
||||||
Abbrechen
|
|
||||||
</Button>
|
|
||||||
<Button type="submit" disabled={mutation.isPending}>
|
|
||||||
Abschließen
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user