Feat: Complete smart contact matching, CRM features integration, and premium glassmorphism UI
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 10s
All checks were successful
Build & Push Friends Image to Gitea Registry / build-and-push (push) Successful in 10s
This commit is contained in:
216
server.js
216
server.js
@@ -37,7 +37,8 @@ app.get("/api/contacts", async (req, res) => {
|
||||
}
|
||||
|
||||
try {
|
||||
const suffix = query ? `?query=${encodeURIComponent(query)}` : "";
|
||||
const limit = 150;
|
||||
const suffix = query ? `?query=${encodeURIComponent(query)}&limit=${limit}` : `?limit=${limit}`;
|
||||
const payload = await monicaFetch(`/api/contacts${suffix}`);
|
||||
const contacts = Array.isArray(payload.data) ? payload.data : [];
|
||||
res.json({
|
||||
@@ -53,16 +54,160 @@ app.get("/api/contacts", async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.get("/api/contacts/:id", async (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!monicaApiToken) {
|
||||
res.json(demoContactDetails(id));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contactPayload = await monicaFetch(`/api/contacts/${id}`);
|
||||
const contact = contactPayload.data || {};
|
||||
|
||||
let emails = [];
|
||||
let phones = [];
|
||||
try {
|
||||
const fieldsPayload = await monicaFetch(`/api/contacts/${id}/contactfields`);
|
||||
const fields = Array.isArray(fieldsPayload.data) ? fieldsPayload.data : [];
|
||||
emails = fields.filter(f => f.contact_field_type?.name?.toLowerCase().includes("email") || f.contact_field_type_id === 1).map(f => f.data);
|
||||
phones = fields.filter(f => f.contact_field_type?.name?.toLowerCase().includes("phone") || f.contact_field_type_id === 2 || f.contact_field_type_id === 3 || f.contact_field_type?.name?.toLowerCase().includes("telefon") || f.contact_field_type?.name?.toLowerCase().includes("mobil")).map(f => f.data);
|
||||
} catch (e) {
|
||||
console.log("Could not load contact fields:", e.message);
|
||||
}
|
||||
|
||||
let addressStr = "";
|
||||
try {
|
||||
const addressPayload = await monicaFetch(`/api/contacts/${id}/addresses`);
|
||||
const addresses = Array.isArray(addressPayload.data) ? addressPayload.data : [];
|
||||
if (addresses.length > 0) {
|
||||
const addr = addresses[0];
|
||||
addressStr = [addr.street, addr.postal_code, addr.city].filter(Boolean).join(", ");
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Could not load addresses:", e.message);
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: contact.id,
|
||||
first_name: contact.first_name || "",
|
||||
last_name: contact.last_name || "",
|
||||
name: contact.complete_name || [contact.first_name, contact.last_name].filter(Boolean).join(" "),
|
||||
description: contact.description || "",
|
||||
birthdate: contact.birthdate || "",
|
||||
email: emails[0] || "",
|
||||
phone: phones[0] || "",
|
||||
address: addressStr || "",
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(502).json({ error: "Monica contact details could not be loaded.", detail: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/contacts/create", async (req, res) => {
|
||||
const { first_name, last_name, description, email, phone, address } = req.body || {};
|
||||
if (!first_name) {
|
||||
res.status(400).json({ error: "First name is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!monicaApiToken) {
|
||||
const newId = Math.floor(Math.random() * 1000) + 10;
|
||||
const newContact = {
|
||||
id: newId,
|
||||
first_name,
|
||||
last_name,
|
||||
name: [first_name, last_name].filter(Boolean).join(" "),
|
||||
description: description || "Neu angelegter Demo-Kontakt",
|
||||
email: email || "",
|
||||
phone: phone || "",
|
||||
address: address || "",
|
||||
};
|
||||
res.json({ ok: true, mode: "demo", data: newContact });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const contactPayload = await monicaFetch("/api/contacts", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
first_name,
|
||||
last_name: last_name || "",
|
||||
description: description || "",
|
||||
}),
|
||||
});
|
||||
const contact = contactPayload.data || {};
|
||||
const contactId = contact.id;
|
||||
|
||||
if (contactId && email) {
|
||||
try {
|
||||
await monicaFetch("/api/contactfields", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
contact_id: contactId,
|
||||
contact_field_type_id: 1, // Email
|
||||
data: email,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Could not save email:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (contactId && phone) {
|
||||
try {
|
||||
await monicaFetch("/api/contactfields", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
contact_id: contactId,
|
||||
contact_field_type_id: 3, // Cell phone
|
||||
data: phone,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Could not save phone:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (contactId && address) {
|
||||
try {
|
||||
await monicaFetch("/api/addresses", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
contact_id: contactId,
|
||||
name: "Hauptadresse",
|
||||
street: address,
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Could not save address:", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
ok: true,
|
||||
mode: "monica",
|
||||
data: {
|
||||
id: contactId,
|
||||
name: contact.complete_name || [first_name, last_name].filter(Boolean).join(" "),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(502).json({ error: "Contact could not be created in Monica.", detail: error.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/analyze", async (req, res) => {
|
||||
const text = String(req.body.text || "").trim();
|
||||
const mode = String(req.body.mode || "auto");
|
||||
const contactsList = req.body.contacts || [];
|
||||
|
||||
if (!text) {
|
||||
res.status(400).json({ error: "Text is required." });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json(analyzeText(text, mode));
|
||||
res.json(analyzeText(text, mode, contactsList));
|
||||
});
|
||||
|
||||
app.post("/api/save", async (req, res) => {
|
||||
@@ -152,9 +297,9 @@ async function monicaWrite(type, apiPath, body) {
|
||||
}
|
||||
}
|
||||
|
||||
function analyzeText(text, mode = "auto") {
|
||||
function analyzeText(text, mode = "auto", contactsList = []) {
|
||||
const normalized = text.replace(/\s+/g, " ").trim();
|
||||
const contacts = mode === "journal" ? [] : inferContacts(normalized);
|
||||
const contacts = mode === "journal" ? [] : inferContacts(normalized, contactsList);
|
||||
const reminder = inferReminder(normalized);
|
||||
const type = mode === "auto" ? inferType(normalized, contacts, reminder) : mode;
|
||||
const summary = summarize(normalized);
|
||||
@@ -194,20 +339,50 @@ function inferType(text, contacts, reminder) {
|
||||
return "journal";
|
||||
}
|
||||
|
||||
function inferContacts(text) {
|
||||
function inferContacts(text, contactsList = []) {
|
||||
const candidates = [];
|
||||
const patterns = [
|
||||
/(?:mit|bei|von|über|wegen)\s+([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)/g,
|
||||
/([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)\s+(?:getroffen|gesprochen|telefoniert|gesehen)/g,
|
||||
/(?:treffe|spreche|telefoniere|schreibe)\s+(?:mit\s+)?([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)/g,
|
||||
/@([A-Za-zÄÖÜäöüß]+(?:\s+[A-Za-zÄÖÜäöüß]+)?)/g,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const name = cleanupName(match[1]);
|
||||
if (name && !candidates.some((item) => item.name.toLowerCase() === name.toLowerCase())) {
|
||||
candidates.push({ name });
|
||||
// 1. Scan for echten Kontakten (from DB/Monica)
|
||||
if (Array.isArray(contactsList) && contactsList.length > 0) {
|
||||
for (const contact of contactsList) {
|
||||
if (!contact.name) continue;
|
||||
const nameParts = contact.name.trim().split(/\s+/);
|
||||
const firstName = nameParts[0];
|
||||
const fullName = contact.name.trim();
|
||||
|
||||
const escFullName = fullName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const escFirstName = firstName.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
|
||||
const regexFull = new RegExp(`\\b${escFullName}\\b`, 'i');
|
||||
const regexFirst = new RegExp(`\\b${escFirstName}\\b`, 'i');
|
||||
|
||||
if (regexFull.test(text) || (firstName.length > 2 && regexFirst.test(text))) {
|
||||
if (!candidates.some((item) => item.id === contact.id)) {
|
||||
candidates.push({
|
||||
id: contact.id,
|
||||
name: contact.name,
|
||||
description: contact.description || ""
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback / Ergänzung durch klassische Regexes (wenn keine echten Kontakte gematcht wurden)
|
||||
if (candidates.length === 0) {
|
||||
const patterns = [
|
||||
/(?:mit|bei|von|über|wegen)\s+([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)/g,
|
||||
/([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)\s+(?:getroffen|gesprochen|telefoniert|gesehen)/g,
|
||||
/(?:treffe|spreche|telefoniere|schreibe)\s+(?:mit\s+)?([A-ZÄÖÜ][a-zäöüß]+(?:\s+[A-ZÄÖÜ][a-zäöüß]+)?)/g,
|
||||
/@([A-Za-zÄÖÜäöüß]+(?:\s+[A-Za-zÄÖÜäöüß]+)?)/g,
|
||||
];
|
||||
|
||||
for (const pattern of patterns) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
const name = cleanupName(match[1]);
|
||||
if (name && !candidates.some((item) => item.name.toLowerCase() === name.toLowerCase())) {
|
||||
candidates.push({ name });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -215,6 +390,15 @@ function inferContacts(text) {
|
||||
return candidates.slice(0, 3);
|
||||
}
|
||||
|
||||
function demoContactDetails(id) {
|
||||
const details = {
|
||||
1: { id: 1, first_name: "Aaron", last_name: "Lingel", name: "Aaron Lingel", description: "Bester Kumpel auf dem Campus", email: "aaron.lingel@example.com", phone: "+49 176 898989", address: "Fischbrötchenallee 42, 22767 Hamburg" },
|
||||
2: { id: 2, first_name: "Tom", last_name: "", name: "Tom", description: "Vibecoding-Kollege", email: "tom@example.com", phone: "+49 152 111222", address: "Mischweg 7, 20357 Hamburg" },
|
||||
3: { id: 3, first_name: "MrDiderot", last_name: "", name: "MrDiderot", description: "Admin und CRM Host", email: "mrdiderot@mischlabs.de", phone: "+49 170 333444", address: "NAS-Zentrale 1, 20095 Hamburg" },
|
||||
};
|
||||
return details[id] || { id, first_name: "Unbekannt", last_name: "", name: "Unbekannter Kontakt", description: "", email: "", phone: "", address: "" };
|
||||
}
|
||||
|
||||
function cleanupName(name) {
|
||||
const cleaned = name
|
||||
.replace(/\b(heute|morgen|gestern|freitag|samstag|sonntag|montag|dienstag|mittwoch|donnerstag)\b/gi, "")
|
||||
|
||||
Reference in New Issue
Block a user