Kontaktadresse: Neues Feld Position, Filter-Funktionen null-sicher, Projektnamen-Spalte entfernt

- Server: Neue Spalte position in Kontaktadresse-Modell (String(200), nullable)
- Server: Migration V01_04 (ALTER TABLE kontaktadressen ADD COLUMN position)
- Server: position in Geschäftslogik create/update aufgenommen
- Server: CLI-Musterdaten um Position ergänzt (Geschäftsführer, IT-Leiter etc.)
- Client: position in iKontaktadresse-Interface, View nach Firma, Columnmapper, Filter
- Client: Projektnamen-Spalte aus Kontaktadressenliste entfernt (steht in Überschrift)
- Client: Filter-Funktionen in allen Listen null-sicher (kunde, kontakt, projekt, kontaktadresse, call_log)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-04 14:58:52 +02:00
1 parent b510c50497
commit e9a2f48ede
10 files changed
+59 -36

No files matched your search

+7 -6
View File
@@ -164,11 +164,11 @@ def _seed_muster_kontaktadressen():
projekte = session.query(Projekt).order_by(Projekt.id).all() projekte = session.query(Projekt).order_by(Projekt.id).all()
vorlagen = [ vorlagen = [
("Herr", "Thomas Becker", "Becker Webdesign GmbH"), ("Herr", "Thomas Becker", "Becker Webdesign GmbH", "Geschäftsführer"),
("Frau", "Sandra Klein", "Klein Marketing"), ("Frau", "Sandra Klein", "Klein Marketing", "Marketingleiterin"),
("Herr", "Michael Wagner", "Wagner Online Shop"), ("Herr", "Michael Wagner", "Wagner Online Shop", "IT-Leiter"),
("Frau", "Julia Hoffmann", "Hoffmann Consulting"), ("Frau", "Julia Hoffmann", "Hoffmann Consulting", "Beraterin"),
("Herr", "Stefan Roth", "Roth IT Solutions"), ("Herr", "Stefan Roth", "Roth IT Solutions", "Technischer Leiter"),
] ]
orte = [ orte = [
("Industriestr. 12", "45131", "Essen", "Deutschland"), ("Industriestr. 12", "45131", "Essen", "Deutschland"),
@@ -181,7 +181,7 @@ def _seed_muster_kontaktadressen():
for projekt in projekte: for projekt in projekte:
anzahl = rng.randint(1, 3) anzahl = rng.randint(1, 3)
for _ in range(anzahl): for _ in range(anzahl):
anrede, name, firma = vorlagen[vorlagen_idx % len(vorlagen)] anrede, name, firma, position = vorlagen[vorlagen_idx % len(vorlagen)]
vorlagen_idx += 1 vorlagen_idx += 1
strasse, plz, ort, land = rng.choice(orte) strasse, plz, ort, land = rng.choice(orte)
ka = Kontaktadresse() ka = Kontaktadresse()
@@ -189,6 +189,7 @@ def _seed_muster_kontaktadressen():
ka.anrede = anrede ka.anrede = anrede
ka.name = name ka.name = name
ka.firma = firma ka.firma = firma
ka.position = position
ka.telefon = "+49 {vorwahl} {nummer}".format( ka.telefon = "+49 {vorwahl} {nummer}".format(
vorwahl=rng.randint(100, 999), vorwahl=rng.randint(100, 999),
nummer=rng.randint(100000, 999999) nummer=rng.randint(100000, 999999)
+1
View File
@@ -168,6 +168,7 @@ class Kontaktadresse(dbapp.Model):
anrede = Column(String(30), nullable=True) anrede = Column(String(30), nullable=True)
name = Column(String(200), nullable=False) name = Column(String(200), nullable=False)
firma = Column(String(200), nullable=True) firma = Column(String(200), nullable=True)
position = Column(String(200), nullable=True)
telefon = Column(String(100), nullable=True) telefon = Column(String(100), nullable=True)
email = Column(String(200), nullable=True) email = Column(String(200), nullable=True)
strasse = Column(String(200), nullable=True) strasse = Column(String(200), nullable=True)
+13
View File
@@ -78,6 +78,19 @@ def _db_V01_03():
session.commit() session.commit()
def _db_V01_04():
"""
Fügt die Spalte 'position' zur Tabelle 'kontaktadressen' hinzu.
Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden,
fehlt diese Spalte, da das Feld erst nachträglich ergänzt wurde.
"""
from sqlalchemy import text
session.execute(text("ALTER TABLE kontaktadressen ADD COLUMN position VARCHAR(200)"))
_updatesysteminfo('01.04')
session.commit()
def checkupdates(): def checkupdates():
""" """
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
@@ -68,6 +68,7 @@ def create_kontaktadresse(data: dict) -> Kontaktadresse:
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip() kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.position = (data.get("position") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
@@ -107,6 +108,7 @@ def update_kontaktadresse(data: dict) -> Kontaktadresse:
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip() kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.position = (data.get("position") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
+8 -8
View File
@@ -74,14 +74,14 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let log: iCallLog = node.nodevalue let log: iCallLog = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return log.kunden_name.toLowerCase().includes(searchLower) || return (log.kunden_name || "").toLowerCase().includes(searchLower) ||
log.projekt_name.toLowerCase().includes(searchLower) || (log.projekt_name || "").toLowerCase().includes(searchLower) ||
log.datum.includes(searchLower) || (log.datum || "").includes(searchLower) ||
log.uhrzeit_beginn.toLowerCase().includes(searchLower) || (log.uhrzeit_beginn || "").toLowerCase().includes(searchLower) ||
log.betreff.toLowerCase().includes(searchLower) || (log.betreff || "").toLowerCase().includes(searchLower) ||
log.name_ansprechpartner.toLowerCase().includes(searchLower) || (log.name_ansprechpartner || "").toLowerCase().includes(searchLower) ||
log.notiz.toLowerCase().includes(searchLower) || (log.notiz || "").toLowerCase().includes(searchLower) ||
log.wiedervorlage_datum.includes(searchLower) (log.wiedervorlage_datum || "").includes(searchLower)
} }
return { return {
+5 -5
View File
@@ -54,11 +54,11 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let kontakt: iKontakt = node.nodevalue let kontakt: iKontakt = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return kontakt.name.toLowerCase().includes(searchLower) || return (kontakt.name || "").toLowerCase().includes(searchLower) ||
kontakt.position.toLowerCase().includes(searchLower) || (kontakt.position || "").toLowerCase().includes(searchLower) ||
kontakt.telefon.toLowerCase().includes(searchLower) || (kontakt.telefon || "").toLowerCase().includes(searchLower) ||
kontakt.email.toLowerCase().includes(searchLower) || (kontakt.email || "").toLowerCase().includes(searchLower) ||
kontakt.kunden_name.toLowerCase().includes(searchLower) (kontakt.kunden_name || "").toLowerCase().includes(searchLower)
} }
return { return {
+12 -7
View File
@@ -64,10 +64,10 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
return [ return [
m("div", m("strong", kontaktadresse.name)), m("div", m("strong", kontaktadresse.name)),
m("span", kontaktadresse.firma), m("span", kontaktadresse.firma),
m("span", kontaktadresse.position),
m("span", kontaktadresse.telefon), m("span", kontaktadresse.telefon),
m("span", kontaktadresse.email), m("span", kontaktadresse.email),
m("span", kontaktadresse.ort), m("span", kontaktadresse.ort),
m("span", kontaktadresse.projekt_name),
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}}, m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontaktadresse(kontaktadresse, e)}, m("i.material-icons", "edit")), m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontaktadresse(kontaktadresse, e)}, m("i.material-icons", "edit")),
m("button.w3-button.w3-small", {title: "Gesprächsnotiz hinzufügen", style: {padding: '0 2px'}, onclick: (e: Event) => addCallLog(kontaktadresse, e)}, m("i.material-icons", "add_call")) m("button.w3-button.w3-small", {title: "Gesprächsnotiz hinzufügen", style: {padding: '0 2px'}, onclick: (e: Event) => addCallLog(kontaktadresse, e)}, m("i.material-icons", "add_call"))
@@ -78,12 +78,13 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let kontaktadresse: iKontaktadresse = node.nodevalue let kontaktadresse: iKontaktadresse = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return kontaktadresse.name.toLowerCase().includes(searchLower) || return (kontaktadresse.name || "").toLowerCase().includes(searchLower) ||
kontaktadresse.firma.toLowerCase().includes(searchLower) || (kontaktadresse.firma || "").toLowerCase().includes(searchLower) ||
kontaktadresse.telefon.toLowerCase().includes(searchLower) || (kontaktadresse.position || "").toLowerCase().includes(searchLower) ||
kontaktadresse.email.toLowerCase().includes(searchLower) || (kontaktadresse.telefon || "").toLowerCase().includes(searchLower) ||
kontaktadresse.ort.toLowerCase().includes(searchLower) || (kontaktadresse.email || "").toLowerCase().includes(searchLower) ||
kontaktadresse.projekt_name.toLowerCase().includes(searchLower) (kontaktadresse.ort || "").toLowerCase().includes(searchLower) ||
(kontaktadresse.projekt_name || "").toLowerCase().includes(searchLower)
} }
return { return {
@@ -152,6 +153,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
anrede: string anrede: string
name: string name: string
firma: string firma: string
position: string
telefon: string telefon: string
email: string email: string
strasse: string strasse: string
@@ -166,6 +168,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
anrede: "", anrede: "",
name: "", name: "",
firma: "", firma: "",
position: "",
telefon: "", telefon: "",
email: "", email: "",
strasse: "", strasse: "",
@@ -200,6 +203,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
anrede: (formData.get("anrede") as string) || "", anrede: (formData.get("anrede") as string) || "",
name: (formData.get("name") as string) || "", name: (formData.get("name") as string) || "",
firma: (formData.get("firma") as string) || "", firma: (formData.get("firma") as string) || "",
position: (formData.get("position") as string) || "",
telefon: (formData.get("telefon") as string) || "", telefon: (formData.get("telefon") as string) || "",
email: (formData.get("email") as string) || "", email: (formData.get("email") as string) || "",
strasse: (formData.get("strasse") as string) || "", strasse: (formData.get("strasse") as string) || "",
@@ -228,6 +232,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontaktadresse.anrede, type: "text"} as iInput)), m("label", "Anrede", m(Input, {name: "anrede", value: currentKontaktadresse.anrede, type: "text"} as iInput)),
m("label", "Name", m(Input, {name: "name", value: currentKontaktadresse.name, type: "text", required: true} as iInput)), m("label", "Name", m(Input, {name: "name", value: currentKontaktadresse.name, type: "text", required: true} as iInput)),
m("label", "Firma", m(Input, {name: "firma", value: currentKontaktadresse.firma, type: "text"} as iInput)), m("label", "Firma", m(Input, {name: "firma", value: currentKontaktadresse.firma, type: "text"} as iInput)),
m("label", "Position", m(Input, {name: "position", value: currentKontaktadresse.position, type: "text"} as iInput)),
m("label", "Telefon", m(Input, {name: "telefon", value: currentKontaktadresse.telefon, type: "tel"} as iInput)), m("label", "Telefon", m(Input, {name: "telefon", value: currentKontaktadresse.telefon, type: "tel"} as iInput)),
m("label", "E-Mail", m(Input, {name: "email", value: currentKontaktadresse.email, type: "email"} as iInput)), m("label", "E-Mail", m(Input, {name: "email", value: currentKontaktadresse.email, type: "email"} as iInput)),
m("label", "Straße", m(Input, {name: "strasse", value: currentKontaktadresse.strasse, type: "text"} as iInput)), m("label", "Straße", m(Input, {name: "strasse", value: currentKontaktadresse.strasse, type: "text"} as iInput)),
+5 -5
View File
@@ -117,11 +117,11 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let kunde: iKunde = node.nodevalue let kunde: iKunde = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return kunde.name.toLowerCase().includes(searchLower) || return (kunde.name || "").toLowerCase().includes(searchLower) ||
kunde.strasse.toLowerCase().includes(searchLower) || (kunde.strasse || "").toLowerCase().includes(searchLower) ||
kunde.plz.includes(searchLower) || (kunde.plz || "").includes(searchLower) ||
kunde.ort.toLowerCase().includes(searchLower) || (kunde.ort || "").toLowerCase().includes(searchLower) ||
kunde.land.toLowerCase().includes(searchLower) (kunde.land || "").toLowerCase().includes(searchLower)
} }
return { return {
+5 -5
View File
@@ -91,11 +91,11 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let projekt: iProjekt = node.nodevalue let projekt: iProjekt = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return projekt.kunden_name.toLowerCase().includes(searchLower) || return (projekt.kunden_name || "").toLowerCase().includes(searchLower) ||
projekt.projektname.toLowerCase().includes(searchLower) || (projekt.projektname || "").toLowerCase().includes(searchLower) ||
projekt.zweck.toLowerCase().includes(searchLower) || (projekt.zweck || "").toLowerCase().includes(searchLower) ||
projekt.verantwortlicher.toLowerCase().includes(searchLower) || (projekt.verantwortlicher || "").toLowerCase().includes(searchLower) ||
projekt.startdatum.includes(searchLower) (projekt.startdatum || "").includes(searchLower)
} }
return { return {
+1
View File
@@ -14,6 +14,7 @@ export interface iKontaktadresse {
anrede: string anrede: string
name: string name: string
firma: string firma: string
position: string
telefon: string telefon: string
email: string email: string
strasse: string strasse: string