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:
1 parent
4b172081b9
commit
e04a52857a
15 files changed
+647
-287
No files matched your search
Vendored
+18
@@ -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,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> = () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -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()
|
||||||
@@ -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")
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)),
|
||||||
|
|||||||
@@ -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
@@ -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) => {
|
||||||
|
|||||||
@@ -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,38 +21,23 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
m.route.set("/calllogliste", {projektId: projekt.id})
|
m.route.set("/calllogliste", {projektId: projekt.id})
|
||||||
@@ -68,7 +47,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
|||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
m.route.set("/kontaktadresseliste", {projektId: projekt.id})
|
m.route.set("/kontaktadresseliste", {projektId: projekt.id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCallLog = (projekt: iProjekt, e: Event) => {
|
const addCallLog = (projekt: iProjekt, e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
@@ -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,
|
||||||
@@ -90,12 +69,12 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
|||||||
erledigt: false
|
erledigt: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||||
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),
|
||||||
@@ -108,39 +87,54 @@ 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) ||
|
||||||
projekt.startdatum.includes(searchLower)
|
projekt.startdatum.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 = 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) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
style: {
|
style: {
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
'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)),
|
||||||
|
|||||||
@@ -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
@@ -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. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in new issue
Block a user