Putzplan von Mietern selbst pflegbar, Belegupload bei Ausgaben, Warmmiete in Kaltmiete/Nebenkosten/WLAN-Kosten aufgesplittet
This commit is contained in:
parent
117c0b66d9
commit
dd7077a214
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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 || {};
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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.',
|
||||||
|
|||||||
@ -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)
|
||||||
|
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user