Fälligkeit der Miete bei Vertragserstellung (1./15.) + endgültiges Löschen zurückgezogener Einladungen
This commit is contained in:
parent
3111f64ecd
commit
89d2a911e1
@ -73,7 +73,8 @@ export class PaymentService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Berechnet die Ampel-Status je aktivem Vertrag für das Vermieter-Cockpit.
|
* Berechnet die Ampel-Status je aktivem Vertrag für das Vermieter-Cockpit.
|
||||||
* GRÜN = bezahlt, GELB = fällig aber Frist (3. Werktag) nicht erreicht, ROT = überfällig.
|
* GRÜN = bezahlt, GELB = fällig aber Frist (vertraglicher Fälligkeitstag,
|
||||||
|
* Contract.paymentDueDay) noch nicht erreicht, ROT = überfällig.
|
||||||
*/
|
*/
|
||||||
async getRentTrafficLights(referenceDate: Date = new Date()): Promise<RentTrafficLight[]> {
|
async getRentTrafficLights(referenceDate: Date = new Date()): Promise<RentTrafficLight[]> {
|
||||||
const activeContracts = await this.prisma.contract.findMany({
|
const activeContracts = await this.prisma.contract.findMany({
|
||||||
@ -94,19 +95,19 @@ export class PaymentService {
|
|||||||
|
|
||||||
return activeContracts.map((c: ContractWithPayments) => {
|
return activeContracts.map((c: ContractWithPayments) => {
|
||||||
const currentPayment = c.payments[0];
|
const currentPayment = c.payments[0];
|
||||||
const dueThreshold = thirdBusinessDay(referenceDate);
|
const dueDate = dueDateForMonth(c.paymentDueDay, referenceDate);
|
||||||
const paid = currentPayment?.status === PaymentStatus.PAID;
|
const paid = currentPayment?.status === PaymentStatus.PAID;
|
||||||
|
|
||||||
let status: RentTrafficLight['status'] = 'YELLOW';
|
let status: RentTrafficLight['status'] = 'YELLOW';
|
||||||
if (paid) status = 'GREEN';
|
if (paid) status = 'GREEN';
|
||||||
else if (referenceDate > dueThreshold) status = 'RED';
|
else if (referenceDate > dueDate) status = 'RED';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
contractId: c.id,
|
contractId: c.id,
|
||||||
roomNumber: c.room.roomNumber,
|
roomNumber: c.room.roomNumber,
|
||||||
tenantName: c.user.fullName,
|
tenantName: c.user.fullName,
|
||||||
status,
|
status,
|
||||||
dueDate: currentPayment?.dueDate.toISOString() ?? '',
|
dueDate: dueDate.toISOString(),
|
||||||
amountDue: Number(currentPayment?.amount ?? c.totalWarmRent),
|
amountDue: Number(currentPayment?.amount ?? c.totalWarmRent),
|
||||||
amountPaid: paid ? Number(currentPayment.amount) : 0,
|
amountPaid: paid ? Number(currentPayment.amount) : 0,
|
||||||
};
|
};
|
||||||
@ -118,10 +119,13 @@ export class PaymentService {
|
|||||||
* Mahn-Notification, sofern noch keine für diesen Zahlungslauf gesendet wurde.
|
* Mahn-Notification, sofern noch keine für diesen Zahlungslauf gesendet wurde.
|
||||||
*/
|
*/
|
||||||
async runDailyDunningCheck(referenceDate: Date = new Date()) {
|
async runDailyDunningCheck(referenceDate: Date = new Date()) {
|
||||||
|
// Payment.dueDate ist der je Zahlung konkret hinterlegte Fälligkeitstermin
|
||||||
|
// (abgeleitet aus Contract.paymentDueDay) — überfällig, sobald er
|
||||||
|
// verstrichen ist, keine erneute Näherung über den Vertrag nötig.
|
||||||
const overdue = await this.prisma.payment.findMany({
|
const overdue = await this.prisma.payment.findMany({
|
||||||
where: {
|
where: {
|
||||||
status: { in: [PaymentStatus.PENDING, PaymentStatus.OVERDUE] },
|
status: { in: [PaymentStatus.PENDING, PaymentStatus.OVERDUE] },
|
||||||
dueDate: { lt: thirdBusinessDay(referenceDate) },
|
dueDate: { lt: referenceDate },
|
||||||
dunningSentAt: null,
|
dunningSentAt: null,
|
||||||
},
|
},
|
||||||
include: { contract: { include: { user: true, room: true } } },
|
include: { contract: { include: { user: true, room: true } } },
|
||||||
@ -168,15 +172,16 @@ function endOfMonth(d: Date): Date {
|
|||||||
return new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59);
|
return new Date(d.getFullYear(), d.getMonth() + 1, 0, 23, 59, 59);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Berechnet den 3. Werktag (Mo-Fr, ohne Feiertagsberücksichtigung) des Monats von `d`. */
|
/**
|
||||||
function thirdBusinessDay(d: Date): Date {
|
* Fälligkeitsdatum der Miete im Monat von `referenceDate`, gemäß dem im
|
||||||
let count = 0;
|
* Mietvertrag festgelegten Kalendertag (Contract.paymentDueDay, z.B. 1 oder
|
||||||
const cursor = startOfMonth(d);
|
* 15). Liegt der Tag nach dem Monatsende (z.B. 31 im Februar), wird auf den
|
||||||
while (count < 3) {
|
* letzten Tag des Monats begrenzt.
|
||||||
const day = cursor.getDay();
|
*/
|
||||||
if (day !== 0 && day !== 6) count++;
|
function dueDateForMonth(paymentDueDay: number, referenceDate: Date): Date {
|
||||||
if (count < 3) cursor.setDate(cursor.getDate() + 1);
|
const lastDayOfMonth = endOfMonth(referenceDate).getDate();
|
||||||
}
|
const day = Math.min(paymentDueDay, lastDayOfMonth);
|
||||||
cursor.setHours(23, 59, 59, 999);
|
const due = new Date(referenceDate.getFullYear(), referenceDate.getMonth(), day);
|
||||||
return cursor;
|
due.setHours(23, 59, 59, 999);
|
||||||
|
return due;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -453,6 +453,9 @@ contractsRouter.post(
|
|||||||
if (!Number.isFinite(Number(depositAmount)) || Number(depositAmount) < 0) {
|
if (!Number.isFinite(Number(depositAmount)) || Number(depositAmount) < 0) {
|
||||||
return res.status(400).json({ error: 'depositAmount muss eine Zahl sein' });
|
return res.status(400).json({ error: 'depositAmount muss eine Zahl sein' });
|
||||||
}
|
}
|
||||||
|
if (paymentDueDay !== undefined && paymentDueDay !== null && ![1, 15].includes(Number(paymentDueDay))) {
|
||||||
|
return res.status(400).json({ error: 'paymentDueDay muss 1 oder 15 sein' });
|
||||||
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({ where: { id: userId } });
|
const user = await prisma.user.findUnique({ where: { id: userId } });
|
||||||
if (!user) return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
if (!user) return res.status(404).json({ error: 'Mieter nicht gefunden' });
|
||||||
@ -486,7 +489,7 @@ contractsRouter.post(
|
|||||||
endDate: parsedEnd,
|
endDate: parsedEnd,
|
||||||
totalWarmRent: Number(totalWarmRent),
|
totalWarmRent: Number(totalWarmRent),
|
||||||
depositAmount: Number(depositAmount),
|
depositAmount: Number(depositAmount),
|
||||||
paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 3,
|
paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 1,
|
||||||
noticePeriodMonths: Number.isFinite(Number(noticePeriodMonths)) ? Number(noticePeriodMonths) : 3,
|
noticePeriodMonths: Number.isFinite(Number(noticePeriodMonths)) ? Number(noticePeriodMonths) : 3,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -34,7 +34,7 @@ invitationsRouter.post(
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireRole('LANDLORD', 'ADMIN'),
|
requireRole('LANDLORD', 'ADMIN'),
|
||||||
async (req: AuthedRequest, res: Response) => {
|
async (req: AuthedRequest, res: Response) => {
|
||||||
const { email, fullName, roomId, role, contractStartDate, contractEndDate } = req.body || {};
|
const { email, fullName, roomId, role, contractStartDate, contractEndDate, paymentDueDay } = req.body || {};
|
||||||
if (!email || !String(email).trim()) {
|
if (!email || !String(email).trim()) {
|
||||||
return res.status(400).json({ error: 'E-Mail-Adresse erforderlich' });
|
return res.status(400).json({ error: 'E-Mail-Adresse erforderlich' });
|
||||||
}
|
}
|
||||||
@ -74,6 +74,9 @@ invitationsRouter.post(
|
|||||||
if (contractStartDate !== undefined && contractStartDate !== null && Number.isNaN(Date.parse(contractStartDate))) {
|
if (contractStartDate !== undefined && contractStartDate !== null && Number.isNaN(Date.parse(contractStartDate))) {
|
||||||
return res.status(400).json({ error: 'Mietbeginn ist ungültig' });
|
return res.status(400).json({ error: 'Mietbeginn ist ungültig' });
|
||||||
}
|
}
|
||||||
|
if (paymentDueDay !== undefined && paymentDueDay !== null && ![1, 15].includes(Number(paymentDueDay))) {
|
||||||
|
return res.status(400).json({ error: 'Fälligkeit muss der 1. oder der 15. sein' });
|
||||||
|
}
|
||||||
parsedContractStart = contractStartDate ? new Date(contractStartDate) : new Date();
|
parsedContractStart = contractStartDate ? new Date(contractStartDate) : new Date();
|
||||||
parsedContractEnd = contractEndDate ? new Date(contractEndDate) : null;
|
parsedContractEnd = contractEndDate ? new Date(contractEndDate) : null;
|
||||||
|
|
||||||
@ -98,6 +101,7 @@ invitationsRouter.post(
|
|||||||
roomId: roomId || null,
|
roomId: roomId || null,
|
||||||
contractStartDate: parsedContractStart,
|
contractStartDate: parsedContractStart,
|
||||||
contractEndDate: parsedContractEnd,
|
contractEndDate: parsedContractEnd,
|
||||||
|
paymentDueDay: roomId && paymentDueDay ? Number(paymentDueDay) : null,
|
||||||
invitedById: req.user!.id,
|
invitedById: req.user!.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
@ -140,6 +144,24 @@ invitationsRouter.delete(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// DELETE /v1/invitations/:id/permanent (nur Admin) — zurückgezogene Einladung
|
||||||
|
// endgültig aus der Datenbank entfernen (im Gegensatz zum "Zurückziehen"
|
||||||
|
// oben, das nur den Status auf REVOKED setzt).
|
||||||
|
invitationsRouter.delete(
|
||||||
|
'/invitations/:id/permanent',
|
||||||
|
requireAuth,
|
||||||
|
requireRole('ADMIN'),
|
||||||
|
async (req: AuthedRequest, res: Response) => {
|
||||||
|
const result = await prisma.invitation.deleteMany({
|
||||||
|
where: { id: req.params.id, status: 'REVOKED' },
|
||||||
|
});
|
||||||
|
if (result.count === 0) {
|
||||||
|
return res.status(404).json({ error: 'Zurückgezogene Einladung nicht gefunden' });
|
||||||
|
}
|
||||||
|
res.status(204).send();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// GET /v1/invitations/:token/preview (öffentlich) — Infos zur Einladung vor Annahme
|
// GET /v1/invitations/:token/preview (öffentlich) — Infos zur Einladung vor Annahme
|
||||||
invitationsRouter.get('/invitations/:token/preview', async (req: Request, res: Response) => {
|
invitationsRouter.get('/invitations/:token/preview', async (req: Request, res: Response) => {
|
||||||
const invitation = await prisma.invitation.findUnique({
|
const invitation = await prisma.invitation.findUnique({
|
||||||
@ -274,6 +296,7 @@ invitationsRouter.post('/invitations/:token/accept', async (req: Request, res: R
|
|||||||
roomId: room.id,
|
roomId: room.id,
|
||||||
startDate: invitation.contractStartDate || new Date(),
|
startDate: invitation.contractStartDate || new Date(),
|
||||||
endDate: invitation.contractEndDate,
|
endDate: invitation.contractEndDate,
|
||||||
|
paymentDueDay: invitation.paymentDueDay || 1,
|
||||||
totalWarmRent: Number(room.baseRent) + Number(room.utilityPauschal),
|
totalWarmRent: Number(room.baseRent) + Number(room.utilityPauschal),
|
||||||
depositAmount: Number(room.baseRent) * 3,
|
depositAmount: Number(room.baseRent) * 3,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
|||||||
@ -119,7 +119,7 @@ export function generateContractPdf(contract: ContractForPdf): Promise<Buffer> {
|
|||||||
section('§ 4 Miete und Nebenkosten', () => {
|
section('§ 4 Miete und Nebenkosten', () => {
|
||||||
doc.text(`Die Warmmiete beträgt ${formatEuro(contract.totalWarmRent)} monatlich.`);
|
doc.text(`Die Warmmiete beträgt ${formatEuro(contract.totalWarmRent)} monatlich.`);
|
||||||
doc.text(
|
doc.text(
|
||||||
`Sie ist im Voraus, spätestens am ${contract.paymentDueDay}. Werktag eines jeden Monats, auf das ` +
|
`Sie ist im Voraus, spätestens am ${contract.paymentDueDay}. eines jeden Monats, auf das ` +
|
||||||
'vom Vermieter benannte Konto zu entrichten.',
|
'vom Vermieter benannte Konto zu entrichten.',
|
||||||
);
|
);
|
||||||
doc.text(UTILITY_LABEL[contract.utilityBillingModel]);
|
doc.text(UTILITY_LABEL[contract.utilityBillingModel]);
|
||||||
|
|||||||
@ -249,6 +249,7 @@ model Invitation {
|
|||||||
acceptedAt DateTime? @map("accepted_at")
|
acceptedAt DateTime? @map("accepted_at")
|
||||||
contractStartDate DateTime? @map("contract_start_date")
|
contractStartDate DateTime? @map("contract_start_date")
|
||||||
contractEndDate DateTime? @map("contract_end_date")
|
contractEndDate DateTime? @map("contract_end_date")
|
||||||
|
paymentDueDay Int? @map("payment_due_day") // Fälligkeit: 1 oder 15
|
||||||
|
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
|
||||||
@ -273,7 +274,7 @@ model Contract {
|
|||||||
|
|
||||||
totalWarmRent Decimal @map("total_warm_rent") @db.Decimal(8, 2)
|
totalWarmRent Decimal @map("total_warm_rent") @db.Decimal(8, 2)
|
||||||
depositAmount Decimal @map("deposit_amount") @db.Decimal(8, 2)
|
depositAmount Decimal @map("deposit_amount") @db.Decimal(8, 2)
|
||||||
paymentDueDay Int @default(3) @map("payment_due_day") // Fälligkeit: 3. Werktag
|
paymentDueDay Int @default(1) @map("payment_due_day") // Fälligkeit: Kalendertag im Monat (z.B. 1 oder 15)
|
||||||
|
|
||||||
// Kündigungsfristen & Anpassungsklauseln
|
// Kündigungsfristen & Anpassungsklauseln
|
||||||
noticePeriodMonths Int @default(3) @map("notice_period_months")
|
noticePeriodMonths Int @default(3) @map("notice_period_months")
|
||||||
|
|||||||
@ -681,6 +681,13 @@
|
|||||||
<input type="checkbox" id="inviteUnbefristet" checked /> unbefristet
|
<input type="checkbox" id="inviteUnbefristet" checked /> unbefristet
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="field" id="inviteDueDayField" style="display:none">
|
||||||
|
<label for="inviteDueDay">Fälligkeit der Miete</label>
|
||||||
|
<select id="inviteDueDay">
|
||||||
|
<option value="1">Immer zum 1.</option>
|
||||||
|
<option value="15">Immer zum 15.</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<button type="submit" class="btn-primary" style="width:auto" id="inviteSubmitBtn">Einladen</button>
|
<button type="submit" class="btn-primary" style="width:auto" id="inviteSubmitBtn">Einladen</button>
|
||||||
</form>
|
</form>
|
||||||
<div id="inviteLinkResult" style="display:none" class="invite-link-box">
|
<div id="inviteLinkResult" style="display:none" class="invite-link-box">
|
||||||
@ -1553,11 +1560,14 @@
|
|||||||
const startInput = document.getElementById('inviteStartDate');
|
const startInput = document.getElementById('inviteStartDate');
|
||||||
const endInput = document.getElementById('inviteEndDate');
|
const endInput = document.getElementById('inviteEndDate');
|
||||||
const unbefristetCheckbox = document.getElementById('inviteUnbefristet');
|
const unbefristetCheckbox = document.getElementById('inviteUnbefristet');
|
||||||
|
const dueDayField = document.getElementById('inviteDueDayField');
|
||||||
|
const dueDaySelect = document.getElementById('inviteDueDay');
|
||||||
|
|
||||||
roomSelect.onchange = () => {
|
roomSelect.onchange = () => {
|
||||||
const hasRoom = !!roomSelect.value;
|
const hasRoom = !!roomSelect.value;
|
||||||
startField.style.display = hasRoom ? 'flex' : 'none';
|
startField.style.display = hasRoom ? 'flex' : 'none';
|
||||||
endField.style.display = hasRoom ? 'flex' : 'none';
|
endField.style.display = hasRoom ? 'flex' : 'none';
|
||||||
|
dueDayField.style.display = hasRoom ? 'flex' : 'none';
|
||||||
if (hasRoom && !startInput.value) startInput.value = new Date().toISOString().slice(0, 10);
|
if (hasRoom && !startInput.value) startInput.value = new Date().toISOString().slice(0, 10);
|
||||||
};
|
};
|
||||||
unbefristetCheckbox.onchange = () => {
|
unbefristetCheckbox.onchange = () => {
|
||||||
@ -1581,9 +1591,10 @@
|
|||||||
const roomId = roomSelect.value || undefined;
|
const roomId = roomSelect.value || undefined;
|
||||||
const contractStartDate = roomId ? (startInput.value || undefined) : undefined;
|
const contractStartDate = roomId ? (startInput.value || undefined) : undefined;
|
||||||
const contractEndDate = roomId && !unbefristetCheckbox.checked ? (endInput.value || undefined) : undefined;
|
const contractEndDate = roomId && !unbefristetCheckbox.checked ? (endInput.value || undefined) : undefined;
|
||||||
|
const paymentDueDay = roomId ? Number(dueDaySelect.value) : undefined;
|
||||||
const data = await apiFetch('/invitations', {
|
const data = await apiFetch('/invitations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, fullName, roomId, contractStartDate, contractEndDate }),
|
body: JSON.stringify({ email, fullName, roomId, contractStartDate, contractEndDate, paymentDueDay }),
|
||||||
});
|
});
|
||||||
document.getElementById('inviteLinkText').textContent = data.inviteLink;
|
document.getElementById('inviteLinkText').textContent = data.inviteLink;
|
||||||
document.getElementById('inviteLinkResult').style.display = 'flex';
|
document.getElementById('inviteLinkResult').style.display = 'flex';
|
||||||
@ -1591,6 +1602,7 @@
|
|||||||
document.getElementById('inviteEmail').value = '';
|
document.getElementById('inviteEmail').value = '';
|
||||||
startField.style.display = 'none';
|
startField.style.display = 'none';
|
||||||
endField.style.display = 'none';
|
endField.style.display = 'none';
|
||||||
|
dueDayField.style.display = 'none';
|
||||||
startInput.value = '';
|
startInput.value = '';
|
||||||
endInput.value = '';
|
endInput.value = '';
|
||||||
unbefristetCheckbox.checked = true;
|
unbefristetCheckbox.checked = true;
|
||||||
@ -1639,6 +1651,13 @@
|
|||||||
revokeBtn.onclick = () => revokeInvitation(inv.id);
|
revokeBtn.onclick = () => revokeInvitation(inv.id);
|
||||||
actionsTd.appendChild(revokeBtn);
|
actionsTd.appendChild(revokeBtn);
|
||||||
}
|
}
|
||||||
|
if (inv.status === 'REVOKED' && currentUser.role === 'ADMIN') {
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'btn-secondary';
|
||||||
|
deleteBtn.textContent = 'Endgültig löschen';
|
||||||
|
deleteBtn.onclick = () => deleteInvitationPermanently(inv.id);
|
||||||
|
actionsTd.appendChild(deleteBtn);
|
||||||
|
}
|
||||||
tr.appendChild(actionsTd);
|
tr.appendChild(actionsTd);
|
||||||
body.appendChild(tr);
|
body.appendChild(tr);
|
||||||
});
|
});
|
||||||
@ -1657,6 +1676,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteInvitationPermanently(id) {
|
||||||
|
if (!confirm('Diese zurückgezogene Einladung endgültig löschen? Das kann nicht rückgängig gemacht werden.')) return;
|
||||||
|
try {
|
||||||
|
await apiFetch(`/invitations/${id}/permanent`, { method: 'DELETE' });
|
||||||
|
loadInvitations();
|
||||||
|
} catch (err) {
|
||||||
|
alert(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
// COCKPIT-DATEN
|
// COCKPIT-DATEN
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
@ -1739,6 +1768,13 @@
|
|||||||
amountRow.appendChild(el('span', 'value', formatEuro(light.amountDue)));
|
amountRow.appendChild(el('span', 'value', formatEuro(light.amountDue)));
|
||||||
card.appendChild(amountRow);
|
card.appendChild(amountRow);
|
||||||
|
|
||||||
|
if (light.dueDate) {
|
||||||
|
const dueDateRow = el('div', 'amount-row');
|
||||||
|
dueDateRow.appendChild(el('span', 'label', 'Fälligkeitsdatum'));
|
||||||
|
dueDateRow.appendChild(el('span', 'value', new Date(light.dueDate).toLocaleDateString('de-DE')));
|
||||||
|
card.appendChild(dueDateRow);
|
||||||
|
}
|
||||||
|
|
||||||
if (light.status === 'GREEN') {
|
if (light.status === 'GREEN') {
|
||||||
const paidRow = el('div', 'amount-row');
|
const paidRow = el('div', 'amount-row');
|
||||||
paidRow.appendChild(el('span', 'label', 'Bezahlt'));
|
paidRow.appendChild(el('span', 'label', 'Bezahlt'));
|
||||||
@ -3755,6 +3791,19 @@
|
|||||||
depositField.appendChild(depositInput);
|
depositField.appendChild(depositInput);
|
||||||
formArea.appendChild(depositField);
|
formArea.appendChild(depositField);
|
||||||
|
|
||||||
|
const dueDayField = el('div', 'field');
|
||||||
|
dueDayField.style.minWidth = '150px';
|
||||||
|
dueDayField.appendChild(el('label', null, 'Fälligkeit der Miete'));
|
||||||
|
const dueDaySelect = document.createElement('select');
|
||||||
|
[[1, 'Immer zum 1.'], [15, 'Immer zum 15.']].forEach(([value, label]) => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = value;
|
||||||
|
opt.textContent = label;
|
||||||
|
dueDaySelect.appendChild(opt);
|
||||||
|
});
|
||||||
|
dueDayField.appendChild(dueDaySelect);
|
||||||
|
formArea.appendChild(dueDayField);
|
||||||
|
|
||||||
const roomInfoById = {};
|
const roomInfoById = {};
|
||||||
|
|
||||||
// Datumsbewusste Zimmerauswahl: bei jeder Änderung von Mietbeginn/-ende
|
// Datumsbewusste Zimmerauswahl: bei jeder Änderung von Mietbeginn/-ende
|
||||||
@ -3816,6 +3865,7 @@
|
|||||||
endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined),
|
endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined),
|
||||||
totalWarmRent: rentInput.value,
|
totalWarmRent: rentInput.value,
|
||||||
depositAmount: depositInput.value,
|
depositAmount: depositInput.value,
|
||||||
|
paymentDueDay: Number(dueDaySelect.value),
|
||||||
}, submitBtn);
|
}, submitBtn);
|
||||||
formArea.appendChild(submitBtn);
|
formArea.appendChild(submitBtn);
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user