Suspects: Spalte Mail und Löschen direkt in der Tabelle

- email als Symbol mit Adresse im Tooltip, Klick öffnet mailto:
  (Spalte per migrateColumns nachgezogen)
- Löschen-Knopf am Zeilenende; Server erlaubt Löschen jetzt auch
  ohne vorheriges Archivieren

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
schakal00783
2026-09-29 12:33:32 +02:00
co-authored by Claude Opus 5.5
parent 6a452f1510
commit 78d6bef52c
7 changed files with 62 additions and 42 deletions
+34 -28
View File
@@ -31,12 +31,13 @@ interface SuspectsProps {
onChanged: () => void;
}
type TextField = 'name' | 'role' | 'company' | 'linkedin' | 'website' | 'notes';
type TextField = 'name' | 'role' | 'company' | 'email' | 'linkedin' | 'website' | 'notes';
const EMPTY_DRAFT: Record<TextField, string> & { last_contact_on: string } = {
name: '',
role: '',
company: '',
email: '',
linkedin: '',
website: '',
notes: '',
@@ -161,23 +162,25 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
</p>
<div className="overflow-x-auto rounded-xl border border-ink-200 bg-surface">
<table className="w-full min-w-[1100px] table-fixed text-sm">
<table className="w-full min-w-[1200px] table-fixed text-sm">
<colgroup>
<col className="w-[15%]" />
<col className="w-[14%]" />
<col className="w-[14%]" />
<col className="w-[64px]" />
<col className="w-[72px]" />
<col className="w-[72px]" />
<col />
<col className="w-[130px]" />
<col className="w-[120px]" />
<col className="w-[96px]" />
<col className="w-[128px]" />
</colgroup>
<thead>
<tr className="border-b border-ink-200 text-left text-[11px] font-semibold uppercase tracking-wider text-ink-500">
<th className="px-3 py-3">Name</th>
<th className="px-3 py-3">Funktion</th>
<th className="px-3 py-3">Unternehmen</th>
<th className="px-3 py-3 text-center">Mail</th>
<th className="px-3 py-3 text-center">LinkedIn</th>
<th className="px-3 py-3 text-center">Website</th>
<th className="px-3 py-3">Notiz</th>
@@ -206,6 +209,9 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
/>
</td>
))}
<td className="px-1 py-1 text-center">
<LinkCell value={row.email} kind="mail" onCommit={(value) => void update(row, { email: value })} />
</td>
<td className="px-1 py-1 text-center">
<LinkCell value={row.linkedin} kind="linkedin" onCommit={(value) => void update(row, { linkedin: value })} />
</td>
@@ -231,6 +237,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
<div className="flex justify-end gap-1">
<IconButton title="Archivieren" onClick={() => void archive(row)} path={ARCHIVE_ICON} />
<IconButton title="Lead erstellen" onClick={() => setToLead(row)} path={LEAD_ICON} tone="brand" />
<IconButton title="Löschen" onClick={() => setRemoving(row)} path={DELETE_ICON} tone="red" />
</div>
</td>
</tr>
@@ -251,7 +258,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
}
}}
>
{(['name', 'role', 'company', 'linkedin', 'website', 'notes'] as const).map((field) => (
{(['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}
@@ -278,7 +285,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
{archived && rows.length === 0 && (
<tr>
<td colSpan={9} className="py-12 text-center text-sm text-ink-400">
<td colSpan={10} className="py-12 text-center text-sm text-ink-400">
Das Archiv ist leer.
</td>
</tr>
@@ -328,6 +335,7 @@ const DRAFT_PLACEHOLDERS: Record<TextField, string> = {
name: '+ Name',
role: 'Funktion',
company: 'Unternehmen',
email: 'Mail',
linkedin: 'LinkedIn-URL',
website: 'Website',
notes: 'Notiz',
@@ -372,8 +380,16 @@ const TextCell: React.FC<{ value: string | null; required?: boolean; onCommit: (
);
};
type LinkKind = 'linkedin' | 'website' | 'mail';
const LINK_KINDS: Record<LinkKind, { color: string; href: (value: string) => string; placeholder: string }> = {
linkedin: { color: 'text-[#0a66c2]', href: (v) => v, placeholder: 'URL' },
website: { color: 'text-brand-600', href: (v) => v, placeholder: 'URL' },
mail: { color: 'text-brand-600', href: (v) => `mailto:${v}`, placeholder: 'Mail' },
};
/** Link als Symbol; die Adresse zeigt der Tooltip. Bearbeiten über den Stift. */
const LinkCell: React.FC<{ value: string | null; kind: 'linkedin' | 'website'; onCommit: (value: string | null) => void }> = ({
const LinkCell: React.FC<{ value: string | null; kind: LinkKind; onCommit: (value: string | null) => void }> = ({
value,
kind,
onCommit,
@@ -399,7 +415,8 @@ const LinkCell: React.FC<{ value: string | null; kind: 'linkedin' | 'website'; o
setEditing(false);
}
}}
placeholder="URL"
type={kind === 'mail' ? 'email' : 'text'}
placeholder={LINK_KINDS[kind].placeholder}
className={`${cellInput} text-xs`}
/>
);
@@ -407,23 +424,11 @@ const LinkCell: React.FC<{ value: string | null; kind: 'linkedin' | 'website'; o
return (
<span className="group inline-flex items-center gap-0.5">
<a
href={value}
target="_blank"
rel="noopener noreferrer"
title={value}
className={`inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-ink-100 ${
kind === 'linkedin' ? 'text-[#0a66c2]' : 'text-brand-600'
}`}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d={kind === 'linkedin' ? LINKEDIN_ICON : LINK_ICON} />
</svg>
</a>
<StaticLink href={value} kind={kind} />
<button
type="button"
onClick={() => setEditing(true)}
title="Link bearbeiten"
title={kind === 'mail' ? 'Mail bearbeiten' : 'Link bearbeiten'}
className="invisible rounded p-0.5 text-ink-400 hover:text-ink-700 group-hover:visible"
>
<svg viewBox="0 0 24 24" className="h-3 w-3" fill="currentColor" aria-hidden="true">
@@ -534,6 +539,7 @@ const ArchivedRow: React.FC<{ row: Suspect; onRestore: () => void; onDelete: ()
</td>
<td className="truncate px-3 py-2.5 text-ink-600">{row.role ?? '–'}</td>
<td className="truncate px-3 py-2.5 text-ink-600">{row.company ?? '–'}</td>
<td className="px-3 py-2.5 text-center">{row.email && <StaticLink href={row.email} kind="mail" />}</td>
<td className="px-3 py-2.5 text-center">{row.linkedin && <StaticLink href={row.linkedin} kind="linkedin" />}</td>
<td className="px-3 py-2.5 text-center">{row.website && <StaticLink href={row.website} kind="website" />}</td>
<td className="truncate px-3 py-2.5 text-ink-600" title={row.notes ?? undefined}>
@@ -550,18 +556,16 @@ const ArchivedRow: React.FC<{ row: Suspect; onRestore: () => void; onDelete: ()
</tr>
);
const StaticLink: React.FC<{ href: string; kind: 'linkedin' | 'website' }> = ({ href, kind }) => (
const StaticLink: React.FC<{ href: string; kind: LinkKind }> = ({ href, kind }) => (
<a
href={href}
target="_blank"
href={LINK_KINDS[kind].href(href)}
target={kind === 'mail' ? undefined : '_blank'}
rel="noopener noreferrer"
title={href}
className={`inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-ink-100 ${
kind === 'linkedin' ? 'text-[#0a66c2]' : 'text-brand-600'
}`}
className={`inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-ink-100 ${LINK_KINDS[kind].color}`}
>
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
<path d={kind === 'linkedin' ? LINKEDIN_ICON : LINK_ICON} />
<path d={kind === 'linkedin' ? LINKEDIN_ICON : kind === 'mail' ? MAIL_ICON : LINK_ICON} />
</svg>
</a>
);
@@ -607,6 +611,7 @@ function leadFromSuspect(row: Suspect, companies: Company[], contacts: Contact[]
const notes = [
`Aus Suspects: ${row.name}${row.role ? `, ${row.role}` : ''}${row.company ? ` (${row.company})` : ''}`,
row.email ? `Mail: ${row.email}` : null,
row.linkedin ? `LinkedIn: ${row.linkedin}` : null,
row.website ? `Website: ${row.website}` : null,
row.last_contact_on ? `Letzter Kontakt: ${formatDate(row.last_contact_on)}` : null,
@@ -634,5 +639,6 @@ const ALARM_ICON =
'M12 5a8 8 0 1 1 0 16 8 8 0 0 1 0-16Zm0 2a6 6 0 1 0 0 12 6 6 0 0 0 0-12Zm-1 2h2v4l3 2-1 1.6-4-2.6V9ZM6.3 2.3l1.4 1.4-3.9 3.9-1.4-1.4 3.9-3.9Zm11.4 0 3.9 3.9-1.4 1.4-3.9-3.9 1.4-1.4Z';
const LINK_ICON =
'M10.6 13.4a1 1 0 0 1 0-1.4l3.5-3.5a1 1 0 1 1 1.4 1.4L12 13.4a1 1 0 0 1-1.4 0ZM8.5 11.3 6.4 13.4a3 3 0 0 0 4.2 4.2l2.1-2.1 1.4 1.4-2.1 2.1a5 5 0 0 1-7-7l2.1-2.1 1.4 1.4Zm7-5.7a3 3 0 0 0-4.2 0L9.2 7.7 7.8 6.3l2.1-2.1a5 5 0 0 1 7 7l-2.1 2.1-1.4-1.4 2.1-2.1a3 3 0 0 0 0-4.2Z';
const MAIL_ICON = 'M3 5h18v14H3V5Zm2 2v.4l7 4.6 7-4.6V7H5Zm14 2.8-7 4.6-7-4.6V17h14V9.8Z';
const LINKEDIN_ICON =
'M4 3h16a1 1 0 0 1 1 1v16a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1Zm3.3 7H5v8h2.3v-8ZM6.2 5.8a1.3 1.3 0 1 0 0 2.7 1.3 1.3 0 0 0 0-2.7ZM19 13.4c0-2.2-1.2-3.5-3.1-3.5-1.1 0-1.9.6-2.2 1.1V10h-2.2v8h2.3v-4.2c0-1.1.5-1.8 1.4-1.8.8 0 1.3.6 1.3 1.8V18H19v-4.6Z';
+2 -1
View File
@@ -440,6 +440,7 @@ export interface Suspect {
name: string;
role: string | null;
company: string | null;
email: string | null;
linkedin: string | null;
website: string | null;
notes: string | null;
@@ -454,7 +455,7 @@ export interface Suspect {
}
export type SuspectInput = Partial<
Pick<Suspect, 'name' | 'role' | 'company' | 'linkedin' | 'website' | 'notes' | 'last_contact_on' | 'archived' | 'lead_id'>
Pick<Suspect, 'name' | 'role' | 'company' | 'email' | 'linkedin' | 'website' | 'notes' | 'last_contact_on' | 'archived' | 'lead_id'>
> & { remind_in_days?: number | null };
export const suspectsApi = {