Files
kbcrm/Client/Components/kontaktadresse.ts
T
gg a8838a1a38 Call_Log: Ansprechpartner als Select aus Kontaktadressen des Projekts
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>
2026-09-01 18:21:28 +02:00

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))
])
)
)
}
}
}