Server+Client: REST-API für Entität Projekt, Kontakt auf REST umgestellt, ID-basiertes Laden, CLI-Musterprojekte, VS-Code Task

- Server: ORM-Modell Projekt (Tabelle projekte) mit FK kunden_id, Relationship zu Kunde
- Server: Geschäftslogik projekt.py (get/create/update/delete)
- Server: DB-Migration V01_02 (projekte-Tabelle)
- Server: REST-Endpunkte /xhr/projekt/* mit Autorisierung
- Server: CLI init-db legt Muster-Projekte je Kunde an (1-3)
- Client: kontakt.ts auf REST umgestellt, In-Memory entfernt, snake_case
- Client: projekt.ts auf REST umgestellt, In-Memory behalten für call_log/kontaktadresse
- Client: ID-basiertes Laden (nur ID in Route-Params, fetchById im oninit)
- Client: Kundenname in Überschrift von Kontaktliste/View und Projektliste/View
- Client: Select-Widget entfernt (Kunde voreingestellt), Hidden-Input für kunden_id
- Client: kunde.ts showProjekte/showKontakte auf kunden_id geändert
- Client: call_log.ts/kontaktadresse.ts Feldzugriffe auf iProjekt snake_case angepasst
- AGENTS.md: Vorgehensweise ID-basiertes Laden hinterlegt, Branch-Regel formuliert
- VS-Code Task "Flask: init-db" mit venv-Pfaden

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-04 12:03:52 +02:00
1 parent 4b172081b9
commit e04a52857a
15 files changed
+638 -278

No files matched your search

+18
View File
@@ -98,6 +98,24 @@
"showReuseMessage": true, "showReuseMessage": true,
"clear": 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
}
} }
] ]
} }
+3 -1
View File
@@ -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. In den Programmen sollen aussagekräftige Kommentare in deutsch erstellt werden.
## Vorgaben git Versionsverwaltung ## 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 ## 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. 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. 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. 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: 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 ```typescript
export const Select: m.ClosureComponent<iSelect> = () => { export const Select: m.ClosureComponent<iSelect> = () => {
+45
View File
@@ -34,6 +34,7 @@ def register_cli_commands(app: Flask):
from crm.model import checkdb as crm_checkdb from crm.model import checkdb as crm_checkdb
crm_checkdb.checkupdates() crm_checkdb.checkupdates()
_seed_muster_kunden() _seed_muster_kunden()
_seed_muster_projekte()
click.echo("Fertig.") click.echo("Fertig.")
@@ -102,6 +103,50 @@ def _seed_muster_kunden():
session.commit() 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(): def _reset_appdb():
""" """
Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen Setzt die CRM-Datenbank zurück: alle Tabellen löschen und neu anlegen
+62 -1
View File
@@ -12,7 +12,8 @@ erfolgt ausschließlich über __bind_key__.
import datetime import datetime
import decimal 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 from db import dbapp
@@ -93,4 +94,64 @@ class Kunde(dbapp.Model):
return columns2dict(self) 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") print("########## crm/model importiert")
+24
View File
@@ -43,6 +43,30 @@ def _db_V01_00():
session.commit() 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(): def checkupdates():
""" """
Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle Ermittelt die aktuelle DB-Version aus der Systeminfo-Tabelle und führt alle
+120
View File
@@ -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 <vibe@mistral.ai>
: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()
+116
View File
@@ -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 <vibe@mistral.ai>
: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()
+92
View File
@@ -19,6 +19,8 @@ from flask import jsonify, request
from auth.decorators import authorisation_required from auth.decorators import authorisation_required
from crm.model import KUNDEN_MANAGER, MITARBEITER from crm.model import KUNDEN_MANAGER, MITARBEITER
from crm.model import kunde as kunden from crm.model import kunde as kunden
from crm.model import kontakt as kontakte
from crm.model import projekt as projekte
print("crm-routes") print("crm-routes")
@@ -68,4 +70,94 @@ def kunde_delete():
return jsonify({}), 200 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") print("########## crm.routes importiert")
+2 -2
View File
@@ -49,7 +49,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
if (filterProjektId !== null) { if (filterProjektId !== null) {
const projekt = getProjektById(filterProjektId) const projekt = getProjektById(filterProjektId)
if (projekt) { if (projekt) {
params.kundenName = projekt.kundenName params.kundenName = projekt.kunden_name
params.projektId = projekt.id params.projektId = projekt.id
params.projektName = projekt.projektname params.projektName = projekt.projektname
} }
@@ -212,7 +212,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
let kundenId = -1 let kundenId = -1
if (kundeVoreingestellt) { if (kundeVoreingestellt) {
const projekt = getProjektById(projektId) const projekt = getProjektById(projektId)
if (projekt) { kundenId = projekt.kundenId } if (projekt) { kundenId = projekt.kunden_id }
} }
currentLog = { currentLog = {
id: routeParams.id ? parseInt(routeParams.id) : -1, id: routeParams.id ? parseInt(routeParams.id) : -1,
+60 -90
View File
@@ -1,23 +1,17 @@
import m from "mithril" import m from "mithril"
import {Form, iForm} from "./widgets/form" import {Form, iForm} from "./widgets/form"
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs" import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
import {Select, iSelect, iSelectOption} from "./widgets/select"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {iKontakt, getKontakteByKundenId, getNewKontaktId, addKontakt, updateKontakt} from "../models/kontakt" import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
import {iKunde, fetchKunden} from "../models/kunde" 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) => { export const kontaktliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createKontaktTreenodes() let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null let selectedNode: iTreenode | null = null
let filterKundenId: number | 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 // Kundenliste asynchron vom Server laden, für die Kundenauflösung
// beim Anlegen eines neuen Kontakts. // beim Anlegen eines neuen Kontakts.
let kundenListe: iKunde[] = [] let kundenListe: iKunde[] = []
@@ -27,21 +21,13 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
} }
const addKontaktRoute = () => { const addKontaktRoute = () => {
const params: any = { const params: any = {id: -1}
id: -1,
anrede: "",
name: "",
position: "",
telefon: "",
email: "",
notiz: ""
}
// Nur bei kundengefilterter Liste den Kunden vorbelegen. // Nur bei kundengefilterter Liste den Kunden vorbelegen.
if (filterKundenId !== null) { if (filterKundenId !== null) {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) { if (kunde) {
params.kundenId = kunde.id params.kunden_id = kunde.id
params.kundenname = kunde.name params.kunden_name = kunde.name
} }
} }
m.route.set("/kontakt", params) m.route.set("/kontakt", params)
@@ -49,17 +35,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
const editKontakt = (kontakt: iKontakt, e: Event) => { const editKontakt = (kontakt: iKontakt, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/kontakt", { m.route.set("/kontakt", {id: kontakt.id})
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
})
} }
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => { const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
@@ -70,7 +46,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
m("span", kontakt.position), m("span", kontakt.position),
m("span", kontakt.telefon), m("span", kontakt.telefon),
m("span", kontakt.email), 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")) 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.position.toLowerCase().includes(searchLower) ||
kontakt.telefon.toLowerCase().includes(searchLower) || kontakt.telefon.toLowerCase().includes(searchLower) ||
kontakt.email.toLowerCase().includes(searchLower) || kontakt.email.toLowerCase().includes(searchLower) ||
kontakt.kundenName.toLowerCase().includes(searchLower) kontakt.kunden_name.toLowerCase().includes(searchLower)
} }
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams && routeParams.kundenId) { if (routeParams && routeParams.kunden_id) {
filterKundenId = parseInt(routeParams.kundenId) filterKundenId = parseInt(routeParams.kunden_id)
kundenName = routeParams.kunden_name || ""
} else { } else {
filterKundenId = null 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. // Kundenliste asynchron vom Server laden.
fetchKunden().then((result) => { fetchKunden().then((result) => {
kundenListe = 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) => { view: (vnode) => {
@@ -106,7 +100,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
'flex-direction': 'column', 'flex-direction': 'column',
height: 'inherit', height: 'inherit',
'overflow-y': 'inherit'}}, 'overflow-y': 'inherit'}},
m("h1", "Kontaktliste"), m("h1", kundenName ? "Kontaktliste für " + kundenName : "Kontaktliste"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable, m(Treetable,
{ {
@@ -127,12 +121,12 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
} }
export const Kontakt: m.ClosureComponent = (vnode) => { export const Kontakt: m.ClosureComponent = (vnode) => {
// currentKontakt hält den Formularzustand. kundenId referenziert den // currentKontakt hält den Formularzustand. kunden_id referenziert den
// Kunden eindeutig, kundenname dient der Anzeige. // Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift.
let currentKontakt: { let currentKontakt: {
id: number id: number
kundenId: number kunden_id: number
kundenname: string kunden_name: string
anrede: string anrede: string
name: string name: string
position: string position: string
@@ -141,8 +135,8 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
notiz: string notiz: string
} = { } = {
id: -1, id: -1,
kundenId: -1, kunden_id: -1,
kundenname: "", kunden_name: "",
anrede: "", anrede: "",
name: "", name: "",
position: "", position: "",
@@ -150,44 +144,31 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
email: "", email: "",
notiz: "" 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 { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams) { const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1 if (id !== -1) {
kundeVoreingestellt = kundenId !== -1 // Kontaktdaten frisch vom Server holen, um zwischenzeitliche
currentKontakt = { // Änderungen anderer Nutzer nicht zu überschreiben.
id: routeParams.id ? parseInt(routeParams.id) : -1, fetchKontaktById(id).then((result) => {
kundenId: kundenId, currentKontakt = result
kundenname: routeParams.kundenname || "", })
anrede: routeParams.anrede || "", } else {
name: routeParams.name || "", // Neuanlegen: kunden_id und kunden_name aus Route-Params
position: routeParams.position || "", // (voreingestellt aus der Kontaktliste).
telefon: routeParams.telefon || "", currentKontakt.kunden_id = routeParams && routeParams.kunden_id ? parseInt(routeParams.kunden_id) : -1
email: routeParams.email || "", currentKontakt.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
notiz: routeParams.notiz || ""
}
} }
fetchKunden().then((result) => {
kundenListe = result
})
}, },
view: (vnode) => { view: (vnode) => {
const submit = (form: HTMLFormElement): boolean => { const submit = (form: HTMLFormElement): boolean => {
const formData = new FormData(form) const formData = new FormData(form)
const 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 = { const gespeicherterKontakt: iKontakt = {
id: currentKontakt.id, id: currentKontakt.id,
kundenId: kundenId, kunden_id: currentKontakt.kunden_id,
kundenName: kunde ? kunde.name : "", kunden_name: "",
anrede: (formData.get("anrede") as string) || "", anrede: (formData.get("anrede") as string) || "",
name: (formData.get("name") as string) || "", name: (formData.get("name") as string) || "",
position: (formData.get("position") as string) || "", position: (formData.get("position") as string) || "",
@@ -196,33 +177,22 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
notiz: (formData.get("notiz") as string) || "" notiz: (formData.get("notiz") as string) || ""
} }
if (currentKontakt.id === -1) { if (currentKontakt.id === -1) {
gespeicherterKontakt.id = getNewKontaktId() createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}))
addKontakt(gespeicherterKontakt)
} else { } 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 return true
} }
const cancel = (e: Event) => { 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", 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("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("label", "Kunde", m("input", {type: "hidden", name: "kunden_id", value: currentKontakt.kunden_id}),
m(Select, {
name: "kundenId",
value: currentKontakt.kundenId,
options: kundenOptionen,
readonly: kundeVoreingestellt,
required: true
} as iSelect)),
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontakt.anrede, type: "text"} as iInput)), 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", "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)), m("label", "Position", m(Input, {name: "position", value: currentKontakt.position, type: "text"} as iInput)),
+1 -1
View File
@@ -76,7 +76,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
const projekt = getProjektById(kontaktadresse.projektId) const projekt = getProjektById(kontaktadresse.projektId)
m.route.set("/call_log", { m.route.set("/call_log", {
id: -1, id: -1,
kundenName: projekt ? projekt.kundenName : "", kundenName: projekt ? projekt.kunden_name : "",
projektId: kontaktadresse.projektId, projektId: kontaktadresse.projektId,
projektName: kontaktadresse.projektName, projektName: kontaktadresse.projektName,
datum: datum, datum: datum,
+13 -15
View File
@@ -2,7 +2,7 @@ import m from "mithril"
import {Form, iForm} from "./widgets/form" import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs" import { Input, iInput} from "./widgets/inputs"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2" 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) => { export const kunde: m.ClosureComponent = (vnode) => {
let currentKunde: iKunde = { let currentKunde: iKunde = {
@@ -17,15 +17,13 @@ export const kunde: m.ClosureComponent = (vnode) => {
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams) { const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
currentKunde = { if (id !== -1) {
id: routeParams.id ? parseInt(routeParams.id) : -1, // Kundendaten frisch vom Server holen, um zwischenzeitliche
name: routeParams.name || "", // Änderungen anderer Nutzer nicht zu überschreiben.
strasse: routeParams.strasse || "", fetchKundeById(id).then((result) => {
plz: routeParams.plz || "", currentKunde = result
ort: routeParams.ort || "", })
land: routeParams.land || ""
}
} }
}, },
view: (vnode) => { view: (vnode) => {
@@ -54,7 +52,7 @@ export const kunde: m.ClosureComponent = (vnode) => {
m("h1", currentKunde.id === -1 ? "Neuen Kunden anlegen" : "Kundenstammblatt"), m("h1", currentKunde.id === -1 ? "Neuen Kunden anlegen" : "Kundenstammblatt"),
currentKunde.id !== -1 currentKunde.id !== -1
? m("button.w3-button.w3-theme", ? 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") m("i.material-icons", "contact_page"), " Kontakte")
: null : null
), ),
@@ -82,21 +80,21 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
const editKunde = (kunde: iKunde, e: Event) => { const editKunde = (kunde: iKunde, e: Event) => {
e.stopPropagation() 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) => { const showProjekte = (kunde: iKunde, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/projektliste", {kundenId: kunde.id}) m.route.set("/projektliste", {kunden_id: kunde.id})
} }
const showKontakte = (kunde: iKunde, e: Event) => { const showKontakte = (kunde: iKunde, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/kontaktliste", {kundenId: kunde.id}) m.route.set("/kontaktliste", {kunden_id: kunde.id})
} }
const addKundeRoute = () => { 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) => { const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
+58 -86
View File
@@ -1,23 +1,17 @@
import m from "mithril" import m from "mithril"
import {Form, iForm} from "./widgets/form" import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs" 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 {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" 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) => { export const projektliste: m.ClosureComponent = (vnode) => {
let treenodes: iTreenode[] = createProjektTreenodes() let treenodes: iTreenode[] = []
let selectedNode: iTreenode | null = null let selectedNode: iTreenode | null = null
let filterKundenId: number | 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 // Kundenliste asynchron vom Server laden, für die Kundenauflösung
// beim Anlegen eines neuen Projekts. // beim Anlegen eines neuen Projekts.
let kundenListe: iKunde[] = [] let kundenListe: iKunde[] = []
@@ -27,20 +21,13 @@ export const projektliste: m.ClosureComponent = (vnode) => {
} }
const addProjektRoute = () => { const addProjektRoute = () => {
const params: any = { const params: any = {id: -1}
id: -1,
kundenname: "",
projektname: "",
zweck: "",
verantwortlicher: "",
startdatum: ""
}
// Nur bei kundengefilterter Liste den Kunden vorbelegen. // Nur bei kundengefilterter Liste den Kunden vorbelegen.
if (filterKundenId !== null) { if (filterKundenId !== null) {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId) const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) { if (kunde) {
params.kundenId = kunde.id params.kunden_id = kunde.id
params.kundenname = kunde.name params.kunden_name = kunde.name
} }
} }
m.route.set("/projekt", params) m.route.set("/projekt", params)
@@ -48,15 +35,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const editProjekt = (projekt: iProjekt, e: Event) => { const editProjekt = (projekt: iProjekt, e: Event) => {
e.stopPropagation() e.stopPropagation()
m.route.set("/projekt", { m.route.set("/projekt", {id: projekt.id})
id: projekt.id,
kundenId: projekt.kundenId,
kundenname: projekt.kundenName,
projektname: projekt.projektname,
zweck: projekt.zweck,
verantwortlicher: projekt.verantwortlicher,
startdatum: projekt.startdatum
})
} }
const showCallLogs = (projekt: iProjekt, e: Event) => { const showCallLogs = (projekt: iProjekt, e: Event) => {
@@ -76,7 +55,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1] const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
m.route.set("/call_log", { m.route.set("/call_log", {
id: -1, id: -1,
kundenName: projekt.kundenName, kundenName: projekt.kunden_name,
projektId: projekt.id, projektId: projekt.id,
projektName: projekt.projektname, projektName: projekt.projektname,
datum: datum, datum: datum,
@@ -95,7 +74,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
let projekt: iProjekt = node.nodevalue let projekt: iProjekt = node.nodevalue
return [ return [
m("div", m("strong", projekt.kundenName)), m("div", m("strong", projekt.kunden_name)),
m("span", projekt.projektname), m("span", projekt.projektname),
m("span", projekt.zweck), m("span", projekt.zweck),
m("span", projekt.verantwortlicher), m("span", projekt.verantwortlicher),
@@ -112,7 +91,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
const filterFunction = (node: iTreenode, searchvalue: string): boolean => { const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
let projekt: iProjekt = node.nodevalue let projekt: iProjekt = node.nodevalue
let searchLower = searchvalue.toLowerCase() let searchLower = searchvalue.toLowerCase()
return projekt.kundenName.toLowerCase().includes(searchLower) || return projekt.kunden_name.toLowerCase().includes(searchLower) ||
projekt.projektname.toLowerCase().includes(searchLower) || projekt.projektname.toLowerCase().includes(searchLower) ||
projekt.zweck.toLowerCase().includes(searchLower) || projekt.zweck.toLowerCase().includes(searchLower) ||
projekt.verantwortlicher.toLowerCase().includes(searchLower) || projekt.verantwortlicher.toLowerCase().includes(searchLower) ||
@@ -122,15 +101,30 @@ export const projektliste: m.ClosureComponent = (vnode) => {
return { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams && routeParams.kundenId) { if (routeParams && routeParams.kunden_id) {
filterKundenId = parseInt(routeParams.kundenId) filterKundenId = parseInt(routeParams.kunden_id)
kundenName = routeParams.kunden_name || ""
} else { } else {
filterKundenId = null 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. // Kundenliste asynchron vom Server laden.
fetchKunden().then((result) => { fetchKunden().then((result) => {
kundenListe = result kundenListe = result
if (filterKundenId !== null && kundenName === "") {
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
if (kunde) {
kundenName = kunde.name
}
}
}) })
}, },
view: (vnode) => { view: (vnode) => {
@@ -140,7 +134,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
'flex-direction': 'column', 'flex-direction': 'column',
height: 'inherit', height: 'inherit',
'overflow-y': 'inherit'}}, 'overflow-y': 'inherit'}},
m("h1", "Projektliste"), m("h1", kundenName ? "Projektliste für " + kundenName : "Projektliste"),
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}}, m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
m(Treetable, m(Treetable,
{ {
@@ -161,85 +155,70 @@ export const projektliste: m.ClosureComponent = (vnode) => {
} }
export const Projekt: m.ClosureComponent = (vnode) => { export const Projekt: m.ClosureComponent = (vnode) => {
// currentProjekt hält den Formularzustand. kundenId referenziert den // currentProjekt hält den Formularzustand. kunden_id referenziert den
// Kunden eindeutig, kundenname dient der Anzeige. // Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift.
let currentProjekt: { let currentProjekt: {
id: number id: number
kundenId: number kunden_id: number
kundenname: string kunden_name: string
projektname: string projektname: string
zweck: string zweck: string
verantwortlicher: string verantwortlicher: string
startdatum: string startdatum: string
} = { } = {
id: -1, id: -1,
kundenId: -1, kunden_id: -1,
kundenname: "", kunden_name: "",
projektname: "", projektname: "",
zweck: "", zweck: "",
verantwortlicher: "", verantwortlicher: "",
startdatum: "" 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 { return {
oninit: (vnode) => { oninit: (vnode) => {
const routeParams = m.route.param() const routeParams = m.route.param()
if (routeParams) { const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1 if (id !== -1) {
kundeVoreingestellt = kundenId !== -1 // Projektdaten frisch vom Server holen, um zwischenzeitliche
currentProjekt = { // Änderungen anderer Nutzer nicht zu überschreiben.
id: routeParams.id ? parseInt(routeParams.id) : -1, fetchProjektById(id).then((result) => {
kundenId: kundenId, currentProjekt = result
kundenname: routeParams.kundenname || "", })
projektname: routeParams.projektname || "", } else {
zweck: routeParams.zweck || "", // Neuanlegen: kunden_id und kunden_name aus Route-Params
verantwortlicher: routeParams.verantwortlicher || "", // (voreingestellt aus der Projektliste).
startdatum: routeParams.startdatum || "" 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) => { view: (vnode) => {
const submit = (form: HTMLFormElement): boolean => { const submit = (form: HTMLFormElement): boolean => {
const formData = new FormData(form) 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 = { const gespeichertesProjekt: iProjekt = {
id: currentProjekt.id, id: currentProjekt.id,
kundenId: kundenId, kunden_id: currentProjekt.kunden_id,
kundenName: kunde ? kunde.name : "", kunden_name: "",
projektname: (formData.get("projektname") as string) || "", projektname: (formData.get("projektname") as string) || "",
zweck: (formData.get("zweck") as string) || "", zweck: (formData.get("zweck") as string) || "",
verantwortlicher: (formData.get("verantwortlicher") as string) || "", verantwortlicher: (formData.get("verantwortlicher") as string) || "",
startdatum: (formData.get("startdatum") as string) || "" startdatum: (formData.get("startdatum") as string) || ""
} }
if (currentProjekt.id === -1) { if (currentProjekt.id === -1) {
gespeichertesProjekt.id = getNewProjektId() createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}))
addProjekt(gespeichertesProjekt)
} else { } 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 return true
} }
const cancel = (e: Event) => { 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", return m("div.w3-container.w3-auto.form-view-container",
m("div", {style: {display: 'flex', 'align-items': 'center', 'justify-content': 'space-between'}}, 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 currentProjekt.id !== -1
? m("button.w3-button.w3-theme", ? m("button.w3-button.w3-theme",
{onclick: () => m.route.set("/kontaktadresseliste", {projektId: currentProjekt.id})}, {onclick: () => m.route.set("/kontaktadresseliste", {projektId: currentProjekt.id})},
@@ -248,14 +227,7 @@ export const Projekt: m.ClosureComponent = (vnode) => {
), ),
m("div", {style: {flex: '1', 'min-height': '0'}}, m("div", {style: {flex: '1', 'min-height': '0'}},
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [ m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
m("label", "Kunde", m("input", {type: "hidden", name: "kunden_id", value: currentProjekt.kunden_id}),
m(Select, {
name: "kundenId",
value: currentProjekt.kundenId,
options: kundenOptionen,
readonly: kundeVoreingestellt,
required: true
} as iSelect)),
m("label", "Projektname", m(Input, {name: "projektname", value: currentProjekt.projektname, type: "text", required: true} as iInput)), 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", "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)), m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: currentProjekt.verantwortlicher, type: "text"} as iInput)),
+7 -68
View File
@@ -2,12 +2,15 @@ import m from "mithril"
/** /**
* Datenmodell für einen Kontakt (Ansprechpartner beim Kunden). * 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 { export interface iKontakt {
id: number id: number
kundenId: number kunden_id: number
kundenName: string kunden_name: string
anrede: string anrede: string
name: string name: string
position: string position: string
@@ -16,75 +19,11 @@ export interface iKontakt {
notiz: string 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) */ /* REST-API-Aufrufe (Client-Seite) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */ /* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */ /* serverseitigen REST-Endpunkten (/xhr/kontakt/*). */
/* implementiert und werden später nachgereicht. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**
+17 -14
View File
@@ -2,11 +2,14 @@ import m from "mithril"
/** /**
* Datenmodell für ein Projekt. * 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 { export interface iProjekt {
id: number id: number
kundenId: number kunden_id: number
kundenName: string kunden_name: string
projektname: string projektname: string
zweck: string zweck: string
verantwortlicher: string verantwortlicher: string
@@ -14,14 +17,15 @@ export interface iProjekt {
} }
/** /**
* In-Memory-Datenspeicher für Projekte. Wird als Fallback solange verwendet, * In-Memory-Datenspeicher für Projekte. Wird noch von call_log.ts und
* bis die serverseitigen REST-Endpunkte verfügbar sind. * kontaktadresse.ts verwendet, bis diese Entitäten ebenfalls auf REST
* umgestellt sind.
*/ */
export const projektDaten: iProjekt[] = [ 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: 1, kunden_id: 1, kunden_name: "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: 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, kundenId: 2, kundenName: "Beispiel AG", projektname: "CRM-System", zweck: "Kundenbeziehungsmanagement", verantwortlicher: "Peter Müller", startdatum: "2024-02-20"}, {id: 3, kunden_id: 2, kunden_name: "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: 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. * alle Projekte zurückgegeben.
*/ */
export function getProjekteByKundenId(kundenId: number | null): iProjekt[] { export function getProjekteByKundenId(kunden_id: number | null): iProjekt[] {
return kundenId return kunden_id
? projektDaten.filter((p: iProjekt) => p.kundenId === kundenId) ? projektDaten.filter((p: iProjekt) => p.kunden_id === kunden_id)
: [...projektDaten] : [...projektDaten]
} }
@@ -80,8 +84,7 @@ export function getProjektById(id: number): iProjekt | undefined {
/* REST-API-Aufrufe (Client-Seite) */ /* REST-API-Aufrufe (Client-Seite) */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/* Die folgenden Funktionen kapseln die Kommunikation mit den */ /* Die folgenden Funktionen kapseln die Kommunikation mit den */
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */ /* serverseitigen REST-Endpunkten (/xhr/projekt/*). */
/* implementiert und werden später nachgereicht. */
/* ------------------------------------------------------------------ */ /* ------------------------------------------------------------------ */
/** /**