Server+Client: REST-API für Entität Projekt, Kontakt auf REST umgestellt, ID-basiertes Laden, CLI-Musterprojekte, VS-Code Task
- Server: ORM-Modell Projekt (Tabelle projekte) mit FK kunden_id, Relationship zu Kunde - Server: Geschäftslogik projekt.py (get/create/update/delete) - Server: DB-Migration V01_02 (projekte-Tabelle) - Server: REST-Endpunkte /xhr/projekt/* mit Autorisierung - Server: CLI init-db legt Muster-Projekte je Kunde an (1-3) - Client: kontakt.ts auf REST umgestellt, In-Memory entfernt, snake_case - Client: projekt.ts auf REST umgestellt, In-Memory behalten für call_log/kontaktadresse - Client: ID-basiertes Laden (nur ID in Route-Params, fetchById im oninit) - Client: Kundenname in Überschrift von Kontaktliste/View und Projektliste/View - Client: Select-Widget entfernt (Kunde voreingestellt), Hidden-Input für kunden_id - Client: kunde.ts showProjekte/showKontakte auf kunden_id geändert - Client: call_log.ts/kontaktadresse.ts Feldzugriffe auf iProjekt snake_case angepasst - AGENTS.md: Vorgehensweise ID-basiertes Laden hinterlegt, Branch-Regel formuliert - VS-Code Task "Flask: init-db" mit venv-Pfaden Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
1 parent
4b172081b9
commit
e04a52857a
15 files changed
+647
-287
No files matched your search
@@ -1,23 +1,17 @@
|
||||
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 {iKontakt, fetchKontakte, fetchKontaktById, createKontaktREST, updateKontaktREST} from "../models/kontakt"
|
||||
import {iKunde, fetchKunden} from "../models/kunde"
|
||||
|
||||
function createKontaktTreenodes(kundenId: number | null = null): iTreenode[] {
|
||||
let filteredKontakte = getKontakteByKundenId(kundenId)
|
||||
|
||||
return filteredKontakte.map((kontakt: iKontakt) => {
|
||||
return new Treenode(kontakt, true, [])
|
||||
})
|
||||
}
|
||||
|
||||
export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
let treenodes: iTreenode[] = createKontaktTreenodes()
|
||||
let treenodes: iTreenode[] = []
|
||||
let selectedNode: iTreenode | null = null
|
||||
let filterKundenId: number | null = null
|
||||
// Kundenname für die Überschrift, asynchron aus der Kundenliste
|
||||
// aufgelöst.
|
||||
let kundenName: string = ""
|
||||
// Kundenliste asynchron vom Server laden, für die Kundenauflösung
|
||||
// beim Anlegen eines neuen Kontakts.
|
||||
let kundenListe: iKunde[] = []
|
||||
@@ -27,21 +21,13 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
}
|
||||
|
||||
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 = kundenListe.find((k: iKunde) => k.id === filterKundenId)
|
||||
if (kunde) {
|
||||
params.kundenId = kunde.id
|
||||
params.kundenname = kunde.name
|
||||
params.kunden_id = kunde.id
|
||||
params.kunden_name = kunde.name
|
||||
}
|
||||
}
|
||||
m.route.set("/kontakt", params)
|
||||
@@ -49,17 +35,7 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
|
||||
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) => {
|
||||
@@ -70,7 +46,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"))
|
||||
]
|
||||
}
|
||||
@@ -82,21 +58,39 @@ export const kontaktliste: m.ClosureComponent = (vnode) => {
|
||||
kontakt.position.toLowerCase().includes(searchLower) ||
|
||||
kontakt.telefon.toLowerCase().includes(searchLower) ||
|
||||
kontakt.email.toLowerCase().includes(searchLower) ||
|
||||
kontakt.kundenName.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 = ""
|
||||
}
|
||||
treenodes = createKontaktTreenodes(filterKundenId)
|
||||
// Kontakte asynchron vom Server laden. m.request löst nach
|
||||
// Promise-Auflösung automatisch ein Redraw aus.
|
||||
fetchKontakte().then((result) => {
|
||||
let filtered = result
|
||||
if (filterKundenId !== null) {
|
||||
filtered = result.filter((k: iKontakt) => k.kunden_id === filterKundenId)
|
||||
}
|
||||
treenodes = filtered.map((kontakt: iKontakt) => new Treenode(kontakt, true, []))
|
||||
})
|
||||
// Kundenliste asynchron vom Server laden.
|
||||
fetchKunden().then((result) => {
|
||||
kundenListe = result
|
||||
// Fallback: Falls kunden_name nicht in den Route-Params
|
||||
// enthalten war, aus der Kundenliste auflösen.
|
||||
if (filterKundenId !== null && kundenName === "") {
|
||||
const kunde = kundenListe.find((k: iKunde) => k.id === filterKundenId)
|
||||
if (kunde) {
|
||||
kundenName = kunde.name
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
view: (vnode) => {
|
||||
@@ -106,7 +100,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,
|
||||
{
|
||||
@@ -127,12 +121,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
|
||||
@@ -141,8 +135,8 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
notiz: string
|
||||
} = {
|
||||
id: -1,
|
||||
kundenId: -1,
|
||||
kundenname: "",
|
||||
kunden_id: -1,
|
||||
kunden_name: "",
|
||||
anrede: "",
|
||||
name: "",
|
||||
position: "",
|
||||
@@ -150,44 +144,31 @@ export const Kontakt: m.ClosureComponent = (vnode) => {
|
||||
email: "",
|
||||
notiz: ""
|
||||
}
|
||||
// Ist der Kunde voreingestellt (gefilterte Kontaktliste)?
|
||||
let kundeVoreingestellt: boolean = false
|
||||
// Kundenliste asynchron vom Server laden, für Select-Optionen und
|
||||
// Namensauflösung im submit-Handler.
|
||||
let kundenListe: iKunde[] = []
|
||||
|
||||
return {
|
||||
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 : ""
|
||||
}
|
||||
fetchKunden().then((result) => {
|
||||
kundenListe = result
|
||||
})
|
||||
},
|
||||
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 = kundenListe.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) || "",
|
||||
@@ -196,33 +177,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[] = kundenListe.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)),
|
||||
|
||||
Reference in new issue
Block a user