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",
|
||||
"group": "none",
|
||||
"label": "Open auth.db",
|
||||
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe auth.db",
|
||||
"command": "sqlite3 auth.db",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/App_Server/data"
|
||||
},
|
||||
@@ -85,7 +85,7 @@
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"label": "Open app.db",
|
||||
"command": "C:/Daten-GGG/OneDrive/GGG-Workspace/Projekte/Sqlite3/sqlite3.exe kbp_crm.db",
|
||||
"command": "sqlite3 kbp_crm.db",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/App_Server/data"
|
||||
},
|
||||
@@ -98,6 +98,24 @@
|
||||
"showReuseMessage": true,
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "shell",
|
||||
"group": "none",
|
||||
"label": "Flask: init-db",
|
||||
"command": "${workspaceFolder}/env/bin/flask --app root/wsgi:app init-db",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/App_Server"
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared",
|
||||
"showReuseMessage": false,
|
||||
"clear": false
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -3,7 +3,7 @@ Beachte die strukturellen Vorgaben, die in ai-context/AI-operative-Vorgaben.md d
|
||||
In den Programmen sollen aussagekräftige Kommentare in deutsch erstellt werden.
|
||||
|
||||
## Vorgaben git Versionsverwaltung
|
||||
Ein neuer Branch darf nur auf ausdrückliche Anweisung hin erstellt werden. Ohne diese Anwendung werden Änderungen immer in main gemacht.
|
||||
Ein neuer Branch darf nur auf ausdrückliche Anweisung hin erstellt werden. Ohne diese Anweisung werden Änderungen immer in main gemacht.
|
||||
|
||||
## Vorgaben für die Typescript und mithriljs Entwicklung
|
||||
Verwende bei der GUI primar die widgets, die es in client/Components/widgets gibt. Fehler widgets, gib eines Hinweis und erstelle einen Vorschlag für ein entsprechendes neues Widget.
|
||||
@@ -28,6 +28,8 @@ m("div.w3-bar-block.w3-border", {
|
||||
Für Symbole auf Buttons und ähnlichen Steuerelementen verwende Symbole aus den Google Material Icons.
|
||||
Erstelle die Datenmodelle für die Typescript-Entwicklung im Verzeichnis Client/Models. Erstelle für die verschiedenen Datenmodelle (Kunde, Projekt usw.) eigene .ts-Dateien. Erstelle in der jeweiligen Datei auch den Aufruf der REST-Api für die Server-Seite.
|
||||
|
||||
Beim Aufruf einer Bearbeitungsmaske werden in den Route-Params nur die ID (und ggf. zum Vorbelegen notwendige Eltern-FKs wie `kunden_id`) übergeben — nicht die vollständigen Felder des Objekts. Die Maske lädt die Daten frisch vom Server via `fetchXxxById(id)` im `oninit`. So wird sichergestellt, dass die Daten aktuell sind und zwischenzeitliche Änderungen anderer Nutzer nicht mit veralteten Werten aus der Liste überschrieben werden. Beim Neuanlegen (`id === -1`) entfällt der Fetch; vorbelegte Werte (z.B. `kunden_id`) kommen aus den Route-Params.
|
||||
|
||||
Bei `m.ClosureComponent` den Closure-Parameter weglassen (`() =>`) und Attribute ausschließlich über das `vnode` des jeweiligen Lifecycle-Hooks (`view`, `oninit`, `onupdate` etc.) lesen. Das `vnode` der Closure-Funktion ist ein Snapshot zum Erzeugungszeitpunkt und wird bei Redraws nicht aktualisiert — daraus gelesene `attrs` sind veraltet. Initialisierung, die `attrs` braucht, gehört nach `oninit`. Beispiel:
|
||||
```typescript
|
||||
export const Select: m.ClosureComponent<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
|
||||
|
||||
## 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
|
||||
crm_checkdb.checkupdates()
|
||||
_seed_muster_kunden()
|
||||
_seed_muster_projekte()
|
||||
_seed_muster_kontaktadressen()
|
||||
_seed_muster_calllogs()
|
||||
|
||||
click.echo("Fertig.")
|
||||
|
||||
@@ -102,6 +105,163 @@ def _seed_muster_kunden():
|
||||
session.commit()
|
||||
|
||||
|
||||
def _seed_muster_projekte():
|
||||
"""
|
||||
Legt für jeden Muster-Kunden 1 bis 3 Muster-Projekte an, falls die
|
||||
projekte-Tabelle noch leer ist.
|
||||
"""
|
||||
import random
|
||||
|
||||
from crm.model import Kunde, Projekt
|
||||
|
||||
session = dbapp.session
|
||||
if session.query(Projekt).count() > 0:
|
||||
return
|
||||
|
||||
kunden = session.query(Kunde).order_by(Kunde.id).all()
|
||||
vorlagen = [
|
||||
("Website-Relaunch", "Neugestaltung der Unternehmenswebsite"),
|
||||
("E-Commerce Plattform", "Online-Shop Entwicklung"),
|
||||
("CRM-System", "Kundenbeziehungsmanagement"),
|
||||
("Mobile App", "iOS- und Android-Entwicklung"),
|
||||
("SEO-Optimierung", "Suchmaschinenoptimierung"),
|
||||
]
|
||||
verantwortliche = [
|
||||
"Max Mustermann", "Anna Schmidt", "Peter Müller",
|
||||
"Lisa Bauer", "Thomas Becker",
|
||||
]
|
||||
rng = random.Random(42)
|
||||
vorlagen_idx = 0
|
||||
for kunde in kunden:
|
||||
anzahl = rng.randint(1, 3)
|
||||
for _ in range(anzahl):
|
||||
name, zweck = vorlagen[vorlagen_idx % len(vorlagen)]
|
||||
vorlagen_idx += 1
|
||||
projekt = Projekt()
|
||||
projekt.kunden_id = kunde.id
|
||||
projekt.projektname = name
|
||||
projekt.zweck = zweck
|
||||
projekt.verantwortlicher = rng.choice(verantwortliche)
|
||||
projekt.startdatum = "2024-0{monat}-1{tag}".format(
|
||||
monat=rng.randint(1, 9), tag=rng.randint(0, 9)
|
||||
)
|
||||
session.add(projekt)
|
||||
session.commit()
|
||||
|
||||
|
||||
def _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():
|
||||
"""
|
||||
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 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
|
||||
|
||||
@@ -93,4 +94,152 @@ class Kunde(dbapp.Model):
|
||||
return columns2dict(self)
|
||||
|
||||
|
||||
class Kontakt(dbapp.Model):
|
||||
"""
|
||||
Ansprechpartner bei einem Kunden. Jeder Kontakt ist genau einem Kunden
|
||||
zugeordnet (kunden_id). Der Kundenname (kunden_name) ist keine eigene
|
||||
Spalte, sondern wird über die Relationship zu Kunde abgeleitet und nur
|
||||
in to_dict() für die JSON-Serialisierung ergänzt.
|
||||
"""
|
||||
__tablename__ = 'kontakte'
|
||||
__bind_key__ = BINDKEY
|
||||
__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")
|
||||
@@ -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()
|
||||
|
||||
|
||||
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():
|
||||
"""
|
||||
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 crm.model import KUNDEN_MANAGER, MITARBEITER
|
||||
from crm.model import kunde as kunden
|
||||
from crm.model import kontakt as kontakte
|
||||
from crm.model import projekt as projekte
|
||||
from crm.model import kontaktadresse as kontaktadressen
|
||||
from crm.model import call_log as call_logs
|
||||
|
||||
print("crm-routes")
|
||||
|
||||
@@ -68,4 +72,188 @@ def kunde_delete():
|
||||
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")
|
||||
+168
-168
@@ -3,27 +3,23 @@ import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
|
||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {iCallLog, getCallLogsByProjektId, getNewCallLogId, addCallLogData, updateCallLogData} from "../models/call_log"
|
||||
import {iKunde, getKunden} from "../models/kunde"
|
||||
import {getProjekteByKundenId, getProjektById} from "../models/projekt"
|
||||
import {getKontaktadressenByProjektId, getKontaktadresseById} from "../models/kontaktadresse"
|
||||
import {iCallLog, fetchCallLogs, fetchCallLogById, createCallLogREST, updateCallLogREST} from "../models/call_log"
|
||||
import {iKunde, fetchKunden} from "../models/kunde"
|
||||
import {iProjekt, fetchProjekte, fetchProjektById} from "../models/projekt"
|
||||
import {iKontaktadresse, fetchKontaktadressen} from "../models/kontaktadresse"
|
||||
|
||||
// Aktualisierungsintervall für die Dauer-Anzeige in Millisekunden.
|
||||
// 1000 = jede Sekunde, 60000 = jede Minute.
|
||||
const DAUER_UPDATE_INTERVALL = 1000
|
||||
|
||||
function createCallLogTreenodes(projektId: number | null = null): iTreenode[] {
|
||||
let filteredLogs = getCallLogsByProjektId(projektId)
|
||||
|
||||
return filteredLogs.map((log: iCallLog) => {
|
||||
return new Treenode(log, true, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
let treenodes: iTreenode[] = createCallLogTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let filterProjektId: number | null = null
|
||||
// Projektname für die Überschrift, asynchron 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) => {
|
||||
selectedNode = node
|
||||
@@ -36,56 +32,38 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
const params: any = {
|
||||
id: -1,
|
||||
datum: datum,
|
||||
uhrzeitBeginn: uhrzeit,
|
||||
uhrzeit_beginn: uhrzeit,
|
||||
dauer: "",
|
||||
betreff: "",
|
||||
nameAnsprechpartner: "",
|
||||
name_ansprechpartner: "",
|
||||
telefon: "",
|
||||
notiz: "",
|
||||
wiedervorlageDatum: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false
|
||||
}
|
||||
// Bei projektgefilterter Liste Kunde und Projekt vorbelegen.
|
||||
if (filterProjektId !== null) {
|
||||
const projekt = getProjektById(filterProjektId)
|
||||
if (projekt) {
|
||||
params.kundenName = projekt.kundenName
|
||||
params.projektId = projekt.id
|
||||
params.projektName = projekt.projektname
|
||||
}
|
||||
params.projekt_id = filterProjektId
|
||||
params.projekt_name = projektName
|
||||
params.kunden_name = kundenName
|
||||
}
|
||||
m.route.set("/call_log", params)
|
||||
}
|
||||
|
||||
const editCallLog = (log: iCallLog, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/call_log", {
|
||||
id: log.id,
|
||||
kundenName: log.kundenName,
|
||||
projektId: log.projektId,
|
||||
projektName: log.projektName,
|
||||
datum: log.datum,
|
||||
uhrzeitBeginn: log.uhrzeitBeginn,
|
||||
dauer: log.dauer,
|
||||
betreff: log.betreff,
|
||||
nameAnsprechpartner: log.nameAnsprechpartner,
|
||||
telefon: log.telefon,
|
||||
notiz: log.notiz,
|
||||
wiedervorlageDatum: log.wiedervorlageDatum,
|
||||
erledigt: log.erledigt
|
||||
})
|
||||
m.route.set("/call_log", {id: log.id})
|
||||
}
|
||||
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
let log: iCallLog = node.nodevalue
|
||||
|
||||
return [
|
||||
m("div", m("strong", log.kundenName + ": " + log.projektName)),
|
||||
m("div", m("strong", log.kunden_name + ": " + log.projekt_name)),
|
||||
m("span", log.datum),
|
||||
m("span", log.uhrzeitBeginn),
|
||||
m("span", log.uhrzeit_beginn),
|
||||
m("span", log.dauer + " min"),
|
||||
m("span", log.betreff),
|
||||
m("span", log.wiedervorlageDatum),
|
||||
m("span", log.wiedervorlage_datum),
|
||||
m("span", log.erledigt ? "Erledigt" : "Offen"),
|
||||
m("button.w3-button.w3-small", {title: "Gesprächsnotiz bearbeiten", style: {padding: '0 2px'}, onclick: (e: Event) => editCallLog(log, e)}, m("i.material-icons", "edit"))
|
||||
]
|
||||
@@ -94,25 +72,42 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let log: iCallLog = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return log.kundenName.toLowerCase().includes(searchLower) ||
|
||||
log.projektName.toLowerCase().includes(searchLower) ||
|
||||
log.datum.includes(searchLower) ||
|
||||
log.uhrzeitBeginn.toLowerCase().includes(searchLower) ||
|
||||
log.betreff.toLowerCase().includes(searchLower) ||
|
||||
log.nameAnsprechpartner.toLowerCase().includes(searchLower) ||
|
||||
log.notiz.toLowerCase().includes(searchLower) ||
|
||||
log.wiedervorlageDatum.includes(searchLower)
|
||||
return (log.kunden_name || "").toLowerCase().includes(searchLower) ||
|
||||
(log.projekt_name || "").toLowerCase().includes(searchLower) ||
|
||||
(log.datum || "").includes(searchLower) ||
|
||||
(log.uhrzeit_beginn || "").toLowerCase().includes(searchLower) ||
|
||||
(log.betreff || "").toLowerCase().includes(searchLower) ||
|
||||
(log.name_ansprechpartner || "").toLowerCase().includes(searchLower) ||
|
||||
(log.notiz || "").toLowerCase().includes(searchLower) ||
|
||||
(log.wiedervorlage_datum || "").includes(searchLower)
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams && routeParams.projektId) {
|
||||
filterProjektId = parseInt(routeParams.projektId)
|
||||
if (routeParams && routeParams.projekt_id) {
|
||||
filterProjektId = parseInt(routeParams.projekt_id)
|
||||
projektName = routeParams.projekt_name || ""
|
||||
} else {
|
||||
filterProjektId = null
|
||||
projektName = ""
|
||||
}
|
||||
// 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) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
@@ -121,7 +116,7 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'}},
|
||||
m("h1", "Gesprächsnotizen"),
|
||||
m("h1", projektName ? "Gesprächsnotizen für " + projektName : "Gesprächsnotizen"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
@@ -142,181 +137,190 @@ export const call_log_liste: m.ClosureComponent = (vnode) => {
|
||||
}
|
||||
|
||||
export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
// currentLog hält den Formularzustand. kundenId ist intern und wird über
|
||||
// das Projekt aufgelöst; kundenName/projektName dienen der Anzeige.
|
||||
// currentLog hält den Formularzustand. projekt_id referenziert das
|
||||
// Projekt, kunden_name und projekt_name dienen der Anzeige.
|
||||
let currentLog: {
|
||||
id: number
|
||||
kundenId: number
|
||||
kundenName: string
|
||||
projektId: number
|
||||
projektName: string
|
||||
projekt_id: number
|
||||
projekt_name: string
|
||||
kunden_name: string
|
||||
datum: string
|
||||
uhrzeitBeginn: string
|
||||
uhrzeit_beginn: string
|
||||
dauer: string
|
||||
betreff: string
|
||||
nameAnsprechpartner: string
|
||||
name_ansprechpartner: string
|
||||
telefon: string
|
||||
notiz: string
|
||||
wiedervorlageDatum: string
|
||||
wiedervorlage_datum: string
|
||||
erledigt: boolean
|
||||
} = {
|
||||
id: -1,
|
||||
kundenId: -1,
|
||||
kundenName: "",
|
||||
projektId: -1,
|
||||
projektName: "",
|
||||
projekt_id: -1,
|
||||
projekt_name: "",
|
||||
kunden_name: "",
|
||||
datum: "",
|
||||
uhrzeitBeginn: "",
|
||||
uhrzeit_beginn: "",
|
||||
dauer: "",
|
||||
betreff: "",
|
||||
nameAnsprechpartner: "",
|
||||
name_ansprechpartner: "",
|
||||
telefon: "",
|
||||
notiz: "",
|
||||
wiedervorlageDatum: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false
|
||||
}
|
||||
// Sind Kunde und Projekt voreingestellt (Liste wurde projektgefiltert
|
||||
// oder aus der Projektliste aufgerufen)?
|
||||
let kundeVoreingestellt: boolean = false
|
||||
// Ist das Projekt voreingestellt (Liste wurde projektgefiltert)?
|
||||
let projektVoreingestellt: boolean = false
|
||||
// Ist der Ansprechpartner voreingestellt (aus Kontaktadressenliste)?
|
||||
// Wenn ja, wird ein readonly Input gezeigt; sonst ein Select, das die
|
||||
// Kontaktadressen des gewählten Projekts anbietet.
|
||||
let ansprechpartnerVoreingestellt: boolean = false
|
||||
// Startzeit für die Dauer-Messung (nur bei Neuanlage gesetzt).
|
||||
let startZeit: Date | null = null
|
||||
// Timer-Handle für die Dauer-Aktualisierung.
|
||||
let timerId: number | null = null
|
||||
// Direkte DOM-Referenz auf das Dauer-Feld, damit der Timer den Wert
|
||||
// ohne Redraw aktualisieren kann (ein Redraw würde die übrigen Felder
|
||||
// mit den veralteten currentLog-Werten überschreiben).
|
||||
// Direkte DOM-Referenz auf das Dauer-Feld.
|
||||
let dauerInput: HTMLInputElement | null = null
|
||||
// Asynchron geladene Listen für Select-Optionen.
|
||||
let kundenListe: iKunde[] = []
|
||||
let projektListe: iProjekt[] = []
|
||||
let kontaktadressenListe: iKontaktadresse[] = []
|
||||
|
||||
/**
|
||||
* Formatiert verstrichene Sekunden als mm:ss (Minuten:Sekunden).
|
||||
* Formatiert verstrichene Sekunden als mm:ss.
|
||||
*/
|
||||
const formatDauer = (sekunden: number): string => {
|
||||
const m = Math.floor(sekunden / 60)
|
||||
const s = sekunden % 60
|
||||
return m.toString().padStart(2, '0') + ':' + s.toString().padStart(2, '0')
|
||||
const min = Math.floor(sekunden / 60)
|
||||
const sek = sekunden % 60
|
||||
return min.toString().padStart(2, '0') + ':' + sek.toString().padStart(2, '0')
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams) {
|
||||
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
|
||||
kundeVoreingestellt = projektId !== -1
|
||||
// kundenId aus dem Projekt ableiten, falls nicht direkt gesetzt.
|
||||
let kundenId = -1
|
||||
if (kundeVoreingestellt) {
|
||||
const projekt = getProjektById(projektId)
|
||||
if (projekt) { kundenId = projekt.kundenId }
|
||||
}
|
||||
currentLog = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
kundenId: kundenId,
|
||||
kundenName: routeParams.kundenName || "",
|
||||
projektId: projektId,
|
||||
projektName: routeParams.projektName || "",
|
||||
datum: routeParams.datum || "",
|
||||
uhrzeitBeginn: routeParams.uhrzeitBeginn || "",
|
||||
dauer: routeParams.dauer || "",
|
||||
betreff: routeParams.betreff || "",
|
||||
nameAnsprechpartner: routeParams.nameAnsprechpartner || "",
|
||||
telefon: routeParams.telefon || "",
|
||||
notiz: routeParams.notiz || "",
|
||||
wiedervorlageDatum: routeParams.wiedervorlageDatum || "",
|
||||
erledigt: routeParams.erledigt === true || routeParams.erledigt === "true"
|
||||
}
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
|
||||
if (id !== -1) {
|
||||
// Gesprächsnotiz frisch vom Server holen.
|
||||
fetchCallLogById(id).then((result) => {
|
||||
currentLog = result
|
||||
projektVoreingestellt = true
|
||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||
})
|
||||
} else {
|
||||
projektVoreingestellt = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) !== -1 : false
|
||||
currentLog.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||
currentLog.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||
currentLog.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
|
||||
currentLog.datum = routeParams && routeParams.datum ? routeParams.datum : ""
|
||||
currentLog.uhrzeit_beginn = routeParams && routeParams.uhrzeit_beginn ? routeParams.uhrzeit_beginn : ""
|
||||
currentLog.dauer = routeParams && routeParams.dauer ? routeParams.dauer : ""
|
||||
currentLog.betreff = routeParams && routeParams.betreff ? routeParams.betreff : ""
|
||||
currentLog.name_ansprechpartner = routeParams && routeParams.name_ansprechpartner ? routeParams.name_ansprechpartner : ""
|
||||
currentLog.telefon = routeParams && routeParams.telefon ? routeParams.telefon : ""
|
||||
currentLog.notiz = routeParams && routeParams.notiz ? routeParams.notiz : ""
|
||||
currentLog.wiedervorlage_datum = routeParams && routeParams.wiedervorlage_datum ? routeParams.wiedervorlage_datum : ""
|
||||
currentLog.erledigt = routeParams && (routeParams.erledigt === true || routeParams.erledigt === "true")
|
||||
ansprechpartnerVoreingestellt = currentLog.name_ansprechpartner !== ""
|
||||
// Bei Neuanlage Startzeit merken und Dauer-Timer starten.
|
||||
ansprechpartnerVoreingestellt = currentLog.nameAnsprechpartner !== ""
|
||||
if (currentLog.id === -1) {
|
||||
startZeit = new Date()
|
||||
timerId = window.setInterval(() => {
|
||||
if (startZeit) {
|
||||
// Verstrichene Sekunden berechnen; das Intervall
|
||||
// steuert nur die Update-Frequenz.
|
||||
const sekunden = Math.floor((Date.now() - startZeit.getTime()) / 1000)
|
||||
const dauer = formatDauer(sekunden)
|
||||
currentLog.dauer = dauer
|
||||
// Nur das Dauer-Feld direkt im DOM aktualisieren,
|
||||
// ohne redraw (sonst werden andere Felder zurückgesetzt).
|
||||
if (dauerInput) { dauerInput.value = dauer }
|
||||
}
|
||||
}, DAUER_UPDATE_INTERVALL)
|
||||
}
|
||||
}
|
||||
// Alle Listen asynchron laden, für Select-Optionen.
|
||||
fetchKunden().then((result) => { kundenListe = result })
|
||||
fetchProjekte().then((result) => { projektListe = result })
|
||||
fetchKontaktadressen().then((result) => { kontaktadressenListe = result })
|
||||
},
|
||||
onremove: (vnode) => {
|
||||
// Timer stoppen, damit er nach Verlassen des Views nicht weiterläuft.
|
||||
if (timerId != null) { window.clearInterval(timerId) }
|
||||
},
|
||||
view: (vnode) => {
|
||||
// Select-Optionen aus Kunden- und Projektliste aufbauen.
|
||||
const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => {
|
||||
// kunden_id aus dem geladenen Projekt ableiten (fuer abhaengiges
|
||||
// Projekt-Select). Wird nur bei nicht voreingestelltem Projekt
|
||||
// benoetigt.
|
||||
const internKundenId = (() => {
|
||||
if (currentLog.projekt_id !== -1) {
|
||||
const p = projektListe.find((p: iProjekt) => p.id === currentLog.projekt_id)
|
||||
return p ? p.kunden_id : -1
|
||||
}
|
||||
return -1
|
||||
})()
|
||||
|
||||
const kundenOptionen: iSelectOption[] = kundenListe.map((k: iKunde) => {
|
||||
return {value: k.id, label: k.name}
|
||||
})
|
||||
// Projekt-Optionen sind vom gewählten Kunden abhängig.
|
||||
const projektOptionen: iSelectOption[] = getProjekteByKundenId(currentLog.kundenId).map((p) => {
|
||||
const projektOptionen: iSelectOption[] = projektListe
|
||||
.filter((p: iProjekt) => internKundenId === -1 || p.kunden_id === internKundenId)
|
||||
.map((p: iProjekt) => {
|
||||
return {value: p.id, label: p.projektname}
|
||||
})
|
||||
// Kontaktadressen-Optionen sind vom gewählten Projekt abhängig.
|
||||
// Werden nur benötigt, wenn der Ansprechpartner nicht voreingestellt ist.
|
||||
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 + ")" : "")}
|
||||
})
|
||||
: []
|
||||
// Einheit der Dauer-Anzeige im Format mm:ss.
|
||||
|
||||
const submit = (form: HTMLFormElement): boolean => {
|
||||
const formData = new FormData(form)
|
||||
const gespeichertesLog: iCallLog = {
|
||||
id: currentLog.id,
|
||||
projekt_id: currentLog.projekt_id,
|
||||
projekt_name: "",
|
||||
kunden_name: "",
|
||||
datum: (formData.get("datum") as string) || "",
|
||||
uhrzeit_beginn: (formData.get("uhrzeit_beginn") as string) || "",
|
||||
dauer: (formData.get("dauer") as string) || "",
|
||||
betreff: (formData.get("betreff") as string) || "",
|
||||
name_ansprechpartner: (formData.get("name_ansprechpartner") as string) || "",
|
||||
telefon: (formData.get("telefon") as string) || "",
|
||||
notiz: (formData.get("notiz") as string) || "",
|
||||
wiedervorlage_datum: (formData.get("wiedervorlage_datum") as string) || "",
|
||||
erledigt: formData.get("erledigt") === "on"
|
||||
}
|
||||
if (currentLog.id === -1) {
|
||||
createCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
||||
} else {
|
||||
updateCallLogREST(gespeichertesLog).then(() => m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name}))
|
||||
}
|
||||
return true
|
||||
}
|
||||
const cancel = (e: Event) => {
|
||||
m.route.set("/calllogliste", {projekt_id: currentLog.projekt_id, projekt_name: currentLog.projekt_name})
|
||||
}
|
||||
return m("div.w3-container.w3-auto.form-view-container",
|
||||
m("h1", currentLog.id === -1 ? "Neue Gesprächsnotiz anlegen" : "Gesprächsnotiz"),
|
||||
// Form füllt den verbleibenden Platz; das Form-Widget selbst
|
||||
// sorgt (mit stickyButtons, Default) dafür, dass nur der
|
||||
// Inhalt scrollt und die Buttons fixiert bleiben.
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
m(Form, {showButtons: true, onsubmit: (null), oncancel: (null)} as iForm, [
|
||||
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
|
||||
m("input", {type: "hidden", name: "projekt_id", value: currentLog.projekt_id}),
|
||||
// Kunde und Projekt als Read-Only-Text, da voreingestellt.
|
||||
m("label", "Kunde",
|
||||
m(Select, {
|
||||
name: "kundenId",
|
||||
value: currentLog.kundenId,
|
||||
options: kundenOptionen,
|
||||
readonly: kundeVoreingestellt,
|
||||
required: true,
|
||||
onchanged: (kundenId: number) => {
|
||||
// Bei Kundenwechsel die kundenId übernehmen
|
||||
// und projektId zurücksetzen, da die Projekte
|
||||
// kundenabhängig sind.
|
||||
currentLog.kundenId = kundenId
|
||||
currentLog.projektId = -1
|
||||
}
|
||||
} as iSelect)),
|
||||
m("input.w3-input.w3-border", {
|
||||
type: "text",
|
||||
value: currentLog.kunden_name,
|
||||
readonly: true,
|
||||
style: {'margin-bottom': '5px'}
|
||||
})),
|
||||
m("label", "Projekt",
|
||||
m(Select, {
|
||||
name: "projektId",
|
||||
value: currentLog.projektId,
|
||||
options: projektOptionen,
|
||||
readonly: kundeVoreingestellt,
|
||||
required: true,
|
||||
onchanged: (projektId: number) => {
|
||||
currentLog.projektId = projektId
|
||||
// Bei Projektwechsel Ansprechpartner und Telefon
|
||||
// zurücksetzen, falls nicht voreingestellt.
|
||||
if (!ansprechpartnerVoreingestellt) {
|
||||
currentLog.nameAnsprechpartner = ""
|
||||
currentLog.telefon = ""
|
||||
}
|
||||
}
|
||||
} as iSelect)),
|
||||
m("input.w3-input.w3-border", {
|
||||
type: "text",
|
||||
value: currentLog.projekt_name,
|
||||
readonly: true,
|
||||
style: {'margin-bottom': '5px'}
|
||||
})),
|
||||
// Datum, Startzeit und Dauer in einer Zeile.
|
||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||
m("label", {style: {flex: '1'}}, "Datum",
|
||||
m(Input, {name: "datum", value: currentLog.datum, type: "date"} as iInput)),
|
||||
m("label", {style: {flex: '1'}}, "Uhrzeit Beginn",
|
||||
m(Input, {name: "uhrzeitBeginn", value: currentLog.uhrzeitBeginn, type: "time"} as iInput)),
|
||||
m(Input, {name: "uhrzeit_beginn", value: currentLog.uhrzeit_beginn, type: "time"} as iInput)),
|
||||
m("label", {style: {flex: '1'}}, "Dauer (mm:ss)",
|
||||
// Raw input statt Input-Widget, damit oncreate die
|
||||
// DOM-Referenz für den Timer greifen kann. Text-Typ,
|
||||
// da das Format mm:ss keine Zahl ist.
|
||||
m("input.w3-input.w3-border", {
|
||||
name: "dauer",
|
||||
type: "text",
|
||||
@@ -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)),
|
||||
// Ansprechpartner und Telefon in einer Zeile.
|
||||
// Wenn der Ansprechpartner voreingestellt ist (aus Kontaktadressenliste),
|
||||
// wird ein readonly Input gezeigt; sonst ein Select, das die
|
||||
// Kontaktadressen des gewählten Projekts anbietet. Bei Auswahl
|
||||
// werden Name und Telefon automatisch übernommen.
|
||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||
m("label", {style: {flex: '1'}}, "Name Ansprechpartner",
|
||||
ansprechpartnerVoreingestellt
|
||||
? m(Input, {name: "nameAnsprechpartner", value: currentLog.nameAnsprechpartner, type: "text", readonly: true} as iInput)
|
||||
? m(Input, {name: "name_ansprechpartner", value: currentLog.name_ansprechpartner, type: "text", readonly: true} as iInput)
|
||||
: m(Select, {
|
||||
name: "kontaktadresseId",
|
||||
name: "kontaktadresse_id",
|
||||
value: -1,
|
||||
options: kontaktadressenOptionen,
|
||||
onchanged: (kontaktadresseId: number) => {
|
||||
const ka = getKontaktadresseById(kontaktadresseId)
|
||||
const ka = kontaktadressenListe.find((k: iKontaktadresse) => k.id === kontaktadresseId)
|
||||
if (ka) {
|
||||
currentLog.nameAnsprechpartner = ka.name
|
||||
currentLog.name_ansprechpartner = ka.name
|
||||
currentLog.telefon = ka.telefon
|
||||
}
|
||||
}
|
||||
@@ -352,7 +352,7 @@ export const Call_Log: m.ClosureComponent = (vnode) => {
|
||||
m(Input, {name: "telefon", value: currentLog.telefon, type: "tel"} as iInput))
|
||||
),
|
||||
m("label", "Notiz", m(Textarea, {name: "notiz", value: currentLog.notiz, rows: 4} as iTextarea)),
|
||||
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlageDatum", value: currentLog.wiedervorlageDatum, type: "date"} as iInput)),
|
||||
m("label", "Wiedervorlage Datum", m(Input, {name: "wiedervorlage_datum", value: currentLog.wiedervorlage_datum, type: "date"} as iInput)),
|
||||
m("label", m(Input, {name: "erledigt", type: "checkbox", value: currentLog.erledigt} as iInput), "Erledigt")
|
||||
])
|
||||
)
|
||||
|
||||
@@ -1,62 +1,35 @@
|
||||
import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
|
||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {iKontakt, getKontakteByKundenId, getNewKontaktId, addKontakt, updateKontakt} from "../models/kontakt"
|
||||
import {iKunde, getKunden} from "../models/kunde"
|
||||
|
||||
function createKontaktTreenodes(kundenId: number | null = null): iTreenode[] {
|
||||
let filteredKontakte = getKontakteByKundenId(kundenId)
|
||||
|
||||
return filteredKontakte.map((kontakt: iKontakt) => {
|
||||
return new Treenode(kontakt, true, [])
|
||||
})
|
||||
}
|
||||
import {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
|
||||
import {iKunde, fetchKundeById} from "../models/kunde"
|
||||
|
||||
export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
let treenodes: iTreenode[] = createKontaktTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let filterKundenId: number | null = null
|
||||
// Kundenname für die Überschrift, asynchron per fetchKundeById
|
||||
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||
let kundenName: string = ""
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
selectedNode = node
|
||||
}
|
||||
|
||||
const addKontaktRoute = () => {
|
||||
const params: any = {
|
||||
id: -1,
|
||||
anrede: "",
|
||||
name: "",
|
||||
position: "",
|
||||
telefon: "",
|
||||
email: "",
|
||||
notiz: ""
|
||||
}
|
||||
const params: any = {id: -1}
|
||||
// Nur bei kundengefilterter Liste den Kunden vorbelegen.
|
||||
if (filterKundenId !== null) {
|
||||
const kunde = getKunden().find((k: iKunde) => k.id === filterKundenId)
|
||||
if (kunde) {
|
||||
params.kundenId = kunde.id
|
||||
params.kundenname = kunde.name
|
||||
}
|
||||
params.kunden_id = filterKundenId
|
||||
params.kunden_name = kundenName
|
||||
}
|
||||
m.route.set("/kontakt", params)
|
||||
}
|
||||
|
||||
const editKontakt = (kontakt: iKontakt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/kontakt", {
|
||||
id: kontakt.id,
|
||||
kundenId: kontakt.kundenId,
|
||||
kundenname: kontakt.kundenName,
|
||||
anrede: kontakt.anrede,
|
||||
name: kontakt.name,
|
||||
position: kontakt.position,
|
||||
telefon: kontakt.telefon,
|
||||
email: kontakt.email,
|
||||
notiz: kontakt.notiz
|
||||
})
|
||||
m.route.set("/kontakt", {id: kontakt.id})
|
||||
}
|
||||
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
@@ -67,7 +40,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
m("span", kontakt.position),
|
||||
m("span", kontakt.telefon),
|
||||
m("span", kontakt.email),
|
||||
m("span", kontakt.kundenName),
|
||||
m("span", kontakt.kunden_name),
|
||||
m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontakt(kontakt, e)}, m("i.material-icons", "edit"))
|
||||
]
|
||||
}
|
||||
@@ -75,22 +48,34 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let kontakt: iKontakt = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return kontakt.name.toLowerCase().includes(searchLower) ||
|
||||
kontakt.position.toLowerCase().includes(searchLower) ||
|
||||
kontakt.telefon.toLowerCase().includes(searchLower) ||
|
||||
kontakt.email.toLowerCase().includes(searchLower) ||
|
||||
kontakt.kundenName.toLowerCase().includes(searchLower)
|
||||
return (kontakt.name || "").toLowerCase().includes(searchLower) ||
|
||||
(kontakt.position || "").toLowerCase().includes(searchLower) ||
|
||||
(kontakt.telefon || "").toLowerCase().includes(searchLower) ||
|
||||
(kontakt.email || "").toLowerCase().includes(searchLower) ||
|
||||
(kontakt.kunden_name || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams && routeParams.kundenId) {
|
||||
filterKundenId = parseInt(routeParams.kundenId)
|
||||
if (routeParams && routeParams.kunden_id) {
|
||||
filterKundenId = parseInt(routeParams.kunden_id)
|
||||
kundenName = routeParams.kunden_name || ""
|
||||
} else {
|
||||
filterKundenId = null
|
||||
kundenName = ""
|
||||
}
|
||||
// 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) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
@@ -99,7 +84,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'}},
|
||||
m("h1", "Kontaktliste"),
|
||||
m("h1", kundenName ? "Kontaktliste für " + kundenName : "Kontaktliste"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
@@ -120,12 +105,12 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
}
|
||||
|
||||
export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
// currentKontakt hält den Formularzustand. kundenId referenziert den
|
||||
// Kunden eindeutig, kundenname dient der Anzeige.
|
||||
// currentKontakt hält den Formularzustand. kunden_id referenziert den
|
||||
// Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift.
|
||||
let currentKontakt: {
|
||||
id: number
|
||||
kundenId: number
|
||||
kundenname: string
|
||||
kunden_id: number
|
||||
kunden_name: string
|
||||
anrede: string
|
||||
name: string
|
||||
position: string
|
||||
@@ -134,8 +119,8 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
notiz: string
|
||||
} = {
|
||||
id: -1,
|
||||
kundenId: -1,
|
||||
kundenname: "",
|
||||
kunden_id: -1,
|
||||
kunden_name: "",
|
||||
anrede: "",
|
||||
name: "",
|
||||
position: "",
|
||||
@@ -143,38 +128,31 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
email: "",
|
||||
notiz: ""
|
||||
}
|
||||
// Ist der Kunde voreingestellt (gefilterte Kontaktliste)?
|
||||
let kundeVoreingestellt: boolean = false
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams) {
|
||||
const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1
|
||||
kundeVoreingestellt = kundenId !== -1
|
||||
currentKontakt = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
kundenId: kundenId,
|
||||
kundenname: routeParams.kundenname || "",
|
||||
anrede: routeParams.anrede || "",
|
||||
name: routeParams.name || "",
|
||||
position: routeParams.position || "",
|
||||
telefon: routeParams.telefon || "",
|
||||
email: routeParams.email || "",
|
||||
notiz: routeParams.notiz || ""
|
||||
}
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
|
||||
if (id !== -1) {
|
||||
// Kontaktdaten frisch vom Server holen, um zwischenzeitliche
|
||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||
fetchKontaktById(id).then((result) => {
|
||||
currentKontakt = result
|
||||
})
|
||||
} else {
|
||||
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
||||
// (voreingestellt aus der Kontaktliste).
|
||||
currentKontakt.kunden_id = routeParams && routeParams.kunden_id ? parseInt(routeParams.kunden_id) : -1
|
||||
currentKontakt.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
const submit = (form: HTMLFormElement): boolean => {
|
||||
const formData = new FormData(form)
|
||||
const kundenId = parseInt(formData.get("kundenId") as string)
|
||||
// Kundennamen zur gewählten kundenId auflösen.
|
||||
const kunde = getKunden().find((k: iKunde) => k.id === kundenId)
|
||||
const gespeicherterKontakt: iKontakt = {
|
||||
id: currentKontakt.id,
|
||||
kundenId: kundenId,
|
||||
kundenName: kunde ? kunde.name : "",
|
||||
kunden_id: currentKontakt.kunden_id,
|
||||
kunden_name: "",
|
||||
anrede: (formData.get("anrede") as string) || "",
|
||||
name: (formData.get("name") as string) || "",
|
||||
position: (formData.get("position") as string) || "",
|
||||
@@ -183,33 +161,22 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
notiz: (formData.get("notiz") as string) || ""
|
||||
}
|
||||
if (currentKontakt.id === -1) {
|
||||
gespeicherterKontakt.id = getNewKontaktId()
|
||||
addKontakt(gespeicherterKontakt)
|
||||
createKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}))
|
||||
} else {
|
||||
updateKontakt(gespeicherterKontakt)
|
||||
updateKontaktREST(gespeicherterKontakt).then(() => m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name}))
|
||||
}
|
||||
m.route.set("/kontaktliste")
|
||||
return true
|
||||
}
|
||||
const cancel = (e: Event) => {
|
||||
m.route.set("/kontaktliste")
|
||||
m.route.set("/kontaktliste", {kunden_id: currentKontakt.kunden_id, kunden_name: currentKontakt.kunden_name})
|
||||
}
|
||||
// Select-Optionen aus der Kundenliste aufbauen.
|
||||
const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => {
|
||||
return {value: k.id, label: k.name}
|
||||
})
|
||||
return m("div.w3-container.w3-auto.form-view-container",
|
||||
m("h1", currentKontakt.id === -1 ? "Neuen Kontakt anlegen" : "Kontakt-Stammblatt"),
|
||||
m("h1", currentKontakt.id === -1
|
||||
? "Neuer Kontakt für " + currentKontakt.kunden_name
|
||||
: "Kontakt-Stammblatt"),
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
|
||||
m("label", "Kunde",
|
||||
m(Select, {
|
||||
name: "kundenId",
|
||||
value: currentKontakt.kundenId,
|
||||
options: kundenOptionen,
|
||||
readonly: kundeVoreingestellt,
|
||||
required: true
|
||||
} as iSelect)),
|
||||
m("input", {type: "hidden", name: "kunden_id", value: currentKontakt.kunden_id}),
|
||||
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontakt.anrede, type: "text"} as iInput)),
|
||||
m("label", "Name", m(Input, {name: "name", value: currentKontakt.name, type: "text", required: true} as iInput)),
|
||||
m("label", "Position", m(Input, {name: "position", value: currentKontakt.position, type: "text"} as iInput)),
|
||||
|
||||
@@ -1,70 +1,34 @@
|
||||
import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
|
||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {iKontaktadresse, getKontaktadressenByProjektId, getNewKontaktadresseId, addKontaktadresse, updateKontaktadresse} from "../models/kontaktadresse"
|
||||
import {iProjekt, getProjekte, getProjektById} from "../models/projekt"
|
||||
|
||||
function createKontaktadresseTreenodes(projektId: number | null = null): iTreenode[] {
|
||||
let filteredKontaktadressen = getKontaktadressenByProjektId(projektId)
|
||||
|
||||
return filteredKontaktadressen.map((kontaktadresse: iKontaktadresse) => {
|
||||
return new Treenode(kontaktadresse, true, [])
|
||||
})
|
||||
}
|
||||
import {iKontaktadresse, fetchKontaktadressen, fetchKontaktadresseById, createKontaktadresseREST, updateKontaktadresseREST} from "../models/kontaktadresse"
|
||||
import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||
|
||||
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
let treenodes: iTreenode[] = createKontaktadresseTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let filterProjektId: number | null = null
|
||||
// Projektname für die Überschrift, asynchron per fetchProjektById
|
||||
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||
let projektName: string = ""
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
selectedNode = node
|
||||
}
|
||||
|
||||
const addKontaktadresseRoute = () => {
|
||||
const params: any = {
|
||||
id: -1,
|
||||
anrede: "",
|
||||
name: "",
|
||||
firma: "",
|
||||
telefon: "",
|
||||
email: "",
|
||||
strasse: "",
|
||||
plz: "",
|
||||
ort: "",
|
||||
land: "",
|
||||
notiz: ""
|
||||
}
|
||||
// Nur bei projektgefilterter Liste das Projekt vorbelegen.
|
||||
const params: any = {id: -1}
|
||||
if (filterProjektId !== null) {
|
||||
const projekt = getProjekte().find((p: iProjekt) => p.id === filterProjektId)
|
||||
if (projekt) {
|
||||
params.projektId = projekt.id
|
||||
params.projektname = projekt.projektname
|
||||
}
|
||||
params.projekt_id = filterProjektId
|
||||
params.projekt_name = projektName
|
||||
}
|
||||
m.route.set("/kontaktadresse", params)
|
||||
}
|
||||
|
||||
const editKontaktadresse = (kontaktadresse: iKontaktadresse, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/kontaktadresse", {
|
||||
id: kontaktadresse.id,
|
||||
projektId: kontaktadresse.projektId,
|
||||
projektname: kontaktadresse.projektName,
|
||||
anrede: kontaktadresse.anrede,
|
||||
name: kontaktadresse.name,
|
||||
firma: kontaktadresse.firma,
|
||||
telefon: kontaktadresse.telefon,
|
||||
email: kontaktadresse.email,
|
||||
strasse: kontaktadresse.strasse,
|
||||
plz: kontaktadresse.plz,
|
||||
ort: kontaktadresse.ort,
|
||||
land: kontaktadresse.land,
|
||||
notiz: kontaktadresse.notiz
|
||||
})
|
||||
m.route.set("/kontaktadresse", {id: kontaktadresse.id})
|
||||
}
|
||||
|
||||
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
|
||||
@@ -72,21 +36,18 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
const now = new Date()
|
||||
const datum = now.toISOString().split('T')[0]
|
||||
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
|
||||
// Kundennamen über das Projekt auflösen.
|
||||
const projekt = getProjektById(kontaktadresse.projektId)
|
||||
m.route.set("/call_log", {
|
||||
id: -1,
|
||||
kundenName: projekt ? projekt.kundenName : "",
|
||||
projektId: kontaktadresse.projektId,
|
||||
projektName: kontaktadresse.projektName,
|
||||
projekt_id: kontaktadresse.projekt_id,
|
||||
projekt_name: kontaktadresse.projekt_name,
|
||||
datum: datum,
|
||||
uhrzeitBeginn: uhrzeit,
|
||||
uhrzeit_beginn: uhrzeit,
|
||||
dauer: "",
|
||||
betreff: "",
|
||||
nameAnsprechpartner: kontaktadresse.name,
|
||||
name_ansprechpartner: kontaktadresse.name,
|
||||
telefon: kontaktadresse.telefon,
|
||||
notiz: "",
|
||||
wiedervorlageDatum: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false
|
||||
})
|
||||
}
|
||||
@@ -97,10 +58,10 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
return [
|
||||
m("div", m("strong", kontaktadresse.name)),
|
||||
m("span", kontaktadresse.firma),
|
||||
m("span", kontaktadresse.position),
|
||||
m("span", kontaktadresse.telefon),
|
||||
m("span", kontaktadresse.email),
|
||||
m("span", kontaktadresse.ort),
|
||||
m("span", kontaktadresse.projektName),
|
||||
m("div", {style: {display: 'flex', gap: '0', 'white-space': 'nowrap'}},
|
||||
m("button.w3-button.w3-small", {style: {padding: '0 2px'}, onclick: (e: Event) => editKontaktadresse(kontaktadresse, e)}, m("i.material-icons", "edit")),
|
||||
m("button.w3-button.w3-small", {title: "Gesprächsnotiz hinzufügen", style: {padding: '0 2px'}, onclick: (e: Event) => addCallLog(kontaktadresse, e)}, m("i.material-icons", "add_call"))
|
||||
@@ -111,23 +72,37 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let kontaktadresse: iKontaktadresse = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return kontaktadresse.name.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.firma.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.telefon.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.email.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.ort.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.projektName.toLowerCase().includes(searchLower)
|
||||
return (kontaktadresse.name || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.firma || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.position || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.telefon || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.email || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.ort || "").toLowerCase().includes(searchLower) ||
|
||||
(kontaktadresse.projekt_name || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams && routeParams.projektId) {
|
||||
filterProjektId = parseInt(routeParams.projektId)
|
||||
if (routeParams && routeParams.projekt_id) {
|
||||
filterProjektId = parseInt(routeParams.projekt_id)
|
||||
projektName = routeParams.projekt_name || ""
|
||||
} else {
|
||||
filterProjektId = null
|
||||
projektName = ""
|
||||
}
|
||||
// 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) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
@@ -136,7 +111,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'}},
|
||||
m("h1", "Kontaktadressen"),
|
||||
m("h1", projektName ? "Kontaktadressen für " + projektName : "Kontaktadressen"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
@@ -157,15 +132,16 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
}
|
||||
|
||||
export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||
// currentKontaktadresse hält den Formularzustand. projektId referenziert das
|
||||
// Projekt eindeutig, projektname dient der Anzeige.
|
||||
// currentKontaktadresse hält den Formularzustand. projekt_id referenziert
|
||||
// das Projekt eindeutig, projekt_name dient der Anzeige in der Überschrift.
|
||||
let currentKontaktadresse: {
|
||||
id: number
|
||||
projektId: number
|
||||
projektname: string
|
||||
projekt_id: number
|
||||
projekt_name: string
|
||||
anrede: string
|
||||
name: string
|
||||
firma: string
|
||||
position: string
|
||||
telefon: string
|
||||
email: string
|
||||
strasse: string
|
||||
@@ -175,11 +151,12 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||
notiz: string
|
||||
} = {
|
||||
id: -1,
|
||||
projektId: -1,
|
||||
projektname: "",
|
||||
projekt_id: -1,
|
||||
projekt_name: "",
|
||||
anrede: "",
|
||||
name: "",
|
||||
firma: "",
|
||||
position: "",
|
||||
telefon: "",
|
||||
email: "",
|
||||
strasse: "",
|
||||
@@ -188,45 +165,33 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||
land: "",
|
||||
notiz: ""
|
||||
}
|
||||
// Ist das Projekt voreingestellt (gefilterte Kontaktadressenliste)?
|
||||
let projektVoreingestellt: boolean = false
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams) {
|
||||
const projektId = routeParams.projektId ? parseInt(routeParams.projektId) : -1
|
||||
projektVoreingestellt = projektId !== -1
|
||||
currentKontaktadresse = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
projektId: projektId,
|
||||
projektname: routeParams.projektname || "",
|
||||
anrede: routeParams.anrede || "",
|
||||
name: routeParams.name || "",
|
||||
firma: routeParams.firma || "",
|
||||
telefon: routeParams.telefon || "",
|
||||
email: routeParams.email || "",
|
||||
strasse: routeParams.strasse || "",
|
||||
plz: routeParams.plz || "",
|
||||
ort: routeParams.ort || "",
|
||||
land: routeParams.land || "",
|
||||
notiz: routeParams.notiz || ""
|
||||
}
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
|
||||
if (id !== -1) {
|
||||
// Kontaktadresse frisch vom Server holen.
|
||||
fetchKontaktadresseById(id).then((result) => {
|
||||
currentKontaktadresse = result
|
||||
})
|
||||
} else {
|
||||
// Neuanlegen: projekt_id und projekt_name aus Route-Params.
|
||||
currentKontaktadresse.projekt_id = routeParams && routeParams.projekt_id ? parseInt(routeParams.projekt_id) : -1
|
||||
currentKontaktadresse.projekt_name = routeParams && routeParams.projekt_name ? routeParams.projekt_name : ""
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
const submit = (form: HTMLFormElement): boolean => {
|
||||
const formData = new FormData(form)
|
||||
const projektId = parseInt(formData.get("projektId") as string)
|
||||
// Projektnamen zur gewählten projektId auflösen.
|
||||
const projekt = getProjekte().find((p: iProjekt) => p.id === projektId)
|
||||
const gespeicherteKontaktadresse: iKontaktadresse = {
|
||||
id: currentKontaktadresse.id,
|
||||
projektId: projektId,
|
||||
projektName: projekt ? projekt.projektname : "",
|
||||
projekt_id: currentKontaktadresse.projekt_id,
|
||||
projekt_name: "",
|
||||
anrede: (formData.get("anrede") as string) || "",
|
||||
name: (formData.get("name") as string) || "",
|
||||
firma: (formData.get("firma") as string) || "",
|
||||
position: (formData.get("position") as string) || "",
|
||||
telefon: (formData.get("telefon") as string) || "",
|
||||
email: (formData.get("email") as string) || "",
|
||||
strasse: (formData.get("strasse") as string) || "",
|
||||
@@ -236,40 +201,29 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||
notiz: (formData.get("notiz") as string) || ""
|
||||
}
|
||||
if (currentKontaktadresse.id === -1) {
|
||||
gespeicherteKontaktadresse.id = getNewKontaktadresseId()
|
||||
addKontaktadresse(gespeicherteKontaktadresse)
|
||||
createKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
||||
} else {
|
||||
updateKontaktadresse(gespeicherteKontaktadresse)
|
||||
updateKontaktadresseREST(gespeicherteKontaktadresse).then(() => m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name}))
|
||||
}
|
||||
m.route.set("/kontaktadresseliste")
|
||||
return true
|
||||
}
|
||||
const cancel = (e: Event) => {
|
||||
m.route.set("/kontaktadresseliste")
|
||||
m.route.set("/kontaktadresseliste", {projekt_id: currentKontaktadresse.projekt_id, projekt_name: currentKontaktadresse.projekt_name})
|
||||
}
|
||||
// Select-Optionen aus der Projektliste aufbauen.
|
||||
const projektOptionen: iSelectOption[] = getProjekte().map((p: iProjekt) => {
|
||||
return {value: p.id, label: p.projektname}
|
||||
})
|
||||
return m("div.w3-container.w3-auto.form-view-container",
|
||||
m("h1", currentKontaktadresse.id === -1 ? "Neue Kontaktadresse anlegen" : "Kontaktadresse-Stammblatt"),
|
||||
m("h1", currentKontaktadresse.id === -1
|
||||
? "Neue Kontaktadresse für " + currentKontaktadresse.projekt_name
|
||||
: "Kontaktadresse-Stammblatt"),
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
|
||||
m("label", "Projekt",
|
||||
m(Select, {
|
||||
name: "projektId",
|
||||
value: currentKontaktadresse.projektId,
|
||||
options: projektOptionen,
|
||||
readonly: projektVoreingestellt,
|
||||
required: true
|
||||
} as iSelect)),
|
||||
m("input", {type: "hidden", name: "projekt_id", value: currentKontaktadresse.projekt_id}),
|
||||
m("label", "Anrede", m(Input, {name: "anrede", value: currentKontaktadresse.anrede, type: "text"} as iInput)),
|
||||
m("label", "Name", m(Input, {name: "name", value: currentKontaktadresse.name, type: "text", required: true} as iInput)),
|
||||
m("label", "Firma", m(Input, {name: "firma", value: currentKontaktadresse.firma, type: "text"} as iInput)),
|
||||
m("label", "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", "E-Mail", m(Input, {name: "email", value: currentKontaktadresse.email, type: "email"} as iInput)),
|
||||
m("label", "Straße", m(Input, {name: "strasse", value: currentKontaktadresse.strasse, type: "text"} as iInput)),
|
||||
// PLZ und Ort in einer Zeile.
|
||||
m("div", {style: {display: 'flex', gap: '8px', 'margin-bottom': '5px'}},
|
||||
m("label", {style: {flex: '0 0 120px'}}, "PLZ",
|
||||
m(Input, {name: "plz", value: currentKontaktadresse.plz, type: "text"} as iInput)),
|
||||
|
||||
+28
-37
@@ -2,7 +2,7 @@ import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput} from "./widgets/inputs"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {iKunde, kundenDaten, getNewKundeId, addKunde, updateKunde} from "../models/kunde"
|
||||
import {iKunde, fetchKunden, fetchKundeById, createKundeREST, updateKundeREST} from "../models/kunde"
|
||||
|
||||
export const kunde: m.ClosureComponent = (vnode) => {
|
||||
let currentKunde: iKunde = {
|
||||
@@ -17,15 +17,13 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams) {
|
||||
currentKunde = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
name: routeParams.name || "",
|
||||
strasse: routeParams.strasse || "",
|
||||
plz: routeParams.plz || "",
|
||||
ort: routeParams.ort || "",
|
||||
land: routeParams.land || ""
|
||||
}
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
|
||||
if (id !== -1) {
|
||||
// Kundendaten frisch vom Server holen, um zwischenzeitliche
|
||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||
fetchKundeById(id).then((result) => {
|
||||
currentKunde = result
|
||||
})
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
@@ -40,12 +38,10 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
||||
land: (formData.get("land") as string) || ""
|
||||
}
|
||||
if (currentKunde.id === -1) {
|
||||
gespeicherterKunde.id = getNewKundeId()
|
||||
addKunde(gespeicherterKunde)
|
||||
createKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
||||
} else {
|
||||
updateKunde(gespeicherterKunde)
|
||||
updateKundeREST(gespeicherterKunde).then(() => m.route.set("/kundenliste"))
|
||||
}
|
||||
m.route.set("/kundenliste")
|
||||
return true
|
||||
}
|
||||
const cancel = (e: Event) => {
|
||||
@@ -56,7 +52,7 @@ export const kunde: m.ClosureComponent = (vnode) => {
|
||||
m("h1", currentKunde.id === -1 ? "Neuen Kunden anlegen" : "Kundenstammblatt"),
|
||||
currentKunde.id !== -1
|
||||
? m("button.w3-button.w3-theme",
|
||||
{onclick: () => m.route.set("/kontaktliste", {kundenId: currentKunde.id})},
|
||||
{onclick: () => m.route.set("/kontaktliste", {kunden_id: currentKunde.id})},
|
||||
m("i.material-icons", "contact_page"), " Kontakte")
|
||||
: null
|
||||
),
|
||||
@@ -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) => {
|
||||
let treenodes: iTreenode[] = createKundenTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
@@ -96,21 +80,21 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
|
||||
|
||||
const editKunde = (kunde: iKunde, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/kunde", {id: kunde.id, name: kunde.name, strasse: kunde.strasse, plz: kunde.plz, ort: kunde.ort, land: kunde.land})
|
||||
m.route.set("/kunde", {id: kunde.id})
|
||||
}
|
||||
|
||||
const showProjekte = (kunde: iKunde, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/projektliste", {kundenId: kunde.id})
|
||||
m.route.set("/projektliste", {kunden_id: kunde.id})
|
||||
}
|
||||
|
||||
const showKontakte = (kunde: iKunde, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/kontaktliste", {kundenId: kunde.id})
|
||||
m.route.set("/kontaktliste", {kunden_id: kunde.id})
|
||||
}
|
||||
|
||||
const addKundeRoute = () => {
|
||||
m.route.set("/kunde", {id: -1, name: "", strasse: "", plz: "", ort: "", land: ""})
|
||||
m.route.set("/kunde", {id: -1})
|
||||
}
|
||||
|
||||
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
||||
@@ -133,14 +117,21 @@ export const kundenliste: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let kunde: iKunde = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return kunde.name.toLowerCase().includes(searchLower) ||
|
||||
kunde.strasse.toLowerCase().includes(searchLower) ||
|
||||
kunde.plz.includes(searchLower) ||
|
||||
kunde.ort.toLowerCase().includes(searchLower) ||
|
||||
kunde.land.toLowerCase().includes(searchLower)
|
||||
return (kunde.name || "").toLowerCase().includes(searchLower) ||
|
||||
(kunde.strasse || "").toLowerCase().includes(searchLower) ||
|
||||
(kunde.plz || "").includes(searchLower) ||
|
||||
(kunde.ort || "").toLowerCase().includes(searchLower) ||
|
||||
(kunde.land || "").toLowerCase().includes(searchLower)
|
||||
}
|
||||
|
||||
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) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
style: {
|
||||
|
||||
@@ -1,69 +1,45 @@
|
||||
import m from "mithril"
|
||||
import {Form, iForm} from "./widgets/form"
|
||||
import { Input, iInput} from "./widgets/inputs"
|
||||
import {Select, iSelect, iSelectOption} from "./widgets/select"
|
||||
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||
import {iProjekt, getProjekteByKundenId, getNewProjektId, addProjekt, updateProjekt} from "../models/projekt"
|
||||
import {iKunde, getKunden} from "../models/kunde"
|
||||
|
||||
function createProjektTreenodes(kundenId: number | null = null): iTreenode[] {
|
||||
let filteredProjekte = getProjekteByKundenId(kundenId)
|
||||
|
||||
return filteredProjekte.map((projekt: iProjekt) => {
|
||||
return new Treenode(projekt, true, [])
|
||||
})
|
||||
}
|
||||
import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt"
|
||||
import {iKunde, fetchKundeById} from "../models/kunde"
|
||||
|
||||
export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
let treenodes: iTreenode[] = createProjektTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let filterKundenId: number | null = null
|
||||
// Kundenname für die Überschrift, asynchron per fetchKundeById
|
||||
// aufgelöst, falls nicht in den Route-Params enthalten.
|
||||
let kundenName: string = ""
|
||||
|
||||
const handleRowMarked = (node: iTreenode) => {
|
||||
selectedNode = node
|
||||
}
|
||||
|
||||
const addProjektRoute = () => {
|
||||
const params: any = {
|
||||
id: -1,
|
||||
kundenname: "",
|
||||
projektname: "",
|
||||
zweck: "",
|
||||
verantwortlicher: "",
|
||||
startdatum: ""
|
||||
}
|
||||
const params: any = {id: -1}
|
||||
// Nur bei kundengefilterter Liste den Kunden vorbelegen.
|
||||
if (filterKundenId !== null) {
|
||||
const kunde = getKunden().find((k: iKunde) => k.id === filterKundenId)
|
||||
if (kunde) {
|
||||
params.kundenId = kunde.id
|
||||
params.kundenname = kunde.name
|
||||
}
|
||||
params.kunden_id = filterKundenId
|
||||
params.kunden_name = kundenName
|
||||
}
|
||||
m.route.set("/projekt", params)
|
||||
}
|
||||
|
||||
const editProjekt = (projekt: iProjekt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/projekt", {
|
||||
id: projekt.id,
|
||||
kundenId: projekt.kundenId,
|
||||
kundenname: projekt.kundenName,
|
||||
projektname: projekt.projektname,
|
||||
zweck: projekt.zweck,
|
||||
verantwortlicher: projekt.verantwortlicher,
|
||||
startdatum: projekt.startdatum
|
||||
})
|
||||
m.route.set("/projekt", {id: projekt.id})
|
||||
}
|
||||
|
||||
const showCallLogs = (projekt: iProjekt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/calllogliste", {projektId: projekt.id})
|
||||
m.route.set("/calllogliste", {projekt_id: projekt.id})
|
||||
}
|
||||
|
||||
const showKontaktadressen = (projekt: iProjekt, e: Event) => {
|
||||
e.stopPropagation()
|
||||
m.route.set("/kontaktadresseliste", {projektId: projekt.id})
|
||||
m.route.set("/kontaktadresseliste", {projekt_id: projekt.id})
|
||||
}
|
||||
|
||||
const addCallLog = (projekt: iProjekt, e: Event) => {
|
||||
@@ -73,17 +49,17 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
const uhrzeit = now.toTimeString().split(':')[0] + ':' + now.toTimeString().split(':')[1]
|
||||
m.route.set("/call_log", {
|
||||
id: -1,
|
||||
kundenName: projekt.kundenName,
|
||||
projektId: projekt.id,
|
||||
projektName: projekt.projektname,
|
||||
kunden_name: projekt.kunden_name,
|
||||
projekt_id: projekt.id,
|
||||
projekt_name: projekt.projektname,
|
||||
datum: datum,
|
||||
uhrzeitBeginn: uhrzeit,
|
||||
uhrzeit_beginn: uhrzeit,
|
||||
dauer: "",
|
||||
betreff: "",
|
||||
nameAnsprechpartner: "",
|
||||
name_ansprechpartner: "",
|
||||
telefon: "",
|
||||
notiz: "",
|
||||
wiedervorlageDatum: "",
|
||||
wiedervorlage_datum: "",
|
||||
erledigt: false
|
||||
})
|
||||
}
|
||||
@@ -92,7 +68,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
let projekt: iProjekt = node.nodevalue
|
||||
|
||||
return [
|
||||
m("div", m("strong", projekt.kundenName)),
|
||||
m("div", m("strong", projekt.kunden_name)),
|
||||
m("span", projekt.projektname),
|
||||
m("span", projekt.zweck),
|
||||
m("span", projekt.verantwortlicher),
|
||||
@@ -109,22 +85,34 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
const filterFunction = (node: iTreenode, searchvalue: string): boolean => {
|
||||
let projekt: iProjekt = node.nodevalue
|
||||
let searchLower = searchvalue.toLowerCase()
|
||||
return projekt.kundenName.toLowerCase().includes(searchLower) ||
|
||||
projekt.projektname.toLowerCase().includes(searchLower) ||
|
||||
projekt.zweck.toLowerCase().includes(searchLower) ||
|
||||
projekt.verantwortlicher.toLowerCase().includes(searchLower) ||
|
||||
projekt.startdatum.includes(searchLower)
|
||||
return (projekt.kunden_name || "").toLowerCase().includes(searchLower) ||
|
||||
(projekt.projektname || "").toLowerCase().includes(searchLower) ||
|
||||
(projekt.zweck || "").toLowerCase().includes(searchLower) ||
|
||||
(projekt.verantwortlicher || "").toLowerCase().includes(searchLower) ||
|
||||
(projekt.startdatum || "").includes(searchLower)
|
||||
}
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams && routeParams.kundenId) {
|
||||
filterKundenId = parseInt(routeParams.kundenId)
|
||||
if (routeParams && routeParams.kunden_id) {
|
||||
filterKundenId = parseInt(routeParams.kunden_id)
|
||||
kundenName = routeParams.kunden_name || ""
|
||||
} else {
|
||||
filterKundenId = null
|
||||
kundenName = ""
|
||||
}
|
||||
// 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) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
@@ -133,7 +121,7 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
'flex-direction': 'column',
|
||||
height: 'inherit',
|
||||
'overflow-y': 'inherit'}},
|
||||
m("h1", "Projektliste"),
|
||||
m("h1", kundenName ? "Projektliste für " + kundenName : "Projektliste"),
|
||||
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
||||
m(Treetable,
|
||||
{
|
||||
@@ -154,95 +142,79 @@ export const projektliste: m.ClosureComponent = (vnode) => {
|
||||
}
|
||||
|
||||
export const Projekt: m.ClosureComponent = (vnode) => {
|
||||
// currentProjekt hält den Formularzustand. kundenId referenziert den
|
||||
// Kunden eindeutig, kundenname dient der Anzeige.
|
||||
// currentProjekt hält den Formularzustand. kunden_id referenziert den
|
||||
// Kunden eindeutig, kunden_name dient der Anzeige in der Überschrift.
|
||||
let currentProjekt: {
|
||||
id: number
|
||||
kundenId: number
|
||||
kundenname: string
|
||||
kunden_id: number
|
||||
kunden_name: string
|
||||
projektname: string
|
||||
zweck: string
|
||||
verantwortlicher: string
|
||||
startdatum: string
|
||||
} = {
|
||||
id: -1,
|
||||
kundenId: -1,
|
||||
kundenname: "",
|
||||
kunden_id: -1,
|
||||
kunden_name: "",
|
||||
projektname: "",
|
||||
zweck: "",
|
||||
verantwortlicher: "",
|
||||
startdatum: ""
|
||||
}
|
||||
// Ist der Kunde voreingestellt (gefilterte Projektliste)?
|
||||
let kundeVoreingestellt: boolean = false
|
||||
|
||||
return {
|
||||
oninit: (vnode) => {
|
||||
const routeParams = m.route.param()
|
||||
if (routeParams) {
|
||||
const kundenId = routeParams.kundenId ? parseInt(routeParams.kundenId) : -1
|
||||
kundeVoreingestellt = kundenId !== -1
|
||||
currentProjekt = {
|
||||
id: routeParams.id ? parseInt(routeParams.id) : -1,
|
||||
kundenId: kundenId,
|
||||
kundenname: routeParams.kundenname || "",
|
||||
projektname: routeParams.projektname || "",
|
||||
zweck: routeParams.zweck || "",
|
||||
verantwortlicher: routeParams.verantwortlicher || "",
|
||||
startdatum: routeParams.startdatum || ""
|
||||
}
|
||||
const id = routeParams && routeParams.id ? parseInt(routeParams.id) : -1
|
||||
if (id !== -1) {
|
||||
// Projektdaten frisch vom Server holen, um zwischenzeitliche
|
||||
// Änderungen anderer Nutzer nicht zu überschreiben.
|
||||
fetchProjektById(id).then((result) => {
|
||||
currentProjekt = result
|
||||
})
|
||||
} else {
|
||||
// Neuanlegen: kunden_id und kunden_name aus Route-Params
|
||||
// (voreingestellt aus der Projektliste).
|
||||
currentProjekt.kunden_id = routeParams && routeParams.kunden_id ? parseInt(routeParams.kunden_id) : -1
|
||||
currentProjekt.kunden_name = routeParams && routeParams.kunden_name ? routeParams.kunden_name : ""
|
||||
}
|
||||
},
|
||||
view: (vnode) => {
|
||||
const submit = (form: HTMLFormElement): boolean => {
|
||||
const formData = new FormData(form)
|
||||
const kundenId = parseInt(formData.get("kundenId") as string)
|
||||
// Kundennamen zur gewählten kundenId auflösen.
|
||||
const kunde = getKunden().find((k: iKunde) => k.id === kundenId)
|
||||
const gespeichertesProjekt: iProjekt = {
|
||||
id: currentProjekt.id,
|
||||
kundenId: kundenId,
|
||||
kundenName: kunde ? kunde.name : "",
|
||||
kunden_id: currentProjekt.kunden_id,
|
||||
kunden_name: "",
|
||||
projektname: (formData.get("projektname") as string) || "",
|
||||
zweck: (formData.get("zweck") as string) || "",
|
||||
verantwortlicher: (formData.get("verantwortlicher") as string) || "",
|
||||
startdatum: (formData.get("startdatum") as string) || ""
|
||||
}
|
||||
if (currentProjekt.id === -1) {
|
||||
gespeichertesProjekt.id = getNewProjektId()
|
||||
addProjekt(gespeichertesProjekt)
|
||||
createProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}))
|
||||
} else {
|
||||
updateProjekt(gespeichertesProjekt)
|
||||
updateProjektREST(gespeichertesProjekt).then(() => m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name}))
|
||||
}
|
||||
m.route.set("/projektliste")
|
||||
return true
|
||||
}
|
||||
const cancel = (e: Event) => {
|
||||
m.route.set("/projektliste")
|
||||
m.route.set("/projektliste", {kunden_id: currentProjekt.kunden_id, kunden_name: currentProjekt.kunden_name})
|
||||
}
|
||||
// Select-Optionen aus der Kundenliste aufbauen.
|
||||
const kundenOptionen: iSelectOption[] = getKunden().map((k: iKunde) => {
|
||||
return {value: k.id, label: k.name}
|
||||
})
|
||||
return m("div.w3-container.w3-auto.form-view-container",
|
||||
m("div", {style: {display: 'flex', 'align-items': 'center', 'justify-content': 'space-between'}},
|
||||
m("h1", currentProjekt.id === -1 ? "Neues Projekt anlegen" : "Projekt-Stammblatt"),
|
||||
m("h1", currentProjekt.id === -1
|
||||
? "Neues Projekt für " + currentProjekt.kunden_name
|
||||
: "Projekt-Stammblatt"),
|
||||
currentProjekt.id !== -1
|
||||
? m("button.w3-button.w3-theme",
|
||||
{onclick: () => m.route.set("/kontaktadresseliste", {projektId: currentProjekt.id})},
|
||||
{onclick: () => m.route.set("/kontaktadresseliste", {projekt_id: currentProjekt.id})},
|
||||
m("i.material-icons", "contact_page"), " Kontaktadressen")
|
||||
: null
|
||||
),
|
||||
m("div", {style: {flex: '1', 'min-height': '0'}},
|
||||
m(Form, {showButtons: true, onsubmit: submit, oncancel: cancel} as iForm, [
|
||||
m("label", "Kunde",
|
||||
m(Select, {
|
||||
name: "kundenId",
|
||||
value: currentProjekt.kundenId,
|
||||
options: kundenOptionen,
|
||||
readonly: kundeVoreingestellt,
|
||||
required: true
|
||||
} as iSelect)),
|
||||
m("input", {type: "hidden", name: "kunden_id", value: currentProjekt.kunden_id}),
|
||||
m("label", "Projektname", m(Input, {name: "projektname", value: currentProjekt.projektname, type: "text", required: true} as iInput)),
|
||||
m("label", "Zweck", m(Input, {name: "zweck", value: currentProjekt.zweck, type: "text"} as iInput)),
|
||||
m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: currentProjekt.verantwortlicher, type: "text"} as iInput)),
|
||||
|
||||
+20
-67
@@ -2,86 +2,31 @@ import m from "mithril"
|
||||
|
||||
/**
|
||||
* Datenmodell für eine Gesprächsnotiz (Call-Log).
|
||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
|
||||
* serverseitig aus der Projekt-Relationship abgeleitet und im JSON ergänzt.
|
||||
*/
|
||||
export interface iCallLog {
|
||||
id: number
|
||||
kundenName: string
|
||||
projektId: number
|
||||
projektName: string
|
||||
projekt_id: number
|
||||
projekt_name: string
|
||||
kunden_name: string
|
||||
datum: string
|
||||
uhrzeitBeginn: string
|
||||
uhrzeit_beginn: string
|
||||
dauer: string
|
||||
betreff: string
|
||||
nameAnsprechpartner: string
|
||||
name_ansprechpartner: string
|
||||
telefon: string
|
||||
notiz: string
|
||||
wiedervorlageDatum: string
|
||||
wiedervorlage_datum: string
|
||||
erledigt: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* In-Memory-Datenspeicher für Gesprächsnotizen. Wird als Fallback solange
|
||||
* verwendet, bis die serverseitigen REST-Endpunkte verfügbar sind.
|
||||
*/
|
||||
export const callLogDaten: iCallLog[] = [
|
||||
{id: 1, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-20", uhrzeitBeginn: "09:30", dauer: "45", betreff: "Anforderungen klären", nameAnsprechpartner: "Herr Schmidt", telefon: "+49 123 45678", notiz: "Kunde möchte responsive Design und neue Farbgestaltung. Budget noch offen.", wiedervorlageDatum: "2024-01-27", erledigt: false},
|
||||
{id: 2, kundenName: "Musterfirma GmbH", projektId: 1, projektName: "Website-Relaunch", datum: "2024-01-25", uhrzeitBeginn: "14:00", dauer: "30", betreff: "Feedback zum Design", nameAnsprechpartner: "Frau Müller", telefon: "+49 123 45679", notiz: "Design-Entwürfe wurden positiv aufgenommen. Kleine Änderungen an der Navigation nötig.", wiedervorlageDatum: "2024-02-01", erledigt: false},
|
||||
{id: 3, kundenName: "Beispiel AG", projektId: 2, projektName: "E-Commerce Plattform", datum: "2024-02-10", uhrzeitBeginn: "10:00", dauer: "60", betreff: "Technische Anforderungen", nameAnsprechpartner: "Herr Weber", telefon: "+49 123 45680", notiz: "Payment-Gateway und Versand-Schnittstellen müssen integriert werden. Zeitplan besprochen.", wiedervorlageDatum: "2024-02-15", erledigt: true},
|
||||
{id: 4, kundenName: "Beispiel AG", projektId: 3, projektName: "CRM-System", datum: "2024-02-18", uhrzeitBeginn: "11:15", dauer: "20", betreff: "Schulungstermin", nameAnsprechpartner: "Frau Bauer", telefon: "+49 123 45681", notiz: "Schulung für 10 Mitarbeiter geplant. Raum noch zu buchen.", wiedervorlageDatum: "2024-02-20", erledigt: false},
|
||||
{id: 5, kundenName: "Test Unternehmen", projektId: 4, projektName: "Mobile App", datum: "2024-03-05", uhrzeitBeginn: "15:45", dauer: "35", betreff: "Feature-Request", nameAnsprechpartner: "Herr König", telefon: "+49 123 45682", notiz: "Kunde wünscht Push-Benachrichtigungen und Offline-Modus.", wiedervorlageDatum: "2024-03-12", erledigt: false}
|
||||
]
|
||||
|
||||
/**
|
||||
* Liefert die nächste freie Call-Log-ID auf Basis des In-Memory-Speichers.
|
||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
||||
*/
|
||||
export function getNewCallLogId(): number {
|
||||
if (callLogDaten.length === 0) {
|
||||
return 1
|
||||
}
|
||||
return Math.max(...callLogDaten.map((log: iCallLog) => log.id)) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine neue Gesprächsnotiz dem In-Memory-Speicher hinzu.
|
||||
*/
|
||||
export function addCallLogData(log: iCallLog): void {
|
||||
callLogDaten.push(log)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine bestehende Gesprächsnotiz im In-Memory-Speicher.
|
||||
*/
|
||||
export function updateCallLogData(log: iCallLog): void {
|
||||
const index = callLogDaten.findIndex((l: iCallLog) => l.id === log.id)
|
||||
if (index !== -1) {
|
||||
callLogDaten[index] = log
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Kopie aller Gesprächsnotizen aus dem In-Memory-Speicher.
|
||||
*/
|
||||
export function getCallLogs(): iCallLog[] {
|
||||
return [...callLogDaten]
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Gesprächsnotizen eines bestimmten Projekts. Ist projektId null,
|
||||
* werden alle Gesprächsnotizen zurückgegeben.
|
||||
*/
|
||||
export function getCallLogsByProjektId(projektId: number | null): iCallLog[] {
|
||||
return projektId
|
||||
? callLogDaten.filter((l: iCallLog) => l.projektId === projektId)
|
||||
: [...callLogDaten]
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* REST-API-Aufrufe (Client-Seite) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
|
||||
/* implementiert und werden später nachgereicht. */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/calllog/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
@@ -91,11 +36,19 @@ const CALLLOG_REST_BASE = "/xhr/calllog"
|
||||
|
||||
/**
|
||||
* 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({
|
||||
method: "GET",
|
||||
url: CALLLOG_REST_BASE + "/list"
|
||||
url: CALLLOG_REST_BASE + "/list",
|
||||
params: params
|
||||
}).then((result) => result as iCallLog[])
|
||||
}
|
||||
|
||||
|
||||
+17
-70
@@ -2,12 +2,15 @@ import m from "mithril"
|
||||
|
||||
/**
|
||||
* Datenmodell für einen Kontakt (Ansprechpartner beim Kunden).
|
||||
* Jeder Kontakt ist genau einem Kunden zugeordnet (FK kundenId).
|
||||
* Jeder Kontakt ist genau einem Kunden zugeordnet (FK kunden_id).
|
||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||
* kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||
* Kunde-Relationship abgeleitet und im JSON ergänzt.
|
||||
*/
|
||||
export interface iKontakt {
|
||||
id: number
|
||||
kundenId: number
|
||||
kundenName: string
|
||||
kunden_id: number
|
||||
kunden_name: string
|
||||
anrede: string
|
||||
name: string
|
||||
position: string
|
||||
@@ -16,75 +19,11 @@ export interface iKontakt {
|
||||
notiz: string
|
||||
}
|
||||
|
||||
/**
|
||||
* In-Memory-Datenspeicher für Kontakte. Wird als Fallback solange verwendet,
|
||||
* bis die serverseitigen REST-Endpunkte verfügbar sind.
|
||||
*/
|
||||
export const kontaktDaten: iKontakt[] = [
|
||||
{id: 1, kundenId: 1, kundenName: "Musterfirma GmbH", anrede: "Herr", name: "Max Mustermann", position: "Geschäftsführer", telefon: "+49 123 45678", email: "m.mustermann@musterfirma.de", notiz: "Erster Ansprechpartner für das Website-Projekt."},
|
||||
{id: 2, kundenId: 1, kundenName: "Musterfirma GmbH", anrede: "Frau", name: "Anna Schmidt", position: "Marketingleiterin", telefon: "+49 123 45679", email: "a.schmidt@musterfirma.de", notiz: "Zuständig für Design-Fragen."},
|
||||
{id: 3, kundenId: 2, kundenName: "Beispiel AG", anrede: "Herr", name: "Peter Müller", position: "IT-Leiter", telefon: "+49 543 21012", email: "p.mueller@beispiel-ag.de", notiz: "Technischer Ansprechpartner."},
|
||||
{id: 4, kundenId: 3, kundenName: "Test Unternehmen", anrede: "Frau", name: "Lisa Bauer", position: "Projektmanagerin", telefon: "+49 678 90123", email: "l.bauer@test-unternehmen.com", notiz: ""}
|
||||
]
|
||||
|
||||
/**
|
||||
* Liefert die nächste freie Kontakt-ID auf Basis des In-Memory-Speichers.
|
||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
||||
*/
|
||||
export function getNewKontaktId(): number {
|
||||
if (kontaktDaten.length === 0) {
|
||||
return 1
|
||||
}
|
||||
return Math.max(...kontaktDaten.map((kontakt: iKontakt) => kontakt.id)) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt einen neuen Kontakt dem In-Memory-Speicher hinzu.
|
||||
*/
|
||||
export function addKontakt(kontakt: iKontakt): void {
|
||||
kontaktDaten.push(kontakt)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert einen bestehenden Kontakt im In-Memory-Speicher.
|
||||
*/
|
||||
export function updateKontakt(kontakt: iKontakt): void {
|
||||
const index = kontaktDaten.findIndex((k: iKontakt) => k.id === kontakt.id)
|
||||
if (index !== -1) {
|
||||
kontaktDaten[index] = kontakt
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Kopie aller Kontakte aus dem In-Memory-Speicher.
|
||||
*/
|
||||
export function getKontakte(): iKontakt[] {
|
||||
return [...kontaktDaten]
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Kontakte eines bestimmten Kunden. Ist kundenId null, werden
|
||||
* alle Kontakte zurückgegeben.
|
||||
*/
|
||||
export function getKontakteByKundenId(kundenId: number | null): iKontakt[] {
|
||||
return kundenId
|
||||
? kontaktDaten.filter((k: iKontakt) => k.kundenId === kundenId)
|
||||
: [...kontaktDaten]
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert einen einzelnen Kontakt anhand seiner ID oder undefined.
|
||||
*/
|
||||
export function getKontaktById(id: number): iKontakt | undefined {
|
||||
return kontaktDaten.find((k: iKontakt) => k.id === id)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* REST-API-Aufrufe (Client-Seite) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
|
||||
/* implementiert und werden später nachgereicht. */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/kontakt/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
@@ -94,11 +33,19 @@ const KONTAKT_REST_BASE = "/xhr/kontakt"
|
||||
|
||||
/**
|
||||
* 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({
|
||||
method: "GET",
|
||||
url: KONTAKT_REST_BASE + "/list"
|
||||
url: KONTAKT_REST_BASE + "/list",
|
||||
params: params
|
||||
}).then((result) => result as iKontakt[])
|
||||
}
|
||||
|
||||
|
||||
@@ -2,15 +2,19 @@ import m from "mithril"
|
||||
|
||||
/**
|
||||
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
|
||||
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projektId).
|
||||
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||
* projekt_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||
* Projekt-Relationship abgeleitet und im JSON ergänzt.
|
||||
*/
|
||||
export interface iKontaktadresse {
|
||||
id: number
|
||||
projektId: number
|
||||
projektName: string
|
||||
projekt_id: number
|
||||
projekt_name: string
|
||||
anrede: string
|
||||
name: string
|
||||
firma: string
|
||||
position: string
|
||||
telefon: string
|
||||
email: string
|
||||
strasse: string
|
||||
@@ -20,75 +24,11 @@ export interface iKontaktadresse {
|
||||
notiz: string
|
||||
}
|
||||
|
||||
/**
|
||||
* In-Memory-Datenspeicher für Kontaktadressen. Wird als Fallback solange verwendet,
|
||||
* bis die serverseitigen REST-Endpunkte verfügbar sind.
|
||||
*/
|
||||
export const kontaktadresseDaten: iKontaktadresse[] = [
|
||||
{id: 1, projektId: 1, projektName: "Website-Relaunch", anrede: "Herr", name: "Thomas Becker", firma: "Becker Webdesign GmbH", telefon: "+49 201 123456", email: "t.becker@becker-webdesign.de", strasse: "Industriestr. 12", plz: "45131", ort: "Essen", land: "Deutschland", notiz: "Interessent für Website-Relaunch, Erstkontakt auf Messe."},
|
||||
{id: 2, projektId: 1, projektName: "Website-Relaunch", anrede: "Frau", name: "Sandra Klein", firma: "Klein Marketing", telefon: "+49 211 987654", email: "s.klein@klein-marketing.de", strasse: "Königsallee 5", plz: "40212", ort: "Düsseldorf", land: "Deutschland", notiz: ""},
|
||||
{id: 3, projektId: 2, projektName: "E-Commerce Plattform", anrede: "Herr", name: "Michael Wagner", firma: "Wagner Online Shop", telefon: "+49 221 555123", email: "m.wagner@wagner-shop.de", strasse: "Hauptstr. 88", plz: "50667", ort: "Köln", land: "Deutschland", notiz: "Sucht E-Commerce-Lösung, Budget geklärt."},
|
||||
{id: 4, projektId: 3, projektName: "CRM-System", anrede: "Frau", name: "Julia Hoffmann", firma: "Hoffmann Consulting", telefon: "+49 89 444333", email: "j.hoffmann@hoffmann-consulting.de", strasse: "Marienplatz 3", plz: "80331", ort: "München", land: "Deutschland", notiz: "Wiedervorlage im nächsten Quartal."}
|
||||
]
|
||||
|
||||
/**
|
||||
* Liefert die nächste freie Kontaktadressen-ID auf Basis des In-Memory-Speichers.
|
||||
* Sobald die serverseitige ID-Vergabe verfügbar ist, entfällt diese Funktion.
|
||||
*/
|
||||
export function getNewKontaktadresseId(): number {
|
||||
if (kontaktadresseDaten.length === 0) {
|
||||
return 1
|
||||
}
|
||||
return Math.max(...kontaktadresseDaten.map((ka: iKontaktadresse) => ka.id)) + 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt eine neue Kontaktadresse dem In-Memory-Speicher hinzu.
|
||||
*/
|
||||
export function addKontaktadresse(kontaktadresse: iKontaktadresse): void {
|
||||
kontaktadresseDaten.push(kontaktadresse)
|
||||
}
|
||||
|
||||
/**
|
||||
* Aktualisiert eine bestehende Kontaktadresse im In-Memory-Speicher.
|
||||
*/
|
||||
export function updateKontaktadresse(kontaktadresse: iKontaktadresse): void {
|
||||
const index = kontaktadresseDaten.findIndex((k: iKontaktadresse) => k.id === kontaktadresse.id)
|
||||
if (index !== -1) {
|
||||
kontaktadresseDaten[index] = kontaktadresse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine Kopie aller Kontaktadressen aus dem In-Memory-Speicher.
|
||||
*/
|
||||
export function getKontaktadressen(): iKontaktadresse[] {
|
||||
return [...kontaktadresseDaten]
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert die Kontaktadressen eines bestimmten Projekts. Ist projektId null,
|
||||
* werden alle Kontaktadressen zurückgegeben.
|
||||
*/
|
||||
export function getKontaktadressenByProjektId(projektId: number | null): iKontaktadresse[] {
|
||||
return projektId
|
||||
? kontaktadresseDaten.filter((k: iKontaktadresse) => k.projektId === projektId)
|
||||
: [...kontaktadresseDaten]
|
||||
}
|
||||
|
||||
/**
|
||||
* Liefert eine einzelne Kontaktadresse anhand ihrer ID oder undefined.
|
||||
*/
|
||||
export function getKontaktadresseById(id: number): iKontaktadresse | undefined {
|
||||
return kontaktadresseDaten.find((k: iKontaktadresse) => k.id === id)
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* REST-API-Aufrufe (Client-Seite) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
|
||||
/* implementiert und werden später nachgereicht. */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/kontaktadresse/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
@@ -98,11 +38,19 @@ const KONTAKTADRESSE_REST_BASE = "/xhr/kontaktadresse"
|
||||
|
||||
/**
|
||||
* 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({
|
||||
method: "GET",
|
||||
url: KONTAKTADRESSE_REST_BASE + "/list"
|
||||
url: KONTAKTADRESSE_REST_BASE + "/list",
|
||||
params: params
|
||||
}).then((result) => result as iKontaktadresse[])
|
||||
}
|
||||
|
||||
|
||||
+3
-48
@@ -12,57 +12,11 @@ export interface iKunde {
|
||||
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) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
|
||||
/* implementiert und werden später nachgereicht. */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/kunde/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
@@ -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> {
|
||||
return m.request({
|
||||
|
||||
+16
-69
@@ -2,86 +2,25 @@ import m from "mithril"
|
||||
|
||||
/**
|
||||
* Datenmodell für ein Projekt.
|
||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||
* kunden_name ist keine ORM-Spalte, sondern wird serverseitig aus der
|
||||
* Kunde-Relationship abgeleitet und im JSON ergänzt.
|
||||
*/
|
||||
export interface iProjekt {
|
||||
id: number
|
||||
kundenId: number
|
||||
kundenName: string
|
||||
kunden_id: number
|
||||
kunden_name: string
|
||||
projektname: string
|
||||
zweck: string
|
||||
verantwortlicher: string
|
||||
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) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Die folgenden Funktionen kapseln die Kommunikation mit den */
|
||||
/* serverseitigen REST-Endpunkten. Die Endpunkte sind noch nicht */
|
||||
/* implementiert und werden später nachgereicht. */
|
||||
/* serverseitigen REST-Endpunkten (/xhr/projekt/*). */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
@@ -91,11 +30,19 @@ const PROJEKT_REST_BASE = "/xhr/projekt"
|
||||
|
||||
/**
|
||||
* 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({
|
||||
method: "GET",
|
||||
url: PROJEKT_REST_BASE + "/list"
|
||||
url: PROJEKT_REST_BASE + "/list",
|
||||
params: params
|
||||
}).then((result) => result as iProjekt[])
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user