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:
parent
eaf7d1eac3
commit
0d58b79836
|
|
@ -95,6 +95,13 @@ export async function getAllContracts(filters: ContractFilters) {
|
|||
provider: true,
|
||||
tariff: true,
|
||||
contractCategory: true,
|
||||
mobileDetails: {
|
||||
select: {
|
||||
phoneNumber: true,
|
||||
simCards: { select: { phoneNumber: true, isMain: true } },
|
||||
},
|
||||
},
|
||||
carInsuranceDetails: { select: { licensePlate: true } },
|
||||
},
|
||||
}),
|
||||
prisma.contract.count({ where }),
|
||||
|
|
@ -845,6 +852,13 @@ export interface ContractTreeNode {
|
|||
provider?: { id: number; name: string } | null;
|
||||
tariff?: { 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[];
|
||||
hasHistory: boolean;
|
||||
|
|
@ -875,6 +889,15 @@ export async function getContractTreeForCustomer(customerId: number): Promise<Co
|
|||
provider: { select: { id: true, name: true } },
|
||||
tariff: { 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' }],
|
||||
});
|
||||
|
|
|
|||
132
backend/todo.md
132
backend/todo.md
|
|
@ -1,101 +1,75 @@
|
|||
Vertragliste bei Energie mit Anschlussadresse/Lieferadresse noch in der Liste
|
||||
Bei Mobilfunk die Mobilfunknummer und wenn vorhanden Karteninhaber
|
||||
Bei Festnetz, die Anschlussadresse/Lieferadresse
|
||||
Bei KFZ das Kennzeichen
|
||||
# ende
|
||||
# 📋 OpenCRM – Todo-Liste
|
||||
|
||||
#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
|
||||
Kunde entfernt einen Haken im Portal → PDF löschen + Tabs sperren
|
||||
Entsperrung nur durch: alle Haken wieder setzen ODER neues PDF hochladen
|
||||
# ende
|
||||
## 🔜 Offen
|
||||
|
||||
#erledigt
|
||||
Zweitarif (Gibt es auch 3 Tarifuzähler?) Zähler HT/NT bei Strom Zähler hinzufügen.
|
||||
Auch in die Berechnung die Verbäuche dann darstellen
|
||||
# ende
|
||||
### Vertragsliste erweitern
|
||||
Zusatzinfos in der Vertragsliste anzeigen:
|
||||
- **Energie:** Anschlussadresse / Lieferadresse
|
||||
- **Mobilfunk:** Mobilfunknummer + Karteninhaber (wenn vorhanden)
|
||||
- **Festnetz:** Anschlussadresse / Lieferadresse
|
||||
- **KFZ:** Kennzeichen
|
||||
|
||||
#erledigt
|
||||
Alle Datumsfelder mit 0 davor wenn es ne einstellige Zahl ist
|
||||
### Email Log & System testen
|
||||
- Senden testen
|
||||
- Empfangen testen
|
||||
|
||||
Jetzt : 1.1.2026
|
||||
Und gewollt 01.01.2026
|
||||
# ende
|
||||
### Security System testen
|
||||
|
||||
### Geburtstagskalender
|
||||
- Geburtstagsgruß als Modal beim ersten Login am Geburtstag
|
||||
- Bis 7 Tage nach Geburtstag: nachträgliche Glückwünsche
|
||||
|
||||
#erledigt
|
||||
Die Auditmeldungen aussagekräftig
|
||||
# ende
|
||||
### Email → Vertragsdokumente
|
||||
Wenn eine Email einem Vertrag zugeordnet ist:
|
||||
- Anhänge auch in Vertragsdokumente speichern
|
||||
- Rechnungen wie Kündigungsdokumente behandeln
|
||||
|
||||
Email Log und system testen
|
||||
Sprich senden und Empfnagen
|
||||
# ende
|
||||
---
|
||||
|
||||
Security System testen
|
||||
# ende
|
||||
## ✅ Erledigt
|
||||
|
||||
#erledigt
|
||||
Datenschutzerklärung Website unserer Seite und ein impressum im Kundenportal.
|
||||
- [x] **Datenschutzerklärung PDF ↔ Online-Einwilligungen synchronisieren**
|
||||
- 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.
|
||||
Bitte mach mir da auch einen Vorschagstext rein
|
||||
# ende
|
||||
- [x] **Zweitarif-Zähler (HT/NT)** bei Strom + Verbrauchsberechnung
|
||||
|
||||
Geburtstagskalender, und Geburtgsgruß als Modal beim ersten Login an dem Tag,
|
||||
Sollte der Login bis n7 btage nach Geburtsag sein dann Glückwunsch nachträglich
|
||||
# ende
|
||||
- [x] **Datumsformate vereinheitlichen** (01.01.2026 statt 1.1.2026)
|
||||
|
||||
#erledigt
|
||||
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.
|
||||
- [x] **Audit-Log aussagekräftig** (Vorher/Nachher bei allen Änderungen)
|
||||
|
||||
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
|
||||
Haben wir bei den Vertragen (also alle) ein Dokumentfeld zum Upload von, Auftragsformular, Lieferbestätigung, Vertragsunterlagen?
|
||||
hier sind wieder png,pdf erlaubt
|
||||
# ende
|
||||
- [x] **Vertragsdokumente-Upload** (Auftragsformular, Lieferbestätigung, Vertragsunterlagen als PDF/PNG)
|
||||
|
||||
- [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.
|
||||
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.
|
||||
Ist die Frage wo legen wir die PDF Vorlagen hin.
|
||||
Vielleicht sogar ein Editor, für Vorlagen wo man dann rein zieht an welcher stelle, welches feld stehen soll aus den kunden daten.
|
||||
Dann kann man das für weiter Formulare machen die PDF sind.
|
||||
Moon fachhandle als Beispiel, hat ne API, deshalb kommt das später ;-)
|
||||
- [x] **PDF-Auftragsvorlagen-System**
|
||||
- Template-Editor in Einstellungen
|
||||
- PDF hochladen, Formularfelder automatisch auslesen
|
||||
- CRM-Felder zuordnen (visuell mit Vorschau)
|
||||
- Seitenweise Sortierung der Felder
|
||||
- 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
|
||||
# ende
|
||||
- [x] **Eigentümer-Verwaltung**
|
||||
- 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
|
||||
Und Rechnungen wie bei den Kündigungsdokumenten
|
||||
# ende
|
||||
- [x] **Objekttyp + Lage + Lage des Anschlusses** bei Festnetz-Verträgen (DSL/Glasfaser/Kabel)
|
||||
|
||||
|
||||
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
|
||||
- [x] **Bankverbindung-Fallback** im PDF-Generator (neueste aktive Bankverbindung des Kunden)
|
||||
|
|
|
|||
|
|
@ -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 { gdprApi } from '../../services/api';
|
||||
import { formatDate } from '../../utils/dateFormat';
|
||||
import { getContractTypeInfo } from '../../utils/contractInfo';
|
||||
import type { Contract, ContractType, ContractStatus } from '../../types';
|
||||
|
||||
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}` : '')} />
|
||||
</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 && (
|
||||
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
Beginn: {formatDate(contract.startDate)}
|
||||
|
|
@ -455,12 +464,19 @@ export default function ContractList() {
|
|||
</tr>
|
||||
</thead>
|
||||
<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">
|
||||
<td className="py-3 px-4 font-mono text-sm">
|
||||
<Link to={`/contracts/${contract.id}`} state={pushHistory('/contracts')} className="text-blue-600 hover:underline">
|
||||
{contract.contractNumber}
|
||||
</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>
|
||||
{!isCustomer && (
|
||||
<td className="py-3 px-4">
|
||||
|
|
@ -529,7 +545,8 @@ export default function ContractList() {
|
|||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 CopyButton, { CopyableBlock } from '../../components/ui/CopyButton';
|
||||
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';
|
||||
|
||||
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}` : '')} />
|
||||
</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 && (
|
||||
<p className={`text-sm text-gray-500 ${isPredecessor ? 'ml-6' : ''}`}>
|
||||
Beginn: {formatDate(contract.startDate)}
|
||||
|
|
|
|||
|
|
@ -601,6 +601,13 @@ export interface ContractTreeNodeContract {
|
|||
provider?: { id: number; name: string } | null;
|
||||
tariff?: { 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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue