Files
business-card-scanner/app/duplicates.py
Lucas Orth bf0a397597
All checks were successful
Build & Deploy / deploy (push) Successful in 10m41s
Anlass, Dubletten, Rückseite, Teilen; Startseite und App-Anmutung
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>
2026-09-06 15:09:08 +02:00

56 lines
1.8 KiB
Python

"""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