Quellcode des ICON CRM (3)
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
import { pool } from './db.js';
|
||||
|
||||
/*
|
||||
* Historie und Erwaehnungen.
|
||||
*
|
||||
* Jede schreibende Route ruft hier herein, statt selbst in icon_history zu
|
||||
* schreiben. Damit gilt an einer Stelle, was die Featureliste verlangt: in
|
||||
* jeder Historie steht, welcher Benutzer was geaendert hat, und jedes @ im
|
||||
* Text taucht als eigener Eintrag auf.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mention-Token im Text: `@[Anzeigename](contact:12)`.
|
||||
*
|
||||
* Der Name steht mit im Token, damit ein Text auch ohne geladene Listen
|
||||
* lesbar bleibt; beim Rendern gewinnt der aktuelle Name aus den Daten.
|
||||
*/
|
||||
const MENTION_TOKEN = /@\[([^\]\n]+)\]\((contact|company|user):(\d+)\)/g;
|
||||
|
||||
export function extractMentions(text) {
|
||||
if (!text) return [];
|
||||
const found = new Map();
|
||||
MENTION_TOKEN.lastIndex = 0;
|
||||
let match;
|
||||
while ((match = MENTION_TOKEN.exec(String(text))) !== null) {
|
||||
const key = `${match[2]}:${match[3]}`;
|
||||
if (!found.has(key)) found.set(key, { type: match[2], id: Number(match[3]), label: match[1] });
|
||||
}
|
||||
return [...found.values()];
|
||||
}
|
||||
|
||||
/** Text ohne Token-Syntax - fuer Suche und Vorschauen. */
|
||||
export function mentionPlainText(text) {
|
||||
if (!text) return '';
|
||||
return String(text).replace(MENTION_TOKEN, (_all, label) => `@${label}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schreibt einen Historieneintrag und indiziert die darin enthaltenen
|
||||
* Erwaehnungen.
|
||||
*
|
||||
* @param {object} entry
|
||||
* @param {'contact'|'company'|'tender'|'project'|'lead'|'user'|'system'} entry.entityType
|
||||
* @param {number|null} entry.entityId
|
||||
* @param {string|null} entry.entityLabel Name zum Zeitpunkt des Eintrags
|
||||
* @param {string} entry.action created | updated | deleted | note | ...
|
||||
* @param {string|null} entry.text
|
||||
* @param {object|null} entry.changes { feld: [vorher, nachher] }
|
||||
* @param {number|null} entry.userId
|
||||
* @returns {Promise<number>} id des Eintrags
|
||||
*/
|
||||
export async function logHistory({
|
||||
entityType,
|
||||
entityId = null,
|
||||
entityLabel = null,
|
||||
action = 'note',
|
||||
text = null,
|
||||
changes = null,
|
||||
userId = null,
|
||||
}) {
|
||||
const [result] = await pool.query(
|
||||
`INSERT INTO icon_history (entity_type, entity_id, entity_label, action, text, changes, user_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
||||
[
|
||||
entityType,
|
||||
entityId,
|
||||
entityLabel ? String(entityLabel).slice(0, 300) : null,
|
||||
action,
|
||||
text ?? null,
|
||||
changes && Object.keys(changes).length > 0 ? JSON.stringify(changes) : null,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
|
||||
const historyId = result.insertId;
|
||||
await indexMentions('history', historyId, text);
|
||||
return historyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traegt die Erwaehnungen eines Textes in icon_mentions nach.
|
||||
*
|
||||
* Erst loeschen, dann neu schreiben: so verschwindet ein @, das aus einer
|
||||
* Notiz wieder entfernt wurde, auch aus dem Index.
|
||||
*/
|
||||
export async function indexMentions(sourceType, sourceId, text) {
|
||||
const mentions = extractMentions(text);
|
||||
await pool.query('DELETE FROM icon_mentions WHERE source_type = ? AND source_id = ?', [
|
||||
sourceType,
|
||||
sourceId,
|
||||
]);
|
||||
if (mentions.length === 0) return [];
|
||||
|
||||
const values = mentions.flatMap((m) => [sourceType, sourceId, m.type, m.id]);
|
||||
await pool.query(
|
||||
`INSERT IGNORE INTO icon_mentions (source_type, source_id, target_type, target_id)
|
||||
VALUES ${mentions.map(() => '(?, ?, ?, ?)').join(', ')}`,
|
||||
values
|
||||
);
|
||||
return mentions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vergleicht alten und neuen Datensatz und liefert nur die geaenderten Felder.
|
||||
*
|
||||
* Verglichen wird ueber String(), weil MariaDB Zahlen mal als Number und mal
|
||||
* als String liefert (DECIMAL etwa immer als String) - ein strikter Vergleich
|
||||
* wuerde daraus eine Scheinaenderung machen.
|
||||
*/
|
||||
export function diffFields(before, after, fields) {
|
||||
const changes = {};
|
||||
for (const field of fields) {
|
||||
if (after[field] === undefined) continue;
|
||||
const from = before?.[field] ?? null;
|
||||
const to = after[field] ?? null;
|
||||
if (String(from ?? '') === String(to ?? '')) continue;
|
||||
changes[field] = [from, to];
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
/** Menschenlesbare Namen der Felder - fuer den Text in der Historie. */
|
||||
export const FIELD_LABELS = {
|
||||
name: 'Name',
|
||||
first_name: 'Vorname',
|
||||
last_name: 'Nachname',
|
||||
position: 'Position',
|
||||
email: 'E-Mail',
|
||||
phone: 'Telefon',
|
||||
linkedin: 'LinkedIn',
|
||||
company_id: 'Unternehmen',
|
||||
kind: 'Art',
|
||||
status: 'Status',
|
||||
lead_source: 'Lead-Quelle',
|
||||
contact_state: 'Kontaktstand',
|
||||
freelancers: 'Freelancer-Einsatz',
|
||||
bio: 'Notiz',
|
||||
tags: 'Schlagworte',
|
||||
rate_onsite: 'Stundensatz vor Ort',
|
||||
rate_remote: 'Stundensatz remote',
|
||||
cv_url: 'CV-Link',
|
||||
avatar: 'Foto',
|
||||
owner_id: 'Zustaendig',
|
||||
group_name: 'Gruppe',
|
||||
industry: 'Branche',
|
||||
employees: 'Mitarbeiterzahl',
|
||||
website: 'Website',
|
||||
address: 'Adresse',
|
||||
logo: 'Logo',
|
||||
rating: 'Bewertung',
|
||||
is_customer: 'Kunde',
|
||||
customer_state: 'Kundenstatus',
|
||||
notes: 'Notizen',
|
||||
title: 'Titel',
|
||||
source: 'Quelle',
|
||||
link: 'Link',
|
||||
stage: 'Phase',
|
||||
online: 'Online',
|
||||
details: 'Details',
|
||||
deadline: 'Frist',
|
||||
started_on: 'Beginn',
|
||||
ended_on: 'Ende',
|
||||
rate: 'Satz',
|
||||
contact_id: 'Kontakt',
|
||||
archived: 'Archiviert',
|
||||
remind_at: 'Erinnerung',
|
||||
reminder_days: 'Erinnerungsintervall',
|
||||
};
|
||||
|
||||
/** "Status, Telefon und 2 weitere Felder geaendert" */
|
||||
export function describeChanges(changes) {
|
||||
const labels = Object.keys(changes).map((key) => FIELD_LABELS[key] || key);
|
||||
if (labels.length === 0) return 'ohne Änderung gespeichert';
|
||||
if (labels.length <= 3) return `${labels.join(', ')} geändert`;
|
||||
return `${labels.slice(0, 2).join(', ')} und ${labels.length - 2} weitere Felder geändert`;
|
||||
}
|
||||
Reference in New Issue
Block a user