Dateien nach "web-dashboard" hochladen
This commit is contained in:
parent
ca6d851a70
commit
c8a5984f8b
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user