Im Call-Log-Formular wird "Name Ansprechpartner" durch ein Select ersetzt, das die Kontaktadressen des gewählten Projekts anbietet, sofern der Name nicht bereits voreingestellt ist (z. B. aus der Kontaktadressenliste). Bei Auswahl einer Kontaktadresse werden Name und Telefon automatisch übernommen. Bei Projektwechsel werden Ansprechpartner und Telefon zurückgesetzt. Kontaktadressenliste: add_call-Button pro Zeile, der eine Gesprächsnotiz mit den ForeignKeys aus der Kontaktadresse (projektId, kundenName, name, telefon) anlegt. AGENTS.md: Vorgabe ergänzt — Farben priorisiert aus app-theme.css. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
287 lines
13 KiB
TypeScript
287 lines
13 KiB
TypeScript
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, [])
|
|
})
|
|
}
|
|
|
|
export const kontaktadresseliste: m.ClosureComponent = (vnode) => {
|
|
let treenodes: iTreenode[] = createKontaktadresseTreenodes()
|
|
let selectedNode: iTreenode | null = null
|
|
let filterProjektId: number | null = null
|
|
|
|
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.
|
|
if (filterProjektId !== null) {
|
|
const projekt = getProjekte().find((p: iProjekt) => p.id === filterProjektId)
|
|
if (projekt) {
|
|
params.projektId = projekt.id
|
|
params.projektname = projekt.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
|
|
})
|
|
}
|
|
|
|
const addCallLog = (kontaktadresse: iKontaktadresse, e: Event) => {
|
|
e.stopPropagation()
|
|
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,
|
|
datum: datum,
|
|
uhrzeitBeginn: uhrzeit,
|
|
dauer: "",
|
|
betreff: "",
|
|
nameAnsprechpartner: kontaktadresse.name,
|
|
telefon: kontaktadresse.telefon,
|
|
notiz: "",
|
|
wiedervorlageDatum: "",
|
|
erledigt: false
|
|
})
|
|
}
|
|
|
|
const columnmapper: (node: iTreenode) => any[] = (node: iTreenode) => {
|
|
let kontaktadresse: iKontaktadresse = node.nodevalue
|
|
|
|
return [
|
|
m("div", m("strong", kontaktadresse.name)),
|
|
m("span", kontaktadresse.firma),
|
|
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"))
|
|
)
|
|
]
|
|
}
|
|
|
|
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 {
|
|
oninit: (vnode) => {
|
|
const routeParams = m.route.param()
|
|
if (routeParams && routeParams.projektId) {
|
|
filterProjektId = parseInt(routeParams.projektId)
|
|
} else {
|
|
filterProjektId = null
|
|
}
|
|
treenodes = createKontaktadresseTreenodes(filterProjektId)
|
|
},
|
|
view: (vnode) => {
|
|
return m("div.w3-container.w3-auto", {
|
|
style: {
|
|
display: 'flex',
|
|
'flex-direction': 'column',
|
|
height: 'inherit',
|
|
'overflow-y': 'inherit'}},
|
|
m("h1", "Kontaktadressen"),
|
|
m("div", {style: {flex: '1', 'overflow-y': 'inherit'}},
|
|
m(Treetable,
|
|
{
|
|
treenodes: treenodes,
|
|
columnmapper: columnmapper,
|
|
filter: filterFunction,
|
|
onRowMarked: handleRowMarked,
|
|
isSelected: (node: iTreenode) => node === selectedNode
|
|
} as iTreetable,
|
|
m(TopbarButton, {onclick: addKontaktadresseRoute, tooltip: "Neue Kontaktadresse anlegen"} as iTopbarButton,
|
|
m("i.material-icons", "person_add")
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|
// currentKontaktadresse hält den Formularzustand. projektId referenziert das
|
|
// Projekt eindeutig, projektname dient der Anzeige.
|
|
let currentKontaktadresse: {
|
|
id: number
|
|
projektId: number
|
|
projektname: string
|
|
anrede: string
|
|
name: string
|
|
firma: string
|
|
telefon: string
|
|
email: string
|
|
strasse: string
|
|
plz: string
|
|
ort: string
|
|
land: string
|
|
notiz: string
|
|
} = {
|
|
id: -1,
|
|
projektId: -1,
|
|
projektname: "",
|
|
anrede: "",
|
|
name: "",
|
|
firma: "",
|
|
telefon: "",
|
|
email: "",
|
|
strasse: "",
|
|
plz: "",
|
|
ort: "",
|
|
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 || ""
|
|
}
|
|
}
|
|
},
|
|
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 : "",
|
|
anrede: (formData.get("anrede") as string) || "",
|
|
name: (formData.get("name") as string) || "",
|
|
firma: (formData.get("firma") as string) || "",
|
|
telefon: (formData.get("telefon") as string) || "",
|
|
email: (formData.get("email") as string) || "",
|
|
strasse: (formData.get("strasse") as string) || "",
|
|
plz: (formData.get("plz") as string) || "",
|
|
ort: (formData.get("ort") as string) || "",
|
|
land: (formData.get("land") as string) || "",
|
|
notiz: (formData.get("notiz") as string) || ""
|
|
}
|
|
if (currentKontaktadresse.id === -1) {
|
|
gespeicherteKontaktadresse.id = getNewKontaktadresseId()
|
|
addKontaktadresse(gespeicherteKontaktadresse)
|
|
} else {
|
|
updateKontaktadresse(gespeicherteKontaktadresse)
|
|
}
|
|
m.route.set("/kontaktadresseliste")
|
|
return true
|
|
}
|
|
const cancel = (e: Event) => {
|
|
m.route.set("/kontaktadresseliste")
|
|
}
|
|
// 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("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("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)),
|
|
m("label", {style: {flex: '1'}}, "Ort",
|
|
m(Input, {name: "ort", value: currentKontaktadresse.ort, type: "text"} as iInput))
|
|
),
|
|
m("label", "Land", m(Input, {name: "land", value: currentKontaktadresse.land, type: "text"} as iInput)),
|
|
m("label", "Notiz", m(Textarea, {name: "notiz", value: currentKontaktadresse.notiz, rows: 4} as iTextarea))
|
|
])
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|