Dateien nach "web-dashboard" hochladen

This commit is contained in:
Giuseppe Lombardo 2026-08-12 21:56:12 +00:00
parent ca6d851a70
commit c8a5984f8b

View File

@ -707,6 +707,66 @@
</div>
<div class="ticket-list" id="documentList"></div>
</section>
<section id="noticeDeadlinesSection">
<h2>Kündigungsfristen</h2>
<p class="hint-text">Zeigt für befristete Mietverträge, bis wann spätestens gekündigt werden muss.</p>
<div class="ticket-list" id="noticeDeadlinesList"></div>
</section>
<section id="utilityStatementsSection">
<h2>Nebenkostenabrechnung</h2>
<div class="invite-card" id="utilityStatementFormCard" style="display:none; margin-bottom:16px;">
<div id="utilityStatementFormError" style="display:none" class="form-error"></div>
<form id="utilityStatementForm" class="invite-form-row" style="flex-wrap:wrap;">
<div class="field" style="flex:1 1 150px;">
<label for="utilPeriodStart">Zeitraum von</label>
<input type="date" id="utilPeriodStart" required />
</div>
<div class="field" style="flex:1 1 150px;">
<label for="utilPeriodEnd">Zeitraum bis</label>
<input type="date" id="utilPeriodEnd" required />
</div>
<div class="field" style="flex:1 1 140px;">
<label for="utilTotalAmount">Gesamtbetrag (€)</label>
<input type="number" step="0.01" min="0.01" id="utilTotalAmount" required placeholder="z. B. 840.00" />
</div>
<div class="field" style="flex:2 1 200px;">
<label for="utilDescription">Beschreibung</label>
<input type="text" id="utilDescription" placeholder="z. B. Nebenkosten 2025" />
</div>
<button type="submit" class="btn-primary" style="width:auto" id="utilStatementSubmitBtn">Abrechnung anlegen (Split nach Zimmergröße)</button>
</form>
</div>
<div class="ticket-list" id="utilityStatementsList"></div>
</section>
<section id="craftsmenSection">
<h2>Handwerker-Kontakte</h2>
<div class="invite-card" id="craftsmanFormCard" style="display:none; margin-bottom:16px;">
<div id="craftsmanFormError" style="display:none" class="form-error"></div>
<form id="craftsmanForm" class="invite-form-row" style="flex-wrap:wrap;">
<div class="field" style="flex:1 1 180px;">
<label for="craftsmanName">Firma / Name</label>
<input type="text" id="craftsmanName" required placeholder="z. B. Sanitär Müller" />
</div>
<div class="field" style="flex:1 1 150px;">
<label for="craftsmanTrade">Gewerk</label>
<select id="craftsmanTrade"></select>
</div>
<div class="field" style="flex:1 1 150px;">
<label for="craftsmanPhone">Telefon</label>
<input type="text" id="craftsmanPhone" placeholder="z. B. 06131 12345" />
</div>
<div class="field" style="flex:1 1 180px;">
<label for="craftsmanEmail">E-Mail</label>
<input type="email" id="craftsmanEmail" placeholder="kontakt@handwerker.de" />
</div>
<button type="submit" class="btn-primary" style="width:auto" id="craftsmanSubmitBtn">Hinzufügen</button>
</form>
</div>
<div class="ticket-list" id="craftsmenList"></div>
</section>
</main>
</div>
@ -733,6 +793,8 @@
const STORAGE_LOCATION_LABEL = { FRIDGE: 'Kühlschrank', FREEZER: 'Gefrierfach', KITCHEN_CABINET_1: 'Küchenschrank 1', KITCHEN_CABINET_2: 'Küchenschrank 2', KITCHEN_CABINET_3: 'Küchenschrank 3', PANTRY: 'Vorratsschrank' };
const TRASH_TYPE_LABEL = { RESTMUELL: 'Restmüll', BIOMUELL: 'Biomüll', GELBER_SACK: 'Gelber Sack', PAPIER: 'Papier', GLAS: 'Glas' };
const DOCUMENT_CATEGORY_LABEL = { HAUSORDNUNG: 'Hausordnung', WLAN: 'WLAN', VERTRAG: 'Vertrag', TUTORIAL: 'Tutorial', SONSTIGES: 'Sonstiges' };
const CRAFTSMAN_TRADE_LABEL = { SANITAER: 'Sanitär', ELEKTRIK: 'Elektrik', HEIZUNG: 'Heizung', SCHREINEREI: 'Schreinerei', MALER: 'Maler', SCHLUESSELDIENST: 'Schlüsseldienst', SONSTIGES: 'Sonstiges' };
const SETTLEMENT_STATUS_LABEL = { OPEN: 'Offen', SETTLED: 'Beglichen' };
function formatEuro(n) {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n || 0);
@ -910,6 +972,8 @@
document.getElementById('storageFormCard').style.display = isLandlord ? 'block' : 'none';
document.getElementById('trashFormCard').style.display = isLandlord ? 'block' : 'none';
document.getElementById('documentFormCard').style.display = isLandlord ? 'block' : 'none';
document.getElementById('utilityStatementFormCard').style.display = isLandlord ? 'block' : 'none';
document.getElementById('craftsmanFormCard').style.display = isLandlord ? 'block' : 'none';
initTicketForm();
initAbsenceForm();
@ -919,6 +983,8 @@
initGuestCodeForm();
if (isLandlord) initStorageForms();
if (isLandlord) initDocumentForm();
if (isLandlord) initUtilityStatementForm();
if (isLandlord) initCraftsmanForm();
loadData();
loadCleaningTasks();
@ -929,6 +995,9 @@
loadGuestCodes();
loadStorage();
loadDocuments();
loadNoticeDeadlines();
loadUtilityStatements();
loadCraftsmen();
setInterval(loadData, REFRESH_INTERVAL_MS);
setInterval(loadCleaningTasks, REFRESH_INTERVAL_MS);
@ -939,6 +1008,9 @@
setInterval(loadGuestCodes, REFRESH_INTERVAL_MS);
setInterval(loadStorage, REFRESH_INTERVAL_MS);
setInterval(loadDocuments, REFRESH_INTERVAL_MS);
setInterval(loadNoticeDeadlines, REFRESH_INTERVAL_MS);
setInterval(loadUtilityStatements, REFRESH_INTERVAL_MS);
setInterval(loadCraftsmen, REFRESH_INTERVAL_MS);
}
// ---------------------------------------------------------------------
@ -1142,6 +1214,9 @@
`${t.room ? t.room.roomNumber : 'Gemeinschaftsbereich'} · ${CATEGORY_LABEL[t.category] || t.category} · ` +
`${t.creator ? t.creator.fullName + ' · ' : ''}${new Date(t.createdAt).toLocaleDateString('de-DE')} · ` +
TICKET_STATUS_LABEL[t.status]));
if (t.craftsman) {
left.appendChild(el('div', 'meta-text', `Handwerker: ${t.craftsman.name} (${CRAFTSMAN_TRADE_LABEL[t.craftsman.trade] || t.craftsman.trade})${t.craftsman.phone ? ' · ' + t.craftsman.phone : ''}`));
}
row.appendChild(left);
const right = el('div');
@ -1151,6 +1226,22 @@
right.appendChild(el('span', `priority-chip ${t.priority}`, PRIORITY_LABEL[t.priority] || t.priority));
if (isLandlord) {
const craftsmanSelect = document.createElement('select');
craftsmanSelect.style.maxWidth = '160px';
const noneOpt = document.createElement('option');
noneOpt.value = '';
noneOpt.textContent = 'Kein Handwerker';
craftsmanSelect.appendChild(noneOpt);
lastCraftsmen.forEach(c => {
const opt = document.createElement('option');
opt.value = c.id;
opt.textContent = c.name;
if (t.craftsman && t.craftsman.id === c.id) opt.selected = true;
craftsmanSelect.appendChild(opt);
});
craftsmanSelect.onchange = () => assignCraftsmanToTicket(t.id, craftsmanSelect.value);
right.appendChild(craftsmanSelect);
if (t.status === 'OPEN') {
const btn = document.createElement('button');
btn.className = 'btn-secondary';
@ -2322,6 +2413,262 @@
}
}
// ---------------------------------------------------------------------
// KÜNDIGUNGSFRISTEN
// ---------------------------------------------------------------------
async function loadNoticeDeadlines() {
if (!currentUser) return;
try {
const data = await apiFetch('/contracts/notice-deadlines');
renderNoticeDeadlines(data.deadlines);
} catch (err) {
console.error('[notice-deadlines] Laden fehlgeschlagen:', err.message);
}
}
function renderNoticeDeadlines(deadlines) {
const list = document.getElementById('noticeDeadlinesList');
list.innerHTML = '';
if (!deadlines.length) {
list.appendChild(el('div', 'empty-state', 'Keine aktiven Verträge gefunden.'));
return;
}
const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
deadlines.forEach(d => {
const row = el('div', 'ticket-row');
const left = el('div');
const who = isLandlord ? `${d.user.fullName} · ${d.room ? d.room.roomNumber : ''} · ` : '';
left.appendChild(el('div', 'title', who + (d.endDate ? `Vertrag befristet bis ${new Date(d.endDate).toLocaleDateString('de-DE')}` : 'Unbefristeter Vertrag')));
const metaParts = [`Kündigungsfrist: ${d.noticePeriodMonths} Monate`];
if (d.noticeDeadline) {
metaParts.push(`spätester Kündigungstermin: ${new Date(d.noticeDeadline).toLocaleDateString('de-DE')}`);
if (d.isOverdue) metaParts.push('Frist bereits verstrichen');
else if (d.isUrgent) metaParts.push(`noch ${d.daysUntilDeadline} Tage`);
}
left.appendChild(el('div', 'meta-text', metaParts.join(' · ')));
row.appendChild(left);
if (d.isUrgent || d.isOverdue) {
const right = el('div');
right.appendChild(el('span', `priority-chip ${d.isOverdue ? 'EMERGENCY' : 'HIGH'}`, d.isOverdue ? 'Frist abgelaufen' : 'Bald fällig'));
row.appendChild(right);
}
list.appendChild(row);
});
}
// ---------------------------------------------------------------------
// NEBENKOSTENABRECHNUNG
// ---------------------------------------------------------------------
function initUtilityStatementForm() {
const form = document.getElementById('utilityStatementForm');
const errorBox = document.getElementById('utilityStatementFormError');
form.onsubmit = async (e) => {
e.preventDefault();
errorBox.style.display = 'none';
const submitBtn = document.getElementById('utilStatementSubmitBtn');
submitBtn.disabled = true;
submitBtn.textContent = 'Speichert…';
try {
const periodStart = document.getElementById('utilPeriodStart').value;
const periodEnd = document.getElementById('utilPeriodEnd').value;
const totalAmount = document.getElementById('utilTotalAmount').value;
const description = document.getElementById('utilDescription').value.trim();
await apiFetch('/utility-statements', {
method: 'POST',
body: JSON.stringify({ periodStart, periodEnd, totalAmount, description }),
});
form.reset();
loadUtilityStatements();
} catch (err) {
errorBox.textContent = err.message;
errorBox.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Abrechnung anlegen (Split nach Zimmergröße)';
}
};
}
async function loadUtilityStatements() {
if (!currentUser) return;
try {
const data = await apiFetch('/utility-statements');
renderUtilityStatements(data.statements);
} catch (err) {
console.error('[utility-statements] Laden fehlgeschlagen:', err.message);
}
}
function renderUtilityStatements(statements) {
const list = document.getElementById('utilityStatementsList');
list.innerHTML = '';
if (!statements.length) {
list.appendChild(el('div', 'empty-state', 'Noch keine Nebenkostenabrechnung erstellt.'));
return;
}
const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
statements.forEach(s => {
const wrap = el('div', 'ticket-row');
wrap.style.flexDirection = 'column';
wrap.style.alignItems = 'stretch';
const head = el('div');
head.appendChild(el('div', 'title',
`${new Date(s.periodStart).toLocaleDateString('de-DE')} ${new Date(s.periodEnd).toLocaleDateString('de-DE')} · ${formatEuro(s.totalAmount)}`));
head.appendChild(el('div', 'meta-text', s.description || ''));
wrap.appendChild(head);
(s.shares || []).forEach(share => {
const shareRow = el('div', 'ticket-row');
shareRow.style.marginTop = '6px';
const left = el('div');
left.appendChild(el('div', 'title', isLandlord ? `${share.user.fullName} · ${share.room ? share.room.roomNumber : ''}` : 'Mein Anteil'));
left.appendChild(el('div', 'meta-text', `${formatEuro(share.shareAmount)} · ${SETTLEMENT_STATUS_LABEL[share.settlementStatus]}`));
shareRow.appendChild(left);
if (share.settlementStatus === 'OPEN' && (isLandlord || share.user.id === currentUser.id)) {
const btn = document.createElement('button');
btn.className = 'btn-secondary';
btn.textContent = 'Als beglichen markieren';
btn.onclick = () => settleUtilityShare(share.id);
shareRow.appendChild(btn);
}
wrap.appendChild(shareRow);
});
if (isLandlord) {
const delBtn = document.createElement('button');
delBtn.className = 'btn-secondary';
delBtn.style.marginTop = '8px';
delBtn.style.width = 'fit-content';
delBtn.textContent = 'Abrechnung löschen';
delBtn.onclick = () => deleteUtilityStatement(s.id);
wrap.appendChild(delBtn);
}
list.appendChild(wrap);
});
}
async function settleUtilityShare(id) {
try {
await apiFetch(`/utility-statement-shares/${id}`, { method: 'PATCH' });
loadUtilityStatements();
} catch (err) {
alert(err.message);
}
}
async function deleteUtilityStatement(id) {
try {
await apiFetch(`/utility-statements/${id}`, { method: 'DELETE' });
loadUtilityStatements();
} catch (err) {
alert(err.message);
}
}
// ---------------------------------------------------------------------
// HANDWERKER-KONTAKTE
// ---------------------------------------------------------------------
let lastCraftsmen = [];
function populateCraftsmanTradeSelect() {
const select = document.getElementById('craftsmanTrade');
select.innerHTML = '';
Object.keys(CRAFTSMAN_TRADE_LABEL).forEach(key => {
const opt = document.createElement('option');
opt.value = key;
opt.textContent = CRAFTSMAN_TRADE_LABEL[key];
select.appendChild(opt);
});
}
function initCraftsmanForm() {
populateCraftsmanTradeSelect();
const form = document.getElementById('craftsmanForm');
const errorBox = document.getElementById('craftsmanFormError');
form.onsubmit = async (e) => {
e.preventDefault();
errorBox.style.display = 'none';
const submitBtn = document.getElementById('craftsmanSubmitBtn');
submitBtn.disabled = true;
submitBtn.textContent = 'Speichert…';
try {
const name = document.getElementById('craftsmanName').value.trim();
const trade = document.getElementById('craftsmanTrade').value;
const phone = document.getElementById('craftsmanPhone').value.trim();
const email = document.getElementById('craftsmanEmail').value.trim();
await apiFetch('/craftsmen', { method: 'POST', body: JSON.stringify({ name, trade, phone, email }) });
form.reset();
populateCraftsmanTradeSelect();
loadCraftsmen();
} catch (err) {
errorBox.textContent = err.message;
errorBox.style.display = 'block';
} finally {
submitBtn.disabled = false;
submitBtn.textContent = 'Hinzufügen';
}
};
}
async function loadCraftsmen() {
if (!currentUser) return;
try {
const data = await apiFetch('/craftsmen');
lastCraftsmen = data.craftsmen;
renderCraftsmen(data.craftsmen);
} catch (err) {
console.error('[craftsmen] Laden fehlgeschlagen:', err.message);
}
}
function renderCraftsmen(craftsmen) {
const list = document.getElementById('craftsmenList');
if (!list) return;
list.innerHTML = '';
const isLandlord = currentUser.role === 'LANDLORD' || currentUser.role === 'ADMIN';
if (!craftsmen.length) {
list.appendChild(el('div', 'empty-state', 'Noch keine Handwerker-Kontakte hinterlegt.'));
return;
}
craftsmen.forEach(c => {
const row = el('div', 'ticket-row');
const left = el('div');
left.appendChild(el('div', 'title', `${c.name} · ${CRAFTSMAN_TRADE_LABEL[c.trade] || c.trade}`));
left.appendChild(el('div', 'meta-text', [c.phone, c.email].filter(Boolean).join(' · ') || 'Keine Kontaktdaten hinterlegt'));
row.appendChild(left);
if (isLandlord) {
const delBtn = document.createElement('button');
delBtn.className = 'btn-secondary';
delBtn.textContent = 'Entfernen';
delBtn.onclick = () => deleteCraftsman(c.id);
row.appendChild(delBtn);
}
list.appendChild(row);
});
}
async function deleteCraftsman(id) {
try {
await apiFetch(`/craftsmen/${id}`, { method: 'DELETE' });
loadCraftsmen();
} catch (err) {
alert(err.message);
}
}
async function assignCraftsmanToTicket(ticketId, craftsmanId) {
try {
await apiFetch(`/tickets/${ticketId}`, { method: 'PATCH', body: JSON.stringify({ craftsmanId: craftsmanId || null }) });
loadData();
} catch (err) {
alert(err.message);
}
}
bootstrap();
</script>