diff --git a/AGENTS.md b/AGENTS.md index 22d41e7..549dabe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,6 +49,8 @@ export const Select: m.ClosureComponent = () => { } ``` +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 diff --git a/App_Server/root/cli.py b/App_Server/root/cli.py index d269a11..b768da7 100644 --- a/App_Server/root/cli.py +++ b/App_Server/root/cli.py @@ -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 diff --git a/App_Server/root/crm/model/__init__.py b/App_Server/root/crm/model/__init__.py index 0436fc7..7f8ff8e 100644 --- a/App_Server/root/crm/model/__init__.py +++ b/App_Server/root/crm/model/__init__.py @@ -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") diff --git a/App_Server/root/crm/model/call_log.py b/App_Server/root/crm/model/call_log.py new file mode 100644 index 0000000..088a2b3 --- /dev/null +++ b/App_Server/root/crm/model/call_log.py @@ -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 +: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() diff --git a/App_Server/root/crm/model/checkdb.py b/App_Server/root/crm/model/checkdb.py index c3bf5a9..9f387f3 100644 --- a/App_Server/root/crm/model/checkdb.py +++ b/App_Server/root/crm/model/checkdb.py @@ -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 diff --git a/App_Server/root/crm/model/kontaktadresse.py b/App_Server/root/crm/model/kontaktadresse.py new file mode 100644 index 0000000..0efcfea --- /dev/null +++ b/App_Server/root/crm/model/kontaktadresse.py @@ -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 +: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() diff --git a/App_Server/root/crm/routes.py b/App_Server/root/crm/routes.py index a034ce4..1937dda 100644 --- a/App_Server/root/crm/routes.py +++ b/App_Server/root/crm/routes.py @@ -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") diff --git a/Client/Components/call_log.ts b/Client/Components/call_log.ts index b6dae8b..d475822 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -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") ]) ) diff --git a/Client/Components/kontaktadresse.ts b/Client/Components/kontaktadresse.ts index b786707..91cb80c 100644 --- a/Client/Components/kontaktadresse.ts +++ b/Client/Components/kontaktadresse.ts @@ -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)), diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 925a0a7..229b882 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -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 ), diff --git a/Client/models/call_log.ts b/Client/models/call_log.ts index ef25ae7..75b2c75 100644 --- a/Client/models/call_log.ts +++ b/Client/models/call_log.ts @@ -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/*). */ /* ------------------------------------------------------------------ */ /** diff --git a/Client/models/kontaktadresse.ts b/Client/models/kontaktadresse.ts index 3f2b6b2..587e613 100644 --- a/Client/models/kontaktadresse.ts +++ b/Client/models/kontaktadresse.ts @@ -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/*). */ /* ------------------------------------------------------------------ */ /** diff --git a/Client/models/projekt.ts b/Client/models/projekt.ts index 8153d80..9776345 100644 --- a/Client/models/projekt.ts +++ b/Client/models/projekt.ts @@ -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) */ /* ------------------------------------------------------------------ */