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:
1 parent
e04a52857a
commit
b510c50497
13 files changed
+799
-481
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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()
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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
@@ -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")
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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
@@ -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. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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) */
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|||||||
Reference in new issue
Block a user