Add/update meeting tracker variants and English build

This commit is contained in:
Stefan Koelle
2026-05-01 22:07:23 +02:00
commit 11a5f62d68
8 changed files with 1541 additions and 0 deletions
+63
View File
@@ -0,0 +1,63 @@
# Meeting Cost Tracker
A small single-page tool that shows, in real time, how much a meeting is costing while it runs.
It is intentionally simple: enter how many developers are present, enter the average monthly gross salary, press **Start**, and the page begins counting the cost every second.
## What it calculates
The tracker estimates meeting cost from salary, employer overhead, team size, and elapsed time. The calculation is based on the idea that one developers salary can be converted into an annual employer cost, then into a working-hour and working-second cost. Similar meeting-cost tools commonly start from annual salary, convert it to an hourly rate, and multiply by meeting duration and headcount. [meetingtoll](https://www.meetingtoll.com/blog/meeting-cost-formula-per-employee)
## Formula
The app uses this formula:
\[
AnnualSalary = AvgMonthlySalary \times 12
\]
\[
AnnualSalaryWithEmployerContribution = AnnualSalary \times 1.2
\]
\[
TotalAnnualCost = AnnualSalaryWithEmployerContribution \times DevelopersPresent
\]
\[
CostPerSecond = \frac{TotalAnnualCost}{220 \times 8 \times 60 \times 60}
\]
The 1.2 multiplier is a simplified overhead factor for employer contributions, which is in the same rough range as public Germany payroll references that place employer burden around 20 to 23 percent above gross salary. [boundlesshq](https://boundlesshq.com/blog/payroll-in-germany/)
## Why 220 days
The app divides by 220 working days instead of 365 calendar days. That makes the result closer to actual working time, because meetings happen during paid work, not across the full calendar year. [capme](https://www.capme.app/meeting-cost-calculator)
Using 220 days, 8 hours per day, and 60 minutes per hour is a practical simplification that turns annual cost into a live per-second burn rate. It is not a payroll-grade accounting model, but it is a useful way to visualize meeting cost in real time. [meetingking](https://meetingking.com/meeting-cost-calculator/)
## Example
If 5 developers are present and the average monthly gross salary is 5,000 €:
- Annual salary per developer: 60,000 €
- Annual salary incl. employer contribution: 72,000 €
- Total annual cost for the meeting: 360,000 €
- Cost per second: about 0.0568 €
- Cost per minute: about 3.41 €
- Cost per hour: about 204.55 €
These numbers are consistent with the formula above.
## Controls
- **Start** begins the live counter at zero.
- **Reset** stops the counter and sets the accumulated cost back to zero.
- The cost per second is recalculated whenever the inputs change.
- The accumulated cost updates once per second while the counter is running.
## Notes
This tool is designed to make meeting costs visible, not to produce exact payroll accounting. In real companies, the true employer cost can vary by insurance rates, caps, bonuses, and other overhead, so the 1.2 factor should be understood as a simple approximation. [payrollgermany](https://payrollgermany.de/blog/employer-contributions-to-social-security-in-germany-a-comprehensive-guide/)
The result is best used as a conversation starter: it helps teams notice how quickly meeting time turns into money.
+85
View File
@@ -0,0 +1,85 @@
# Prompt: Meeting Cost Tracker — HTML App
Build a single, self-contained HTML page (no external dependencies except CDN fonts/icons) that calculates and displays the live running cost of a software engineering meeting.
---
## Layout & Inputs
On page load, show two input fields and two buttons:
- **Input 1** — Label: "Developers present", type: number, integer, min 1, no default value
- **Input 2** — Label: "Avg. monthly gross salary (€)", type: number, pre-filled with `5000`
- **Start button** — starts the cost counter from zero
- **Reset button** — stops the counter and resets the displayed cost back to zero; inputs remain editable
---
## Display
Below the inputs, show two display areas (visible at all times, initially showing zero):
1. **Large display — "Total cost so far"**
- Updates every second while the counter is running
- Shows the accumulated cost since Start was clicked
- Format: German locale with 2 decimal places, e.g. `1.234,56 €`
- This should be the visually dominant element on the page (large font, prominent placement)
2. **Smaller display — "Cost per second"**
- Shows the calculated `CostPerSecond` value (static — only changes when inputs change)
- Same number format: `0,03 €`
---
## Calculation
```
AnnualSalary = AvgMonthlySalary × 12
AnnualSalaryWithEmployerContributions = AnnualSalary × 1.2
TotalAnnualCost = AnnualSalaryWithEmployerContributions × DevelopersPresent
CostPerSecond = TotalAnnualCost ÷ 220 days ÷ 8 hours ÷ 60 minutes ÷ 60 seconds
```
> **Note on 220 days:** This uses 220 working days per year (accounts for weekends and public holidays), not 365 calendar days — this gives a more accurate cost-per-working-second figure.
The `CostPerSecond` value must be recalculated whenever the inputs change. If the counter is running while the user changes an input, the new rate applies immediately to subsequent seconds.
---
## Button Behavior
- **Start**: Begins the interval counter (1-second tick). Disabled while counter is running.
- **Reset**: Stops the counter, sets accumulated cost display back to `0,00 €`, re-enables the Start button.
- There is no Pause. Reset is the only way to stop the counter.
---
## Validation
Before starting the counter, validate:
- "Developers present" must be a whole number ≥ 1
- "Avg. monthly gross salary" must be > 0
- Show a clear inline error message if validation fails; do not start the counter
---
## Design & UX
- Single-page, fully self-contained HTML file
- No frameworks required — vanilla HTML, CSS, JS is preferred
- Clean, modern dark UI — this will be used on a screen during internal meetings
- The "Total cost so far" number should be the largest visual element on the page — make it dramatic and easy to read from across a room
- The page should work well at 1080p (1920×1080) full-screen in a browser
- Include a subtle visual indicator (e.g. pulsing dot or color change) when the counter is actively running
- No localStorage, no cookies, no server-side code
---
## Example Values (for testing)
- 8 developers, €5,000 avg salary
- AnnualSalary = 60,000 €
- AnnualSalaryWithEmployerContributions = 72,000 €
- TotalAnnualCost = 576,000 €
- CostPerSecond = 576,000 ÷ 220 ÷ 8 ÷ 3,600 ≈ **0.0909 €/sec**
- After 60 seconds: ≈ 5,45 €
+274
View File
@@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="de" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meeting Cost Tracker</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
:root, [data-theme="light"] {
--color-bg:#f7f6f2;--color-surface:#f9f8f5;--color-surface-2:#fbfbf9;
--color-surface-offset:#f3f0ec;--color-divider:#dcd9d5;--color-border:#d4d1ca;
--color-text:#28251d;--color-text-muted:#7a7974;--color-text-faint:#bab9b4;
--color-primary:#01696f;--color-primary-hover:#0c4e54;
--color-warning:#964219;--color-success:#437a22;
--shadow-sm:0 1px 2px oklch(0.2 0.01 80/0.06);
--shadow-md:0 4px 12px oklch(0.2 0.01 80/0.08);
--shadow-lg:0 12px 32px oklch(0.2 0.01 80/0.12);
--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--radius-full:9999px;
--text-xs:clamp(.75rem,.7rem + .25vw,.875rem);
--text-sm:clamp(.875rem,.8rem + .35vw,1rem);
--text-base:clamp(1rem,.95rem + .25vw,1.125rem);
--text-lg:clamp(1.125rem,1rem + .75vw,1.5rem);
--text-xl:clamp(1.5rem,1.2rem + 1.25vw,2.25rem);
--space-1:.25rem;--space-2:.5rem;--space-3:.75rem;--space-4:1rem;
--space-5:1.25rem;--space-6:1.5rem;--space-8:2rem;
--font-body:'Inter','Helvetica Neue',sans-serif;
--font-mono:'JetBrains Mono','Courier New',monospace;
--transition:180ms cubic-bezier(.16,1,.3,1);
}
[data-theme="dark"] {
--color-bg:#171614;--color-surface:#1c1b19;--color-surface-2:#201f1d;
--color-surface-offset:#22211f;--color-divider:#262523;--color-border:#393836;
--color-text:#cdccca;--color-text-muted:#797876;--color-text-faint:#5a5957;
--color-primary:#4f98a3;--color-primary-hover:#227f8b;
--color-warning:#bb653b;--color-success:#6daa45;
--shadow-sm:0 1px 2px oklch(0 0 0/.2);
--shadow-md:0 4px 12px oklch(0 0 0/.3);
--shadow-lg:0 12px 32px oklch(0 0 0/.4);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility}
body{min-height:100dvh;font-family:var(--font-body);font-size:var(--text-base);color:var(--color-text);background:var(--color-bg);display:flex;flex-direction:column;align-items:center;padding:var(--space-8) var(--space-4)}
input,button{font:inherit;color:inherit}
a,button,input{transition:color var(--transition),background var(--transition),border-color var(--transition),box-shadow var(--transition)}
button{cursor:pointer;background:none;border:none}
.app-header{width:100%;max-width:720px;display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-8)}
.logo{display:flex;align-items:center;gap:var(--space-3)}
.logo-icon{color:var(--color-primary)}
.logo-text{font-size:var(--text-lg);font-weight:600;color:var(--color-text);letter-spacing:-.02em}
.logo-sub{font-size:var(--text-xs);color:var(--color-text-muted);margin-top:1px}
.theme-toggle{width:36px;height:36px;border-radius:var(--radius-md);display:flex;align-items:center;justify-content:center;color:var(--color-text-muted);border:1px solid oklch(from var(--color-text) l c h/.1)}
.theme-toggle:hover{color:var(--color-text);background:var(--color-surface)}
.card{width:100%;max-width:720px;background:var(--color-surface);border:1px solid oklch(from var(--color-text) l c h/.08);border-radius:var(--radius-xl);padding:var(--space-8);box-shadow:var(--shadow-md)}
.form-row{display:grid;grid-template-columns:1fr 1fr auto;gap:var(--space-4);align-items:end;margin-bottom:var(--space-6)}
@media(max-width:600px){.form-row{grid-template-columns:1fr}}
.form-field{display:flex;flex-direction:column;gap:var(--space-2)}
.form-label{font-size:var(--text-xs);font-weight:600;color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.06em}
.form-input{padding:var(--space-3) var(--space-4);background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--text-base);color:var(--color-text);outline:none;width:100%}
.form-input:focus{border-color:var(--color-primary);box-shadow:0 0 0 3px oklch(from var(--color-primary) l c h/.15)}
.form-input:disabled{opacity:.5;cursor:not-allowed}
.form-hint{font-size:var(--text-xs);color:var(--color-text-faint);margin-top:var(--space-1)}
.input-prefix-wrap{position:relative}
.input-prefix-wrap .form-input{padding-left:var(--space-8)}
.input-prefix{position:absolute;left:var(--space-3);top:50%;transform:translateY(-50%);color:var(--color-text-muted);font-size:var(--text-sm);pointer-events:none;line-height:1}
.btn-start{padding:var(--space-3) var(--space-6);background:var(--color-primary);color:#fff;border-radius:var(--radius-md);font-size:var(--text-sm);font-weight:600;white-space:nowrap;display:flex;align-items:center;gap:var(--space-2);box-shadow:var(--shadow-sm);height:44px}
.btn-start:hover{background:var(--color-primary-hover);box-shadow:var(--shadow-md)}
.btn-start:active{transform:scale(.98)}
.btn-reset{padding:var(--space-2) var(--space-4);border:1px solid var(--color-border);border-radius:var(--radius-md);font-size:var(--text-xs);color:var(--color-text-muted);font-weight:500;display:flex;align-items:center;gap:var(--space-2)}
.btn-reset:hover{color:var(--color-text);border-color:var(--color-text-muted)}
.divider{height:1px;background:var(--color-divider);margin:var(--space-6) 0}
.action-row{display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-3);margin-bottom:var(--space-6)}
.status-badge{display:inline-flex;align-items:center;gap:var(--space-2);padding:var(--space-1) var(--space-3);border-radius:var(--radius-full);font-size:var(--text-xs);font-weight:600;letter-spacing:.04em;text-transform:uppercase;background:var(--color-surface-offset);color:var(--color-text-muted);border:1px solid var(--color-border)}
.status-badge.running{background:color-mix(in oklch,var(--color-warning) 12%,var(--color-surface));color:var(--color-warning);border-color:color-mix(in oklch,var(--color-warning) 30%,var(--color-border))}
.status-badge.stopped{background:color-mix(in oklch,var(--color-success) 10%,var(--color-surface));color:var(--color-success);border-color:color-mix(in oklch,var(--color-success) 30%,var(--color-border))}
.running-indicator{width:10px;height:10px;border-radius:var(--radius-full);background:currentColor;display:inline-block}
@keyframes pulse{0%{opacity:1}50%{opacity:.3}100%{opacity:1}}
.running-indicator.active{animation:pulse 1.2s ease-in-out infinite}
.cost-display-wrap{position:relative;background:var(--color-surface-offset);border:1px solid oklch(from var(--color-text) l c h/.06);border-radius:var(--radius-lg);padding:var(--space-8) var(--space-8) var(--space-6);text-align:center;min-height:160px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.cost-display-label{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.08em;font-weight:600;margin-bottom:var(--space-3)}
.cost-display-amount{font-family:var(--font-mono);font-size:clamp(2.5rem,6vw,4.5rem);font-weight:600;letter-spacing:-.03em;color:var(--color-text);line-height:1;transition:color .3s ease}
.cost-display-amount.running{color:var(--color-warning)}
.cost-display-currency{font-size:var(--text-xl);font-weight:400;color:var(--color-text-muted);vertical-align:super;margin-right:var(--space-2);font-family:var(--font-mono)}
.cost-display-elapsed{margin-top:var(--space-3);font-size:var(--text-xs);color:var(--color-text-faint);font-family:var(--font-mono)}
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:var(--space-3);margin-top:var(--space-6)}
.info-tile{background:var(--color-surface-2,var(--color-surface));border:1px solid oklch(from var(--color-text) l c h/.06);border-radius:var(--radius-md);padding:var(--space-4)}
.info-tile-label{font-size:var(--text-xs);color:var(--color-text-muted);text-transform:uppercase;letter-spacing:.05em;font-weight:600;margin-bottom:var(--space-2)}
.info-tile-value{font-family:var(--font-mono);font-size:var(--text-sm);font-weight:600;color:var(--color-text)}
</style>
</head>
<body>
<header class="app-header">
<div class="logo">
<svg class="logo-icon" width="32" height="32" viewBox="0 0 32 32" fill="none" aria-label="Meeting Cost Tracker">
<circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/>
<path d="M16 8v8l5 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="16" cy="16" r="2" fill="currentColor"/>
</svg>
<div>
<div class="logo-text">Meeting Cost Tracker</div>
<div class="logo-sub">Echtzeit-Kostenübersicht fuer Meetings</div>
</div>
</div>
<button class="theme-toggle" data-theme-toggle aria-label="Theme umschalten">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/>
</svg>
</button>
</header>
<main class="card">
<div class="form-row">
<div class="form-field">
<label class="form-label" for="devCount">Entwickler anwesend</label>
<input class="form-input" type="number" id="devCount" min="1" max="500" value="5">
<div class="form-hint">Anzahl der Teilnehmer</div>
</div>
<div class="form-field">
<label class="form-label" for="avgSalary">Avg. Monatsgehalt</label>
<div class="input-prefix-wrap">
<span class="input-prefix">EUR</span>
<input class="form-input" type="number" id="avgSalary" min="1000" max="50000" value="5000" style="padding-left:3.5rem">
</div>
<div class="form-hint">Brutto, durchschnittlich</div>
</div>
<button class="btn-start" id="mainBtn" onclick="handleBtn()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" id="btnIcon"><polygon points="5,3 19,12 5,21"/></svg>
<span id="btnLabel">Start</span>
</button>
</div>
<div class="divider"></div>
<div class="action-row">
<span class="status-badge" id="statusBadge">
<span class="running-indicator" id="runIndicator"></span>
Bereit
</span>
<button class="btn-reset" id="resetBtn" onclick="resetTimer()" style="display:none">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/></svg>
Zuruecksetzen
</button>
</div>
<div class="cost-display-wrap">
<div class="cost-display-label">Verbrauchte Meeting-Kosten</div>
<div class="cost-display-amount" id="costDisplay">
<span class="cost-display-currency">EUR</span><span id="costValue">0,00</span>
</div>
<div class="cost-display-elapsed" id="elapsedDisplay">Noch nicht gestartet</div>
</div>
<div class="info-grid" id="infoGrid" style="display:none">
<div class="info-tile">
<div class="info-tile-label">Kosten / Sekunde</div>
<div class="info-tile-value" id="tilePerSec">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">Kosten / Minute</div>
<div class="info-tile-value" id="tilePerMin">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">Kosten / Stunde</div>
<div class="info-tile-value" id="tilePerHour">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">Personen x Gehalt</div>
<div class="info-tile-value" id="tileTotal">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">Jahresgehalt gesamt</div>
<div class="info-tile-value" id="tileAnnual">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">inkl. Arbeitgeberanteil (×1,2)</div>
<div class="info-tile-value" id="tileEmployer">-</div>
</div>
<div class="info-tile">
<div class="info-tile-label">Gesamtkosten / Jahr</div>
<div class="info-tile-value" id="tileAnnualTotal">-</div>
</div>
</div>
</main>
<script>
(function(){
var root=document.documentElement;
var dark=false;
var btn=document.querySelector('[data-theme-toggle]');
var sunSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
var moonSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
if(btn){
// initialize button/icon to reflect light theme
root.setAttribute('data-theme', dark ? 'dark' : 'light');
btn.innerHTML = dark ? sunSvg : moonSvg;
btn.addEventListener('click', function(){
dark = !dark; root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg;
});
}
})();
var timer=null,isRunning=false,startTime=null,pausedElapsed=0;
function fmt(n){return n.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2});}
function fmtElapsed(sec){
var h=Math.floor(sec/3600),m=Math.floor((sec%3600)/60),s=Math.floor(sec%60);
if(h>0)return h+'h '+String(m).padStart(2,'0')+'m '+String(s).padStart(2,'0')+'s';
if(m>0)return m+'m '+String(s).padStart(2,'0')+'s';
return s+'s gelaufen';
}
function getParams(){
var devCount=Math.max(1,parseInt(document.getElementById('devCount').value)||1);
var avgSalary=Math.max(0,parseFloat(document.getElementById('avgSalary').value)||5000);
var annualSalary=avgSalary*12;
var annualWithEmployer=annualSalary*1.2;
var totalCost=annualWithEmployer*devCount;
var costPerSecond=totalCost/220/8/60/60;
return{devCount:devCount,avgSalary:avgSalary,totalCost:totalCost,costPerSecond:costPerSecond};
}
function tick(){
var elapsed=pausedElapsed+(Date.now()-startTime)/1000;
var p=getParams();
var spent=p.costPerSecond*elapsed;
document.getElementById('costValue').textContent=fmt(spent);
document.getElementById('elapsedDisplay').textContent=fmtElapsed(elapsed);
document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('de-DE',{minimumFractionDigits:4,maximumFractionDigits:4});
document.getElementById('tilePerMin').textContent='EUR '+fmt(p.costPerSecond*60);
document.getElementById('tilePerHour').textContent='EUR '+fmt(p.costPerSecond*3600);
document.getElementById('tileTotal').textContent=p.devCount+' x EUR '+fmt(p.avgSalary);
// show annual / employer / total year values (as integers)
var annual = p.annualSalary || p.avgSalary*12;
var employer = p.annualWithEmployer || annual*1.2;
var annualTotal = p.totalCost || employer * p.devCount;
document.getElementById('tileAnnual').textContent = annual.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
document.getElementById('tileEmployer').textContent = employer.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
document.getElementById('tileAnnualTotal').textContent = annualTotal.toLocaleString('de-DE',{minimumFractionDigits:0,maximumFractionDigits:0}) + ' €';
}
function handleBtn(){isRunning?pauseTimer():startTimer();}
function startTimer(){
isRunning=true;startTime=Date.now();
timer=setInterval(tick,100);
document.getElementById('btnLabel').textContent='Pause';
document.getElementById('btnIcon').innerHTML='<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
document.getElementById('costDisplay').classList.add('running');
document.getElementById('statusBadge').className='status-badge running';
document.getElementById('statusBadge').innerHTML='<span class="running-indicator active"></span> Laeuft';
document.getElementById('resetBtn').style.display='flex';
document.getElementById('infoGrid').style.display='grid';
document.getElementById('devCount').disabled=true;
document.getElementById('avgSalary').disabled=true;
tick();
}
function pauseTimer(){
isRunning=false;pausedElapsed+=(Date.now()-startTime)/1000;
clearInterval(timer);timer=null;
document.getElementById('btnLabel').textContent='Weiter';
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
document.getElementById('costDisplay').classList.remove('running');
document.getElementById('statusBadge').className='status-badge stopped';
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Pausiert';
}
function resetTimer(){
if(timer)clearInterval(timer);
timer=null;isRunning=false;pausedElapsed=0;startTime=null;
document.getElementById('costValue').textContent='0,00';
document.getElementById('elapsedDisplay').textContent='Noch nicht gestartet';
document.getElementById('btnLabel').textContent='Start';
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
document.getElementById('costDisplay').classList.remove('running');
document.getElementById('statusBadge').className='status-badge';
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Bereit';
document.getElementById('resetBtn').style.display='none';
document.getElementById('infoGrid').style.display='none';
document.getElementById('devCount').disabled=false;
document.getElementById('avgSalary').disabled=false;
}
// No auto-start: user must press Start. (Previously auto-started here.)
</script>
</body>
</html>
+170
View File
@@ -0,0 +1,170 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meeting Cost Tracker — English</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;600&display=swap" rel="stylesheet">
<style>
/* same styles as variant2 (kept compact) */
:root, [data-theme="light"] { --color-bg:#f7f6f2;--color-surface:#f9f8f5;--color-surface-2:#fbfbf9;--color-surface-offset:#f3f0ec;--color-divider:#dcd9d5;--color-border:#d4d1ca;--color-text:#28251d;--color-text-muted:#7a7974;--color-text-faint:#bab9b4;--color-primary:#01696f;--color-primary-hover:#0c4e54;--color-warning:#964219;--color-success:#437a22;--shadow-sm:0 1px 2px rgba(0,0,0,0.06);--shadow-md:0 4px 12px rgba(0,0,0,0.08);--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--text-xs:.75rem;--text-sm:0.9rem;--text-base:1rem;--space-3:.75rem;--space-4:1rem;--space-6:1.5rem;--space-8:2rem;--font-body:'Inter',system-ui,sans-serif;--font-mono:'JetBrains Mono',monospace}
[data-theme="dark"]{--color-bg:#171614;--color-surface:#1c1b19;--color-text:#cdccca}
*{box-sizing:border-box;margin:0;padding:0} body{min-height:100dvh;font-family:var(--font-body);font-size:var(--text-base);color:var(--color-text);background:var(--color-bg);display:flex;flex-direction:column;align-items:center;padding:var(--space-8) var(--space-4)}
.app-header{width:100%;max-width:720px;display:flex;align-items:center;justify-content:space-between;margin-bottom:var(--space-8)}
.logo-text{font-size:1.125rem;font-weight:600}
.logo-sub{font-size:0.85rem;color:var(--color-text-muted)}
.theme-toggle{width:36px;height:36px;border-radius:.5rem;display:flex;align-items:center;justify-content:center;border:1px solid rgba(0,0,0,0.06);background:transparent}
.card{width:100%;max-width:720px;background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-xl);padding:var(--space-8);box-shadow:var(--shadow-md)}
.form-row{display:grid;grid-template-columns:1fr 1fr auto;gap:1rem;align-items:end;margin-bottom:1.5rem}
.form-field{display:flex;flex-direction:column;gap:.5rem}
.form-label{font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase}
.form-input{padding:.75rem 1rem;background:var(--color-bg);border:1px solid var(--color-border);border-radius:.5rem}
.input-prefix{position:absolute;left:1rem;top:50%;transform:translateY(-50%);color:var(--color-text-muted)}
.btn-start{padding:.6rem 1.2rem;background:var(--color-primary);color:#fff;border-radius:.5rem;display:flex;align-items:center;gap:.5rem}
.btn-reset{padding:.4rem .75rem;border:1px solid var(--color-border);border-radius:.5rem}
.divider{height:1px;background:var(--color-divider);margin:1.25rem 0}
.status-badge{display:inline-flex;align-items:center;gap:.5rem;padding:.25rem .75rem;border-radius:9999px;background:var(--color-surface-offset);color:var(--color-text-muted);border:1px solid var(--color-border)}
.running-indicator{width:10px;height:10px;border-radius:9999px;background:currentColor}
.running-indicator.active{animation:pulse 1.2s infinite}@keyframes pulse{0%{opacity:1}50%{opacity:.3}100%{opacity:1}}
.cost-display-wrap{position:relative;background:var(--color-surface-offset);border:1px solid var(--color-border);border-radius:1rem;padding:2rem;text-align:center;min-height:160px;display:flex;flex-direction:column;align-items:center;justify-content:center}
.cost-display-amount{font-family:var(--font-mono);font-size:clamp(2.5rem,6vw,4.5rem);font-weight:700}
.info-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(160px,1fr));gap:1rem;margin-top:1.5rem}
.info-tile{background:var(--color-surface-2);border:1px solid var(--color-border);border-radius:.5rem;padding:1rem}
.info-tile-label{font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;margin-bottom:.5rem}
.info-tile-value{font-family:var(--font-mono);font-weight:600}
</style>
</head>
<body>
<header class="app-header">
<div style="display:flex;align-items:center;gap:1rem">
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" aria-hidden="true"><circle cx="16" cy="16" r="14" stroke="currentColor" stroke-width="2"/><path d="M16 8v8l5 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><circle cx="16" cy="16" r="2" fill="currentColor"/></svg>
<div>
<div class="logo-text">Meeting Cost Tracker</div>
<div class="logo-sub">Live meeting cost overview</div>
</div>
</div>
<button class="theme-toggle" data-theme-toggle aria-label="Toggle theme">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>
</button>
</header>
<main class="card">
<div class="form-row">
<div class="form-field">
<label class="form-label" for="devCount">Developers present</label>
<input class="form-input" type="number" id="devCount" min="1" max="500" value="5" aria-label="Developers present">
<div style="font-size:.85rem;color:var(--color-text-muted)">number of participants</div>
</div>
<div class="form-field">
<label class="form-label" for="avgSalary">Avg. monthly gross salary</label>
<div style="position:relative">
<span class="input-prefix">EUR</span>
<input class="form-input" type="number" id="avgSalary" min="1000" max="50000" value="5000" style="padding-left:3.5rem" aria-label="Average monthly salary">
</div>
<div style="font-size:.85rem;color:var(--color-text-muted)">brutto, avarage salary</div>
</div>
<button class="btn-start" id="mainBtn" onclick="handleBtn()">
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" id="btnIcon"><polygon points="5,3 19,12 5,21"/></svg>
<span id="btnLabel">Start</span>
</button>
</div>
<div class="divider"></div>
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem">
<span class="status-badge" id="statusBadge"><span class="running-indicator" id="runIndicator"></span> Ready</span>
<button class="btn-reset" id="resetBtn" onclick="resetTimer()" style="display:none">Reset</button>
</div>
<div class="cost-display-wrap">
<div style="font-size:.75rem;color:var(--color-text-muted);text-transform:uppercase;margin-bottom:.5rem">Total cost so far</div>
<div class="cost-display-amount" id="costDisplay"><span style="font-family:var(--font-mono);font-size:1rem;margin-right:.4rem">EUR</span><span id="costValue">0.00</span></div>
<div style="margin-top:.5rem;font-size:.85rem;color:var(--color-text-muted)" id="elapsedDisplay">Not started</div>
</div>
<div class="info-grid" id="infoGrid" style="display:none">
<div class="info-tile"><div class="info-tile-label">Cost / second</div><div class="info-tile-value" id="tilePerSec">-</div></div>
<div class="info-tile"><div class="info-tile-label">Cost / minute</div><div class="info-tile-value" id="tilePerMin">-</div></div>
<div class="info-tile"><div class="info-tile-label">Cost / hour</div><div class="info-tile-value" id="tilePerHour">-</div></div>
<div class="info-tile"><div class="info-tile-label">People × salary</div><div class="info-tile-value" id="tileTotal">-</div></div>
<div class="info-tile"><div class="info-tile-label">Annual salary (per person)</div><div class="info-tile-value" id="tileAnnual">-</div></div>
<div class="info-tile"><div class="info-tile-label">incl. employer contributions (×1.2)</div><div class="info-tile-value" id="tileEmployer">-</div></div>
<div class="info-tile"><div class="info-tile-label">Total annual cost</div><div class="info-tile-value" id="tileAnnualTotal">-</div></div>
</div>
</main>
<script>
(function(){
var root=document.documentElement; var dark=false; var btn=document.querySelector('[data-theme-toggle]');
var sunSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/></svg>';
var moonSvg='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
if(btn){ root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg; btn.addEventListener('click', function(){ dark=!dark; root.setAttribute('data-theme', dark ? 'dark' : 'light'); btn.innerHTML = dark ? sunSvg : moonSvg; }); }
})();
var timer=null,isRunning=false,startTime=null,pausedElapsed=0;
// English formatting (GBP/GB style but currency EUR)
function fmt(n){ return new Intl.NumberFormat('en-GB',{ style:'currency', currency:'EUR', minimumFractionDigits:2, maximumFractionDigits:2 }).format(n); }
function fmtPlain(n,d){ return n.toLocaleString('en-GB',{minimumFractionDigits:d,maximumFractionDigits:d}); }
function fmtElapsed(sec){ var h=Math.floor(sec/3600), m=Math.floor((sec%3600)/60), s=Math.floor(sec%60); if(h>0) return h + 'h ' + String(m).padStart(2,'0') + 'm ' + String(s).padStart(2,'0') + 's'; if(m>0) return m + 'm ' + String(s).padStart(2,'0') + 's'; return Math.floor(sec) + 's elapsed'; }
function getParams(){
var devCount=Math.max(1,parseInt(document.getElementById('devCount').value)||1);
var avgSalary=Math.max(0,parseFloat(document.getElementById('avgSalary').value)||5000);
var annualSalary=avgSalary*12;
var annualWithEmployer=annualSalary*1.2;
var totalCost=annualWithEmployer*devCount;
var costPerSecond=totalCost/220/8/60/60;
return { devCount:devCount, avgSalary:avgSalary, annualSalary:annualSalary, annualWithEmployer:annualWithEmployer, totalCost:totalCost, costPerSecond:costPerSecond };
}
function tick(){
var elapsed = pausedElapsed + (Date.now()-startTime)/1000;
var p = getParams();
var spent = p.costPerSecond * elapsed;
document.getElementById('costValue').textContent = fmtPlain(spent,2);
document.getElementById('elapsedDisplay').textContent = fmtElapsed(elapsed);
document.getElementById('tilePerSec').textContent = 'EUR ' + p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4});
document.getElementById('tilePerMin').textContent = 'EUR ' + fmtPlain(p.costPerSecond*60,2);
document.getElementById('tilePerHour').textContent = 'EUR ' + fmtPlain(p.costPerSecond*3600,2);
document.getElementById('tileTotal').textContent = p.devCount + ' x EUR ' + fmtPlain(p.avgSalary,2);
// annual / employer / total
document.getElementById('tileAnnual').textContent = fmtPlain(p.annualSalary,0) + ' €';
document.getElementById('tileEmployer').textContent = fmtPlain(p.annualWithEmployer,0) + ' €';
document.getElementById('tileAnnualTotal').textContent = fmtPlain(p.totalCost,0) + ' €';
}
function handleBtn(){ isRunning ? pauseTimer() : startTimer(); }
function startTimer(){
isRunning=true; startTime=Date.now(); timer=setInterval(tick,100);
document.getElementById('btnLabel').textContent='Pause';
document.getElementById('btnIcon').innerHTML='<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>';
document.getElementById('costDisplay').classList.add('running');
document.getElementById('statusBadge').className='status-badge running';
document.getElementById('statusBadge').innerHTML='<span class="running-indicator active"></span> Running';
document.getElementById('resetBtn').style.display='inline-flex';
document.getElementById('infoGrid').style.display='grid';
document.getElementById('devCount').disabled=true; document.getElementById('avgSalary').disabled=true;
tick();
}
function pauseTimer(){
isRunning=false; pausedElapsed += (Date.now()-startTime)/1000; clearInterval(timer); timer=null;
document.getElementById('btnLabel').textContent='Resume';
document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>';
document.getElementById('costDisplay').classList.remove('running');
document.getElementById('statusBadge').className='status-badge stopped';
document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Paused';
}
function resetTimer(){ if(timer) clearInterval(timer); timer=null; isRunning=false; pausedElapsed=0; startTime=null; document.getElementById('costValue').textContent='0.00'; document.getElementById('elapsedDisplay').textContent='Not started'; document.getElementById('btnLabel').textContent='Start'; document.getElementById('btnIcon').innerHTML='<polygon points="5,3 19,12 5,21"/>'; document.getElementById('costDisplay').classList.remove('running'); document.getElementById('statusBadge').className='status-badge'; document.getElementById('statusBadge').innerHTML='<span class="running-indicator"></span> Ready'; document.getElementById('resetBtn').style.display='none'; document.getElementById('infoGrid').style.display='none'; document.getElementById('devCount').disabled=false; document.getElementById('avgSalary').disabled=false; }
// input listeners: update per-second display when editing
document.getElementById('devCount').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
document.getElementById('avgSalary').addEventListener('input', function(){ if(!isRunning){ var p=getParams(); if(p.costPerSecond>0) document.getElementById('tilePerSec').textContent='EUR '+p.costPerSecond.toLocaleString('en-GB',{minimumFractionDigits:4,maximumFractionDigits:4}); }});
// No auto-start: user must press Start.
</script>
</body>
</html>
+381
View File
@@ -0,0 +1,381 @@
<!DOCTYPE html>
<html lang="de" data-theme="dark">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Meeting Kostenuhr</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300..700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
<style>
:root, [data-theme="light"] {
--color-bg: #f7f6f2; --color-surface: #f9f8f5; --color-surface-2: #fbfbf9;
--color-surface-offset: #f3f0ec; --color-divider: #dcd9d5; --color-border: #d4d1ca;
--color-text: #28251d; --color-text-muted: #7a7974; --color-text-faint: #bab9b4;
--color-primary: #01696f; --color-primary-hover: #0c4e54;
--color-warning: #964219; --color-error: #a12c7b;
--shadow-sm: 0 1px 2px oklch(0.2 0.01 80 / 0.06);
--shadow-md: 0 4px 12px oklch(0.2 0.01 80 / 0.08);
}
[data-theme="dark"] {
--color-bg: #0f0e0d; --color-surface: #161513; --color-surface-2: #1c1b19;
--color-surface-offset: #222120; --color-divider: #2a2927; --color-border: #333230;
--color-text: #cdccca; --color-text-muted: #797876; --color-text-faint: #4a4948;
--color-primary: #4f98a3; --color-primary-hover: #6ab8c3;
--color-warning: #e8954a; --color-error: #d163a7;
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.25);
--shadow-md: 0 4px 12px oklch(0 0 0 / 0.35);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--color-bg:#0f0e0d; --color-surface:#161513; --color-surface-2:#1c1b19;
--color-surface-offset:#222120; --color-divider:#2a2927; --color-border:#333230;
--color-text:#cdccca; --color-text-muted:#797876; --color-text-faint:#4a4948;
--color-primary:#4f98a3; --color-primary-hover:#6ab8c3;
--color-warning:#e8954a; --color-error:#d163a7;
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.25);
--shadow-md: 0 4px 12px oklch(0 0 0 / 0.35);
}
}
:root {
--radius-sm: 0.375rem; --radius-md: 0.5rem; --radius-lg: 0.75rem;
--radius-xl: 1rem; --radius-full: 9999px;
--tr: 180ms cubic-bezier(0.16, 1, 0.3, 1);
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.8rem + 0.35vw, 1rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--font-body: 'Inter', 'Helvetica Neue', sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; }
body {
min-height: 100dvh;
font-family: var(--font-body); font-size: var(--text-base);
color: var(--color-text); background-color: var(--color-bg);
display: flex; flex-direction: column; align-items: center;
padding: 2rem 1rem;
transition: background-color var(--tr), color var(--tr);
}
input, button, select { font: inherit; color: inherit; }
button { cursor: pointer; background: none; border: none; }
.header {
width: 100%; max-width: 680px;
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 2.5rem;
}
.logo { display: flex; align-items: center; gap: 0.75rem; }
.logo-icon { width: 32px; height: 32px; color: var(--color-primary); }
.logo-text { font-size: var(--text-base); font-weight: 600; letter-spacing: -0.01em; }
.theme-toggle {
width: 36px; height: 36px; border-radius: var(--radius-md);
display: flex; align-items: center; justify-content: center;
color: var(--color-text-muted);
transition: color var(--tr), background var(--tr);
}
.theme-toggle:hover { color: var(--color-text); background: var(--color-surface-offset); }
.card {
width: 100%; max-width: 680px;
background: var(--color-surface); border: 1px solid var(--color-border);
border-radius: var(--radius-xl); padding: 2rem;
box-shadow: var(--shadow-md);
transition: background var(--tr), border-color var(--tr);
}
.controls {
display: grid; grid-template-columns: 1fr 1fr auto;
gap: 1rem; align-items: flex-end; margin-bottom: 2rem;
}
@media (max-width: 540px) {
.controls { grid-template-columns: 1fr 1fr; }
.start-btn { grid-column: 1 / -1; }
}
.field { display: flex; flex-direction: column; gap: 0.5rem; }
label {
font-size: var(--text-xs); font-weight: 500;
text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted);
}
.input-wrapper { position: relative; }
.input-wrapper .unit {
position: absolute; right: 1rem; top: 50%; transform: translateY(-50%);
font-size: var(--text-sm); color: var(--color-text-muted); pointer-events: none;
}
input[type="number"] {
width: 100%; padding: 0.75rem 1rem;
background: var(--color-surface-2); border: 1px solid var(--color-border);
border-radius: var(--radius-md); font-size: var(--text-base); color: var(--color-text);
outline: none;
transition: border-color var(--tr), box-shadow var(--tr);
-moz-appearance: textfield; appearance: textfield;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; }
input[type="number"]:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px color-mix(in oklch, var(--color-primary) 20%, transparent);
}
input[type="number"].has-unit { padding-right: 3rem; }
input:disabled { opacity: 0.5; }
.start-btn {
padding: 0.75rem 1.5rem;
background: var(--color-primary); color: #fff;
border-radius: var(--radius-md); font-size: var(--text-sm); font-weight: 600;
height: 44px; white-space: nowrap;
transition: background var(--tr), transform var(--tr);
box-shadow: var(--shadow-sm); border: 1px solid transparent;
}
.start-btn:hover { background: var(--color-primary-hover); }
.start-btn:active { transform: scale(0.98); }
.start-btn.running {
background: transparent; border-color: var(--color-error); color: var(--color-error);
}
.start-btn.running:hover { background: color-mix(in oklch, var(--color-error) 10%, transparent); }
.meta-row { display: flex; gap: 1.5rem; margin-bottom: 1.5rem; flex-wrap: wrap; }
.meta-item { display: flex; flex-direction: column; gap: 0.25rem; }
.meta-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-faint); font-weight: 500; }
.meta-value { font-size: var(--text-sm); font-family: var(--font-mono); color: var(--color-text-muted); }
.divider { height: 1px; background: var(--color-divider); margin-bottom: 2rem; }
.cost-display {
text-align: center; padding: 2.5rem 1.5rem;
background: var(--color-surface-2); border: 1px solid var(--color-border);
border-radius: var(--radius-lg); position: relative; overflow: hidden;
margin-bottom: 1.5rem; min-height: 180px;
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem;
transition: background var(--tr);
}
.cost-display::before {
content: ''; position: absolute; inset: 0;
background: radial-gradient(ellipse at 50% 0%, color-mix(in oklch, var(--color-primary) 8%, transparent) 0%, transparent 70%);
pointer-events: none; opacity: 0; transition: opacity 0.4s ease;
}
.cost-display.running::before { opacity: 1; }
.cost-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.1em; color: var(--color-text-faint); font-weight: 500; }
.cost-amount {
font-family: var(--font-mono);
font-size: clamp(2.5rem, 5vw + 1rem, 5rem);
font-weight: 700; color: var(--color-text);
line-height: 1; letter-spacing: -0.02em;
transition: color 0.3s ease; font-variant-numeric: tabular-nums;
}
.cost-amount.running { color: var(--color-primary); }
.cost-amount .currency {
font-size: 0.45em; font-weight: 400; vertical-align: 0.15em;
color: var(--color-text-muted); margin-right: 0.1em;
}
.cost-sub { font-size: var(--text-xs); color: var(--color-text-faint); font-family: var(--font-mono); }
.idle-hint { display: flex; flex-direction: column; align-items: center; gap: 0.75rem; color: var(--color-text-faint); }
.idle-hint svg { width: 32px; height: 32px; opacity: 0.4; }
.idle-hint p { font-size: var(--text-sm); max-width: 36ch; }
.rate-row {
display: flex; align-items: center; justify-content: center; gap: 0.75rem;
padding: 1rem 1.5rem;
background: var(--color-surface-offset); border: 1px solid var(--color-border);
border-radius: var(--radius-md);
}
.rate-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-muted); font-weight: 500; }
.rate-value { font-family: var(--font-mono); font-size: var(--text-base); font-weight: 700; color: var(--color-warning); font-variant-numeric: tabular-nums; }
.rate-dot { width: 6px; height: 6px; border-radius: var(--radius-full); background: var(--color-text-faint); flex-shrink: 0; }
.rate-dot.pulse { background: var(--color-warning); animation: pulse 1s ease-in-out infinite; }
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.7); }
}
.timer-row {
display: flex; justify-content: space-between; align-items: center;
margin-top: 1.5rem; padding-top: 1rem; border-top: 1px solid var(--color-divider);
}
.timer-label { font-size: var(--text-xs); text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-text-faint); font-weight: 500; }
.timer-value { font-family: var(--font-mono); font-size: var(--text-sm); color: var(--color-text-muted); font-variant-numeric: tabular-nums; }
:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 3px; border-radius: var(--radius-sm); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; }
}
</style>
</head>
<body>
<header class="header">
<div class="logo">
<svg class="logo-icon" viewBox="0 0 32 32" fill="none" aria-label="Meeting Kostenuhr">
<circle cx="16" cy="16" r="13" stroke="currentColor" stroke-width="1.5"/>
<path d="M16 8v8l5 3" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span class="logo-text">Meeting Kostenuhr</span>
</div>
<button class="theme-toggle" data-theme-toggle aria-label="Erscheinungsbild wechseln">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
</svg>
</button>
</header>
<main>
<div class="card">
<div class="controls">
<div class="field">
<label for="developers">Anwesende Entwickler</label>
<input type="number" id="developers" min="1" max="200" value="5" placeholder="z.B. 8">
</div>
<div class="field">
<label for="salary">Durchschnittsgehalt</label>
<div class="input-wrapper">
<input type="number" id="salary" min="1000" max="50000" value="5000" class="has-unit" placeholder="5000">
<span class="unit">&#8364;/Mo</span>
</div>
</div>
<button class="start-btn" id="startBtn" aria-label="Timer starten" onclick="toggleTimer()">
&#9654; Start
</button>
</div>
<div class="meta-row" id="metaRow" style="display:none">
<div class="meta-item">
<span class="meta-label">Jahresgehalt gesamt</span>
<span class="meta-value" id="metaJahres">-</span>
</div>
<div class="meta-item">
<span class="meta-label">inkl. Arbeitgeberanteil (&#215;1,2)</span>
<span class="meta-value" id="metaAG">-</span>
</div>
<div class="meta-item">
<span class="meta-label">Gesamtkosten / Jahr</span>
<span class="meta-value" id="metaGesamt">-</span>
</div>
</div>
<div class="divider" id="metaDivider" style="display:none"></div>
<div class="cost-display" id="costDisplay">
<div class="idle-hint" id="idleHint">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<circle cx="12" cy="12" r="10"/>
<path d="M12 6v6l4 2"/>
</svg>
<p>Entwickler-Anzahl und Gehalt eingeben, dann Start dr&#252;cken</p>
</div>
</div>
<div class="rate-row" id="rateRow" style="opacity:0.4; pointer-events:none">
<div class="rate-dot" id="rateDot"></div>
<span class="rate-label">Kosten pro Sekunde</span>
<span class="rate-value" id="rateValue">&#8212;</span>
</div>
<div class="timer-row" id="timerRow" style="display:none">
<span class="timer-label">Meeting-Dauer</span>
<span class="timer-value" id="timerValue">00:00:00</span>
</div>
</div>
</main>
<script>
(function(){
var t = document.querySelector('[data-theme-toggle]');
var r = document.documentElement;
var d = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
r.setAttribute('data-theme', d);
setIcon(t, d);
if(t) t.addEventListener('click', function(){
d = d==='dark' ? 'light' : 'dark';
r.setAttribute('data-theme', d);
setIcon(t, d);
});
function setIcon(b, mode){
if(!b) return;
if(mode==='dark'){
b.innerHTML='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"/></svg>';
} else {
b.innerHTML='<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>';
}
}
})();
var interval=null, startTime=null, elapsed=0, rate=0, running=false;
function fmt(v,d){ return v.toLocaleString('de-DE',{minimumFractionDigits:d,maximumFractionDigits:d}); }
function fmtT(s){ var h=Math.floor(s/3600),m=Math.floor(s%3600/60),ss=s%60; return [h,m,ss].map(function(v){return String(v).padStart(2,'0');}).join(':'); }
function calc(){
var dev=parseInt(document.getElementById('developers').value)||0;
var sal=parseFloat(document.getElementById('salary').value)||0;
var jg=sal*12, ag=jg*1.2, ges=ag*dev, ps=ges/220/8/3600;
return {dev:dev,sal:sal,jg:jg,ag:ag,ges:ges,ps:ps};
}
function render(cost){
var cd=document.getElementById('costDisplay');
var hint=document.getElementById('idleHint');
if(hint) hint.style.display='none';
var lbl=document.getElementById('cLbl'), amt=document.getElementById('cAmt'), sub=document.getElementById('cSub');
if(!amt){
lbl=document.createElement('span'); lbl.className='cost-label'; lbl.id='cLbl'; lbl.textContent='Bisher verbraucht';
amt=document.createElement('div'); amt.className='cost-amount'; amt.id='cAmt';
sub=document.createElement('span'); sub.className='cost-sub'; sub.id='cSub';
cd.appendChild(lbl); cd.appendChild(amt); cd.appendChild(sub);
}
amt.innerHTML='<span class="currency">\u20ac</span>'+fmt(cost,2);
amt.className='cost-amount'+(running?' running':'');
sub.textContent=elapsed+' Sek. \u00d7 '+fmt(rate,4)+' \u20ac/s';
}
function showMeta(c){
document.getElementById('metaRow').style.display='';
document.getElementById('metaDivider').style.display='';
document.getElementById('metaJahres').textContent=fmt(c.jg,0)+' \u20ac';
document.getElementById('metaAG').textContent=fmt(c.ag,0)+' \u20ac';
document.getElementById('metaGesamt').textContent=fmt(c.ges,0)+' \u20ac';
}
function toggleTimer(){ if(!running) startTimer(); else stopTimer(); }
function startTimer(){
var c=calc();
if(c.dev<1||c.sal<1){ alert('Bitte Werte eingeben.'); return; }
rate=c.ps; running=true; elapsed=0; startTime=Date.now();
var btn=document.getElementById('startBtn');
btn.innerHTML='\u23f9 Stop'; btn.classList.add('running');
document.getElementById('costDisplay').classList.add('running');
var rr=document.getElementById('rateRow');
rr.style.opacity='1'; rr.style.pointerEvents='';
document.getElementById('rateDot').classList.add('pulse');
document.getElementById('rateValue').textContent=fmt(c.ps,4)+' \u20ac';
document.getElementById('timerRow').style.display='';
document.getElementById('developers').disabled=true;
document.getElementById('salary').disabled=true;
showMeta(c); render(0);
interval=setInterval(function(){
elapsed=Math.floor((Date.now()-startTime)/1000);
render(rate*elapsed);
document.getElementById('timerValue').textContent=fmtT(elapsed);
},200);
}
function stopTimer(){
clearInterval(interval); interval=null; running=false;
var btn=document.getElementById('startBtn');
btn.innerHTML='\u25b6 Start'; btn.classList.remove('running');
document.getElementById('costDisplay').classList.remove('running');
document.getElementById('rateDot').classList.remove('pulse');
document.getElementById('developers').disabled=false;
document.getElementById('salary').disabled=false;
render(rate*elapsed);
}
['developers','salary'].forEach(function(id){
document.getElementById(id).addEventListener('input',function(){
if(!running){ var c=calc(); if(c.ps>0){ document.getElementById('rateValue').textContent=fmt(c.ps,4)+' \u20ac'; document.getElementById('rateRow').style.opacity='1'; } }
});
});
</script>
</body>
</html>
+46
View File
@@ -0,0 +1,46 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Meeting Cost Tracker (EN)</title>
<style>
/* Minimal styles to match variant2 look */
body{font-family:Inter,system-ui,sans-serif;background:#f7f6f2;color:#28251d;padding:40px}
.card{max-width:900px;margin:0 auto;background:#fff;padding:20px;border-radius:12px}
.label{font-size:12px;color:#6b7280}
.big{font-family:monospace;font-size:48px}
</style>
</head>
<body>
<div class="card">
<h1>Meeting Cost Tracker</h1>
<div>
<label class="label">Developers present</label>
<input id="devCount" type="number" value="5" min="1" />
</div>
<div>
<label class="label">Avg monthly gross salary (€)</label>
<input id="avgSalary" type="number" value="5000" min="1" />
</div>
<div style="margin-top:12px">
<button id="startBtn">Start</button>
<button id="resetBtn">Reset</button>
</div>
<hr/>
<div>
<div class="label">Total cost so far</div>
<div id="total" class="big">0,00 €</div>
<div class="label">Cost per second: <span id="perSec">0,00 €</span></div>
</div>
</div>
<script>
/* Simple duplicate of variant2 calculations in English */
function fmt(n){return n.toLocaleString('de-DE',{minimumFractionDigits:2,maximumFractionDigits:2})+' €'}
function calc(){var d=Number(document.getElementById('devCount').value)||1;var s=Number(document.getElementById('avgSalary').value)||5000;var annual=s*12;var withEmp=annual*1.2;var total=withEmp*d;var perSec=total/220/8/3600;return{d:d,s:s,annual:annual,withEmp:withEmp,total:total,perSec:perSec}}
var timer=null, acc=0;
document.getElementById('startBtn').addEventListener('click',function(){ if(timer) return; var p=calc(); timer=setInterval(function(){acc+=p.perSec;document.getElementById('total').textContent=fmt(acc);},1000); document.getElementById('perSec').textContent=fmt(calc().perSec);});
document.getElementById('resetBtn').addEventListener('click',function(){ clearInterval(timer);timer=null;acc=0;document.getElementById('total').textContent=fmt(0);});
</script>
</body>
</html>
+234
View File
@@ -0,0 +1,234 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Meeting Cost Tracker</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#0b0f14;
--panel:#0f1620;
--muted:#9aa6b2;
--accent:#00d4ff;
--danger:#ff6b6b;
--glass: rgba(255,255,255,0.03);
}
html,body{height:100%;}
body{
margin:0;padding:40px;box-sizing:border-box;
font-family:Inter, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial;
background:linear-gradient(180deg,#06070a 0%, #081018 60%); color:#e6eef6;
-webkit-font-smoothing:antialiased;
}
.wrap{max-width:1100px;margin:0 auto;display:grid;grid-template-columns:1fr;gap:28px}
.panel{background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01)); padding:22px;border-radius:12px;box-shadow:0 6px 30px rgba(0,0,0,0.6);}
.controls{display:flex;gap:18px;align-items:end;flex-wrap:wrap}
.field{display:flex;flex-direction:column;gap:6px}
label{font-size:13px;color:var(--muted)}
input[type=number]{
background:var(--panel);border:1px solid rgba(255,255,255,0.04);padding:10px 12px;border-radius:8px;color:inherit;font-size:16px;width:220px
}
.small{width:150px}
.row-actions{display:flex;gap:10px}
button{cursor:pointer;border:0;padding:10px 16px;border-radius:9px;font-weight:600}
button.start{background:linear-gradient(90deg,var(--accent),#6ee7b7);color:#042028}
button.start[disabled]{opacity:0.45;cursor:not-allowed}
button.reset{background:transparent;border:1px solid rgba(255,255,255,0.06);color:var(--muted)}
.big-display{display:flex;align-items:center;gap:20px;padding:28px;border-radius:12px}
.amount{font-weight:800;font-size:86px;line-height:1;color:#fff;letter-spacing:-2px}
.meta{display:flex;flex-direction:column;gap:8px}
.label{color:var(--muted);font-size:15px}
.per-sec{font-size:20px;color:var(--accent);font-weight:700}
.status{display:flex;align-items:center;gap:12px}
.pulse{width:14px;height:14px;border-radius:999px;background:#17343b;box-shadow:0 0 0 4px rgba(0,0,0,0.2);position:relative}
.pulse.active{background:var(--accent);box-shadow:0 0 20px rgba(0,212,255,0.18);}
.pulse.active::after{content:"";position:absolute;inset:0;border-radius:999px;animation:ping 1.2s infinite}
@keyframes ping{0%{transform:scale(1);opacity:0.9}50%{transform:scale(1.9);opacity:0.22}100%{transform:scale(2.6);opacity:0}}
.errors{color:var(--danger);font-size:13px;margin-top:6px}
/* layout for 1080p wide */
@media(min-width:900px){
.wrap{grid-template-columns:1fr}
.big-display{background:var(--glass);justify-content:space-between}
.meta{align-items:flex-end;text-align:right}
}
footer{color:var(--muted);font-size:13px;padding-top:8px}
</style>
</head>
<body>
<div class="wrap">
<div class="panel controls" style="align-items:flex-start">
<div class="field">
<label for="devs">Developers present</label>
<input id="devs" type="number" min="1" step="1" placeholder="Anzahl Entwickler" aria-label="Developers present">
<div id="devs-error" class="errors" aria-live="polite" style="display:none"></div>
</div>
<div class="field">
<label for="salary">Avg. monthly gross salary (€)</label>
<input id="salary" class="small" type="number" min="0.01" step="0.01" value="5000" aria-label="Avg monthly salary">
<div id="salary-error" class="errors" aria-live="polite" style="display:none"></div>
</div>
<div class="row-actions" style="margin-left:6px">
<button id="startBtn" class="start">Start</button>
<button id="resetBtn" class="reset">Reset</button>
</div>
<div style="flex:1"></div>
<div class="status" style="margin-top:6px">
<div id="dot" class="pulse" title="Counter status"></div>
<div style="color:var(--muted);font-size:13px">Counter status</div>
</div>
</div>
<div class="panel big-display">
<div>
<div class="label">Total cost so far</div>
<div id="total" class="amount">0,00 €</div>
</div>
<div class="meta">
<div class="label">Cost per second</div>
<div id="perSecond" class="per-sec">0,00 €</div>
<footer>Using 220 working days · employer contributions ×1.2</footer>
</div>
</div>
</div>
<script>
(function(){
const devsInput = document.getElementById('devs');
const salaryInput = document.getElementById('salary');
const startBtn = document.getElementById('startBtn');
const resetBtn = document.getElementById('resetBtn');
const totalEl = document.getElementById('total');
const perSecEl = document.getElementById('perSecond');
const devsErr = document.getElementById('devs-error');
const salErr = document.getElementById('salary-error');
const dot = document.getElementById('dot');
const fmt = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR', minimumFractionDigits:2, maximumFractionDigits:2 });
const WORKING_SECONDS_PER_YEAR = 220 * 8 * 60 * 60; // 220 days * 8 hours * 3600 sec
let intervalId = null;
let accumulated = 0;
let costPerSecond = 0;
function parseInputs(){
const devsRaw = devsInput.value.trim();
const salaryRaw = salaryInput.value.trim();
const devs = devsRaw === '' ? NaN : Number(devsRaw);
const salary = salaryRaw === '' ? NaN : Number(salaryRaw);
return {devs, salary};
}
function validateBeforeStart(){
const {devs, salary} = parseInputs();
let ok = true;
devsErr.style.display = 'none'; salErr.style.display = 'none';
if (!Number.isInteger(devs) || devs < 1){
devsErr.textContent = 'Please enter a whole number ≥ 1'; devsErr.style.display = 'block'; ok=false;
}
if (!(typeof salary === 'number') || Number.isNaN(salary) || salary <= 0){
salErr.textContent = 'Monthly salary must be > 0'; salErr.style.display = 'block'; ok=false;
}
return ok;
}
function recalc(){
const {devs, salary} = parseInputs();
// Clear inline errors while editing
devsErr.style.display = 'none'; salErr.style.display = 'none';
if (!Number.isFinite(devs) || !Number.isFinite(salary) || devs <= 0 || salary <= 0){
costPerSecond = 0;
perSecEl.textContent = fmt.format(0);
return;
}
const annual = salary * 12;
const withContrib = annual * 1.2;
const totalAnnual = withContrib * devs;
costPerSecond = totalAnnual / WORKING_SECONDS_PER_YEAR;
perSecEl.textContent = fmt.format(costPerSecond);
}
function tick(){
// add current costPerSecond (which may have changed) and update display
accumulated += costPerSecond;
totalEl.textContent = fmt.format(accumulated);
}
function start(){
if (intervalId) return;
if (!validateBeforeStart()) return;
// ensure latest calc applied
recalc();
intervalId = setInterval(tick, 1000);
startBtn.disabled = true;
dot.classList.add('active');
// update UI immediately (don't wait 1s for first tick)
totalEl.textContent = fmt.format(accumulated);
}
function reset(){
if (intervalId){
clearInterval(intervalId); intervalId = null;
}
accumulated = 0;
totalEl.textContent = fmt.format(0);
startBtn.disabled = false;
dot.classList.remove('active');
}
// wire events
startBtn.addEventListener('click', start);
resetBtn.addEventListener('click', reset);
devsInput.addEventListener('input', function(){
// If user types non-integer, show a gentle inline message but don't block running counter
const v = devsInput.value.trim();
if (v === ''){
devsErr.textContent = 'Required'; devsErr.style.display='block';
} else if (!Number.isInteger(Number(v)) || Number(v) < 1){
devsErr.textContent = 'Must be a whole number ≥ 1'; devsErr.style.display='block';
} else {
devsErr.style.display='none';
}
recalc();
});
salaryInput.addEventListener('input', function(){
const v = salaryInput.value.trim();
if (v === '' || Number(v) <= 0){
salErr.textContent = 'Must be > 0'; salErr.style.display='block';
} else {
salErr.style.display='none';
}
recalc();
});
// initialize displays
perSecEl.textContent = fmt.format(0);
totalEl.textContent = fmt.format(0);
// compute initial per-second using prefilled salary (devs empty so stays 0)
recalc();
// Keyboard: allow Enter on salary or devs to start
[devsInput, salaryInput].forEach(el=>{
el.addEventListener('keydown', (e)=>{ if(e.key === 'Enter'){ start(); } });
});
})();
</script>
</body>
</html>
+288
View File
@@ -0,0 +1,288 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Software-Entwickler Kostenzähler</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
font-family: Arial, Helvetica, sans-serif;
background: #f4f6f8;
color: #1f2937;
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
}
.container {
width: 100%;
max-width: 900px;
background: #ffffff;
border-radius: 18px;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08);
padding: 32px;
}
h1 {
margin: 0 0 8px;
font-size: 32px;
text-align: center;
}
.subtitle {
margin: 0 0 32px;
text-align: center;
color: #6b7280;
font-size: 16px;
}
.inputs {
display: grid;
grid-template-columns: 1fr 1fr auto;
gap: 16px;
align-items: end;
margin-bottom: 32px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 700;
}
input {
width: 100%;
padding: 14px 16px;
border: 1px solid #d1d5db;
border-radius: 12px;
font-size: 18px;
}
button {
padding: 15px 28px;
border: none;
border-radius: 12px;
background: #2563eb;
color: #ffffff;
font-size: 18px;
font-weight: 700;
cursor: pointer;
transition: background 0.2s ease;
}
button:hover {
background: #1d4ed8;
}
.counter-box {
background: #111827;
color: #ffffff;
border-radius: 18px;
padding: 36px 24px;
text-align: center;
margin-bottom: 24px;
}
.counter-label {
font-size: 18px;
color: #d1d5db;
margin-bottom: 12px;
}
.counter-value {
font-size: clamp(42px, 8vw, 84px);
font-weight: 800;
letter-spacing: -2px;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.metric {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 14px;
padding: 18px;
}
.metric-title {
color: #6b7280;
font-size: 14px;
margin-bottom: 8px;
}
.metric-value {
font-size: 22px;
font-weight: 800;
}
.formula {
background: #f9fafb;
border-left: 5px solid #2563eb;
border-radius: 12px;
padding: 18px 20px;
color: #374151;
line-height: 1.6;
font-size: 15px;
}
.error {
display: none;
margin-bottom: 20px;
padding: 14px 16px;
border-radius: 12px;
background: #fee2e2;
color: #991b1b;
font-weight: 700;
}
@media (max-width: 760px) {
.inputs,
.metrics {
grid-template-columns: 1fr;
}
button {
width: 100%;
}
}
</style>
</head>
<body>
<main class="container">
<h1>Software-Entwickler Kostenzähler</h1>
<p class="subtitle">Live-Anzeige der laufenden Kosten während einer Besprechung oder Wartezeit.</p>
<section class="inputs">
<div>
<label for="softwareEntwicklerAnwesend">Software-Entwickler anwesend</label>
<input id="softwareEntwicklerAnwesend" type="number" min="0" step="1" value="1" />
</div>
<div>
<label for="durchschnittsgehalt">Durchschnittsgehalt pro Monat (€)</label>
<input id="durchschnittsgehalt" type="number" min="0" step="100" value="5000" />
</div>
<button id="startButton">Start</button>
</section>
<div id="error" class="error">Bitte gültige Werte größer als 0 eingeben.</div>
<section class="counter-box">
<div class="counter-label">Bisher verbraucht</div>
<div id="verbrauch" class="counter-value">0,00 €</div>
</section>
<section class="metrics">
<div class="metric">
<div class="metric-title">Kosten pro Sekunde</div>
<div id="kostenProSekunde" class="metric-value">0,00 €</div>
</div>
<div class="metric">
<div class="metric-title">Gesamtkosten pro Jahr</div>
<div id="gesamtkosten" class="metric-value">0,00 €</div>
</div>
<div class="metric">
<div class="metric-title">Laufzeit</div>
<div id="laufzeit" class="metric-value">0 s</div>
</div>
</section>
<section class="formula">
<strong>Berechnung:</strong><br />
Jahresgehalt = Durchschnittsgehalt × 12<br />
Jahresgehalt mit Arbeitgeberanteil = Jahresgehalt × 1,2<br />
Gesamtkosten = Jahresgehalt mit Arbeitgeberanteil × Software-Entwickler anwesend<br />
Kosten pro Sekunde = Gesamtkosten ÷ 365 Tage ÷ 8 Stunden ÷ 60 Minuten ÷ 60 Sekunden
</section>
</main>
<script>
const softwareEntwicklerInput = document.getElementById('softwareEntwicklerAnwesend');
const durchschnittsgehaltInput = document.getElementById('durchschnittsgehalt');
const startButton = document.getElementById('startButton');
const verbrauchElement = document.getElementById('verbrauch');
const kostenProSekundeElement = document.getElementById('kostenProSekunde');
const gesamtkostenElement = document.getElementById('gesamtkosten');
const laufzeitElement = document.getElementById('laufzeit');
const errorElement = document.getElementById('error');
let intervalId = null;
let startZeit = null;
let kostenProSekunde = 0;
const euroFormatter = new Intl.NumberFormat('de-DE', {
style: 'currency',
currency: 'EUR',
minimumFractionDigits: 2,
maximumFractionDigits: 2
});
function berechneKosten() {
const softwareEntwicklerAnwesend = Number(softwareEntwicklerInput.value);
const durchschnittsgehalt = Number(durchschnittsgehaltInput.value);
if (softwareEntwicklerAnwesend <= 0 || durchschnittsgehalt <= 0) {
return null;
}
const jahresgehalt = durchschnittsgehalt * 12;
const jahresgehaltMitArbeitgeberanteil = jahresgehalt * 1.2;
const gesamtkosten = jahresgehaltMitArbeitgeberanteil * softwareEntwicklerAnwesend;
const kostenProSekunde = gesamtkosten / 365 / 8 / 60 / 60;
return {
gesamtkosten,
kostenProSekunde
};
}
function aktualisiereAnzeige() {
const jetzt = new Date();
const vergangeneSekunden = Math.floor((jetzt - startZeit) / 1000);
const verbrauch = vergangeneSekunden * kostenProSekunde;
verbrauchElement.textContent = euroFormatter.format(verbrauch);
laufzeitElement.textContent = `${vergangeneSekunden} s`;
}
startButton.addEventListener('click', () => {
const kosten = berechneKosten();
if (!kosten) {
errorElement.style.display = 'block';
return;
}
errorElement.style.display = 'none';
if (intervalId) {
clearInterval(intervalId);
}
kostenProSekunde = kosten.kostenProSekunde;
startZeit = new Date();
kostenProSekundeElement.textContent = euroFormatter.format(kostenProSekunde);
gesamtkostenElement.textContent = euroFormatter.format(kosten.gesamtkosten);
verbrauchElement.textContent = euroFormatter.format(0);
laufzeitElement.textContent = '0 s';
aktualisiereAnzeige();
intervalId = setInterval(aktualisiereAnzeige, 1000);
});
</script>
</body>
</html>