Server+Client: REST-API für Kontaktadresse und CallLog, In-Memory vollständig entfernt, CLI-Musterdaten, Route-Param-Fix
- Server: ORM-Modelle Kontaktadresse (kontaktadressen) und CallLog (call_logs) mit FK projekt_id - Server: Geschäftslogik kontaktadresse.py und call_log.py (get/create/update/delete) - Server: DB-Migration V01_03 (kontaktadressen + call_logs Tabellen) - Server: REST-Endpunkte /xhr/kontaktadresse/* und /xhr/calllog/* mit Autorisierung - Server: CLI init-db legt Muster-Kontaktadressen (1-3/Projekt) und Muster-Gesprächsnotizen (2-5/Projekt) an - Client: kontaktadresse.ts auf REST umgestellt, snake_case, In-Memory entfernt - Client: call_log.ts auf REST umgestellt, snake_case, In-Memory entfernt, Submit-Handler implementiert - Client: projekt.ts In-Memory vollständig entfernt (keine weiteren Verbraucher) - Client: Route-Params projektId → projekt_id korrigiert (3 Stellen in projekt.ts) - Client: addCallLog in projekt.ts Route-Params auf snake_case angepasst - AGENTS.md: Erinnerung client-build-Skill nach .ts-Änderungen hinzugefügt Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
e04a52857a
commit
b510c50497
13 files changed
+821
-503
No files matched your search
@@ -1,48 +1,32 @@
|
||||
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, fetchProjekte} 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 aus der Projektliste
|
||||
// aufgelöst.
|
||||
let projektName: string = ""
|
||||
// Projektliste asynchron vom Server laden, für die Projektauflösung
|
||||
// beim Anlegen einer neuen Kontaktadresse.
|
||||
let projektListe: iProjekt[] = []
|
||||
|
||||
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)
|
||||
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
|
||||
if (projekt) {
|
||||
params.projektId = projekt.id
|
||||
params.projektname = projekt.projektname
|
||||
params.projekt_id = projekt.id
|
||||
params.projekt_name = projekt.projektname
|
||||
}
|
||||
}
|
||||
m.route.set("/kontaktadresse", params)
|
||||
@@ -50,21 +34,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
|
||||
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 +42,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.kunden_name : "",
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -100,7 +67,7 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
m("span", kontaktadresse.telefon),
|
||||
m("span", kontaktadresse.email),
|
||||
m("span", kontaktadresse.ort),
|
||||
m("span", kontaktadresse.projektName),
|
||||
m("span", kontaktadresse.projekt_name),
|
||||
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"))
|
||||
@@ -116,18 +83,37 @@ export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
||||
kontaktadresse.telefon.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.email.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.ort.toLowerCase().includes(searchLower) ||
|
||||
kontaktadresse.projektName.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 = ""
|
||||
}
|
||||
treenodes = createKontaktadresseTreenodes(filterProjektId)
|
||||
// Kontaktadressen asynchron vom Server laden.
|
||||
fetchKontaktadressen().then((result) => {
|
||||
let filtered = result
|
||||
if (filterProjektId !== null) {
|
||||
filtered = result.filter((k: iKontaktadresse) => k.projekt_id === filterProjektId)
|
||||
}
|
||||
treenodes = filtered.map((kontaktadresse: iKontaktadresse) => new Treenode(kontaktadresse, true, []))
|
||||
})
|
||||
// Projektliste asynchron vom Server laden.
|
||||
fetchProjekte().then((result) => {
|
||||
projektListe = result
|
||||
if (filterProjektId !== null && projektName === "") {
|
||||
const projekt = projektListe.find((p: iProjekt) => p.id === filterProjektId)
|
||||
if (projekt) {
|
||||
projektName = projekt.projektname
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
return m("div.w3-container.w3-auto", {
|
||||
@@ -136,7 +122,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,12 +143,12 @@ 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
|
||||
@@ -175,8 +161,8 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
||||
notiz: string
|
||||
} = {
|
||||
id: -1,
|
||||
projektId: -1,
|
||||
projektname: "",
|
||||
projekt_id: -1,
|
||||
projekt_name: "",
|
||||
anrede: "",
|
||||
name: "",
|
||||
firma: "",
|
||||
@@ -188,42 +174,29 @@ 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) || "",
|
||||
@@ -236,40 +209,28 @@ 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", "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)),
|
||||
|
||||
Reference in new issue
Block a user