11 KiB
CLAUDE.md - AI Assistant Guide for Dienstplan-Pro
Project Overview
Dienstplan-Pro is a German-language Progressive Web App (PWA) for calculating bonus payments for weekend and holiday duty shifts according to NRW (Nordrhein-Westfalen) regulations. The application is designed for healthcare or similar organizations that need to track employee on-call duties and calculate corresponding bonuses.
Primary Language: German (UI, comments, and documentation)
Tech Stack: Vanilla JavaScript, HTML5, CSS3, LocalStorage API
Deployment: Docker container (Node.js serve) on self-hosted Hetzner server, fronted by Caddy reverse proxy with automatic Let's Encrypt TLS. Live at https://bonus.pixel-by-design.de
Architecture
File Structure
Dienstplan-Pro/
├── index.html # Main HTML entry point with tab-based UI
├── app.js # Main application class (DienstplanApp) - UI management
├── calculator.js # BonusCalculator class - core business logic
├── holidays.js # HolidayProvider class - NRW holiday data 2025-2030
├── storage.js # DataStorage class - LocalStorage persistence
├── styles.css # All CSS styles (responsive, gradient theme)
├── sw.js # Service Worker for PWA offline support
├── manifest.json # PWA manifest configuration
├── test-suite.js # Comprehensive test runner with assertions
├── test.html # Browser-based test interface
├── Dockerfile # Production deployment configuration
├── README.md # User documentation (German)
└── TEST_GUIDE.md # Testing documentation (German)
Module Responsibilities
| Module | Class | Purpose |
|---|---|---|
app.js |
DienstplanApp |
UI orchestration, event handling, user interactions |
calculator.js |
BonusCalculator |
Bonus calculation logic, day type classification |
holidays.js |
HolidayProvider |
NRW public holiday lookup (2025-2030) |
storage.js |
DataStorage |
LocalStorage CRUD operations, import/export |
Global Dependencies
All classes are attached to window for cross-module access:
window.DienstplanApp(instantiated as globalapp)window.BonusCalculatorwindow.HolidayProviderwindow.DataStorage
Script loading order in index.html is critical:
holidays.jscalculator.jsstorage.jsapp.js
Business Logic
Tag-Klassifizierung (Slot pro Dienst)
Jeder Dienst bekommt genau einen Slot (variants.js → classify):
- fr: echter Freitag · oder Tag vor einem Mo–Do-Feiertag
- sa: echter Samstag · oder Sandwich-Tag (Feiertag UND Tag davor)
- so: echter Sonntag · oder Mo–Do-Feiertag (ohne Sandwich)
- weekday: Mo–Do ohne Feiertagsbezug
Echte Fr/Sa/So gewinnen immer. Sätze: fr/sa/so = 450 €, weekday = 250 €; Anteile 0,5 oder 1,0.
Bonus-Berechnung (3 Varianten — variants.js, NICHT mehr das alte 2.0-Schwellen-Modell)
calculator.js läuft alle drei Varianten und nimmt die mit dem höchsten Betrag (Gleichstand → niedrigste Variantennummer). Werte normal (Urlaub = halbiert):
- V1 — greift wenn
fr+so ≥ 1UNDweekday ≥ 3; Abzug 1 aus fr+so (Fr-Priorität) + 3 weekday;sawird voll bezahlt. - V2 — greift wenn
sa ≥ 1UNDweekday ≥ 2; Abzug 1 sa + 2 weekday;fr/sowerden voll bezahlt. - V3 — greift wenn
fr+sa+so ≥ 2; Abzug 2 aus dem Pool (Reihenfolge fr → so → sa);weekdaywird voll bezahlt.
Urlaubsmodus (Flag pro Mitarbeiter/Monat) halbiert alle Schwellen und Abzüge. Maßgeblich ist variants.js; die In-App-Doku (Einstellungen → Berechnungsregeln) ist aktuell.
Mehrbenutzer (v1.0)
Seit dem Team-Release ist die App hinter Magic-Link-Login; Daten sind pro Nutzer (user_id) getrennt. Backend in server/ (auth.js, index.js, mailer.js, ratelimit.js, audit.js). Konfiguration via Env-Variablen — siehe .env.example (u.a. ADMIN_EMAIL Pflicht/Fail-Fast, SMTP, Session-Fristen).
Duty Shares
Duties can be full (1.0) or half (0.5). Half duties count as 0.5 toward all calculations.
Data Storage
LocalStorage Keys
STORAGE_KEY_EMPLOYEES = 'dienstplan_employees' // Array of employee names
STORAGE_KEY_DUTIES = 'dienstplan_duties' // Nested duty object
Data Structure
// Employees: string[]
["Max Mustermann", "Anna Schmidt"]
// Duties: { employeeName: { "YYYY-MM": duties[] } }
{
"Max Mustermann": {
"2025-11": [
{ "date": "2025-11-22T11:00:00.000Z", "share": 1.0 },
{ "date": "2025-11-23T11:00:00.000Z", "share": 0.5 }
]
}
}
Date Handling
- Dates are stored as ISO strings in LocalStorage
- Use
T12:00:00when creating dates to avoid timezone edge cases - Dates are converted back to Date objects when retrieved
UI Structure
The app uses a tab-based interface with 4 sections:
- Dienste eintragen (Enter Duties) - Add/remove shifts
- Berechnung (Calculation) - Calculate and view bonuses
- Mitarbeiter verwalten (Manage Employees) - CRUD for employees
- Einstellungen (Settings) - Export/import, rules info, data clearing
Toast Notifications
Use app.showToast(message, type) where type is:
'success'- Green'error'- Red'info'- Blue
Testing
Running Tests
- Serve the app:
python3 -m http.server 8000or use the Docker container - Open
http://localhost:8000/test.html - Click "Alle Tests ausführen"
Test Categories
- HolidayProvider: Holiday detection, day-before-holiday
- Calculator - Tag-Klassifizierung: Day type classification
- Calculator - Bonusberechnung: Bonus calculation scenarios
- Storage: CRUD operations, import/export
- Edge Cases: Rounding, performance, leap years
Adding Tests
runner.test('Test Name', (t) => {
const calculator = new BonusCalculator(new HolidayProvider());
const duties = [{ date: new Date('2025-11-22T12:00:00'), share: 1.0 }];
const result = calculator.calculateMonthlyBonus(duties);
t.assertEqual(result.totalBonus, 450, 'Expected bonus');
t.assertTrue(result.thresholdReached, 'Threshold should be reached');
t.assertAlmostEqual(result.qualifyingDays, 1.0, 0.01, 'Qualifying days');
});
Development Workflow
Local Development
# Option 1: Python server
python3 -m http.server 8000
# Option 2: Node.js
npx http-server -p 8000
# Option 3: Docker
docker build -t dienstplan-pro .
docker run -p 3000:3000 -e PORT=3000 dienstplan-pro
Making Changes
- All JavaScript is vanilla ES6+ classes
- No build step required
- Refresh browser to see changes
- Run test suite after changes
Deployment (Hetzner)
Live URL: https://bonus.pixel-by-design.de
Server: root@65.21.60.83 (Hetzner)
Container: dienstplan-pro on the matrix_default Docker network so the
matrix-caddy-1 reverse proxy can resolve it by hostname.
The Dockerfile runs an Express server (server/index.js) that serves the
static frontend and the /api/state persistence API, backed by SQLite:
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .
ENV PORT=3000
ENV DATA_DIR=/data
EXPOSE 3000
CMD ["node", "server/index.js"]
Caddy block in /opt/matrix/Caddyfile (app + /api/* behind Basic-Auth):
bonus.pixel-by-design.de {
basic_auth {
benad <BCRYPT_HASH>
}
reverse_proxy dienstplan-pro:3000
}
Wichtig: Die SQLite-DB und die täglichen Backups liegen auf dem benannten Docker-Volume
dienstplan-data(/dataim Container, Backups unter/data/backups/). Niemals ohne dieses Volume deployen — sonst löschtdocker rmbeim nächsten Update alle Daten unwiderruflich. Die Domain ist komplett hinter Caddy Basic-Auth (gilt auch für/api/*).
Update procedure (when pushing new code):
ssh root@65.21.60.83
cd /root/Dienstplan-Pro
git pull
docker build -t dienstplan-pro:latest .
docker stop dienstplan-pro && docker rm dienstplan-pro
docker run -d --name dienstplan-pro --network matrix_default \
--restart unless-stopped -e PORT=3000 -e DATA_DIR=/data \
-v dienstplan-data:/data dienstplan-pro:latest
Caddy reloads not needed unless the Caddyfile changes.
Code Conventions
Language
- All user-facing text: German
- Code comments: German (existing pattern)
- Variable/function names: English (existing pattern)
Naming
- Classes: PascalCase (
BonusCalculator,DataStorage) - Methods/functions: camelCase (
calculateMonthlyBonus,isHoliday) - Constants: UPPER_SNAKE_CASE (
RATE_NORMAL,STORAGE_KEY_DUTIES) - DOM IDs: kebab-case (
employee-select-duty,calc-month-select)
Error Handling
- Storage operations include try/catch with German console.error messages
- User-facing errors shown via toast notifications
- Invalid data returns empty arrays/objects rather than throwing
Date Format
- Display: German locale
toLocaleDateString('de-DE') - Storage: ISO string
- Internal: JavaScript Date objects with noon time (
T12:00:00)
Common Tasks
Adding a New Holiday
Edit holidays.js, add to the appropriate year array:
{ date: 'YYYY-MM-DD', name: 'Holiday Name' }
Modifying Calculation Rules
Edit calculator.js constants:
this.RATE_NORMAL = 250;
this.RATE_WEEKEND = 450;
this.MIN_QUALIFYING_DAYS = 2.0;
this.DEDUCTION_AMOUNT = 2.0;
Adding Export Formats
The app supports multiple export formats in app.js:
exportData()- JSON backupexportCSV()- Excel-compatible CSV with BOMexportBonusReport()- HTML report for printing/PDFgenerateEmailReport()- Copyable email text
Extending the UI
- Add HTML in
index.htmlwithin appropriate tab-content div - Add event listener in
setupEventListeners()inapp.js - Implement handler method in
DienstplanAppclass - Style in
styles.cssfollowing existing patterns
PWA Features
- Service Worker (
sw.js): Caches all assets for offline use - Manifest (
manifest.json): Enables "Add to Home Screen" - Cache Version:
dienstplan-pro-v1(increment when updating assets)
Key Gotchas
- Timezone Issues: Always use
T12:00:00when creating dates from strings to avoid midnight edge cases - LocalStorage Limits: ~5MB, sufficient for typical use but no warning when approaching limit
- Float Precision: Use
assertAlmostEqualin tests for floating-point comparisons - Script Order:
holidays.jsmust load beforecalculator.js - Employee Deletion: Removes all associated duties automatically
- Duty Updates: Adding a duty on existing date replaces (not duplicates)
Git Conventions
Recent commit patterns:
feat:New featuresfix:Bug fixes- Keep commit messages concise and descriptive