Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93827dde0f | ||
|
|
e9a2f48ede | ||
|
|
b510c50497 | ||
|
|
e04a52857a | ||
|
|
4b172081b9 | ||
|
|
455157942a | ||
|
|
15d4e2ac48 |
No files matched your search
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: client-build
|
||||||
|
description: Load this skill after modifying TypeScript files under Client/ to compile and bundle the frontend. Runs tsc and rollup to produce the deployable JS bundle in NGINX_Server/html/static/.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Client Build
|
||||||
|
|
||||||
|
Führt den TypeScript- und Rollup-Build für die Client-Seite aus. Wird benötigt,
|
||||||
|
nachdem `.ts`-Dateien unter `Client/` geändert wurden, damit die kompilierten
|
||||||
|
Bundles in `NGINX_Server/html/static/` aktualisiert werden.
|
||||||
|
|
||||||
|
## Build-Schritte
|
||||||
|
|
||||||
|
1. **TypeScript kompilieren** — kompiliert alle `.ts`-Dateien nach `Client/tsout/`:
|
||||||
|
```bash
|
||||||
|
cd /usr/opt/pythonapp/Client && npx tsc
|
||||||
|
```
|
||||||
|
Bei Fehlern: Ausgabe lesen, Fehler beheben, erneut ausführen.
|
||||||
|
|
||||||
|
2. **Rollup bundle** — bündelt `tsout/index.js` zu den statischen JS-Dateien:
|
||||||
|
```bash
|
||||||
|
cd /usr/opt/pythonapp/Client && npx rollup -c
|
||||||
|
```
|
||||||
|
`rollup -c` liest die Konfiguration aus `rollup.config.mjs`. Es fasst alle
|
||||||
|
kompilierten `.js`-Dateien plus Mithril zu einem selbstausführenden Bundle
|
||||||
|
(Format: iife) zusammen und schreibt zwei Dateien nach
|
||||||
|
`NGINX_Server/html/static/`:
|
||||||
|
- `kbp_crm_00.00.01.js` — unminifiziert (für Debug)
|
||||||
|
- `kbp_crm_00.00.01.min.js` — minifiziert via terser (für Produktion)
|
||||||
|
|
||||||
|
Die Dateinamen setzen sich aus `projectname` und `version` aus `config.ini`
|
||||||
|
zusammen (wird in `rollup.config.mjs` via `inireader.mjs` gelesen).
|
||||||
|
|
||||||
|
## Beide Schritte in einem Aufruf
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /usr/opt/pythonapp/Client && npx tsc && npx rollup -c
|
||||||
|
```
|
||||||
|
|
||||||
|
## Wann ausführen
|
||||||
|
|
||||||
|
- Nach jeder Änderung an `.ts`-Dateien unter `Client/` (Modelle, Komponenten,
|
||||||
|
Widgets, index.ts)
|
||||||
|
- Vor einem Commit, der Client-Änderungen enthält, damit die deploybaren
|
||||||
|
Bundles aktuell sind
|
||||||
|
- Nicht nötig bei reinen Server-Änderungen (`App_Server/`)
|
||||||
|
|
||||||
|
## Fehlerbehandlung
|
||||||
|
|
||||||
|
- **tsc-Fehler:** TypeScript-Compiler-Fehler müssen vor dem Rollup-Schritt
|
||||||
|
behoben werden — Rollup kann nicht ohne erfolgreiche Kompilierung bündeln.
|
||||||
|
- **rollup-Fehler:** Meist fehlende Imports oder Pfadprobleme. Die
|
||||||
|
`rollup.config.mjs` erwartet `tsout/index.js` als Input — sicherstellen,
|
||||||
|
dass tsc erfolgreich war.
|
||||||
Vendored
+20
-2
@@ -67,7 +67,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"group": "none",
|
"group": "none",
|
||||||
"label": "Open auth.db",
|
"label": "Open auth.db",
|
||||||
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe auth.db",
|
"command": "sqlite3 auth.db",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/App_Server/data"
|
"cwd": "${workspaceFolder}/App_Server/data"
|
||||||
},
|
},
|
||||||
@@ -85,7 +85,7 @@
|
|||||||
"type": "shell",
|
"type": "shell",
|
||||||
"group": "none",
|
"group": "none",
|
||||||
"label": "Open app.db",
|
"label": "Open app.db",
|
||||||
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe kbp_crm.db",
|
"command": "sqlite3 kbp_crm.db",
|
||||||
"options": {
|
"options": {
|
||||||
"cwd": "${workspaceFolder}/App_Server/data"
|
"cwd": "${workspaceFolder}/App_Server/data"
|
||||||
},
|
},
|
||||||
@@ -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> = () => {
|
||||||
@@ -47,4 +49,9 @@ export const Select: m.ClosureComponent<iSelect> = () => {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Nach jeder Änderung an `.ts`-Dateien unter `Client/` ist der Build-Prozess (tsc + rollup) auszuführen, damit die deploybaren Bundles in `NGINX_Server/html/static/` aktuell sind. Verwende dafür den `client-build`-Skill.
|
||||||
|
|
||||||
## Vorgaben für die Server-seitige Entwicklung unter Python
|
## Vorgaben für die Server-seitige Entwicklung unter Python
|
||||||
|
|
||||||
|
## Generierte Dateien nicht beachten
|
||||||
|
Das Verzeichnis `Client/tsout/` enthält die JavaScript-Ausgabe der TypeScript-Kompilation und wird durch den Build-Prozess (tsc + rollup) erzeugt. Diese Dateien sind nicht manuell zu bearbeiten und brauchen bei der Codeanalyse nicht berücksichtigt zu werden. Ebenso sind die JavaScript-Dateien in `NGINX_Server/html/static/` (z.B. `kbp_crm_00.00.01.js`) die Ausgabe des Rollup-Builds und nicht zu untersuchen.
|
||||||
@@ -34,6 +34,9 @@ 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()
|
||||||
|
_seed_muster_kontaktadressen()
|
||||||
|
_seed_muster_calllogs()
|
||||||
|
|
||||||
click.echo("Fertig.")
|
click.echo("Fertig.")
|
||||||
|
|
||||||
@@ -102,6 +105,163 @@ 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 _seed_muster_kontaktadressen():
|
||||||
|
"""
|
||||||
|
Legt für jedes Muster-Projekt 1 bis 3 Kontaktadressen an, falls die
|
||||||
|
kontaktadressen-Tabelle noch leer ist.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
from crm.model import Projekt, Kontaktadresse
|
||||||
|
|
||||||
|
session = dbapp.session
|
||||||
|
if session.query(Kontaktadresse).count() > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
projekte = session.query(Projekt).order_by(Projekt.id).all()
|
||||||
|
vorlagen = [
|
||||||
|
("Herr", "Thomas Becker", "Becker Webdesign GmbH", "Geschäftsführer"),
|
||||||
|
("Frau", "Sandra Klein", "Klein Marketing", "Marketingleiterin"),
|
||||||
|
("Herr", "Michael Wagner", "Wagner Online Shop", "IT-Leiter"),
|
||||||
|
("Frau", "Julia Hoffmann", "Hoffmann Consulting", "Beraterin"),
|
||||||
|
("Herr", "Stefan Roth", "Roth IT Solutions", "Technischer Leiter"),
|
||||||
|
]
|
||||||
|
orte = [
|
||||||
|
("Industriestr. 12", "45131", "Essen", "Deutschland"),
|
||||||
|
("Königsallee 5", "40212", "Düsseldorf", "Deutschland"),
|
||||||
|
("Hauptstr. 88", "50667", "Köln", "Deutschland"),
|
||||||
|
("Marienplatz 3", "80331", "München", "Deutschland"),
|
||||||
|
]
|
||||||
|
rng = random.Random(123)
|
||||||
|
vorlagen_idx = 0
|
||||||
|
for projekt in projekte:
|
||||||
|
anzahl = rng.randint(1, 3)
|
||||||
|
for _ in range(anzahl):
|
||||||
|
anrede, name, firma, position = vorlagen[vorlagen_idx % len(vorlagen)]
|
||||||
|
vorlagen_idx += 1
|
||||||
|
strasse, plz, ort, land = rng.choice(orte)
|
||||||
|
ka = Kontaktadresse()
|
||||||
|
ka.projekt_id = projekt.id
|
||||||
|
ka.anrede = anrede
|
||||||
|
ka.name = name
|
||||||
|
ka.firma = firma
|
||||||
|
ka.position = position
|
||||||
|
ka.telefon = "+49 {vorwahl} {nummer}".format(
|
||||||
|
vorwahl=rng.randint(100, 999),
|
||||||
|
nummer=rng.randint(100000, 999999)
|
||||||
|
)
|
||||||
|
ka.email = name.lower().replace(" ", ".") + "@example.de"
|
||||||
|
ka.strasse = strasse
|
||||||
|
ka.plz = plz
|
||||||
|
ka.ort = ort
|
||||||
|
ka.land = land
|
||||||
|
ka.notiz = ""
|
||||||
|
session.add(ka)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _seed_muster_calllogs():
|
||||||
|
"""
|
||||||
|
Legt für jedes Muster-Projekt 2 bis 5 Gesprächsnotizen an, falls die
|
||||||
|
call_logs-Tabelle noch leer ist.
|
||||||
|
"""
|
||||||
|
import random
|
||||||
|
|
||||||
|
from crm.model import Projekt, CallLog
|
||||||
|
|
||||||
|
session = dbapp.session
|
||||||
|
if session.query(CallLog).count() > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
projekte = session.query(Projekt).order_by(Projekt.id).all()
|
||||||
|
betreffvorlagen = [
|
||||||
|
"Anforderungen klären",
|
||||||
|
"Feedback zum Design",
|
||||||
|
"Technische Anforderungen",
|
||||||
|
"Schulungstermin",
|
||||||
|
"Feature-Request",
|
||||||
|
"Budgetgespräch",
|
||||||
|
"Projekt-Review",
|
||||||
|
]
|
||||||
|
notizvorlagen = [
|
||||||
|
"Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.",
|
||||||
|
"Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.",
|
||||||
|
"Payment-Gateway und Versand-Schnittstellen müssen integriert werden.",
|
||||||
|
"Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.",
|
||||||
|
"Kunde wünscht Push-Benachrichtigungen und Offline-Modus.",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
ansprechpartner = ["Herr Schmidt", "Frau Müller", "Herr Weber", "Frau Bauer", "Herr König"]
|
||||||
|
rng = random.Random(456)
|
||||||
|
for projekt in projekte:
|
||||||
|
anzahl = rng.randint(2, 5)
|
||||||
|
for _ in range(anzahl):
|
||||||
|
log = CallLog()
|
||||||
|
log.projekt_id = projekt.id
|
||||||
|
monat = rng.randint(1, 9)
|
||||||
|
tag = rng.randint(1, 28)
|
||||||
|
log.datum = "2024-0{m}-{t}".format(m=monat, t=tag) if monat < 10 else "2024-{m}-{t}".format(m=monat, t=tag)
|
||||||
|
log.uhrzeit_beginn = "{h:02d}:{min:02d}".format(
|
||||||
|
h=rng.randint(8, 17), min=rng.randint(0, 59)
|
||||||
|
)
|
||||||
|
log.dauer = str(rng.randint(15, 90))
|
||||||
|
log.betreff = rng.choice(betreffvorlagen)
|
||||||
|
log.name_ansprechpartner = rng.choice(ansprechpartner)
|
||||||
|
log.telefon = "+49 {v} {n}".format(
|
||||||
|
v=rng.randint(100, 999), n=rng.randint(100000, 999999)
|
||||||
|
)
|
||||||
|
log.notiz = rng.choice(notizvorlagen)
|
||||||
|
wiedervorlage_monat = monat + 1 if monat < 12 else 1
|
||||||
|
log.wiedervorlage_datum = "2024-0{m}-{t}".format(m=wiedervorlage_monat, t=tag) if wiedervorlage_monat < 10 else "2024-{m}-{t}".format(m=wiedervorlage_monat, t=tag)
|
||||||
|
log.erledigt = rng.choice([True, False])
|
||||||
|
session.add(log)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
def _reset_appdb():
|
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 Boolean, Column, DateTime, ForeignKey, Index, Integer, String
|
||||||
|
from sqlalchemy.orm import relationship
|
||||||
|
|
||||||
from db import dbapp
|
from db import dbapp
|
||||||
|
|
||||||
@@ -93,4 +94,152 @@ 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
|
||||||
|
__table_args__ = (
|
||||||
|
Index('ix_kontakte_kunden_id', 'kunden_id'),
|
||||||
|
)
|
||||||
|
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
|
||||||
|
__table_args__ = (
|
||||||
|
Index('ix_projekte_kunden_id', 'kunden_id'),
|
||||||
|
)
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Kontaktadresse(dbapp.Model):
|
||||||
|
"""
|
||||||
|
Akquise-Ziel innerhalb eines Projekts. Jede Kontaktadresse ist genau
|
||||||
|
einem Projekt zugeordnet (projekt_id). Der Projektname (projekt_name)
|
||||||
|
ist keine eigene Spalte, sondern wird über die Relationship zu Projekt
|
||||||
|
abgeleitet und nur in to_dict() für die JSON-Serialisierung ergänzt.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'kontaktadressen'
|
||||||
|
__bind_key__ = BINDKEY
|
||||||
|
__table_args__ = (
|
||||||
|
Index('ix_kontaktadressen_projekt_id', 'projekt_id'),
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
|
||||||
|
anrede = Column(String(30), nullable=True)
|
||||||
|
name = Column(String(200), nullable=False)
|
||||||
|
firma = Column(String(200), nullable=True)
|
||||||
|
position = Column(String(200), nullable=True)
|
||||||
|
telefon = Column(String(100), nullable=True)
|
||||||
|
email = Column(String(200), nullable=True)
|
||||||
|
strasse = Column(String(200), nullable=True)
|
||||||
|
plz = Column(String(20), nullable=True)
|
||||||
|
ort = Column(String(100), nullable=True)
|
||||||
|
land = Column(String(80), nullable=True)
|
||||||
|
notiz = Column(String(2000), nullable=True)
|
||||||
|
|
||||||
|
projekt = relationship('Projekt')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
|
||||||
|
über columns2dict serialisiert; projekt_name wird zusätzlich aus der
|
||||||
|
Projekt-Relationship eingefügt.
|
||||||
|
"""
|
||||||
|
result = columns2dict(self)
|
||||||
|
result['projekt_name'] = self.projekt.projektname if self.projekt else None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class CallLog(dbapp.Model):
|
||||||
|
"""
|
||||||
|
Gesprächsnotiz für ein Projekt. Jede Gesprächsnotiz ist genau einem
|
||||||
|
Projekt zugeordnet (projekt_id). Der Projektname (projekt_name) und
|
||||||
|
der Kundenname (kunden_name) sind keine eigenen Spalten, sondern
|
||||||
|
werden über die Projekt-Relationship abgeleitet und nur in to_dict()
|
||||||
|
für die JSON-Serialisierung ergänzt.
|
||||||
|
"""
|
||||||
|
__tablename__ = 'call_logs'
|
||||||
|
__bind_key__ = BINDKEY
|
||||||
|
__table_args__ = (
|
||||||
|
Index('ix_call_logs_projekt_id', 'projekt_id'),
|
||||||
|
)
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
projekt_id = Column(Integer, ForeignKey('projekte.id'), nullable=False)
|
||||||
|
datum = Column(String(20), nullable=True)
|
||||||
|
uhrzeit_beginn = Column(String(10), nullable=True)
|
||||||
|
dauer = Column(String(10), nullable=True)
|
||||||
|
betreff = Column(String(200), nullable=True)
|
||||||
|
name_ansprechpartner = Column(String(200), nullable=True)
|
||||||
|
telefon = Column(String(100), nullable=True)
|
||||||
|
notiz = Column(String(2000), nullable=True)
|
||||||
|
wiedervorlage_datum = Column(String(20), nullable=True)
|
||||||
|
erledigt = Column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
|
projekt = relationship('Projekt')
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
"""
|
||||||
|
Liefert die Gesprächsnotiz als Dictionary. Die Tabellenspalten werden
|
||||||
|
über columns2dict serialisiert; projekt_name und kunden_name werden
|
||||||
|
zusätzlich aus der Projekt-Relationship eingefügt.
|
||||||
|
"""
|
||||||
|
result = columns2dict(self)
|
||||||
|
if self.projekt:
|
||||||
|
result['projekt_name'] = self.projekt.projektname
|
||||||
|
result['kunden_name'] = self.projekt.kunde.name if self.projekt.kunde else None
|
||||||
|
else:
|
||||||
|
result['projekt_name'] = None
|
||||||
|
result['kunden_name'] = None
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
print("########## crm/model importiert")
|
print("########## crm/model importiert")
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
Geschäftslogik für die Entität CallLog (Gesprächsnotiz).
|
||||||
|
|
||||||
|
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
|
||||||
|
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
|
||||||
|
dort über den Decorator authorisation_required.
|
||||||
|
|
||||||
|
:author: Mistral Vibe <vibe@mistral.ai>
|
||||||
|
:date: 2026-09-04
|
||||||
|
:version: 1.0
|
||||||
|
"""
|
||||||
|
from db import dbapp
|
||||||
|
from crm.model import CallLog, Projekt
|
||||||
|
|
||||||
|
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
|
||||||
|
session = dbapp.session
|
||||||
|
|
||||||
|
|
||||||
|
def get_call_logs(projekt_id: int | None = None):
|
||||||
|
"""
|
||||||
|
Liefert Gesprächsnotizen, sortiert nach Datum absteigend.
|
||||||
|
|
||||||
|
Ist projekt_id gesetzt, werden nur die Notizen dieses Projekts
|
||||||
|
zurückgegeben (serverseitige Filterung). Ohne projekt_id werden
|
||||||
|
alle Notizen geliefert.
|
||||||
|
"""
|
||||||
|
query = CallLog.query
|
||||||
|
if projekt_id is not None:
|
||||||
|
query = query.filter(CallLog.projekt_id == projekt_id)
|
||||||
|
return query.order_by(CallLog.datum.desc()).all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_call_log(call_log_id: int) -> CallLog:
|
||||||
|
"""
|
||||||
|
Liefert eine einzelne Gesprächsnotiz anhand ihrer ID.
|
||||||
|
Wirft ApplicationError (404), falls die Notiz nicht existiert.
|
||||||
|
"""
|
||||||
|
call_log = CallLog.query.get(call_log_id)
|
||||||
|
if call_log is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Gesprächsnotiz mit der ID {} wurde nicht gefunden.".format(call_log_id),
|
||||||
|
htmlstatus=HtmlStatusCode.Not_Found,
|
||||||
|
)
|
||||||
|
return call_log
|
||||||
|
|
||||||
|
|
||||||
|
def create_call_log(data: dict) -> CallLog:
|
||||||
|
"""
|
||||||
|
Legt eine neue Gesprächsnotiz an. Die vom Client übergebene ID wird
|
||||||
|
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
|
||||||
|
|
||||||
|
- data: dict mit den Feldern projekt_id, datum, uhrzeit_beginn, dauer,
|
||||||
|
betreff, name_ansprechpartner, telefon, notiz,
|
||||||
|
wiedervorlage_datum, erledigt
|
||||||
|
"""
|
||||||
|
projekt_id = data.get("projekt_id")
|
||||||
|
if projekt_id is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Es muss ein Projekt ausgewählt werden.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
projekt = Projekt.query.get(int(projekt_id))
|
||||||
|
if projekt is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
call_log = CallLog()
|
||||||
|
call_log.projekt_id = int(projekt_id)
|
||||||
|
call_log.datum = (data.get("datum") or "").strip() or None
|
||||||
|
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
||||||
|
call_log.dauer = (data.get("dauer") or "").strip() or None
|
||||||
|
call_log.betreff = (data.get("betreff") or "").strip() or None
|
||||||
|
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
|
||||||
|
call_log.telefon = (data.get("telefon") or "").strip() or None
|
||||||
|
call_log.notiz = (data.get("notiz") or "").strip() or None
|
||||||
|
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
|
||||||
|
call_log.erledigt = bool(data.get("erledigt", False))
|
||||||
|
session.add(call_log)
|
||||||
|
session.commit()
|
||||||
|
return call_log
|
||||||
|
|
||||||
|
|
||||||
|
def update_call_log(data: dict) -> CallLog:
|
||||||
|
"""
|
||||||
|
Aktualisiert eine bestehende Gesprächsnotiz. Die ID im data-Dict
|
||||||
|
bestimmt den zu ändernden Datensatz.
|
||||||
|
"""
|
||||||
|
call_log = get_call_log(data["id"])
|
||||||
|
projekt_id = data.get("projekt_id")
|
||||||
|
if projekt_id is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Es muss ein Projekt ausgewählt werden.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
projekt = Projekt.query.get(int(projekt_id))
|
||||||
|
if projekt is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
call_log.projekt_id = int(projekt_id)
|
||||||
|
call_log.datum = (data.get("datum") or "").strip() or None
|
||||||
|
call_log.uhrzeit_beginn = (data.get("uhrzeit_beginn") or "").strip() or None
|
||||||
|
call_log.dauer = (data.get("dauer") or "").strip() or None
|
||||||
|
call_log.betreff = (data.get("betreff") or "").strip() or None
|
||||||
|
call_log.name_ansprechpartner = (data.get("name_ansprechpartner") or "").strip() or None
|
||||||
|
call_log.telefon = (data.get("telefon") or "").strip() or None
|
||||||
|
call_log.notiz = (data.get("notiz") or "").strip() or None
|
||||||
|
call_log.wiedervorlage_datum = (data.get("wiedervorlage_datum") or "").strip() or None
|
||||||
|
call_log.erledigt = bool(data.get("erledigt", False))
|
||||||
|
session.commit()
|
||||||
|
return call_log
|
||||||
|
|
||||||
|
|
||||||
|
def delete_call_log(call_log_id: int):
|
||||||
|
"""
|
||||||
|
Löscht eine Gesprächsnotiz anhand ihrer ID.
|
||||||
|
Wirft ApplicationError (404), falls die Notiz nicht existiert.
|
||||||
|
"""
|
||||||
|
call_log = get_call_log(call_log_id)
|
||||||
|
session.delete(call_log)
|
||||||
|
session.commit()
|
||||||
@@ -43,6 +43,71 @@ 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 _db_V01_03():
|
||||||
|
"""
|
||||||
|
Legt die Tabellen 'kontaktadressen' und 'call_logs' an. Bei bestehenden
|
||||||
|
Datenbanken, die mit Version 01.02 initialisiert wurden, fehlen diese
|
||||||
|
Tabellen, da die Modelle erst nachträglich ergänzt wurden.
|
||||||
|
"""
|
||||||
|
dbapp.create_all(bind_key=BINDKEY)
|
||||||
|
_updatesysteminfo('01.03')
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _db_V01_04():
|
||||||
|
"""
|
||||||
|
Fügt die Spalte 'position' zur Tabelle 'kontaktadressen' hinzu.
|
||||||
|
Bei bestehenden Datenbanken, die mit Version 01.03 initialisiert wurden,
|
||||||
|
fehlt diese Spalte, da das Feld erst nachträglich ergänzt wurde.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
session.execute(text("ALTER TABLE kontaktadressen ADD COLUMN position VARCHAR(200)"))
|
||||||
|
_updatesysteminfo('01.04')
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def _db_V01_05():
|
||||||
|
"""
|
||||||
|
Legt Indizes auf den Fremdschlüsselspalten an, die für die
|
||||||
|
serverseitige Filterung der Listen-Endpunkte genutzt werden.
|
||||||
|
CREATE INDEX IF NOT EXISTS ist idempotent und überspringet bereits
|
||||||
|
vorhandene Indizes.
|
||||||
|
"""
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
session.execute(text("CREATE INDEX IF NOT EXISTS ix_kontakte_kunden_id ON kontakte (kunden_id)"))
|
||||||
|
session.execute(text("CREATE INDEX IF NOT EXISTS ix_projekte_kunden_id ON projekte (kunden_id)"))
|
||||||
|
session.execute(text("CREATE INDEX IF NOT EXISTS ix_kontaktadressen_projekt_id ON kontaktadressen (projekt_id)"))
|
||||||
|
session.execute(text("CREATE INDEX IF NOT EXISTS ix_call_logs_projekt_id ON call_logs (projekt_id)"))
|
||||||
|
_updatesysteminfo('01.05')
|
||||||
|
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,127 @@
|
|||||||
|
"""
|
||||||
|
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(kunden_id: int | None = None):
|
||||||
|
"""
|
||||||
|
Liefert Kontakte, sortiert nach Name.
|
||||||
|
|
||||||
|
Ist kunden_id gesetzt, werden nur die Kontakte dieses Kunden
|
||||||
|
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
|
||||||
|
alle Kontakte geliefert.
|
||||||
|
"""
|
||||||
|
query = Kontakt.query
|
||||||
|
if kunden_id is not None:
|
||||||
|
query = query.filter(Kontakt.kunden_id == kunden_id)
|
||||||
|
return 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,137 @@
|
|||||||
|
"""
|
||||||
|
Geschäftslogik für die Entität Kontaktadresse.
|
||||||
|
|
||||||
|
Diese Funktionen kapseln den DB-Zugriff und die fachlichen Validierungen.
|
||||||
|
Aufgerufen werden sie aus crm/routes.py; die Autorisierungsprüfung erfolgt
|
||||||
|
dort über den Decorator authorisation_required.
|
||||||
|
|
||||||
|
:author: Mistral Vibe <vibe@mistral.ai>
|
||||||
|
:date: 2026-09-04
|
||||||
|
:version: 1.0
|
||||||
|
"""
|
||||||
|
from db import dbapp
|
||||||
|
from crm.model import Kontaktadresse, Projekt
|
||||||
|
|
||||||
|
from auth.model.errors import ApplicationError, HtmlStatusCode
|
||||||
|
|
||||||
|
session = dbapp.session
|
||||||
|
|
||||||
|
|
||||||
|
def get_kontaktadressen(projekt_id: int | None = None):
|
||||||
|
"""
|
||||||
|
Liefert Kontaktadressen, sortiert nach Name.
|
||||||
|
|
||||||
|
Ist projekt_id gesetzt, werden nur die Kontaktadressen dieses
|
||||||
|
Projekts zurückgegeben (serverseitige Filterung). Ohne projekt_id
|
||||||
|
werden alle Kontaktadressen geliefert.
|
||||||
|
"""
|
||||||
|
query = Kontaktadresse.query
|
||||||
|
if projekt_id is not None:
|
||||||
|
query = query.filter(Kontaktadresse.projekt_id == projekt_id)
|
||||||
|
return query.order_by(Kontaktadresse.name).all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_kontaktadresse(kontaktadresse_id: int) -> Kontaktadresse:
|
||||||
|
"""
|
||||||
|
Liefert eine einzelne Kontaktadresse anhand ihrer ID.
|
||||||
|
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
|
||||||
|
"""
|
||||||
|
kontaktadresse = Kontaktadresse.query.get(kontaktadresse_id)
|
||||||
|
if kontaktadresse is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Kontaktadresse mit der ID {} wurde nicht gefunden.".format(kontaktadresse_id),
|
||||||
|
htmlstatus=HtmlStatusCode.Not_Found,
|
||||||
|
)
|
||||||
|
return kontaktadresse
|
||||||
|
|
||||||
|
|
||||||
|
def create_kontaktadresse(data: dict) -> Kontaktadresse:
|
||||||
|
"""
|
||||||
|
Legt eine neue Kontaktadresse an. Die vom Client übergebene ID wird
|
||||||
|
ignoriert; die ID wird serverseitig per Auto-Increment vergeben.
|
||||||
|
|
||||||
|
- data: dict mit den Feldern projekt_id, anrede, name, firma, telefon,
|
||||||
|
email, strasse, plz, ort, land, notiz
|
||||||
|
"""
|
||||||
|
projekt_id = data.get("projekt_id")
|
||||||
|
if projekt_id is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Es muss ein Projekt ausgewählt werden.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
projekt = Projekt.query.get(int(projekt_id))
|
||||||
|
if projekt is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
if not data.get("name") or not str(data["name"]).strip():
|
||||||
|
raise ApplicationError(
|
||||||
|
"Der Name darf nicht leer sein.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
kontaktadresse = Kontaktadresse()
|
||||||
|
kontaktadresse.projekt_id = int(projekt_id)
|
||||||
|
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
|
||||||
|
kontaktadresse.name = data["name"].strip()
|
||||||
|
kontaktadresse.firma = (data.get("firma") or "").strip() or None
|
||||||
|
kontaktadresse.position = (data.get("position") or "").strip() or None
|
||||||
|
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
|
||||||
|
kontaktadresse.email = (data.get("email") or "").strip() or None
|
||||||
|
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
|
||||||
|
kontaktadresse.plz = (data.get("plz") or "").strip() or None
|
||||||
|
kontaktadresse.ort = (data.get("ort") or "").strip() or None
|
||||||
|
kontaktadresse.land = (data.get("land") or "").strip() or None
|
||||||
|
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
|
||||||
|
session.add(kontaktadresse)
|
||||||
|
session.commit()
|
||||||
|
return kontaktadresse
|
||||||
|
|
||||||
|
|
||||||
|
def update_kontaktadresse(data: dict) -> Kontaktadresse:
|
||||||
|
"""
|
||||||
|
Aktualisiert eine bestehende Kontaktadresse. Die ID im data-Dict
|
||||||
|
bestimmt den zu ändernden Datensatz.
|
||||||
|
"""
|
||||||
|
kontaktadresse = get_kontaktadresse(data["id"])
|
||||||
|
projekt_id = data.get("projekt_id")
|
||||||
|
if projekt_id is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Es muss ein Projekt ausgewählt werden.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
projekt = Projekt.query.get(int(projekt_id))
|
||||||
|
if projekt is None:
|
||||||
|
raise ApplicationError(
|
||||||
|
"Das ausgewählte Projekt existiert nicht.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
if not data.get("name") or not str(data["name"]).strip():
|
||||||
|
raise ApplicationError(
|
||||||
|
"Der Name darf nicht leer sein.",
|
||||||
|
htmlstatus=HtmlStatusCode.Bad_Request,
|
||||||
|
)
|
||||||
|
kontaktadresse.projekt_id = int(projekt_id)
|
||||||
|
kontaktadresse.anrede = (data.get("anrede") or "").strip() or None
|
||||||
|
kontaktadresse.name = data["name"].strip()
|
||||||
|
kontaktadresse.firma = (data.get("firma") or "").strip() or None
|
||||||
|
kontaktadresse.position = (data.get("position") or "").strip() or None
|
||||||
|
kontaktadresse.telefon = (data.get("telefon") or "").strip() or None
|
||||||
|
kontaktadresse.email = (data.get("email") or "").strip() or None
|
||||||
|
kontaktadresse.strasse = (data.get("strasse") or "").strip() or None
|
||||||
|
kontaktadresse.plz = (data.get("plz") or "").strip() or None
|
||||||
|
kontaktadresse.ort = (data.get("ort") or "").strip() or None
|
||||||
|
kontaktadresse.land = (data.get("land") or "").strip() or None
|
||||||
|
kontaktadresse.notiz = (data.get("notiz") or "").strip() or None
|
||||||
|
session.commit()
|
||||||
|
return kontaktadresse
|
||||||
|
|
||||||
|
|
||||||
|
def delete_kontaktadresse(kontaktadresse_id: int):
|
||||||
|
"""
|
||||||
|
Löscht eine Kontaktadresse anhand ihrer ID.
|
||||||
|
Wirft ApplicationError (404), falls die Kontaktadresse nicht existiert.
|
||||||
|
"""
|
||||||
|
kontaktadresse = get_kontaktadresse(kontaktadresse_id)
|
||||||
|
session.delete(kontaktadresse)
|
||||||
|
session.commit()
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""
|
||||||
|
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(kunden_id: int | None = None):
|
||||||
|
"""
|
||||||
|
Liefert Projekte, sortiert nach projektname.
|
||||||
|
|
||||||
|
Ist kunden_id gesetzt, werden nur die Projekte dieses Kunden
|
||||||
|
zurückgegeben (serverseitige Filterung). Ohne kunden_id werden
|
||||||
|
alle Projekte geliefert.
|
||||||
|
"""
|
||||||
|
query = Projekt.query
|
||||||
|
if kunden_id is not None:
|
||||||
|
query = query.filter(Projekt.kunden_id == kunden_id)
|
||||||
|
return 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,10 @@ 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
|
||||||
|
from crm.model import kontaktadresse as kontaktadressen
|
||||||
|
from crm.model import call_log as call_logs
|
||||||
|
|
||||||
print("crm-routes")
|
print("crm-routes")
|
||||||
|
|
||||||
@@ -68,4 +72,188 @@ def kunde_delete():
|
|||||||
return jsonify({}), 200
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Kontakt #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/xhr/kontakt/list")
|
||||||
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
|
def kontakt_list():
|
||||||
|
"""Liefert Kontakte als Liste. Optional nach kunden_id gefiltert."""
|
||||||
|
kunden_id = request.args.get("kunden_id", type=int)
|
||||||
|
result = [k.to_dict() for k in kontakte.get_kontakte(kunden_id)]
|
||||||
|
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 Projekte als Liste. Optional nach kunden_id gefiltert."""
|
||||||
|
kunden_id = request.args.get("kunden_id", type=int)
|
||||||
|
result = [p.to_dict() for p in projekte.get_projekte(kunden_id)]
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Kontaktadresse #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/xhr/kontaktadresse/list")
|
||||||
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
|
def kontaktadresse_list():
|
||||||
|
"""Liefert Kontaktadressen als Liste. Optional nach projekt_id gefiltert."""
|
||||||
|
projekt_id = request.args.get("projekt_id", type=int)
|
||||||
|
result = [k.to_dict() for k in kontaktadressen.get_kontaktadressen(projekt_id)]
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/xhr/kontaktadresse/get")
|
||||||
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
|
def kontaktadresse_get():
|
||||||
|
"""Liefert eine einzelne Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||||
|
kontaktadresse = kontaktadressen.get_kontaktadresse(int(request.args.get("id")))
|
||||||
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/xhr/kontaktadresse/create")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def kontaktadresse_create():
|
||||||
|
"""Legt eine neue Kontaktadresse an. ID wird serverseitig vergeben."""
|
||||||
|
kontaktadresse = kontaktadressen.create_kontaktadresse(request.json)
|
||||||
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/xhr/kontaktadresse/update")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def kontaktadresse_update():
|
||||||
|
"""Aktualisiert eine bestehende Kontaktadresse."""
|
||||||
|
kontaktadresse = kontaktadressen.update_kontaktadresse(request.json)
|
||||||
|
return jsonify(kontaktadresse.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/xhr/kontaktadresse/delete")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def kontaktadresse_delete():
|
||||||
|
"""Löscht eine Kontaktadresse anhand ihrer ID (Query-Param id)."""
|
||||||
|
kontaktadressen.delete_kontaktadresse(int(request.args.get("id")))
|
||||||
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# CallLog (Gesprächsnotiz) #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/xhr/calllog/list")
|
||||||
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
|
def calllog_list():
|
||||||
|
"""Liefert Gesprächsnotizen als Liste. Optional nach projekt_id gefiltert."""
|
||||||
|
projekt_id = request.args.get("projekt_id", type=int)
|
||||||
|
result = [l.to_dict() for l in call_logs.get_call_logs(projekt_id)]
|
||||||
|
return jsonify(result), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/xhr/calllog/get")
|
||||||
|
@authorisation_required([MITARBEITER, KUNDEN_MANAGER])
|
||||||
|
def calllog_get():
|
||||||
|
"""Liefert eine einzelne Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||||
|
call_log = call_logs.get_call_log(int(request.args.get("id")))
|
||||||
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/xhr/calllog/create")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def calllog_create():
|
||||||
|
"""Legt eine neue Gesprächsnotiz an. ID wird serverseitig vergeben."""
|
||||||
|
call_log = call_logs.create_call_log(request.json)
|
||||||
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/xhr/calllog/update")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def calllog_update():
|
||||||
|
"""Aktualisiert eine bestehende Gesprächsnotiz."""
|
||||||
|
call_log = call_logs.update_call_log(request.json)
|
||||||
|
return jsonify(call_log.to_dict()), 200
|
||||||
|
|
||||||
|
|
||||||
|
@app.delete("/xhr/calllog/delete")
|
||||||
|
@authorisation_required([KUNDEN_MANAGER])
|
||||||
|
def calllog_delete():
|
||||||
|
"""Löscht eine Gesprächsnotiz anhand ihrer ID (Query-Param id)."""
|
||||||
|
call_logs.delete_call_log(int(request.args.get("id")))
|
||||||
|
return jsonify({}), 200
|
||||||
|
|
||||||
|
|
||||||
print("########## crm.routes importiert")
|
print("########## crm.routes importiert")
|
||||||
+168
-168
@@ -3,27 +3,23 @@ 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 {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 {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log"
|
import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCallLogREST} from "../models/call_log"
|
||||||
import {iKunde, getKunden} from "../models/kunde"
|
import {iKunde, fetchKunden} from "../models/kunde"
|
||||||
import {getProjekteByKundenId, getProjektById} from "../models/projekt"
|
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
||||||
import {getKontaktadressenByProjektId, getKontaktadresseById} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
||||||
|
|
||||||
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
||||||
// 1000 = jede Sekunde, 60000 = jede Minute.
|
|
||||||
const DAUER_UPDATE_INTERVALL = 1000
|
const DAUER_UPDATE_INTERVALL = 1000
|
||||||
|
|
||||||
function createCallLogTreenodes(projektId: number | null = null): iTreenode[] {
|
|
||||||
let filteredLogs = getCallLogsByProjektId(projektId)
|
|
||||||
|
|
||||||
return filteredLogs.map((log: iCallLog) => {
|
|
||||||
return new Treenode(log, true, [])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const call_log_liste: m.ClosureComponent = (vnode) => {
|
export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = createCallLogTreenodes()
|
let treenodes: iTreenode[] = []
|
||||||
let selectedNode: iTreenode | null = null
|
let selectedNode: iTreenode | null = null
|
||||||
let filterProjektId: number | null = null
|
let filterProjektId: number | null = null
|
||||||
|
// Projektname für die Überschrift, asynchron per fetchProjektById
|
||||||
|
// aufgelöst. kunden_name wird ebenfalls aus dem Projekt bezogen,
|
||||||
|
// da es nicht in den Route-Params enthalten ist.
|
||||||
|
let projektName: string = ""
|
||||||
|
let kundenName: string = ""
|
||||||
|
|
||||||
const handleRowMarked = (node: iTreenode) => {
|
const handleRowMarked = (node: iTreenode) => {
|
||||||
selectedNode = node
|
selectedNode = node
|
||||||
@@ -36,56 +32,38 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
const params: any = {
|
const params: any = {
|
||||||
id: -1,
|
id: -1,
|
||||||
datum: datum,
|
datum: datum,
|
||||||
uhrzeitBeginn: uhrzeit,
|
uhrzeit_beginn: uhrzeit,
|
||||||
dauer: "",
|
dauer: "",
|
||||||
betreff: "",
|
betreff: "",
|
||||||
nameAnsprechpartner: "",
|
name_ansprechpartner: "",
|
||||||
telefon: "",
|
telefon: "",
|
||||||
notiz: "",
|
notiz: "",
|
||||||
wiedervorlageDatum: "",
|
wiedervorlage_datum: "",
|
||||||
erledigt: false
|
erledigt: false
|
||||||
}
|
}
|
||||||
// Bei projektgefilterter Liste Kunde und Projekt vorbelegen.
|
|
||||||
if (filterProjektId !== null) {
|
if (filterProjektId !== null) {
|
||||||
const projekt = getProjektById(filterProjektId)
|
params.projekt_id = filterProjektId
|
||||||
if (projekt) {
|
params.projekt_name = projektName
|
||||||
params.kundenName = projekt.kundenName
|
params.kunden_name = kundenName
|
||||||
params.projektId = projekt.id
|
|
||||||
params.projektName = projekt.projektname
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m.route.set("/call_log", params)
|
m.route.set("/call_log", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editCallLog = (log: iCallLog, e: Event) => {
|
const editCallLog = (log: iCallLog, e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
m.route.set("/call_log", {
|
m.route.set("/call_log", {id: log.id})
|
||||||
id: log.id,
|
|
||||||
kundenName: log.kundenName,
|
|
||||||
projektId: log.projektId,
|
|
||||||
projektName: log.projektName,
|
|
||||||
datum: log.datum,
|
|
||||||
uhrzeitBeginn: log.uhrzeitBeginn,
|
|
||||||
dauer: log.dauer,
|
|
||||||
betreff: log.betreff,
|
|
||||||
nameAnsprechpartner: log.nameAnsprechpartner,
|
|
||||||
telefon: log.telefon,
|
|
||||||
notiz: log.notiz,
|
|
||||||
wiedervorlageDatum: log.wiedervorlageDatum,
|
|
||||||
erledigt: log.erledigt
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||||
let log: iCallLog = node.nodevalue
|
let log: iCallLog = node.nodevalue
|
||||||
|
|
||||||
return [
|
return [
|
||||||
m("div", m("strong", log.kundenName + ": " + log.projektName)),
|
m("div", m("strong", log.kunden_name + ": " + log.projekt_name)),
|
||||||
m("span", log.datum),
|
m("span", log.datum),
|
||||||
m("span", log.uhrzeitBeginn),
|
m("span", log.uhrzeit_beginn),
|
||||||
m("span", log.dauer + " min"),
|
m("span", log.dauer + " min"),
|
||||||
m("span", log.betreff),
|
m("span", log.betreff),
|
||||||
m("span", log.wiedervorlageDatum),
|
m("span", log.wiedervorlage_datum),
|
||||||
m("span", log.erledigt ? "Erledigt" : "Offen"),
|
m("span", log.erledigt ? "Erledigt" : "Offen"),
|
||||||
m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit"))
|
m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit"))
|
||||||
]
|
]
|
||||||
@@ -94,25 +72,42 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||||
let log: iCallLog = node.nodevalue
|
let log: iCallLog = node.nodevalue
|
||||||
let searchLower = searchvalue.toLowerCase()
|
let searchLower = searchvalue.toLowerCase()
|
||||||
return log.kundenName.toLowerCase().includes(searchLower) ||
|
return (log.kunden_name || "").toLowerCase().includes(searchLower) ||
|
||||||
log.projektName.toLowerCase().includes(searchLower) ||
|
(log.projekt_name || "").toLowerCase().includes(searchLower) ||
|
||||||
log.datum.includes(searchLower) ||
|
(log.datum || "").includes(searchLower) ||
|
||||||
log.uhrzeitBeginn.toLowerCase().includes(searchLower) ||
|
(log.uhrzeit_beginn || "").toLowerCase().includes(searchLower) ||
|
||||||
log.betreff.toLowerCase().includes(searchLower) ||
|
(log.betreff || "").toLowerCase().includes(searchLower) ||
|
||||||
log.nameAnsprechpartner.toLowerCase().includes(searchLower) ||
|
(log.name_ansprechpartner || "").toLowerCase().includes(searchLower) ||
|
||||||
log.notiz.toLowerCase().includes(searchLower) ||
|
(log.notiz || "").toLowerCase().includes(searchLower) ||
|
||||||
log.wiedervorlageDatum.includes(searchLower)
|
(log.wiedervorlage_datum || "").includes(searchLower)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oninit: (vnode) => {
|
oninit: (vnode) => {
|
||||||
const routeParams = m.route.param()
|
const routeParams = m.route.param()
|
||||||
if (routeParams && routeParams.projektId) {
|
if (routeParams && routeParams.projekt_id) {
|
||||||
filterProjektId = parseInt(routeParams.projektId)
|
filterProjektId = parseInt(routeParams.projekt_id)
|
||||||
|
projektName = routeParams.projekt_name || ""
|
||||||
} else {
|
} else {
|
||||||
filterProjektId = null
|
filterProjektId = null
|
||||||
|
projektName = ""
|
||||||
|
}
|
||||||
|
// Gesprächsnotizen asynchron vom Server laden. Die Filterung
|
||||||
|
// nach projekt_id erfolgt serverseitig, sofern ein Filter
|
||||||
|
// gesetzt ist.
|
||||||
|
fetchCallLogs(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
||||||
|
treenodes = result.map((log: iCallLog) => new Treenode(log, true, []))
|
||||||
|
})
|
||||||
|
// Projektnamen und Kundennamen asynchron auflösen, falls
|
||||||
|
// nicht in den Route-Params enthalten.
|
||||||
|
if (filterProjektId !== null) {
|
||||||
|
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
|
||||||
|
if (projektName === "") {
|
||||||
|
projektName = projekt.projektname
|
||||||
|
}
|
||||||
|
kundenName = projekt.kunden_name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
treenodes = createCallLogTreenodes(filterProjektId)
|
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
@@ -121,7 +116,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
'flex-direction': 'column',
|
'flex-direction': 'column',
|
||||||
height: 'inherit',
|
height: 'inherit',
|
||||||
'overflow-y': 'inherit'}},
|
'overflow-y': 'inherit'}},
|
||||||
m("h1", "Gesprächsnotizen"),
|
m("h1", projektName ? "Gesprächsnotizen für " + projektName : "Gesprächsnotizen"),
|
||||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||||
m(Treetable,
|
m(Treetable,
|
||||||
{
|
{
|
||||||
@@ -142,181 +137,190 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Call_Log: m.ClosureComponent = (vnode) => {
|
export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||||
// currentLog hält den Formularzustand. kundenId ist intern und wird über
|
// currentLog hält den Formularzustand. projekt_id referenziert das
|
||||||
// das Projekt aufgelöst; kundenName/projektName dienen der Anzeige.
|
// Projekt, kunden_name und projekt_name dienen der Anzeige.
|
||||||
let currentLog: {
|
let currentLog: {
|
||||||
id: number
|
id: number
|
||||||
kundenId: number
|
projekt_id: number
|
||||||
kundenName: string
|
projekt_name: string
|
||||||
projektId: number
|
kunden_name: string
|
||||||
projektName: string
|
|
||||||
datum: string
|
datum: string
|
||||||
uhrzeitBeginn: string
|
uhrzeit_beginn: string
|
||||||
dauer: string
|
dauer: string
|
||||||
betreff: string
|
betreff: string
|
||||||
nameAnsprechpartner: string
|
name_ansprechpartner: string
|
||||||
telefon: string
|
telefon: string
|
||||||
notiz: string
|
notiz: string
|
||||||
wiedervorlageDatum: string
|
wiedervorlage_datum: string
|
||||||
erledigt: boolean
|
erledigt: boolean
|
||||||
} = {
|
} = {
|
||||||
id: -1,
|
id: -1,
|
||||||
kundenId: -1,
|
projekt_id: -1,
|
||||||
kundenName: "",
|
projekt_name: "",
|
||||||
projektId: -1,
|
kunden_name: "",
|
||||||
projektName: "",
|
|
||||||
datum: "",
|
datum: "",
|
||||||
uhrzeitBeginn: "",
|
uhrzeit_beginn: "",
|
||||||
dauer: "",
|
dauer: "",
|
||||||
betreff: "",
|
betreff: "",
|
||||||
nameAnsprechpartner: "",
|
name_ansprechpartner: "",
|
||||||
telefon: "",
|
telefon: "",
|
||||||
notiz: "",
|
notiz: "",
|
||||||
wiedervorlageDatum: "",
|
wiedervorlage_datum: "",
|
||||||
erledigt: false
|
erledigt: false
|
||||||
}
|
}
|
||||||
// Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert
|
// Ist das Projekt voreingestellt (Liste wurde projektgefiltert)?
|
||||||
// oder aus der Projektliste aufgerufen)?
|
let projektVoreingestellt: boolean = false
|
||||||
let kundeVoreingestellt: boolean = false
|
|
||||||
// Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)?
|
// Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)?
|
||||||
// Wenn ja, wird ein readonly Input gezeigt; sonst ein Select, das die
|
|
||||||
// Kontaktadressen des gewählten Projekts anbietet.
|
|
||||||
let ansprechpartnerVoreingestellt: boolean = false
|
let ansprechpartnerVoreingestellt: boolean = false
|
||||||
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
|
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
|
||||||
let startZeit: Date | null = null
|
let startZeit: Date | null = null
|
||||||
// Timer-Handle für die Dauer-Aktualisierung.
|
// Timer-Handle für die Dauer-Aktualisierung.
|
||||||
let timerId: number | null = null
|
let timerId: number | null = null
|
||||||
// Direkte DOM-Referenz auf das Dauer-Feld, damit der Timer den Wert
|
// Direkte DOM-Referenz auf das Dauer-Feld.
|
||||||
// ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder
|
|
||||||
// mit den veralteten currentLog-Werten überschreiben).
|
|
||||||
let dauerInput: HTMLInputElement | null = null
|
let dauerInput: HTMLInputElement | null = null
|
||||||
|
// Asynchron geladene Listen für Select-Optionen.
|
||||||
|
let kundenListe: iKunde[] = []
|
||||||
|
let projektListe: iProjekt[] = []
|
||||||
|
let kontaktadressenListe: iKontaktadresse[] = []
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatiert verstrichene Sekunden als mm:ss (Minuten:Sekunden).
|
* Formatiert verstrichene Sekunden als mm:ss.
|
||||||
*/
|
*/
|
||||||
const formatDauer = (sekunden: number): string => {
|
const formatDauer = (sekunden: number): string => {
|
||||||
const m = Math.floor(sekunden / 60)
|
const min = Math.floor(sekunden / 60)
|
||||||
const s = sekunden % 60
|
const sek = sekunden % 60
|
||||||
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
|
return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0')
|
||||||
}
|
}
|
||||||
|
|
||||||
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 projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
|
if (id !== -1) {
|
||||||
kundeVoreingestellt = projektId !== -1
|
// Gesprächsnotiz frisch vom Server holen.
|
||||||
// kundenId aus dem Projekt ableiten, falls nicht direkt gesetzt.
|
fetchCallLogById(id).then((result) => {
|
||||||
let kundenId = -1
|
currentLog = result
|
||||||
if (kundeVoreingestellt) {
|
projektVoreingestellt = true
|
||||||
const projekt = getProjektById(projektId)
|
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||||
if (projekt) { kundenId = projekt.kundenId }
|
})
|
||||||
}
|
} else {
|
||||||
currentLog = {
|
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
|
||||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||||
kundenId: kundenId,
|
currentLog.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||||
kundenName: routeParams.kundenName || "",
|
currentLog.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
|
||||||
projektId: projektId,
|
currentLog.datum = routeParams && routeParams.datum ? routeParams.datum : ""
|
||||||
projektName: routeParams.projektName || "",
|
currentLog.uhrzeit_beginn = routeParams && routeParams.uhrzeit_beginn ? routeParams.uhrzeit_beginn : ""
|
||||||
datum: routeParams.datum || "",
|
currentLog.dauer = routeParams && routeParams.dauer ? routeParams.dauer : ""
|
||||||
uhrzeitBeginn: routeParams.uhrzeitBeginn || "",
|
currentLog.betreff = routeParams && routeParams.betreff ? routeParams.betreff : ""
|
||||||
dauer: routeParams.dauer || "",
|
currentLog.name_ansprechpartner = routeParams && routeParams.name_ansprechpartner ? routeParams.name_ansprechpartner : ""
|
||||||
betreff: routeParams.betreff || "",
|
currentLog.telefon = routeParams && routeParams.telefon ? routeParams.telefon : ""
|
||||||
nameAnsprechpartner: routeParams.nameAnsprechpartner || "",
|
currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
|
||||||
telefon: routeParams.telefon || "",
|
currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
|
||||||
notiz: routeParams.notiz || "",
|
currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
|
||||||
wiedervorlageDatum: routeParams.wiedervorlageDatum || "",
|
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||||
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
|
|
||||||
}
|
|
||||||
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
|
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
|
||||||
ansprechpartnerVoreingestellt = currentLog.nameAnsprechpartner !== ""
|
|
||||||
if (currentLog.id === -1) {
|
|
||||||
startZeit = new Date()
|
startZeit = new Date()
|
||||||
timerId = window.setInterval(() => {
|
timerId = window.setInterval(() => {
|
||||||
if (startZeit) {
|
if (startZeit) {
|
||||||
// Verstrichene Sekunden berechnen; das Intervall
|
|
||||||
// steuert nur die Update-Frequenz.
|
|
||||||
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
|
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
|
||||||
const dauer = formatDauer(sekunden)
|
const dauer = formatDauer(sekunden)
|
||||||
currentLog.dauer = dauer
|
currentLog.dauer = dauer
|
||||||
// Nur das Dauer-Feld direkt im DOM aktualisieren,
|
|
||||||
// ohne redraw (sonst werden andere Felder zurückgesetzt).
|
|
||||||
if (dauerInput) { dauerInput.value = dauer }
|
if (dauerInput) { dauerInput.value = dauer }
|
||||||
}
|
}
|
||||||
}, DAUER_UPDATE_INTERVALL)
|
}, DAUER_UPDATE_INTERVALL)
|
||||||
}
|
}
|
||||||
}
|
// Alle Listen asynchron laden, für Select-Optionen.
|
||||||
|
fetchKunden().then((result) => { kundenListe = result })
|
||||||
|
fetchProjekte().then((result) => { projektListe = result })
|
||||||
|
fetchKontaktadressen().then((result) => { kontaktadressenListe = result })
|
||||||
},
|
},
|
||||||
onremove: (vnode) => {
|
onremove: (vnode) => {
|
||||||
// Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft.
|
|
||||||
if (timerId != null) { window.clearInterval(timerId) }
|
if (timerId != null) { window.clearInterval(timerId) }
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
// Select-Optionen aus Kunden- und Projektliste aufbauen.
|
// kunden_id aus dem geladenen Projekt ableiten (fuer abhaengiges
|
||||||
const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => {
|
// Projekt-Select). Wird nur bei nicht voreingestelltem Projekt
|
||||||
|
// benoetigt.
|
||||||
|
const internKundenId = (() => {
|
||||||
|
if (currentLog.projekt_id !== -1) {
|
||||||
|
const p = projektListe.find((p: iProjekt) => p.id === currentLog.projekt_id)
|
||||||
|
return p ? p.kunden_id : -1
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
})()
|
||||||
|
|
||||||
|
const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => {
|
||||||
return {value: k.id, label: k.name}
|
return {value: k.id, label: k.name}
|
||||||
})
|
})
|
||||||
// Projekt-Optionen sind vom gewählten Kunden abhängig.
|
// Projekt-Optionen sind vom gewählten Kunden abhängig.
|
||||||
const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => {
|
const projektOptionen: iSelectOption[] = projektListe
|
||||||
|
.filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId)
|
||||||
|
.map((p: iProjekt) => {
|
||||||
return {value: p.id, label: p.projektname}
|
return {value: p.id, label: p.projektname}
|
||||||
})
|
})
|
||||||
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
|
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
|
||||||
// Werden nur benötigt, wenn der Ansprechpartner nicht voreingestellt ist.
|
|
||||||
const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt
|
const kontaktadressenOptionen: iSelectOption[] = !ansprechpartnerVoreingestellt
|
||||||
? getKontaktadressenByProjektId(currentLog.projektId).map((ka) => {
|
? kontaktadressenListe
|
||||||
|
.filter((ka: iKontaktadresse) => ka.projekt_id === currentLog.projekt_id)
|
||||||
|
.map((ka: iKontaktadresse) => {
|
||||||
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
|
return {value: ka.id, label: ka.name + (ka.firma ? " (" + ka.firma + ")" : "")}
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
// Einheit der Dauer-Anzeige im Format mm:ss.
|
|
||||||
|
const submit = (form: HTMLFormElement): boolean => {
|
||||||
|
const formData = new FormData(form)
|
||||||
|
const gespeichertesLog: iCallLog = {
|
||||||
|
id: currentLog.id,
|
||||||
|
projekt_id: currentLog.projekt_id,
|
||||||
|
projekt_name: "",
|
||||||
|
kunden_name: "",
|
||||||
|
datum: (formData.get("datum") as string) || "",
|
||||||
|
uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "",
|
||||||
|
dauer: (formData.get("dauer") as string) || "",
|
||||||
|
betreff: (formData.get("betreff") as string) || "",
|
||||||
|
name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "",
|
||||||
|
telefon: (formData.get("telefon") as string) || "",
|
||||||
|
notiz: (formData.get("notiz") as string) || "",
|
||||||
|
wiedervorlage_datum: (formData.get("wiedervorlage_datum") as string) || "",
|
||||||
|
erledigt: formData.get("erledigt") === "on"
|
||||||
|
}
|
||||||
|
if (currentLog.id === -1) {
|
||||||
|
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
||||||
|
} else {
|
||||||
|
updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
const cancel = (e: Event) => {
|
||||||
|
m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})
|
||||||
|
}
|
||||||
return m("div.w3-container.w3-auto.form-view-container",
|
return m("div.w3-container.w3-auto.form-view-container",
|
||||||
m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"),
|
m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"),
|
||||||
// Form füllt den verbleibenden Platz; das Form-Widget selbst
|
|
||||||
// sorgt (mit stickyButtons, Default) dafür, dass nur der
|
|
||||||
// Inhalt scrollt und die Buttons fixiert bleiben.
|
|
||||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||||
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
|
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
|
||||||
|
m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id}),
|
||||||
|
// Kunde und Projekt als Read-Only-Text, da voreingestellt.
|
||||||
m("label", "Kunde",
|
m("label", "Kunde",
|
||||||
m(Select, {
|
m("input.w3-input.w3-border", {
|
||||||
name: "kundenId",
|
type: "text",
|
||||||
value: currentLog.kundenId,
|
value: currentLog.kunden_name,
|
||||||
options: kundenOptionen,
|
readonly: true,
|
||||||
readonly: kundeVoreingestellt,
|
style: {'margin-bottom': '5px'}
|
||||||
required: true,
|
})),
|
||||||
onchanged: (kundenId: number) => {
|
|
||||||
// Bei Kundenwechsel die kundenId übernehmen
|
|
||||||
// und projektId zurücksetzen, da die Projekte
|
|
||||||
// kundenabhängig sind.
|
|
||||||
currentLog.kundenId = kundenId
|
|
||||||
currentLog.projektId = -1
|
|
||||||
}
|
|
||||||
} as iSelect)),
|
|
||||||
m("label", "Projekt",
|
m("label", "Projekt",
|
||||||
m(Select, {
|
m("input.w3-input.w3-border", {
|
||||||
name: "projektId",
|
type: "text",
|
||||||
value: currentLog.projektId,
|
value: currentLog.projekt_name,
|
||||||
options: projektOptionen,
|
readonly: true,
|
||||||
readonly: kundeVoreingestellt,
|
style: {'margin-bottom': '5px'}
|
||||||
required: true,
|
})),
|
||||||
onchanged: (projektId: number) => {
|
|
||||||
currentLog.projektId = projektId
|
|
||||||
// Bei Projektwechsel Ansprechpartner und Telefon
|
|
||||||
// zurücksetzen, falls nicht voreingestellt.
|
|
||||||
if (!ansprechpartnerVoreingestellt) {
|
|
||||||
currentLog.nameAnsprechpartner = ""
|
|
||||||
currentLog.telefon = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} as iSelect)),
|
|
||||||
// Datum, Startzeit und Dauer in einer Zeile.
|
// Datum, Startzeit und Dauer in einer Zeile.
|
||||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||||
m("label", {style: {flex: '1'}}, "Datum",
|
m("label", {style: {flex: '1'}}, "Datum",
|
||||||
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
|
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
|
||||||
m("label", {style: {flex: '1'}}, "Uhrzeit Beginn",
|
m("label", {style: {flex: '1'}}, "Uhrzeit Beginn",
|
||||||
m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)),
|
m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)),
|
||||||
m("label", {style: {flex: '1'}}, "Dauer (mm:ss)",
|
m("label", {style: {flex: '1'}}, "Dauer (mm:ss)",
|
||||||
// Raw input statt Input-Widget, damit oncreate die
|
|
||||||
// DOM-Referenz für den Timer greifen kann. Text-Typ,
|
|
||||||
// da das Format mm:ss keine Zahl ist.
|
|
||||||
m("input.w3-input.w3-border", {
|
m("input.w3-input.w3-border", {
|
||||||
name: "dauer",
|
name: "dauer",
|
||||||
type: "text",
|
type: "text",
|
||||||
@@ -328,22 +332,18 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
),
|
),
|
||||||
m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)),
|
m("label", "Betreff", m(Input, {name: "betreff", value: currentLog.betreff, type: "text"} as iInput)),
|
||||||
// Ansprechpartner und Telefon in einer Zeile.
|
// Ansprechpartner und Telefon in einer Zeile.
|
||||||
// Wenn der Ansprechpartner voreingestellt ist (aus Kontaktadressenliste),
|
|
||||||
// wird ein readonly Input gezeigt; sonst ein Select, das die
|
|
||||||
// Kontaktadressen des gewählten Projekts anbietet. Bei Auswahl
|
|
||||||
// werden Name und Telefon automatisch übernommen.
|
|
||||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||||
m("label", {style: {flex: '1'}}, "Name Ansprechpartner",
|
m("label", {style: {flex: '1'}}, "Name Ansprechpartner",
|
||||||
ansprechpartnerVoreingestellt
|
ansprechpartnerVoreingestellt
|
||||||
? m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text", readonly: true} as iInput)
|
? m(Input, {name: "name_ansprechpartner", value: currentLog.name_ansprechpartner, type: "text", readonly: true} as iInput)
|
||||||
: m(Select, {
|
: m(Select, {
|
||||||
name: "kontaktadresseId",
|
name: "kontaktadresse_id",
|
||||||
value: -1,
|
value: -1,
|
||||||
options: kontaktadressenOptionen,
|
options: kontaktadressenOptionen,
|
||||||
onchanged: (kontaktadresseId: number) => {
|
onchanged: (kontaktadresseId: number) => {
|
||||||
const ka = getKontaktadresseById(kontaktadresseId)
|
const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
|
||||||
if (ka) {
|
if (ka) {
|
||||||
currentLog.nameAnsprechpartner = ka.name
|
currentLog.name_ansprechpartner = ka.name
|
||||||
currentLog.telefon = ka.telefon
|
currentLog.telefon = ka.telefon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +352,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
|||||||
m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput))
|
m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput))
|
||||||
),
|
),
|
||||||
m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)),
|
m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)),
|
||||||
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)),
|
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlage_datum", value: currentLog.wiedervorlage_datum, type: "date"} as iInput)),
|
||||||
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
|
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
|
||||||
])
|
])
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,62 +1,35 @@
|
|||||||
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, getKunden} from "../models/kunde"
|
import {iKunde, fetchKundeById} 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 per fetchKundeById
|
||||||
|
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||||
|
let kundenName: string = ""
|
||||||
|
|
||||||
const handleRowMarked = (node: iTreenode) => {
|
const handleRowMarked = (node: iTreenode) => {
|
||||||
selectedNode = node
|
selectedNode = node
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = getKunden().find((k: iKunde) => k.id === filterKundenId)
|
params.kunden_id = filterKundenId
|
||||||
if (kunde) {
|
params.kunden_name = kundenName
|
||||||
params.kundenId = kunde.id
|
|
||||||
params.kundenname = kunde.name
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m.route.set("/kontakt", params)
|
m.route.set("/kontakt", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
||||||
@@ -67,7 +40,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"))
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -75,22 +48,34 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
|||||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||||
let kontakt: iKontakt = node.nodevalue
|
let kontakt: iKontakt = node.nodevalue
|
||||||
let searchLower = searchvalue.toLowerCase()
|
let searchLower = searchvalue.toLowerCase()
|
||||||
return kontakt.name.toLowerCase().includes(searchLower) ||
|
return (kontakt.name || "").toLowerCase().includes(searchLower) ||
|
||||||
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 = ""
|
||||||
|
}
|
||||||
|
// Kontakte asynchron vom Server laden. Die Filterung nach
|
||||||
|
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
||||||
|
fetchKontakte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
||||||
|
treenodes = result.map((kontakt: iKontakt) => new Treenode(kontakt, true, []))
|
||||||
|
})
|
||||||
|
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
|
if (filterKundenId !== null && kundenName === "") {
|
||||||
|
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
||||||
|
kundenName = kunde.name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
treenodes = createKontaktTreenodes(filterKundenId)
|
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
@@ -99,7 +84,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,
|
||||||
{
|
{
|
||||||
@@ -120,12 +105,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
|
||||||
@@ -134,8 +119,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: "",
|
||||||
@@ -143,38 +128,31 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
|||||||
email: "",
|
email: "",
|
||||||
notiz: ""
|
notiz: ""
|
||||||
}
|
}
|
||||||
// Ist der Kunde voreingestellt (gefilterte Kontaktliste)?
|
|
||||||
let kundeVoreingestellt: boolean = false
|
|
||||||
|
|
||||||
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 || ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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 = getKunden().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) || "",
|
||||||
@@ -183,33 +161,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[] = getKunden().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,70 +1,34 @@
|
|||||||
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 {iKontaktadresse, getKontaktadressenByProjektId, getNewKontaktadresseId, addKontaktadresse, updateKontaktadresse} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
|
||||||
import {iProjekt, getProjekte, getProjektById} from "../models/projekt"
|
import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||||
|
|
||||||
function createKontaktadresseTreenodes(projektId: number | null = null): iTreenode[] {
|
|
||||||
let filteredKontaktadressen = getKontaktadressenByProjektId(projektId)
|
|
||||||
|
|
||||||
return filteredKontaktadressen.map((kontaktadresse: iKontaktadresse) => {
|
|
||||||
return new Treenode(kontaktadresse, true, [])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = createKontaktadresseTreenodes()
|
let treenodes: iTreenode[] = []
|
||||||
let selectedNode: iTreenode | null = null
|
let selectedNode: iTreenode | null = null
|
||||||
let filterProjektId: number | null = null
|
let filterProjektId: number | null = null
|
||||||
|
// Projektname für die Überschrift, asynchron per fetchProjektById
|
||||||
|
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||||
|
let projektName: string = ""
|
||||||
|
|
||||||
const handleRowMarked = (node: iTreenode) => {
|
const handleRowMarked = (node: iTreenode) => {
|
||||||
selectedNode = node
|
selectedNode = node
|
||||||
}
|
}
|
||||||
|
|
||||||
const addKontaktadresseRoute = () => {
|
const addKontaktadresseRoute = () => {
|
||||||
const params: any = {
|
const params: any = {id: -1}
|
||||||
id: -1,
|
|
||||||
anrede: "",
|
|
||||||
name: "",
|
|
||||||
firma: "",
|
|
||||||
telefon: "",
|
|
||||||
email: "",
|
|
||||||
strasse: "",
|
|
||||||
plz: "",
|
|
||||||
ort: "",
|
|
||||||
land: "",
|
|
||||||
notiz: ""
|
|
||||||
}
|
|
||||||
// Nur bei projektgefilterter Liste das Projekt vorbelegen.
|
|
||||||
if (filterProjektId !== null) {
|
if (filterProjektId !== null) {
|
||||||
const projekt = getProjekte().find((p: iProjekt) => p.id === filterProjektId)
|
params.projekt_id = filterProjektId
|
||||||
if (projekt) {
|
params.projekt_name = projektName
|
||||||
params.projektId = projekt.id
|
|
||||||
params.projektname = projekt.projektname
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
m.route.set("/kontaktadresse", params)
|
m.route.set("/kontaktadresse", params)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => {
|
const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
m.route.set("/kontaktadresse", {
|
m.route.set("/kontaktadresse", {id: kontaktadresse.id})
|
||||||
id: kontaktadresse.id,
|
|
||||||
projektId: kontaktadresse.projektId,
|
|
||||||
projektname: kontaktadresse.projektName,
|
|
||||||
anrede: kontaktadresse.anrede,
|
|
||||||
name: kontaktadresse.name,
|
|
||||||
firma: kontaktadresse.firma,
|
|
||||||
telefon: kontaktadresse.telefon,
|
|
||||||
email: kontaktadresse.email,
|
|
||||||
strasse: kontaktadresse.strasse,
|
|
||||||
plz: kontaktadresse.plz,
|
|
||||||
ort: kontaktadresse.ort,
|
|
||||||
land: kontaktadresse.land,
|
|
||||||
notiz: kontaktadresse.notiz
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
|
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
|
||||||
@@ -72,21 +36,18 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const datum = now.toISOString().split('T')[0]
|
const datum = now.toISOString().split('T')[0]
|
||||||
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
|
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
|
||||||
// Kundennamen über das Projekt auflösen.
|
|
||||||
const projekt = getProjektById(kontaktadresse.projektId)
|
|
||||||
m.route.set("/call_log", {
|
m.route.set("/call_log", {
|
||||||
id: -1,
|
id: -1,
|
||||||
kundenName: projekt ? projekt.kundenName : "",
|
projekt_id: kontaktadresse.projekt_id,
|
||||||
projektId: kontaktadresse.projektId,
|
projekt_name: kontaktadresse.projekt_name,
|
||||||
projektName: kontaktadresse.projektName,
|
|
||||||
datum: datum,
|
datum: datum,
|
||||||
uhrzeitBeginn: uhrzeit,
|
uhrzeit_beginn: uhrzeit,
|
||||||
dauer: "",
|
dauer: "",
|
||||||
betreff: "",
|
betreff: "",
|
||||||
nameAnsprechpartner: kontaktadresse.name,
|
name_ansprechpartner: kontaktadresse.name,
|
||||||
telefon: kontaktadresse.telefon,
|
telefon: kontaktadresse.telefon,
|
||||||
notiz: "",
|
notiz: "",
|
||||||
wiedervorlageDatum: "",
|
wiedervorlage_datum: "",
|
||||||
erledigt: false
|
erledigt: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -97,10 +58,10 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
return [
|
return [
|
||||||
m("div", m("strong", kontaktadresse.name)),
|
m("div", m("strong", kontaktadresse.name)),
|
||||||
m("span", kontaktadresse.firma),
|
m("span", kontaktadresse.firma),
|
||||||
|
m("span", kontaktadresse.position),
|
||||||
m("span", kontaktadresse.telefon),
|
m("span", kontaktadresse.telefon),
|
||||||
m("span", kontaktadresse.email),
|
m("span", kontaktadresse.email),
|
||||||
m("span", kontaktadresse.ort),
|
m("span", kontaktadresse.ort),
|
||||||
m("span", kontaktadresse.projektName),
|
|
||||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
|
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
|
||||||
m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontaktadresse(kontaktadresse, e)}, m("i.material-icons", "edit")),
|
m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontaktadresse(kontaktadresse, e)}, m("i.material-icons", "edit")),
|
||||||
m("button.w3-button.w3-small", {title: "Gesprächsnotiz hinzufügen", style: {padding: '0 2px'}, onclick: (e: Event) => addCallLog(kontaktadresse, e)}, m("i.material-icons", "add_call"))
|
m("button.w3-button.w3-small", {title: "Gesprächsnotiz hinzufügen", style: {padding: '0 2px'}, onclick: (e: Event) => addCallLog(kontaktadresse, e)}, m("i.material-icons", "add_call"))
|
||||||
@@ -111,23 +72,37 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||||
let kontaktadresse: iKontaktadresse = node.nodevalue
|
let kontaktadresse: iKontaktadresse = node.nodevalue
|
||||||
let searchLower = searchvalue.toLowerCase()
|
let searchLower = searchvalue.toLowerCase()
|
||||||
return kontaktadresse.name.toLowerCase().includes(searchLower) ||
|
return (kontaktadresse.name || "").toLowerCase().includes(searchLower) ||
|
||||||
kontaktadresse.firma.toLowerCase().includes(searchLower) ||
|
(kontaktadresse.firma || "").toLowerCase().includes(searchLower) ||
|
||||||
kontaktadresse.telefon.toLowerCase().includes(searchLower) ||
|
(kontaktadresse.position || "").toLowerCase().includes(searchLower) ||
|
||||||
kontaktadresse.email.toLowerCase().includes(searchLower) ||
|
(kontaktadresse.telefon || "").toLowerCase().includes(searchLower) ||
|
||||||
kontaktadresse.ort.toLowerCase().includes(searchLower) ||
|
(kontaktadresse.email || "").toLowerCase().includes(searchLower) ||
|
||||||
kontaktadresse.projektName.toLowerCase().includes(searchLower)
|
(kontaktadresse.ort || "").toLowerCase().includes(searchLower) ||
|
||||||
|
(kontaktadresse.projekt_name || "").toLowerCase().includes(searchLower)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
oninit: (vnode) => {
|
oninit: (vnode) => {
|
||||||
const routeParams = m.route.param()
|
const routeParams = m.route.param()
|
||||||
if (routeParams && routeParams.projektId) {
|
if (routeParams && routeParams.projekt_id) {
|
||||||
filterProjektId = parseInt(routeParams.projektId)
|
filterProjektId = parseInt(routeParams.projekt_id)
|
||||||
|
projektName = routeParams.projekt_name || ""
|
||||||
} else {
|
} else {
|
||||||
filterProjektId = null
|
filterProjektId = null
|
||||||
|
projektName = ""
|
||||||
|
}
|
||||||
|
// Kontaktadressen asynchron vom Server laden. Die Filterung
|
||||||
|
// nach projekt_id erfolgt serverseitig, sofern ein Filter
|
||||||
|
// gesetzt ist.
|
||||||
|
fetchKontaktadressen(filterProjektId !== null ? filterProjektId : undefined).then((result) => {
|
||||||
|
treenodes = result.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
|
||||||
|
})
|
||||||
|
// Projektnamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
|
if (filterProjektId !== null && projektName === "") {
|
||||||
|
fetchProjektById(filterProjektId).then((projekt: iProjekt) => {
|
||||||
|
projektName = projekt.projektname
|
||||||
|
})
|
||||||
}
|
}
|
||||||
treenodes = createKontaktadresseTreenodes(filterProjektId)
|
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
@@ -136,7 +111,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
'flex-direction': 'column',
|
'flex-direction': 'column',
|
||||||
height: 'inherit',
|
height: 'inherit',
|
||||||
'overflow-y': 'inherit'}},
|
'overflow-y': 'inherit'}},
|
||||||
m("h1", "Kontaktadressen"),
|
m("h1", projektName ? "Kontaktadressen für " + projektName : "Kontaktadressen"),
|
||||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||||
m(Treetable,
|
m(Treetable,
|
||||||
{
|
{
|
||||||
@@ -157,15 +132,16 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||||
// currentKontaktadresse hält den Formularzustand. projektId referenziert das
|
// currentKontaktadresse hält den Formularzustand. projekt_id referenziert
|
||||||
// Projekt eindeutig, projektname dient der Anzeige.
|
// das Projekt eindeutig, projekt_name dient der Anzeige in der Überschrift.
|
||||||
let currentKontaktadresse: {
|
let currentKontaktadresse: {
|
||||||
id: number
|
id: number
|
||||||
projektId: number
|
projekt_id: number
|
||||||
projektname: string
|
projekt_name: string
|
||||||
anrede: string
|
anrede: string
|
||||||
name: string
|
name: string
|
||||||
firma: string
|
firma: string
|
||||||
|
position: string
|
||||||
telefon: string
|
telefon: string
|
||||||
email: string
|
email: string
|
||||||
strasse: string
|
strasse: string
|
||||||
@@ -175,11 +151,12 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
notiz: string
|
notiz: string
|
||||||
} = {
|
} = {
|
||||||
id: -1,
|
id: -1,
|
||||||
projektId: -1,
|
projekt_id: -1,
|
||||||
projektname: "",
|
projekt_name: "",
|
||||||
anrede: "",
|
anrede: "",
|
||||||
name: "",
|
name: "",
|
||||||
firma: "",
|
firma: "",
|
||||||
|
position: "",
|
||||||
telefon: "",
|
telefon: "",
|
||||||
email: "",
|
email: "",
|
||||||
strasse: "",
|
strasse: "",
|
||||||
@@ -188,45 +165,33 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
land: "",
|
land: "",
|
||||||
notiz: ""
|
notiz: ""
|
||||||
}
|
}
|
||||||
// Ist das Projekt voreingestellt (gefilterte Kontaktadressenliste)?
|
|
||||||
let projektVoreingestellt: boolean = false
|
|
||||||
|
|
||||||
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 projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
|
if (id !== -1) {
|
||||||
projektVoreingestellt = projektId !== -1
|
// Kontaktadresse frisch vom Server holen.
|
||||||
currentKontaktadresse = {
|
fetchKontaktadresseById(id).then((result) => {
|
||||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
currentKontaktadresse = result
|
||||||
projektId: projektId,
|
})
|
||||||
projektname: routeParams.projektname || "",
|
} else {
|
||||||
anrede: routeParams.anrede || "",
|
// Neuanlegen: projekt_id und projekt_name aus Route-Params.
|
||||||
name: routeParams.name || "",
|
currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||||
firma: routeParams.firma || "",
|
currentKontaktadresse.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||||
telefon: routeParams.telefon || "",
|
|
||||||
email: routeParams.email || "",
|
|
||||||
strasse: routeParams.strasse || "",
|
|
||||||
plz: routeParams.plz || "",
|
|
||||||
ort: routeParams.ort || "",
|
|
||||||
land: routeParams.land || "",
|
|
||||||
notiz: routeParams.notiz || ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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 projektId = parseInt(formData.get("projektId") as string)
|
|
||||||
// Projektnamen zur gewählten projektId auflösen.
|
|
||||||
const projekt = getProjekte().find((p: iProjekt) => p.id === projektId)
|
|
||||||
const gespeicherteKontaktadresse: iKontaktadresse = {
|
const gespeicherteKontaktadresse: iKontaktadresse = {
|
||||||
id: currentKontaktadresse.id,
|
id: currentKontaktadresse.id,
|
||||||
projektId: projektId,
|
projekt_id: currentKontaktadresse.projekt_id,
|
||||||
projektName: projekt ? projekt.projektname : "",
|
projekt_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) || "",
|
||||||
firma: (formData.get("firma") as string) || "",
|
firma: (formData.get("firma") as string) || "",
|
||||||
|
position: (formData.get("position") as string) || "",
|
||||||
telefon: (formData.get("telefon") as string) || "",
|
telefon: (formData.get("telefon") as string) || "",
|
||||||
email: (formData.get("email") as string) || "",
|
email: (formData.get("email") as string) || "",
|
||||||
strasse: (formData.get("strasse") as string) || "",
|
strasse: (formData.get("strasse") as string) || "",
|
||||||
@@ -236,40 +201,29 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
notiz: (formData.get("notiz") as string) || ""
|
notiz: (formData.get("notiz") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentKontaktadresse.id === -1) {
|
if (currentKontaktadresse.id === -1) {
|
||||||
gespeicherteKontaktadresse.id = getNewKontaktadresseId()
|
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
||||||
addKontaktadresse(gespeicherteKontaktadresse)
|
|
||||||
} else {
|
} else {
|
||||||
updateKontaktadresse(gespeicherteKontaktadresse)
|
updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
||||||
}
|
}
|
||||||
m.route.set("/kontaktadresseliste")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const cancel = (e: Event) => {
|
const cancel = (e: Event) => {
|
||||||
m.route.set("/kontaktadresseliste")
|
m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})
|
||||||
}
|
}
|
||||||
// Select-Optionen aus der Projektliste aufbauen.
|
|
||||||
const projektOptionen: iSelectOption[] = getProjekte().map((p: iProjekt) => {
|
|
||||||
return {value: p.id, label: p.projektname}
|
|
||||||
})
|
|
||||||
return m("div.w3-container.w3-auto.form-view-container",
|
return m("div.w3-container.w3-auto.form-view-container",
|
||||||
m("h1", currentKontaktadresse.id === -1 ? "Neue Kontaktadresse anlegen" : "Kontaktadresse-Stammblatt"),
|
m("h1", currentKontaktadresse.id === -1
|
||||||
|
? "Neue Kontaktadresse für " + currentKontaktadresse.projekt_name
|
||||||
|
: "Kontaktadresse-Stammblatt"),
|
||||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
m("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", "Projekt",
|
m("input", {type: "hidden", name: "projekt_id", value: currentKontaktadresse.projekt_id}),
|
||||||
m(Select, {
|
|
||||||
name: "projektId",
|
|
||||||
value: currentKontaktadresse.projektId,
|
|
||||||
options: projektOptionen,
|
|
||||||
readonly: projektVoreingestellt,
|
|
||||||
required: true
|
|
||||||
} as iSelect)),
|
|
||||||
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontaktadresse.anrede, type: "text"} as iInput)),
|
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontaktadresse.anrede, type: "text"} as iInput)),
|
||||||
m("label", "Name", m(Input, {name: "name", value: currentKontaktadresse.name, type: "text", required: true} as iInput)),
|
m("label", "Name", m(Input, {name: "name", value: currentKontaktadresse.name, type: "text", required: true} as iInput)),
|
||||||
m("label", "Firma", m(Input, {name: "firma", value: currentKontaktadresse.firma, type: "text"} as iInput)),
|
m("label", "Firma", m(Input, {name: "firma", value: currentKontaktadresse.firma, type: "text"} as iInput)),
|
||||||
|
m("label", "Position", m(Input, {name: "position", value: currentKontaktadresse.position, type: "text"} as iInput)),
|
||||||
m("label", "Telefon", m(Input, {name: "telefon", value: currentKontaktadresse.telefon, type: "tel"} as iInput)),
|
m("label", "Telefon", m(Input, {name: "telefon", value: currentKontaktadresse.telefon, type: "tel"} as iInput)),
|
||||||
m("label", "E-Mail", m(Input, {name: "email", value: currentKontaktadresse.email, type: "email"} as iInput)),
|
m("label", "E-Mail", m(Input, {name: "email", value: currentKontaktadresse.email, type: "email"} as iInput)),
|
||||||
m("label", "Straße", m(Input, {name: "strasse", value: currentKontaktadresse.strasse, type: "text"} as iInput)),
|
m("label", "Straße", m(Input, {name: "strasse", value: currentKontaktadresse.strasse, type: "text"} as iInput)),
|
||||||
// PLZ und Ort in einer Zeile.
|
|
||||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||||
m("label", {style: {flex: '0 0 120px'}}, "PLZ",
|
m("label", {style: {flex: '0 0 120px'}}, "PLZ",
|
||||||
m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)),
|
m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)),
|
||||||
|
|||||||
+28
-37
@@ -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, kundenDaten, getNewKundeId, addKunde, updateKunde} 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) => {
|
||||||
@@ -40,12 +38,10 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
|||||||
land: (formData.get("land") as string) || ""
|
land: (formData.get("land") as string) || ""
|
||||||
}
|
}
|
||||||
if (currentKunde.id === -1) {
|
if (currentKunde.id === -1) {
|
||||||
gespeicherterKunde.id = getNewKundeId()
|
createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
||||||
addKunde(gespeicherterKunde)
|
|
||||||
} else {
|
} else {
|
||||||
updateKunde(gespeicherterKunde)
|
updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
||||||
}
|
}
|
||||||
m.route.set("/kundenliste")
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const cancel = (e: Event) => {
|
const cancel = (e: Event) => {
|
||||||
@@ -56,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
|
||||||
),
|
),
|
||||||
@@ -74,20 +70,8 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Erzeugt aus der Kundendatenliste die Treenode-Struktur für die Treetable der
|
|
||||||
* Kundenliste. Jeder Kunde wird zu einem expandierten Treenode ohne Kinder
|
|
||||||
* (expand=true, children=[]) - d.h. ein Blattknoten, der sofort in der
|
|
||||||
* Tabelle sichtbar ist und sich nicht weiter aufklappen lässt.
|
|
||||||
*/
|
|
||||||
function createKundenTreenodes(): iTreenode[] {
|
|
||||||
return kundenDaten.map((kunde: iKunde) => {
|
|
||||||
return new Treenode(kunde, true, [])
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const kundenliste: m.ClosureComponent = (vnode) => {
|
export const kundenliste: m.ClosureComponent = (vnode) => {
|
||||||
let treenodes: iTreenode[] = createKundenTreenodes()
|
let treenodes: iTreenode[] = []
|
||||||
let selectedNode: iTreenode | null = null
|
let selectedNode: iTreenode | null = null
|
||||||
|
|
||||||
const handleRowMarked = (node: iTreenode) => {
|
const handleRowMarked = (node: iTreenode) => {
|
||||||
@@ -96,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) => {
|
||||||
@@ -133,14 +117,21 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
|
|||||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||||
let kunde: iKunde = node.nodevalue
|
let kunde: iKunde = node.nodevalue
|
||||||
let searchLower = searchvalue.toLowerCase()
|
let searchLower = searchvalue.toLowerCase()
|
||||||
return kunde.name.toLowerCase().includes(searchLower) ||
|
return (kunde.name || "").toLowerCase().includes(searchLower) ||
|
||||||
kunde.strasse.toLowerCase().includes(searchLower) ||
|
(kunde.strasse || "").toLowerCase().includes(searchLower) ||
|
||||||
kunde.plz.includes(searchLower) ||
|
(kunde.plz || "").includes(searchLower) ||
|
||||||
kunde.ort.toLowerCase().includes(searchLower) ||
|
(kunde.ort || "").toLowerCase().includes(searchLower) ||
|
||||||
kunde.land.toLowerCase().includes(searchLower)
|
(kunde.land || "").toLowerCase().includes(searchLower)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
oninit: (vnode) => {
|
||||||
|
// Kundenliste asynchron vom Server laden. m.request löst nach
|
||||||
|
// Promise-Auflösung automatisch ein Redraw aus.
|
||||||
|
fetchKunden().then((kunden: iKunde[]) => {
|
||||||
|
treenodes = kunden.map((kunde: iKunde) => new Treenode(kunde, true, []))
|
||||||
|
})
|
||||||
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
style: {
|
style: {
|
||||||
|
|||||||
@@ -1,69 +1,45 @@
|
|||||||
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, getKunden} from "../models/kunde"
|
import {iKunde, fetchKundeById} 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 per fetchKundeById
|
||||||
|
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||||
|
let kundenName: string = ""
|
||||||
|
|
||||||
const handleRowMarked = (node: iTreenode) => {
|
const handleRowMarked = (node: iTreenode) => {
|
||||||
selectedNode = node
|
selectedNode = node
|
||||||
}
|
}
|
||||||
|
|
||||||
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 = getKunden().find((k: iKunde) => k.id === filterKundenId)
|
params.kunden_id = filterKundenId
|
||||||
if (kunde) {
|
params.kunden_name = kundenName
|
||||||
params.kundenId = kunde.id
|
|
||||||
params.kundenname = 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", {projekt_id: projekt.id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const showKontaktadressen = (projekt: iProjekt, e: Event) => {
|
const showKontaktadressen = (projekt: iProjekt, e: Event) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
m.route.set("/kontaktadresseliste", {projektId: projekt.id})
|
m.route.set("/kontaktadresseliste", {projekt_id: projekt.id})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCallLog = (projekt: iProjekt, e: Event) => {
|
const addCallLog = (projekt: iProjekt, e: Event) => {
|
||||||
@@ -73,17 +49,17 @@ 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,
|
kunden_name: projekt.kunden_name,
|
||||||
projektId: projekt.id,
|
projekt_id: projekt.id,
|
||||||
projektName: projekt.projektname,
|
projekt_name: projekt.projektname,
|
||||||
datum: datum,
|
datum: datum,
|
||||||
uhrzeitBeginn: uhrzeit,
|
uhrzeit_beginn: uhrzeit,
|
||||||
dauer: "",
|
dauer: "",
|
||||||
betreff: "",
|
betreff: "",
|
||||||
nameAnsprechpartner: "",
|
name_ansprechpartner: "",
|
||||||
telefon: "",
|
telefon: "",
|
||||||
notiz: "",
|
notiz: "",
|
||||||
wiedervorlageDatum: "",
|
wiedervorlage_datum: "",
|
||||||
erledigt: false
|
erledigt: false
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -92,7 +68,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),
|
||||||
@@ -109,22 +85,34 @@ 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 = ""
|
||||||
|
}
|
||||||
|
// Projekte asynchron vom Server laden. Die Filterung nach
|
||||||
|
// kunden_id erfolgt serverseitig, sofern ein Filter gesetzt ist.
|
||||||
|
fetchProjekte(filterKundenId !== null ? filterKundenId : undefined).then((result) => {
|
||||||
|
treenodes = result.map((projekt: iProjekt) => new Treenode(projekt, true, []))
|
||||||
|
})
|
||||||
|
// Kundennamen asynchron auflösen, falls nicht in Route-Params.
|
||||||
|
if (filterKundenId !== null && kundenName === "") {
|
||||||
|
fetchKundeById(filterKundenId).then((kunde: iKunde) => {
|
||||||
|
kundenName = kunde.name
|
||||||
|
})
|
||||||
}
|
}
|
||||||
treenodes = createProjektTreenodes(filterKundenId)
|
|
||||||
},
|
},
|
||||||
view: (vnode) => {
|
view: (vnode) => {
|
||||||
return m("div.w3-container.w3-auto", {
|
return m("div.w3-container.w3-auto", {
|
||||||
@@ -133,7 +121,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,
|
||||||
{
|
{
|
||||||
@@ -154,95 +142,79 @@ 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
|
|
||||||
|
|
||||||
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 : ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
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 = getKunden().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[] = getKunden().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", {projekt_id: currentProjekt.id})},
|
||||||
m("i.material-icons", "contact_page"), " Kontaktadressen")
|
m("i.material-icons", "contact_page"), " Kontaktadressen")
|
||||||
: null
|
: null
|
||||||
),
|
),
|
||||||
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)),
|
||||||
|
|||||||
+20
-67
@@ -2,86 +2,31 @@ import m from "mithril"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Datenmodell für eine Gesprächsnotiz (Call-Log).
|
* Datenmodell für eine Gesprächsnotiz (Call-Log).
|
||||||
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
|
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
|
||||||
|
* serverseitig aus der Projekt-Relationship abgeleitet und im JSON ergänzt.
|
||||||
*/
|
*/
|
||||||
export interface iCallLog {
|
export interface iCallLog {
|
||||||
id: number
|
id: number
|
||||||
kundenName: string
|
projekt_id: number
|
||||||
projektId: number
|
projekt_name: string
|
||||||
projektName: string
|
kunden_name: string
|
||||||
datum: string
|
datum: string
|
||||||
uhrzeitBeginn: string
|
uhrzeit_beginn: string
|
||||||
dauer: string
|
dauer: string
|
||||||
betreff: string
|
betreff: string
|
||||||
nameAnsprechpartner: string
|
name_ansprechpartner: string
|
||||||
telefon: string
|
telefon: string
|
||||||
notiz: string
|
notiz: string
|
||||||
wiedervorlageDatum: string
|
wiedervorlage_datum: string
|
||||||
erledigt: boolean
|
erledigt: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* In-Memory-Datenspeicher für Gesprächsnotizen. Wird als Fallback solange
|
|
||||||
* verwendet, bis die serverseitigen REST-Endpunkte verfügbar sind.
|
|
||||||
*/
|
|
||||||
export const callLogDaten: iCallLog[] = [
|
|
||||||
{id: 1, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-20", uhrzeitBeginn: "09:30", dauer: "45", betreff: "Anforderungen klären", nameAnsprechpartner: "Herr Schmidt", telefon: "+49 123 45678", notiz: "Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.", wiedervorlageDatum: "2024-01-27", erledigt: false},
|
|
||||||
{id: 2, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-25", uhrzeitBeginn: "14:00", dauer: "30", betreff: "Feedback zum Design", nameAnsprechpartner: "Frau Müller", telefon: "+49 123 45679", notiz: "Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.", wiedervorlageDatum: "2024-02-01", erledigt: false},
|
|
||||||
{id: 3, kundenName: "Beispiel AG", projektId: 2, projektName: "E-Commerce Plattform", datum: "2024-02-10", uhrzeitBeginn: "10:00", dauer: "60", betreff: "Technische Anforderungen", nameAnsprechpartner: "Herr Weber", telefon: "+49 123 45680", notiz: "Payment-Gateway und Versand-Schnittstellen müssen integriert werden. Zeitplan besprochen.", wiedervorlageDatum: "2024-02-15", erledigt: true},
|
|
||||||
{id: 4, kundenName: "Beispiel AG", projektId: 3, projektName: "CRM-System", datum: "2024-02-18", uhrzeitBeginn: "11:15", dauer: "20", betreff: "Schulungstermin", nameAnsprechpartner: "Frau Bauer", telefon: "+49 123 45681", notiz: "Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.", wiedervorlageDatum: "2024-02-20", erledigt: false},
|
|
||||||
{id: 5, kundenName: "Test Unternehmen", projektId: 4, projektName: "Mobile App", datum: "2024-03-05", uhrzeitBeginn: "15:45", dauer: "35", betreff: "Feature-Request", nameAnsprechpartner: "Herr König", telefon: "+49 123 45682", notiz: "Kunde wünscht Push-Benachrichtigungen und Offline-Modus.", wiedervorlageDatum: "2024-03-12", erledigt: false}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die nächste freie Call-Log-ID auf Basis des In-Memory-Speichers.
|
|
||||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
|
||||||
*/
|
|
||||||
export function getNewCallLogId(): number {
|
|
||||||
if (callLogDaten.length === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return Math.max(...callLogDaten.map((log: iCallLog) => log.id)) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fügt eine neue Gesprächsnotiz dem In-Memory-Speicher hinzu.
|
|
||||||
*/
|
|
||||||
export function addCallLogData(log: iCallLog): void {
|
|
||||||
callLogDaten.push(log)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert eine bestehende Gesprächsnotiz im In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function updateCallLogData(log: iCallLog): void {
|
|
||||||
const index = callLogDaten.findIndex((l: iCallLog) => l.id === log.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
callLogDaten[index] = log
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert eine Kopie aller Gesprächsnotizen aus dem In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function getCallLogs(): iCallLog[] {
|
|
||||||
return [...callLogDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die Gesprächsnotizen eines bestimmten Projekts. Ist projektId null,
|
|
||||||
* werden alle Gesprächsnotizen zurückgegeben.
|
|
||||||
*/
|
|
||||||
export function getCallLogsByProjektId(projektId: number | null): iCallLog[] {
|
|
||||||
return projektId
|
|
||||||
? callLogDaten.filter((l: iCallLog) => l.projektId === projektId)
|
|
||||||
: [...callLogDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* REST-API-Aufrufe (Client-Seite) */
|
/* 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/calllog/*). */
|
||||||
/* implementiert und werden später nachgereicht. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,11 +36,19 @@ const CALLLOG_REST_BASE = "/xhr/calllog"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt die Liste der Gesprächsnotizen vom Server.
|
* Lädt die Liste der Gesprächsnotizen vom Server.
|
||||||
|
* Ist projektId gesetzt (>= 0), werden nur die Notizen dieses Projekts
|
||||||
|
* geliefert (serverseitige Filterung). Ohne Parameter werden alle
|
||||||
|
* Notizen geliefert.
|
||||||
*/
|
*/
|
||||||
export function fetchCallLogs(): Promise<iCallLog[]> {
|
export function fetchCallLogs(projektId?: number): Promise<iCallLog[]> {
|
||||||
|
const params: Record<string, any> = {}
|
||||||
|
if (projektId !== undefined && projektId >= 0) {
|
||||||
|
params.projekt_id = projektId
|
||||||
|
}
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: CALLLOG_REST_BASE + "/list"
|
url: CALLLOG_REST_BASE + "/list",
|
||||||
|
params: params
|
||||||
}).then((result) => result as iCallLog[])
|
}).then((result) => result as iCallLog[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-70
@@ -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. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -94,11 +33,19 @@ const KONTAKT_REST_BASE = "/xhr/kontakt"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt die Kontaktliste vom Server.
|
* Lädt die Kontaktliste vom Server.
|
||||||
|
* Ist kundenId gesetzt (>= 0), werden nur die Kontakte dieses Kunden
|
||||||
|
* geliefert (serverseitige Filterung). Ohne Parameter werden alle
|
||||||
|
* Kontakte geliefert.
|
||||||
*/
|
*/
|
||||||
export function fetchKontakte(): Promise<iKontakt[]> {
|
export function fetchKontakte(kundenId?: number): Promise<iKontakt[]> {
|
||||||
|
const params: Record<string, any> = {}
|
||||||
|
if (kundenId !== undefined && kundenId >= 0) {
|
||||||
|
params.kunden_id = kundenId
|
||||||
|
}
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: KONTAKT_REST_BASE + "/list"
|
url: KONTAKT_REST_BASE + "/list",
|
||||||
|
params: params
|
||||||
}).then((result) => result as iKontakt[])
|
}).then((result) => result as iKontakt[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,15 +2,19 @@ import m from "mithril"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
|
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
|
||||||
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projektId).
|
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
||||||
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
|
* projekt_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||||
|
* Projekt-Relationship abgeleitet und im JSON ergänzt.
|
||||||
*/
|
*/
|
||||||
export interface iKontaktadresse {
|
export interface iKontaktadresse {
|
||||||
id: number
|
id: number
|
||||||
projektId: number
|
projekt_id: number
|
||||||
projektName: string
|
projekt_name: string
|
||||||
anrede: string
|
anrede: string
|
||||||
name: string
|
name: string
|
||||||
firma: string
|
firma: string
|
||||||
|
position: string
|
||||||
telefon: string
|
telefon: string
|
||||||
email: string
|
email: string
|
||||||
strasse: string
|
strasse: string
|
||||||
@@ -20,75 +24,11 @@ export interface iKontaktadresse {
|
|||||||
notiz: string
|
notiz: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* In-Memory-Datenspeicher für Kontaktadressen. Wird als Fallback solange verwendet,
|
|
||||||
* bis die serverseitigen REST-Endpunkte verfügbar sind.
|
|
||||||
*/
|
|
||||||
export const kontaktadresseDaten: iKontaktadresse[] = [
|
|
||||||
{id: 1, projektId: 1, projektName: "Website-Relaunch", anrede: "Herr", name: "Thomas Becker", firma: "Becker Webdesign GmbH", telefon: "+49 201 123456", email: "t.becker@becker-webdesign.de", strasse: "Industriestr. 12", plz: "45131", ort: "Essen", land: "Deutschland", notiz: "Interessent für Website-Relaunch, Erstkontakt auf Messe."},
|
|
||||||
{id: 2, projektId: 1, projektName: "Website-Relaunch", anrede: "Frau", name: "Sandra Klein", firma: "Klein Marketing", telefon: "+49 211 987654", email: "s.klein@klein-marketing.de", strasse: "Königsallee 5", plz: "40212", ort: "Düsseldorf", land: "Deutschland", notiz: ""},
|
|
||||||
{id: 3, projektId: 2, projektName: "E-Commerce Plattform", anrede: "Herr", name: "Michael Wagner", firma: "Wagner Online Shop", telefon: "+49 221 555123", email: "m.wagner@wagner-shop.de", strasse: "Hauptstr. 88", plz: "50667", ort: "Köln", land: "Deutschland", notiz: "Sucht E-Commerce-Lösung, Budget geklärt."},
|
|
||||||
{id: 4, projektId: 3, projektName: "CRM-System", anrede: "Frau", name: "Julia Hoffmann", firma: "Hoffmann Consulting", telefon: "+49 89 444333", email: "j.hoffmann@hoffmann-consulting.de", strasse: "Marienplatz 3", plz: "80331", ort: "München", land: "Deutschland", notiz: "Wiedervorlage im nächsten Quartal."}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die nächste freie Kontaktadressen-ID auf Basis des In-Memory-Speichers.
|
|
||||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
|
||||||
*/
|
|
||||||
export function getNewKontaktadresseId(): number {
|
|
||||||
if (kontaktadresseDaten.length === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return Math.max(...kontaktadresseDaten.map((ka: iKontaktadresse) => ka.id)) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fügt eine neue Kontaktadresse dem In-Memory-Speicher hinzu.
|
|
||||||
*/
|
|
||||||
export function addKontaktadresse(kontaktadresse: iKontaktadresse): void {
|
|
||||||
kontaktadresseDaten.push(kontaktadresse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert eine bestehende Kontaktadresse im In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function updateKontaktadresse(kontaktadresse: iKontaktadresse): void {
|
|
||||||
const index = kontaktadresseDaten.findIndex((k: iKontaktadresse) => k.id === kontaktadresse.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
kontaktadresseDaten[index] = kontaktadresse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert eine Kopie aller Kontaktadressen aus dem In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function getKontaktadressen(): iKontaktadresse[] {
|
|
||||||
return [...kontaktadresseDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die Kontaktadressen eines bestimmten Projekts. Ist projektId null,
|
|
||||||
* werden alle Kontaktadressen zurückgegeben.
|
|
||||||
*/
|
|
||||||
export function getKontaktadressenByProjektId(projektId: number | null): iKontaktadresse[] {
|
|
||||||
return projektId
|
|
||||||
? kontaktadresseDaten.filter((k: iKontaktadresse) => k.projektId === projektId)
|
|
||||||
: [...kontaktadresseDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert eine einzelne Kontaktadresse anhand ihrer ID oder undefined.
|
|
||||||
*/
|
|
||||||
export function getKontaktadresseById(id: number): iKontaktadresse | undefined {
|
|
||||||
return kontaktadresseDaten.find((k: iKontaktadresse) => k.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* REST-API-Aufrufe (Client-Seite) */
|
/* 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/kontaktadresse/*). */
|
||||||
/* implementiert und werden später nachgereicht. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,11 +38,19 @@ const KONTAKTADRESSE_REST_BASE = "/xhr/kontaktadresse"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt die Kontaktadressenliste vom Server.
|
* Lädt die Kontaktadressenliste vom Server.
|
||||||
|
* Ist projektId gesetzt (>= 0), werden nur die Kontaktadressen dieses
|
||||||
|
* Projekts geliefert (serverseitige Filterung). Ohne Parameter werden
|
||||||
|
* alle Kontaktadressen geliefert.
|
||||||
*/
|
*/
|
||||||
export function fetchKontaktadressen(): Promise<iKontaktadresse[]> {
|
export function fetchKontaktadressen(projektId?: number): Promise<iKontaktadresse[]> {
|
||||||
|
const params: Record<string, any> = {}
|
||||||
|
if (projektId !== undefined && projektId >= 0) {
|
||||||
|
params.projekt_id = projektId
|
||||||
|
}
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: KONTAKTADRESSE_REST_BASE + "/list"
|
url: KONTAKTADRESSE_REST_BASE + "/list",
|
||||||
|
params: params
|
||||||
}).then((result) => result as iKontaktadresse[])
|
}).then((result) => result as iKontaktadresse[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-48
@@ -12,57 +12,11 @@ export interface iKunde {
|
|||||||
land: string
|
land: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* In-Memory-Datenspeicher für Kunden. Wird als Fallback solange verwendet,
|
|
||||||
* bis die serverseitigen REST-Endpunkte verfügbar sind.
|
|
||||||
*/
|
|
||||||
export const kundenDaten: iKunde[] = [
|
|
||||||
{id: 1, name: "Musterfirma GmbH", strasse: "Musterstraße 1", plz: "12345", ort: "Musterstadt", land: "Deutschland"},
|
|
||||||
{id: 2, name: "Beispiel AG", strasse: "Beispielweg 2", plz: "54321", ort: "Beispielstadt", land: "Deutschland"},
|
|
||||||
{id: 3, name: "Test Unternehmen", strasse: "Testallee 3", plz: "67890", ort: "Testdorf", land: "Österreich"}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die nächste freie Kunden-ID auf Basis des In-Memory-Speichers.
|
|
||||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
|
||||||
*/
|
|
||||||
export function getNewKundeId(): number {
|
|
||||||
if (kundenDaten.length === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return Math.max(...kundenDaten.map((kunde: iKunde) => kunde.id)) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fügt einen neuen Kunden dem In-Memory-Speicher hinzu.
|
|
||||||
*/
|
|
||||||
export function addKunde(kunde: iKunde): void {
|
|
||||||
kundenDaten.push(kunde)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert einen bestehenden Kunden im In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function updateKunde(kunde: iKunde): void {
|
|
||||||
const index = kundenDaten.findIndex((k: iKunde) => k.id === kunde.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
kundenDaten[index] = kunde
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert eine Kopie aller Kunden aus dem In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function getKunden(): iKunde[] {
|
|
||||||
return [...kundenDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* 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/kunde/*). */
|
||||||
/* implementiert und werden später nachgereicht. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,7 +46,8 @@ export function fetchKundeById(id: number): Promise<iKunde> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Legt einen neuen Kunden serverseitig an.
|
* Legt einen neuen Kunden serverseitig an. Die ID wird vom Server
|
||||||
|
* vergeben und im zurückgegebenen Kundenobjekt gesetzt.
|
||||||
*/
|
*/
|
||||||
export function createKundeREST(kunde: iKunde): Promise<iKunde> {
|
export function createKundeREST(kunde: iKunde): Promise<iKunde> {
|
||||||
return m.request({
|
return m.request({
|
||||||
|
|||||||
+16
-69
@@ -2,86 +2,25 @@ 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
|
||||||
startdatum: string
|
startdatum: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* In-Memory-Datenspeicher für Projekte. Wird als Fallback solange verwendet,
|
|
||||||
* bis die serverseitigen REST-Endpunkte verfügbar sind.
|
|
||||||
*/
|
|
||||||
export const projektDaten: iProjekt[] = [
|
|
||||||
{id: 1, kundenId: 1, kundenName: "Musterfirma GmbH", projektname: "Website-Relaunch", zweck: "Neugestaltung der Unternehmenswebsite", verantwortlicher: "Max Mustermann", startdatum: "2024-01-15"},
|
|
||||||
{id: 2, kundenId: 1, kundenName: "Musterfirma GmbH", projektname: "E-Commerce Plattform", zweck: "Online-Shop Entwicklung", verantwortlicher: "Anna Schmidt", startdatum: "2024-03-01"},
|
|
||||||
{id: 3, kundenId: 2, kundenName: "Beispiel AG", projektname: "CRM-System", zweck: "Kundenbeziehungsmanagement", verantwortlicher: "Peter Müller", startdatum: "2024-02-20"},
|
|
||||||
{id: 4, kundenId: 3, kundenName: "Test Unternehmen", projektname: "Mobile App", zweck: "iOS und Android Entwicklung", verantwortlicher: "Lisa Bauer", startdatum: "2024-04-10"}
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die nächste freie Projekt-ID auf Basis des In-Memory-Speichers.
|
|
||||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
|
||||||
*/
|
|
||||||
export function getNewProjektId(): number {
|
|
||||||
if (projektDaten.length === 0) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return Math.max(...projektDaten.map((projekt: iProjekt) => projekt.id)) + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fügt ein neues Projekt dem In-Memory-Speicher hinzu.
|
|
||||||
*/
|
|
||||||
export function addProjekt(projekt: iProjekt): void {
|
|
||||||
projektDaten.push(projekt)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktualisiert ein bestehendes Projekt im In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function updateProjekt(projekt: iProjekt): void {
|
|
||||||
const index = projektDaten.findIndex((p: iProjekt) => p.id === projekt.id)
|
|
||||||
if (index !== -1) {
|
|
||||||
projektDaten[index] = projekt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert eine Kopie aller Projekte aus dem In-Memory-Speicher.
|
|
||||||
*/
|
|
||||||
export function getProjekte(): iProjekt[] {
|
|
||||||
return [...projektDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert die Projekte eines bestimmten Kunden. Ist kundenId null, werden
|
|
||||||
* alle Projekte zurückgegeben.
|
|
||||||
*/
|
|
||||||
export function getProjekteByKundenId(kundenId: number | null): iProjekt[] {
|
|
||||||
return kundenId
|
|
||||||
? projektDaten.filter((p: iProjekt) => p.kundenId === kundenId)
|
|
||||||
: [...projektDaten]
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Liefert ein einzelnes Projekt anhand seiner ID oder undefined.
|
|
||||||
*/
|
|
||||||
export function getProjektById(id: number): iProjekt | undefined {
|
|
||||||
return projektDaten.find((p: iProjekt) => p.id === id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
/* REST-API-Aufrufe (Client-Seite) */
|
/* 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. */
|
|
||||||
/* ------------------------------------------------------------------ */
|
/* ------------------------------------------------------------------ */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,11 +30,19 @@ const PROJEKT_REST_BASE = "/xhr/projekt"
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt die Projektliste vom Server.
|
* Lädt die Projektliste vom Server.
|
||||||
|
* Ist kundenId gesetzt (>= 0), werden nur die Projekte dieses Kunden
|
||||||
|
* geliefert (serverseitige Filterung). Ohne Parameter werden alle
|
||||||
|
* Projekte geliefert.
|
||||||
*/
|
*/
|
||||||
export function fetchProjekte(): Promise<iProjekt[]> {
|
export function fetchProjekte(kundenId?: number): Promise<iProjekt[]> {
|
||||||
|
const params: Record<string, any> = {}
|
||||||
|
if (kundenId !== undefined && kundenId >= 0) {
|
||||||
|
params.kunden_id = kundenId
|
||||||
|
}
|
||||||
return m.request({
|
return m.request({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: PROJEKT_REST_BASE + "/list"
|
url: PROJEKT_REST_BASE + "/list",
|
||||||
|
params: params
|
||||||
}).then((result) => result as iProjekt[])
|
}).then((result) => result as iProjekt[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user