Bankkarte-/Ausweis-Details in Vertrag + neues Feld Kartennummer
Schema: BankCard.cardNumber (String?, optional) + Migration 20260813100000_bank_card_number (ADD COLUMN IF NOT EXISTS), auf Dev angewandt + prisma generate; Prod via migrate deploy. Eingabefeld "Kartennummer" im Bankkarten-Modal (Kundenakte). Vertragsansicht (ContractDetail) und Vertrag bearbeiten (ContractForm) zeigen jetzt bei Bankkarte zusaetzlich BIC/Bank/Kartennummer/Ablaufdatum und bei Ausweis Behoerde/Ausstellung/Ablaufdatum sowie Geburtsort + Geburtsdatum des Kunden - jeweils mit Copy-Button und nur wenn gesetzt. Im Form je Select in eigenem div gewrappt (Grid-Alignment). Verifiziert: tsc + vite build gruen, cardNumber Round-Trip (update->read), Contract-Include liefert alle Felder inkl. customer.birthDate/birthPlace. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- Kartennummer (Debit-/Kontokarte) als optionales Feld an der Bankkarte.
|
||||||
|
ALTER TABLE `BankCard` ADD COLUMN IF NOT EXISTS `cardNumber` VARCHAR(191) NULL;
|
||||||
@@ -353,6 +353,7 @@ model BankCard {
|
|||||||
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade)
|
||||||
accountHolder String
|
accountHolder String
|
||||||
iban String
|
iban String
|
||||||
|
cardNumber String? // Karten-/Kontokartennummer (optional, z.B. Debitkarte)
|
||||||
bic String?
|
bic String?
|
||||||
bankName String?
|
bankName String?
|
||||||
expiryDate DateTime?
|
expiryDate DateTime?
|
||||||
|
|||||||
@@ -433,7 +433,7 @@ export async function updateBankCard(req: AuthRequest, res: Response): Promise<v
|
|||||||
if (before) {
|
if (before) {
|
||||||
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
const changes: Record<string, { von: unknown; nach: unknown }> = {};
|
||||||
const fieldLabels: Record<string, string> = {
|
const fieldLabels: Record<string, string> = {
|
||||||
iban: 'IBAN', bic: 'BIC', bankName: 'Bank',
|
iban: 'IBAN', bic: 'BIC', bankName: 'Bank', cardNumber: 'Kartennummer',
|
||||||
accountHolder: 'Kontoinhaber', isActive: 'Aktiv',
|
accountHolder: 'Kontoinhaber', isActive: 'Aktiv',
|
||||||
};
|
};
|
||||||
for (const [key, newVal] of Object.entries(data)) {
|
for (const [key, newVal] of Object.entries(data)) {
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ export async function createBankCard(
|
|||||||
data: {
|
data: {
|
||||||
accountHolder: string;
|
accountHolder: string;
|
||||||
iban: string;
|
iban: string;
|
||||||
|
cardNumber?: string;
|
||||||
bic?: string;
|
bic?: string;
|
||||||
bankName?: string;
|
bankName?: string;
|
||||||
expiryDate?: Date;
|
expiryDate?: Date;
|
||||||
@@ -322,6 +323,7 @@ export async function updateBankCard(
|
|||||||
data: {
|
data: {
|
||||||
accountHolder?: string;
|
accountHolder?: string;
|
||||||
iban?: string;
|
iban?: string;
|
||||||
|
cardNumber?: string;
|
||||||
bic?: string;
|
bic?: string;
|
||||||
bankName?: string;
|
bankName?: string;
|
||||||
expiryDate?: Date;
|
expiryDate?: Date;
|
||||||
|
|||||||
@@ -97,6 +97,22 @@ isolierte Instanz (keine Multi-Tenancy im Code), Provisioning + Abrechnung
|
|||||||
|
|
||||||
## ✅ Erledigt
|
## ✅ Erledigt
|
||||||
|
|
||||||
|
- [x] **🪪 Bankkarte-/Ausweis-Details in Vertrag (Ansicht + Bearbeiten) + Kartennummer** (2026-08-13)
|
||||||
|
- **Schema:** neues Feld `BankCard.cardNumber` (String?, optional). Migration
|
||||||
|
`20260813100000_bank_card_number` (`ADD COLUMN IF NOT EXISTS`), auf Dev
|
||||||
|
angewandt + `prisma generate`. Prod zieht via `migrate deploy` im Entrypoint.
|
||||||
|
Eingabefeld „Kartennummer" im Bankkarten-Modal (Kundenakte) ergänzt.
|
||||||
|
- **Vertragsansicht (ContractDetail)** – Karten „Bankkarte"/„Ausweis" zeigen
|
||||||
|
zusätzlich (jeweils mit Copy-Button, nur wenn gesetzt):
|
||||||
|
- Bankkarte: BIC, Bank, Kartennummer, Ablaufdatum
|
||||||
|
- Ausweis: Behörde, Ausstellung, Ablaufdatum + **Geburtsort/Geburtsdatum
|
||||||
|
vom Kunden**
|
||||||
|
- **Vertrag bearbeiten (ContractForm)** – unter den Bankkarte-/Ausweis-
|
||||||
|
Dropdowns dieselben Detailfelder der aktuell gewählten Karte/Ausweis
|
||||||
|
(Copy-Buttons). Selects dafür je in eigenem `<div>` gewrappt (Grid-Alignment).
|
||||||
|
- Datenquelle war bereits vorhanden: `getContractById` (bankCard/identityDocument/
|
||||||
|
customer) + `getCustomerById` (bankCards/identityDocuments) liefern alle Felder.
|
||||||
|
|
||||||
- [x] **📄 PDF-Viewer-Modal für Bankkarte-/Ausweis-Dokument** (2026-08-13)
|
- [x] **📄 PDF-Viewer-Modal für Bankkarte-/Ausweis-Dokument** (2026-08-13)
|
||||||
- Neue wiederverwendbare Komponente `PdfViewerModal` (Modal + `<iframe>` auf
|
- Neue wiederverwendbare Komponente `PdfViewerModal` (Modal + `<iframe>` auf
|
||||||
`viewUrl(documentPath)`, inkl. „In neuem Tab" + „Herunterladen"). Nutzt den
|
`viewUrl(documentPath)`, inkl. „In neuem Tab" + „Herunterladen"). Nutzt den
|
||||||
|
|||||||
@@ -2681,7 +2681,30 @@ export default function ContractDetail() {
|
|||||||
{c.bankCard.iban}
|
{c.bankCard.iban}
|
||||||
<CopyButton value={c.bankCard.iban} />
|
<CopyButton value={c.bankCard.iban} />
|
||||||
</p>
|
</p>
|
||||||
{c.bankCard.bankName && <p className="text-gray-500">{c.bankCard.bankName}</p>}
|
{c.bankCard.bic && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">BIC:</span> {c.bankCard.bic}
|
||||||
|
<CopyButton value={c.bankCard.bic} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.bankCard.bankName && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Bank:</span> {c.bankCard.bankName}
|
||||||
|
<CopyButton value={c.bankCard.bankName} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.bankCard.cardNumber && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Kartennummer:</span> {c.bankCard.cardNumber}
|
||||||
|
<CopyButton value={c.bankCard.cardNumber} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.bankCard.expiryDate && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Ablaufdatum:</span> {formatDate(c.bankCard.expiryDate)}
|
||||||
|
<CopyButton value={formatDate(c.bankCard.expiryDate)} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{c.bankCard.description && (
|
{c.bankCard.description && (
|
||||||
<p className="text-sm text-gray-600 mt-1 italic whitespace-pre-line">
|
<p className="text-sm text-gray-600 mt-1 italic whitespace-pre-line">
|
||||||
{c.bankCard.description}
|
{c.bankCard.description}
|
||||||
@@ -2709,6 +2732,36 @@ export default function ContractDetail() {
|
|||||||
<CopyButton value={c.identityDocument.documentNumber} />
|
<CopyButton value={c.identityDocument.documentNumber} />
|
||||||
</p>
|
</p>
|
||||||
<p className="text-gray-500">{c.identityDocument.type}</p>
|
<p className="text-gray-500">{c.identityDocument.type}</p>
|
||||||
|
{c.identityDocument.issuingAuthority && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Behörde:</span> {c.identityDocument.issuingAuthority}
|
||||||
|
<CopyButton value={c.identityDocument.issuingAuthority} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.identityDocument.issueDate && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Ausstellung:</span> {formatDate(c.identityDocument.issueDate)}
|
||||||
|
<CopyButton value={formatDate(c.identityDocument.issueDate)} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.identityDocument.expiryDate && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Ablaufdatum:</span> {formatDate(c.identityDocument.expiryDate)}
|
||||||
|
<CopyButton value={formatDate(c.identityDocument.expiryDate)} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.customer?.birthPlace && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Geburtsort:</span> {c.customer.birthPlace}
|
||||||
|
<CopyButton value={c.customer.birthPlace} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{c.customer?.birthDate && (
|
||||||
|
<p className="text-sm text-gray-600 flex items-center gap-1">
|
||||||
|
<span className="text-gray-500">Geburtsdatum:</span> {formatDate(c.customer.birthDate)}
|
||||||
|
<CopyButton value={formatDate(c.customer.birthDate)} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1041,6 +1041,7 @@ export default function ContractForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
<Select
|
<Select
|
||||||
label={
|
label={
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
@@ -1077,7 +1078,25 @@ export default function ContractForm() {
|
|||||||
};
|
};
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
{selectedBankCard && (
|
||||||
|
<div className="mt-1.5 space-y-0.5 text-xs text-gray-600">
|
||||||
|
{selectedBankCard.bic && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">BIC:</span> {selectedBankCard.bic}<CopyButton value={selectedBankCard.bic} /></div>
|
||||||
|
)}
|
||||||
|
{selectedBankCard.bankName && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Bank:</span> {selectedBankCard.bankName}<CopyButton value={selectedBankCard.bankName} /></div>
|
||||||
|
)}
|
||||||
|
{selectedBankCard.cardNumber && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Kartennummer:</span> {selectedBankCard.cardNumber}<CopyButton value={selectedBankCard.cardNumber} /></div>
|
||||||
|
)}
|
||||||
|
{selectedBankCard.expiryDate && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Ablaufdatum:</span> {formatDate(selectedBankCard.expiryDate)}<CopyButton value={formatDate(selectedBankCard.expiryDate)} /></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
<Select
|
<Select
|
||||||
label={
|
label={
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
@@ -1106,6 +1125,26 @@ export default function ContractForm() {
|
|||||||
label: `${d.documentNumber} (${d.type})`,
|
label: `${d.documentNumber} (${d.type})`,
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
{selectedDocument && (
|
||||||
|
<div className="mt-1.5 space-y-0.5 text-xs text-gray-600">
|
||||||
|
{selectedDocument.issueDate && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Ausstellung:</span> {formatDate(selectedDocument.issueDate)}<CopyButton value={formatDate(selectedDocument.issueDate)} /></div>
|
||||||
|
)}
|
||||||
|
{selectedDocument.expiryDate && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Ablaufdatum:</span> {formatDate(selectedDocument.expiryDate)}<CopyButton value={formatDate(selectedDocument.expiryDate)} /></div>
|
||||||
|
)}
|
||||||
|
{selectedDocument.issuingAuthority && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Behörde:</span> {selectedDocument.issuingAuthority}<CopyButton value={selectedDocument.issuingAuthority} /></div>
|
||||||
|
)}
|
||||||
|
{customer?.birthPlace && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Geburtsort:</span> {customer.birthPlace}<CopyButton value={customer.birthPlace} /></div>
|
||||||
|
)}
|
||||||
|
{customer?.birthDate && (
|
||||||
|
<div className="flex items-center gap-1"><span className="text-gray-500">Geburtsdatum:</span> {formatDate(customer.birthDate)}<CopyButton value={formatDate(customer.birthDate)} /></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2697,6 +2697,7 @@ function BankCardModal({
|
|||||||
const getInitialFormData = () => ({
|
const getInitialFormData = () => ({
|
||||||
accountHolder: bankCard?.accountHolder || '',
|
accountHolder: bankCard?.accountHolder || '',
|
||||||
iban: bankCard?.iban || '',
|
iban: bankCard?.iban || '',
|
||||||
|
cardNumber: bankCard?.cardNumber || '',
|
||||||
bic: bankCard?.bic || '',
|
bic: bankCard?.bic || '',
|
||||||
bankName: bankCard?.bankName || '',
|
bankName: bankCard?.bankName || '',
|
||||||
expiryDate: bankCard?.expiryDate ? new Date(bankCard.expiryDate).toISOString().split('T')[0] : '',
|
expiryDate: bankCard?.expiryDate ? new Date(bankCard.expiryDate).toISOString().split('T')[0] : '',
|
||||||
@@ -2753,7 +2754,7 @@ function BankCardModal({
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
queryClient.invalidateQueries({ queryKey: ['customer', customerId] });
|
||||||
onClose();
|
onClose();
|
||||||
setFormData({ accountHolder: '', iban: '', bic: '', bankName: '', expiryDate: '', description: '', isActive: true });
|
setFormData({ accountHolder: '', iban: '', cardNumber: '', bic: '', bankName: '', expiryDate: '', description: '', isActive: true });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2830,6 +2831,12 @@ function BankCardModal({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
label="Kartennummer"
|
||||||
|
value={formData.cardNumber}
|
||||||
|
onChange={(e) => setFormData({ ...formData, cardNumber: e.target.value })}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
label="BIC"
|
label="BIC"
|
||||||
value={formData.bic}
|
value={formData.bic}
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ export interface BankCard {
|
|||||||
customerId: number;
|
customerId: number;
|
||||||
accountHolder: string;
|
accountHolder: string;
|
||||||
iban: string;
|
iban: string;
|
||||||
|
cardNumber?: string;
|
||||||
bic?: string;
|
bic?: string;
|
||||||
bankName?: string;
|
bankName?: string;
|
||||||
expiryDate?: string;
|
expiryDate?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user