Suspects: Eingabezeile oben, Letzter Kontakt standardmäßig heute
- Eingabezeile über der Liste, mit Abstand und Knopf Hinzufügen; Liste neueste zuerst, neue Einträge erscheinen direkt darunter - Letzter Kontakt neuer Zeilen = heute (Ortszeit), auch serverseitig, wenn das Feld beim Anlegen fehlt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
78d6bef52c
commit
33c9dbdb2b
@@ -9,8 +9,9 @@ import { useToast } from '../Toast';
|
||||
/*
|
||||
* Suspects - die Stufe vor dem Lead.
|
||||
*
|
||||
* Eine Tabelle, die sich wie eine Liste füllt: unten steht immer eine leere
|
||||
* Zeile, jede Zelle ist direkt bearbeitbar und speichert beim Verlassen.
|
||||
* Eine Tabelle, die sich wie eine Liste füllt: oben steht immer eine
|
||||
* Eingabezeile, abgesetzt von der Liste darunter (neueste zuerst). Jede Zelle
|
||||
* der Liste ist direkt bearbeitbar und speichert beim Verlassen.
|
||||
*
|
||||
* Links (LinkedIn, Website) werden nach dem Speichern zu einem Symbol - die
|
||||
* Adresse steht im Tooltip. Das spart die Breite, die eine URL sonst frisst.
|
||||
@@ -47,12 +48,19 @@ const EMPTY_DRAFT: Record<TextField, string> & { last_contact_on: string } = {
|
||||
const cellInput =
|
||||
'w-full min-w-0 rounded border border-transparent bg-transparent px-2 py-1.5 text-sm text-ink-900 placeholder:text-ink-300 hover:border-ink-200 focus:border-brand-400 focus:bg-surface focus:outline-none';
|
||||
|
||||
const today = () => new Date().toISOString().slice(0, 10);
|
||||
/** Heute in Ortszeit - toISOString waere UTC und laege kurz nach Mitternacht einen Tag zurueck. */
|
||||
const today = () => {
|
||||
const now = new Date();
|
||||
return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
/** Neue Zeile: "Letzter Kontakt" steht auf heute - meist erfasst man jemanden direkt nach dem Gespräch. */
|
||||
const freshDraft = () => ({ ...EMPTY_DRAFT, last_contact_on: today() });
|
||||
|
||||
export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contacts, users, onChanged }) => {
|
||||
const { toast } = useToast();
|
||||
const [rows, setRows] = useState<Suspect[] | null>(null);
|
||||
const [draft, setDraft] = useState({ ...EMPTY_DRAFT });
|
||||
const [draft, setDraft] = useState(freshDraft);
|
||||
const [impulse, setImpulse] = useState<Suspect | null>(null);
|
||||
const [toLead, setToLead] = useState<Suspect | null>(null);
|
||||
const [removing, setRemoving] = useState<Suspect | null>(null);
|
||||
@@ -89,7 +97,9 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
||||
|
||||
/** Neue Zeile anlegen - sobald die Zeile verlassen oder Enter gedrückt wird. */
|
||||
const createDraft = async () => {
|
||||
const hasContent = Object.values(draft).some((value) => value.trim());
|
||||
// Das vorbelegte Datum allein zaehlt nicht als Eingabe.
|
||||
const { last_contact_on: _date, ...texts } = draft;
|
||||
const hasContent = Object.values(texts).some((value) => value.trim());
|
||||
if (!hasContent) return;
|
||||
if (!draft.name.trim()) {
|
||||
toast('Bitte einen Namen eintragen – dann wird die Zeile gespeichert.', 'error');
|
||||
@@ -100,8 +110,8 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
||||
...draft,
|
||||
last_contact_on: draft.last_contact_on || null,
|
||||
});
|
||||
setRows((list) => [...(list ?? []), suspect]);
|
||||
setDraft({ ...EMPTY_DRAFT });
|
||||
setRows((list) => [suspect, ...(list ?? [])]);
|
||||
setDraft(freshDraft());
|
||||
onChanged();
|
||||
requestAnimationFrame(() => draftName.current?.focus());
|
||||
} catch (err) {
|
||||
@@ -158,7 +168,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
||||
<p className="max-w-3xl text-sm text-ink-500">
|
||||
{archived
|
||||
? 'Archivierte Suspects und solche, aus denen ein Lead wurde. Zurückholen oder endgültig löschen.'
|
||||
: 'Personen, die interessant sein könnten. Einfach in die leere Zeile unten schreiben – gespeichert wird beim Verlassen der Zeile oder mit Enter.'}
|
||||
: 'Personen, die interessant sein könnten. Oben in die Eingabezeile schreiben – gespeichert wird mit Enter, „Hinzufügen“ oder beim Verlassen der Zeile.'}
|
||||
</p>
|
||||
|
||||
<div className="overflow-x-auto rounded-xl border border-ink-200 bg-surface">
|
||||
@@ -189,6 +199,65 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
||||
<th className="px-3 py-3" aria-label="Aktionen" />
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
{/*
|
||||
Eingabezeile steht immer oben, abgesetzt von der Liste - neue
|
||||
Einträge erscheinen direkt darunter (Liste: neueste zuerst).
|
||||
*/}
|
||||
{!archived && (
|
||||
<tbody>
|
||||
<tr
|
||||
className="bg-brand-50/60"
|
||||
onBlur={(event) => {
|
||||
// Erst speichern, wenn der Fokus die Zeile ganz verlässt.
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) void createDraft();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void createDraft();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(['name', 'role', 'company', 'email', 'linkedin', 'website', 'notes'] as const).map((field) => (
|
||||
<td key={field} className="px-1 py-1.5">
|
||||
<input
|
||||
ref={field === 'name' ? draftName : undefined}
|
||||
value={draft[field]}
|
||||
onChange={(event) => setDraft({ ...draft, [field]: event.target.value })}
|
||||
placeholder={DRAFT_PLACEHOLDERS[field]}
|
||||
className={`${cellInput} border-ink-200 bg-surface`}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-1 py-1.5">
|
||||
<input
|
||||
type="date"
|
||||
value={draft.last_contact_on}
|
||||
max={today()}
|
||||
onChange={(event) => setDraft({ ...draft, last_contact_on: event.target.value })}
|
||||
className={`${cellInput} border-ink-200 bg-surface`}
|
||||
/>
|
||||
</td>
|
||||
<td />
|
||||
<td className="px-2 py-1.5 text-right">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void createDraft()}
|
||||
disabled={!draft.name.trim()}
|
||||
className="rounded-md bg-brand-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-brand-700 disabled:opacity-40"
|
||||
>
|
||||
Hinzufügen
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/* Abstand zwischen Eingabe und Liste */}
|
||||
<tr aria-hidden="true">
|
||||
<td colSpan={10} className="h-4 border-b border-ink-200 bg-canvas p-0" />
|
||||
</tr>
|
||||
</tbody>
|
||||
)}
|
||||
|
||||
<tbody>
|
||||
{rows.map((row) =>
|
||||
archived ? (
|
||||
@@ -244,42 +313,11 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
||||
)
|
||||
)}
|
||||
|
||||
{!archived && (
|
||||
<tr
|
||||
className="bg-ink-50/40"
|
||||
onBlur={(event) => {
|
||||
// Erst speichern, wenn der Fokus die Zeile ganz verlässt.
|
||||
if (!event.currentTarget.contains(event.relatedTarget as Node | null)) void createDraft();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
void createDraft();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{(['name', 'role', 'company', 'email', 'linkedin', 'website', 'notes'] as const).map((field) => (
|
||||
<td key={field} className="px-1 py-1">
|
||||
<input
|
||||
ref={field === 'name' ? draftName : undefined}
|
||||
value={draft[field]}
|
||||
onChange={(event) => setDraft({ ...draft, [field]: event.target.value })}
|
||||
placeholder={DRAFT_PLACEHOLDERS[field]}
|
||||
className={cellInput}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-1 py-1">
|
||||
<input
|
||||
type="date"
|
||||
value={draft.last_contact_on}
|
||||
max={today()}
|
||||
onChange={(event) => setDraft({ ...draft, last_contact_on: event.target.value })}
|
||||
className={cellInput}
|
||||
/>
|
||||
{!archived && rows.length === 0 && (
|
||||
<tr>
|
||||
<td colSpan={10} className="py-10 text-center text-sm text-ink-400">
|
||||
Noch keine Suspects – oben in die Eingabezeile schreiben.
|
||||
</td>
|
||||
<td />
|
||||
<td />
|
||||
</tr>
|
||||
)}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user