Compare commits
No commits in common. "3111f64ecd9348196812086a79c76d2c1a234b12" and "e6b16ea74e0d33b915c6c0a92a18a810f2189b68" have entirely different histories.
3111f64ecd
...
e6b16ea74e
@ -3,7 +3,6 @@ import { PrismaClient, Prisma, UserRole } from '@prisma/client';
|
||||
import bcrypt from 'bcryptjs';
|
||||
import crypto from 'crypto';
|
||||
import { signAuthToken, requireAuth, requireRole, AuthedRequest } from '../middleware/auth';
|
||||
import { findOverlappingContract } from './contracts';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
@ -21,20 +20,13 @@ function inviteLinkFor(req: Request, token: string): string {
|
||||
return `${baseUrl.replace(/\/$/, '')}/?invite=${token}`;
|
||||
}
|
||||
|
||||
// POST /v1/invitations (nur Vermieter/Admin)
|
||||
// { email, fullName?, roomId?, role?, contractStartDate?, contractEndDate? } -> { invitation, inviteLink }
|
||||
//
|
||||
// contractStartDate/contractEndDate sind nur relevant, wenn ein Zimmer
|
||||
// zugeordnet wird: sie legen fest, ab wann (und ggf. bis wann, sonst
|
||||
// unbefristet) der bei Einladungsannahme automatisch erzeugte Mietvertrag
|
||||
// laufen soll — Basis für die Plausibilitätsprüfung gegen bereits vergebene
|
||||
// Zimmer (siehe findOverlappingContract in contracts.ts).
|
||||
// POST /v1/invitations (nur Vermieter/Admin) — { email, roomId?, role? } -> { invitation, inviteLink }
|
||||
invitationsRouter.post(
|
||||
'/invitations',
|
||||
requireAuth,
|
||||
requireRole('LANDLORD', 'ADMIN'),
|
||||
async (req: AuthedRequest, res: Response) => {
|
||||
const { email, fullName, roomId, role, contractStartDate, contractEndDate } = req.body || {};
|
||||
const { email, roomId, role } = req.body || {};
|
||||
if (!email || !String(email).trim()) {
|
||||
return res.status(400).json({ error: 'E-Mail-Adresse erforderlich' });
|
||||
}
|
||||
@ -61,29 +53,9 @@ invitationsRouter.post(
|
||||
});
|
||||
}
|
||||
|
||||
let parsedContractStart: Date | null = null;
|
||||
let parsedContractEnd: Date | null = null;
|
||||
|
||||
if (roomId) {
|
||||
const room = await prisma.room.findUnique({ where: { id: roomId } });
|
||||
if (!room) return res.status(400).json({ error: 'Zimmer nicht gefunden' });
|
||||
|
||||
if (contractEndDate !== undefined && contractEndDate !== null && Number.isNaN(Date.parse(contractEndDate))) {
|
||||
return res.status(400).json({ error: 'Mietende ist ungültig' });
|
||||
}
|
||||
if (contractStartDate !== undefined && contractStartDate !== null && Number.isNaN(Date.parse(contractStartDate))) {
|
||||
return res.status(400).json({ error: 'Mietbeginn ist ungültig' });
|
||||
}
|
||||
parsedContractStart = contractStartDate ? new Date(contractStartDate) : new Date();
|
||||
parsedContractEnd = contractEndDate ? new Date(contractEndDate) : null;
|
||||
|
||||
const overlap = await findOverlappingContract(prisma, roomId, parsedContractStart, parsedContractEnd);
|
||||
if (overlap) {
|
||||
const overlapUntil = overlap.endDate ? overlap.endDate.toLocaleDateString('de-DE') : 'unbefristet';
|
||||
return res.status(409).json({
|
||||
error: `Das Zimmer ist im gewählten Zeitraum bereits vergeben (belegt bis ${overlapUntil}).`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const token = crypto.randomBytes(24).toString('hex');
|
||||
@ -92,12 +64,9 @@ invitationsRouter.post(
|
||||
const invitation = await prisma.invitation.create({
|
||||
data: {
|
||||
email: normalizedEmail,
|
||||
fullName: typeof fullName === 'string' && fullName.trim() ? fullName.trim() : null,
|
||||
token,
|
||||
role: invitedRole,
|
||||
roomId: roomId || null,
|
||||
contractStartDate: parsedContractStart,
|
||||
contractEndDate: parsedContractEnd,
|
||||
invitedById: req.user!.id,
|
||||
expiresAt,
|
||||
},
|
||||
@ -155,11 +124,8 @@ invitationsRouter.get('/invitations/:token/preview', async (req: Request, res: R
|
||||
}
|
||||
res.status(200).json({
|
||||
email: invitation.email,
|
||||
fullName: invitation.fullName,
|
||||
role: invitation.role,
|
||||
room: invitation.room,
|
||||
contractStartDate: invitation.contractStartDate,
|
||||
contractEndDate: invitation.contractEndDate,
|
||||
expiresAt: invitation.expiresAt,
|
||||
});
|
||||
});
|
||||
@ -272,8 +238,7 @@ invitationsRouter.post('/invitations/:token/accept', async (req: Request, res: R
|
||||
data: {
|
||||
userId: created.id,
|
||||
roomId: room.id,
|
||||
startDate: invitation.contractStartDate || new Date(),
|
||||
endDate: invitation.contractEndDate,
|
||||
startDate: new Date(),
|
||||
totalWarmRent: Number(room.baseRent) + Number(room.utilityPauschal),
|
||||
depositAmount: Number(room.baseRent) * 3,
|
||||
isActive: true,
|
||||
|
||||
@ -237,7 +237,6 @@ model Room {
|
||||
model Invitation {
|
||||
id String @id @default(uuid())
|
||||
email String
|
||||
fullName String? @map("full_name")
|
||||
token String @unique
|
||||
role UserRole @default(TENANT)
|
||||
roomId String? @map("room_id")
|
||||
|
||||
@ -656,10 +656,6 @@
|
||||
<div class="invite-card">
|
||||
<div id="inviteFormError" style="display:none" class="form-error"></div>
|
||||
<form id="inviteForm" class="invite-form-row">
|
||||
<div class="field">
|
||||
<label for="inviteName">Name des Mieters (optional)</label>
|
||||
<input type="text" id="inviteName" placeholder="Max Mustermann" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="inviteEmail">E-Mail-Adresse des Mieters</label>
|
||||
<input type="email" id="inviteEmail" required placeholder="mieter@beispiel.de" />
|
||||
@ -670,17 +666,6 @@
|
||||
<option value="">– kein Zimmer zuordnen –</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" id="inviteStartField" style="display:none">
|
||||
<label for="inviteStartDate">Mietbeginn</label>
|
||||
<input type="date" id="inviteStartDate" />
|
||||
</div>
|
||||
<div class="field" id="inviteEndField" style="display:none">
|
||||
<label for="inviteEndDate">Mietende</label>
|
||||
<input type="date" id="inviteEndDate" />
|
||||
<label style="font-weight:normal; margin-top:4px;">
|
||||
<input type="checkbox" id="inviteUnbefristet" checked /> unbefristet
|
||||
</label>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary" style="width:auto" id="inviteSubmitBtn">Einladen</button>
|
||||
</form>
|
||||
<div id="inviteLinkResult" style="display:none" class="invite-link-box">
|
||||
@ -689,7 +674,7 @@
|
||||
</div>
|
||||
<table class="invite-table" id="inviteTable" style="display:none">
|
||||
<thead>
|
||||
<tr><th>Name</th><th>E-Mail</th><th>Zimmer</th><th>Status</th><th>Eingeladen am</th><th></th></tr>
|
||||
<tr><th>E-Mail</th><th>Zimmer</th><th>Status</th><th>Eingeladen am</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody id="inviteTableBody"></tbody>
|
||||
</table>
|
||||
@ -1320,7 +1305,6 @@
|
||||
sub.textContent = `Einladung als ${ROLE_LABEL[preview.role] || preview.role}` +
|
||||
(preview.room ? ` für ${preview.room.roomNumber}` : '') + '. Bitte Passwort festlegen.';
|
||||
document.getElementById('acceptEmail').value = preview.email;
|
||||
if (preview.fullName) document.getElementById('acceptFullName').value = preview.fullName;
|
||||
form.style.display = 'flex';
|
||||
form.style.flexDirection = 'column';
|
||||
|
||||
@ -1503,71 +1487,22 @@
|
||||
let lastRooms = [];
|
||||
let lastCockpitData = null;
|
||||
|
||||
// Zimmerauswahl bei der Einladung ist datumsbewusst: statt nur nach
|
||||
// Room.status zu filtern (das kennt nur "aktuell frei/belegt"), fragt sie
|
||||
// /contracts/room-availability für den gewählten Mietbeginn/-ende ab. So
|
||||
// lässt sich z.B. ein zum 1.9. gekündigtes Zimmer schon vorab ab 1.9. neu
|
||||
// vergeben, während ein unbefristet/darüber hinaus vergebenes Zimmer für
|
||||
// diesen Zeitraum ausgegraut bleibt (Plausibilitätsprüfung, siehe
|
||||
// findOverlappingContract in contracts.ts).
|
||||
async function refreshInviteRoomOptions() {
|
||||
const select = document.getElementById('inviteRoom');
|
||||
const previousValue = select.value;
|
||||
const startInput = document.getElementById('inviteStartDate');
|
||||
const endInput = document.getElementById('inviteEndDate');
|
||||
const unbefristet = document.getElementById('inviteUnbefristet').checked;
|
||||
const startDate = startInput.value || new Date().toISOString().slice(0, 10);
|
||||
const endDate = unbefristet ? '' : endInput.value;
|
||||
|
||||
select.innerHTML = '<option value="">– kein Zimmer zuordnen –</option>';
|
||||
try {
|
||||
const params = new URLSearchParams({ startDate });
|
||||
if (endDate) params.set('endDate', endDate);
|
||||
const data = await apiFetch(`/contracts/room-availability?${params.toString()}`);
|
||||
(data.rooms || []).forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.available ? r.roomNumber : `${r.roomNumber} (im Zeitraum belegt)`;
|
||||
opt.disabled = !r.available;
|
||||
if (!r.available && r.conflictReason) opt.title = r.conflictReason;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
if (previousValue && [...select.options].some(o => o.value === previousValue && !o.disabled)) {
|
||||
select.value = previousValue;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('[invite] Zimmerverfügbarkeit laden fehlgeschlagen:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function populateRoomSelect() {
|
||||
refreshInviteRoomOptions();
|
||||
const select = document.getElementById('inviteRoom');
|
||||
select.innerHTML = '<option value="">– kein Zimmer zuordnen –</option>';
|
||||
// Nur freie Zimmer zur Auswahl anbieten — ein bereits belegtes Zimmer
|
||||
// kann nicht zusätzlich vergeben werden.
|
||||
lastRooms.filter(r => r.status === 'VACANT').forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.roomNumber;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
function initInvitePanel() {
|
||||
const form = document.getElementById('inviteForm');
|
||||
const errorBox = document.getElementById('inviteFormError');
|
||||
const roomSelect = document.getElementById('inviteRoom');
|
||||
const startField = document.getElementById('inviteStartField');
|
||||
const endField = document.getElementById('inviteEndField');
|
||||
const startInput = document.getElementById('inviteStartDate');
|
||||
const endInput = document.getElementById('inviteEndDate');
|
||||
const unbefristetCheckbox = document.getElementById('inviteUnbefristet');
|
||||
|
||||
roomSelect.onchange = () => {
|
||||
const hasRoom = !!roomSelect.value;
|
||||
startField.style.display = hasRoom ? 'flex' : 'none';
|
||||
endField.style.display = hasRoom ? 'flex' : 'none';
|
||||
if (hasRoom && !startInput.value) startInput.value = new Date().toISOString().slice(0, 10);
|
||||
};
|
||||
unbefristetCheckbox.onchange = () => {
|
||||
endInput.disabled = unbefristetCheckbox.checked;
|
||||
if (unbefristetCheckbox.checked) endInput.value = '';
|
||||
refreshInviteRoomOptions();
|
||||
};
|
||||
startInput.onchange = refreshInviteRoomOptions;
|
||||
endInput.onchange = refreshInviteRoomOptions;
|
||||
endInput.disabled = unbefristetCheckbox.checked;
|
||||
|
||||
form.onsubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@ -1576,26 +1511,16 @@
|
||||
submitBtn.disabled = true;
|
||||
submitBtn.textContent = 'Lädt ein…';
|
||||
try {
|
||||
const fullName = document.getElementById('inviteName').value.trim() || undefined;
|
||||
const email = document.getElementById('inviteEmail').value.trim();
|
||||
const roomId = roomSelect.value || undefined;
|
||||
const contractStartDate = roomId ? (startInput.value || undefined) : undefined;
|
||||
const contractEndDate = roomId && !unbefristetCheckbox.checked ? (endInput.value || undefined) : undefined;
|
||||
const roomId = document.getElementById('inviteRoom').value || undefined;
|
||||
const data = await apiFetch('/invitations', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ email, fullName, roomId, contractStartDate, contractEndDate }),
|
||||
body: JSON.stringify({ email, roomId }),
|
||||
});
|
||||
document.getElementById('inviteLinkText').textContent = data.inviteLink;
|
||||
document.getElementById('inviteLinkResult').style.display = 'flex';
|
||||
document.getElementById('inviteName').value = '';
|
||||
document.getElementById('inviteEmail').value = '';
|
||||
startField.style.display = 'none';
|
||||
endField.style.display = 'none';
|
||||
startInput.value = '';
|
||||
endInput.value = '';
|
||||
unbefristetCheckbox.checked = true;
|
||||
loadInvitations();
|
||||
refreshInviteRoomOptions();
|
||||
} catch (err) {
|
||||
errorBox.textContent = err.message;
|
||||
errorBox.style.display = 'block';
|
||||
@ -1626,7 +1551,6 @@
|
||||
table.style.display = 'table';
|
||||
data.invitations.forEach(inv => {
|
||||
const tr = document.createElement('tr');
|
||||
tr.appendChild(el('td', '', inv.fullName || '–'));
|
||||
tr.appendChild(el('td', '', inv.email));
|
||||
tr.appendChild(el('td', '', inv.room ? inv.room.roomNumber : '–'));
|
||||
tr.appendChild(el('td', '', `<span class="invite-status-chip ${inv.status}">${INVITE_STATUS_LABEL[inv.status] || inv.status}</span>`));
|
||||
@ -3650,14 +3574,17 @@
|
||||
if (!isLandlord) { section.style.display = 'none'; return; }
|
||||
section.style.display = 'block';
|
||||
try {
|
||||
const archiveData = await apiFetch('/contracts/archive');
|
||||
renderContractArchive(archiveData.contracts);
|
||||
const [archiveData, roomsData] = await Promise.all([
|
||||
apiFetch('/contracts/archive'),
|
||||
apiFetch('/contracts/vacant-rooms'),
|
||||
]);
|
||||
renderContractArchive(archiveData.contracts, roomsData.rooms);
|
||||
} catch (err) {
|
||||
console.error('[contracts] Archiv laden fehlgeschlagen:', err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function renderContractArchive(contracts) {
|
||||
function renderContractArchive(contracts, vacantRooms) {
|
||||
const list = document.getElementById('contractArchiveList');
|
||||
list.innerHTML = '';
|
||||
if (!contracts.length) {
|
||||
@ -3685,13 +3612,13 @@
|
||||
row.appendChild(docWrap);
|
||||
}
|
||||
|
||||
row.appendChild(buildReactivateSection(c));
|
||||
row.appendChild(buildReactivateSection(c, vacantRooms));
|
||||
|
||||
list.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function buildReactivateSection(c) {
|
||||
function buildReactivateSection(c, vacantRooms) {
|
||||
const wrap = el('div');
|
||||
|
||||
const toggleBtn = document.createElement('button');
|
||||
@ -3703,122 +3630,75 @@
|
||||
formArea.style.display = 'none';
|
||||
formArea.style.marginTop = '10px';
|
||||
|
||||
const roomField = el('div', 'field');
|
||||
roomField.style.minWidth = '190px';
|
||||
roomField.appendChild(el('label', null, 'Zimmer'));
|
||||
const roomSelect = document.createElement('select');
|
||||
roomField.appendChild(roomSelect);
|
||||
formArea.appendChild(roomField);
|
||||
if (!vacantRooms.length) {
|
||||
formArea.appendChild(el('span', 'meta-text', 'Kein freies Zimmer verfügbar.'));
|
||||
} else {
|
||||
const roomField = el('div', 'field');
|
||||
roomField.style.minWidth = '150px';
|
||||
roomField.appendChild(el('label', null, 'Zimmer'));
|
||||
const roomSelect = document.createElement('select');
|
||||
vacantRooms.forEach(r => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.roomNumber;
|
||||
opt.dataset.baseRent = r.baseRent;
|
||||
opt.dataset.utilityPauschal = r.utilityPauschal;
|
||||
roomSelect.appendChild(opt);
|
||||
});
|
||||
roomField.appendChild(roomSelect);
|
||||
formArea.appendChild(roomField);
|
||||
|
||||
const startField = el('div', 'field');
|
||||
startField.style.minWidth = '150px';
|
||||
startField.appendChild(el('label', null, 'Mietbeginn'));
|
||||
const startInput = document.createElement('input');
|
||||
startInput.type = 'date';
|
||||
startInput.value = new Date().toISOString().slice(0, 10);
|
||||
startField.appendChild(startInput);
|
||||
formArea.appendChild(startField);
|
||||
const startField = el('div', 'field');
|
||||
startField.style.minWidth = '150px';
|
||||
startField.appendChild(el('label', null, 'Mietbeginn'));
|
||||
const startInput = document.createElement('input');
|
||||
startInput.type = 'date';
|
||||
startField.appendChild(startInput);
|
||||
formArea.appendChild(startField);
|
||||
|
||||
const endField = el('div', 'field');
|
||||
endField.style.minWidth = '150px';
|
||||
endField.appendChild(el('label', null, 'Mietende'));
|
||||
const endInput = document.createElement('input');
|
||||
endInput.type = 'date';
|
||||
endInput.disabled = true;
|
||||
endField.appendChild(endInput);
|
||||
const unbefristetLabel = el('label', null, ' unbefristet');
|
||||
unbefristetLabel.style.cssText = 'font-weight:normal; margin-top:4px; display:block;';
|
||||
const unbefristetCheckbox = document.createElement('input');
|
||||
unbefristetCheckbox.type = 'checkbox';
|
||||
unbefristetCheckbox.checked = true;
|
||||
unbefristetLabel.prepend(unbefristetCheckbox);
|
||||
endField.appendChild(unbefristetLabel);
|
||||
formArea.appendChild(endField);
|
||||
const rentField = el('div', 'field');
|
||||
rentField.style.width = '130px';
|
||||
rentField.appendChild(el('label', null, 'Warmmiete (€)'));
|
||||
const rentInput = document.createElement('input');
|
||||
rentInput.type = 'number';
|
||||
rentInput.min = '0';
|
||||
rentInput.step = '0.01';
|
||||
const firstRoom = vacantRooms[0];
|
||||
rentInput.value = (Number(firstRoom.baseRent) + Number(firstRoom.utilityPauschal)).toFixed(2);
|
||||
rentField.appendChild(rentInput);
|
||||
formArea.appendChild(rentField);
|
||||
|
||||
const rentField = el('div', 'field');
|
||||
rentField.style.width = '130px';
|
||||
rentField.appendChild(el('label', null, 'Warmmiete (€)'));
|
||||
const rentInput = document.createElement('input');
|
||||
rentInput.type = 'number';
|
||||
rentInput.min = '0';
|
||||
rentInput.step = '0.01';
|
||||
rentField.appendChild(rentInput);
|
||||
formArea.appendChild(rentField);
|
||||
const depositField = el('div', 'field');
|
||||
depositField.style.width = '130px';
|
||||
depositField.appendChild(el('label', null, 'Kaution (€)'));
|
||||
const depositInput = document.createElement('input');
|
||||
depositInput.type = 'number';
|
||||
depositInput.min = '0';
|
||||
depositInput.step = '0.01';
|
||||
depositInput.value = (Number(firstRoom.baseRent) * 3).toFixed(2);
|
||||
depositField.appendChild(depositInput);
|
||||
formArea.appendChild(depositField);
|
||||
|
||||
const depositField = el('div', 'field');
|
||||
depositField.style.width = '130px';
|
||||
depositField.appendChild(el('label', null, 'Kaution (€)'));
|
||||
const depositInput = document.createElement('input');
|
||||
depositInput.type = 'number';
|
||||
depositInput.min = '0';
|
||||
depositInput.step = '0.01';
|
||||
depositField.appendChild(depositInput);
|
||||
formArea.appendChild(depositField);
|
||||
roomSelect.onchange = () => {
|
||||
const opt = roomSelect.selectedOptions[0];
|
||||
rentInput.value = (Number(opt.dataset.baseRent) + Number(opt.dataset.utilityPauschal)).toFixed(2);
|
||||
depositInput.value = (Number(opt.dataset.baseRent) * 3).toFixed(2);
|
||||
};
|
||||
|
||||
const roomInfoById = {};
|
||||
|
||||
// Datumsbewusste Zimmerauswahl: bei jeder Änderung von Mietbeginn/-ende
|
||||
// wird /contracts/room-availability neu abgefragt, damit z.B. ein zum
|
||||
// gewählten Datum bereits gekündigtes (aber noch aktives) Zimmer als
|
||||
// verfügbar erscheint, ein unbefristet/darüber hinaus vergebenes aber
|
||||
// nicht (siehe findOverlappingContract in contracts.ts).
|
||||
async function refreshRoomOptions() {
|
||||
const startDate = startInput.value || new Date().toISOString().slice(0, 10);
|
||||
const endDate = unbefristetCheckbox.checked ? '' : endInput.value;
|
||||
const previousValue = roomSelect.value;
|
||||
try {
|
||||
const params = new URLSearchParams({ startDate });
|
||||
if (endDate) params.set('endDate', endDate);
|
||||
const data = await apiFetch(`/contracts/room-availability?${params.toString()}`);
|
||||
roomSelect.innerHTML = '';
|
||||
(data.rooms || []).forEach(r => {
|
||||
roomInfoById[r.id] = r;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = r.id;
|
||||
opt.textContent = r.available ? r.roomNumber : `${r.roomNumber} (im Zeitraum belegt)`;
|
||||
opt.disabled = !r.available;
|
||||
if (!r.available && r.conflictReason) opt.title = r.conflictReason;
|
||||
roomSelect.appendChild(opt);
|
||||
});
|
||||
const stillAvailable = [...roomSelect.options].some(o => o.value === previousValue && !o.disabled);
|
||||
roomSelect.value = stillAvailable ? previousValue : ([...roomSelect.options].find(o => !o.disabled)?.value || '');
|
||||
applyRoomDefaults();
|
||||
} catch (err) {
|
||||
console.error('[contracts] Zimmerverfügbarkeit laden fehlgeschlagen:', err.message);
|
||||
}
|
||||
const submitBtn = document.createElement('button');
|
||||
submitBtn.type = 'button';
|
||||
submitBtn.className = 'btn-primary';
|
||||
submitBtn.style.width = 'auto';
|
||||
submitBtn.textContent = 'Neuen Vertrag anlegen';
|
||||
submitBtn.onclick = () => reactivateTenant(c.user.id, {
|
||||
roomId: roomSelect.value,
|
||||
startDate: startInput.value,
|
||||
totalWarmRent: rentInput.value,
|
||||
depositAmount: depositInput.value,
|
||||
}, submitBtn);
|
||||
formArea.appendChild(submitBtn);
|
||||
}
|
||||
|
||||
function applyRoomDefaults() {
|
||||
const info = roomInfoById[roomSelect.value];
|
||||
if (!info) return;
|
||||
rentInput.value = (Number(info.baseRent) + Number(info.utilityPauschal)).toFixed(2);
|
||||
depositInput.value = (Number(info.baseRent) * 3).toFixed(2);
|
||||
}
|
||||
|
||||
roomSelect.onchange = applyRoomDefaults;
|
||||
startInput.onchange = refreshRoomOptions;
|
||||
endInput.onchange = refreshRoomOptions;
|
||||
unbefristetCheckbox.onchange = () => {
|
||||
endInput.disabled = unbefristetCheckbox.checked;
|
||||
if (unbefristetCheckbox.checked) endInput.value = '';
|
||||
refreshRoomOptions();
|
||||
};
|
||||
refreshRoomOptions();
|
||||
|
||||
const submitBtn = document.createElement('button');
|
||||
submitBtn.type = 'button';
|
||||
submitBtn.className = 'btn-primary';
|
||||
submitBtn.style.width = 'auto';
|
||||
submitBtn.textContent = 'Neuen Vertrag anlegen';
|
||||
submitBtn.onclick = () => reactivateTenant(c.user.id, {
|
||||
roomId: roomSelect.value,
|
||||
startDate: startInput.value,
|
||||
endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined),
|
||||
totalWarmRent: rentInput.value,
|
||||
depositAmount: depositInput.value,
|
||||
}, submitBtn);
|
||||
formArea.appendChild(submitBtn);
|
||||
|
||||
toggleBtn.onclick = () => {
|
||||
formArea.style.display = formArea.style.display === 'none' ? 'flex' : 'none';
|
||||
};
|
||||
@ -3830,7 +3710,6 @@
|
||||
|
||||
async function reactivateTenant(userId, options, btn) {
|
||||
if (!options.startDate) { alert('Bitte Mietbeginn angeben.'); return; }
|
||||
if (!options.roomId) { alert('Bitte ein verfügbares Zimmer wählen.'); return; }
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Legt an…';
|
||||
try {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user