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:
1 parent
6e3cc7375d
commit
a23884bf02
5 files changed
+81
-10
No files matched your search
@@ -263,13 +263,14 @@ class Kontaktadresse(dbapp.Model):
|
|||||||
def to_dict(self):
|
def to_dict(self):
|
||||||
"""
|
"""
|
||||||
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
|
Liefert die Kontaktdaten als Dictionary. Die Tabellenspalten werden
|
||||||
über columns2dict serialisiert; projekt_name und kunden_name werden
|
über columns2dict serialisiert; projekt_name, projekt_zweck und
|
||||||
zusätzlich aus der Projekt- bzw. Kunde-Relationship eingefügt.
|
kunden_name werden zusätzlich aus der Projekt- bzw. Kunde-Relationship
|
||||||
is_erledigt wird dynamisch aus den Gesprächsnotizen des Kontakts
|
eingefügt. is_erledigt wird dynamisch aus den Gesprächsnotizen des
|
||||||
berechnet.
|
Kontakts berechnet.
|
||||||
"""
|
"""
|
||||||
result = columns2dict(self)
|
result = columns2dict(self)
|
||||||
result['projekt_name'] = self.projekt.projektname if self.projekt else None
|
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['kunden_name'] = self.projekt.kunde.name if self.projekt and self.projekt.kunde else None
|
||||||
result['is_erledigt'] = self.is_erledigt()
|
result['is_erledigt'] = self.is_erledigt()
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -237,6 +237,7 @@ export const Kontaktadresse: m.ClosureComponent = (vnode) => {
|
|||||||
id: currentKontaktadresse.id,
|
id: currentKontaktadresse.id,
|
||||||
projekt_id: currentKontaktadresse.projekt_id,
|
projekt_id: currentKontaktadresse.projekt_id,
|
||||||
projekt_name: "",
|
projekt_name: "",
|
||||||
|
projekt_zweck: "",
|
||||||
// Server-seitig aus den Relationships abgeleitet;
|
// Server-seitig aus den Relationships abgeleitet;
|
||||||
// Platzhalter beim Senden.
|
// Platzhalter beim Senden.
|
||||||
kunden_name: "",
|
kunden_name: "",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import m from "mithril"
|
import m from "mithril"
|
||||||
import {Form, iForm} from "./widgets/form"
|
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 {Treenode, iTreenode, Treetable, iTreetable, TopbarButton, iTopbarButton} from "./widgets/Treetable_V2"
|
||||||
import {BreadcrumbBar, iBreadcrumb, iBreadcrumbBar, parseBc, serializeBc} from "./widgets/breadcrumb"
|
import {BreadcrumbBar, iBreadcrumb, iBreadcrumbBar, parseBc, serializeBc} from "./widgets/breadcrumb"
|
||||||
import {iProjekt, fetchProjekte, fetchProjektById, createProjektREST, updateProjektREST} from "../models/projekt"
|
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(Form, guard.formAttrs(submit) as iForm, [
|
||||||
m("input", {type: "hidden", name: "kunden_id", value: currentProjekt.kunden_id}),
|
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", "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", "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))
|
m("label", "Startdatum", m(Input, {name: "startdatum", value: currentProjekt.startdatum, type: "date"} as iInput))
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {DirtyMaskHelper} from "./widgets/dirtyguard"
|
|||||||
import {serializeBc, iBreadcrumb} from "./widgets/breadcrumb"
|
import {serializeBc, iBreadcrumb} from "./widgets/breadcrumb"
|
||||||
import {iKunde, fetchKunden} from "../models/kunde"
|
import {iKunde, fetchKunden} from "../models/kunde"
|
||||||
import {iKontaktadresse, fetchKontaktadresseById} from "../models/kontaktadresse"
|
import {iKontaktadresse, fetchKontaktadresseById} from "../models/kontaktadresse"
|
||||||
|
import {iProjekt, fetchProjektById} from "../models/projekt"
|
||||||
import {iCallLog, fetchCallLogsByKontaktadresse, createCallLogREST} from "../models/call_log"
|
import {iCallLog, fetchCallLogsByKontaktadresse, createCallLogREST} from "../models/call_log"
|
||||||
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
import {iTag, fetchTags, berechneWiedervorlageDatum} from "../models/tag"
|
||||||
import {iTagesWorkflowEntry, iTagesWorkflowFilter, fetchTagesWorkflowList} from "../models/tages_workflow"
|
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})
|
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",
|
return m("div.w3-container.w3-auto.form-view-container",
|
||||||
m("h1", "Tages-Workflow" + (kontaktadresse ? ": " + kontaktadresse.name : "")),
|
m("h1", "Tages-Workflow" + (kontaktadresse ? ": " + kontaktadresse.name : "")),
|
||||||
// Hinweis, falls beim Weiterschalten ein Kontakt übersprungen wurde.
|
// Hinweis, falls beim Weiterschalten ein Kontakt übersprungen wurde.
|
||||||
@@ -721,8 +769,28 @@ export const TagesWorkflowMaske: m.ClosureComponent = () => {
|
|||||||
m("div.w3-half",
|
m("div.w3-half",
|
||||||
m("p", {style: {margin: '4px 0'}},
|
m("p", {style: {margin: '4px 0'}},
|
||||||
m("strong", "Kunde: "), kontaktadresse ? kontaktadresse.kunden_name : ""),
|
m("strong", "Kunde: "), kontaktadresse ? kontaktadresse.kunden_name : ""),
|
||||||
m("p", {style: {margin: '4px 0'}},
|
// Zweck des Projekts als Tooltip: wird beim
|
||||||
m("strong", "Projekt: "), kontaktadresse ? kontaktadresse.projekt_name : "")),
|
// Ü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("div.w3-half",
|
||||||
m("p", {style: {margin: '4px 0'}},
|
m("p", {style: {margin: '4px 0'}},
|
||||||
m("strong", "Kontakt: "),
|
m("strong", "Kontakt: "),
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import m from "mithril"
|
|||||||
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
|
* Datenmodell für eine Kontaktadresse (Akquise-Ziel innerhalb eines Projekts).
|
||||||
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
* Jede Kontaktadresse ist genau einem Projekt zugeordnet (FK projekt_id).
|
||||||
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
* Die Feldnamen entsprechen den serverseitigen ORM-Spalten (snake_case).
|
||||||
* projekt_name und kunden_name sind keine ORM-Spalten, sondern werden
|
* projekt_name, projekt_zweck und kunden_name sind keine ORM-Spalten, sondern
|
||||||
* serverseitig aus den Projekt-/Kunde-Relationships abgeleitet und im
|
* werden serverseitig aus den Projekt-/Kunde-Relationships abgeleitet und im
|
||||||
* JSON ergänzt. is_erledigt ist ebenfalls keine Spalte, sondern wird
|
* JSON ergänzt. is_erledigt ist ebenfalls keine Spalte, sondern wird
|
||||||
* serverseitig dynamisch berechnet: True, sobald mindestens eine
|
* serverseitig dynamisch berechnet: True, sobald mindestens eine
|
||||||
* Gesprächsnotiz des Kontakts einen Tag mit is_erledigend=True hat.
|
* Gesprächsnotiz des Kontakts einen Tag mit is_erledigend=True hat.
|
||||||
@@ -14,6 +14,7 @@ export interface iKontaktadresse {
|
|||||||
id: number
|
id: number
|
||||||
projekt_id: number
|
projekt_id: number
|
||||||
projekt_name: string
|
projekt_name: string
|
||||||
|
projekt_zweck: string
|
||||||
kunden_name: string
|
kunden_name: string
|
||||||
anrede: string
|
anrede: string
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
Reference in new issue
Block a user