diff --git a/docs/08-anleitung-benutzer.md b/docs/08-anleitung-benutzer.md index d8e560a..2973169 100644 --- a/docs/08-anleitung-benutzer.md +++ b/docs/08-anleitung-benutzer.md @@ -36,14 +36,15 @@ siehe [13 Suspects](13-suspects.md). 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 einfach anklicken und ändern. -2. **Links:** LinkedIn und Website werden nach dem Speichern zum Symbol. Die - Adresse steht im Tooltip, ein Klick öffnet sie. Zum Ändern auf den kleinen - Stift daneben. +2. **Links:** Mail, LinkedIn und Website werden nach dem Speichern zum Symbol. + Die Adresse steht im Tooltip, ein Klick öffnet sie (Mail: das Mailprogramm). + Zum Ändern auf den kleinen Stift daneben. 3. **Nächster Impuls:** Wecker anklicken, Anzahl Tage eingeben, *Speichern*. Ist der Tag erreicht, erinnert das CRM daran, die Person erneut anzusprechen. *Kontakt aufgenommen* trägt heute als letzten Kontakt ein. 4. **Archivieren** (Kasten-Symbol am Zeilenende): die Zeile wandert nach *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 bekannten Daten. Titel eintragen, *Speichern* – der Lead steht dann unter *Neuer Lead*, der Suspect im Archiv. diff --git a/docs/13-suspects.md b/docs/13-suspects.md index 1f4781f..0997eb2 100644 --- a/docs/13-suspects.md +++ b/docs/13-suspects.md @@ -21,6 +21,7 @@ flowchart LR | Name | `name` | Pflicht – ohne Namen wird eine neue Zeile nicht gespeichert | | Funktion | `role` | Freitext | | 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 | | Website | `website` | ebenso; fehlt `https://`, ergänzt es der Server | | 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 - **Unternehmen** und **Ansprechpartner** nur, wenn es im CRM einen mit gleichem 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 - **Titel bleibt leer** – der ist Pflicht und muss eingetragen werden 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. -## Archiv +## Archiv und Löschen Archivierte und in Leads übernommene Suspects, neueste zuerst. **Zurückholen** -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`. +stellt die Zeile wieder in die Tabelle, **Endgültig löschen** entfernt sie. + +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` @@ -76,6 +79,7 @@ mit `409`. | --- | --- | --- | | `id` | INT | | | `name`, `role`, `company` | VARCHAR(190) | | +| `email` | VARCHAR(190) | ohne `mailto:`; nachträglich per `migrateColumns` ergänzt | | `linkedin`, `website` | VARCHAR(500) | immer mit Protokoll gespeichert | | `notes` | TEXT | | | `last_contact_on` | DATE | | @@ -99,7 +103,7 @@ Alle unter `/api/suspects`, alle hinter `requireAuth`. | POST | `/` | anlegen, `name` Pflicht | | PATCH | `/:id` | beliebige Felder; `remind_in_days` (Zahl oder `null`), `archived`, `lead_id` | | 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` (`counts.suspects_open`, `counts.suspects_archived`). diff --git a/server/src/db.js b/server/src/db.js index e615f7b..482e1fc 100644 --- a/server/src/db.js +++ b/server/src/db.js @@ -179,6 +179,12 @@ async function migrateColumns() { 'title', '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', 'photo', diff --git a/server/src/routes/suspects.js b/server/src/routes/suspects.js index 4d6e536..c97d527 100644 --- a/server/src/routes/suspects.js +++ b/server/src/routes/suspects.js @@ -13,7 +13,7 @@ import { logHistory } from '../history.js'; const router = express.Router(); 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) { const out = {}; @@ -26,6 +26,8 @@ function readBody(body) { for (const field of ['linkedin', 'website']) { 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) { out.last_contact_on = /^\d{4}-\d{2}-\d{2}$/.test(String(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) => { const id = Number(req.params.id); if (!Number.isInteger(id)) return res.status(400).json({ error: 'Ungültige Kennung.' }); 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.archived) return res.status(409).json({ error: 'Bitte zuerst archivieren.' }); await pool.query('DELETE FROM icon_suspects WHERE id = ?', [id]); await logHistory({ entityType: 'system', diff --git a/server/src/schema.sql b/server/src/schema.sql index 85fe120..bba6f32 100644 --- a/server/src/schema.sql +++ b/server/src/schema.sql @@ -356,6 +356,7 @@ CREATE TABLE IF NOT EXISTS icon_suspects ( name VARCHAR(190) NOT NULL, role VARCHAR(190) NULL, company VARCHAR(190) NULL, + email VARCHAR(190) NULL, linkedin VARCHAR(500) NULL, website VARCHAR(500) NULL, notes TEXT NULL, diff --git a/src/components/suspects/Suspects.tsx b/src/components/suspects/Suspects.tsx index df038bb..2784683 100644 --- a/src/components/suspects/Suspects.tsx +++ b/src/components/suspects/Suspects.tsx @@ -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 & { last_contact_on: string } = { name: '', role: '', company: '', + email: '', linkedin: '', website: '', notes: '', @@ -161,23 +162,25 @@ export const Suspects: React.FC = ({ archived, companies, contact

- +
+ - + + @@ -206,6 +209,9 @@ export const Suspects: React.FC = ({ archived, companies, contact /> ))} + @@ -231,6 +237,7 @@ export const Suspects: React.FC = ({ archived, companies, contact
void archive(row)} path={ARCHIVE_ICON} /> setToLead(row)} path={LEAD_ICON} tone="brand" /> + setRemoving(row)} path={DELETE_ICON} tone="red" />
@@ -251,7 +258,7 @@ export const Suspects: React.FC = ({ archived, companies, contact } }} > - {(['name', 'role', 'company', 'linkedin', 'website', 'notes'] as const).map((field) => ( + {(['name', 'role', 'company', 'email', 'linkedin', 'website', 'notes'] as const).map((field) => ( - @@ -328,6 +335,7 @@ const DRAFT_PLACEHOLDERS: Record = { 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 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 ( - - - +
Name Funktion UnternehmenMail LinkedIn Website Notiz + void update(row, { email: value })} /> + void update(row, { linkedin: value })} />
= ({ archived, companies, contact {archived && rows.length === 0 && (
+ Das Archiv ist leer.