Typspezifische Zusatzinfos in Vertragslisten

Jede Vertragszeile zeigt jetzt eine kontextspezifische Zusatzinfo an:
- Strom/Gas: "Lieferadresse: Musterstr. 12, 12345 Berlin"
- DSL/Glasfaser/Kabel: "Anschlussadresse: ..."
- Mobilfunk: "Rufnummer: 0171 1234567" (Hauptkarte bevorzugt)
- KFZ: "Kennzeichen: HB-AB 123"

Sichtbar in:
- Admin-Vertragsliste (/contracts)
- Portal-Vertragsliste (Baumansicht)
- Kunden-Detail → Verträge-Tab

Backend: getAllContracts + getContractTreeForCustomer liefern
mobileDetails (mit simCards), carInsuranceDetails und address mit.

Frontend: Neuer Helper utils/contractInfo.ts mit getContractTypeInfo,
aus dem sowohl Label als auch Wert pro Typ kommt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
duffyduck 2026-04-23 10:19:04 +02:00
parent eaf7d1eac3
commit 0d58b79836
6 changed files with 165 additions and 81 deletions

View File

@ -95,6 +95,13 @@ export async function getAllContracts(filters: ContractFilters) {
provider: true, provider: true,
tariff: true, tariff: true,
contractCategory: true, contractCategory: true,
mobileDetails: {
select: {
phoneNumber: true,
simCards: { select: { phoneNumber: true, isMain: true } },
},
},
carInsuranceDetails: { select: { licensePlate: true } },
}, },
}), }),
prisma.contract.count({ where }), prisma.contract.count({ where }),
@ -845,6 +852,13 @@ export interface ContractTreeNode {
provider?: { id: number; name: string } | null; provider?: { id: number; name: string } | null;
tariff?: { id: number; name: string } | null; tariff?: { id: number; name: string } | null;
contractCategory?: { id: number; name: string } | null; contractCategory?: { id: number; name: string } | null;
customer?: { id: number; firstName: string; lastName: string; companyName: string | null; customerNumber: string } | null;
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
simCards: { phoneNumber: string | null; isMain: boolean }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}; };
predecessors: ContractTreeNode[]; predecessors: ContractTreeNode[];
hasHistory: boolean; hasHistory: boolean;
@ -875,6 +889,15 @@ export async function getContractTreeForCustomer(customerId: number): Promise<Co
provider: { select: { id: true, name: true } }, provider: { select: { id: true, name: true } },
tariff: { select: { id: true, name: true } }, tariff: { select: { id: true, name: true } },
contractCategory: { select: { id: true, name: true } }, contractCategory: { select: { id: true, name: true } },
customer: { select: { id: true, firstName: true, lastName: true, companyName: true, customerNumber: true } },
address: { select: { street: true, houseNumber: true, postalCode: true, city: true } },
mobileDetails: {
select: {
phoneNumber: true,
simCards: { select: { phoneNumber: true, isMain: true } },
},
},
carInsuranceDetails: { select: { licensePlate: true } },
}, },
orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }], orderBy: [{ startDate: 'desc' }, { createdAt: 'desc' }],
}); });

View File

@ -1,101 +1,75 @@
Vertragliste bei Energie mit Anschlussadresse/Lieferadresse noch in der Liste # 📋 OpenCRM Todo-Liste
Bei Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
Bei Festnetz, die Anschlussadresse/Lieferadresse
Bei KFZ das Kennzeichen
# ende
#erledigt ---
Datenschutzerklärung wenn PDF hinterlegt wurde, alle Haken auf Grün setzten.
Und wenn von Kunde im Kundenportal ein Haken weg, pdf wieder löschen und gesperrt setzen. bis endweder alle haken wieder gesetzt sind, oder pdf erneut hochgeladen
Aktuell zählt das PDF als Alternative zu den Online-Haken. Du willst es so:
PDF hochgeladen → alle 4 Online-Consents automatisch auf GRANTED setzen ## 🔜 Offen
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
# ende
#erledigt ### Vertragsliste erweitern
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen. Zusatzinfos in der Vertragsliste anzeigen:
Auch in die Berechnung die Verbäuche dann darstellen - **Energie:** Anschlussadresse / Lieferadresse
# ende - **Mobilfunk:** Mobilfunknummer + Karteninhaber (wenn vorhanden)
- **Festnetz:** Anschlussadresse / Lieferadresse
- **KFZ:** Kennzeichen
#erledigt ### Email Log & System testen
Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist - Senden testen
- Empfangen testen
Jetzt : 1.1.2026 ### Security System testen
Und gewollt 01.01.2026
# ende
### Geburtstagskalender
- Geburtstagsgruß als Modal beim ersten Login am Geburtstag
- Bis 7 Tage nach Geburtstag: nachträgliche Glückwünsche
#erledigt ### Email → Vertragsdokumente
Die Auditmeldungen aussagekräftig Wenn eine Email einem Vertrag zugeordnet ist:
# ende - Anhänge auch in Vertragsdokumente speichern
- Rechnungen wie Kündigungsdokumente behandeln
Email Log und system testen ---
Sprich senden und Empfnagen
# ende
Security System testen ## ✅ Erledigt
# ende
#erledigt - [x] **Datenschutzerklärung PDF ↔ Online-Einwilligungen synchronisieren**
Datenschutzerklärung Website unserer Seite und ein impressum im Kundenportal. - PDF hochgeladen → alle 4 Consents auf GRANTED
- Haken entfernt im Portal → PDF löschen + Tabs sperren
- Entsperrung nur durch alle Haken oder neues PDF
Auch wieder über das Einstellungsmenü editirerbar. - [x] **Zweitarif-Zähler (HT/NT)** bei Strom + Verbrauchsberechnung
Bitte mach mir da auch einen Vorschagstext rein
# ende
Geburtstagskalender, und Geburtgsgruß als Modal beim ersten Login an dem Tag, - [x] **Datumsformate vereinheitlichen** (01.01.2026 statt 1.1.2026)
Sollte der Login bis n7 btage nach Geburtsag sein dann Glückwunsch nachträglich
# ende
#erledigt - [x] **Audit-Log aussagekräftig** (Vorher/Nachher bei allen Änderungen)
Bei der Email datenschutzerklärung erst wenn alle hebel drin sind, auf einen bestätigungsbutton klicken, um sicherzustellen, das alle heben drin sind.
Danch bestätigen, nochmals eine Bestätigiguns emails enden.
Denn jetzt kann der Kudne auch nur einen Haken auslassen, das würd uns aber nichts bringen. - [x] **Impressum + Website-Datenschutzerklärung** im Kundenportal
- Editor in Einstellungen
- Vorschlagstexte
# ende - [x] **Consent-Bestätigungs-Flow per Email**
- Alle Hebel müssen gesetzt sein
- Bestätigungsbutton + Bestätigungsemail
#erledigt - [x] **Vertragsdokumente-Upload** (Auftragsformular, Lieferbestätigung, Vertragsunterlagen als PDF/PNG)
Haben wir bei den Vertragen (also alle) ein Dokumentfeld zum Upload von, Auftragsformular, Lieferbestätigung, Vertragsunterlagen?
hier sind wieder png,pdf erlaubt
# ende
- [x] **Bug: Stressfrei-Email im Auftragsgenerator** (funktioniert jetzt im Vertrag)
EWE Auftragsformular generieren aus Kundendaten, nur wie bei Neuvertrag. Hinter folgevertrag vielleicht ein Pfeil als Drop down und dann Kann man da Neuer Auftrag EWE, später die Liste erweitern mit Moon usw. was man halt hat aber am anfang EWE. Das wäre dann eine PDF die von der EWE kommt die dann ausgefüllt werden soll. - [x] **PDF-Auftragsvorlagen-System**
Und wenn es der Erste Vertrag wäre , dann beim Kunden im TAB Verträge, vielleich dann ein auch ein Pfeil nach unten hinter dem Button mit dem Namen Vertrag hinzufügen. - Template-Editor in Einstellungen
Ist die Frage wo legen wir die PDF Vorlagen hin. - PDF hochladen, Formularfelder automatisch auslesen
Vielleicht sogar ein Editor, für Vorlagen wo man dann rein zieht an welcher stelle, welches feld stehen soll aus den kunden daten. - CRM-Felder zuordnen (visuell mit Vorschau)
Dann kann man das für weiter Formulare machen die PDF sind. - Seitenweise Sortierung der Felder
Moon fachhandle als Beispiel, hat ne API, deshalb kommt das später ;-) - Dynamische Rufnummern-Felder mit Vorwahl-Extraktion
- Nicht zugeordnete Felder bleiben editierbar
- Auftrag generieren aus Vertragsdaten (Button im Vertrags-Detail)
Bei Rufnummern wäre das interessant wie man das dann realisiert, weil jede rufnumemr ja ein einzelenen feld ist, aber wir ja vorher nicht wissen wie viele rufnumemrn der kunde hat. allerdings muss man auch das maximum angeben können, denn wenn nur 8 felder sa sind kann man nur 8 rufnummern portieren. Oder es wird eine Extra seite hinten angehangen also erstellt weitere Rufnummern und bei dem Origionaldokument ein Hinweis, weitere Rufnummern siehe letzte Seite, wenn das da erschöpft sein sollte - [x] **Eigentümer-Verwaltung**
# ende - An Adresse gehängt (Firma, Vorname, Nachname, Anschrift, Kontakt)
- Fallback auf Kundendaten wenn leer
- Nur bei Liefer-/Meldeadressen (nicht Rechnung)
- Namens-Kombinationen (Firma + Vorname + Nachname etc.)
- [x] **Gruppenauswahl Liefer-/Rechnungs-/Eigentümer-Adresse** im Auftragsgenerator
Aus der EMail wenn Vertrag zugeordnet ist, Anhang speichern auch in Vertragsdokumente - [x] **Objekttyp + Lage + Lage des Anschlusses** bei Festnetz-Verträgen (DSL/Glasfaser/Kabel)
Und Rechnungen wie bei den Kündigungsdokumenten
# ende
- [x] **Bankverbindung-Fallback** im PDF-Generator (neueste aktive Bankverbindung des Kunden)
Da steht noch Eigentümer,
Wo können wir das am besten in unser System verpacken?
Denn bei Festnetz und Energieprodukten ist das relevant.
Denn wenn der Kunde zu Miete wohnt ist er nicht Eigentümer.
Ein Eigentümertümer kann auch eine Firma sein, bei ner Wohnungsbaugesselschft zum beispiel,
DA müssten wir auch wieder name Firma etc.
Eigentlich müssten wir das unter adressen packen.
Vielleicht mit ner Möglichkeit wenn eigentümer nicht ausgefüllt ist, ist der Kudne immer selbst Eigentümer
ABer es könnte ja auch mehrere Objekte mit verschiedenen Eigentümern geben.
Scheiße wie am sinnvollsten lösen
# ende
#erledigt
Bug auswahl stressfrei email geht nich im Auftragsgenerator
Fun ktioniert nicht in der Vorschau, sondern nur im Vertrag selbst
# ende
Es soll auch zwischen Lieferadresse und Rechungsadresse ausgewählt werden können. als Gruppe. Beudetet wenn eine Feldgruppe aus einer gruppe entweder liefer / Rechnung / oder eigentümer. Dann soll man das auswählen können
# ende

View File

@ -13,6 +13,7 @@ import CopyButton from '../../components/ui/CopyButton';
import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react'; import { Plus, Search, Eye, Edit, Trash2, User, Users, ChevronDown, ChevronRight, Info, X, ShieldAlert } from 'lucide-react';
import { gdprApi } from '../../services/api'; import { gdprApi } from '../../services/api';
import { formatDate } from '../../utils/dateFormat'; import { formatDate } from '../../utils/dateFormat';
import { getContractTypeInfo } from '../../utils/contractInfo';
import type { Contract, ContractType, ContractStatus } from '../../types'; import type { Contract, ContractType, ContractStatus } from '../../types';
const typeLabels: Record<ContractType, string> = { const typeLabels: Record<ContractType, string> = {
@ -306,6 +307,14 @@ export default function ContractList() {
<CopyButton value={(contract.providerName || contract.provider?.name || '') + ((contract.tariffName || contract.tariff?.name) ? ` - ${contract.tariffName || contract.tariff?.name}` : '')} /> <CopyButton value={(contract.providerName || contract.provider?.name || '') + ((contract.tariffName || contract.tariff?.name) ? ` - ${contract.tariffName || contract.tariff?.name}` : '')} />
</p> </p>
)} )}
{(() => {
const typeInfo = getContractTypeInfo(contract as any);
return typeInfo ? (
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
</p>
) : null;
})()}
{contract.startDate && ( {contract.startDate && (
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}> <p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
Beginn: {formatDate(contract.startDate)} Beginn: {formatDate(contract.startDate)}
@ -455,12 +464,19 @@ export default function ContractList() {
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{data.data.map((contract) => ( {data.data.map((contract) => {
const typeInfo = getContractTypeInfo(contract as any);
return (
<tr key={contract.id} className="border-b hover:bg-gray-50"> <tr key={contract.id} className="border-b hover:bg-gray-50">
<td className="py-3 px-4 font-mono text-sm"> <td className="py-3 px-4 font-mono text-sm">
<Link to={`/contracts/${contract.id}`} state={pushHistory('/contracts')} className="text-blue-600 hover:underline"> <Link to={`/contracts/${contract.id}`} state={pushHistory('/contracts')} className="text-blue-600 hover:underline">
{contract.contractNumber} {contract.contractNumber}
</Link> </Link>
{typeInfo && (
<div className="text-xs text-gray-500 font-sans mt-0.5">
<span className="font-medium text-gray-600">{typeInfo.label}:</span> {typeInfo.value}
</div>
)}
</td> </td>
{!isCustomer && ( {!isCustomer && (
<td className="py-3 px-4"> <td className="py-3 px-4">
@ -529,7 +545,8 @@ export default function ContractList() {
</div> </div>
</td> </td>
</tr> </tr>
))} );
})}
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -16,6 +16,7 @@ import FileUpload from '../../components/ui/FileUpload';
import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft } from 'lucide-react'; import { Edit, Plus, Trash2, MapPin, CreditCard, FileText, Gauge, Eye, EyeOff, Download, Globe, UserPlus, X, Search, Mail, Copy, Check, ChevronDown, ChevronRight, Info, Shield, ShieldCheck, ShieldX, ShieldAlert, Lock, ArrowLeft } from 'lucide-react';
import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton'; import CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
import { formatDate } from '../../utils/dateFormat'; import { formatDate } from '../../utils/dateFormat';
import { getContractTypeInfo } from '../../utils/contractInfo';
import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types'; import type { Address, BankCard, IdentityDocument, Meter, Customer, CustomerRepresentative, CustomerSummary, CustomerConsent, ConsentType, ConsentStatus, RepresentativeAuthorization } from '../../types';
export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) { export default function CustomerDetail({ portalCustomerId }: { portalCustomerId?: number } = {}) {
@ -1671,6 +1672,14 @@ function ContractsTab({
<CopyButton value={(contract.providerName || contract.provider?.name || '') + ((contract.tariffName || contract.tariff?.name) ? ` - ${contract.tariffName || contract.tariff?.name}` : '')} /> <CopyButton value={(contract.providerName || contract.provider?.name || '') + ((contract.tariffName || contract.tariff?.name) ? ` - ${contract.tariffName || contract.tariff?.name}` : '')} />
</p> </p>
)} )}
{(() => {
const typeInfo = getContractTypeInfo(contract as any);
return typeInfo ? (
<p className={`text-sm text-gray-600 ${isPredecessor ? 'ml-6' : ''}`}>
<span className="font-medium text-gray-700">{typeInfo.label}:</span> {typeInfo.value}
</p>
) : null;
})()}
{contract.startDate && ( {contract.startDate && (
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}> <p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
Beginn: {formatDate(contract.startDate)} Beginn: {formatDate(contract.startDate)}

View File

@ -601,6 +601,13 @@ export interface ContractTreeNodeContract {
provider?: { id: number; name: string } | null; provider?: { id: number; name: string } | null;
tariff?: { id: number; name: string } | null; tariff?: { id: number; name: string } | null;
contractCategory?: { id: number; name: string } | null; contractCategory?: { id: number; name: string } | null;
customer?: { id: number; firstName: string; lastName: string; companyName: string | null; customerNumber: string } | null;
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
simCards: { phoneNumber: string | null; isMain: boolean }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
} }
export interface ContractTreeNode { export interface ContractTreeNode {

View File

@ -0,0 +1,54 @@
interface ContractInfoData {
type: string;
address?: { street: string; houseNumber: string; postalCode: string; city: string } | null;
mobileDetails?: {
phoneNumber: string | null;
simCards: { phoneNumber: string | null; isMain: boolean }[];
} | null;
carInsuranceDetails?: { licensePlate: string | null } | null;
}
export interface ContractTypeInfo {
label: string;
value: string;
}
export function getContractTypeInfo(contract: ContractInfoData): ContractTypeInfo | null {
const { type } = contract;
if (type === 'ELECTRICITY' || type === 'GAS') {
const a = contract.address;
if (!a) return null;
return {
label: 'Lieferadresse',
value: `${a.street} ${a.houseNumber}, ${a.postalCode} ${a.city}`,
};
}
if (type === 'DSL' || type === 'FIBER' || type === 'CABLE') {
const a = contract.address;
if (!a) return null;
return {
label: 'Anschlussadresse',
value: `${a.street} ${a.houseNumber}, ${a.postalCode} ${a.city}`,
};
}
if (type === 'MOBILE') {
const md = contract.mobileDetails;
if (!md) return null;
const mainSim = md.simCards?.find((s) => s.isMain && s.phoneNumber);
const anySim = md.simCards?.find((s) => s.phoneNumber);
const phone = mainSim?.phoneNumber || anySim?.phoneNumber || md.phoneNumber;
if (!phone) return null;
return { label: 'Rufnummer', value: phone };
}
if (type === 'CAR_INSURANCE') {
const plate = contract.carInsuranceDetails?.licensePlate;
if (!plate) return null;
return { label: 'Kennzeichen', value: plate };
}
return null;
}