Putzplan von Mietern selbst pflegbar, Belegupload bei Ausgaben, Warmmiete in Kaltmiete/Nebenkosten/WLAN-Kosten aufgesplittet

This commit is contained in:
Giuseppe Lombardo 2026-08-13 18:36:45 +00:00
parent 117c0b66d9
commit dd7077a214
8 changed files with 150 additions and 52 deletions

View File

@ -70,7 +70,8 @@ async function main() {
userId: tenant.id, userId: tenant.id,
roomId: room.id, roomId: room.id,
startDate: new Date('2025-10-01'), startDate: new Date('2025-10-01'),
totalWarmRent, coldRent: def.baseRent,
utilityCosts: def.utilityPauschal,
depositAmount: totalWarmRent * 3, depositAmount: totalWarmRent * 3,
isActive: true, isActive: true,
}, },

View File

@ -108,7 +108,7 @@ export class PaymentService {
tenantName: c.user.fullName, tenantName: c.user.fullName,
status, status,
dueDate: dueDate.toISOString(), dueDate: dueDate.toISOString(),
amountDue: Number(currentPayment?.amount ?? c.totalWarmRent), amountDue: Number(currentPayment?.amount ?? Number(c.coldRent) + Number(c.utilityCosts) + Number(c.wifiCost)),
amountPaid: paid ? Number(currentPayment.amount) : 0, amountPaid: paid ? Number(currentPayment.amount) : 0,
}; };
}); });

View File

@ -7,9 +7,12 @@ const prisma = new PrismaClient();
/** /**
* Router für den Putzplan (rotierende Reinigungsaufgaben). * Router für den Putzplan (rotierende Reinigungsaufgaben).
* *
* Zugriffsregel: ein Mieter sieht (und kann abhaken) nur seine eigenen * Zugriffsregel: der Putzplan wird von den Mietern selbst gepflegt jeder
* zugewiesenen Putzaufgaben. Vermieter/Admin sehen und verwalten den * eingeloggte Nutzer (Mieter wie Vermieter/Admin) kann Putzbereiche anlegen
* kompletten Putzplan aller Mieter. * und Aufgaben eintragen/zuweisen. Ein Mieter sieht (und kann abhaken) nur
* seine eigenen zugewiesenen Aufgaben; Vermieter/Admin sehen den kompletten
* Plan. Die Bestätigung/Verifizierung erledigter Aufgaben bleibt Vermieter/
* Admin vorbehalten (Qualitätskontrolle).
*/ */
export const cleaningTasksRouter = Router(); export const cleaningTasksRouter = Router();
@ -20,11 +23,11 @@ cleaningTasksRouter.get('/cleaning-areas', requireAuth, async (_req: AuthedReque
res.status(200).json({ areas }); res.status(200).json({ areas });
}); });
// POST /cleaning-areas { name } — nur Vermieter/Admin legen neue Bereiche an. // POST /cleaning-areas { name } — jeder eingeloggte Nutzer kann neue
// Bereiche anlegen (Putzplan wird von den Mietern selbst gepflegt).
cleaningTasksRouter.post( cleaningTasksRouter.post(
'/cleaning-areas', '/cleaning-areas',
requireAuth, requireAuth,
requireRole('LANDLORD', 'ADMIN'),
async (req: AuthedRequest, res: Response) => { async (req: AuthedRequest, res: Response) => {
const { name } = req.body || {}; const { name } = req.body || {};
if (!name || !String(name).trim()) { if (!name || !String(name).trim()) {
@ -52,11 +55,12 @@ const TASK_SELECT = {
assignedUser: { select: { id: true, fullName: true } }, assignedUser: { select: { id: true, fullName: true } },
}; };
// Vermieter/Admin: Liste der Mieter (für das Zuweisen-Formular im Web-Dashboard). // Liste der Mieter (für das Putzplan-Formular) — für alle eingeloggten
// Nutzer sichtbar, da der Putzplan von den Mietern selbst gepflegt wird und
// sie Aufgaben auch anderen Mitbewohnern zuweisen können.
cleaningTasksRouter.get( cleaningTasksRouter.get(
'/tenants', '/tenants',
requireAuth, requireAuth,
requireRole('LANDLORD', 'ADMIN'),
async (_req: AuthedRequest, res: Response) => { async (_req: AuthedRequest, res: Response) => {
const tenants = await prisma.user.findMany({ const tenants = await prisma.user.findMany({
where: { role: 'TENANT' }, where: { role: 'TENANT' },
@ -67,11 +71,12 @@ cleaningTasksRouter.get(
}, },
); );
// Vermieter/Admin legen eine neue Putzaufgabe an (z. B. für die kommende Rotationswoche). // Jeder eingeloggte Nutzer (Mieter wie Vermieter/Admin) legt eine neue
// Putzaufgabe an (z. B. für die kommende Rotationswoche) — der Putzplan wird
// von den Mietern selbst gepflegt/ausgefüllt.
cleaningTasksRouter.post( cleaningTasksRouter.post(
'/cleaning-tasks', '/cleaning-tasks',
requireAuth, requireAuth,
requireRole('LANDLORD', 'ADMIN'),
async (req: AuthedRequest, res: Response) => { async (req: AuthedRequest, res: Response) => {
const { assignedUserId, area, weekOf, dueDate } = req.body || {}; const { assignedUserId, area, weekOf, dueDate } = req.body || {};

View File

@ -115,7 +115,9 @@ const CONTRACT_DOCUMENTS_SELECT = {
endDate: true, endDate: true,
isActive: true, isActive: true,
contractDocumentUrls: true, contractDocumentUrls: true,
totalWarmRent: true, coldRent: true,
utilityCosts: true,
wifiCost: true,
depositAmount: true, depositAmount: true,
paymentDueDay: true, paymentDueDay: true,
noticePeriodMonths: true, noticePeriodMonths: true,
@ -177,7 +179,8 @@ contractsRouter.patch(
requireAuth, requireAuth,
requireRole('LANDLORD', 'ADMIN'), requireRole('LANDLORD', 'ADMIN'),
async (req: AuthedRequest, res: Response) => { async (req: AuthedRequest, res: Response) => {
const { startDate, endDate, totalWarmRent, depositAmount, paymentDueDay, noticePeriodMonths } = req.body || {}; const { startDate, endDate, coldRent, utilityCosts, wifiCost, depositAmount, paymentDueDay, noticePeriodMonths } =
req.body || {};
const existing = await prisma.contract.findUnique({ where: { id: req.params.id } }); const existing = await prisma.contract.findUnique({ where: { id: req.params.id } });
if (!existing) return res.status(404).json({ error: 'Vertrag nicht gefunden' }); if (!existing) return res.status(404).json({ error: 'Vertrag nicht gefunden' });
@ -188,8 +191,14 @@ contractsRouter.patch(
if (endDate !== undefined && endDate !== null && Number.isNaN(Date.parse(endDate))) { if (endDate !== undefined && endDate !== null && Number.isNaN(Date.parse(endDate))) {
return res.status(400).json({ error: 'endDate ist ungültig' }); return res.status(400).json({ error: 'endDate ist ungültig' });
} }
if (totalWarmRent !== undefined && (!Number.isFinite(Number(totalWarmRent)) || Number(totalWarmRent) <= 0)) { if (coldRent !== undefined && (!Number.isFinite(Number(coldRent)) || Number(coldRent) <= 0)) {
return res.status(400).json({ error: 'totalWarmRent muss eine positive Zahl sein' }); return res.status(400).json({ error: 'coldRent muss eine positive Zahl sein' });
}
if (utilityCosts !== undefined && (!Number.isFinite(Number(utilityCosts)) || Number(utilityCosts) < 0)) {
return res.status(400).json({ error: 'utilityCosts muss eine Zahl sein' });
}
if (wifiCost !== undefined && (!Number.isFinite(Number(wifiCost)) || Number(wifiCost) < 0)) {
return res.status(400).json({ error: 'wifiCost muss eine Zahl sein' });
} }
if (depositAmount !== undefined && (!Number.isFinite(Number(depositAmount)) || Number(depositAmount) < 0)) { if (depositAmount !== undefined && (!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' });
@ -222,7 +231,9 @@ contractsRouter.patch(
data: { data: {
startDate: startDate !== undefined ? parsedStart : undefined, startDate: startDate !== undefined ? parsedStart : undefined,
endDate: endDate !== undefined ? parsedEnd : undefined, endDate: endDate !== undefined ? parsedEnd : undefined,
totalWarmRent: totalWarmRent !== undefined ? Number(totalWarmRent) : undefined, coldRent: coldRent !== undefined ? Number(coldRent) : undefined,
utilityCosts: utilityCosts !== undefined ? Number(utilityCosts) : undefined,
wifiCost: wifiCost !== undefined ? Number(wifiCost) : undefined,
depositAmount: depositAmount !== undefined ? Number(depositAmount) : undefined, depositAmount: depositAmount !== undefined ? Number(depositAmount) : undefined,
paymentDueDay: paymentDueDay !== undefined ? Number(paymentDueDay) : undefined, paymentDueDay: paymentDueDay !== undefined ? Number(paymentDueDay) : undefined,
noticePeriodMonths: noticePeriodMonths !== undefined ? Number(noticePeriodMonths) : undefined, noticePeriodMonths: noticePeriodMonths !== undefined ? Number(noticePeriodMonths) : undefined,
@ -507,7 +518,8 @@ contractsRouter.post(
requireAuth, requireAuth,
requireRole('LANDLORD', 'ADMIN'), requireRole('LANDLORD', 'ADMIN'),
async (req: AuthedRequest, res: Response) => { async (req: AuthedRequest, res: Response) => {
const { userId, roomId, startDate, endDate, totalWarmRent, depositAmount, paymentDueDay, noticePeriodMonths } = req.body || {}; const { userId, roomId, startDate, endDate, coldRent, utilityCosts, wifiCost, depositAmount, paymentDueDay, noticePeriodMonths } =
req.body || {};
if (!userId || typeof userId !== 'string') return res.status(400).json({ error: 'userId ist erforderlich' }); if (!userId || typeof userId !== 'string') return res.status(400).json({ error: 'userId ist erforderlich' });
if (!roomId || typeof roomId !== 'string') return res.status(400).json({ error: 'roomId ist erforderlich' }); if (!roomId || typeof roomId !== 'string') return res.status(400).json({ error: 'roomId ist erforderlich' });
@ -515,8 +527,14 @@ contractsRouter.post(
if (endDate !== undefined && endDate !== null && Number.isNaN(Date.parse(endDate))) { if (endDate !== undefined && endDate !== null && Number.isNaN(Date.parse(endDate))) {
return res.status(400).json({ error: 'endDate ist ungueltig' }); return res.status(400).json({ error: 'endDate ist ungueltig' });
} }
if (!Number.isFinite(Number(totalWarmRent)) || Number(totalWarmRent) <= 0) { if (!Number.isFinite(Number(coldRent)) || Number(coldRent) <= 0) {
return res.status(400).json({ error: 'totalWarmRent muss eine positive Zahl sein' }); return res.status(400).json({ error: 'coldRent muss eine positive Zahl sein' });
}
if (utilityCosts !== undefined && (!Number.isFinite(Number(utilityCosts)) || Number(utilityCosts) < 0)) {
return res.status(400).json({ error: 'utilityCosts muss eine Zahl sein' });
}
if (wifiCost !== undefined && (!Number.isFinite(Number(wifiCost)) || Number(wifiCost) < 0)) {
return res.status(400).json({ error: 'wifiCost muss eine Zahl sein' });
} }
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' });
@ -555,7 +573,9 @@ contractsRouter.post(
roomId, roomId,
startDate: parsedStart, startDate: parsedStart,
endDate: parsedEnd, endDate: parsedEnd,
totalWarmRent: Number(totalWarmRent), coldRent: Number(coldRent),
utilityCosts: Number.isFinite(Number(utilityCosts)) ? Number(utilityCosts) : 0,
wifiCost: Number.isFinite(Number(wifiCost)) ? Number(wifiCost) : 0,
depositAmount: Number(depositAmount), depositAmount: Number(depositAmount),
paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 1, paymentDueDay: Number.isFinite(Number(paymentDueDay)) ? Number(paymentDueDay) : 1,
noticePeriodMonths: Number.isFinite(Number(noticePeriodMonths)) ? Number(noticePeriodMonths) : 3, noticePeriodMonths: Number.isFinite(Number(noticePeriodMonths)) ? Number(noticePeriodMonths) : 3,

View File

@ -297,7 +297,9 @@ invitationsRouter.post('/invitations/:token/accept', async (req: Request, res: R
startDate: invitation.contractStartDate || new Date(), startDate: invitation.contractStartDate || new Date(),
endDate: invitation.contractEndDate, endDate: invitation.contractEndDate,
paymentDueDay: invitation.paymentDueDay || 1, paymentDueDay: invitation.paymentDueDay || 1,
totalWarmRent: Number(room.baseRent) + Number(room.utilityPauschal), coldRent: Number(room.baseRent),
utilityCosts: Number(room.utilityPauschal),
wifiCost: 0,
depositAmount: Number(room.baseRent) * 3, depositAmount: Number(room.baseRent) * 3,
isActive: true, isActive: true,
}, },

View File

@ -117,7 +117,13 @@ export function generateContractPdf(contract: ContractForPdf): Promise<Buffer> {
// --- § 4 Miete und Nebenkosten --- // --- § 4 Miete und Nebenkosten ---
section('§ 4 Miete und Nebenkosten', () => { section('§ 4 Miete und Nebenkosten', () => {
doc.text(`Die Warmmiete beträgt ${formatEuro(contract.totalWarmRent)} monatlich.`); const totalRent = Number(contract.coldRent) + Number(contract.utilityCosts) + Number(contract.wifiCost);
doc.text(`Die Kaltmiete beträgt ${formatEuro(contract.coldRent)} monatlich.`);
doc.text(`Die Nebenkosten betragen ${formatEuro(contract.utilityCosts)} monatlich.`);
if (Number(contract.wifiCost) > 0) {
doc.text(`Die WLAN-Kosten betragen ${formatEuro(contract.wifiCost)} monatlich.`);
}
doc.text(`Die monatliche Gesamtmiete (Warmmiete) beträgt somit ${formatEuro(totalRent)}.`);
doc.text( doc.text(
`Sie ist im Voraus, spätestens am ${contract.paymentDueDay}. 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.',

View File

@ -272,7 +272,9 @@ model Contract {
startDate DateTime @map("start_date") startDate DateTime @map("start_date")
endDate DateTime? @map("end_date") // NULL = unbefristet endDate DateTime? @map("end_date") // NULL = unbefristet
totalWarmRent Decimal @map("total_warm_rent") @db.Decimal(8, 2) coldRent Decimal @map("cold_rent") @db.Decimal(8, 2) // Kaltmiete
utilityCosts Decimal @default(0) @map("utility_costs") @db.Decimal(8, 2) // Nebenkosten
wifiCost Decimal @default(0) @map("wifi_cost") @db.Decimal(8, 2) // WLAN-Kosten
depositAmount Decimal @map("deposit_amount") @db.Decimal(8, 2) depositAmount Decimal @map("deposit_amount") @db.Decimal(8, 2)
paymentDueDay Int @default(1) @map("payment_due_day") // Fälligkeit: Kalendertag im Monat (z.B. 1 oder 15) paymentDueDay Int @default(1) @map("payment_due_day") // Fälligkeit: Kalendertag im Monat (z.B. 1 oder 15)

View File

@ -827,8 +827,8 @@
<div class="page" id="page-cleaning"> <div class="page" id="page-cleaning">
<section id="cleaningAssignSection" style="display:none"> <section id="cleaningAssignSection">
<h2>Putzaufgabe zuweisen</h2> <h2>Putzaufgabe eintragen</h2>
<div class="invite-card"> <div class="invite-card">
<div id="cleaningFormError" style="display:none" class="form-error"></div> <div id="cleaningFormError" style="display:none" class="form-error"></div>
<form id="cleaningForm" class="invite-form-row" style="flex-wrap:wrap;"> <form id="cleaningForm" class="invite-form-row" style="flex-wrap:wrap;">
@ -937,6 +937,10 @@
<label for="expenseCategory">Kategorie</label> <label for="expenseCategory">Kategorie</label>
<select id="expenseCategory"></select> <select id="expenseCategory"></select>
</div> </div>
<div class="field" style="flex:1 1 200px;">
<label for="expenseReceipt">Beleg (Foto/PDF, optional)</label>
<input type="file" id="expenseReceipt" accept="image/*,application/pdf" />
</div>
<button type="submit" class="btn-primary" style="width:auto" id="expenseSubmitBtn">Eintragen</button> <button type="submit" class="btn-primary" style="width:auto" id="expenseSubmitBtn">Eintragen</button>
</form> </form>
</div> </div>
@ -1473,10 +1477,9 @@
document.getElementById('inviteSection').style.display = isLandlord ? 'block' : 'none'; document.getElementById('inviteSection').style.display = isLandlord ? 'block' : 'none';
document.getElementById('tenantDataSection').style.display = isLandlord ? 'block' : 'none'; document.getElementById('tenantDataSection').style.display = isLandlord ? 'block' : 'none';
document.getElementById('rentSectionTitle').textContent = isLandlord ? 'Miet-Ampel je Zimmer' : 'Meine Miete'; document.getElementById('rentSectionTitle').textContent = isLandlord ? 'Miet-Ampel je Zimmer' : 'Meine Miete';
document.getElementById('cleaningAssignSection').style.display = isLandlord ? 'block' : 'none';
document.getElementById('cleaningSectionTitle').textContent = isLandlord ? 'Putzplan (alle Mieter)' : 'Mein Putzplan'; document.getElementById('cleaningSectionTitle').textContent = isLandlord ? 'Putzplan (alle Mieter)' : 'Mein Putzplan';
if (isLandlord) initInvitePanel(); if (isLandlord) initInvitePanel();
if (isLandlord) initCleaningAssignForm(); initCleaningAssignForm();
document.getElementById('absenceUserField').style.display = isLandlord ? 'flex' : 'none'; document.getElementById('absenceUserField').style.display = isLandlord ? 'flex' : 'none';
@ -2386,7 +2389,9 @@
const amount = document.getElementById('expenseAmount').value; const amount = document.getElementById('expenseAmount').value;
const description = document.getElementById('expenseDescription').value.trim(); const description = document.getElementById('expenseDescription').value.trim();
const category = document.getElementById('expenseCategory').value; const category = document.getElementById('expenseCategory').value;
await apiFetch('/expenses', { method: 'POST', body: JSON.stringify({ amount, description, category }) }); const receiptFile = document.getElementById('expenseReceipt').files[0];
const receiptUrl = receiptFile ? await readFileAsCompressedDataUrl(receiptFile, 1600, 0.82) : undefined;
await apiFetch('/expenses', { method: 'POST', body: JSON.stringify({ amount, description, category, receiptUrl }) });
form.reset(); form.reset();
populateExpenseCategorySelect(); populateExpenseCategorySelect();
loadExpenses(); loadExpenses();
@ -2457,6 +2462,12 @@
const shareText = openShares.map(s => `${s.user.fullName}: ${formatEuro(s.shareAmount)}`).join(', '); const shareText = openShares.map(s => `${s.user.fullName}: ${formatEuro(s.shareAmount)}`).join(', ');
left.appendChild(el('div', 'meta-text', 'Offen: ' + shareText)); left.appendChild(el('div', 'meta-text', 'Offen: ' + shareText));
} }
if (x.receiptUrl) {
const chipWrap = el('div');
chipWrap.style.marginTop = '4px';
chipWrap.appendChild(buildDocumentChip('Beleg', x.receiptUrl));
left.appendChild(chipWrap);
}
row.appendChild(left); row.appendChild(left);
const right = el('div'); const right = el('div');
@ -3640,7 +3651,7 @@
} }
// Editierbare Vertrags-Eckdaten (dieselben Felder wie bei der // Editierbare Vertrags-Eckdaten (dieselben Felder wie bei der
// Vertragserstellung: Mietbeginn/-ende, Warmmiete, Kaution, Fälligkeit, // Vertragserstellung: Mietbeginn/-ende, Kaltmiete/Nebenkosten/WLAN-Kosten, Kaution, Fälligkeit,
// Kündigungsfrist) — jederzeit vom Vermieter über PATCH /contracts/:id // Kündigungsfrist) — jederzeit vom Vermieter über PATCH /contracts/:id
// änderbar, inkl. serverseitiger Plausibilitätsprüfung bei Datumsänderung. // änderbar, inkl. serverseitiger Plausibilitätsprüfung bei Datumsänderung.
function buildContractEckdatenForm(c) { function buildContractEckdatenForm(c) {
@ -3680,16 +3691,38 @@
if (unbefristetCheckbox.checked) endInput.value = ''; if (unbefristetCheckbox.checked) endInput.value = '';
}; };
const rentField = el('div', 'field'); const coldRentField = el('div', 'field');
rentField.style.cssText = 'width:95px; flex:0 0 auto;'; coldRentField.style.cssText = 'width:95px; flex:0 0 auto;';
rentField.appendChild(el('label', null, 'Warmmiete (€)')); coldRentField.appendChild(el('label', null, 'Kaltmiete (€)'));
const rentInput = document.createElement('input'); const coldRentInput = document.createElement('input');
rentInput.type = 'number'; coldRentInput.type = 'number';
rentInput.min = '0'; coldRentInput.min = '0';
rentInput.step = '0.01'; coldRentInput.step = '0.01';
rentInput.value = c.totalWarmRent; coldRentInput.value = c.coldRent;
rentField.appendChild(rentInput); coldRentField.appendChild(coldRentInput);
form.appendChild(rentField); form.appendChild(coldRentField);
const utilityCostsField = el('div', 'field');
utilityCostsField.style.cssText = 'width:95px; flex:0 0 auto;';
utilityCostsField.appendChild(el('label', null, 'Nebenkosten (€)'));
const utilityCostsInput = document.createElement('input');
utilityCostsInput.type = 'number';
utilityCostsInput.min = '0';
utilityCostsInput.step = '0.01';
utilityCostsInput.value = c.utilityCosts;
utilityCostsField.appendChild(utilityCostsInput);
form.appendChild(utilityCostsField);
const wifiCostField = el('div', 'field');
wifiCostField.style.cssText = 'width:95px; flex:0 0 auto;';
wifiCostField.appendChild(el('label', null, 'WLAN-Kosten (€)'));
const wifiCostInput = document.createElement('input');
wifiCostInput.type = 'number';
wifiCostInput.min = '0';
wifiCostInput.step = '0.01';
wifiCostInput.value = c.wifiCost;
wifiCostField.appendChild(wifiCostInput);
form.appendChild(wifiCostField);
const depositField = el('div', 'field'); const depositField = el('div', 'field');
depositField.style.cssText = 'width:95px; flex:0 0 auto;'; depositField.style.cssText = 'width:95px; flex:0 0 auto;';
@ -3726,7 +3759,10 @@
noticeField.appendChild(noticeInput); noticeField.appendChild(noticeInput);
form.appendChild(noticeField); form.appendChild(noticeField);
const allInputs = [startInput, endInput, unbefristetCheckbox, rentInput, depositInput, dueDaySelect, noticeInput]; const allInputs = [
startInput, endInput, unbefristetCheckbox, coldRentInput, utilityCostsInput, wifiCostInput,
depositInput, dueDaySelect, noticeInput,
];
const setEditable = (editable) => { const setEditable = (editable) => {
allInputs.forEach(input => { input.disabled = !editable; }); allInputs.forEach(input => { input.disabled = !editable; });
if (editable) endInput.disabled = unbefristetCheckbox.checked; if (editable) endInput.disabled = unbefristetCheckbox.checked;
@ -3766,7 +3802,9 @@
body: JSON.stringify({ body: JSON.stringify({
startDate: startInput.value, startDate: startInput.value,
endDate: unbefristetCheckbox.checked ? null : (endInput.value || null), endDate: unbefristetCheckbox.checked ? null : (endInput.value || null),
totalWarmRent: rentInput.value, coldRent: coldRentInput.value,
utilityCosts: utilityCostsInput.value,
wifiCost: wifiCostInput.value,
depositAmount: depositInput.value, depositAmount: depositInput.value,
paymentDueDay: Number(dueDaySelect.value), paymentDueDay: Number(dueDaySelect.value),
noticePeriodMonths: noticeInput.value, noticePeriodMonths: noticeInput.value,
@ -3996,15 +4034,36 @@
endField.appendChild(unbefristetLabel); endField.appendChild(unbefristetLabel);
formArea.appendChild(endField); formArea.appendChild(endField);
const rentField = el('div', 'field'); const coldRentField = el('div', 'field');
rentField.style.width = '130px'; coldRentField.style.width = '110px';
rentField.appendChild(el('label', null, 'Warmmiete (€)')); coldRentField.appendChild(el('label', null, 'Kaltmiete (€)'));
const rentInput = document.createElement('input'); const coldRentInput = document.createElement('input');
rentInput.type = 'number'; coldRentInput.type = 'number';
rentInput.min = '0'; coldRentInput.min = '0';
rentInput.step = '0.01'; coldRentInput.step = '0.01';
rentField.appendChild(rentInput); coldRentField.appendChild(coldRentInput);
formArea.appendChild(rentField); formArea.appendChild(coldRentField);
const utilityCostsField = el('div', 'field');
utilityCostsField.style.width = '110px';
utilityCostsField.appendChild(el('label', null, 'Nebenkosten (€)'));
const utilityCostsInput = document.createElement('input');
utilityCostsInput.type = 'number';
utilityCostsInput.min = '0';
utilityCostsInput.step = '0.01';
utilityCostsField.appendChild(utilityCostsInput);
formArea.appendChild(utilityCostsField);
const wifiCostField = el('div', 'field');
wifiCostField.style.width = '110px';
wifiCostField.appendChild(el('label', null, 'WLAN-Kosten (€)'));
const wifiCostInput = document.createElement('input');
wifiCostInput.type = 'number';
wifiCostInput.min = '0';
wifiCostInput.step = '0.01';
wifiCostInput.value = '0';
wifiCostField.appendChild(wifiCostInput);
formArea.appendChild(wifiCostField);
const depositField = el('div', 'field'); const depositField = el('div', 'field');
depositField.style.width = '130px'; depositField.style.width = '130px';
@ -4065,7 +4124,8 @@
function applyRoomDefaults() { function applyRoomDefaults() {
const info = roomInfoById[roomSelect.value]; const info = roomInfoById[roomSelect.value];
if (!info) return; if (!info) return;
rentInput.value = (Number(info.baseRent) + Number(info.utilityPauschal)).toFixed(2); coldRentInput.value = Number(info.baseRent).toFixed(2);
utilityCostsInput.value = Number(info.utilityPauschal).toFixed(2);
depositInput.value = (Number(info.baseRent) * 3).toFixed(2); depositInput.value = (Number(info.baseRent) * 3).toFixed(2);
} }
@ -4088,7 +4148,9 @@
roomId: roomSelect.value, roomId: roomSelect.value,
startDate: startInput.value, startDate: startInput.value,
endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined), endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined),
totalWarmRent: rentInput.value, coldRent: coldRentInput.value,
utilityCosts: utilityCostsInput.value,
wifiCost: wifiCostInput.value,
depositAmount: depositInput.value, depositAmount: depositInput.value,
paymentDueDay: Number(dueDaySelect.value), paymentDueDay: Number(dueDaySelect.value),
}, submitBtn); }, submitBtn);