Abo-Angebote: für Hosting- oder Wartungs-Abo
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7ffe95f24a
commit
e5cc2f3c82
6 files changed
+303
-45
No files matched your search
+19
-7
@@ -788,6 +788,9 @@ const OFFER_STATUS = {
|
||||
|
||||
const offerState = { filter: 'open', customers: null, labelToId: new Map(), context: null, offers: new Map() }
|
||||
|
||||
// Katalogname wie im Formular ("Hosting-Abo"/"Wartungs-Abo")
|
||||
const OFFER_PRODUCT_LABEL = { hosting: 'Hosting-Abo', wartung: 'Wartungs-Abo' }
|
||||
|
||||
function offerStatusPill(o) {
|
||||
const [cls, label] = OFFER_STATUS[o.status] || ['pill-muted', o.status]
|
||||
const sub = o.status === 'aktiv' && o.appliedAt ? `seit ${fmtDate(o.appliedAt)}` : o.status === 'offen' ? 'wartet auf Abschluss' : ''
|
||||
@@ -873,7 +876,7 @@ async function loadOffers() {
|
||||
<tr>
|
||||
<td>${fmtDateTime(o.createdAt)}<br><span class="muted">${escapeHtml(o.ref)}${o.createdBy ? ` · ${escapeHtml(o.createdBy.replace(/\s*<.*>$/, ''))}` : ''}</span></td>
|
||||
<td><strong>${escapeHtml(o.customerName)}</strong>${o.customerEmail ? `<br><span class="muted">${escapeHtml(o.customerEmail).replace('@', '@<wbr>')}</span>` : ''}${o.customerCode ? `<br><span class="muted">Kd.-Nr. ${escapeHtml(o.customerCode)}</span>` : ''}</td>
|
||||
<td>${escapeHtml(o.text)}<br><span class="muted">Ersparnis ${euroFromCents(o.savingsCents)}${o.note ? ` · „${escapeHtml(o.note)}“` : ''}</span>
|
||||
<td><span class="muted">${OFFER_PRODUCT_LABEL[o.productKey] || o.productKey}</span><br>${escapeHtml(o.text)}<br><span class="muted">Ersparnis ${euroFromCents(o.savingsCents)}${o.note ? ` · „${escapeHtml(o.note)}“` : ''}</span>
|
||||
${discount ? `<br><span class="muted">${escapeHtml(discount)}</span>` : ''}
|
||||
${o.internalNote ? `<br><span class="muted"><em>intern: ${escapeHtml(o.internalNote)}</em></span>` : ''}</td>
|
||||
<td style="white-space:nowrap;overflow-wrap:normal">${o.scope === 'laufend' ? '<span class="muted">laufendes Abo</span>' : fmtDate(o.validUntil)}</td>
|
||||
@@ -937,6 +940,7 @@ async function ensureOfferCustomers() {
|
||||
function offerFormValues(form) {
|
||||
return {
|
||||
customerId: offerState.labelToId.get(form.querySelector('#offer-customer').value.trim()) || '',
|
||||
productKey: form.querySelector('#offer-product').value,
|
||||
monthlyPriceCents: Math.round(Number(form.querySelector('#offer-price').value) * 100),
|
||||
months: Number(form.querySelector('#offer-months').value),
|
||||
validUntil: form.querySelector('#offer-valid').value,
|
||||
@@ -956,12 +960,14 @@ function renderOfferPreview(form) {
|
||||
box.textContent = v.customerId ? 'Lade Kunde…' : 'Bitte einen Kunden aus der Liste wählen.'
|
||||
return
|
||||
}
|
||||
const regular = ctx.regularPriceCents
|
||||
const running = Boolean(ctx.subscription?.active)
|
||||
const regular = ctx.products[v.productKey].regularPriceCents
|
||||
// Sofort anwendbar ist ein Angebot nur, wenn das laufende Abo dasselbe Produkt betrifft (F32)
|
||||
const running = Boolean(ctx.subscription?.active && ctx.subscription.productKey === v.productKey)
|
||||
const openOffer = ctx.openOffers[v.productKey]
|
||||
const lines = []
|
||||
let ok = true
|
||||
if (ctx.openOffer) {
|
||||
lines.push(`Es gibt schon ein offenes Angebot (${ctx.openOffer.ref}: ${ctx.openOffer.text}) – bitte zuerst zurückziehen.`)
|
||||
if (openOffer) {
|
||||
lines.push(`Es gibt schon ein offenes ${OFFER_PRODUCT_LABEL[v.productKey]}-Angebot (${openOffer.ref}: ${openOffer.text}) – bitte zuerst zurückziehen.`)
|
||||
ok = false
|
||||
}
|
||||
if (!Number.isInteger(v.monthlyPriceCents) || v.monthlyPriceCents < 0 || v.monthlyPriceCents >= regular) {
|
||||
@@ -1016,8 +1022,8 @@ async function saveOffer(form) {
|
||||
errEl.classList.add('hidden')
|
||||
const v = offerFormValues(form)
|
||||
const ctx = offerState.context
|
||||
const running = Boolean(ctx.subscription?.active)
|
||||
const text = offerPreviewText(v.monthlyPriceCents, v.months, ctx.regularPriceCents, running)
|
||||
const running = Boolean(ctx.subscription?.active && ctx.subscription.productKey === v.productKey)
|
||||
const text = offerPreviewText(v.monthlyPriceCents, v.months, ctx.products[v.productKey].regularPriceCents, running)
|
||||
if (!window.confirm(`Angebot für ${ctx.customer.name || 'den Kunden'} anlegen?\n\n${text}${running ? '' : `\nGültig bis ${fmtYmd(v.validUntil)}`}\n\nDer Coupon wird sofort in Stripe angelegt${running ? ' und auf das laufende Abo angewendet' : ''}.`)) return
|
||||
const save = form.querySelector('#offer-save')
|
||||
save.disabled = true
|
||||
@@ -1065,6 +1071,11 @@ async function openOfferForm(customerId = '') {
|
||||
<input class="credit-input" id="offer-customer" list="offer-customer-list" autocomplete="off"
|
||||
placeholder="Name, E-Mail oder Kundennummer tippen …" value="${preselected ? escapeHtml(offerCustomerLabel(preselected)) : ''}">
|
||||
<datalist id="offer-customer-list">${customers.map((c) => `<option value="${escapeHtml(offerCustomerLabel(c))}"></option>`).join('')}</datalist></div>
|
||||
<div><label for="offer-product">Produkt</label>
|
||||
<select class="credit-input" id="offer-product">
|
||||
<option value="wartung" selected>Wartungs-Abo</option>
|
||||
<option value="hosting">Hosting-Abo</option>
|
||||
</select></div>
|
||||
<div><label for="offer-price">Preis pro Monat im Angebot (€)</label>
|
||||
<input class="credit-input" id="offer-price" type="number" min="0" step="0.01" value="25.00"></div>
|
||||
<div><label for="offer-months">für</label>
|
||||
@@ -1090,6 +1101,7 @@ async function openOfferForm(customerId = '') {
|
||||
if (offerState.labelToId.has(form.querySelector('#offer-customer').value.trim())) loadOfferContext(form)
|
||||
})
|
||||
form.querySelectorAll('#offer-price, #offer-months, #offer-valid').forEach((el) => el.addEventListener('input', () => renderOfferPreview(form)))
|
||||
form.querySelector('#offer-product').addEventListener('change', () => renderOfferPreview(form))
|
||||
form.querySelector('#offer-save').addEventListener('click', () => saveOffer(form))
|
||||
form.querySelector('#offer-cancel').addEventListener('click', () => form.classList.add('hidden'))
|
||||
if (preselected) loadOfferContext(form)
|
||||
|
||||
Reference in new issue
Block a user