Anlass, Dubletten, Rückseite, Teilen; Startseite und App-Anmutung
All checks were successful
Build & Deploy / deploy (push) Successful in 10m41s
All checks were successful
Build & Deploy / deploy (push) Successful in 10m41s
Vier Funktionen: - Anlass: ein Feld auf der Startseite gilt für alle folgenden Scans und wird im Browser gemerkt. Ordnet den Bestand verlässlicher als der Ortsname aus den Koordinaten. Durchsuchbar, nachträglich änderbar. - Dubletten: die Detailansicht zeigt Karten, die dieselbe Person meinen könnten - gleiche E-Mail, gleiche Telefonnummer (verglichen werden die letzten acht Ziffern, damit +49 511 123456 und 0511/123456 aufeinander passen) oder gleicher Nachname bei gleicher Firma. - Rückseite: nachträglich ein zweites Bild zur Karte. Es füllt nur Felder, die die Vorderseite offen gelassen hat. - Schnellwahl bei Erinnerungen: in 3 Tagen / 1 Woche / 2 Wochen / 1 Monat, jeweils 9 Uhr. Das Datumsfeld braucht man damit selten. Teilen als vCard über das System-Teilenblatt, mit Download als Rückfallebene, wo es kein Teilenblatt gibt. Oberfläche: - Startseite führt jetzt: Anlass, große Aufnahmefläche mit Symbol statt Textzeile, leise Alternative, darunter die zuletzt erfassten Karten. Vorher stand dort eine Überschrift mit Zahlen und man musste raten. - Weniger Webseite, mehr App: kein Seitenrahmen auf dem Telefon, Symbole in der Tableiste mit Zählerblasen, kein Tap-Highlight, keine Textmarkierung auf Bedienelementen, Safe-Area oben, Einblendung beim Ansichtswechsel, Mindesthöhe 54 px für Schaltflächen. - Die Marken "exportiert" / "nicht exportiert" sind aus der Liste raus; dafür gibt es den Filter. Neue Spalten occasion und back_image_file kommen per ALTER TABLE in bestehende Datenbanken - CREATE TABLE IF NOT EXISTS rührt eine vorhandene Tabelle nicht an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
55
app/duplicates.py
Normal file
55
app/duplicates.py
Normal file
@@ -0,0 +1,55 @@
|
||||
"""Karten finden, die dieselbe Person meinen.
|
||||
|
||||
Auf Messen trifft man Leute wieder, und dieselbe Karte landet ein zweites Mal
|
||||
im Stapel. Verglichen wird nicht der ganze Datensatz, sondern das, was eine
|
||||
Person eindeutig macht - in dieser Reihenfolge, weil die Aussagekraft faellt.
|
||||
"""
|
||||
from . import db
|
||||
|
||||
|
||||
def _phone_key(value) -> str:
|
||||
"""Nur die letzten acht Ziffern. Damit passen +49 511 123456,
|
||||
0049511123456 und 0511/123456 aufeinander."""
|
||||
digits = "".join(character for character in (value or "") if character.isdigit())
|
||||
return digits[-8:] if len(digits) >= 8 else ""
|
||||
|
||||
|
||||
def _phone_keys(row) -> set:
|
||||
return {_phone_key(row["phone"]), _phone_key(row["mobile"])} - {""}
|
||||
|
||||
|
||||
def _clean(value) -> str:
|
||||
return (value or "").strip().lower()
|
||||
|
||||
|
||||
def _match(card, other) -> str:
|
||||
email = _clean(card["email"])
|
||||
if email and _clean(other["email"]) == email:
|
||||
return "gleiche E-Mail"
|
||||
if _phone_keys(card) & _phone_keys(other):
|
||||
return "gleiche Telefonnummer"
|
||||
last_name, company = _clean(card["last_name"]), _clean(card["company"])
|
||||
if last_name and company:
|
||||
if _clean(other["last_name"]) == last_name and _clean(other["company"]) == company:
|
||||
return "gleicher Name und dieselbe Firma"
|
||||
return ""
|
||||
|
||||
|
||||
def find(card) -> list:
|
||||
"""Aehnliche Karten, neueste zuerst."""
|
||||
others = db.query(
|
||||
"""
|
||||
SELECT id, full_name, first_name, last_name, company, email, phone, mobile,
|
||||
occasion, captured_at, created_at
|
||||
FROM cards
|
||||
WHERE id <> ? AND extract_status <> 'offen'
|
||||
ORDER BY COALESCE(captured_at, created_at) DESC
|
||||
""",
|
||||
(card["id"],),
|
||||
)
|
||||
hits = []
|
||||
for other in others:
|
||||
reason = _match(card, other)
|
||||
if reason:
|
||||
hits.append({**dict(other), "reason": reason})
|
||||
return hits
|
||||
Reference in New Issue
Block a user