Dateien nach "web-dashboard" hochladen
This commit is contained in:
parent
ca6d851a70
commit
c8a5984f8b
@ -707,6 +707,66 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="ticket-list" id="documentList"></div>
|
<div class="ticket-list" id="documentList"></div>
|
||||||
</section>
|
</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>
|
</main>
|
||||||
</div>
|
</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 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 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 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) {
|
function formatEuro(n) {
|
||||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n || 0);
|
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('storageFormCard').style.display = isLandlord ? 'block' : 'none';
|
||||||
document.getElementById('trashFormCard').style.display = isLandlord ? 'block' : 'none';
|
document.getElementById('trashFormCard').style.display = isLandlord ? 'block' : 'none';
|
||||||
document.getElementById('documentFormCard').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();
|
initTicketForm();
|
||||||
initAbsenceForm();
|
initAbsenceForm();
|
||||||
@ -919,6 +983,8 @@
|
|||||||
initGuestCodeForm();
|
initGuestCodeForm();
|
||||||
if (isLandlord) initStorageForms();
|
if (isLandlord) initStorageForms();
|
||||||
if (isLandlord) initDocumentForm();
|
if (isLandlord) initDocumentForm();
|
||||||
|
if (isLandlord) initUtilityStatementForm();
|
||||||
|
if (isLandlord) initCraftsmanForm();
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
loadCleaningTasks();
|
loadCleaningTasks();
|
||||||
@ -929,6 +995,9 @@
|
|||||||
loadGuestCodes();
|
loadGuestCodes();
|
||||||
loadStorage();
|
loadStorage();
|
||||||
loadDocuments();
|
loadDocuments();
|
||||||
|
loadNoticeDeadlines();
|
||||||
|
loadUtilityStatements();
|
||||||
|
loadCraftsmen();
|
||||||
|
|
||||||
setInterval(loadData, REFRESH_INTERVAL_MS);
|
setInterval(loadData, REFRESH_INTERVAL_MS);
|
||||||
setInterval(loadCleaningTasks, REFRESH_INTERVAL_MS);
|
setInterval(loadCleaningTasks, REFRESH_INTERVAL_MS);
|
||||||
@ -939,6 +1008,9 @@
|
|||||||
setInterval(loadGuestCodes, REFRESH_INTERVAL_MS);
|
setInterval(loadGuestCodes, REFRESH_INTERVAL_MS);
|
||||||
setInterval(loadStorage, REFRESH_INTERVAL_MS);
|
setInterval(loadStorage, REFRESH_INTERVAL_MS);
|
||||||
setInterval(loadDocuments, 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.room ? t.room.roomNumber : 'Gemeinschaftsbereich'} · ${CATEGORY_LABEL[t.category] || t.category} · ` +
|
||||||
`${t.creator ? t.creator.fullName + ' · ' : ''}${new Date(t.createdAt).toLocaleDateString('de-DE')} · ` +
|
`${t.creator ? t.creator.fullName + ' · ' : ''}${new Date(t.createdAt).toLocaleDateString('de-DE')} · ` +
|
||||||
TICKET_STATUS_LABEL[t.status]));
|
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);
|
row.appendChild(left);
|
||||||
|
|
||||||
const right = el('div');
|
const right = el('div');
|
||||||
@ -1151,6 +1226,22 @@
|
|||||||
right.appendChild(el('span', `priority-chip ${t.priority}`, PRIORITY_LABEL[t.priority] || t.priority));
|
right.appendChild(el('span', `priority-chip ${t.priority}`, PRIORITY_LABEL[t.priority] || t.priority));
|
||||||
|
|
||||||
if (isLandlord) {
|
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') {
|
if (t.status === 'OPEN') {
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'btn-secondary';
|
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();
|
bootstrap();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user