This repository has been archived on 2026-06-28. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.
Dienstplan/Dienstplan_Portable.html
copilot-swe-agent[bot] 4cebb7bf1a Fix all German spelling errors (missing umlauts)
Co-authored-by: Kenearos <86194771+Kenearos@users.noreply.github.com>
2025-12-10 21:40:49 +00:00

1345 lines
46 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dienstplan NRW - Bonus-Berechnung</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
overflow: hidden;
}
header {
background: linear-gradient(135deg, #4a5568 0%, #2d3748 100%);
color: white;
padding: 25px 30px;
text-align: center;
}
header h1 {
font-size: 1.8em;
margin-bottom: 5px;
}
header p {
opacity: 0.8;
font-size: 0.95em;
}
.steps-indicator {
display: flex;
justify-content: center;
padding: 25px 20px;
background: #f7fafc;
border-bottom: 1px solid #e2e8f0;
}
.step-item {
display: flex;
align-items: center;
margin: 0 10px;
cursor: pointer;
transition: all 0.3s;
}
.step-number {
width: 40px;
height: 40px;
border-radius: 50%;
background: #cbd5e0;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
transition: all 0.3s;
}
.step-item.active .step-number {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
transform: scale(1.1);
}
.step-item.completed .step-number {
background: #48bb78;
}
.step-label {
font-size: 0.9em;
color: #718096;
font-weight: 500;
}
.step-item.active .step-label {
color: #2d3748;
font-weight: 600;
}
.step-connector {
width: 40px;
height: 2px;
background: #cbd5e0;
margin: 0 5px;
}
.content {
padding: 30px;
min-height: 400px;
}
.step-content {
display: none;
}
.step-content.active {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #2d3748;
}
input, select {
width: 100%;
padding: 12px 15px;
border: 2px solid #e2e8f0;
border-radius: 10px;
font-size: 1em;
transition: all 0.3s;
}
input:focus, select:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 10px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 5px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
}
.btn-secondary {
background: #e2e8f0;
color: #4a5568;
}
.btn-secondary:hover {
background: #cbd5e0;
}
.btn-success {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
color: white;
}
.btn-danger {
background: #fc8181;
color: white;
padding: 8px 15px;
font-size: 0.85em;
}
.btn-danger:hover {
background: #f56565;
}
.employee-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 15px;
}
.employee-tag {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 10px 15px;
border-radius: 25px;
display: flex;
align-items: center;
gap: 10px;
font-weight: 500;
}
.employee-tag .remove {
background: rgba(255,255,255,0.3);
border: none;
color: white;
width: 24px;
height: 24px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2em;
line-height: 1;
}
.employee-tag .remove:hover {
background: rgba(255,255,255,0.5);
}
.month-selector {
display: flex;
gap: 15px;
margin-bottom: 25px;
}
.month-selector select {
flex: 1;
}
.duty-entry {
background: #f7fafc;
border-radius: 15px;
padding: 20px;
margin-bottom: 20px;
}
.duty-entry h3 {
color: #2d3748;
margin-bottom: 15px;
font-size: 1.1em;
}
.duty-row {
display: grid;
grid-template-columns: 1fr 1fr 120px 50px;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.duty-list {
margin-top: 25px;
}
.duty-item {
display: grid;
grid-template-columns: 1fr 1fr 100px 80px;
gap: 10px;
padding: 12px 15px;
background: white;
border-radius: 10px;
margin-bottom: 8px;
border-left: 4px solid #cbd5e0;
align-items: center;
}
.duty-item.qualifying {
border-left-color: #48bb78;
background: linear-gradient(90deg, rgba(72, 187, 120, 0.1) 0%, white 50%);
}
.duty-item .day-type {
font-size: 0.85em;
color: #718096;
}
.duty-item.qualifying .day-type {
color: #2f855a;
font-weight: 600;
}
.results-container {
margin-top: 20px;
}
.result-card {
background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-radius: 15px;
padding: 25px;
margin-bottom: 20px;
border: 1px solid #e2e8f0;
}
.result-card h3 {
color: #2d3748;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid #e2e8f0;
}
.result-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.result-item {
background: white;
padding: 15px;
border-radius: 10px;
text-align: center;
}
.result-item .label {
font-size: 0.85em;
color: #718096;
margin-bottom: 5px;
}
.result-item .value {
font-size: 1.4em;
font-weight: 700;
color: #2d3748;
}
.result-item.highlight {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.result-item.highlight .label,
.result-item.highlight .value {
color: white;
}
.result-item.success {
background: linear-gradient(135deg, #48bb78 0%, #38a169 100%);
}
.result-item.success .label,
.result-item.success .value {
color: white;
}
.result-item.warning {
background: linear-gradient(135deg, #ed8936 0%, #dd6b20 100%);
}
.result-item.warning .label,
.result-item.warning .value {
color: white;
}
.threshold-badge {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
margin-left: 10px;
}
.threshold-badge.reached {
background: #c6f6d5;
color: #2f855a;
}
.threshold-badge.not-reached {
background: #fed7d7;
color: #c53030;
}
.total-summary {
background: linear-gradient(135deg, #2d3748 0%, #1a202c 100%);
color: white;
padding: 25px;
border-radius: 15px;
margin-top: 25px;
text-align: center;
}
.total-summary h2 {
font-size: 1.2em;
opacity: 0.9;
margin-bottom: 10px;
}
.total-summary .amount {
font-size: 3em;
font-weight: 700;
}
.info-box {
background: #ebf8ff;
border-left: 4px solid #4299e1;
padding: 15px 20px;
border-radius: 0 10px 10px 0;
margin: 20px 0;
color: #2b6cb0;
}
.info-box h4 {
margin-bottom: 5px;
}
.navigation {
display: flex;
justify-content: space-between;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e2e8f0;
}
.empty-state {
text-align: center;
padding: 40px;
color: #718096;
}
.empty-state svg {
width: 80px;
height: 80px;
margin-bottom: 15px;
opacity: 0.5;
}
.calendar-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 5px;
margin-top: 15px;
}
.calendar-header {
text-align: center;
font-weight: 600;
color: #4a5568;
padding: 8px;
font-size: 0.85em;
}
.calendar-day {
text-align: center;
padding: 10px 5px;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
font-size: 0.9em;
border: 2px solid transparent;
}
.calendar-day:hover {
background: #edf2f7;
}
.calendar-day.empty {
cursor: default;
}
.calendar-day.empty:hover {
background: transparent;
}
.calendar-day.qualifying {
background: #c6f6d5;
color: #2f855a;
font-weight: 600;
}
.calendar-day.selected {
border-color: #667eea;
background: #ebf4ff;
}
.calendar-day.has-duty {
position: relative;
}
.calendar-day.has-duty::after {
content: '';
position: absolute;
bottom: 3px;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 6px;
border-radius: 50%;
background: #667eea;
}
.legend {
display: flex;
gap: 20px;
margin-top: 15px;
font-size: 0.85em;
color: #718096;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 4px;
}
.legend-color.qualifying {
background: #c6f6d5;
}
.legend-color.selected {
border: 2px solid #667eea;
background: #ebf4ff;
}
.print-btn {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
cursor: pointer;
box-shadow: 0 5px 20px rgba(0,0,0,0.3);
display: none;
align-items: center;
justify-content: center;
font-size: 1.5em;
}
.print-btn:hover {
transform: scale(1.1);
}
@media print {
body {
background: white;
padding: 0;
}
.container {
box-shadow: none;
}
.steps-indicator, .navigation, .print-btn {
display: none !important;
}
.step-content {
display: block !important;
}
.step-content:not(.results) {
display: none !important;
}
}
@media (max-width: 600px) {
.duty-row, .duty-item {
grid-template-columns: 1fr 1fr;
}
.month-selector {
flex-direction: column;
}
.step-label {
display: none;
}
.result-grid {
grid-template-columns: 1fr 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>Dienstplan NRW</h1>
<p>Bonus-Berechnung nach Variante 2 (streng)</p>
</header>
<div class="steps-indicator">
<div class="step-item active" data-step="1">
<div class="step-number">1</div>
<span class="step-label">Mitarbeiter</span>
</div>
<div class="step-connector"></div>
<div class="step-item" data-step="2">
<div class="step-number">2</div>
<span class="step-label">Dienste</span>
</div>
<div class="step-connector"></div>
<div class="step-item" data-step="3">
<div class="step-number">3</div>
<span class="step-label">Ergebnis</span>
</div>
</div>
<div class="content">
<!-- Step 1: Mitarbeiter -->
<div class="step-content active" id="step1">
<h2>Schritt 1: Mitarbeiter eingeben</h2>
<p style="color: #718096; margin: 10px 0 25px;">Geben Sie alle Mitarbeiter ein, die in diesem Monat Dienste haben.</p>
<div class="month-selector">
<div class="form-group" style="margin: 0; flex: 1;">
<label for="month">Monat</label>
<select id="month"></select>
</div>
<div class="form-group" style="margin: 0; flex: 1;">
<label for="year">Jahr</label>
<select id="year"></select>
</div>
</div>
<div class="form-group">
<label for="employeeName">Mitarbeiter hinzufügen</label>
<div style="display: flex; gap: 10px;">
<input type="text" id="employeeName" placeholder="Name eingeben..." style="flex: 1;">
<button class="btn btn-primary" onclick="addEmployee()">Hinzufügen</button>
</div>
</div>
<div class="employee-list" id="employeeList"></div>
<div class="info-box" style="margin-top: 25px;">
<h4>Hinweis</h4>
<p>Sie können Mitarbeiter jederzeit hinzufügen oder entfernen. Die Daten werden lokal im Browser gespeichert.</p>
</div>
<div class="navigation">
<div></div>
<button class="btn btn-primary" onclick="goToStep(2)">Weiter zu Dienste</button>
</div>
</div>
<!-- Step 2: Dienste -->
<div class="step-content" id="step2">
<h2>Schritt 2: Dienste eintragen</h2>
<p style="color: #718096; margin: 10px 0 25px;">Tragen Sie die Dienste für <span id="monthYearDisplay"></span> ein.</p>
<div class="duty-entry">
<h3>Neuen Dienst hinzufügen</h3>
<div class="duty-row">
<div class="form-group" style="margin: 0;">
<label for="dutyEmployee">Mitarbeiter</label>
<select id="dutyEmployee"></select>
</div>
<div class="form-group" style="margin: 0;">
<label for="dutyDate">Datum</label>
<input type="date" id="dutyDate">
</div>
<div class="form-group" style="margin: 0;">
<label for="dutyShare">Anteil</label>
<select id="dutyShare">
<option value="1.0">1,0</option>
<option value="0.5">0,5</option>
</select>
</div>
<div style="padding-top: 25px;">
<button class="btn btn-success" onclick="addDuty()">+</button>
</div>
</div>
</div>
<h3>Kalenderübersicht</h3>
<div id="calendarContainer"></div>
<div class="legend">
<div class="legend-item">
<div class="legend-color qualifying"></div>
<span>WE-Tag (Bonus-Tag)</span>
</div>
<div class="legend-item">
<div class="legend-color selected"></div>
<span>Dienst eingetragen</span>
</div>
</div>
<div class="duty-list">
<h3 style="margin-bottom: 15px;">Eingetragene Dienste (<span id="dutyCount">0</span>)</h3>
<div id="dutyListContainer"></div>
</div>
<div class="navigation">
<button class="btn btn-secondary" onclick="goToStep(1)">Zurück</button>
<button class="btn btn-primary" onclick="calculateAndShowResults()">Berechnen</button>
</div>
</div>
<!-- Step 3: Ergebnis -->
<div class="step-content results" id="step3">
<h2>Schritt 3: Ergebnis</h2>
<p style="color: #718096; margin: 10px 0 25px;">Bonus-Berechnung für <span id="resultMonthYear"></span></p>
<div id="resultsContainer"></div>
<div class="navigation">
<button class="btn btn-secondary" onclick="goToStep(2)">Zurück</button>
<button class="btn btn-primary" onclick="window.print()">Drucken</button>
</div>
</div>
</div>
</div>
<button class="print-btn" onclick="window.print()" title="Drucken">
<span>&#128424;</span>
</button>
<script>
// ==================== HOLIDAYS ====================
const NRW_HOLIDAYS = {
2025: [
{ date: '2025-01-01', name: 'Neujahr' },
{ date: '2025-04-18', name: 'Karfreitag' },
{ date: '2025-04-21', name: 'Ostermontag' },
{ date: '2025-05-01', name: 'Tag der Arbeit' },
{ date: '2025-05-29', name: 'Christi Himmelfahrt' },
{ date: '2025-06-09', name: 'Pfingstmontag' },
{ date: '2025-06-19', name: 'Fronleichnam' },
{ date: '2025-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2025-11-01', name: 'Allerheiligen' },
{ date: '2025-12-25', name: '1. Weihnachtstag' },
{ date: '2025-12-26', name: '2. Weihnachtstag' }
],
2026: [
{ date: '2026-01-01', name: 'Neujahr' },
{ date: '2026-04-03', name: 'Karfreitag' },
{ date: '2026-04-06', name: 'Ostermontag' },
{ date: '2026-05-01', name: 'Tag der Arbeit' },
{ date: '2026-05-14', name: 'Christi Himmelfahrt' },
{ date: '2026-05-25', name: 'Pfingstmontag' },
{ date: '2026-06-04', name: 'Fronleichnam' },
{ date: '2026-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2026-11-01', name: 'Allerheiligen' },
{ date: '2026-12-25', name: '1. Weihnachtstag' },
{ date: '2026-12-26', name: '2. Weihnachtstag' }
],
2027: [
{ date: '2027-01-01', name: 'Neujahr' },
{ date: '2027-03-26', name: 'Karfreitag' },
{ date: '2027-03-29', name: 'Ostermontag' },
{ date: '2027-05-01', name: 'Tag der Arbeit' },
{ date: '2027-05-06', name: 'Christi Himmelfahrt' },
{ date: '2027-05-17', name: 'Pfingstmontag' },
{ date: '2027-05-27', name: 'Fronleichnam' },
{ date: '2027-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2027-11-01', name: 'Allerheiligen' },
{ date: '2027-12-25', name: '1. Weihnachtstag' },
{ date: '2027-12-26', name: '2. Weihnachtstag' }
],
2028: [
{ date: '2028-01-01', name: 'Neujahr' },
{ date: '2028-04-14', name: 'Karfreitag' },
{ date: '2028-04-17', name: 'Ostermontag' },
{ date: '2028-05-01', name: 'Tag der Arbeit' },
{ date: '2028-05-25', name: 'Christi Himmelfahrt' },
{ date: '2028-06-05', name: 'Pfingstmontag' },
{ date: '2028-06-15', name: 'Fronleichnam' },
{ date: '2028-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2028-11-01', name: 'Allerheiligen' },
{ date: '2028-12-25', name: '1. Weihnachtstag' },
{ date: '2028-12-26', name: '2. Weihnachtstag' }
],
2029: [
{ date: '2029-01-01', name: 'Neujahr' },
{ date: '2029-03-30', name: 'Karfreitag' },
{ date: '2029-04-02', name: 'Ostermontag' },
{ date: '2029-05-01', name: 'Tag der Arbeit' },
{ date: '2029-05-10', name: 'Christi Himmelfahrt' },
{ date: '2029-05-21', name: 'Pfingstmontag' },
{ date: '2029-05-31', name: 'Fronleichnam' },
{ date: '2029-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2029-11-01', name: 'Allerheiligen' },
{ date: '2029-12-25', name: '1. Weihnachtstag' },
{ date: '2029-12-26', name: '2. Weihnachtstag' }
],
2030: [
{ date: '2030-01-01', name: 'Neujahr' },
{ date: '2030-04-19', name: 'Karfreitag' },
{ date: '2030-04-22', name: 'Ostermontag' },
{ date: '2030-05-01', name: 'Tag der Arbeit' },
{ date: '2030-05-30', name: 'Christi Himmelfahrt' },
{ date: '2030-06-10', name: 'Pfingstmontag' },
{ date: '2030-06-20', name: 'Fronleichnam' },
{ date: '2030-10-03', name: 'Tag der Deutschen Einheit' },
{ date: '2030-11-01', name: 'Allerheiligen' },
{ date: '2030-12-25', name: '1. Weihnachtstag' },
{ date: '2030-12-26', name: '2. Weihnachtstag' }
]
};
// ==================== CONSTANTS ====================
const RATE_NORMAL = 250;
const RATE_WEEKEND = 450;
const MIN_QUALIFYING_DAYS = 2.0;
const DEDUCTION = 1.0;
const MONTHS = [
'Januar', 'Februar', 'März', 'April', 'Mai', 'Juni',
'Juli', 'August', 'September', 'Oktober', 'November', 'Dezember'
];
// ==================== STATE ====================
let employees = [];
let duties = [];
let currentStep = 1;
let selectedMonth = new Date().getMonth();
let selectedYear = new Date().getFullYear();
// ==================== HOLIDAY FUNCTIONS ====================
function formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function isHoliday(date) {
const year = date.getFullYear();
const dateStr = formatDate(date);
if (!NRW_HOLIDAYS[year]) return false;
return NRW_HOLIDAYS[year].some(h => h.date === dateStr);
}
function isDayBeforeHoliday(date) {
const nextDay = new Date(date);
nextDay.setDate(nextDay.getDate() + 1);
return isHoliday(nextDay);
}
function getHolidayName(date) {
const year = date.getFullYear();
const dateStr = formatDate(date);
if (!NRW_HOLIDAYS[year]) return null;
const holiday = NRW_HOLIDAYS[year].find(h => h.date === dateStr);
return holiday ? holiday.name : null;
}
function isQualifyingDay(date) {
const dayOfWeek = date.getDay();
const isWeekend = dayOfWeek === 5 || dayOfWeek === 6 || dayOfWeek === 0;
return isWeekend || isHoliday(date) || isDayBeforeHoliday(date);
}
function getDayTypeLabel(date) {
const dayOfWeek = date.getDay();
const holidayName = getHolidayName(date);
if (holidayName) return `Feiertag (${holidayName})`;
if (isDayBeforeHoliday(date)) return 'Tag vor Feiertag';
if (dayOfWeek === 5) return 'Freitag';
if (dayOfWeek === 6) return 'Samstag';
if (dayOfWeek === 0) return 'Sonntag';
const days = ['Sonntag', 'Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag'];
return days[dayOfWeek];
}
// ==================== CALCULATION ====================
function calculateBonus(employeeDuties) {
if (!employeeDuties || employeeDuties.length === 0) {
return {
normalDays: 0,
qualifyingDaysFriday: 0,
qualifyingDaysOther: 0,
qualifyingDays: 0,
thresholdReached: false,
deduction: 0,
normalDaysPaid: 0,
qualifyingDaysPaid: 0,
bonusNormalDays: 0,
bonusQualifyingDays: 0,
totalBonus: 0,
details: []
};
}
let qualifyingDaysFriday = 0;
let qualifyingDaysOther = 0;
let normalDays = 0;
const details = [];
employeeDuties.forEach(duty => {
const date = new Date(duty.date);
const qualifying = isQualifyingDay(date);
const isFriday = date.getDay() === 5;
const dayType = getDayTypeLabel(date);
if (qualifying) {
if (isFriday) {
qualifyingDaysFriday += duty.share;
} else {
qualifyingDaysOther += duty.share;
}
} else {
normalDays += duty.share;
}
details.push({
date: date,
share: duty.share,
isQualifying: qualifying,
dayType: dayType
});
});
const qualifyingDays = qualifyingDaysFriday + qualifyingDaysOther;
const thresholdReached = qualifyingDays >= MIN_QUALIFYING_DAYS - 0.0001;
let bonus = 0;
let normalDaysPaid = 0;
let qualifyingDaysPaid = 0;
let deduction = 0;
if (thresholdReached) {
deduction = DEDUCTION;
const deductionFromFriday = Math.min(deduction, qualifyingDaysFriday);
const deductionFromOther = Math.max(0, deduction - deductionFromFriday);
const qualifyingDaysFridayPaid = Math.max(0, qualifyingDaysFriday - deductionFromFriday);
const qualifyingDaysOtherPaid = Math.max(0, qualifyingDaysOther - deductionFromOther);
qualifyingDaysPaid = qualifyingDaysFridayPaid + qualifyingDaysOtherPaid;
normalDaysPaid = normalDays;
bonus = (normalDaysPaid * RATE_NORMAL) + (qualifyingDaysPaid * RATE_WEEKEND);
}
return {
normalDays: normalDays,
qualifyingDaysFriday: qualifyingDaysFriday,
qualifyingDaysOther: qualifyingDaysOther,
qualifyingDays: qualifyingDays,
thresholdReached: thresholdReached,
deduction: deduction,
normalDaysPaid: normalDaysPaid,
qualifyingDaysPaid: qualifyingDaysPaid,
bonusNormalDays: normalDaysPaid * RATE_NORMAL,
bonusQualifyingDays: qualifyingDaysPaid * RATE_WEEKEND,
totalBonus: bonus,
details: details
};
}
function formatCurrency(amount) {
return new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR'
}).format(amount);
}
function formatNumber(num) {
return num.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
}
// ==================== UI FUNCTIONS ====================
function init() {
loadFromStorage();
initSelectors();
renderEmployees();
updateStep(1);
}
function initSelectors() {
const monthSelect = document.getElementById('month');
const yearSelect = document.getElementById('year');
MONTHS.forEach((m, i) => {
const opt = document.createElement('option');
opt.value = i;
opt.textContent = m;
if (i === selectedMonth) opt.selected = true;
monthSelect.appendChild(opt);
});
const currentYear = new Date().getFullYear();
for (let y = currentYear - 1; y <= currentYear + 5; y++) {
const opt = document.createElement('option');
opt.value = y;
opt.textContent = y;
if (y === selectedYear) opt.selected = true;
yearSelect.appendChild(opt);
}
monthSelect.addEventListener('change', (e) => {
selectedMonth = parseInt(e.target.value);
saveToStorage();
if (currentStep === 2) renderDutyStep();
});
yearSelect.addEventListener('change', (e) => {
selectedYear = parseInt(e.target.value);
saveToStorage();
if (currentStep === 2) renderDutyStep();
});
}
function goToStep(step) {
if (step === 2 && employees.length === 0) {
alert('Bitte geben Sie mindestens einen Mitarbeiter ein.');
return;
}
updateStep(step);
}
function updateStep(step) {
currentStep = step;
document.querySelectorAll('.step-content').forEach(el => el.classList.remove('active'));
document.getElementById(`step${step}`).classList.add('active');
document.querySelectorAll('.step-item').forEach((el, i) => {
el.classList.remove('active', 'completed');
if (i + 1 === step) {
el.classList.add('active');
} else if (i + 1 < step) {
el.classList.add('completed');
}
});
if (step === 2) {
renderDutyStep();
}
document.querySelector('.print-btn').style.display = step === 3 ? 'flex' : 'none';
}
function addEmployee() {
const input = document.getElementById('employeeName');
const name = input.value.trim();
if (!name) {
alert('Bitte geben Sie einen Namen ein.');
return;
}
if (employees.includes(name)) {
alert('Dieser Mitarbeiter existiert bereits.');
return;
}
employees.push(name);
input.value = '';
renderEmployees();
saveToStorage();
}
function removeEmployee(name) {
employees = employees.filter(e => e !== name);
duties = duties.filter(d => d.employee !== name);
renderEmployees();
saveToStorage();
}
function renderEmployees() {
const container = document.getElementById('employeeList');
if (employees.length === 0) {
container.innerHTML = '<div class="empty-state"><p>Noch keine Mitarbeiter hinzugefügt</p></div>';
return;
}
container.innerHTML = employees.map(name => `
<div class="employee-tag">
<span>${name}</span>
<button class="remove" onclick="removeEmployee('${name}')">&times;</button>
</div>
`).join('');
}
function renderDutyStep() {
document.getElementById('monthYearDisplay').textContent = `${MONTHS[selectedMonth]} ${selectedYear}`;
const empSelect = document.getElementById('dutyEmployee');
empSelect.innerHTML = employees.map(e => `<option value="${e}">${e}</option>`).join('');
const dateInput = document.getElementById('dutyDate');
const firstDay = new Date(selectedYear, selectedMonth, 1);
const lastDay = new Date(selectedYear, selectedMonth + 1, 0);
dateInput.min = formatDate(firstDay);
dateInput.max = formatDate(lastDay);
dateInput.value = formatDate(firstDay);
renderCalendar();
renderDutyList();
}
function renderCalendar() {
const container = document.getElementById('calendarContainer');
const firstDay = new Date(selectedYear, selectedMonth, 1);
const lastDay = new Date(selectedYear, selectedMonth + 1, 0);
const startDay = firstDay.getDay() || 7;
let html = '<div class="calendar-grid">';
const dayNames = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
dayNames.forEach(d => html += `<div class="calendar-header">${d}</div>`);
for (let i = 1; i < startDay; i++) {
html += '<div class="calendar-day empty"></div>';
}
for (let day = 1; day <= lastDay.getDate(); day++) {
const date = new Date(selectedYear, selectedMonth, day);
const dateStr = formatDate(date);
const qualifying = isQualifyingDay(date);
const hasDuty = duties.some(d => d.date === dateStr);
let classes = 'calendar-day';
if (qualifying) classes += ' qualifying';
if (hasDuty) classes += ' has-duty selected';
html += `<div class="${classes}" onclick="selectDate('${dateStr}')" title="${getDayTypeLabel(date)}">${day}</div>`;
}
html += '</div>';
container.innerHTML = html;
}
function selectDate(dateStr) {
document.getElementById('dutyDate').value = dateStr;
}
function addDuty() {
const employee = document.getElementById('dutyEmployee').value;
const dateStr = document.getElementById('dutyDate').value;
const share = parseFloat(document.getElementById('dutyShare').value);
if (!employee || !dateStr) {
alert('Bitte füllen Sie alle Felder aus.');
return;
}
const date = new Date(dateStr);
if (date.getMonth() !== selectedMonth || date.getFullYear() !== selectedYear) {
alert('Das Datum muss im ausgewählten Monat liegen.');
return;
}
duties.push({ employee, date: dateStr, share });
renderCalendar();
renderDutyList();
saveToStorage();
}
function removeDuty(index) {
duties.splice(index, 1);
renderCalendar();
renderDutyList();
saveToStorage();
}
function renderDutyList() {
const container = document.getElementById('dutyListContainer');
const monthDuties = duties.filter(d => {
const date = new Date(d.date);
return date.getMonth() === selectedMonth && date.getFullYear() === selectedYear;
});
document.getElementById('dutyCount').textContent = monthDuties.length;
if (monthDuties.length === 0) {
container.innerHTML = '<div class="empty-state"><p>Noch keine Dienste eingetragen</p></div>';
return;
}
monthDuties.sort((a, b) => new Date(a.date) - new Date(b.date));
container.innerHTML = monthDuties.map((duty, idx) => {
const date = new Date(duty.date);
const qualifying = isQualifyingDay(date);
const dayType = getDayTypeLabel(date);
const realIndex = duties.indexOf(duty);
return `
<div class="duty-item ${qualifying ? 'qualifying' : ''}">
<div>
<strong>${duty.employee}</strong>
</div>
<div>
${date.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: '2-digit' })}
<div class="day-type">${dayType}</div>
</div>
<div>Anteil: ${formatNumber(duty.share)}</div>
<div>
<button class="btn btn-danger" onclick="removeDuty(${realIndex})">Löschen</button>
</div>
</div>
`;
}).join('');
}
function calculateAndShowResults() {
const monthDuties = duties.filter(d => {
const date = new Date(d.date);
return date.getMonth() === selectedMonth && date.getFullYear() === selectedYear;
});
if (monthDuties.length === 0) {
alert('Keine Dienste für diesen Monat eingetragen.');
return;
}
goToStep(3);
renderResults();
}
function renderResults() {
document.getElementById('resultMonthYear').textContent = `${MONTHS[selectedMonth]} ${selectedYear}`;
const monthDuties = duties.filter(d => {
const date = new Date(d.date);
return date.getMonth() === selectedMonth && date.getFullYear() === selectedYear;
});
const byEmployee = {};
monthDuties.forEach(d => {
if (!byEmployee[d.employee]) byEmployee[d.employee] = [];
byEmployee[d.employee].push(d);
});
let totalBonus = 0;
let html = '';
employees.forEach(emp => {
const empDuties = byEmployee[emp] || [];
const result = calculateBonus(empDuties);
totalBonus += result.totalBonus;
html += `
<div class="result-card">
<h3>
${emp}
<span class="threshold-badge ${result.thresholdReached ? 'reached' : 'not-reached'}">
${result.thresholdReached ? 'Schwelle erreicht' : 'Schwelle nicht erreicht'}
</span>
</h3>
<div class="result-grid">
<div class="result-item">
<div class="label">WT-Einheiten</div>
<div class="value">${formatNumber(result.normalDays)}</div>
</div>
<div class="result-item">
<div class="label">WE-Einheiten (Freitag)</div>
<div class="value">${formatNumber(result.qualifyingDaysFriday)}</div>
</div>
<div class="result-item">
<div class="label">WE-Einheiten (Andere)</div>
<div class="value">${formatNumber(result.qualifyingDaysOther)}</div>
</div>
<div class="result-item ${result.thresholdReached ? 'success' : 'warning'}">
<div class="label">WE-Gesamt</div>
<div class="value">${formatNumber(result.qualifyingDays)}</div>
</div>
<div class="result-item">
<div class="label">Abzug</div>
<div class="value">-${formatNumber(result.deduction)}</div>
</div>
<div class="result-item">
<div class="label">WT bezahlt</div>
<div class="value">${formatNumber(result.normalDaysPaid)}</div>
</div>
<div class="result-item">
<div class="label">WE bezahlt</div>
<div class="value">${formatNumber(result.qualifyingDaysPaid)}</div>
</div>
<div class="result-item highlight">
<div class="label">Bonus Gesamt</div>
<div class="value">${formatCurrency(result.totalBonus)}</div>
</div>
</div>
${result.thresholdReached ? `
<div style="margin-top: 15px; font-size: 0.9em; color: #718096;">
WT: ${formatNumber(result.normalDaysPaid)} x ${formatCurrency(RATE_NORMAL)} = ${formatCurrency(result.bonusNormalDays)}<br>
WE: ${formatNumber(result.qualifyingDaysPaid)} x ${formatCurrency(RATE_WEEKEND)} = ${formatCurrency(result.bonusQualifyingDays)}
</div>
` : `
<div style="margin-top: 15px; font-size: 0.9em; color: #c53030;">
WE-Schwelle (2,0) nicht erreicht - kein Bonus
</div>
`}
</div>
`;
});
html += `
<div class="total-summary">
<h2>Gesamtauszahlung</h2>
<div class="amount">${formatCurrency(totalBonus)}</div>
</div>
`;
document.getElementById('resultsContainer').innerHTML = html;
}
// ==================== STORAGE ====================
function saveToStorage() {
try {
localStorage.setItem('dienstplan_portable_employees', JSON.stringify(employees));
localStorage.setItem('dienstplan_portable_duties', JSON.stringify(duties));
localStorage.setItem('dienstplan_portable_month', selectedMonth);
localStorage.setItem('dienstplan_portable_year', selectedYear);
} catch (e) {
console.log('Speichern nicht möglich:', e);
}
}
function loadFromStorage() {
try {
const savedEmployees = localStorage.getItem('dienstplan_portable_employees');
const savedDuties = localStorage.getItem('dienstplan_portable_duties');
const savedMonth = localStorage.getItem('dienstplan_portable_month');
const savedYear = localStorage.getItem('dienstplan_portable_year');
if (savedEmployees) employees = JSON.parse(savedEmployees);
if (savedDuties) duties = JSON.parse(savedDuties);
if (savedMonth !== null) selectedMonth = parseInt(savedMonth);
if (savedYear !== null) selectedYear = parseInt(savedYear);
} catch (e) {
console.log('Laden nicht möglich:', e);
}
}
// Enter key for employee input
document.addEventListener('DOMContentLoaded', () => {
init();
document.getElementById('employeeName').addEventListener('keypress', (e) => {
if (e.key === 'Enter') addEmployee();
});
});
</script>
</body>
</html>