Einladung: Vertragsbeginn/-ende inkl. datumsbewusster Zimmer-Plausibilitätsprüfung
This commit is contained in:
parent
41c18d078f
commit
3111f64ecd
@ -3,6 +3,7 @@ import { PrismaClient, Prisma, UserRole } from '@prisma/client';
|
|||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from 'bcryptjs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { signAuthToken, requireAuth, requireRole, AuthedRequest } from '../middleware/auth';
|
import { signAuthToken, requireAuth, requireRole, AuthedRequest } from '../middleware/auth';
|
||||||
|
import { findOverlappingContract } from './contracts';
|
||||||
|
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
@ -20,13 +21,20 @@ function inviteLinkFor(req: Request, token: string): string {
|
|||||||
return `${baseUrl.replace(/\/$/, '')}/?invite=${token}`;
|
return `${baseUrl.replace(/\/$/, '')}/?invite=${token}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /v1/invitations (nur Vermieter/Admin) — { email, fullName?, roomId?, role? } -> { invitation, inviteLink }
|
// 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).
|
||||||
invitationsRouter.post(
|
invitationsRouter.post(
|
||||||
'/invitations',
|
'/invitations',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireRole('LANDLORD', 'ADMIN'),
|
requireRole('LANDLORD', 'ADMIN'),
|
||||||
async (req: AuthedRequest, res: Response) => {
|
async (req: AuthedRequest, res: Response) => {
|
||||||
const { email, fullName, roomId, role } = req.body || {};
|
const { email, fullName, roomId, role, contractStartDate, contractEndDate } = 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' });
|
||||||
}
|
}
|
||||||
@ -53,9 +61,29 @@ invitationsRouter.post(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let parsedContractStart: Date | null = null;
|
||||||
|
let parsedContractEnd: Date | null = null;
|
||||||
|
|
||||||
if (roomId) {
|
if (roomId) {
|
||||||
const room = await prisma.room.findUnique({ where: { id: roomId } });
|
const room = await prisma.room.findUnique({ where: { id: roomId } });
|
||||||
if (!room) return res.status(400).json({ error: 'Zimmer nicht gefunden' });
|
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');
|
const token = crypto.randomBytes(24).toString('hex');
|
||||||
@ -68,6 +96,8 @@ invitationsRouter.post(
|
|||||||
token,
|
token,
|
||||||
role: invitedRole,
|
role: invitedRole,
|
||||||
roomId: roomId || null,
|
roomId: roomId || null,
|
||||||
|
contractStartDate: parsedContractStart,
|
||||||
|
contractEndDate: parsedContractEnd,
|
||||||
invitedById: req.user!.id,
|
invitedById: req.user!.id,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
},
|
},
|
||||||
@ -128,6 +158,8 @@ invitationsRouter.get('/invitations/:token/preview', async (req: Request, res: R
|
|||||||
fullName: invitation.fullName,
|
fullName: invitation.fullName,
|
||||||
role: invitation.role,
|
role: invitation.role,
|
||||||
room: invitation.room,
|
room: invitation.room,
|
||||||
|
contractStartDate: invitation.contractStartDate,
|
||||||
|
contractEndDate: invitation.contractEndDate,
|
||||||
expiresAt: invitation.expiresAt,
|
expiresAt: invitation.expiresAt,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -240,7 +272,8 @@ invitationsRouter.post('/invitations/:token/accept', async (req: Request, res: R
|
|||||||
data: {
|
data: {
|
||||||
userId: created.id,
|
userId: created.id,
|
||||||
roomId: room.id,
|
roomId: room.id,
|
||||||
startDate: new Date(),
|
startDate: invitation.contractStartDate || new Date(),
|
||||||
|
endDate: invitation.contractEndDate,
|
||||||
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,
|
||||||
|
|||||||
@ -670,6 +670,17 @@
|
|||||||
<option value="">– kein Zimmer zuordnen –</option>
|
<option value="">– kein Zimmer zuordnen –</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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>
|
<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">
|
||||||
@ -1492,22 +1503,71 @@
|
|||||||
let lastRooms = [];
|
let lastRooms = [];
|
||||||
let lastCockpitData = null;
|
let lastCockpitData = null;
|
||||||
|
|
||||||
function populateRoomSelect() {
|
// 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 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>';
|
select.innerHTML = '<option value="">– kein Zimmer zuordnen –</option>';
|
||||||
// Nur freie Zimmer zur Auswahl anbieten — ein bereits belegtes Zimmer
|
try {
|
||||||
// kann nicht zusätzlich vergeben werden.
|
const params = new URLSearchParams({ startDate });
|
||||||
lastRooms.filter(r => r.status === 'VACANT').forEach(r => {
|
if (endDate) params.set('endDate', endDate);
|
||||||
|
const data = await apiFetch(`/contracts/room-availability?${params.toString()}`);
|
||||||
|
(data.rooms || []).forEach(r => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = r.id;
|
opt.value = r.id;
|
||||||
opt.textContent = r.roomNumber;
|
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);
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
function initInvitePanel() {
|
function initInvitePanel() {
|
||||||
const form = document.getElementById('inviteForm');
|
const form = document.getElementById('inviteForm');
|
||||||
const errorBox = document.getElementById('inviteFormError');
|
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) => {
|
form.onsubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -1518,16 +1578,24 @@
|
|||||||
try {
|
try {
|
||||||
const fullName = document.getElementById('inviteName').value.trim() || undefined;
|
const fullName = document.getElementById('inviteName').value.trim() || undefined;
|
||||||
const email = document.getElementById('inviteEmail').value.trim();
|
const email = document.getElementById('inviteEmail').value.trim();
|
||||||
const roomId = document.getElementById('inviteRoom').value || undefined;
|
const roomId = roomSelect.value || undefined;
|
||||||
|
const contractStartDate = roomId ? (startInput.value || undefined) : undefined;
|
||||||
|
const contractEndDate = roomId && !unbefristetCheckbox.checked ? (endInput.value || undefined) : undefined;
|
||||||
const data = await apiFetch('/invitations', {
|
const data = await apiFetch('/invitations', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ email, fullName, roomId }),
|
body: JSON.stringify({ email, fullName, roomId, contractStartDate, contractEndDate }),
|
||||||
});
|
});
|
||||||
document.getElementById('inviteLinkText').textContent = data.inviteLink;
|
document.getElementById('inviteLinkText').textContent = data.inviteLink;
|
||||||
document.getElementById('inviteLinkResult').style.display = 'flex';
|
document.getElementById('inviteLinkResult').style.display = 'flex';
|
||||||
document.getElementById('inviteName').value = '';
|
document.getElementById('inviteName').value = '';
|
||||||
document.getElementById('inviteEmail').value = '';
|
document.getElementById('inviteEmail').value = '';
|
||||||
|
startField.style.display = 'none';
|
||||||
|
endField.style.display = 'none';
|
||||||
|
startInput.value = '';
|
||||||
|
endInput.value = '';
|
||||||
|
unbefristetCheckbox.checked = true;
|
||||||
loadInvitations();
|
loadInvitations();
|
||||||
|
refreshInviteRoomOptions();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errorBox.textContent = err.message;
|
errorBox.textContent = err.message;
|
||||||
errorBox.style.display = 'block';
|
errorBox.style.display = 'block';
|
||||||
@ -3582,17 +3650,14 @@
|
|||||||
if (!isLandlord) { section.style.display = 'none'; return; }
|
if (!isLandlord) { section.style.display = 'none'; return; }
|
||||||
section.style.display = 'block';
|
section.style.display = 'block';
|
||||||
try {
|
try {
|
||||||
const [archiveData, roomsData] = await Promise.all([
|
const archiveData = await apiFetch('/contracts/archive');
|
||||||
apiFetch('/contracts/archive'),
|
renderContractArchive(archiveData.contracts);
|
||||||
apiFetch('/contracts/vacant-rooms'),
|
|
||||||
]);
|
|
||||||
renderContractArchive(archiveData.contracts, roomsData.rooms);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[contracts] Archiv laden fehlgeschlagen:', err.message);
|
console.error('[contracts] Archiv laden fehlgeschlagen:', err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderContractArchive(contracts, vacantRooms) {
|
function renderContractArchive(contracts) {
|
||||||
const list = document.getElementById('contractArchiveList');
|
const list = document.getElementById('contractArchiveList');
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
if (!contracts.length) {
|
if (!contracts.length) {
|
||||||
@ -3620,13 +3685,13 @@
|
|||||||
row.appendChild(docWrap);
|
row.appendChild(docWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
row.appendChild(buildReactivateSection(c, vacantRooms));
|
row.appendChild(buildReactivateSection(c));
|
||||||
|
|
||||||
list.appendChild(row);
|
list.appendChild(row);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildReactivateSection(c, vacantRooms) {
|
function buildReactivateSection(c) {
|
||||||
const wrap = el('div');
|
const wrap = el('div');
|
||||||
|
|
||||||
const toggleBtn = document.createElement('button');
|
const toggleBtn = document.createElement('button');
|
||||||
@ -3638,21 +3703,10 @@
|
|||||||
formArea.style.display = 'none';
|
formArea.style.display = 'none';
|
||||||
formArea.style.marginTop = '10px';
|
formArea.style.marginTop = '10px';
|
||||||
|
|
||||||
if (!vacantRooms.length) {
|
|
||||||
formArea.appendChild(el('span', 'meta-text', 'Kein freies Zimmer verfügbar.'));
|
|
||||||
} else {
|
|
||||||
const roomField = el('div', 'field');
|
const roomField = el('div', 'field');
|
||||||
roomField.style.minWidth = '150px';
|
roomField.style.minWidth = '190px';
|
||||||
roomField.appendChild(el('label', null, 'Zimmer'));
|
roomField.appendChild(el('label', null, 'Zimmer'));
|
||||||
const roomSelect = document.createElement('select');
|
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);
|
roomField.appendChild(roomSelect);
|
||||||
formArea.appendChild(roomField);
|
formArea.appendChild(roomField);
|
||||||
|
|
||||||
@ -3661,9 +3715,26 @@
|
|||||||
startField.appendChild(el('label', null, 'Mietbeginn'));
|
startField.appendChild(el('label', null, 'Mietbeginn'));
|
||||||
const startInput = document.createElement('input');
|
const startInput = document.createElement('input');
|
||||||
startInput.type = 'date';
|
startInput.type = 'date';
|
||||||
|
startInput.value = new Date().toISOString().slice(0, 10);
|
||||||
startField.appendChild(startInput);
|
startField.appendChild(startInput);
|
||||||
formArea.appendChild(startField);
|
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');
|
const rentField = el('div', 'field');
|
||||||
rentField.style.width = '130px';
|
rentField.style.width = '130px';
|
||||||
rentField.appendChild(el('label', null, 'Warmmiete (€)'));
|
rentField.appendChild(el('label', null, 'Warmmiete (€)'));
|
||||||
@ -3671,8 +3742,6 @@
|
|||||||
rentInput.type = 'number';
|
rentInput.type = 'number';
|
||||||
rentInput.min = '0';
|
rentInput.min = '0';
|
||||||
rentInput.step = '0.01';
|
rentInput.step = '0.01';
|
||||||
const firstRoom = vacantRooms[0];
|
|
||||||
rentInput.value = (Number(firstRoom.baseRent) + Number(firstRoom.utilityPauschal)).toFixed(2);
|
|
||||||
rentField.appendChild(rentInput);
|
rentField.appendChild(rentInput);
|
||||||
formArea.appendChild(rentField);
|
formArea.appendChild(rentField);
|
||||||
|
|
||||||
@ -3683,15 +3752,58 @@
|
|||||||
depositInput.type = 'number';
|
depositInput.type = 'number';
|
||||||
depositInput.min = '0';
|
depositInput.min = '0';
|
||||||
depositInput.step = '0.01';
|
depositInput.step = '0.01';
|
||||||
depositInput.value = (Number(firstRoom.baseRent) * 3).toFixed(2);
|
|
||||||
depositField.appendChild(depositInput);
|
depositField.appendChild(depositInput);
|
||||||
formArea.appendChild(depositField);
|
formArea.appendChild(depositField);
|
||||||
|
|
||||||
roomSelect.onchange = () => {
|
const roomInfoById = {};
|
||||||
const opt = roomSelect.selectedOptions[0];
|
|
||||||
rentInput.value = (Number(opt.dataset.baseRent) + Number(opt.dataset.utilityPauschal)).toFixed(2);
|
// Datumsbewusste Zimmerauswahl: bei jeder Änderung von Mietbeginn/-ende
|
||||||
depositInput.value = (Number(opt.dataset.baseRent) * 3).toFixed(2);
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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');
|
const submitBtn = document.createElement('button');
|
||||||
submitBtn.type = 'button';
|
submitBtn.type = 'button';
|
||||||
@ -3701,11 +3813,11 @@
|
|||||||
submitBtn.onclick = () => reactivateTenant(c.user.id, {
|
submitBtn.onclick = () => reactivateTenant(c.user.id, {
|
||||||
roomId: roomSelect.value,
|
roomId: roomSelect.value,
|
||||||
startDate: startInput.value,
|
startDate: startInput.value,
|
||||||
|
endDate: unbefristetCheckbox.checked ? undefined : (endInput.value || undefined),
|
||||||
totalWarmRent: rentInput.value,
|
totalWarmRent: rentInput.value,
|
||||||
depositAmount: depositInput.value,
|
depositAmount: depositInput.value,
|
||||||
}, submitBtn);
|
}, submitBtn);
|
||||||
formArea.appendChild(submitBtn);
|
formArea.appendChild(submitBtn);
|
||||||
}
|
|
||||||
|
|
||||||
toggleBtn.onclick = () => {
|
toggleBtn.onclick = () => {
|
||||||
formArea.style.display = formArea.style.display === 'none' ? 'flex' : 'none';
|
formArea.style.display = formArea.style.display === 'none' ? 'flex' : 'none';
|
||||||
@ -3718,6 +3830,7 @@
|
|||||||
|
|
||||||
async function reactivateTenant(userId, options, btn) {
|
async function reactivateTenant(userId, options, btn) {
|
||||||
if (!options.startDate) { alert('Bitte Mietbeginn angeben.'); return; }
|
if (!options.startDate) { alert('Bitte Mietbeginn angeben.'); return; }
|
||||||
|
if (!options.roomId) { alert('Bitte ein verfügbares Zimmer wählen.'); return; }
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = 'Legt an…';
|
btn.textContent = 'Legt an…';
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user