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:
co-authored by
Claude Opus 5.5
parent
6a452f1510
commit
78d6bef52c
@@ -36,14 +36,15 @@ siehe [13 Suspects](13-suspects.md).
|
|||||||
1. **Erfassen:** in die leere Zeile unten schreiben (Name ist Pflicht). Die Zeile
|
1. **Erfassen:** in die leere Zeile unten schreiben (Name ist Pflicht). Die Zeile
|
||||||
speichert, sobald du sie verlässt oder Enter drückst. Vorhandene Zellen
|
speichert, sobald du sie verlässt oder Enter drückst. Vorhandene Zellen
|
||||||
einfach anklicken und ändern.
|
einfach anklicken und ändern.
|
||||||
2. **Links:** LinkedIn und Website werden nach dem Speichern zum Symbol. Die
|
2. **Links:** Mail, LinkedIn und Website werden nach dem Speichern zum Symbol.
|
||||||
Adresse steht im Tooltip, ein Klick öffnet sie. Zum Ändern auf den kleinen
|
Die Adresse steht im Tooltip, ein Klick öffnet sie (Mail: das Mailprogramm).
|
||||||
Stift daneben.
|
Zum Ändern auf den kleinen Stift daneben.
|
||||||
3. **Nächster Impuls:** Wecker anklicken, Anzahl Tage eingeben, *Speichern*. Ist
|
3. **Nächster Impuls:** Wecker anklicken, Anzahl Tage eingeben, *Speichern*. Ist
|
||||||
der Tag erreicht, erinnert das CRM daran, die Person erneut anzusprechen.
|
der Tag erreicht, erinnert das CRM daran, die Person erneut anzusprechen.
|
||||||
*Kontakt aufgenommen* trägt heute als letzten Kontakt ein.
|
*Kontakt aufgenommen* trägt heute als letzten Kontakt ein.
|
||||||
4. **Archivieren** (Kasten-Symbol am Zeilenende): die Zeile wandert nach
|
4. **Archivieren** (Kasten-Symbol am Zeilenende): die Zeile wandert nach
|
||||||
*Suspects → Archiv*. Dort *Zurückholen* oder *Endgültig löschen*.
|
*Suspects → Archiv*. Dort *Zurückholen* oder *Endgültig löschen*.
|
||||||
|
**Löschen** (rotes Symbol) entfernt die Zeile sofort endgültig – nach Rückfrage.
|
||||||
5. **Lead erstellen** (Symbol daneben): die Lead-Karte öffnet sich mit allen
|
5. **Lead erstellen** (Symbol daneben): die Lead-Karte öffnet sich mit allen
|
||||||
bekannten Daten. Titel eintragen, *Speichern* – der Lead steht dann unter
|
bekannten Daten. Titel eintragen, *Speichern* – der Lead steht dann unter
|
||||||
*Neuer Lead*, der Suspect im Archiv.
|
*Neuer Lead*, der Suspect im Archiv.
|
||||||
|
|||||||
+10
-6
@@ -21,6 +21,7 @@ flowchart LR
|
|||||||
| Name | `name` | Pflicht – ohne Namen wird eine neue Zeile nicht gespeichert |
|
| Name | `name` | Pflicht – ohne Namen wird eine neue Zeile nicht gespeichert |
|
||||||
| Funktion | `role` | Freitext |
|
| Funktion | `role` | Freitext |
|
||||||
| Unternehmen | `company` | Freitext, **kein** Verweis auf `icon_companies` |
|
| Unternehmen | `company` | Freitext, **kein** Verweis auf `icon_companies` |
|
||||||
|
| Mail | `email` | nach dem Speichern nur ein Briefsymbol, Adresse im Tooltip, Klick öffnet das Mailprogramm |
|
||||||
| LinkedIn | `linkedin` | nach dem Speichern nur ein Symbol, URL im Tooltip |
|
| LinkedIn | `linkedin` | nach dem Speichern nur ein Symbol, URL im Tooltip |
|
||||||
| Website | `website` | ebenso; fehlt `https://`, ergänzt es der Server |
|
| Website | `website` | ebenso; fehlt `https://`, ergänzt es der Server |
|
||||||
| Notiz | `notes` | einzeilig, volle Länge im Tooltip |
|
| Notiz | `notes` | einzeilig, volle Länge im Tooltip |
|
||||||
@@ -56,19 +57,21 @@ Das Lead-Symbol am Zeilenende öffnet die **Lead-Karte vorbelegt**
|
|||||||
- Phase **Neuer Lead**, zuständig der angemeldete Benutzer
|
- Phase **Neuer Lead**, zuständig der angemeldete Benutzer
|
||||||
- **Unternehmen** und **Ansprechpartner** nur, wenn es im CRM einen mit gleichem
|
- **Unternehmen** und **Ansprechpartner** nur, wenn es im CRM einen mit gleichem
|
||||||
Namen gibt (Groß/klein, Rechtsform und Satzzeichen egal)
|
Namen gibt (Groß/klein, Rechtsform und Satzzeichen egal)
|
||||||
- **Notiz** mit allem aus der Zeile: Name, Funktion, Unternehmen, LinkedIn,
|
- **Notiz** mit allem aus der Zeile: Name, Funktion, Unternehmen, Mail, LinkedIn,
|
||||||
Website, letzter Kontakt, Notiz
|
Website, letzter Kontakt, Notiz
|
||||||
- **Titel bleibt leer** – der ist Pflicht und muss eingetragen werden
|
- **Titel bleibt leer** – der ist Pflicht und muss eingetragen werden
|
||||||
|
|
||||||
Erst beim Speichern entsteht der Lead; danach wandert der Suspect ins Archiv
|
Erst beim Speichern entsteht der Lead; danach wandert der Suspect ins Archiv
|
||||||
und trägt `lead_id` (im Archiv als **Lead** markiert). Abbrechen ändert nichts.
|
und trägt `lead_id` (im Archiv als **Lead** markiert). Abbrechen ändert nichts.
|
||||||
|
|
||||||
## Archiv
|
## Archiv und Löschen
|
||||||
|
|
||||||
Archivierte und in Leads übernommene Suspects, neueste zuerst. **Zurückholen**
|
Archivierte und in Leads übernommene Suspects, neueste zuerst. **Zurückholen**
|
||||||
stellt die Zeile wieder in die Tabelle, **Endgültig löschen** entfernt sie
|
stellt die Zeile wieder in die Tabelle, **Endgültig löschen** entfernt sie.
|
||||||
(mit Rückfrage). Löschen geht nur aus dem Archiv – der Server antwortet sonst
|
|
||||||
mit `409`.
|
Löschen geht auch direkt in der Tabelle (rotes Symbol am Zeilenende) – beide
|
||||||
|
Wege fragen vorher nach. Anfangs war Löschen nur aus dem Archiv möglich; auf
|
||||||
|
Wunsch vom 29.09.2026 gibt es den Knopf jetzt auch in der Tabelle.
|
||||||
|
|
||||||
## Tabelle `icon_suspects`
|
## Tabelle `icon_suspects`
|
||||||
|
|
||||||
@@ -76,6 +79,7 @@ mit `409`.
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `id` | INT | |
|
| `id` | INT | |
|
||||||
| `name`, `role`, `company` | VARCHAR(190) | |
|
| `name`, `role`, `company` | VARCHAR(190) | |
|
||||||
|
| `email` | VARCHAR(190) | ohne `mailto:`; nachträglich per `migrateColumns` ergänzt |
|
||||||
| `linkedin`, `website` | VARCHAR(500) | immer mit Protokoll gespeichert |
|
| `linkedin`, `website` | VARCHAR(500) | immer mit Protokoll gespeichert |
|
||||||
| `notes` | TEXT | |
|
| `notes` | TEXT | |
|
||||||
| `last_contact_on` | DATE | |
|
| `last_contact_on` | DATE | |
|
||||||
@@ -99,7 +103,7 @@ Alle unter `/api/suspects`, alle hinter `requireAuth`.
|
|||||||
| POST | `/` | anlegen, `name` Pflicht |
|
| POST | `/` | anlegen, `name` Pflicht |
|
||||||
| PATCH | `/:id` | beliebige Felder; `remind_in_days` (Zahl oder `null`), `archived`, `lead_id` |
|
| PATCH | `/:id` | beliebige Felder; `remind_in_days` (Zahl oder `null`), `archived`, `lead_id` |
|
||||||
| POST | `/:id/acknowledge` | `{ snooze_days }` verschiebt, sonst Kontakt heute + Impuls weg |
|
| POST | `/:id/acknowledge` | `{ snooze_days }` verschiebt, sonst Kontakt heute + Impuls weg |
|
||||||
| DELETE | `/:id` | nur archivierte (`409` sonst) |
|
| DELETE | `/:id` | endgültig löschen (aus Tabelle oder Archiv) |
|
||||||
|
|
||||||
Die Zähler im Menü kommen aus `GET /api/dashboard`
|
Die Zähler im Menü kommen aus `GET /api/dashboard`
|
||||||
(`counts.suspects_open`, `counts.suspects_archived`).
|
(`counts.suspects_open`, `counts.suspects_archived`).
|
||||||
|
|||||||
@@ -179,6 +179,12 @@ async function migrateColumns() {
|
|||||||
'title',
|
'title',
|
||||||
'ALTER TABLE icon_contacts ADD COLUMN title VARCHAR(60) NULL AFTER id',
|
'ALTER TABLE icon_contacts ADD COLUMN title VARCHAR(60) NULL AFTER id',
|
||||||
],
|
],
|
||||||
|
// Mail-Adresse der Suspects - kam am Tag der Einfuehrung dazu (29.09.2026).
|
||||||
|
[
|
||||||
|
'icon_suspects',
|
||||||
|
'email',
|
||||||
|
'ALTER TABLE icon_suspects ADD COLUMN email VARCHAR(190) NULL AFTER company',
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'icon_cv_imports',
|
'icon_cv_imports',
|
||||||
'photo',
|
'photo',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { logHistory } from '../history.js';
|
|||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
router.use(requireAuth);
|
router.use(requireAuth);
|
||||||
|
|
||||||
const TEXT_FIELDS = { name: 190, role: 190, company: 190, linkedin: 500, website: 500, notes: 5000 };
|
const TEXT_FIELDS = { name: 190, role: 190, company: 190, email: 190, linkedin: 500, website: 500, notes: 5000 };
|
||||||
|
|
||||||
function readBody(body) {
|
function readBody(body) {
|
||||||
const out = {};
|
const out = {};
|
||||||
@@ -26,6 +26,8 @@ function readBody(body) {
|
|||||||
for (const field of ['linkedin', 'website']) {
|
for (const field of ['linkedin', 'website']) {
|
||||||
if (out[field] && !/^https?:\/\//i.test(out[field])) out[field] = `https://${out[field]}`;
|
if (out[field] && !/^https?:\/\//i.test(out[field])) out[field] = `https://${out[field]}`;
|
||||||
}
|
}
|
||||||
|
// "mailto:" aus kopierten Links entfernen - gespeichert wird die blanke Adresse.
|
||||||
|
if (out.email) out.email = out.email.replace(/^mailto:/i, '');
|
||||||
if (body.last_contact_on !== undefined) {
|
if (body.last_contact_on !== undefined) {
|
||||||
out.last_contact_on = /^\d{4}-\d{2}-\d{2}$/.test(String(body.last_contact_on ?? ''))
|
out.last_contact_on = /^\d{4}-\d{2}-\d{2}$/.test(String(body.last_contact_on ?? ''))
|
||||||
? body.last_contact_on
|
? body.last_contact_on
|
||||||
@@ -152,14 +154,13 @@ router.post('/:id/acknowledge', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Endgueltig loeschen - nur aus dem Archiv, damit nichts aus Versehen verschwindet. */
|
/** Endgueltig loeschen - aus der Tabelle wie aus dem Archiv (die Oberflaeche fragt nach). */
|
||||||
router.delete('/:id', async (req, res, next) => {
|
router.delete('/:id', async (req, res, next) => {
|
||||||
const id = Number(req.params.id);
|
const id = Number(req.params.id);
|
||||||
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Kennung.' });
|
if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Kennung.' });
|
||||||
try {
|
try {
|
||||||
const [[row]] = await pool.query('SELECT name, archived FROM icon_suspects WHERE id = ?', [id]);
|
const [[row]] = await pool.query('SELECT name FROM icon_suspects WHERE id = ?', [id]);
|
||||||
if (!row) return res.status(404).json({ error: 'Suspect nicht gefunden.' });
|
if (!row) return res.status(404).json({ error: 'Suspect nicht gefunden.' });
|
||||||
if (!row.archived) return res.status(409).json({ error: 'Bitte zuerst archivieren.' });
|
|
||||||
await pool.query('DELETE FROM icon_suspects WHERE id = ?', [id]);
|
await pool.query('DELETE FROM icon_suspects WHERE id = ?', [id]);
|
||||||
await logHistory({
|
await logHistory({
|
||||||
entityType: 'system',
|
entityType: 'system',
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ CREATE TABLE IF NOT EXISTS icon_suspects (
|
|||||||
name VARCHAR(190) NOT NULL,
|
name VARCHAR(190) NOT NULL,
|
||||||
role VARCHAR(190) NULL,
|
role VARCHAR(190) NULL,
|
||||||
company VARCHAR(190) NULL,
|
company VARCHAR(190) NULL,
|
||||||
|
email VARCHAR(190) NULL,
|
||||||
linkedin VARCHAR(500) NULL,
|
linkedin VARCHAR(500) NULL,
|
||||||
website VARCHAR(500) NULL,
|
website VARCHAR(500) NULL,
|
||||||
notes TEXT NULL,
|
notes TEXT NULL,
|
||||||
|
|||||||
@@ -31,12 +31,13 @@ interface SuspectsProps {
|
|||||||
onChanged: () => void;
|
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 } = {
|
const EMPTY_DRAFT: Record<TextField, string> & { last_contact_on: string } = {
|
||||||
name: '',
|
name: '',
|
||||||
role: '',
|
role: '',
|
||||||
company: '',
|
company: '',
|
||||||
|
email: '',
|
||||||
linkedin: '',
|
linkedin: '',
|
||||||
website: '',
|
website: '',
|
||||||
notes: '',
|
notes: '',
|
||||||
@@ -161,23 +162,25 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="overflow-x-auto rounded-xl border border-ink-200 bg-surface">
|
<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>
|
<colgroup>
|
||||||
<col className="w-[15%]" />
|
<col className="w-[15%]" />
|
||||||
<col className="w-[14%]" />
|
<col className="w-[14%]" />
|
||||||
<col className="w-[14%]" />
|
<col className="w-[14%]" />
|
||||||
|
<col className="w-[64px]" />
|
||||||
<col className="w-[72px]" />
|
<col className="w-[72px]" />
|
||||||
<col className="w-[72px]" />
|
<col className="w-[72px]" />
|
||||||
<col />
|
<col />
|
||||||
<col className="w-[130px]" />
|
<col className="w-[130px]" />
|
||||||
<col className="w-[120px]" />
|
<col className="w-[120px]" />
|
||||||
<col className="w-[96px]" />
|
<col className="w-[128px]" />
|
||||||
</colgroup>
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-ink-200 text-left text-[11px] font-semibold uppercase tracking-wider text-ink-500">
|
<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">Name</th>
|
||||||
<th className="px-3 py-3">Funktion</th>
|
<th className="px-3 py-3">Funktion</th>
|
||||||
<th className="px-3 py-3">Unternehmen</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">LinkedIn</th>
|
||||||
<th className="px-3 py-3 text-center">Website</th>
|
<th className="px-3 py-3 text-center">Website</th>
|
||||||
<th className="px-3 py-3">Notiz</th>
|
<th className="px-3 py-3">Notiz</th>
|
||||||
@@ -206,6 +209,9 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
|||||||
/>
|
/>
|
||||||
</td>
|
</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">
|
<td className="px-1 py-1 text-center">
|
||||||
<LinkCell value={row.linkedin} kind="linkedin" onCommit={(value) => void update(row, { linkedin: value })} />
|
<LinkCell value={row.linkedin} kind="linkedin" onCommit={(value) => void update(row, { linkedin: value })} />
|
||||||
</td>
|
</td>
|
||||||
@@ -231,6 +237,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
|||||||
<div className="flex justify-end gap-1">
|
<div className="flex justify-end gap-1">
|
||||||
<IconButton title="Archivieren" onClick={() => void archive(row)} path={ARCHIVE_ICON} />
|
<IconButton title="Archivieren" onClick={() => void archive(row)} path={ARCHIVE_ICON} />
|
||||||
<IconButton title="Lead erstellen" onClick={() => setToLead(row)} path={LEAD_ICON} tone="brand" />
|
<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>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</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">
|
<td key={field} className="px-1 py-1">
|
||||||
<input
|
<input
|
||||||
ref={field === 'name' ? draftName : undefined}
|
ref={field === 'name' ? draftName : undefined}
|
||||||
@@ -278,7 +285,7 @@ export const Suspects: React.FC<SuspectsProps> = ({ archived, companies, contact
|
|||||||
|
|
||||||
{archived && rows.length === 0 && (
|
{archived && rows.length === 0 && (
|
||||||
<tr>
|
<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.
|
Das Archiv ist leer.
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -328,6 +335,7 @@ const DRAFT_PLACEHOLDERS: Record<TextField, string> = {
|
|||||||
name: '+ Name',
|
name: '+ Name',
|
||||||
role: 'Funktion',
|
role: 'Funktion',
|
||||||
company: 'Unternehmen',
|
company: 'Unternehmen',
|
||||||
|
email: 'Mail',
|
||||||
linkedin: 'LinkedIn-URL',
|
linkedin: 'LinkedIn-URL',
|
||||||
website: 'Website',
|
website: 'Website',
|
||||||
notes: 'Notiz',
|
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. */
|
/** 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,
|
value,
|
||||||
kind,
|
kind,
|
||||||
onCommit,
|
onCommit,
|
||||||
@@ -399,7 +415,8 @@ const LinkCell: React.FC<{ value: string | null; kind: 'linkedin' | 'website'; o
|
|||||||
setEditing(false);
|
setEditing(false);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="URL"
|
type={kind === 'mail' ? 'email' : 'text'}
|
||||||
|
placeholder={LINK_KINDS[kind].placeholder}
|
||||||
className={`${cellInput} text-xs`}
|
className={`${cellInput} text-xs`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -407,23 +424,11 @@ const LinkCell: React.FC<{ value: string | null; kind: 'linkedin' | 'website'; o
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<span className="group inline-flex items-center gap-0.5">
|
<span className="group inline-flex items-center gap-0.5">
|
||||||
<a
|
<StaticLink href={value} kind={kind} />
|
||||||
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>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditing(true)}
|
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"
|
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">
|
<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>
|
||||||
<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.role ?? '–'}</td>
|
||||||
<td className="truncate px-3 py-2.5 text-ink-600">{row.company ?? '–'}</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.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="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}>
|
<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>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
const StaticLink: React.FC<{ href: string; kind: 'linkedin' | 'website' }> = ({ href, kind }) => (
|
const StaticLink: React.FC<{ href: string; kind: LinkKind }> = ({ href, kind }) => (
|
||||||
<a
|
<a
|
||||||
href={href}
|
href={LINK_KINDS[kind].href(href)}
|
||||||
target="_blank"
|
target={kind === 'mail' ? undefined : '_blank'}
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
title={href}
|
title={href}
|
||||||
className={`inline-flex h-7 w-7 items-center justify-center rounded-md hover:bg-ink-100 ${
|
className={`inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-ink-100 ${LINK_KINDS[kind].color}`}
|
||||||
kind === 'linkedin' ? 'text-[#0a66c2]' : 'text-brand-600'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
|
<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>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@@ -607,6 +611,7 @@ function leadFromSuspect(row: Suspect, companies: Company[], contacts: Contact[]
|
|||||||
|
|
||||||
const notes = [
|
const notes = [
|
||||||
`Aus Suspects: ${row.name}${row.role ? `, ${row.role}` : ''}${row.company ? ` (${row.company})` : ''}`,
|
`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.linkedin ? `LinkedIn: ${row.linkedin}` : null,
|
||||||
row.website ? `Website: ${row.website}` : null,
|
row.website ? `Website: ${row.website}` : null,
|
||||||
row.last_contact_on ? `Letzter Kontakt: ${formatDate(row.last_contact_on)}` : 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';
|
'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 =
|
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';
|
'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 =
|
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';
|
'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
@@ -440,6 +440,7 @@ export interface Suspect {
|
|||||||
name: string;
|
name: string;
|
||||||
role: string | null;
|
role: string | null;
|
||||||
company: string | null;
|
company: string | null;
|
||||||
|
email: string | null;
|
||||||
linkedin: string | null;
|
linkedin: string | null;
|
||||||
website: string | null;
|
website: string | null;
|
||||||
notes: string | null;
|
notes: string | null;
|
||||||
@@ -454,7 +455,7 @@ export interface Suspect {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type SuspectInput = Partial<
|
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 };
|
> & { remind_in_days?: number | null };
|
||||||
|
|
||||||
export const suspectsApi = {
|
export const suspectsApi = {
|
||||||
|
|||||||
Reference in New Issue
Block a user