Server+Client: REST-API für Kontaktadresse und CallLog, In-Memory vollständig entfernt, CLI-Musterdaten, Route-Param-Fix

- Server: ORM-Modelle Kontaktadresse (kontaktadressen) und CallLog (call_logs) mit FK projekt_id
- Server: Geschäftslogik kontaktadresse.py und call_log.py (get/create/update/delete)
- Server: DB-Migration V01_03 (kontaktadressen + call_logs Tabellen)
- Server: REST-Endpunkte /xhr/kontaktadresse/* und /xhr/calllog/* mit Autorisierung
- Server: CLI init-db legt Muster-Kontaktadressen (1-3/Projekt) und Muster-Gesprächsnotizen (2-5/Projekt) an
- Client: kontaktadresse.ts auf REST umgestellt, snake_case, In-Memory entfernt
- Client: call_log.ts auf REST umgestellt, snake_case, In-Memory entfernt, Submit-Handler implementiert
- Client: projekt.ts In-Memory vollständig entfernt (keine weiteren Verbraucher)
- Client: Route-Params projektId → projekt_id korrigiert (3 Stellen in projekt.ts)
- Client: addCallLog in projekt.ts Route-Params auf snake_case angepasst
- AGENTS.md: Erinnerung client-build-Skill nach .ts-Änderungen hinzugefügt

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-04 12:35:07 +02:00
1 parent e04a52857a
commit b510c50497
13 files changed
+799 -481

No files matched your search

+2
View File
@@ -49,6 +49,8 @@ export const Select: m.ClosureComponent<iSelect> = () => {
} }
``` ```
Nach jeder Änderung an `.ts`-Dateien unter `Client/` ist der Build-Prozess (tsc + rollup) auszuführen, damit die deploybaren Bundles in `NGINX_Server/html/static/` aktuell sind. Verwende dafür den `client-build`-Skill.
## Vorgaben für die Server-seitige Entwicklung unter Python ## Vorgaben für die Server-seitige Entwicklung unter Python
## Generierte Dateien nicht beachten ## Generierte Dateien nicht beachten
+114
View File
@@ -35,6 +35,8 @@ def register_cli_commands(app: Flask):
crm_checkdb.checkupdates() crm_checkdb.checkupdates()
_seed_muster_kunden() _seed_muster_kunden()
_seed_muster_projekte() _seed_muster_projekte()
_seed_muster_kontaktadressen()
_seed_muster_calllogs()
click.echo("Fertig.") click.echo("Fertig.")
@@ -147,6 +149,118 @@ def _seed_muster_projekte():
session.commit() session.commit()
def _seed_muster_kontaktadressen():
"""
Legt für jedes Muster-Projekt 1 bis 3 Kontaktadressen an, falls die
kontaktadressen-Tabelle noch leer ist.
"""
import random
from crm.model import Projekt, Kontaktadresse
session = dbapp.session
if session.query(Kontaktadresse).count() > 0:
return
projekte = session.query(Projekt).order_by(Projekt.id).all()
vorlagen = [
("Herr", "Thomas Becker", "Becker Webdesign GmbH"),
("Frau", "Sandra Klein", "Klein Marketing"),
("Herr", "Michael Wagner", "Wagner Online Shop"),
("Frau", "Julia Hoffmann", "Hoffmann Consulting"),
("Herr", "Stefan Roth", "Roth IT Solutions"),
]
orte = [
("Industriestr. 12", "45131", "Essen", "Deutschland"),
("Königsallee 5", "40212", "Düsseldorf", "Deutschland"),
("Hauptstr. 88", "50667", "Köln", "Deutschland"),
("Marienplatz 3", "80331", "München", "Deutschland"),
]
rng = random.Random(123)
vorlagen_idx = 0
for projekt in projekte:
anzahl = rng.randint(1, 3)
for _ in range(anzahl):
anrede, name, firma = vorlagen[vorlagen_idx % len(vorlagen)]
vorlagen_idx += 1
strasse, plz, ort, land = rng.choice(orte)
ka = Kontaktadresse()
ka.projekt_id = projekt.id
ka.anrede = anrede
ka.name = name
ka.firma = firma
ka.telefon = "+49 {vorwahl} {nummer}".format(
vorwahl=rng.randint(100, 999),
nummer=rng.randint(100000, 999999)
)
ka.email = name.lower().replace(" ", ".") + "@example.de"
ka.strasse = strasse
ka.plz = plz
ka.ort = ort
ka.land = land
ka.notiz = ""
session.add(ka)
session.commit()
def _seed_muster_calllogs():
"""
Legt für jedes Muster-Projekt 2 bis 5 Gesprächsnotizen an, falls die
call_logs-Tabelle noch leer ist.
"""
import random
from crm.model import Projekt, CallLog
session = dbapp.session
if session.query(CallLog).count() > 0:
return
projekte = session.query(Projekt).order_by(Projekt.id).all()
betreffvorlagen = [
"Anforderungen klären",
"Feedback zum Design",
"Technische Anforderungen",
"Schulungstermin",
"Feature-Request",
"Budgetgespräch",
"Projekt-Review",
]
notizvorlagen = [
"Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.",
"Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.",
"Payment-Gateway und Versand-Schnittstellen müssen integriert werden.",
"Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.",
"Kunde wünscht Push-Benachrichtigungen und Offline-Modus.",
"",
]
ansprechpartner = ["Herr Schmidt", "Frau Müller", "Herr Weber", "Frau Bauer", "Herr König"]
rng = random.Random(456)
for projekt in projekte:
anzahl = rng.randint(2, 5)
for _ in range(anzahl):
log = CallLog()
log.projekt_id = projekt.id
monat = rng.randint(1, 9)
tag = rng.randint(1, 28)
log.datum = "2024-0{m}-{t}".format(m=monat, t=tag) if monat < 10 else "2024-{m}-{t}".format(m=monat, t=tag)
log.uhrzeit_beginn = "{h:02d}:{min:02d}".format(
h=rng.randint(8, 17), min=rng.randint(0, 59)
)
log.dauer = str(rng.randint(15, 90))
log.betreff = rng.choice(betreffvorlagen)
log.name_ansprechpartner = rng.choice(ansprechpartner)
log.telefon = "+49 {v} {n}".format(
v=rng.randint(100, 999), n=rng.randint(100000, 999999)
)
log.notiz = rng.choice(notizvorlagen)
wiedervorlage_monat = monat + 1 if monat < 12 else 1
log.wiedervorlage_datum = "2024-0{m}-{t}".format(m=wiedervorlage_monat, t=tag) if wiedervorlage_monat < 10 else "2024-{m}-{t}".format(m=wiedervorlage_monat, t=tag)
log.erledigt = rng.choice([True, False])
session.add(log)
session.commit()
def _reset_appdb(): def _reset_appdb():
""" """
Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen
+76 -1
View File
@@ -12,7 +12,7 @@ erfolgt ausschließlich über __bind_key__.
import datetime import datetime
import decimal import decimal
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from db import dbapp from db import dbapp
@@ -154,4 +154,79 @@ class Projekt(dbapp.Model):
return result return result
class Kontaktadresse(dbapp.Model):
"""
Akquise-Ziel innerhalb eines Projekts. Jede Kontaktadresse ist genau
einem Projekt zugeordnet (projekt_id). Der Projektname (projekt_name)
ist keine eigene Spalte, sondern wird über die Relationship zu Projekt
abgeleitet und nur in to_dict() für die JSON-Serialisierung ergänzt.
"""
__tablename__ = 'kontaktadressen'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
anrede = Column(String(30), nullable=True)
name = Column(String(200), nullable=False)
firma = Column(String(200), nullable=True)
telefon = Column(String(100), nullable=True)
email = Column(String(200), nullable=True)
strasse = Column(String(200), nullable=True)
plz = Column(String(20), nullable=True)
ort = Column(String(100), nullable=True)
land = Column(String(80), nullable=True)
notiz = Column(String(2000), nullable=True)
projekt = relationship('Projekt')
def to_dict(self):
"""
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
über columns2dict serialisiert; projekt_name wird zusätzlich aus der
Projekt-Relationship eingefügt.
"""
result = columns2dict(self)
result['projekt_name'] = self.projekt.projektname if self.projekt else None
return result
class CallLog(dbapp.Model):
"""
Gesprächsnotiz für ein Projekt. Jede Gesprächsnotiz ist genau einem
Projekt zugeordnet (projekt_id). Der Projektname (projekt_name) und
der Kundenname (kunden_name) sind keine eigenen Spalten, sondern
werden über die Projekt-Relationship abgeleitet und nur in to_dict()
für die JSON-Serialisierung ergänzt.
"""
__tablename__ = 'call_logs'
__bind_key__ = BINDKEY
id = Column(Integer, primary_key=True)
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
datum = Column(String(20), nullable=True)
uhrzeit_beginn = Column(String(10), nullable=True)
dauer = Column(String(10), nullable=True)
betreff = Column(String(200), nullable=True)
name_ansprechpartner = Column(String(200), nullable=True)
telefon = Column(String(100), nullable=True)
notiz = Column(String(2000), nullable=True)
wiedervorlage_datum = Column(String(20), nullable=True)
erledigt = Column(Boolean, nullable=False, default=False)
projekt = relationship('Projekt')
def to_dict(self):
"""
Liefert die Gesprächsnotiz als Dictionary. Die Tabellenspalten werden
über columns2dict serialisiert; projekt_name und kunden_name werden
zusätzlich aus der Projekt-Relationship eingefügt.
"""
result = columns2dict(self)
if self.projekt:
result['projekt_name'] = self.projekt.projektname
result['kunden_name'] = self.projekt.kunde.name if self.projekt.kunde else None
else:
result['projekt_name'] = None
result['kunden_name'] = None
return result
print("########## crm/model importiert") print("########## crm/model importiert")
+117
View File
@@ -0,0 +1,117 @@
"""
Geschäftslogik für die Entität CallLog (Gesprächsnotiz).
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
dort über den Decorator authorisation_required.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-04
:version: 1.0
"""
from db import dbapp
from crm.model import CallLog, Projekt
from auth.model.errors import ApplicationError, HtmlStatusCode
session = dbapp.session
def get_call_logs():
"""
Liefert alle Gesprächsnotizen, sortiert nach Datum absteigend.
"""
return CallLog.query.order_by(CallLog.datum.desc()).all()
def get_call_log(call_log_id: int) -> CallLog:
"""
Liefert eine einzelne Gesprächsnotiz anhand ihrer ID.
Wirft ApplicationError (404), falls die Notiz nicht existiert.
"""
call_log = CallLog.query.get(call_log_id)
if call_log is None:
raise ApplicationError(
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return call_log
def create_call_log(data: dict) -> CallLog:
"""
Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
- data: dict mit den Feldern projekt_id, datum, uhrzeit_beginn, dauer,
betreff, name_ansprechpartner, telefon, notiz,
wiedervorlage_datum, erledigt
"""
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
call_log = CallLog()
call_log.projekt_id = int(projekt_id)
call_log.datum = (data.get("datum") or "").strip() or None
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
call_log.dauer = (data.get("dauer") or "").strip() or None
call_log.betreff = (data.get("betreff") or "").strip() or None
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
call_log.telefon = (data.get("telefon") or "").strip() or None
call_log.notiz = (data.get("notiz") or "").strip() or None
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
call_log.erledigt = bool(data.get("erledigt", False))
session.add(call_log)
session.commit()
return call_log
def update_call_log(data: dict) -> CallLog:
"""
Aktualisiert eine bestehende Gesprächsnotiz. Die ID im data-Dict
bestimmt den zu ändernden Datensatz.
"""
call_log = get_call_log(data["id"])
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
call_log.projekt_id = int(projekt_id)
call_log.datum = (data.get("datum") or "").strip() or None
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
call_log.dauer = (data.get("dauer") or "").strip() or None
call_log.betreff = (data.get("betreff") or "").strip() or None
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
call_log.telefon = (data.get("telefon") or "").strip() or None
call_log.notiz = (data.get("notiz") or "").strip() or None
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
call_log.erledigt = bool(data.get("erledigt", False))
session.commit()
return call_log
def delete_call_log(call_log_id: int):
"""
Löscht eine Gesprächsnotiz anhand ihrer ID.
Wirft ApplicationError (404), falls die Notiz nicht existiert.
"""
call_log = get_call_log(call_log_id)
session.delete(call_log)
session.commit()
+11
View File
@@ -67,6 +67,17 @@ def _db_V01_02():
session.commit() session.commit()
def _db_V01_03():
"""
Legt die Tabellen 'kontaktadressen' und 'call_logs' an. Bei bestehenden
Datenbanken, die mit Version 01.02 initialisiert wurden, fehlen diese
Tabellen, da die Modelle erst nachträglich ergänzt wurden.
"""
dbapp.create_all(bind_key=BINDKEY)
_updatesysteminfo('01.03')
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
+128
View File
@@ -0,0 +1,128 @@
"""
Geschäftslogik für die Entität Kontaktadresse.
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
dort über den Decorator authorisation_required.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-04
:version: 1.0
"""
from db import dbapp
from crm.model import Kontaktadresse, Projekt
from auth.model.errors import ApplicationError, HtmlStatusCode
session = dbapp.session
def get_kontaktadressen():
"""
Liefert alle Kontaktadressen, sortiert nach Name.
"""
return Kontaktadresse.query.order_by(Kontaktadresse.name).all()
def get_kontaktadresse(kontaktadresse_id: int) -> Kontaktadresse:
"""
Liefert eine einzelne Kontaktadresse anhand ihrer ID.
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
"""
kontaktadresse = Kontaktadresse.query.get(kontaktadresse_id)
if kontaktadresse is None:
raise ApplicationError(
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return kontaktadresse
def create_kontaktadresse(data: dict) -> Kontaktadresse:
"""
Legt eine neue Kontaktadresse an. Die vom Client übergebene ID wird
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
- data: dict mit den Feldern projekt_id, anrede, name, firma, telefon,
email, strasse, plz, ort, land, notiz
"""
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("name") or not str(data["name"]).strip():
raise ApplicationError(
"Der Name darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kontaktadresse = Kontaktadresse()
kontaktadresse.projekt_id = int(projekt_id)
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
kontaktadresse.plz = (data.get("plz") or "").strip() or None
kontaktadresse.ort = (data.get("ort") or "").strip() or None
kontaktadresse.land = (data.get("land") or "").strip() or None
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
session.add(kontaktadresse)
session.commit()
return kontaktadresse
def update_kontaktadresse(data: dict) -> Kontaktadresse:
"""
Aktualisiert eine bestehende Kontaktadresse. Die ID im data-Dict
bestimmt den zu ändernden Datensatz.
"""
kontaktadresse = get_kontaktadresse(data["id"])
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("name") or not str(data["name"]).strip():
raise ApplicationError(
"Der Name darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kontaktadresse.projekt_id = int(projekt_id)
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
kontaktadresse.plz = (data.get("plz") or "").strip() or None
kontaktadresse.ort = (data.get("ort") or "").strip() or None
kontaktadresse.land = (data.get("land") or "").strip() or None
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
session.commit()
return kontaktadresse
def delete_kontaktadresse(kontaktadresse_id: int):
"""
Löscht eine Kontaktadresse anhand ihrer ID.
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
"""
kontaktadresse = get_kontaktadresse(kontaktadresse_id)
session.delete(kontaktadresse)
session.commit()
+92
View File
@@ -21,6 +21,8 @@ from crm.model import KUNDEN_MANAGER, MITARBEITER
from crm.model import kunde as kunden from crm.model import kunde as kunden
from crm.model import kontakt as kontakte from crm.model import kontakt as kontakte
from crm.model import projekt as projekte from crm.model import projekt as projekte
from crm.model import kontaktadresse as kontaktadressen
from crm.model import call_log as call_logs
print("crm-routes") print("crm-routes")
@@ -160,4 +162,94 @@ def projekt_delete():
return jsonify({}), 200 return jsonify({}), 200
# --------------------------------------------------------------------------- #
# Kontaktadresse #
# --------------------------------------------------------------------------- #
@app.get("/xhr/kontaktadresse/list")
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
def kontaktadresse_list():
"""Liefert alle Kontaktadressen als Liste."""
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen()]
return jsonify(result), 200
@app.get("/xhr/kontaktadresse/get")
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
def kontaktadresse_get():
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id)."""
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")))
return jsonify(kontaktadresse.to_dict()), 200
@app.post("/xhr/kontaktadresse/create")
@authorisation_required([KUNDEN_MANAGER])
def kontaktadresse_create():
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
return jsonify(kontaktadresse.to_dict()), 200
@app.put("/xhr/kontaktadresse/update")
@authorisation_required([KUNDEN_MANAGER])
def kontaktadresse_update():
"""Aktualisiert eine bestehende Kontaktadresse."""
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
return jsonify(kontaktadresse.to_dict()), 200
@app.delete("/xhr/kontaktadresse/delete")
@authorisation_required([KUNDEN_MANAGER])
def kontaktadresse_delete():
"""Löscht eine Kontaktadresse anhand ihrer ID (Query-Param id)."""
kontaktadressen.delete_kontaktadresse(int(request.args.get("id")))
return jsonify({}), 200
# --------------------------------------------------------------------------- #
# CallLog (Gesprächsnotiz) #
# --------------------------------------------------------------------------- #
@app.get("/xhr/calllog/list")
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
def calllog_list():
"""Liefert alle Gesprächsnotizen als Liste."""
result = [l.to_dict() for l in call_logs.get_call_logs()]
return jsonify(result), 200
@app.get("/xhr/calllog/get")
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
def calllog_get():
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
call_log = call_logs.get_call_log(int(request.args.get("id")))
return jsonify(call_log.to_dict()), 200
@app.post("/xhr/calllog/create")
@authorisation_required([KUNDEN_MANAGER])
def calllog_create():
"""Legt eine neue Gesprächsnotiz an. ID wird serverseitig vergeben."""
call_log = call_logs.create_call_log(request.json)
return jsonify(call_log.to_dict()), 200
@app.put("/xhr/calllog/update")
@authorisation_required([KUNDEN_MANAGER])
def calllog_update():
"""Aktualisiert eine bestehende Gesprächsnotiz."""
call_log = call_logs.update_call_log(request.json)
return jsonify(call_log.to_dict()), 200
@app.delete("/xhr/calllog/delete")
@authorisation_required([KUNDEN_MANAGER])
def calllog_delete():
"""Löscht eine Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
call_logs.delete_call_log(int(request.args.get("id")))
return jsonify({}), 200
print("########## crm.routes importiert") print("########## crm.routes importiert")
+164 -166
View File
@@ -3,27 +3,22 @@ import {Form, iForm} from "./widgets/form"
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
import {Select, iSelect, iSelectOption} from "./widgets/select" import {Select, iSelect, iSelectOption} from "./widgets/select"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log" import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCallLogREST} from "../models/call_log"
import {iKunde, fetchKunden} from "../models/kunde" import {iKunde, fetchKunden} from "../models/kunde"
import {getProjekteByKundenId, getProjektById} from "../models/projekt" import {iProjekt, fetchProjekte} from "../models/projekt"
import {getKontaktadressenByProjektId, getKontaktadresseById} from "../models/kontaktadresse" import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden. // Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
// 1000 = jede Sekunde, 60000 = jede Minute.
const DAUER_UPDATE_INTERVALL = 1000 const DAUER_UPDATE_INTERVALL = 1000
function createCallLogTreenodes(projektId: number | null = null): iTreenode[] {
let filteredLogs = getCallLogsByProjektId(projektId)
return filteredLogs.map((log: iCallLog) => {
return new Treenode(log, true, [])
})
}
export const call_log_liste: m.ClosureComponent = (vnode) => { export const call_log_liste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createCallLogTreenodes() let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null let selectedNode: iTreenode | null = null
let filterProjektId: number | null = null let filterProjektId: number | null = null
// Projektname für die Überschrift, asynchron aufgelöst.
let projektName: string = ""
// Projektliste asynchron laden, für Projektauflösung beim Neuanlegen.
let projektListe: iProjekt[] = []
const handleRowMarked = (node: iTreenode) => { const handleRowMarked = (node: iTreenode) => {
selectedNode = node selectedNode = node
@@ -36,22 +31,21 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
const params: any = { const params: any = {
id: -1, id: -1,
datum: datum, datum: datum,
uhrzeitBeginn: uhrzeit, uhrzeit_beginn: uhrzeit,
dauer: "", dauer: "",
betreff: "", betreff: "",
nameAnsprechpartner: "", name_ansprechpartner: "",
telefon: "", telefon: "",
notiz: "", notiz: "",
wiedervorlageDatum: "", wiedervorlage_datum: "",
erledigt: false erledigt: false
} }
// Bei projektgefilterter Liste Kunde und Projekt vorbelegen.
if (filterProjektId !== null) { if (filterProjektId !== null) {
const projekt = getProjektById(filterProjektId) const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) { if (projekt) {
params.kundenName = projekt.kunden_name params.projekt_id = projekt.id
params.projektId = projekt.id params.projekt_name = projekt.projektname
params.projektName = projekt.projektname params.kunden_name = projekt.kunden_name
} }
} }
m.route.set("/call_log", params) m.route.set("/call_log", params)
@@ -59,33 +53,19 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
const editCallLog = (log: iCallLog, e: Event) => { const editCallLog = (log: iCallLog, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/call_log", { m.route.set("/call_log", {id: log.id})
id: log.id,
kundenName: log.kundenName,
projektId: log.projektId,
projektName: log.projektName,
datum: log.datum,
uhrzeitBeginn: log.uhrzeitBeginn,
dauer: log.dauer,
betreff: log.betreff,
nameAnsprechpartner: log.nameAnsprechpartner,
telefon: log.telefon,
notiz: log.notiz,
wiedervorlageDatum: log.wiedervorlageDatum,
erledigt: log.erledigt
})
} }
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
let log: iCallLog = node.nodevalue let log: iCallLog = node.nodevalue
return [ return [
m("div", m("strong", log.kundenName + ": " + log.projektName)), m("div", m("strong", log.kunden_name + ": " + log.projekt_name)),
m("span", log.datum), m("span", log.datum),
m("span", log.uhrzeitBeginn), m("span", log.uhrzeit_beginn),
m("span", log.dauer + " min"), m("span", log.dauer + " min"),
m("span", log.betreff), m("span", log.betreff),
m("span", log.wiedervorlageDatum), m("span", log.wiedervorlage_datum),
m("span", log.erledigt ? "Erledigt" : "Offen"), m("span", log.erledigt ? "Erledigt" : "Offen"),
m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit")) m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit"))
] ]
@@ -94,25 +74,44 @@ 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.kundenName.toLowerCase().includes(searchLower) || return log.kunden_name.toLowerCase().includes(searchLower) ||
log.projektName.toLowerCase().includes(searchLower) || log.projekt_name.toLowerCase().includes(searchLower) ||
log.datum.includes(searchLower) || log.datum.includes(searchLower) ||
log.uhrzeitBeginn.toLowerCase().includes(searchLower) || log.uhrzeit_beginn.toLowerCase().includes(searchLower) ||
log.betreff.toLowerCase().includes(searchLower) || log.betreff.toLowerCase().includes(searchLower) ||
log.nameAnsprechpartner.toLowerCase().includes(searchLower) || log.name_ansprechpartner.toLowerCase().includes(searchLower) ||
log.notiz.toLowerCase().includes(searchLower) || log.notiz.toLowerCase().includes(searchLower) ||
log.wiedervorlageDatum.includes(searchLower) log.wiedervorlage_datum.includes(searchLower)
} }
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams && routeParams.projektId) { if (routeParams && routeParams.projekt_id) {
filterProjektId = parseInt(routeParams.projektId) filterProjektId = parseInt(routeParams.projekt_id)
projektName = routeParams.projekt_name || ""
} else { } else {
filterProjektId = null filterProjektId = null
projektName = ""
} }
treenodes = createCallLogTreenodes(filterProjektId) // Gesprächsnotizen asynchron vom Server laden.
fetchCallLogs().then((result) => {
let filtered = result
if (filterProjektId !== null) {
filtered = result.filter((l: iCallLog) => l.projekt_id === filterProjektId)
}
treenodes = filtered.map((log: iCallLog) => new Treenode(log, true, []))
})
// Projektliste asynchron laden, für Projektnamen-Auflösung.
fetchProjekte().then((result) => {
projektListe = result
if (filterProjektId !== null && projektName === "") {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
projektName = projekt.projektname
}
}
})
}, },
view: (vnode) => { view: (vnode) => {
return m("div.w3-container.w3-auto", { return m("div.w3-container.w3-auto", {
@@ -121,7 +120,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
'flex-direction': 'column', 'flex-direction': 'column',
height: 'inherit', height: 'inherit',
'overflow-y': 'inherit'}}, 'overflow-y': 'inherit'}},
m("h1", "Gesprächsnotizen"), m("h1", projektName ? "Gesprächsnotizen für " + projektName : "Gesprächsnotizen"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable, m(Treetable,
{ {
@@ -142,187 +141,190 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
} }
export const Call_Log: m.ClosureComponent = (vnode) => { export const Call_Log: m.ClosureComponent = (vnode) => {
// currentLog hält den Formularzustand. kundenId ist intern und wird über // currentLog hält den Formularzustand. projekt_id referenziert das
// das Projekt aufgelöst; kundenName/projektName dienen der Anzeige. // Projekt, kunden_name und projekt_name dienen der Anzeige.
let currentLog: { let currentLog: {
id: number id: number
kundenId: number projekt_id: number
kundenName: string projekt_name: string
projektId: number kunden_name: string
projektName: string
datum: string datum: string
uhrzeitBeginn: string uhrzeit_beginn: string
dauer: string dauer: string
betreff: string betreff: string
nameAnsprechpartner: string name_ansprechpartner: string
telefon: string telefon: string
notiz: string notiz: string
wiedervorlageDatum: string wiedervorlage_datum: string
erledigt: boolean erledigt: boolean
} = { } = {
id: -1, id: -1,
kundenId: -1, projekt_id: -1,
kundenName: "", projekt_name: "",
projektId: -1, kunden_name: "",
projektName: "",
datum: "", datum: "",
uhrzeitBeginn: "", uhrzeit_beginn: "",
dauer: "", dauer: "",
betreff: "", betreff: "",
nameAnsprechpartner: "", name_ansprechpartner: "",
telefon: "", telefon: "",
notiz: "", notiz: "",
wiedervorlageDatum: "", wiedervorlage_datum: "",
erledigt: false erledigt: false
} }
// Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert // Ist das Projekt voreingestellt (Liste wurde projektgefiltert)?
// oder aus der Projektliste aufgerufen)? let projektVoreingestellt: boolean = false
let kundeVoreingestellt: boolean = false
// Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)? // Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)?
// Wenn ja, wird ein readonly Input gezeigt; sonst ein Select, das die
// Kontaktadressen des gewählten Projekts anbietet.
let ansprechpartnerVoreingestellt: boolean = false let ansprechpartnerVoreingestellt: boolean = false
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt). // Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
let startZeit: Date | null = null let startZeit: Date | null = null
// Timer-Handle für die Dauer-Aktualisierung. // Timer-Handle für die Dauer-Aktualisierung.
let timerId: number | null = null let timerId: number | null = null
// Direkte DOM-Referenz auf das Dauer-Feld, damit der Timer den Wert // Direkte DOM-Referenz auf das Dauer-Feld.
// ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder
// mit den veralteten currentLog-Werten überschreiben).
let dauerInput: HTMLInputElement | null = null let dauerInput: HTMLInputElement | null = null
// Kundenliste asynchron vom Server laden, für Select-Optionen. // Asynchron geladene Listen für Select-Optionen.
let kundenListe: iKunde[] = [] let kundenListe: iKunde[] = []
let projektListe: iProjekt[] = []
let kontaktadressenListe: iKontaktadresse[] = []
/** /**
* Formatiert verstrichene Sekunden als mm:ss (Minuten:Sekunden). * Formatiert verstrichene Sekunden als mm:ss.
*/ */
const formatDauer = (sekunden: number): string => { const formatDauer = (sekunden: number): string => {
const m = Math.floor(sekunden / 60) const min = Math.floor(sekunden / 60)
const s = sekunden % 60 const sek = sekunden % 60
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0') return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0')
} }
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams) { const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1 if (id !== -1) {
kundeVoreingestellt = projektId !== -1 // Gesprächsnotiz frisch vom Server holen.
// kundenId aus dem Projekt ableiten, falls nicht direkt gesetzt. fetchCallLogById(id).then((result) => {
let kundenId = -1 currentLog = result
if (kundeVoreingestellt) { projektVoreingestellt = true
const projekt = getProjektById(projektId) ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
if (projekt) { kundenId = projekt.kunden_id } })
} } else {
currentLog = { projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
id: routeParams.id ? parseInt(routeParams.id) : -1, currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
kundenId: kundenId, currentLog.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
kundenName: routeParams.kundenName || "", currentLog.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
projektId: projektId, currentLog.datum = routeParams && routeParams.datum ? routeParams.datum : ""
projektName: routeParams.projektName || "", currentLog.uhrzeit_beginn = routeParams && routeParams.uhrzeit_beginn ? routeParams.uhrzeit_beginn : ""
datum: routeParams.datum || "", currentLog.dauer = routeParams && routeParams.dauer ? routeParams.dauer : ""
uhrzeitBeginn: routeParams.uhrzeitBeginn || "", currentLog.betreff = routeParams && routeParams.betreff ? routeParams.betreff : ""
dauer: routeParams.dauer || "", currentLog.name_ansprechpartner = routeParams && routeParams.name_ansprechpartner ? routeParams.name_ansprechpartner : ""
betreff: routeParams.betreff || "", currentLog.telefon = routeParams && routeParams.telefon ? routeParams.telefon : ""
nameAnsprechpartner: routeParams.nameAnsprechpartner || "", currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
telefon: routeParams.telefon || "", currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
notiz: routeParams.notiz || "", currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
wiedervorlageDatum: routeParams.wiedervorlageDatum || "", ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
}
// Bei Neuanlage Startzeit merken und Dauer-Timer starten. // Bei Neuanlage Startzeit merken und Dauer-Timer starten.
ansprechpartnerVoreingestellt = currentLog.nameAnsprechpartner !== ""
if (currentLog.id === -1) {
startZeit = new Date() startZeit = new Date()
timerId = window.setInterval(() => { timerId = window.setInterval(() => {
if (startZeit) { if (startZeit) {
// Verstrichene Sekunden berechnen; das Intervall
// steuert nur die Update-Frequenz.
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000) const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
const dauer = formatDauer(sekunden) const dauer = formatDauer(sekunden)
currentLog.dauer = dauer currentLog.dauer = dauer
// Nur das Dauer-Feld direkt im DOM aktualisieren,
// ohne redraw (sonst werden andere Felder zurückgesetzt).
if (dauerInput) { dauerInput.value = dauer } if (dauerInput) { dauerInput.value = dauer }
} }
}, DAUER_UPDATE_INTERVALL) }, DAUER_UPDATE_INTERVALL)
} }
} // Alle Listen asynchron laden, für Select-Optionen.
// Kundenliste asynchron vom Server laden. fetchKunden().then((result) => { kundenListe = result })
fetchKunden().then((result) => { fetchProjekte().then((result) => { projektListe = result })
kundenListe = result fetchKontaktadressen().then((result) => { kontaktadressenListe = result })
})
}, },
onremove: (vnode) => { onremove: (vnode) => {
// Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft.
if (timerId != null) { window.clearInterval(timerId) } if (timerId != null) { window.clearInterval(timerId) }
}, },
view: (vnode) => { view: (vnode) => {
// Select-Optionen aus Kunden- und Projektliste aufbauen. // kunden_id aus dem geladenen Projekt ableiten (fuer abhaengiges
// Projekt-Select). Wird nur bei nicht voreingestelltem Projekt
// benoetigt.
const internKundenId = (() => {
if (currentLog.projekt_id !== -1) {
const p = projektListe.find((p: iProjekt) => p.id === currentLog.projekt_id)
return p ? p.kunden_id : -1
}
return -1
})()
const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => {
return {value: k.id, label: k.name} return {value: k.id, label: k.name}
}) })
// Projekt-Optionen sind vom gewählten Kunden abhängig. // Projekt-Optionen sind vom gewählten Kunden abhängig.
const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => { const projektOptionen: iSelectOption[] = projektListe
.filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId)
.map((p: iProjekt) => {
return {value: p.id, label: p.projektname} return {value: p.id, label: p.projektname}
}) })
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig. // Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
// Werden nur benötigt, wenn der Ansprechpartner nicht voreingestellt ist.
const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt
? getKontaktadressenByProjektId(currentLog.projektId).map((ka) => { ? kontaktadressenListe
.filter((ka: iKontaktadresse) => ka.projekt_id === currentLog.projekt_id)
.map((ka: iKontaktadresse) => {
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")} return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
}) })
: [] : []
// Einheit der Dauer-Anzeige im Format mm:ss.
const submit = (form: HTMLFormElement): boolean => {
const formData = new FormData(form)
const gespeichertesLog: iCallLog = {
id: currentLog.id,
projekt_id: currentLog.projekt_id,
projekt_name: "",
kunden_name: "",
datum: (formData.get("datum") as string) || "",
uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "",
dauer: (formData.get("dauer") as string) || "",
betreff: (formData.get("betreff") as string) || "",
name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "",
telefon: (formData.get("telefon") as string) || "",
notiz: (formData.get("notiz") as string) || "",
wiedervorlage_datum: (formData.get("wiedervorlage_datum") as string) || "",
erledigt: formData.get("erledigt") === "on"
}
if (currentLog.id === -1) {
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
} else {
updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
}
return true
}
const cancel = (e: Event) => {
m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})
}
return m("div.w3-container.w3-auto.form-view-container", return m("div.w3-container.w3-auto.form-view-container",
m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"), m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"),
// Form füllt den verbleibenden Platz; das Form-Widget selbst
// sorgt (mit stickyButtons, Default) dafür, dass nur der
// Inhalt scrollt und die Buttons fixiert bleiben.
m("div", {style: {flex: '1', 'min-height': '0'}}, m("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [ m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id}),
// Kunde und Projekt als Read-Only-Text, da voreingestellt.
m("label", "Kunde", m("label", "Kunde",
m(Select, { m("input.w3-input.w3-border", {
name: "kundenId", type: "text",
value: currentLog.kundenId, value: currentLog.kunden_name,
options: kundenOptionen, readonly: true,
readonly: kundeVoreingestellt, style: {'margin-bottom': '5px'}
required: true, })),
onchanged: (kundenId: number) => {
// Bei Kundenwechsel die kundenId übernehmen
// und projektId zurücksetzen, da die Projekte
// kundenabhängig sind.
currentLog.kundenId = kundenId
currentLog.projektId = -1
}
} as iSelect)),
m("label", "Projekt", m("label", "Projekt",
m(Select, { m("input.w3-input.w3-border", {
name: "projektId", type: "text",
value: currentLog.projektId, value: currentLog.projekt_name,
options: projektOptionen, readonly: true,
readonly: kundeVoreingestellt, style: {'margin-bottom': '5px'}
required: true, })),
onchanged: (projektId: number) => {
currentLog.projektId = projektId
// Bei Projektwechsel Ansprechpartner und Telefon
// zurücksetzen, falls nicht voreingestellt.
if (!ansprechpartnerVoreingestellt) {
currentLog.nameAnsprechpartner = ""
currentLog.telefon = ""
}
}
} as iSelect)),
// Datum, Startzeit und Dauer in einer Zeile. // Datum, Startzeit und Dauer in einer Zeile.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '1'}}, "Datum", m("label", {style: {flex: '1'}}, "Datum",
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)), m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
m("label", {style: {flex: '1'}}, "Uhrzeit Beginn", m("label", {style: {flex: '1'}}, "Uhrzeit Beginn",
m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)), m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)),
m("label", {style: {flex: '1'}}, "Dauer (mm:ss)", m("label", {style: {flex: '1'}}, "Dauer (mm:ss)",
// Raw input statt Input-Widget, damit oncreate die
// DOM-Referenz für den Timer greifen kann. Text-Typ,
// da das Format mm:ss keine Zahl ist.
m("input.w3-input.w3-border", { m("input.w3-input.w3-border", {
name: "dauer", name: "dauer",
type: "text", type: "text",
@@ -334,22 +336,18 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
), ),
m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)), m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)),
// Ansprechpartner und Telefon in einer Zeile. // Ansprechpartner und Telefon in einer Zeile.
// Wenn der Ansprechpartner voreingestellt ist (aus Kontaktadressenliste),
// wird ein readonly Input gezeigt; sonst ein Select, das die
// Kontaktadressen des gewählten Projekts anbietet. Bei Auswahl
// werden Name und Telefon automatisch übernommen.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '1'}}, "Name Ansprechpartner", m("label", {style: {flex: '1'}}, "Name Ansprechpartner",
ansprechpartnerVoreingestellt ansprechpartnerVoreingestellt
? m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text", readonly: true} as iInput) ? m(Input, {name: "name_ansprechpartner", value: currentLog.name_ansprechpartner, type: "text", readonly: true} as iInput)
: m(Select, { : m(Select, {
name: "kontaktadresseId", name: "kontaktadresse_id",
value: -1, value: -1,
options: kontaktadressenOptionen, options: kontaktadressenOptionen,
onchanged: (kontaktadresseId: number) => { onchanged: (kontaktadresseId: number) => {
const ka = getKontaktadresseById(kontaktadresseId) const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
if (ka) { if (ka) {
currentLog.nameAnsprechpartner = ka.name currentLog.name_ansprechpartner = ka.name
currentLog.telefon = ka.telefon currentLog.telefon = ka.telefon
} }
} }
@@ -358,7 +356,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput)) m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput))
), ),
m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)), m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)),
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)), m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlage_datum", value: currentLog.wiedervorlage_datum, type: "date"} as iInput)),
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt") m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
]) ])
) )
+69 -108
View File
@@ -1,48 +1,32 @@
import m from "mithril" import m from "mithril"
import {Form, iForm} from "./widgets/form" import {Form, iForm} from "./widgets/form"
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
import {Select, iSelect, iSelectOption} from "./widgets/select"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {iKontaktadresse, getKontaktadressenByProjektId, getNewKontaktadresseId, addKontaktadresse, updateKontaktadresse} from "../models/kontaktadresse" import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
import {iProjekt, getProjekte, getProjektById} from "../models/projekt" import {iProjekt, fetchProjekte} from "../models/projekt"
function createKontaktadresseTreenodes(projektId: number | null = null): iTreenode[] {
let filteredKontaktadressen = getKontaktadressenByProjektId(projektId)
return filteredKontaktadressen.map((kontaktadresse: iKontaktadresse) => {
return new Treenode(kontaktadresse, true, [])
})
}
export const kontaktadresseliste: m.ClosureComponent = (vnode) => { export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createKontaktadresseTreenodes() let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null let selectedNode: iTreenode | null = null
let filterProjektId: number | null = null let filterProjektId: number | null = null
// Projektname für die Überschrift, asynchron aus der Projektliste
// aufgelöst.
let projektName: string = ""
// Projektliste asynchron vom Server laden, für die Projektauflösung
// beim Anlegen einer neuen Kontaktadresse.
let projektListe: iProjekt[] = []
const handleRowMarked = (node: iTreenode) => { const handleRowMarked = (node: iTreenode) => {
selectedNode = node selectedNode = node
} }
const addKontaktadresseRoute = () => { const addKontaktadresseRoute = () => {
const params: any = { const params: any = {id: -1}
id: -1,
anrede: "",
name: "",
firma: "",
telefon: "",
email: "",
strasse: "",
plz: "",
ort: "",
land: "",
notiz: ""
}
// Nur bei projektgefilterter Liste das Projekt vorbelegen.
if (filterProjektId !== null) { if (filterProjektId !== null) {
const projekt = getProjekte().find((p: iProjekt) => p.id === filterProjektId) const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) { if (projekt) {
params.projektId = projekt.id params.projekt_id = projekt.id
params.projektname = projekt.projektname params.projekt_name = projekt.projektname
} }
} }
m.route.set("/kontaktadresse", params) m.route.set("/kontaktadresse", params)
@@ -50,21 +34,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => { const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/kontaktadresse", { m.route.set("/kontaktadresse", {id: kontaktadresse.id})
id: kontaktadresse.id,
projektId: kontaktadresse.projektId,
projektname: kontaktadresse.projektName,
anrede: kontaktadresse.anrede,
name: kontaktadresse.name,
firma: kontaktadresse.firma,
telefon: kontaktadresse.telefon,
email: kontaktadresse.email,
strasse: kontaktadresse.strasse,
plz: kontaktadresse.plz,
ort: kontaktadresse.ort,
land: kontaktadresse.land,
notiz: kontaktadresse.notiz
})
} }
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => { const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
@@ -72,21 +42,18 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const now = new Date() const now = new Date()
const datum = now.toISOString().split('T')[0] const datum = now.toISOString().split('T')[0]
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1] const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
// Kundennamen über das Projekt auflösen.
const projekt = getProjektById(kontaktadresse.projektId)
m.route.set("/call_log", { m.route.set("/call_log", {
id: -1, id: -1,
kundenName: projekt ? projekt.kunden_name : "", projekt_id: kontaktadresse.projekt_id,
projektId: kontaktadresse.projektId, projekt_name: kontaktadresse.projekt_name,
projektName: kontaktadresse.projektName,
datum: datum, datum: datum,
uhrzeitBeginn: uhrzeit, uhrzeit_beginn: uhrzeit,
dauer: "", dauer: "",
betreff: "", betreff: "",
nameAnsprechpartner: kontaktadresse.name, name_ansprechpartner: kontaktadresse.name,
telefon: kontaktadresse.telefon, telefon: kontaktadresse.telefon,
notiz: "", notiz: "",
wiedervorlageDatum: "", wiedervorlage_datum: "",
erledigt: false erledigt: false
}) })
} }
@@ -100,7 +67,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
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.projektName), 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"))
@@ -116,18 +83,37 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
kontaktadresse.telefon.toLowerCase().includes(searchLower) || kontaktadresse.telefon.toLowerCase().includes(searchLower) ||
kontaktadresse.email.toLowerCase().includes(searchLower) || kontaktadresse.email.toLowerCase().includes(searchLower) ||
kontaktadresse.ort.toLowerCase().includes(searchLower) || kontaktadresse.ort.toLowerCase().includes(searchLower) ||
kontaktadresse.projektName.toLowerCase().includes(searchLower) kontaktadresse.projekt_name.toLowerCase().includes(searchLower)
} }
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams && routeParams.projektId) { if (routeParams && routeParams.projekt_id) {
filterProjektId = parseInt(routeParams.projektId) filterProjektId = parseInt(routeParams.projekt_id)
projektName = routeParams.projekt_name || ""
} else { } else {
filterProjektId = null filterProjektId = null
projektName = ""
} }
treenodes = createKontaktadresseTreenodes(filterProjektId) // Kontaktadressen asynchron vom Server laden.
fetchKontaktadressen().then((result) => {
let filtered = result
if (filterProjektId !== null) {
filtered = result.filter((k: iKontaktadresse) => k.projekt_id === filterProjektId)
}
treenodes = filtered.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
})
// Projektliste asynchron vom Server laden.
fetchProjekte().then((result) => {
projektListe = result
if (filterProjektId !== null && projektName === "") {
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
projektName = projekt.projektname
}
}
})
}, },
view: (vnode) => { view: (vnode) => {
return m("div.w3-container.w3-auto", { return m("div.w3-container.w3-auto", {
@@ -136,7 +122,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
'flex-direction': 'column', 'flex-direction': 'column',
height: 'inherit', height: 'inherit',
'overflow-y': 'inherit'}}, 'overflow-y': 'inherit'}},
m("h1", "Kontaktadressen"), m("h1", projektName ? "Kontaktadressen für " + projektName : "Kontaktadressen"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable, m(Treetable,
{ {
@@ -157,12 +143,12 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
} }
export const Kontaktadresse: m.ClosureComponent = (vnode) => { export const Kontaktadresse: m.ClosureComponent = (vnode) => {
// currentKontaktadresse hält den Formularzustand. projektId referenziert das // currentKontaktadresse hält den Formularzustand. projekt_id referenziert
// Projekt eindeutig, projektname dient der Anzeige. // das Projekt eindeutig, projekt_name dient der Anzeige in der Überschrift.
let currentKontaktadresse: { let currentKontaktadresse: {
id: number id: number
projektId: number projekt_id: number
projektname: string projekt_name: string
anrede: string anrede: string
name: string name: string
firma: string firma: string
@@ -175,8 +161,8 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
notiz: string notiz: string
} = { } = {
id: -1, id: -1,
projektId: -1, projekt_id: -1,
projektname: "", projekt_name: "",
anrede: "", anrede: "",
name: "", name: "",
firma: "", firma: "",
@@ -188,42 +174,29 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
land: "", land: "",
notiz: "" notiz: ""
} }
// Ist das Projekt voreingestellt (gefilterte Kontaktadressenliste)?
let projektVoreingestellt: boolean = false
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams) { const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1 if (id !== -1) {
projektVoreingestellt = projektId !== -1 // Kontaktadresse frisch vom Server holen.
currentKontaktadresse = { fetchKontaktadresseById(id).then((result) => {
id: routeParams.id ? parseInt(routeParams.id) : -1, currentKontaktadresse = result
projektId: projektId, })
projektname: routeParams.projektname || "", } else {
anrede: routeParams.anrede || "", // Neuanlegen: projekt_id und projekt_name aus Route-Params.
name: routeParams.name || "", currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
firma: routeParams.firma || "", currentKontaktadresse.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
telefon: routeParams.telefon || "",
email: routeParams.email || "",
strasse: routeParams.strasse || "",
plz: routeParams.plz || "",
ort: routeParams.ort || "",
land: routeParams.land || "",
notiz: routeParams.notiz || ""
}
} }
}, },
view: (vnode) => { view: (vnode) => {
const submit = (form: HTMLFormElement): boolean => { const submit = (form: HTMLFormElement): boolean => {
const formData = new FormData(form) const formData = new FormData(form)
const projektId = parseInt(formData.get("projektId") as string)
// Projektnamen zur gewählten projektId auflösen.
const projekt = getProjekte().find((p: iProjekt) => p.id === projektId)
const gespeicherteKontaktadresse: iKontaktadresse = { const gespeicherteKontaktadresse: iKontaktadresse = {
id: currentKontaktadresse.id, id: currentKontaktadresse.id,
projektId: projektId, projekt_id: currentKontaktadresse.projekt_id,
projektName: projekt ? projekt.projektname : "", projekt_name: "",
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) || "",
@@ -236,40 +209,28 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
notiz: (formData.get("notiz") as string) || "" notiz: (formData.get("notiz") as string) || ""
} }
if (currentKontaktadresse.id === -1) { if (currentKontaktadresse.id === -1) {
gespeicherteKontaktadresse.id = getNewKontaktadresseId() createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
addKontaktadresse(gespeicherteKontaktadresse)
} else { } else {
updateKontaktadresse(gespeicherteKontaktadresse) updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
} }
m.route.set("/kontaktadresseliste")
return true return true
} }
const cancel = (e: Event) => { const cancel = (e: Event) => {
m.route.set("/kontaktadresseliste") m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})
} }
// Select-Optionen aus der Projektliste aufbauen.
const projektOptionen: iSelectOption[] = getProjekte().map((p: iProjekt) => {
return {value: p.id, label: p.projektname}
})
return m("div.w3-container.w3-auto.form-view-container", return m("div.w3-container.w3-auto.form-view-container",
m("h1", currentKontaktadresse.id === -1 ? "Neue Kontaktadresse anlegen" : "Kontaktadresse-Stammblatt"), m("h1", currentKontaktadresse.id === -1
? "Neue Kontaktadresse für " + currentKontaktadresse.projekt_name
: "Kontaktadresse-Stammblatt"),
m("div", {style: {flex: '1', 'min-height': '0'}}, m("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("label", "Projekt", m("input", {type: "hidden", name: "projekt_id", value: currentKontaktadresse.projekt_id}),
m(Select, {
name: "projektId",
value: currentKontaktadresse.projektId,
options: projektOptionen,
readonly: projektVoreingestellt,
required: true
} as iSelect)),
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", "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)),
// PLZ und Ort in einer Zeile.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}}, m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '0 0 120px'}}, "PLZ", m("label", {style: {flex: '0 0 120px'}}, "PLZ",
m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)), m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)),
+9 -9
View File
@@ -40,12 +40,12 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const showCallLogs = (projekt: iProjekt, e: Event) => { const showCallLogs = (projekt: iProjekt, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/calllogliste", {projektId: projekt.id}) m.route.set("/calllogliste", {projekt_id: projekt.id})
} }
const showKontaktadressen = (projekt: iProjekt, e: Event) => { const showKontaktadressen = (projekt: iProjekt, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/kontaktadresseliste", {projektId: projekt.id}) m.route.set("/kontaktadresseliste", {projekt_id: projekt.id})
} }
const addCallLog = (projekt: iProjekt, e: Event) => { const addCallLog = (projekt: iProjekt, e: Event) => {
@@ -55,17 +55,17 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1] const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
m.route.set("/call_log", { m.route.set("/call_log", {
id: -1, id: -1,
kundenName: projekt.kunden_name, kunden_name: projekt.kunden_name,
projektId: projekt.id, projekt_id: projekt.id,
projektName: projekt.projektname, projekt_name: projekt.projektname,
datum: datum, datum: datum,
uhrzeitBeginn: uhrzeit, uhrzeit_beginn: uhrzeit,
dauer: "", dauer: "",
betreff: "", betreff: "",
nameAnsprechpartner: "", name_ansprechpartner: "",
telefon: "", telefon: "",
notiz: "", notiz: "",
wiedervorlageDatum: "", wiedervorlage_datum: "",
erledigt: false erledigt: false
}) })
} }
@@ -221,7 +221,7 @@ export const Projekt: m.ClosureComponent = (vnode) => {
: "Projekt-Stammblatt"), : "Projekt-Stammblatt"),
currentProjekt.id !== -1 currentProjekt.id !== -1
? m("button.w3-button.w3-theme", ? m("button.w3-button.w3-theme",
{onclick: () => m.route.set("/kontaktadresseliste", {projektId: currentProjekt.id})}, {onclick: () => m.route.set("/kontaktadresseliste", {projekt_id: currentProjekt.id})},
m("i.material-icons", "contact_page"), " Kontaktadressen") m("i.material-icons", "contact_page"), " Kontaktadressen")
: null : null
), ),
+10 -65
View File
@@ -2,86 +2,31 @@ import m from "mithril"
/** /**
* Datenmodell für eine Gesprächsnotiz (Call-Log). * Datenmodell für eine Gesprächsnotiz (Call-Log).
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
* serverseitig aus der Projekt-Relationship abgeleitet und im JSON ergänzt.
*/ */
export interface iCallLog { export interface iCallLog {
id: number id: number
kundenName: string projekt_id: number
projektId: number projekt_name: string
projektName: string kunden_name: string
datum: string datum: string
uhrzeitBeginn: string uhrzeit_beginn: string
dauer: string dauer: string
betreff: string betreff: string
nameAnsprechpartner: string name_ansprechpartner: string
telefon: string telefon: string
notiz: string notiz: string
wiedervorlageDatum: string wiedervorlage_datum: string
erledigt: boolean erledigt: boolean
} }
/**
* In-Memory-Datenspeicher für Gesprächsnotizen. Wird als Fallback solange
* verwendet, bis die serverseitigen REST-Endpunkte verfügbar sind.
*/
export const callLogDaten: iCallLog[] = [
{id: 1, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-20", uhrzeitBeginn: "09:30", dauer: "45", betreff: "Anforderungen klären", nameAnsprechpartner: "Herr Schmidt", telefon: "+49 123 45678", notiz: "Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.", wiedervorlageDatum: "2024-01-27", erledigt: false},
{id: 2, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-25", uhrzeitBeginn: "14:00", dauer: "30", betreff: "Feedback zum Design", nameAnsprechpartner: "Frau Müller", telefon: "+49 123 45679", notiz: "Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.", wiedervorlageDatum: "2024-02-01", erledigt: false},
{id: 3, kundenName: "Beispiel AG", projektId: 2, projektName: "E-Commerce Plattform", datum: "2024-02-10", uhrzeitBeginn: "10:00", dauer: "60", betreff: "Technische Anforderungen", nameAnsprechpartner: "Herr Weber", telefon: "+49 123 45680", notiz: "Payment-Gateway und Versand-Schnittstellen müssen integriert werden. Zeitplan besprochen.", wiedervorlageDatum: "2024-02-15", erledigt: true},
{id: 4, kundenName: "Beispiel AG", projektId: 3, projektName: "CRM-System", datum: "2024-02-18", uhrzeitBeginn: "11:15", dauer: "20", betreff: "Schulungstermin", nameAnsprechpartner: "Frau Bauer", telefon: "+49 123 45681", notiz: "Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.", wiedervorlageDatum: "2024-02-20", erledigt: false},
{id: 5, kundenName: "Test Unternehmen", projektId: 4, projektName: "Mobile App", datum: "2024-03-05", uhrzeitBeginn: "15:45", dauer: "35", betreff: "Feature-Request", nameAnsprechpartner: "Herr König", telefon: "+49 123 45682", notiz: "Kunde wünscht Push-Benachrichtigungen und Offline-Modus.", wiedervorlageDatum: "2024-03-12", erledigt: false}
]
/**
* Liefert die nächste freie Call-Log-ID auf Basis des In-Memory-Speichers.
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
*/
export function getNewCallLogId(): number {
if (callLogDaten.length === 0) {
return 1
}
return Math.max(...callLogDaten.map((log: iCallLog) => log.id)) + 1
}
/**
* Fügt eine neue Gesprächsnotiz dem In-Memory-Speicher hinzu.
*/
export function addCallLogData(log: iCallLog): void {
callLogDaten.push(log)
}
/**
* Aktualisiert eine bestehende Gesprächsnotiz im In-Memory-Speicher.
*/
export function updateCallLogData(log: iCallLog): void {
const index = callLogDaten.findIndex((l: iCallLog) => l.id === log.id)
if (index !== -1) {
callLogDaten[index] = log
}
}
/**
* Liefert eine Kopie aller Gesprächsnotizen aus dem In-Memory-Speicher.
*/
export function getCallLogs(): iCallLog[] {
return [...callLogDaten]
}
/**
* Liefert die Gesprächsnotizen eines bestimmten Projekts. Ist projektId null,
* werden alle Gesprächsnotizen zurückgegeben.
*/
export function getCallLogsByProjektId(projektId: number | null): iCallLog[] {
return projektId
? callLogDaten.filter((l: iCallLog) => l.projektId === projektId)
: [...callLogDaten]
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* REST-API-Aufrufe (Client-Seite) */ /* REST-API-Aufrufe (Client-Seite) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */ /* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */ /* serverseitigen REST-Endpunkten (/xhr/calllog/*). */
/* implementiert und werden später nachgereicht. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
+7 -68
View File
@@ -2,12 +2,15 @@ import m from "mithril"
/** /**
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts). * Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projektId). * Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
* projekt_name ist keine ORM-Spalte, sondern wird serverseitig aus der
* Projekt-Relationship abgeleitet und im JSON ergänzt.
*/ */
export interface iKontaktadresse { export interface iKontaktadresse {
id: number id: number
projektId: number projekt_id: number
projektName: string projekt_name: string
anrede: string anrede: string
name: string name: string
firma: string firma: string
@@ -20,75 +23,11 @@ export interface iKontaktadresse {
notiz: string notiz: string
} }
/**
* In-Memory-Datenspeicher für Kontaktadressen. Wird als Fallback solange verwendet,
* bis die serverseitigen REST-Endpunkte verfügbar sind.
*/
export const kontaktadresseDaten: iKontaktadresse[] = [
{id: 1, projektId: 1, projektName: "Website-Relaunch", anrede: "Herr", name: "Thomas Becker", firma: "Becker Webdesign GmbH", telefon: "+49 201 123456", email: "t.becker@becker-webdesign.de", strasse: "Industriestr. 12", plz: "45131", ort: "Essen", land: "Deutschland", notiz: "Interessent für Website-Relaunch, Erstkontakt auf Messe."},
{id: 2, projektId: 1, projektName: "Website-Relaunch", anrede: "Frau", name: "Sandra Klein", firma: "Klein Marketing", telefon: "+49 211 987654", email: "s.klein@klein-marketing.de", strasse: "Königsallee 5", plz: "40212", ort: "Düsseldorf", land: "Deutschland", notiz: ""},
{id: 3, projektId: 2, projektName: "E-Commerce Plattform", anrede: "Herr", name: "Michael Wagner", firma: "Wagner Online Shop", telefon: "+49 221 555123", email: "m.wagner@wagner-shop.de", strasse: "Hauptstr. 88", plz: "50667", ort: "Köln", land: "Deutschland", notiz: "Sucht E-Commerce-Lösung, Budget geklärt."},
{id: 4, projektId: 3, projektName: "CRM-System", anrede: "Frau", name: "Julia Hoffmann", firma: "Hoffmann Consulting", telefon: "+49 89 444333", email: "j.hoffmann@hoffmann-consulting.de", strasse: "Marienplatz 3", plz: "80331", ort: "München", land: "Deutschland", notiz: "Wiedervorlage im nächsten Quartal."}
]
/**
* Liefert die nächste freie Kontaktadressen-ID auf Basis des In-Memory-Speichers.
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
*/
export function getNewKontaktadresseId(): number {
if (kontaktadresseDaten.length === 0) {
return 1
}
return Math.max(...kontaktadresseDaten.map((ka: iKontaktadresse) => ka.id)) + 1
}
/**
* Fügt eine neue Kontaktadresse dem In-Memory-Speicher hinzu.
*/
export function addKontaktadresse(kontaktadresse: iKontaktadresse): void {
kontaktadresseDaten.push(kontaktadresse)
}
/**
* Aktualisiert eine bestehende Kontaktadresse im In-Memory-Speicher.
*/
export function updateKontaktadresse(kontaktadresse: iKontaktadresse): void {
const index = kontaktadresseDaten.findIndex((k: iKontaktadresse) => k.id === kontaktadresse.id)
if (index !== -1) {
kontaktadresseDaten[index] = kontaktadresse
}
}
/**
* Liefert eine Kopie aller Kontaktadressen aus dem In-Memory-Speicher.
*/
export function getKontaktadressen(): iKontaktadresse[] {
return [...kontaktadresseDaten]
}
/**
* Liefert die Kontaktadressen eines bestimmten Projekts. Ist projektId null,
* werden alle Kontaktadressen zurückgegeben.
*/
export function getKontaktadressenByProjektId(projektId: number | null): iKontaktadresse[] {
return projektId
? kontaktadresseDaten.filter((k: iKontaktadresse) => k.projektId === projektId)
: [...kontaktadresseDaten]
}
/**
* Liefert eine einzelne Kontaktadresse anhand ihrer ID oder undefined.
*/
export function getKontaktadresseById(id: number): iKontaktadresse | undefined {
return kontaktadresseDaten.find((k: iKontaktadresse) => k.id === id)
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* REST-API-Aufrufe (Client-Seite) */ /* REST-API-Aufrufe (Client-Seite) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */ /* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */ /* serverseitigen REST-Endpunkten (/xhr/kontaktadresse/*). */
/* implementiert und werden später nachgereicht. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
-64
View File
@@ -16,70 +16,6 @@ export interface iProjekt {
startdatum: string startdatum: string
} }
/**
* In-Memory-Datenspeicher für Projekte. Wird noch von call_log.ts und
* kontaktadresse.ts verwendet, bis diese Entitäten ebenfalls auf REST
* umgestellt sind.
*/
export const projektDaten: iProjekt[] = [
{id: 1, kunden_id: 1, kunden_name: "Musterfirma GmbH", projektname: "Website-Relaunch", zweck: "Neugestaltung der Unternehmenswebsite", verantwortlicher: "Max Mustermann", startdatum: "2024-01-15"},
{id: 2, kunden_id: 1, kunden_name: "Musterfirma GmbH", projektname: "E-Commerce Plattform", zweck: "Online-Shop Entwicklung", verantwortlicher: "Anna Schmidt", startdatum: "2024-03-01"},
{id: 3, kunden_id: 2, kunden_name: "Beispiel AG", projektname: "CRM-System", zweck: "Kundenbeziehungsmanagement", verantwortlicher: "Peter Müller", startdatum: "2024-02-20"},
{id: 4, kunden_id: 3, kunden_name: "Test Unternehmen", projektname: "Mobile App", zweck: "iOS und Android Entwicklung", verantwortlicher: "Lisa Bauer", startdatum: "2024-04-10"}
]
/**
* Liefert die nächste freie Projekt-ID auf Basis des In-Memory-Speichers.
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
*/
export function getNewProjektId(): number {
if (projektDaten.length === 0) {
return 1
}
return Math.max(...projektDaten.map((projekt: iProjekt) => projekt.id)) + 1
}
/**
* Fügt ein neues Projekt dem In-Memory-Speicher hinzu.
*/
export function addProjekt(projekt: iProjekt): void {
projektDaten.push(projekt)
}
/**
* Aktualisiert ein bestehendes Projekt im In-Memory-Speicher.
*/
export function updateProjekt(projekt: iProjekt): void {
const index = projektDaten.findIndex((p: iProjekt) => p.id === projekt.id)
if (index !== -1) {
projektDaten[index] = projekt
}
}
/**
* Liefert eine Kopie aller Projekte aus dem In-Memory-Speicher.
*/
export function getProjekte(): iProjekt[] {
return [...projektDaten]
}
/**
* Liefert die Projekte eines bestimmten Kunden. Ist kunden_id null, werden
* alle Projekte zurückgegeben.
*/
export function getProjekteByKundenId(kunden_id: number | null): iProjekt[] {
return kunden_id
? projektDaten.filter((p: iProjekt) => p.kunden_id === kunden_id)
: [...projektDaten]
}
/**
* Liefert ein einzelnes Projekt anhand seiner ID oder undefined.
*/
export function getProjektById(id: number): iProjekt | undefined {
return projektDaten.find((p: iProjekt) => p.id === id)
}
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* REST-API-Aufrufe (Client-Seite) */ /* REST-API-Aufrufe (Client-Seite) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */