From 15bf520bc1da6614257292fadb2c0555ae29841a Mon Sep 17 00:00:00 2001 From: Kenearos Date: Tue, 12 May 2026 18:23:11 +0200 Subject: [PATCH] feat: result card shows winner banner, all-variants details, vacation toggle --- app.js | 146 ++++++++++++++++++++++++++++++++++++++--------------- styles.css | 122 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+), 41 deletions(-) diff --git a/app.js b/app.js index 6884375..b50f8dd 100644 --- a/app.js +++ b/app.js @@ -371,70 +371,134 @@ class DienstplanApp { } /** - * Create a result card for an employee + * Create a result card for an employee (new variants shape). */ createResultCard(employeeName, result) { const card = document.createElement('div'); card.className = 'result-card'; - let content = `

${employeeName}

`; + const ctx = this._currentCalcContext || {}; + const yearMonth = ctx.yearMonth || ''; + const vacChecked = result.isVacation ? 'checked' : ''; + const safeName = String(employeeName).replace(/"/g, '"'); + const safeYm = String(yearMonth).replace(/"/g, '"'); - if (!result.thresholdReached) { + // Header + vacation toggle + let content = ` +
+

${employeeName}

+ +
+ `; + + if (result.isVacation) { + content += `
Urlaubsmodus aktiv - Schwellen halbiert
`; + } + + // Winner banner + if (!result.winner.eligible || result.totalBonus === 0) { content += `
-

Schwellenwert nicht erreicht

-

Es wurden nur ${result.qualifyingDays.toFixed(1)} qualifizierende Tage gearbeitet. - Mindestens ${this.calculator.MIN_QUALIFYING_DAYS} Tage erforderlich.

+

Keine Variante triggert

+

Mit den eingetragenen Diensten erreicht keine der drei Varianten einen positiven Bonus.

Keine Bonuszahlung

`; } else { content += ` -
-
-
Normale Tage
-
${result.normalDays.toFixed(1)}
-
-
-
WE/Feiertag Tage
-
${result.qualifyingDays.toFixed(1)}
-
-
-
Abzug
-
-${result.qualifyingDaysDeducted.toFixed(1)}
-
-
-
Normale Tage (bezahlt)
-
${result.normalDaysPaid.toFixed(1)}
-
-
-
WE/Feiertag (bezahlt)
-
${result.qualifyingDaysPaid.toFixed(1)}
-
-
- -
-
-
Normale Tage (250€)
-
${this.calculator.formatCurrency(result.bonusNormalDays)}
-
-
-
WE/Feiertag (450€)
-
${this.calculator.formatCurrency(result.bonusQualifyingDays)}
-
-
-
-

Gesamtbonus

+

Variante ${result.winner.variantId} ★ Sieger

${this.calculator.formatCurrency(result.totalBonus)}
`; } + // Classified summary line + const c = result.classified; + content += ` +
+ Fr: ${c.fr.toFixed(1)} + Sa: ${c.sa.toFixed(1)} + So: ${c.so.toFixed(1)} + Werktage: ${c.weekday.toFixed(1)} +
+ `; + + // Collapsible variant breakdown + content += `
Alle Varianten anzeigen`; + for (const v of result.allResults) { + content += this.renderVariantBlock(v, result.winner.variantId); + } + content += `
`; + card.innerHTML = content; + + // Attach vacation-toggle handler + const cb = card.querySelector('input[data-vacation-employee]'); + if (cb) { + cb.addEventListener('change', (e) => this.onVacationToggle(e)); + } return card; } + /** + * Render a single variant sub-panel. + */ + renderVariantBlock(v, winnerId) { + const isWinner = v.variantId === winnerId; + const star = isWinner ? '' : ''; + const labels = { + 1: 'V1: 1 (Fr/So) + 3 Werktage', + 2: 'V2: 1 Sa + 2 Werktage', + 3: 'V3 (loose): 2 qualifizierende Tage (Pool Fr+Sa+So)' + }; + let thresholdStr = '-'; + if (v.threshold) { + if (v.variantId === 1) thresholdStr = `Fr+So ≥ ${v.threshold.frSo}, Werktage ≥ ${v.threshold.weekday}`; + if (v.variantId === 2) thresholdStr = `Sa ≥ ${v.threshold.sa}, Werktage ≥ ${v.threshold.weekday}`; + if (v.variantId === 3) thresholdStr = `Pool ≥ ${v.threshold.pool}`; + } + const elig = v.eligible ? 'erfüllt' + : 'nicht erfüllt'; + return ` +
+
${star}${labels[v.variantId]}
+
Schwelle:${thresholdStr}
+
Eligibility:${elig}
+
Abzug: + Fr ${v.deduction.fr.toFixed(2)} - Sa ${v.deduction.sa.toFixed(2)} - So ${v.deduction.so.toFixed(2)} - WT ${v.deduction.weekday.toFixed(2)} +
+
Bezahlt: + Fr ${v.paidShares.fr.toFixed(2)} - Sa ${v.paidShares.sa.toFixed(2)} - So ${v.paidShares.so.toFixed(2)} - WT ${v.paidShares.weekday.toFixed(2)} +
+
Bonus:${this.calculator.formatCurrency(v.bonus)}
+
+ `; + } + + /** + * Handle vacation checkbox toggle. + */ + onVacationToggle(e) { + const cb = e.target; + const name = cb.getAttribute('data-vacation-employee'); + const ym = cb.getAttribute('data-vacation-yearmonth'); + try { + this.storage.setVacationMode(name, ym, cb.checked); + // Re-run calc to reflect the new state + this.calculateBonuses(); + } catch (err) { + this.showToast('Urlaubsmodus konnte nicht gespeichert werden', 'error'); + cb.checked = !cb.checked; // revert visual state + } + } + // --- NEW: EMAIL REPORT GENERATOR --- generateEmailReport() { // Need to grab current selected calc month/year diff --git a/styles.css b/styles.css index 86c29b8..706eda2 100644 --- a/styles.css +++ b/styles.css @@ -536,3 +536,125 @@ header h1 { padding: 20px 0; } } + +/* === Variants UI === */ +.result-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 10px; + margin-bottom: 10px; +} + +.vacation-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 12px; + background: #fff; + border: 2px solid #e0e0e0; + border-radius: 6px; + font-size: 0.9rem; + cursor: pointer; + user-select: none; +} + +.vacation-toggle input[type="checkbox"] { + margin: 0; + cursor: pointer; +} + +.vacation-active-banner { + background: #fff3cd; + border-left: 4px solid #ffc107; + padding: 8px 12px; + border-radius: 4px; + margin-bottom: 12px; + color: #856404; + font-size: 0.9rem; +} + +.classified-summary { + display: flex; + gap: 20px; + flex-wrap: wrap; + padding: 10px 15px; + background: #f8f9fa; + border-radius: 6px; + margin: 12px 0; + font-size: 0.9rem; +} + +.variant-details { + margin-top: 15px; + background: #f8f9fa; + border-radius: 6px; + padding: 10px 15px; +} + +.variant-details summary { + cursor: pointer; + font-weight: 500; + color: #667eea; + padding: 4px 0; +} + +.variant-card { + background: white; + border: 1px solid #e0e0e0; + border-radius: 6px; + padding: 12px 15px; + margin: 10px 0; +} + +.variant-card.winner { + border-color: #28a745; + box-shadow: 0 0 0 2px rgba(40, 167, 69, 0.15); +} + +.variant-header { + margin-bottom: 8px; + font-size: 0.95rem; +} + +.variant-badge { + display: inline-block; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + margin-right: 6px; + background: #28a745; + color: white; + font-weight: 600; +} + +.variant-row { + display: flex; + justify-content: space-between; + gap: 10px; + padding: 4px 0; + font-size: 0.85rem; + color: #555; + border-top: 1px solid #f0f0f0; +} + +.variant-row:first-of-type { + border-top: none; +} + +.variant-bonus { + font-weight: 600; + color: #333; + font-size: 0.95rem; +} + +.variant-eligible { + color: #28a745; + font-weight: 600; +} + +.variant-not-eligible { + color: #dc3545; + font-weight: 600; +}