feat(crm): Aufgabenbeschreibung des Projekts prominent zugänglich machen

- Projektmaske: Zweck-Feld als Textarea (längere Eingaben), Label
  in "Aufgabenbeschreibung" umbenannt
- Tages-Workflow-Maske: Aufgabenbeschreibung als Tooltip auf dem
  Projekt-Label/-Namen der Kopfzeile, plus Icon-Button, der ein Modal
  mit Aufgabenbeschreibung, Verantwortlichem und Startdatum öffnet
- Server: Kontaktadresse.to_dict() liefert zusätzlich projekt_zweck

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
gg committed 2026-09-27 16:12:00 +02:00
1 parent 6e3cc7375d
commit a23884bf02
5 files changed
+81 -10

No files matched your search

+5 -4
View File
@@ -263,13 +263,14 @@ class Kontaktadresse(dbapp.Model):
def to_dict(self):
"""
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
über columns2dict serialisiert; projekt_name und kunden_name werden
zusätzlich aus der Projekt- bzw. Kunde-Relationship eingefügt.
is_erledigt wird dynamisch aus den Gesprächsnotizen des Kontakts
berechnet.
über columns2dict serialisiert; projekt_name, projekt_zweck und
kunden_name werden zusätzlich aus der Projekt- bzw. Kunde-Relationship
eingefügt. is_erledigt wird dynamisch aus den Gesprächsnotizen des
Kontakts berechnet.
"""
result = columns2dict(self)
result['projekt_name'] = self.projekt.projektname if self.projekt else None
result['projekt_zweck'] = self.projekt.zweck if self.projekt else None
result['kunden_name'] = self.projekt.kunde.name if self.projekt and self.projekt.kunde else None
result['is_erledigt'] = self.is_erledigt()
return result
+1
View File
@@ -237,6 +237,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
id: currentKontaktadresse.id,
projekt_id: currentKontaktadresse.projekt_id,
projekt_name: "",
projekt_zweck: "",
// Server-seitig aus den Relationships abgeleitet;
// Platzhalter beim Senden.
kunden_name: "",
+2 -2
View File
@@ -1,6 +1,6 @@
import m from "mithril"
import {Form, iForm} from "./widgets/form"
import { Input, iInput} from "./widgets/inputs"
import {Input, iInput, Textarea, iTextarea} from "./widgets/inputs"
import {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
import {BreadcrumbBar, iBreadcrumb, iBreadcrumbBar, parseBc, serializeBc} from "./widgets/breadcrumb"
import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt"
@@ -275,7 +275,7 @@ export const Projekt: m.ClosureComponent = (vnode) => {
m(Form, guard.formAttrs(submit) as iForm, [
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", "Aufgabenbeschreibung", m(Textarea, {name: "zweck", value: currentProjekt.zweck, rows: 4} as iTextarea)),
m("label", "Verantwortlicher", m(Input, {name: "verantwortlicher", value: currentProjekt.verantwortlicher, type: "text"} as iInput)),
m("label", "Startdatum", m(Input, {name: "startdatum", value: currentProjekt.startdatum, type: "date"} as iInput))
])
+70 -2
View File
@@ -10,6 +10,7 @@ import {DirtyMaskHelper} from "./widgets/dirtyguard"
import {serializeBc, iBreadcrumb} from "./widgets/breadcrumb"
import {iKunde, fetchKunden} from "../models/kunde"
import {iKontaktadresse, fetchKontaktadresseById} from "../models/kontaktadresse"
import {iProjekt, fetchProjektById} from "../models/projekt"
import {iCallLog, fetchCallLogsByKontaktadresse, createCallLogREST} from "../models/call_log"
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
import {iTagesWorkflowEntry, iTagesWorkflowFilter, fetchTagesWorkflowList} from "../models/tages_workflow"
@@ -700,6 +701,53 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
m.route.set("/kontaktadresse", {id: geladeneId, bc: bc})
}
/**
* Öffnet ein Modal mit den Projektdetails (Aufgabenbeschreibung,
* Verantwortlicher, Startdatum) des Projekts des aktuellen Kontakts
* (Info-Button hinter dem Projektnamen in der Kopfzeile). Das
* Projekt wird on-demand frisch vom Server geladen, damit
* Verantwortlicher und Startdatum aktuell sind.
*/
const zweckAnzeigen = () => {
if (!kontaktadresse) return
const kontaktadresseId = kontaktadresse.id
fetchProjektById(kontaktadresse.projekt_id).then((projekt: iProjekt) => {
m.mount(gridMountpoints.modalhook, {
view: () => {
return m(ModalView,
{
title: "Aufgabenbeschreibung des Projekts",
type: ModalType.MESSAGE,
width: '40%'
} as iModalView,
m("div.w3-panel",
m("p", m("strong", "Projekt: "), projekt.projektname),
m("p", m("strong", "Verantwortlicher: "), projekt.verantwortlicher || "—"),
m("p", m("strong", "Startdatum: "), projekt.startdatum || "—"),
m("p", m("strong", "Aufgabenbeschreibung: ")),
m("p", projekt.zweck
? projekt.zweck
: "Für dieses Projekt ist keine Aufgabenbeschreibung hinterlegt.")),
m("div.w3-bar.w3-padding-16",
m("button.w3-button.w3-theme.w3-right",
{
onclick: () => {
m.mount(gridMountpoints.modalhook, null)
}
},
"Schließen"))
)
}
})
}).catch((e: any) => {
// Der Kontakt kann inzwischen gewechselt haben — dann das
// Modal für den alten Kontakt nicht mehr öffnen.
if (kontaktadresse && kontaktadresse.id === kontaktadresseId) {
showServerError(e)
}
})
}
return m("div.w3-container.w3-auto.form-view-container",
m("h1", "Tages-Workflow" + (kontaktadresse ? ": " + kontaktadresse.name : "")),
// Hinweis, falls beim Weiterschalten ein Kontakt übersprungen wurde.
@@ -721,8 +769,28 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
m("div.w3-half",
m("p", {style: {margin: '4px 0'}},
m("strong", "Kunde: "), kontaktadresse ? kontaktadresse.kunden_name : ""),
m("p", {style: {margin: '4px 0'}},
m("strong", "Projekt: "), kontaktadresse ? kontaktadresse.projekt_name : "")),
// Zweck des Projekts als Tooltip: wird beim
// Überfahren von Label und Projektname angezeigt.
m("p", {
style: {margin: '4px 0'},
title: kontaktadresse && kontaktadresse.projekt_zweck ? kontaktadresse.projekt_zweck : undefined
},
m("strong", "Projekt: "), kontaktadresse ? kontaktadresse.projekt_name : "",
// Info-Button hinter dem Projektnamen: öffnet
// ein Modal mit der Aufgabenbeschreibung
// (Alternative zum Tooltip, auch auf
// Touch-Geräten nutzbar).
// Ohne Hintergrundfarbe — nur das Icon ist
// sichtbar, der w3-button liefert weiterhin
// den Hover-Effekt als Klick-Feedback.
m("button.w3-button.w3-small",
{
type: "button",
title: "Projekt-Aufgabenbeschreibung anzeigen",
style: {padding: '0 6px', 'margin-left': '4px'},
onclick: zweckAnzeigen
},
m("i.material-icons", {style: {fontSize: '16px'}}, "info")))),
m("div.w3-half",
m("p", {style: {margin: '4px 0'}},
m("strong", "Kontakt: "),
+3 -2
View File
@@ -4,8 +4,8 @@ import m from "mithril"
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
* serverseitig aus den Projekt-/Kunde-Relationships abgeleitet und im
* projekt_name, projekt_zweck und kunden_name sind keine ORM-Spalten, sondern
* werden serverseitig aus den Projekt-/Kunde-Relationships abgeleitet und im
* JSON ergänzt. is_erledigt ist ebenfalls keine Spalte, sondern wird
* serverseitig dynamisch berechnet: True, sobald mindestens eine
* Gesprächsnotiz des Kontakts einen Tag mit is_erledigend=True hat.
@@ -14,6 +14,7 @@ export interface iKontaktadresse {
id: number
projekt_id: number
projekt_name: string
projekt_zweck: string
kunden_name: string
anrede: string
name: string