diff --git a/routes/contacts_routes.py b/routes/contacts_routes.py index 3d99bf738..692822d17 100644 --- a/routes/contacts_routes.py +++ b/routes/contacts_routes.py @@ -735,16 +735,35 @@ def setup_contacts_routes(): """Add a new contact.""" name = (data.get("name") or "").strip() email = (data.get("email") or "").strip() - if not email: - return {"success": False, "error": "Email required"} - # Check if already exists - contacts = _fetch_contacts() - for c in contacts: - if email.lower() in [e.lower() for e in c["emails"]]: - return {"success": True, "message": "Already exists", "contact": c} + phone = (data.get("phone") or "").strip() + address = (data.get("address") or "").strip() + if not email and not name: + return {"success": False, "error": "Name or email required"} + # Check if already exists by email + if email: + contacts = _fetch_contacts() + for c in contacts: + if email.lower() in [e.lower() for e in c["emails"]]: + return {"success": True, "message": "Already exists", "contact": c} if not name: name = email.split("@")[0] - ok = _create_contact(name, email) + ok = _create_contact(name, email, address) + # If a phone was provided, do an immediate update to thread it + # through (the simple _create_contact signature only takes name + + # email + address; phones happen via update). + if ok and phone: + try: + fresh = _fetch_contacts(force=True) + created = next((c for c in fresh if name == c.get("name") and (not email or email in c.get("emails", []))), None) + if created: + _update_contact( + created["uid"], name, + created.get("emails", []), + [phone], + address, + ) + except Exception: + pass return {"success": ok} @router.post("/import") @@ -820,7 +839,7 @@ def setup_contacts_routes(): # match PUT /{uid} with uid="config". @router.put("/{uid}") async def edit_contact(uid: str, data: dict, _admin: str = Depends(require_admin)): - """Edit an existing contact — name / emails / phones.""" + """Edit an existing contact — name / emails / phones / address.""" name = (data.get("name") or "").strip() emails = data.get("emails") phones = data.get("phones") @@ -828,11 +847,12 @@ def setup_contacts_routes(): emails = [data["email"]] emails = [e.strip() for e in (emails or []) if e and e.strip()] phones = [p.strip() for p in (phones or []) if p and p.strip()] - if not name and not emails: - return {"success": False, "error": "Name or email required"} + address = (data.get("address") or "").strip() + if not name and not emails and not address: + return {"success": False, "error": "Name, email, or address required"} if not name and emails: name = emails[0].split("@")[0] - ok = _update_contact(uid, name, emails, phones) + ok = _update_contact(uid, name, emails, phones, address) return {"success": ok} @router.delete("/{uid}") diff --git a/static/js/settings.js b/static/js/settings.js index 5d23e1f91..990e78065 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -4011,11 +4011,14 @@ async function initUnifiedIntegrations() { - `; try { @@ -4051,11 +4054,18 @@ async function initUnifiedIntegrations() { el('cm-add-save')?.addEventListener('click', async () => { const name = el('cm-add-name').value.trim(); const email = el('cm-add-email').value.trim(); - if (!email) { el('cm-add-email').focus(); return; } + const phone = el('cm-add-phone')?.value.trim() || ''; + const address = el('cm-add-address')?.value.trim() || ''; + // Need at least a name or email; address-only entries without a + // name aren't useful as a contact. + if (!name && !email) { (name ? el('cm-add-email') : el('cm-add-name')).focus(); return; } try { - await fetch('/api/contacts/add', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email }) }); + await fetch('/api/contacts/add', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, email, phone, address }) }); } catch (_) {} - el('cm-add-name').value = ''; el('cm-add-email').value = ''; + el('cm-add-name').value = ''; + el('cm-add-email').value = ''; + if (el('cm-add-phone')) el('cm-add-phone').value = ''; + if (el('cm-add-address')) el('cm-add-address').value = ''; el('cm-add-row').style.display = 'none'; await _renderContactsManager(); }); @@ -4154,33 +4164,66 @@ async function initUnifiedIntegrations() { } // Sort by name for a stable list. contacts.sort((a, b) => (a.name || '').localeCompare(b.name || '')); - list.innerHTML = contacts.map(c => { - const emails = (c.emails || []).join(', '); - const phones = (c.phones || []).join(', '); - const sub = [emails, phones].filter(Boolean).join(' · '); - return `
-
-
-
${esc(c.name || '(no name)')}
-
${esc(sub)}
+ + // Live filter — search across name/emails/phones/address. + const searchInput = el('cm-search'); + const q = (searchInput?.value || '').trim().toLowerCase(); + const filtered = !q ? contacts : contacts.filter(c => { + const hay = [ + c.name || '', + (c.emails || []).join(' '), + (c.phones || []).join(' '), + c.address || '', + ].join(' ').toLowerCase(); + return hay.includes(q); + }); + if (cnt) cnt.textContent = contacts.length ? `(${filtered.length}/${contacts.length})` : ''; + + if (!filtered.length) { + list.innerHTML = `
${q ? 'No matches.' : 'No contacts yet.'}
`; + } else { + list.innerHTML = filtered.map(c => { + const emails = (c.emails || []).join(', '); + const phones = (c.phones || []).join(', '); + const address = c.address || ''; + const sub = [emails, phones, address].filter(Boolean).join(' · '); + return `
+
+
+
${esc(c.name || '(no name)')}
+
${esc(sub)}
+
+ +
- - -
- -
`; - }).join(''); + +
`; + }).join(''); + } + + // Wire the search input — debounced so we don't refetch on every key. + if (searchInput && !searchInput._wired) { + searchInput._wired = true; + let _t; + searchInput.addEventListener('input', () => { + clearTimeout(_t); + _t = setTimeout(() => _renderContactsManager(), 80); + }); + } + // Stash latest contacts so the search input doesn't have to refetch. + list._lastContacts = contacts; // Wire each row's edit / delete / save / cancel. list.querySelectorAll('.contact-row').forEach(row => { const uid = row.dataset.uid; @@ -4199,6 +4242,7 @@ async function initUnifiedIntegrations() { name: row.querySelector('.contact-edit-name').value.trim(), emails: row.querySelector('.contact-edit-emails').value.split(',').map(s => s.trim()).filter(Boolean), phones: row.querySelector('.contact-edit-phones').value.split(',').map(s => s.trim()).filter(Boolean), + address: row.querySelector('.contact-edit-address')?.value.trim() || '', }; try { await fetch('/api/contacts/' + encodeURIComponent(uid), { method: 'PUT', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });