mirror of
https://github.com/skoelle/PeopleCostCounter.git
synced 2026-09-17 20:00:25 +00:00
171 lines
14 KiB
HTML
171 lines
14 KiB
HTML
<!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>
|