diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6dee676..d49d0a0 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -98,6 +98,24 @@ "showReuseMessage": true, "clear": true } + }, + { + "type": "shell", + "group": "none", + "label": "Flask: init-db", + "command": "${workspaceFolder}/env/bin/flask --app root/wsgi:app init-db", + "options": { + "cwd": "${workspaceFolder}/App_Server" + }, + "problemMatcher": [], + "presentation": { + "echo": true, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": false + } } ] } \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 75e460b..22d41e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,7 +3,7 @@ Beachte die strukturellen Vorgaben, die in ai-context/AI-operative-Vorgaben.md d In den Programmen sollen aussagekräftige Kommentare in deutsch erstellt werden. ## Vorgaben git Versionsverwaltung -Ein neuer Branch darf nur auf ausdrückliche Anweisung hin erstellt werden. Ohne diese Anwendung werden Änderungen immer in main gemacht. +Ein neuer Branch darf nur auf ausdrückliche Anweisung hin erstellt werden. Ohne diese Anweisung werden Änderungen immer in main gemacht. ## Vorgaben für die Typescript und mithriljs Entwicklung Verwende bei der GUI primar die widgets, die es in client/Components/widgets gibt. Fehler widgets, gib eines Hinweis und erstelle einen Vorschlag für ein entsprechendes neues Widget. @@ -28,6 +28,8 @@ m("div.w3-bar-block.w3-border", { Für Symbole auf Buttons und ähnlichen Steuerelementen verwende Symbole aus den Google Material Icons. Erstelle die Datenmodelle für die Typescript-Entwicklung im Verzeichnis Client/Models. Erstelle für die verschiedenen Datenmodelle (Kunde, Projekt usw.) eigene .ts-Dateien. Erstelle in der jeweiligen Datei auch den Aufruf der REST-Api für die Server-Seite. +Beim Aufruf einer Bearbeitungsmaske werden in den Route-Params nur die ID (und ggf. zum Vorbelegen notwendige Eltern-FKs wie `kunden_id`) übergeben — nicht die vollständigen Felder des Objekts. Die Maske lädt die Daten frisch vom Server via `fetchXxxById(id)` im `oninit`. So wird sichergestellt, dass die Daten aktuell sind und zwischenzeitliche Änderungen anderer Nutzer nicht mit veralteten Werten aus der Liste überschrieben werden. Beim Neuanlegen (`id === -1`) entfällt der Fetch; vorbelegte Werte (z.B. `kunden_id`) kommen aus den Route-Params. + Bei `m.ClosureComponent` den Closure-Parameter weglassen (`() =>`) und Attribute ausschließlich über das `vnode` des jeweiligen Lifecycle-Hooks (`view`, `oninit`, `onupdate` etc.) lesen. Das `vnode` der Closure-Funktion ist ein Snapshot zum Erzeugungszeitpunkt und wird bei Redraws nicht aktualisiert — daraus gelesene `attrs` sind veraltet. Initialisierung, die `attrs` braucht, gehört nach `oninit`. Beispiel: ```typescript export const Select: m.ClosureComponent = () => { diff --git a/App_Server/root/cli.py b/App_Server/root/cli.py index 469c240..d269a11 100644 --- a/App_Server/root/cli.py +++ b/App_Server/root/cli.py @@ -34,6 +34,7 @@ def register_cli_commands(app: Flask): from crm.model import checkdb as crm_checkdb crm_checkdb.checkupdates() _seed_muster_kunden() + _seed_muster_projekte() click.echo("Fertig.") @@ -102,6 +103,50 @@ def _seed_muster_kunden(): session.commit() +def _seed_muster_projekte(): + """ + Legt für jeden Muster-Kunden 1 bis 3 Muster-Projekte an, falls die + projekte-Tabelle noch leer ist. + """ + import random + + from crm.model import Kunde, Projekt + + session = dbapp.session + if session.query(Projekt).count() > 0: + return + + kunden = session.query(Kunde).order_by(Kunde.id).all() + vorlagen = [ + ("Website-Relaunch", "Neugestaltung der Unternehmenswebsite"), + ("E-Commerce Plattform", "Online-Shop Entwicklung"), + ("CRM-System", "Kundenbeziehungsmanagement"), + ("Mobile App", "iOS- und Android-Entwicklung"), + ("SEO-Optimierung", "Suchmaschinenoptimierung"), + ] + verantwortliche = [ + "Max Mustermann", "Anna Schmidt", "Peter Müller", + "Lisa Bauer", "Thomas Becker", + ] + rng = random.Random(42) + vorlagen_idx = 0 + for kunde in kunden: + anzahl = rng.randint(1, 3) + for _ in range(anzahl): + name, zweck = vorlagen[vorlagen_idx % len(vorlagen)] + vorlagen_idx += 1 + projekt = Projekt() + projekt.kunden_id = kunde.id + projekt.projektname = name + projekt.zweck = zweck + projekt.verantwortlicher = rng.choice(verantwortliche) + projekt.startdatum = "2024-0{monat}-1{tag}".format( + monat=rng.randint(1, 9), tag=rng.randint(0, 9) + ) + session.add(projekt) + 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 d972eee..0436fc7 100644 --- a/App_Server/root/crm/model/__init__.py +++ b/App_Server/root/crm/model/__init__.py @@ -12,7 +12,8 @@ erfolgt ausschließlich über __bind_key__. import datetime import decimal -from sqlalchemy import Column, DateTime, Integer, String +from sqlalchemy import Column, DateTime, ForeignKey, Integer, String +from sqlalchemy.orm import relationship from db import dbapp @@ -93,4 +94,64 @@ class Kunde(dbapp.Model): return columns2dict(self) +class Kontakt(dbapp.Model): + """ + Ansprechpartner bei einem Kunden. Jeder Kontakt ist genau einem Kunden + zugeordnet (kunden_id). Der Kundenname (kunden_name) ist keine eigene + Spalte, sondern wird über die Relationship zu Kunde abgeleitet und nur + in to_dict() für die JSON-Serialisierung ergänzt. + """ + __tablename__ = 'kontakte' + __bind_key__ = BINDKEY + id = Column(Integer, primary_key=True) + kunden_id = Column(Integer, ForeignKey('kunden.id'), nullable=False) + anrede = Column(String(30), nullable=True) + name = Column(String(200), nullable=False) + position = Column(String(200), nullable=True) + telefon = Column(String(100), nullable=True) + email = Column(String(200), nullable=True) + notiz = Column(String(2000), nullable=True) + + kunde = relationship('Kunde') + + def to_dict(self): + """ + Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden + über columns2dict serialisiert; kunden_name wird zusätzlich aus der + Kunde-Relationship eingefügt. + """ + result = columns2dict(self) + result['kunden_name'] = self.kunde.name if self.kunde else None + return result + + +class Projekt(dbapp.Model): + """ + Projekt eines Kunden. Jedes Projekt ist genau einem Kunden zugeordnet + (kunden_id). Der Kundenname (kunden_name) ist keine eigene Spalte, + sondern wird über die Relationship zu Kunde abgeleitet und nur in + to_dict() für die JSON-Serialisierung ergänzt. + """ + __tablename__ = 'projekte' + __bind_key__ = BINDKEY + id = Column(Integer, primary_key=True) + kunden_id = Column(Integer, ForeignKey('kunden.id'), nullable=False) + projektname = Column(String(200), nullable=False) + zweck = Column(String(2000), nullable=True) + verantwortlicher = Column(String(200), nullable=True) + startdatum = Column(String(20), nullable=True) + + kunde = relationship('Kunde') + + def to_dict(self): + """ + Liefert die Projektdaten als Dictionary. Die Tabellenspalten werden + über columns2dict serialisiert; kunden_name wird zusätzlich aus der + Kunde-Relationship eingefügt. + """ + result = columns2dict(self) + result['kunden_name'] = self.kunde.name if self.kunde else None + return result + + print("########## crm/model importiert") diff --git a/App_Server/root/crm/model/checkdb.py b/App_Server/root/crm/model/checkdb.py index 0cd8c01..c3bf5a9 100644 --- a/App_Server/root/crm/model/checkdb.py +++ b/App_Server/root/crm/model/checkdb.py @@ -43,6 +43,30 @@ def _db_V01_00(): session.commit() +def _db_V01_01(): + """ + Legt die Tabelle 'kontakte' an. Bei bestehenden Datenbanken, die mit + Version 01.00 initialisiert wurden, fehlt diese Tabelle, da das + Kontakt-Modell erst nachträglich ergänzt wurde. create_all erzeugt + nur fehlende Tabellen, bestehende bleiben unangetastet. + """ + dbapp.create_all(bind_key=BINDKEY) + _updatesysteminfo('01.01') + session.commit() + + +def _db_V01_02(): + """ + Legt die Tabelle 'projekte' an. Bei bestehenden Datenbanken, die mit + Version 01.01 initialisiert wurden, fehlt diese Tabelle, da das + Projekt-Modell erst nachträglich ergänzt wurde. create_all erzeugt + nur fehlende Tabellen, bestehende bleiben unangetastet. + """ + dbapp.create_all(bind_key=BINDKEY) + _updatesysteminfo('01.02') + 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/kontakt.py b/App_Server/root/crm/model/kontakt.py new file mode 100644 index 0000000..f80665c --- /dev/null +++ b/App_Server/root/crm/model/kontakt.py @@ -0,0 +1,120 @@ +""" +Geschäftslogik für die Entität Kontakt. + +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-03 +:version: 1.0 +""" +from db import dbapp +from crm.model import Kontakt, Kunde + +from auth.model.errors import ApplicationError, HtmlStatusCode + +session = dbapp.session + + +def get_kontakte(): + """ + Liefert alle Kontakte, sortiert nach Name. + """ + return Kontakt.query.order_by(Kontakt.name).all() + + +def get_kontakt(kontakt_id: int) -> Kontakt: + """ + Liefert einen einzelnen Kontakt anhand seiner ID. + Wirft ApplicationError (404), falls der Kontakt nicht existiert. + """ + kontakt = Kontakt.query.get(kontakt_id) + if kontakt is None: + raise ApplicationError( + "Kontakt mit der ID {} wurde nicht gefunden.".format(kontakt_id), + htmlstatus=HtmlStatusCode.Not_Found, + ) + return kontakt + + +def create_kontakt(data: dict) -> Kontakt: + """ + Legt einen neuen Kontakt an. Die vom Client übergebene ID wird ignoriert; + die ID wird serverseitig per Auto-Increment vergeben. + + - data: dict mit den Feldern kunden_id, anrede, name, position, telefon, + email, notiz + """ + kunden_id = data.get("kunden_id") + if kunden_id is None: + raise ApplicationError( + "Es muss ein Kunde ausgewählt werden.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde = Kunde.query.get(int(kunden_id)) + if kunde is None: + raise ApplicationError( + "Der ausgewählte Kunde existiert nicht.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + if not data.get("name") or not str(data["name"]).strip(): + raise ApplicationError( + "Der Kontaktname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kontakt = Kontakt() + kontakt.kunden_id = int(kunden_id) + kontakt.anrede = (data.get("anrede") or "").strip() or None + kontakt.name = data["name"].strip() + kontakt.position = (data.get("position") or "").strip() or None + kontakt.telefon = (data.get("telefon") or "").strip() or None + kontakt.email = (data.get("email") or "").strip() or None + kontakt.notiz = (data.get("notiz") or "").strip() or None + session.add(kontakt) + session.commit() + return kontakt + + +def update_kontakt(data: dict) -> Kontakt: + """ + Aktualisiert einen bestehenden Kontakt. Die ID im data-Dict bestimmt den + zu ändernden Datensatz. + """ + kontakt = get_kontakt(data["id"]) + kunden_id = data.get("kunden_id") + if kunden_id is None: + raise ApplicationError( + "Es muss ein Kunde ausgewählt werden.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde = Kunde.query.get(int(kunden_id)) + if kunde is None: + raise ApplicationError( + "Der ausgewählte Kunde existiert nicht.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + if not data.get("name") or not str(data["name"]).strip(): + raise ApplicationError( + "Der Kontaktname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kontakt.kunden_id = int(kunden_id) + kontakt.anrede = (data.get("anrede") or "").strip() or None + kontakt.name = data["name"].strip() + kontakt.position = (data.get("position") or "").strip() or None + kontakt.telefon = (data.get("telefon") or "").strip() or None + kontakt.email = (data.get("email") or "").strip() or None + kontakt.notiz = (data.get("notiz") or "").strip() or None + session.commit() + return kontakt + + +def delete_kontakt(kontakt_id: int): + """ + Löscht einen Kontakt anhand seiner ID. + Wirft ApplicationError (404), falls der Kontakt nicht existiert. + """ + kontakt = get_kontakt(kontakt_id) + session.delete(kontakt) + session.commit() diff --git a/App_Server/root/crm/model/projekt.py b/App_Server/root/crm/model/projekt.py new file mode 100644 index 0000000..c6942b5 --- /dev/null +++ b/App_Server/root/crm/model/projekt.py @@ -0,0 +1,116 @@ +""" +Geschäftslogik für die Entität Projekt. + +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 Projekt, Kunde + +from auth.model.errors import ApplicationError, HtmlStatusCode + +session = dbapp.session + + +def get_projekte(): + """ + Liefert alle Projekte, sortiert nach projektname. + """ + return Projekt.query.order_by(Projekt.projektname).all() + + +def get_projekt(projekt_id: int) -> Projekt: + """ + Liefert ein einzelnes Projekt anhand seiner ID. + Wirft ApplicationError (404), falls das Projekt nicht existiert. + """ + projekt = Projekt.query.get(projekt_id) + if projekt is None: + raise ApplicationError( + "Projekt mit der ID {} wurde nicht gefunden.".format(projekt_id), + htmlstatus=HtmlStatusCode.Not_Found, + ) + return projekt + + +def create_projekt(data: dict) -> Projekt: + """ + Legt ein neues Projekt an. Die vom Client übergebene ID wird ignoriert; + die ID wird serverseitig per Auto-Increment vergeben. + + - data: dict mit den Feldern kunden_id, projektname, zweck, + verantwortlicher, startdatum + """ + kunden_id = data.get("kunden_id") + if kunden_id is None: + raise ApplicationError( + "Es muss ein Kunde ausgewählt werden.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde = Kunde.query.get(int(kunden_id)) + if kunde is None: + raise ApplicationError( + "Der ausgewählte Kunde existiert nicht.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + if not data.get("projektname") or not str(data["projektname"]).strip(): + raise ApplicationError( + "Der Projektname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + projekt = Projekt() + projekt.kunden_id = int(kunden_id) + projekt.projektname = data["projektname"].strip() + projekt.zweck = (data.get("zweck") or "").strip() or None + projekt.verantwortlicher = (data.get("verantwortlicher") or "").strip() or None + projekt.startdatum = (data.get("startdatum") or "").strip() or None + session.add(projekt) + session.commit() + return projekt + + +def update_projekt(data: dict) -> Projekt: + """ + Aktualisiert ein bestehendes Projekt. Die ID im data-Dict bestimmt den + zu ändernden Datensatz. + """ + projekt = get_projekt(data["id"]) + kunden_id = data.get("kunden_id") + if kunden_id is None: + raise ApplicationError( + "Es muss ein Kunde ausgewählt werden.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + kunde = Kunde.query.get(int(kunden_id)) + if kunde is None: + raise ApplicationError( + "Der ausgewählte Kunde existiert nicht.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + if not data.get("projektname") or not str(data["projektname"]).strip(): + raise ApplicationError( + "Der Projektname darf nicht leer sein.", + htmlstatus=HtmlStatusCode.Bad_Request, + ) + projekt.kunden_id = int(kunden_id) + projekt.projektname = data["projektname"].strip() + projekt.zweck = (data.get("zweck") or "").strip() or None + projekt.verantwortlicher = (data.get("verantwortlicher") or "").strip() or None + projekt.startdatum = (data.get("startdatum") or "").strip() or None + session.commit() + return projekt + + +def delete_projekt(projekt_id: int): + """ + Löscht ein Projekt anhand seiner ID. + Wirft ApplicationError (404), falls das Projekt nicht existiert. + """ + projekt = get_projekt(projekt_id) + session.delete(projekt) + session.commit() diff --git a/App_Server/root/crm/routes.py b/App_Server/root/crm/routes.py index 1873879..a034ce4 100644 --- a/App_Server/root/crm/routes.py +++ b/App_Server/root/crm/routes.py @@ -19,6 +19,8 @@ from flask import jsonify, request from auth.decorators import authorisation_required 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 print("crm-routes") @@ -68,4 +70,94 @@ def kunde_delete(): return jsonify({}), 200 +# --------------------------------------------------------------------------- # +# Kontakt # +# --------------------------------------------------------------------------- # + + +@app.get("/xhr/kontakt/list") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def kontakt_list(): + """Liefert alle Kontakte als Liste.""" + result = [k.to_dict() for k in kontakte.get_kontakte()] + return jsonify(result), 200 + + +@app.get("/xhr/kontakt/get") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def kontakt_get(): + """Liefert einen einzelnen Kontakt anhand seiner ID (Query-Param id).""" + kontakt = kontakte.get_kontakt(int(request.args.get("id"))) + return jsonify(kontakt.to_dict()), 200 + + +@app.post("/xhr/kontakt/create") +@authorisation_required([KUNDEN_MANAGER]) +def kontakt_create(): + """Legt einen neuen Kontakt an. ID wird serverseitig vergeben.""" + kontakt = kontakte.create_kontakt(request.json) + return jsonify(kontakt.to_dict()), 200 + + +@app.put("/xhr/kontakt/update") +@authorisation_required([KUNDEN_MANAGER]) +def kontakt_update(): + """Aktualisiert einen bestehenden Kontakt.""" + kontakt = kontakte.update_kontakt(request.json) + return jsonify(kontakt.to_dict()), 200 + + +@app.delete("/xhr/kontakt/delete") +@authorisation_required([KUNDEN_MANAGER]) +def kontakt_delete(): + """Löscht einen Kontakt anhand seiner ID (Query-Param id).""" + kontakte.delete_kontakt(int(request.args.get("id"))) + return jsonify({}), 200 + + +# --------------------------------------------------------------------------- # +# Projekt # +# --------------------------------------------------------------------------- # + + +@app.get("/xhr/projekt/list") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def projekt_list(): + """Liefert alle Projekte als Liste.""" + result = [p.to_dict() for p in projekte.get_projekte()] + return jsonify(result), 200 + + +@app.get("/xhr/projekt/get") +@authorisation_required([MITARBEITER, KUNDEN_MANAGER]) +def projekt_get(): + """Liefert ein einzelnes Projekt anhand seiner ID (Query-Param id).""" + projekt = projekte.get_projekt(int(request.args.get("id"))) + return jsonify(projekt.to_dict()), 200 + + +@app.post("/xhr/projekt/create") +@authorisation_required([KUNDEN_MANAGER]) +def projekt_create(): + """Legt ein neues Projekt an. ID wird serverseitig vergeben.""" + projekt = projekte.create_projekt(request.json) + return jsonify(projekt.to_dict()), 200 + + +@app.put("/xhr/projekt/update") +@authorisation_required([KUNDEN_MANAGER]) +def projekt_update(): + """Aktualisiert ein bestehendes Projekt.""" + projekt = projekte.update_projekt(request.json) + return jsonify(projekt.to_dict()), 200 + + +@app.delete("/xhr/projekt/delete") +@authorisation_required([KUNDEN_MANAGER]) +def projekt_delete(): + """Löscht ein Projekt anhand seiner ID (Query-Param id).""" + projekte.delete_projekt(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 d311d95..b6dae8b 100644 --- a/Client/Components/call_log.ts +++ b/Client/Components/call_log.ts @@ -49,7 +49,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => { if (filterProjektId !== null) { const projekt = getProjektById(filterProjektId) if (projekt) { - params.kundenName = projekt.kundenName + params.kundenName = projekt.kunden_name params.projektId = projekt.id params.projektName = projekt.projektname } @@ -212,7 +212,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => { let kundenId = -1 if (kundeVoreingestellt) { const projekt = getProjektById(projektId) - if (projekt) { kundenId = projekt.kundenId } + if (projekt) { kundenId = projekt.kunden_id } } currentLog = { id: routeParams.id ? parseInt(routeParams.id) : -1, diff --git a/Client/Components/kontakt.ts b/Client/Components/kontakt.ts index dd6ab9e..82bff24 100644 --- a/Client/Components/kontakt.ts +++ b/Client/Components/kontakt.ts @@ -1,23 +1,17 @@ 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 {iKontakt, getKontakteByKundenId, getNewKontaktId, addKontakt, updateKontakt} from "../models/kontakt" +import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt" import {iKunde, fetchKunden} from "../models/kunde" -function createKontaktTreenodes(kundenId: number | null = null): iTreenode[] { - let filteredKontakte = getKontakteByKundenId(kundenId) - - return filteredKontakte.map((kontakt: iKontakt) => { - return new Treenode(kontakt, true, []) - }) -} - export const kontaktliste: m.ClosureComponent = (vnode) => { - let treenodes: iTreenode[] = createKontaktTreenodes() + let treenodes: iTreenode[] = [] let selectedNode: iTreenode | null = null let filterKundenId: number | null = null + // Kundenname für die Überschrift, asynchron aus der Kundenliste + // aufgelöst. + let kundenName: string = "" // Kundenliste asynchron vom Server laden, für die Kundenauflösung // beim Anlegen eines neuen Kontakts. let kundenListe: iKunde[] = [] @@ -27,21 +21,13 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { } const addKontaktRoute = () => { - const params: any = { - id: -1, - anrede: "", - name: "", - position: "", - telefon: "", - email: "", - notiz: "" - } + const params: any = {id: -1} // Nur bei kundengefilterter Liste den Kunden vorbelegen. if (filterKundenId !== null) { const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) if (kunde) { - params.kundenId = kunde.id - params.kundenname = kunde.name + params.kunden_id = kunde.id + params.kunden_name = kunde.name } } m.route.set("/kontakt", params) @@ -49,17 +35,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { const editKontakt = (kontakt: iKontakt, e: Event) => { e.stopPropagation() - m.route.set("/kontakt", { - id: kontakt.id, - kundenId: kontakt.kundenId, - kundenname: kontakt.kundenName, - anrede: kontakt.anrede, - name: kontakt.name, - position: kontakt.position, - telefon: kontakt.telefon, - email: kontakt.email, - notiz: kontakt.notiz - }) + m.route.set("/kontakt", {id: kontakt.id}) } const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { @@ -70,7 +46,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { m("span", kontakt.position), m("span", kontakt.telefon), m("span", kontakt.email), - m("span", kontakt.kundenName), + m("span", kontakt.kunden_name), m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontakt(kontakt, e)}, m("i.material-icons", "edit")) ] } @@ -82,21 +58,39 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { kontakt.position.toLowerCase().includes(searchLower) || kontakt.telefon.toLowerCase().includes(searchLower) || kontakt.email.toLowerCase().includes(searchLower) || - kontakt.kundenName.toLowerCase().includes(searchLower) + kontakt.kunden_name.toLowerCase().includes(searchLower) } return { oninit: (vnode) => { const routeParams = m.route.param() - if (routeParams && routeParams.kundenId) { - filterKundenId = parseInt(routeParams.kundenId) + if (routeParams && routeParams.kunden_id) { + filterKundenId = parseInt(routeParams.kunden_id) + kundenName = routeParams.kunden_name || "" } else { filterKundenId = null + kundenName = "" } - treenodes = createKontaktTreenodes(filterKundenId) + // Kontakte asynchron vom Server laden. m.request löst nach + // Promise-Auflösung automatisch ein Redraw aus. + fetchKontakte().then((result) => { + let filtered = result + if (filterKundenId !== null) { + filtered = result.filter((k: iKontakt) => k.kunden_id === filterKundenId) + } + treenodes = filtered.map((kontakt: iKontakt) => new Treenode(kontakt, true, [])) + }) // Kundenliste asynchron vom Server laden. fetchKunden().then((result) => { kundenListe = result + // Fallback: Falls kunden_name nicht in den Route-Params + // enthalten war, aus der Kundenliste auflösen. + if (filterKundenId !== null && kundenName === "") { + const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) + if (kunde) { + kundenName = kunde.name + } + } }) }, view: (vnode) => { @@ -106,7 +100,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { 'flex-direction': 'column', height: 'inherit', 'overflow-y': 'inherit'}}, - m("h1", "Kontaktliste"), + m("h1", kundenName ? "Kontaktliste für " + kundenName : "Kontaktliste"), m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m(Treetable, { @@ -127,12 +121,12 @@ export const kontaktliste: m.ClosureComponent = (vnode) => { } export const Kontakt: m.ClosureComponent = (vnode) => { - // currentKontakt hält den Formularzustand. kundenId referenziert den - // Kunden eindeutig, kundenname dient der Anzeige. + // currentKontakt hält den Formularzustand. kunden_id referenziert den + // Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift. let currentKontakt: { id: number - kundenId: number - kundenname: string + kunden_id: number + kunden_name: string anrede: string name: string position: string @@ -141,8 +135,8 @@ export const Kontakt: m.ClosureComponent = (vnode) => { notiz: string } = { id: -1, - kundenId: -1, - kundenname: "", + kunden_id: -1, + kunden_name: "", anrede: "", name: "", position: "", @@ -150,44 +144,31 @@ export const Kontakt: m.ClosureComponent = (vnode) => { email: "", notiz: "" } - // Ist der Kunde voreingestellt (gefilterte Kontaktliste)? - let kundeVoreingestellt: boolean = false - // Kundenliste asynchron vom Server laden, für Select-Optionen und - // Namensauflösung im submit-Handler. - let kundenListe: iKunde[] = [] return { oninit: (vnode) => { const routeParams = m.route.param() - if (routeParams) { - const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1 - kundeVoreingestellt = kundenId !== -1 - currentKontakt = { - id: routeParams.id ? parseInt(routeParams.id) : -1, - kundenId: kundenId, - kundenname: routeParams.kundenname || "", - anrede: routeParams.anrede || "", - name: routeParams.name || "", - position: routeParams.position || "", - telefon: routeParams.telefon || "", - email: routeParams.email || "", - notiz: routeParams.notiz || "" - } + const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1 + if (id !== -1) { + // Kontaktdaten frisch vom Server holen, um zwischenzeitliche + // Änderungen anderer Nutzer nicht zu überschreiben. + fetchKontaktById(id).then((result) => { + currentKontakt = result + }) + } else { + // Neuanlegen: kunden_id und kunden_name aus Route-Params + // (voreingestellt aus der Kontaktliste). + currentKontakt.kunden_id = routeParams && routeParams.kunden_id ? parseInt(routeParams.kunden_id) : -1 + currentKontakt.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : "" } - fetchKunden().then((result) => { - kundenListe = result - }) }, view: (vnode) => { const submit = (form: HTMLFormElement): boolean => { const formData = new FormData(form) - const kundenId = parseInt(formData.get("kundenId") as string) - // Kundennamen zur gewählten kundenId auflösen. - const kunde = kundenListe.find((k: iKunde) => k.id === kundenId) const gespeicherterKontakt: iKontakt = { id: currentKontakt.id, - kundenId: kundenId, - kundenName: kunde ? kunde.name : "", + kunden_id: currentKontakt.kunden_id, + kunden_name: "", anrede: (formData.get("anrede") as string) || "", name: (formData.get("name") as string) || "", position: (formData.get("position") as string) || "", @@ -196,33 +177,22 @@ export const Kontakt: m.ClosureComponent = (vnode) => { notiz: (formData.get("notiz") as string) || "" } if (currentKontakt.id === -1) { - gespeicherterKontakt.id = getNewKontaktId() - addKontakt(gespeicherterKontakt) + createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})) } else { - updateKontakt(gespeicherterKontakt) + updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})) } - m.route.set("/kontaktliste") return true } const cancel = (e: Event) => { - m.route.set("/kontaktliste") + m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}) } - // Select-Optionen aus der Kundenliste aufbauen. - const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { - return {value: k.id, label: k.name} - }) return m("div.w3-container.w3-auto.form-view-container", - m("h1", currentKontakt.id === -1 ? "Neuen Kontakt anlegen" : "Kontakt-Stammblatt"), + m("h1", currentKontakt.id === -1 + ? "Neuer Kontakt für " + currentKontakt.kunden_name + : "Kontakt-Stammblatt"), m("div", {style: {flex: '1', 'min-height': '0'}}, m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ - m("label", "Kunde", - m(Select, { - name: "kundenId", - value: currentKontakt.kundenId, - options: kundenOptionen, - readonly: kundeVoreingestellt, - required: true - } as iSelect)), + m("input", {type: "hidden", name: "kunden_id", value: currentKontakt.kunden_id}), m("label", "Anrede", m(Input, {name: "anrede", value: currentKontakt.anrede, type: "text"} as iInput)), m("label", "Name", m(Input, {name: "name", value: currentKontakt.name, type: "text", required: true} as iInput)), m("label", "Position", m(Input, {name: "position", value: currentKontakt.position, type: "text"} as iInput)), diff --git a/Client/Components/kontaktadresse.ts b/Client/Components/kontaktadresse.ts index 3d974ba..b786707 100644 --- a/Client/Components/kontaktadresse.ts +++ b/Client/Components/kontaktadresse.ts @@ -76,7 +76,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => { const projekt = getProjektById(kontaktadresse.projektId) m.route.set("/call_log", { id: -1, - kundenName: projekt ? projekt.kundenName : "", + kundenName: projekt ? projekt.kunden_name : "", projektId: kontaktadresse.projektId, projektName: kontaktadresse.projektName, datum: datum, diff --git a/Client/Components/kunde.ts b/Client/Components/kunde.ts index 1babe51..d01273f 100644 --- a/Client/Components/kunde.ts +++ b/Client/Components/kunde.ts @@ -2,7 +2,7 @@ import m from "mithril" import {Form, iForm} from "./widgets/form" import { Input, iInput} from "./widgets/inputs" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" -import {iKunde, fetchKunden, createKundeREST, updateKundeREST} from "../models/kunde" +import {iKunde, fetchKunden, fetchKundeById, createKundeREST, updateKundeREST} from "../models/kunde" export const kunde: m.ClosureComponent = (vnode) => { let currentKunde: iKunde = { @@ -17,15 +17,13 @@ export const kunde: m.ClosureComponent = (vnode) => { return { oninit: (vnode) => { const routeParams = m.route.param() - if (routeParams) { - currentKunde = { - id: routeParams.id ? parseInt(routeParams.id) : -1, - name: routeParams.name || "", - strasse: routeParams.strasse || "", - plz: routeParams.plz || "", - ort: routeParams.ort || "", - land: routeParams.land || "" - } + const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1 + if (id !== -1) { + // Kundendaten frisch vom Server holen, um zwischenzeitliche + // Änderungen anderer Nutzer nicht zu überschreiben. + fetchKundeById(id).then((result) => { + currentKunde = result + }) } }, view: (vnode) => { @@ -54,7 +52,7 @@ export const kunde: m.ClosureComponent = (vnode) => { m("h1", currentKunde.id === -1 ? "Neuen Kunden anlegen" : "Kundenstammblatt"), currentKunde.id !== -1 ? m("button.w3-button.w3-theme", - {onclick: () => m.route.set("/kontaktliste", {kundenId: currentKunde.id})}, + {onclick: () => m.route.set("/kontaktliste", {kunden_id: currentKunde.id})}, m("i.material-icons", "contact_page"), " Kontakte") : null ), @@ -82,21 +80,21 @@ export const kundenliste: m.ClosureComponent = (vnode) => { const editKunde = (kunde: iKunde, e: Event) => { e.stopPropagation() - m.route.set("/kunde", {id: kunde.id, name: kunde.name, strasse: kunde.strasse, plz: kunde.plz, ort: kunde.ort, land: kunde.land}) + m.route.set("/kunde", {id: kunde.id}) } const showProjekte = (kunde: iKunde, e: Event) => { e.stopPropagation() - m.route.set("/projektliste", {kundenId: kunde.id}) + m.route.set("/projektliste", {kunden_id: kunde.id}) } const showKontakte = (kunde: iKunde, e: Event) => { e.stopPropagation() - m.route.set("/kontaktliste", {kundenId: kunde.id}) + m.route.set("/kontaktliste", {kunden_id: kunde.id}) } const addKundeRoute = () => { - m.route.set("/kunde", {id: -1, name: "", strasse: "", plz: "", ort: "", land: ""}) + m.route.set("/kunde", {id: -1}) } const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { diff --git a/Client/Components/projekt.ts b/Client/Components/projekt.ts index 93aaf84..925a0a7 100644 --- a/Client/Components/projekt.ts +++ b/Client/Components/projekt.ts @@ -1,23 +1,17 @@ import m from "mithril" import {Form, iForm} from "./widgets/form" import { Input, iInput} from "./widgets/inputs" -import {Select, iSelect, iSelectOption} from "./widgets/select" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" -import {iProjekt, getProjekteByKundenId, getNewProjektId, addProjekt, updateProjekt} from "../models/projekt" +import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt" import {iKunde, fetchKunden} from "../models/kunde" -function createProjektTreenodes(kundenId: number | null = null): iTreenode[] { - let filteredProjekte = getProjekteByKundenId(kundenId) - - return filteredProjekte.map((projekt: iProjekt) => { - return new Treenode(projekt, true, []) - }) -} - export const projektliste: m.ClosureComponent = (vnode) => { - let treenodes: iTreenode[] = createProjektTreenodes() + let treenodes: iTreenode[] = [] let selectedNode: iTreenode | null = null let filterKundenId: number | null = null + // Kundenname für die Überschrift, asynchron aus der Kundenliste + // aufgelöst. + let kundenName: string = "" // Kundenliste asynchron vom Server laden, für die Kundenauflösung // beim Anlegen eines neuen Projekts. let kundenListe: iKunde[] = [] @@ -27,38 +21,23 @@ export const projektliste: m.ClosureComponent = (vnode) => { } const addProjektRoute = () => { - const params: any = { - id: -1, - kundenname: "", - projektname: "", - zweck: "", - verantwortlicher: "", - startdatum: "" - } + const params: any = {id: -1} // Nur bei kundengefilterter Liste den Kunden vorbelegen. if (filterKundenId !== null) { const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) if (kunde) { - params.kundenId = kunde.id - params.kundenname = kunde.name + params.kunden_id = kunde.id + params.kunden_name = kunde.name } } m.route.set("/projekt", params) } - + const editProjekt = (projekt: iProjekt, e: Event) => { e.stopPropagation() - m.route.set("/projekt", { - id: projekt.id, - kundenId: projekt.kundenId, - kundenname: projekt.kundenName, - projektname: projekt.projektname, - zweck: projekt.zweck, - verantwortlicher: projekt.verantwortlicher, - startdatum: projekt.startdatum - }) + m.route.set("/projekt", {id: projekt.id}) } - + const showCallLogs = (projekt: iProjekt, e: Event) => { e.stopPropagation() m.route.set("/calllogliste", {projektId: projekt.id}) @@ -68,7 +47,7 @@ export const projektliste: m.ClosureComponent = (vnode) => { e.stopPropagation() m.route.set("/kontaktadresseliste", {projektId: projekt.id}) } - + const addCallLog = (projekt: iProjekt, e: Event) => { e.stopPropagation() const now = new Date() @@ -76,7 +55,7 @@ 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.kundenName, + kundenName: projekt.kunden_name, projektId: projekt.id, projektName: projekt.projektname, datum: datum, @@ -90,12 +69,12 @@ export const projektliste: m.ClosureComponent = (vnode) => { erledigt: false }) } - + const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { let projekt: iProjekt = node.nodevalue return [ - m("div", m("strong", projekt.kundenName)), + m("div", m("strong", projekt.kunden_name)), m("span", projekt.projektname), m("span", projekt.zweck), m("span", projekt.verantwortlicher), @@ -108,39 +87,54 @@ export const projektliste: m.ClosureComponent = (vnode) => { ) ] } - + const filterFunction = (node: iTreenode, searchvalue: string): boolean => { let projekt: iProjekt = node.nodevalue let searchLower = searchvalue.toLowerCase() - return projekt.kundenName.toLowerCase().includes(searchLower) || + return projekt.kunden_name.toLowerCase().includes(searchLower) || projekt.projektname.toLowerCase().includes(searchLower) || projekt.zweck.toLowerCase().includes(searchLower) || projekt.verantwortlicher.toLowerCase().includes(searchLower) || projekt.startdatum.includes(searchLower) } - + return { oninit: (vnode) => { const routeParams = m.route.param() - if (routeParams && routeParams.kundenId) { - filterKundenId = parseInt(routeParams.kundenId) + if (routeParams && routeParams.kunden_id) { + filterKundenId = parseInt(routeParams.kunden_id) + kundenName = routeParams.kunden_name || "" } else { filterKundenId = null + kundenName = "" } - treenodes = createProjektTreenodes(filterKundenId) + // Projekte asynchron vom Server laden. + fetchProjekte().then((result) => { + let filtered = result + if (filterKundenId !== null) { + filtered = result.filter((p: iProjekt) => p.kunden_id === filterKundenId) + } + treenodes = filtered.map((projekt: iProjekt) => new Treenode(projekt, true, [])) + }) // Kundenliste asynchron vom Server laden. fetchKunden().then((result) => { kundenListe = result + if (filterKundenId !== null && kundenName === "") { + const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) + if (kunde) { + kundenName = kunde.name + } + } }) }, 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", "Projektliste"), + m("h1", kundenName ? "Projektliste für " + kundenName : "Projektliste"), m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m(Treetable, { @@ -161,85 +155,70 @@ export const projektliste: m.ClosureComponent = (vnode) => { } export const Projekt: m.ClosureComponent = (vnode) => { - // currentProjekt hält den Formularzustand. kundenId referenziert den - // Kunden eindeutig, kundenname dient der Anzeige. + // currentProjekt hält den Formularzustand. kunden_id referenziert den + // Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift. let currentProjekt: { id: number - kundenId: number - kundenname: string + kunden_id: number + kunden_name: string projektname: string zweck: string verantwortlicher: string startdatum: string } = { id: -1, - kundenId: -1, - kundenname: "", + kunden_id: -1, + kunden_name: "", projektname: "", zweck: "", verantwortlicher: "", startdatum: "" } - // Ist der Kunde voreingestellt (gefilterte Projektliste)? - let kundeVoreingestellt: boolean = false - // Kundenliste asynchron vom Server laden, für Select-Optionen und - // Namensauflösung im submit-Handler. - let kundenListe: iKunde[] = [] return { oninit: (vnode) => { const routeParams = m.route.param() - if (routeParams) { - const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1 - kundeVoreingestellt = kundenId !== -1 - currentProjekt = { - id: routeParams.id ? parseInt(routeParams.id) : -1, - kundenId: kundenId, - kundenname: routeParams.kundenname || "", - projektname: routeParams.projektname || "", - zweck: routeParams.zweck || "", - verantwortlicher: routeParams.verantwortlicher || "", - startdatum: routeParams.startdatum || "" - } + const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1 + if (id !== -1) { + // Projektdaten frisch vom Server holen, um zwischenzeitliche + // Änderungen anderer Nutzer nicht zu überschreiben. + fetchProjektById(id).then((result) => { + currentProjekt = result + }) + } else { + // Neuanlegen: kunden_id und kunden_name aus Route-Params + // (voreingestellt aus der Projektliste). + currentProjekt.kunden_id = routeParams && routeParams.kunden_id ? parseInt(routeParams.kunden_id) : -1 + currentProjekt.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : "" } - fetchKunden().then((result) => { - kundenListe = result - }) }, view: (vnode) => { const submit = (form: HTMLFormElement): boolean => { const formData = new FormData(form) - const kundenId = parseInt(formData.get("kundenId") as string) - // Kundennamen zur gewählten kundenId auflösen. - const kunde = kundenListe.find((k: iKunde) => k.id === kundenId) const gespeichertesProjekt: iProjekt = { id: currentProjekt.id, - kundenId: kundenId, - kundenName: kunde ? kunde.name : "", + kunden_id: currentProjekt.kunden_id, + kunden_name: "", projektname: (formData.get("projektname") as string) || "", zweck: (formData.get("zweck") as string) || "", verantwortlicher: (formData.get("verantwortlicher") as string) || "", startdatum: (formData.get("startdatum") as string) || "" } if (currentProjekt.id === -1) { - gespeichertesProjekt.id = getNewProjektId() - addProjekt(gespeichertesProjekt) + createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})) } else { - updateProjekt(gespeichertesProjekt) + updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})) } - m.route.set("/projektliste") return true } const cancel = (e: Event) => { - m.route.set("/projektliste") + m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}) } - // Select-Optionen aus der Kundenliste aufbauen. - const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => { - return {value: k.id, label: k.name} - }) return m("div.w3-container.w3-auto.form-view-container", m("div", {style: {display: 'flex', 'align-items': 'center', 'justify-content': 'space-between'}}, - m("h1", currentProjekt.id === -1 ? "Neues Projekt anlegen" : "Projekt-Stammblatt"), + m("h1", currentProjekt.id === -1 + ? "Neues Projekt für " + currentProjekt.kunden_name + : "Projekt-Stammblatt"), currentProjekt.id !== -1 ? m("button.w3-button.w3-theme", {onclick: () => m.route.set("/kontaktadresseliste", {projektId: currentProjekt.id})}, @@ -248,14 +227,7 @@ export const Projekt: m.ClosureComponent = (vnode) => { ), m("div", {style: {flex: '1', 'min-height': '0'}}, m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ - m("label", "Kunde", - m(Select, { - name: "kundenId", - value: currentProjekt.kundenId, - options: kundenOptionen, - readonly: kundeVoreingestellt, - required: true - } as iSelect)), + m("input", {type: "hidden", name: "kunden_id", value: currentProjekt.kunden_id}), m("label", "Projektname", m(Input, {name: "projektname", value: currentProjekt.projektname, type: "text", required: true} as iInput)), m("label", "Zweck", m(Input, {name: "zweck", value: currentProjekt.zweck, type: "text"} as iInput)), m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: currentProjekt.verantwortlicher, type: "text"} as iInput)), diff --git a/Client/models/kontakt.ts b/Client/models/kontakt.ts index a198e6a..5f2bd58 100644 --- a/Client/models/kontakt.ts +++ b/Client/models/kontakt.ts @@ -2,12 +2,15 @@ import m from "mithril" /** * Datenmodell für einen Kontakt (Ansprechpartner beim Kunden). - * Jeder Kontakt ist genau einem Kunden zugeordnet (FK kundenId). + * Jeder Kontakt ist genau einem Kunden zugeordnet (FK kunden_id). + * Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case). + * kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der + * Kunde-Relationship abgeleitet und im JSON ergänzt. */ export interface iKontakt { id: number - kundenId: number - kundenName: string + kunden_id: number + kunden_name: string anrede: string name: string position: string @@ -16,75 +19,11 @@ export interface iKontakt { notiz: string } -/** - * In-Memory-Datenspeicher für Kontakte. Wird als Fallback solange verwendet, - * bis die serverseitigen REST-Endpunkte verfügbar sind. - */ -export const kontaktDaten: iKontakt[] = [ - {id: 1, kundenId: 1, kundenName: "Musterfirma GmbH", anrede: "Herr", name: "Max Mustermann", position: "Geschäftsführer", telefon: "+49 123 45678", email: "m.mustermann@musterfirma.de", notiz: "Erster Ansprechpartner für das Website-Projekt."}, - {id: 2, kundenId: 1, kundenName: "Musterfirma GmbH", anrede: "Frau", name: "Anna Schmidt", position: "Marketingleiterin", telefon: "+49 123 45679", email: "a.schmidt@musterfirma.de", notiz: "Zuständig für Design-Fragen."}, - {id: 3, kundenId: 2, kundenName: "Beispiel AG", anrede: "Herr", name: "Peter Müller", position: "IT-Leiter", telefon: "+49 543 21012", email: "p.mueller@beispiel-ag.de", notiz: "Technischer Ansprechpartner."}, - {id: 4, kundenId: 3, kundenName: "Test Unternehmen", anrede: "Frau", name: "Lisa Bauer", position: "Projektmanagerin", telefon: "+49 678 90123", email: "l.bauer@test-unternehmen.com", notiz: ""} -] - -/** - * Liefert die nächste freie Kontakt-ID auf Basis des In-Memory-Speichers. - * Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion. - */ -export function getNewKontaktId(): number { - if (kontaktDaten.length === 0) { - return 1 - } - return Math.max(...kontaktDaten.map((kontakt: iKontakt) => kontakt.id)) + 1 -} - -/** - * Fügt einen neuen Kontakt dem In-Memory-Speicher hinzu. - */ -export function addKontakt(kontakt: iKontakt): void { - kontaktDaten.push(kontakt) -} - -/** - * Aktualisiert einen bestehenden Kontakt im In-Memory-Speicher. - */ -export function updateKontakt(kontakt: iKontakt): void { - const index = kontaktDaten.findIndex((k: iKontakt) => k.id === kontakt.id) - if (index !== -1) { - kontaktDaten[index] = kontakt - } -} - -/** - * Liefert eine Kopie aller Kontakte aus dem In-Memory-Speicher. - */ -export function getKontakte(): iKontakt[] { - return [...kontaktDaten] -} - -/** - * Liefert die Kontakte eines bestimmten Kunden. Ist kundenId null, werden - * alle Kontakte zurückgegeben. - */ -export function getKontakteByKundenId(kundenId: number | null): iKontakt[] { - return kundenId - ? kontaktDaten.filter((k: iKontakt) => k.kundenId === kundenId) - : [...kontaktDaten] -} - -/** - * Liefert einen einzelnen Kontakt anhand seiner ID oder undefined. - */ -export function getKontaktById(id: number): iKontakt | undefined { - return kontaktDaten.find((k: iKontakt) => 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/kontakt/*). */ /* ------------------------------------------------------------------ */ /** diff --git a/Client/models/projekt.ts b/Client/models/projekt.ts index b2793a9..8153d80 100644 --- a/Client/models/projekt.ts +++ b/Client/models/projekt.ts @@ -2,11 +2,14 @@ import m from "mithril" /** * Datenmodell für ein Projekt. + * Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case). + * kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der + * Kunde-Relationship abgeleitet und im JSON ergänzt. */ export interface iProjekt { id: number - kundenId: number - kundenName: string + kunden_id: number + kunden_name: string projektname: string zweck: string verantwortlicher: string @@ -14,14 +17,15 @@ export interface iProjekt { } /** - * In-Memory-Datenspeicher für Projekte. Wird als Fallback solange verwendet, - * bis die serverseitigen REST-Endpunkte verfügbar sind. + * 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, kundenId: 1, kundenName: "Musterfirma GmbH", projektname: "Website-Relaunch", zweck: "Neugestaltung der Unternehmenswebsite", verantwortlicher: "Max Mustermann", startdatum: "2024-01-15"}, - {id: 2, kundenId: 1, kundenName: "Musterfirma GmbH", projektname: "E-Commerce Plattform", zweck: "Online-Shop Entwicklung", verantwortlicher: "Anna Schmidt", startdatum: "2024-03-01"}, - {id: 3, kundenId: 2, kundenName: "Beispiel AG", projektname: "CRM-System", zweck: "Kundenbeziehungsmanagement", verantwortlicher: "Peter Müller", startdatum: "2024-02-20"}, - {id: 4, kundenId: 3, kundenName: "Test Unternehmen", projektname: "Mobile App", zweck: "iOS und Android Entwicklung", verantwortlicher: "Lisa Bauer", startdatum: "2024-04-10"} + {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"} ] /** @@ -60,12 +64,12 @@ export function getProjekte(): iProjekt[] { } /** - * Liefert die Projekte eines bestimmten Kunden. Ist kundenId null, werden + * Liefert die Projekte eines bestimmten Kunden. Ist kunden_id null, werden * alle Projekte zurückgegeben. */ -export function getProjekteByKundenId(kundenId: number | null): iProjekt[] { - return kundenId - ? projektDaten.filter((p: iProjekt) => p.kundenId === kundenId) +export function getProjekteByKundenId(kunden_id: number | null): iProjekt[] { + return kunden_id + ? projektDaten.filter((p: iProjekt) => p.kunden_id === kunden_id) : [...projektDaten] } @@ -80,8 +84,7 @@ export function getProjektById(id: number): iProjekt | undefined { /* 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/projekt/*). */ /* ------------------------------------------------------------------ */ /**