Admin: News je Projekt aktivieren und abschalten

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
knsoandClaude Opus 5.5 committed 2026-09-30 16:56:41 +00:00
1 parent a421308019
commit 6dac6aa073
6 files changed
+361 -9

No files matched your search

+82
View File
@@ -185,6 +185,81 @@ function renderInvoices(data) {
</tbody></table>`
}
const FEATURE_STATES = {
off: ['Aus', 'pill-muted'],
ordered: ['Bestellt', 'pill-amber'],
setup: ['Einrichtung', 'pill-warn'],
active: ['Aktiv', 'pill-ok'],
disabled: ['Abgeschaltet', 'pill-red'],
}
function featureRowHtml(p) {
const n = p.features.news
const [label, cls] = FEATURE_STATES[n.state] || [n.state, 'pill-muted']
const ticket = n.ticketMissing
? '<span class="pill pill-red">Ticket fehlt</span>'
: n.ticketWoid ? `#${escapeHtml(n.ticketWoid)}` : '<span class="muted">–</span>'
return `
<tr data-feature-project="${escapeHtml(p.projectId)}">
<td>${escapeHtml(p.projectName || '–')}</td>
<td><span class="pill ${cls}">${escapeHtml(label)}</span>${n.adminNote ? `<div class="muted" title="${escapeHtml(n.adminNote)}">${escapeHtml(n.adminNote.slice(0, 60))}${n.adminNote.length > 60 ? '…' : ''}</div>` : ''}</td>
<td>${ticket}</td>
<td class="feature-actions">
${n.state === 'active' ? '' : '<button type="button" class="mini-btn primary" data-feature-ask="activate">Aktivieren (ohne Zahlung)</button>'}
${n.state === 'off' ? '' : n.state === 'disabled' ? '' : '<button type="button" class="mini-btn" data-feature-ask="disable">Abschalten</button>'}
</td>
</tr>`
}
function renderFeatures(projects) {
if (!projects.length) return '<p class="muted">Keine Projekte für diesen Kunden.</p>'
return `<table class="admin-table"><thead><tr><th>Projekt</th><th>News</th><th>Ticket</th><th>Aktionen</th></tr></thead><tbody>
${projects.map(featureRowHtml).join('')}
</tbody></table><p class="feature-message muted" role="status"></p>`
}
function bindFeatureRows(box, customerId) {
const message = box.querySelector('.feature-message')
box.querySelectorAll('[data-feature-project]').forEach((row) => {
const cell = row.querySelector('.feature-actions')
const original = cell.innerHTML
const bindAsk = () => cell.querySelectorAll('[data-feature-ask]').forEach((btn) =>
btn.addEventListener('click', () => askFeature(btn.dataset.featureAsk)))
const askFeature = (action) => {
const text = action === 'activate'
? 'News ohne Zahlung aktivieren?'
: 'News abschalten? Die Website zeigt dann keine Beiträge mehr.'
cell.innerHTML = `<span>${text}</span>
<input class="code-input" data-feature-note placeholder="Notiz (optional)" maxlength="120">
<button type="button" class="mini-btn primary" data-feature-yes>Ja</button>
<button type="button" class="mini-btn" data-feature-no>Abbrechen</button>`
cell.querySelector('[data-feature-no]').addEventListener('click', () => {
cell.innerHTML = original
bindAsk()
})
cell.querySelector('[data-feature-yes]').addEventListener('click', async () => {
const note = cell.querySelector('[data-feature-note]').value.trim()
cell.querySelectorAll('button').forEach((b) => { b.disabled = true })
try {
const { projects } = await api(`/api/portal-admin/funktionen/${encodeURIComponent(customerId)}/news`, {
method: 'POST',
body: JSON.stringify({ projectId: row.dataset.featureProject, action, note }),
})
row.outerHTML = featureRowHtml(projects[0])
bindFeatureRows(box, customerId)
box.querySelector('.feature-message').textContent = 'Gespeichert.'
} catch (err) {
cell.innerHTML = original
bindAsk()
message.textContent = err.message
message.className = 'feature-message error'
}
})
}
bindAsk()
})
}
async function toggleCustomerDetail(customerId, kind, cell) {
const existing = document.getElementById(`detail-${customerId}`)
if (existing) {
@@ -206,6 +281,10 @@ async function toggleCustomerDetail(customerId, kind, cell) {
if (kind === 'activity') {
const { tickets } = await api(`/api/portal-admin/customers/${encodeURIComponent(customerId)}/activity`)
tr.innerHTML = `<td colspan="7"><h4>Tickets &amp; Arbeiten</h4>${renderActivity(tickets)}</td>`
} else if (kind === 'features') {
const { projects } = await api(`/api/portal-admin/funktionen/${encodeURIComponent(customerId)}`)
tr.innerHTML = `<td colspan="7"><h4>Funktionen</h4>${renderFeatures(projects)}</td>`
bindFeatureRows(tr, customerId)
} else {
const data = await api(`/api/portal-admin/customers/${encodeURIComponent(customerId)}/invoices`)
tr.innerHTML = `<td colspan="7"><h4>Rechnungen</h4>${renderInvoices(data)}</td>`
@@ -246,6 +325,7 @@ async function loadCustomers(search = '') {
title="Bedarf im Gespräch aufnehmen und Preis kalkulieren">Rechnungsanalyse</a>` : ''}
<button class="mini-btn${isLead(c) ? '' : ' primary'}" data-view-as="${escapeHtml(c.id)}">Als Kunde ansehen</button>
<button class="mini-btn" data-activity="${escapeHtml(c.id)}">Arbeiten</button>
<button class="mini-btn" data-features="${escapeHtml(c.id)}">Funktionen</button>
<button class="mini-btn" data-invoices="${escapeHtml(c.id)}"${c.invoiceNinjaLinked ? '' : ' title="Nicht mit InvoiceNinja verknüpft"'}>Rechnungen</button>
<button class="mini-btn" data-offer-for="${escapeHtml(c.id)}" title="Individuelles Abo-Angebot anlegen">Angebot</button>
</div>
@@ -258,6 +338,8 @@ async function loadCustomers(search = '') {
btn.addEventListener('click', () => viewAsCustomer(btn.dataset.viewAs)))
tbody.querySelectorAll('[data-activity]').forEach((btn) =>
btn.addEventListener('click', () => toggleCustomerDetail(btn.dataset.activity, 'activity')))
tbody.querySelectorAll('[data-features]').forEach((btn) =>
btn.addEventListener('click', () => toggleCustomerDetail(btn.dataset.features, 'features')))
tbody.querySelectorAll('[data-invoices]').forEach((btn) =>
btn.addEventListener('click', () => toggleCustomerDetail(btn.dataset.invoices, 'invoices')))
tbody.querySelectorAll('[data-offer-for]').forEach((btn) =>