Gutschriften: Auszahlungskonto (Bankkonto des Kunden) fuer Ueberweisungen
Bei Geld-Gutschriften kann die Auszahlung auf ein anderes Konto gehen als das Vertrags-Abbuchkonto: - CreditNote.payoutBankCardId + Migration (FK ON DELETE SET NULL, damit Loeschen einer Bankkarte die Gutschrift nicht mitreisst). - Formular: Dropdown mit allen Bankkonten des Kunden (Default = Vertrags-Abbuchkonto). getCreditNoteDefaults liefert bankCards + contractBankCardId. - Server prueft, dass die gewaehlte Bankkarte dem Kunden des Vertrags gehoert (kein Fremdkonto unterschieben). - PDF: bei Ueberweisung 'Unsere Bankverbindung' (Absender) + darunter 'an Bankkonto: <Kunden-IBAN> (<Inhaber>)'. Section-Zeile zeigt das Auszahlungskonto. Lokal verifiziert (Anlage + PDF). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ import Input from '../ui/Input';
|
||||
import Select from '../ui/Select';
|
||||
import Badge from '../ui/Badge';
|
||||
import { creditNoteApi } from '../../services/api';
|
||||
import type { CreditNote, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis } from '../../types';
|
||||
import type { CreditNote, CreditNoteType, CreditNoteCustomerType, CreditNoteAmountBasis, CreditNotePayoutBankCard } from '../../types';
|
||||
|
||||
const round2 = (n: number) => Math.round((n + Number.EPSILON) * 100) / 100;
|
||||
|
||||
@@ -35,6 +35,13 @@ function calcAmounts(amount: number, vatRelevant: boolean, basis: CreditNoteAmou
|
||||
|
||||
const todayIso = () => new Date().toISOString().split('T')[0];
|
||||
|
||||
interface FormDefaults {
|
||||
customerType: CreditNoteCustomerType;
|
||||
vatRelevant: boolean;
|
||||
bankCards: CreditNotePayoutBankCard[];
|
||||
contractBankCardId: number | null;
|
||||
}
|
||||
|
||||
interface FormState {
|
||||
type: CreditNoteType;
|
||||
sachwertDescription: string;
|
||||
@@ -47,10 +54,11 @@ interface FormState {
|
||||
place: string;
|
||||
signedAt: string;
|
||||
goodsReceived: boolean;
|
||||
payoutBankCardId: string;
|
||||
notes: string;
|
||||
}
|
||||
|
||||
function emptyForm(defaults?: { customerType: CreditNoteCustomerType; vatRelevant: boolean }): FormState {
|
||||
function emptyForm(defaults?: FormDefaults): FormState {
|
||||
return {
|
||||
type: 'GELD',
|
||||
sachwertDescription: '',
|
||||
@@ -63,6 +71,8 @@ function emptyForm(defaults?: { customerType: CreditNoteCustomerType; vatRelevan
|
||||
place: '',
|
||||
signedAt: '',
|
||||
goodsReceived: false,
|
||||
// Vorschlag: das Abbuchkonto des Vertrags (kann umgestellt werden).
|
||||
payoutBankCardId: defaults?.contractBankCardId ? String(defaults.contractBankCardId) : '',
|
||||
notes: '',
|
||||
};
|
||||
}
|
||||
@@ -81,6 +91,7 @@ function formFromCreditNote(cn: CreditNote): FormState {
|
||||
place: cn.place ?? '',
|
||||
signedAt: cn.signedAt ? cn.signedAt.split('T')[0] : '',
|
||||
goodsReceived: cn.goodsReceived,
|
||||
payoutBankCardId: cn.payoutBankCardId ? String(cn.payoutBankCardId) : '',
|
||||
notes: cn.notes ?? '',
|
||||
};
|
||||
}
|
||||
@@ -95,11 +106,12 @@ function CreditNoteFormModal({
|
||||
}: {
|
||||
contractId: number;
|
||||
editing: CreditNote | null;
|
||||
defaults?: { customerType: CreditNoteCustomerType; vatRelevant: boolean };
|
||||
defaults?: FormDefaults;
|
||||
onClose: () => void;
|
||||
onSaved: () => void;
|
||||
}) {
|
||||
const [form, setForm] = useState<FormState>(editing ? formFromCreditNote(editing) : emptyForm(defaults));
|
||||
const bankCards = defaults?.bankCards ?? [];
|
||||
const set = <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((f) => ({ ...f, [key]: value }));
|
||||
|
||||
const preview = calcAmounts(parseFloat(form.amount), form.vatRelevant, form.amountBasis, parseFloat(form.vatRate) || 0);
|
||||
@@ -118,6 +130,7 @@ function CreditNoteFormModal({
|
||||
place: form.place || null,
|
||||
signedAt: form.signedAt || null,
|
||||
goodsReceived: form.type === 'SACHWERT' ? form.goodsReceived : false,
|
||||
payoutBankCardId: form.type === 'GELD' && form.payoutBankCardId ? Number(form.payoutBankCardId) : null,
|
||||
notes: form.notes || null,
|
||||
};
|
||||
return editing ? creditNoteApi.update(editing.id, payload) : creditNoteApi.create(contractId, payload);
|
||||
@@ -221,6 +234,25 @@ function CreditNoteFormModal({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{form.type === 'GELD' && (
|
||||
bankCards.length > 0 ? (
|
||||
<Select
|
||||
label="Auszahlungskonto (Bankkonto des Kunden)"
|
||||
value={form.payoutBankCardId}
|
||||
onChange={(e) => set('payoutBankCardId', e.target.value)}
|
||||
options={bankCards.map((bc) => ({
|
||||
value: String(bc.id),
|
||||
label: `${bc.iban} (${bc.accountHolder})${bc.description ? ` – ${bc.description}` : ''}`,
|
||||
}))}
|
||||
placeholder="Kein Konto angeben"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs text-gray-500">
|
||||
Für diesen Kunden sind keine Bankkonten hinterlegt – Auszahlungskonto kann daher nicht gewählt werden.
|
||||
</p>
|
||||
)
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<Input
|
||||
label="Datum der Gutschrift"
|
||||
@@ -431,6 +463,12 @@ export default function CreditNotesSection({ contractId, canEdit }: { contractId
|
||||
<div className="text-sm text-gray-600 mt-0.5">{cn.sachwertDescription}</div>
|
||||
)}
|
||||
{cn.notes && <div className="text-xs text-gray-500 mt-0.5 italic">{cn.notes}</div>}
|
||||
{cn.type === 'GELD' && cn.payoutBankCardId && (() => {
|
||||
const card = defaults?.bankCards.find((bc) => bc.id === cn.payoutBankCardId);
|
||||
return card ? (
|
||||
<div className="text-xs text-gray-500 mt-0.5">an Bankkonto: <span className="font-mono">{card.iban}</span> ({card.accountHolder})</div>
|
||||
) : null;
|
||||
})()}
|
||||
{cn.type === 'GELD' && (
|
||||
<div className="mt-1">
|
||||
<ReceiptControls cn={cn} canEdit={canEdit} onChanged={refresh} />
|
||||
|
||||
@@ -81,6 +81,7 @@ export interface CreditNote {
|
||||
place?: string | null;
|
||||
signedAt?: string | null;
|
||||
goodsReceived: boolean;
|
||||
payoutBankCardId?: number | null;
|
||||
receiptPath?: string | null;
|
||||
pdfPath?: string | null;
|
||||
notes?: string | null;
|
||||
@@ -89,10 +90,20 @@ export interface CreditNote {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CreditNotePayoutBankCard {
|
||||
id: number;
|
||||
iban: string;
|
||||
accountHolder: string;
|
||||
bankName?: string | null;
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
export interface CreditNoteDefaults {
|
||||
customerType: CreditNoteCustomerType;
|
||||
vatRelevant: boolean;
|
||||
nextNumber: string;
|
||||
bankCards: CreditNotePayoutBankCard[];
|
||||
contractBankCardId: number | null;
|
||||
}
|
||||
|
||||
export interface CreditNoteNumberRange {
|
||||
|
||||
Reference in New Issue
Block a user