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

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

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

No files matched your search

+2
View File
@@ -49,6 +49,8 @@ export const Select: m.ClosureComponent<iSelect> = () => {
}
```
Nach jeder Änderung an `.ts`-Dateien unter `Client/` ist der Build-Prozess (tsc + rollup) auszuführen, damit die deploybaren Bundles in `NGINX_Server/html/static/` aktuell sind. Verwende dafür den `client-build`-Skill.
## Vorgaben für die Server-seitige Entwicklung unter Python
## Generierte Dateien nicht beachten
+114
View File
@@ -35,6 +35,8 @@ def register_cli_commands(app: Flask):
crm_checkdb.checkupdates()
_seed_muster_kunden()
_seed_muster_projekte()
_seed_muster_kontaktadressen()
_seed_muster_calllogs()
click.echo("Fertig.")
@@ -147,6 +149,118 @@ def _seed_muster_projekte():
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():
"""
Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen
+76 -1
View File
@@ -12,7 +12,7 @@ erfolgt ausschließlich über __bind_key__.
import datetime
import decimal
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from db import dbapp
@@ -154,4 +154,79 @@ class Projekt(dbapp.Model):
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")
+117
View File
@@ -0,0 +1,117 @@
"""
Geschäftslogik für die Entität CallLog (Gesprächsnotiz).
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
dort über den Decorator authorisation_required.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-04
:version: 1.0
"""
from db import dbapp
from crm.model import CallLog, Projekt
from auth.model.errors import ApplicationError, HtmlStatusCode
session = dbapp.session
def get_call_logs():
"""
Liefert alle Gesprächsnotizen, sortiert nach Datum absteigend.
"""
return CallLog.query.order_by(CallLog.datum.desc()).all()
def get_call_log(call_log_id: int) -> CallLog:
"""
Liefert eine einzelne Gesprächsnotiz anhand ihrer ID.
Wirft ApplicationError (404), falls die Notiz nicht existiert.
"""
call_log = CallLog.query.get(call_log_id)
if call_log is None:
raise ApplicationError(
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return call_log
def create_call_log(data: dict) -> CallLog:
"""
Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
- data: dict mit den Feldern projekt_id, datum, uhrzeit_beginn, dauer,
betreff, name_ansprechpartner, telefon, notiz,
wiedervorlage_datum, erledigt
"""
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
call_log = CallLog()
call_log.projekt_id = int(projekt_id)
call_log.datum = (data.get("datum") or "").strip() or None
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
call_log.dauer = (data.get("dauer") or "").strip() or None
call_log.betreff = (data.get("betreff") or "").strip() or None
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
call_log.telefon = (data.get("telefon") or "").strip() or None
call_log.notiz = (data.get("notiz") or "").strip() or None
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
call_log.erledigt = bool(data.get("erledigt", False))
session.add(call_log)
session.commit()
return call_log
def update_call_log(data: dict) -> CallLog:
"""
Aktualisiert eine bestehende Gesprächsnotiz. Die ID im data-Dict
bestimmt den zu ändernden Datensatz.
"""
call_log = get_call_log(data["id"])
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
call_log.projekt_id = int(projekt_id)
call_log.datum = (data.get("datum") or "").strip() or None
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
call_log.dauer = (data.get("dauer") or "").strip() or None
call_log.betreff = (data.get("betreff") or "").strip() or None
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
call_log.telefon = (data.get("telefon") or "").strip() or None
call_log.notiz = (data.get("notiz") or "").strip() or None
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
call_log.erledigt = bool(data.get("erledigt", False))
session.commit()
return call_log
def delete_call_log(call_log_id: int):
"""
Löscht eine Gesprächsnotiz anhand ihrer ID.
Wirft ApplicationError (404), falls die Notiz nicht existiert.
"""
call_log = get_call_log(call_log_id)
session.delete(call_log)
session.commit()
+11
View File
@@ -67,6 +67,17 @@ def _db_V01_02():
session.commit()
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():
"""
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
+128
View File
@@ -0,0 +1,128 @@
"""
Geschäftslogik für die Entität Kontaktadresse.
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
dort über den Decorator authorisation_required.
:author: Mistral Vibe <vibe@mistral.ai>
:date: 2026-09-04
:version: 1.0
"""
from db import dbapp
from crm.model import Kontaktadresse, Projekt
from auth.model.errors import ApplicationError, HtmlStatusCode
session = dbapp.session
def get_kontaktadressen():
"""
Liefert alle Kontaktadressen, sortiert nach Name.
"""
return Kontaktadresse.query.order_by(Kontaktadresse.name).all()
def get_kontaktadresse(kontaktadresse_id: int) -> Kontaktadresse:
"""
Liefert eine einzelne Kontaktadresse anhand ihrer ID.
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
"""
kontaktadresse = Kontaktadresse.query.get(kontaktadresse_id)
if kontaktadresse is None:
raise ApplicationError(
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
htmlstatus=HtmlStatusCode.Not_Found,
)
return kontaktadresse
def create_kontaktadresse(data: dict) -> Kontaktadresse:
"""
Legt eine neue Kontaktadresse an. Die vom Client übergebene ID wird
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
- data: dict mit den Feldern projekt_id, anrede, name, firma, telefon,
email, strasse, plz, ort, land, notiz
"""
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("name") or not str(data["name"]).strip():
raise ApplicationError(
"Der Name darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kontaktadresse = Kontaktadresse()
kontaktadresse.projekt_id = int(projekt_id)
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
kontaktadresse.plz = (data.get("plz") or "").strip() or None
kontaktadresse.ort = (data.get("ort") or "").strip() or None
kontaktadresse.land = (data.get("land") or "").strip() or None
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
session.add(kontaktadresse)
session.commit()
return kontaktadresse
def update_kontaktadresse(data: dict) -> Kontaktadresse:
"""
Aktualisiert eine bestehende Kontaktadresse. Die ID im data-Dict
bestimmt den zu ändernden Datensatz.
"""
kontaktadresse = get_kontaktadresse(data["id"])
projekt_id = data.get("projekt_id")
if projekt_id is None:
raise ApplicationError(
"Es muss ein Projekt ausgewählt werden.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
projekt = Projekt.query.get(int(projekt_id))
if projekt is None:
raise ApplicationError(
"Das ausgewählte Projekt existiert nicht.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
if not data.get("name") or not str(data["name"]).strip():
raise ApplicationError(
"Der Name darf nicht leer sein.",
htmlstatus=HtmlStatusCode.Bad_Request,
)
kontaktadresse.projekt_id = int(projekt_id)
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
kontaktadresse.name = data["name"].strip()
kontaktadresse.firma = (data.get("firma") or "").strip() or None
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
kontaktadresse.email = (data.get("email") or "").strip() or None
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
kontaktadresse.plz = (data.get("plz") or "").strip() or None
kontaktadresse.ort = (data.get("ort") or "").strip() or None
kontaktadresse.land = (data.get("land") or "").strip() or None
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
session.commit()
return kontaktadresse
def delete_kontaktadresse(kontaktadresse_id: int):
"""
Löscht eine Kontaktadresse anhand ihrer ID.
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
"""
kontaktadresse = get_kontaktadresse(kontaktadresse_id)
session.delete(kontaktadresse)
session.commit()
+92
View File
@@ -21,6 +21,8 @@ from crm.model import KUNDEN_MANAGER, MITARBEITER
from crm.model import kunde as kunden
from crm.model import kontakt as kontakte
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")
@@ -160,4 +162,94 @@ def projekt_delete():
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")
+186 -188
View File
@@ -3,28 +3,23 @@ import {Form, iForm} from "./widgets/form"
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 {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 {getProjekteByKundenId, getProjektById} from "../models/projekt"
import {getKontaktadressenByProjektId, getKontaktadresseById} from "../models/kontaktadresse"
import {iProjekt, fetchProjekte} from "../models/projekt"
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
// 1000 = jede Sekunde, 60000 = jede Minute.
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) => {
let treenodes: iTreenode[] = createCallLogTreenodes()
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | 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) => {
selectedNode = node
}
@@ -36,92 +31,96 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
const params: any = {
id: -1,
datum: datum,
uhrzeitBeginn: uhrzeit,
uhrzeit_beginn: uhrzeit,
dauer: "",
betreff: "",
nameAnsprechpartner: "",
name_ansprechpartner: "",
telefon: "",
notiz: "",
wiedervorlageDatum: "",
wiedervorlage_datum: "",
erledigt: false
}
// Bei projektgefilterter Liste Kunde und Projekt vorbelegen.
if (filterProjektId !== null) {
const projekt = getProjektById(filterProjektId)
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
params.kundenName = projekt.kunden_name
params.projektId = projekt.id
params.projektName = projekt.projektname
params.projekt_id = projekt.id
params.projekt_name = projekt.projektname
params.kunden_name = projekt.kunden_name
}
}
m.route.set("/call_log", params)
}
const editCallLog = (log: iCallLog, e: Event) => {
e.stopPropagation()
m.route.set("/call_log", {
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
})
m.route.set("/call_log", {id: log.id})
}
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
let log: iCallLog = node.nodevalue
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.uhrzeitBeginn),
m("span", log.uhrzeit_beginn),
m("span", log.dauer + " min"),
m("span", log.betreff),
m("span", log.wiedervorlageDatum),
m("span", log.wiedervorlage_datum),
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"))
]
}
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let log: iCallLog = node.nodevalue
let searchLower = searchvalue.toLowerCase()
return log.kundenName.toLowerCase().includes(searchLower) ||
log.projektName.toLowerCase().includes(searchLower) ||
return log.kunden_name.toLowerCase().includes(searchLower) ||
log.projekt_name.toLowerCase().includes(searchLower) ||
log.datum.includes(searchLower) ||
log.uhrzeitBeginn.toLowerCase().includes(searchLower) ||
log.uhrzeit_beginn.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.wiedervorlageDatum.includes(searchLower)
log.wiedervorlage_datum.includes(searchLower)
}
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams && routeParams.projektId) {
filterProjektId = parseInt(routeParams.projektId)
if (routeParams && routeParams.projekt_id) {
filterProjektId = parseInt(routeParams.projekt_id)
projektName = routeParams.projekt_name || ""
} else {
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) => {
return m("div.w3-container.w3-auto", {
style: {
display: 'flex',
'flex-direction': 'column',
height: 'inherit',
display: 'flex',
'flex-direction': 'column',
height: '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(Treetable,
{
@@ -142,187 +141,190 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
}
export const Call_Log: m.ClosureComponent = (vnode) => {
// currentLog hält den Formularzustand. kundenId ist intern und wird über
// das Projekt aufgelöst; kundenName/projektName dienen der Anzeige.
// currentLog hält den Formularzustand. projekt_id referenziert das
// Projekt, kunden_name und projekt_name dienen der Anzeige.
let currentLog: {
id: number
kundenId: number
kundenName: string
projektId: number
projektName: string
projekt_id: number
projekt_name: string
kunden_name: string
datum: string
uhrzeitBeginn: string
uhrzeit_beginn: string
dauer: string
betreff: string
nameAnsprechpartner: string
name_ansprechpartner: string
telefon: string
notiz: string
wiedervorlageDatum: string
wiedervorlage_datum: string
erledigt: boolean
} = {
id: -1,
kundenId: -1,
kundenName: "",
projektId: -1,
projektName: "",
projekt_id: -1,
projekt_name: "",
kunden_name: "",
datum: "",
uhrzeitBeginn: "",
uhrzeit_beginn: "",
dauer: "",
betreff: "",
nameAnsprechpartner: "",
name_ansprechpartner: "",
telefon: "",
notiz: "",
wiedervorlageDatum: "",
wiedervorlage_datum: "",
erledigt: false
}
// Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert
// oder aus der Projektliste aufgerufen)?
let kundeVoreingestellt: boolean = false
// Ist das Projekt voreingestellt (Liste wurde projektgefiltert)?
let projektVoreingestellt: boolean = false
// 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
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
let startZeit: Date | null = null
// Timer-Handle für die Dauer-Aktualisierung.
let timerId: number | null = null
// Direkte DOM-Referenz auf das Dauer-Feld, damit der Timer den Wert
// ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder
// mit den veralteten currentLog-Werten überschreiben).
// Direkte DOM-Referenz auf das Dauer-Feld.
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 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 m = Math.floor(sekunden / 60)
const s = sekunden % 60
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
const min = Math.floor(sekunden / 60)
const sek = sekunden % 60
return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0')
}
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams) {
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
kundeVoreingestellt = projektId !== -1
// kundenId aus dem Projekt ableiten, falls nicht direkt gesetzt.
let kundenId = -1
if (kundeVoreingestellt) {
const projekt = getProjektById(projektId)
if (projekt) { kundenId = projekt.kunden_id }
}
currentLog = {
id: routeParams.id ? parseInt(routeParams.id) : -1,
kundenId: kundenId,
kundenName: routeParams.kundenName || "",
projektId: projektId,
projektName: routeParams.projektName || "",
datum: routeParams.datum || "",
uhrzeitBeginn: routeParams.uhrzeitBeginn || "",
dauer: routeParams.dauer || "",
betreff: routeParams.betreff || "",
nameAnsprechpartner: routeParams.nameAnsprechpartner || "",
telefon: routeParams.telefon || "",
notiz: routeParams.notiz || "",
wiedervorlageDatum: routeParams.wiedervorlageDatum || "",
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
}
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
if (id !== -1) {
// Gesprächsnotiz frisch vom Server holen.
fetchCallLogById(id).then((result) => {
currentLog = result
projektVoreingestellt = true
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
})
} else {
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
currentLog.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
currentLog.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
currentLog.datum = routeParams && routeParams.datum ? routeParams.datum : ""
currentLog.uhrzeit_beginn = routeParams && routeParams.uhrzeit_beginn ? routeParams.uhrzeit_beginn : ""
currentLog.dauer = routeParams && routeParams.dauer ? routeParams.dauer : ""
currentLog.betreff = routeParams && routeParams.betreff ? routeParams.betreff : ""
currentLog.name_ansprechpartner = routeParams && routeParams.name_ansprechpartner ? routeParams.name_ansprechpartner : ""
currentLog.telefon = routeParams && routeParams.telefon ? routeParams.telefon : ""
currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
ansprechpartnerVoreingestellt = currentLog.nameAnsprechpartner !== ""
if (currentLog.id === -1) {
startZeit = new Date()
timerId = window.setInterval(() => {
if (startZeit) {
// Verstrichene Sekunden berechnen; das Intervall
// steuert nur die Update-Frequenz.
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
const dauer = formatDauer(sekunden)
currentLog.dauer = dauer
// Nur das Dauer-Feld direkt im DOM aktualisieren,
// ohne redraw (sonst werden andere Felder zurückgesetzt).
if (dauerInput) { dauerInput.value = dauer }
}
}, DAUER_UPDATE_INTERVALL)
}
startZeit = new Date()
timerId = window.setInterval(() => {
if (startZeit) {
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
const dauer = formatDauer(sekunden)
currentLog.dauer = dauer
if (dauerInput) { dauerInput.value = dauer }
}
}, DAUER_UPDATE_INTERVALL)
}
// Kundenliste asynchron vom Server laden.
fetchKunden().then((result) => {
kundenListe = result
})
// Alle Listen asynchron laden, für Select-Optionen.
fetchKunden().then((result) => { kundenListe = result })
fetchProjekte().then((result) => { projektListe = result })
fetchKontaktadressen().then((result) => { kontaktadressenListe = result })
},
onremove: (vnode) => {
// Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft.
if (timerId != null) { window.clearInterval(timerId) }
},
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) => {
return {value: k.id, label: k.name}
})
// Projekt-Optionen sind vom gewählten Kunden abhängig.
const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => {
return {value: p.id, label: p.projektname}
})
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
// Werden nur benötigt, wenn der Ansprechpartner nicht voreingestellt ist.
const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt
? getKontaktadressenByProjektId(currentLog.projektId).map((ka) => {
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
const projektOptionen: iSelectOption[] = projektListe
.filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId)
.map((p: iProjekt) => {
return {value: p.id, label: p.projektname}
})
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt
? kontaktadressenListe
.filter((ka: iKontaktadresse) => ka.projekt_id === currentLog.projekt_id)
.map((ka: iKontaktadresse) => {
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",
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(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(Select, {
name: "kundenId",
value: currentLog.kundenId,
options: kundenOptionen,
readonly: kundeVoreingestellt,
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("input.w3-input.w3-border", {
type: "text",
value: currentLog.kunden_name,
readonly: true,
style: {'margin-bottom': '5px'}
})),
m("label", "Projekt",
m(Select, {
name: "projektId",
value: currentLog.projektId,
options: projektOptionen,
readonly: kundeVoreingestellt,
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)),
m("input.w3-input.w3-border", {
type: "text",
value: currentLog.projekt_name,
readonly: true,
style: {'margin-bottom': '5px'}
})),
// Datum, Startzeit und Dauer in einer Zeile.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '1'}}, "Datum",
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
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)",
// 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", {
name: "dauer",
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)),
// 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("label", {style: {flex: '1'}}, "Name Ansprechpartner",
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, {
name: "kontaktadresseId",
name: "kontaktadresse_id",
value: -1,
options: kontaktadressenOptionen,
onchanged: (kontaktadresseId: number) => {
const ka = getKontaktadresseById(kontaktadresseId)
const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
if (ka) {
currentLog.nameAnsprechpartner = ka.name
currentLog.name_ansprechpartner = ka.name
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("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")
])
)
+69 -108
View File
@@ -1,48 +1,32 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
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 {iKontaktadresse, getKontaktadressenByProjektId, getNewKontaktadresseId, addKontaktadresse, updateKontaktadresse} from "../models/kontaktadresse"
import {iProjekt, getProjekte, getProjektById} from "../models/projekt"
function createKontaktadresseTreenodes(projektId: number | null = null): iTreenode[] {
let filteredKontaktadressen = getKontaktadressenByProjektId(projektId)
return filteredKontaktadressen.map((kontaktadresse: iKontaktadresse) => {
return new Treenode(kontaktadresse, true, [])
})
}
import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
import {iProjekt, fetchProjekte} from "../models/projekt"
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createKontaktadresseTreenodes()
let treenodes: iTreenode[] = []
let selectedNode: iTreenode | 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) => {
selectedNode = node
}
const addKontaktadresseRoute = () => {
const params: any = {
id: -1,
anrede: "",
name: "",
firma: "",
telefon: "",
email: "",
strasse: "",
plz: "",
ort: "",
land: "",
notiz: ""
}
// Nur bei projektgefilterter Liste das Projekt vorbelegen.
const params: any = {id: -1}
if (filterProjektId !== null) {
const projekt = getProjekte().find((p: iProjekt) => p.id === filterProjektId)
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
if (projekt) {
params.projektId = projekt.id
params.projektname = projekt.projektname
params.projekt_id = projekt.id
params.projekt_name = projekt.projektname
}
}
m.route.set("/kontaktadresse", params)
@@ -50,21 +34,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => {
e.stopPropagation()
m.route.set("/kontaktadresse", {
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
})
m.route.set("/kontaktadresse", {id: kontaktadresse.id})
}
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
@@ -72,21 +42,18 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const now = new Date()
const datum = now.toISOString().split('T')[0]
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", {
id: -1,
kundenName: projekt ? projekt.kunden_name : "",
projektId: kontaktadresse.projektId,
projektName: kontaktadresse.projektName,
projekt_id: kontaktadresse.projekt_id,
projekt_name: kontaktadresse.projekt_name,
datum: datum,
uhrzeitBeginn: uhrzeit,
uhrzeit_beginn: uhrzeit,
dauer: "",
betreff: "",
nameAnsprechpartner: kontaktadresse.name,
name_ansprechpartner: kontaktadresse.name,
telefon: kontaktadresse.telefon,
notiz: "",
wiedervorlageDatum: "",
wiedervorlage_datum: "",
erledigt: false
})
}
@@ -100,7 +67,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
m("span", kontaktadresse.telefon),
m("span", kontaktadresse.email),
m("span", kontaktadresse.ort),
m("span", kontaktadresse.projektName),
m("span", kontaktadresse.projekt_name),
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", {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.email.toLowerCase().includes(searchLower) ||
kontaktadresse.ort.toLowerCase().includes(searchLower) ||
kontaktadresse.projektName.toLowerCase().includes(searchLower)
kontaktadresse.projekt_name.toLowerCase().includes(searchLower)
}
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams && routeParams.projektId) {
filterProjektId = parseInt(routeParams.projektId)
if (routeParams && routeParams.projekt_id) {
filterProjektId = parseInt(routeParams.projekt_id)
projektName = routeParams.projekt_name || ""
} else {
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) => {
return m("div.w3-container.w3-auto", {
@@ -136,7 +122,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
'flex-direction': 'column',
height: 'inherit',
'overflow-y': 'inherit'}},
m("h1", "Kontaktadressen"),
m("h1", projektName ? "Kontaktadressen für " + projektName : "Kontaktadressen"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable,
{
@@ -157,12 +143,12 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
}
export const Kontaktadresse: m.ClosureComponent = (vnode) => {
// currentKontaktadresse hält den Formularzustand. projektId referenziert das
// Projekt eindeutig, projektname dient der Anzeige.
// currentKontaktadresse hält den Formularzustand. projekt_id referenziert
// das Projekt eindeutig, projekt_name dient der Anzeige in der Überschrift.
let currentKontaktadresse: {
id: number
projektId: number
projektname: string
projekt_id: number
projekt_name: string
anrede: string
name: string
firma: string
@@ -175,8 +161,8 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
notiz: string
} = {
id: -1,
projektId: -1,
projektname: "",
projekt_id: -1,
projekt_name: "",
anrede: "",
name: "",
firma: "",
@@ -188,42 +174,29 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
land: "",
notiz: ""
}
// Ist das Projekt voreingestellt (gefilterte Kontaktadressenliste)?
let projektVoreingestellt: boolean = false
return {
oninit: (vnode) => {
const routeParams = m.route.param()
if (routeParams) {
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
projektVoreingestellt = projektId !== -1
currentKontaktadresse = {
id: routeParams.id ? parseInt(routeParams.id) : -1,
projektId: projektId,
projektname: routeParams.projektname || "",
anrede: routeParams.anrede || "",
name: routeParams.name || "",
firma: routeParams.firma || "",
telefon: routeParams.telefon || "",
email: routeParams.email || "",
strasse: routeParams.strasse || "",
plz: routeParams.plz || "",
ort: routeParams.ort || "",
land: routeParams.land || "",
notiz: routeParams.notiz || ""
}
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
if (id !== -1) {
// Kontaktadresse frisch vom Server holen.
fetchKontaktadresseById(id).then((result) => {
currentKontaktadresse = result
})
} else {
// Neuanlegen: projekt_id und projekt_name aus Route-Params.
currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
currentKontaktadresse.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
}
},
view: (vnode) => {
const submit = (form: HTMLFormElement): boolean => {
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 = {
id: currentKontaktadresse.id,
projektId: projektId,
projektName: projekt ? projekt.projektname : "",
projekt_id: currentKontaktadresse.projekt_id,
projekt_name: "",
anrede: (formData.get("anrede") as string) || "",
name: (formData.get("name") as string) || "",
firma: (formData.get("firma") as string) || "",
@@ -236,40 +209,28 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
notiz: (formData.get("notiz") as string) || ""
}
if (currentKontaktadresse.id === -1) {
gespeicherteKontaktadresse.id = getNewKontaktadresseId()
addKontaktadresse(gespeicherteKontaktadresse)
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
} 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
}
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",
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(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("label", "Projekt",
m(Select, {
name: "projektId",
value: currentKontaktadresse.projektId,
options: projektOptionen,
readonly: projektVoreingestellt,
required: true
} as iSelect)),
m("input", {type: "hidden", name: "projekt_id", value: currentKontaktadresse.projekt_id}),
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", "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", "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)),
// PLZ und Ort in einer Zeile.
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
m("label", {style: {flex: '0 0 120px'}}, "PLZ",
m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)),
+9 -9
View File
@@ -40,12 +40,12 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const showCallLogs = (projekt: iProjekt, e: Event) => {
e.stopPropagation()
m.route.set("/calllogliste", {projektId: projekt.id})
m.route.set("/calllogliste", {projekt_id: projekt.id})
}
const showKontaktadressen = (projekt: iProjekt, e: Event) => {
e.stopPropagation()
m.route.set("/kontaktadresseliste", {projektId: projekt.id})
m.route.set("/kontaktadresseliste", {projekt_id: projekt.id})
}
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]
m.route.set("/call_log", {
id: -1,
kundenName: projekt.kunden_name,
projektId: projekt.id,
projektName: projekt.projektname,
kunden_name: projekt.kunden_name,
projekt_id: projekt.id,
projekt_name: projekt.projektname,
datum: datum,
uhrzeitBeginn: uhrzeit,
uhrzeit_beginn: uhrzeit,
dauer: "",
betreff: "",
nameAnsprechpartner: "",
name_ansprechpartner: "",
telefon: "",
notiz: "",
wiedervorlageDatum: "",
wiedervorlage_datum: "",
erledigt: false
})
}
@@ -221,7 +221,7 @@ export const Projekt: m.ClosureComponent = (vnode) => {
: "Projekt-Stammblatt"),
currentProjekt.id !== -1
? 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")
: null
),
+10 -65
View File
@@ -2,86 +2,31 @@ import m from "mithril"
/**
* 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 {
id: number
kundenName: string
projektId: number
projektName: string
projekt_id: number
projekt_name: string
kunden_name: string
datum: string
uhrzeitBeginn: string
uhrzeit_beginn: string
dauer: string
betreff: string
nameAnsprechpartner: string
name_ansprechpartner: string
telefon: string
notiz: string
wiedervorlageDatum: string
wiedervorlage_datum: string
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) */
/* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
/* implementiert und werden später nachgereicht. */
/* serverseitigen REST-Endpunkten (/xhr/calllog/*). */
/* ------------------------------------------------------------------ */
/**
+7 -68
View File
@@ -2,12 +2,15 @@ import m from "mithril"
/**
* 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 {
id: number
projektId: number
projektName: string
projekt_id: number
projekt_name: string
anrede: string
name: string
firma: string
@@ -20,75 +23,11 @@ export interface iKontaktadresse {
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) */
/* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
/* implementiert und werden später nachgereicht. */
/* serverseitigen REST-Endpunkten (/xhr/kontaktadresse/*). */
/* ------------------------------------------------------------------ */
/**
-64
View File
@@ -16,70 +16,6 @@ export interface iProjekt {
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) */
/* ------------------------------------------------------------------ */