hiring_calculator/index.html

1310 lines
No EOL
48 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>OLIVER Hiring Calculator</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!--
MODIFICATIONS (v5 - Admin Panel Restored):
- Re-added the Admin Panel button to the header.
- Re-added the Admin Panel section to the HTML body.
- Re-implemented the JavaScript logic for the Admin Panel.
- Admin functions (add, update, delete) now modify the country list in the current browser session.
- NOTE: Changes made in the Admin Panel will be lost upon page refresh as this is a local file.
-->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;600;700&display=swap" rel="stylesheet" />
<script src="https://alcdn.msauth.net/browser/2.30.0/js/msal-browser.min.js"></script>
<style>
:root {
--primary: #2563eb;
--primary-hover: #1d4ed8;
--bg: #f8fafc;
--surface: #ffffff;
--text: #1e293b;
--border: #e2e8f0;
--success: #10b981;
--danger: #ef4444;
--radius: 8px;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
/* Brand Colors */
--orange: #f25022;
--black: #000000;
--white: #ffffff;
--gold: #ffb900;
--grey: #f3f2f1;
--focus: #0078d4;
--error: #d13438;
}
/* Login Overlay */
#login-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg);
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.login-btn {
background: #f25022;
color: white;
padding: 12px 24px;
font-size: 1.2rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
display: flex;
align-items: center;
gap: 10px;
}
.login-btn:hover {
background: #d04010;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Inter', sans-serif;
}
html,
body {
margin: 0;
padding: 0;
font-family: Montserrat, Arial, sans-serif;
background: #fff;
color: #000
}
header {
background: var(--black);
color: var(--white);
display: flex;
align-items: center;
gap: 16px;
padding: 12px 16px;
position: sticky;
top: 0;
z-index: 10;
}
header img {
height: 28px;
width: auto
}
header h1 {
font-size: 24px;
font-weight: 700;
margin: 0;
flex: 1
}
header .admin-btn {
background: var(--gold);
color: #000;
border: none;
padding: 8px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
#container {
max-width: 1100px;
margin: 0 auto;
padding: 16px
}
.tabs {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 16px
}
.tab {
background: var(--gold);
color: #000;
border: none;
padding: 10px 12px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.tab.active {
background: var(--orange);
color: #fff;
font-weight: 700
}
.panel {
display: none;
border: 1px solid var(--border);
padding: 16px;
border-radius: 6px;
background: #fff
}
.panel.active {
display: block
}
.grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: 12px
}
.field {
grid-column: span 6;
display: flex;
flex-direction: column
}
.field label {
font-size: 14px;
font-weight: 600;
margin-bottom: 6px
}
.field input,
.field select {
padding: 10px 10px;
border: 1px solid var(--border);
border-radius: 4px;
font-size: 14px;
color: #000;
background: #fff;
outline: none;
}
.field input:focus,
.field select:focus {
border-color: var(--focus);
box-shadow: 0 0 0 2px rgba(26, 115, 232, 0.15)
}
.hint {
font-size: 12px;
color: #555
}
.actions {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap
}
.btn-primary {
background: var(--orange);
color: #fff;
border: none;
padding: 12px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 700;
font-size: 16px;
}
.btn-secondary {
background: #fff;
color: #000;
border: 1px solid var(--border);
padding: 10px 16px;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
}
.btn-primary[disabled],
.btn-secondary[disabled] {
opacity: 0.6;
cursor: not-allowed
}
.results {
margin-top: 16px;
background: var(--grey);
border-radius: 6px;
padding: 16px;
border: 1px solid var(--border)
}
.results h3 {
margin: 0 0 8px 0
}
.res-row {
display: flex;
flex-wrap: wrap;
gap: 16px
}
.res-item {
flex: 1 1 220px;
background: #fff;
border-radius: 4px;
border: 1px solid var(--border);
padding: 12px
}
.res-item .label {
font-size: 12px;
color: #555
}
.res-item .value {
font-size: 18px;
font-weight: 700
}
.res-highlight {
background: var(--gold);
color: #000
}
.margin-value {
color: #000;
font-size: 18px;
font-weight: 700
}
.error {
color: var(--error);
font-weight: 600;
margin-top: 6px
}
#toast {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: var(--orange);
color: #fff;
padding: 12px 16px;
border-radius: 4px;
font-weight: 700;
font-size: 16px;
display: none
}
#adminPanel {
display: none;
border: 2px solid var(--black);
padding: 16px;
margin-top: 16px;
border-radius: 6px;
background: #fff;
}
#adminPanel h2 {
margin-top: 0
}
.muted {
color: #666;
font-size: 12px
}
.admin-section {
border: 1px solid var(--border);
padding: 12px;
border-radius: 6px;
margin-bottom: 12px
}
@media (max-width: 800px) {
.field {
grid-column: span 12
}
}
</style>
</head>
<body>
<!-- Login Overlay -->
<div id="login-overlay">
<h1 style="margin-bottom: 2rem;">Hiring Calculator</h1>
<button id="login-btn" class="login-btn">
Log in with Microsoft
</button>
</div>
<div id="app-container" style="display:none;">
<header>
<div style="flex: 1; display: flex; justify-content: flex-start;">
<img id="logoImg" alt="OLIVER logo" />
</div>
<h1 style="flex: 1; text-align: center; margin: 0; white-space: nowrap;">Hiring Calculator</h1>
<div style="flex: 1; display: flex; justify-content: flex-end; gap: 10px;">
<button id="adminToggle" class="admin-btn" title="Open admin panel">⚙️ Admin</button>
<button id="logoutBtn" class="btn-secondary" style="padding: 8px 12px; font-size: 0.9rem;">Log out</button>
</div>
</header>
<div id="container">
<nav class="tabs" id="tabs">
<button class="tab active" data-panel="mgmt">Management Fee</button>
<button class="tab" data-panel="hourly">Project Fee (Hourly)</button>
<button class="tab" data-panel="daily">Project Fee (Daily)</button>
<button class="tab" data-panel="multiplier">Client Multiplier</button>
<button class="tab" data-panel="overhead">Overhead / Clientable</button>
<button class="tab" data-panel="mix-monthly">Mix (Monthly)</button>
<button class="tab" data-panel="mix-hourly">Mix (Hourly)</button>
<button class="tab" data-panel="mix-daily">Mix (Daily)</button>
</nav>
<!-- Management Fee -->
<section id="mgmt" class="panel active">
<div class="grid">
<div class="field"><label for="mgmt-sell">Sell Rate (Annual)</label><input type="number" id="mgmt-sell"
placeholder="e.g., 150000" /></div>
<div class="field"><label for="mgmt-buy">Buy Rate (Annual Gross Salary)</label><input type="number"
id="mgmt-buy" placeholder="e.g., 75000" /></div>
<div class="field"><label for="mgmt-country">Employment Country</label><select id="mgmt-country"></select>
</div>
<div class="field"><label for="mgmt-currency">Currency (auto-suggested)</label><select
id="mgmt-currency"></select></div>
</div>
<div class="actions"><button class="btn-primary" id="mgmt-calc">Calculate</button><button class="btn-secondary"
id="mgmt-clear">Clear</button></div>
<div class="results" id="mgmt-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="mgmt-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="mgmt-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="mgmt-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="mgmt-copy">Copy Results</button></div>
</div>
</section>
<!-- Project Fee Hourly -->
<section id="hourly" class="panel">
<div class="grid">
<div class="field"><label for="hr-sell">Sell Rate (Hourly)</label><input type="number" id="hr-sell"
placeholder="e.g., 95" /></div>
<div class="field"><label for="hr-util">Forecasted Utilisation (%)</label><input type="number" id="hr-util"
placeholder="e.g., 100" min="0" max="100" /></div>
<div class="field"><label for="hr-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="hr-buy"
placeholder="e.g., 45000" /></div>
<div class="field"><label for="hr-country">Employment Country</label><select id="hr-country"></select></div>
<div class="field"><label for="hr-currency">Currency</label><select id="hr-currency"></select></div>
</div>
<div class="actions"><button class="btn-primary" id="hr-calc">Calculate</button><button class="btn-secondary"
id="hr-clear">Clear</button></div>
<div class="results" id="hr-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="hr-annualsell">-</div>
</div>
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="hr-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="hr-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="hr-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="hr-copy">Copy Results</button></div>
</div>
</section>
<!-- Project Fee Daily -->
<section id="daily" class="panel">
<div class="grid">
<div class="field"><label for="dy-sell">Sell Rate (Daily)</label><input type="number" id="dy-sell"
placeholder="e.g., 450" /></div>
<div class="field"><label for="dy-util">Forecasted Utilisation (%)</label><input type="number" id="dy-util"
placeholder="e.g., 100" min="0" max="100" /></div>
<div class="field"><label for="dy-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="dy-buy"
placeholder="e.g., 47880" /></div>
<div class="field"><label for="dy-country">Employment Country</label><select id="dy-country"></select></div>
<div class="field"><label for="dy-currency">Currency</label><select id="dy-currency"></select></div>
</div>
<div class="actions"><button class="btn-primary" id="dy-calc">Calculate</button><button class="btn-secondary"
id="dy-clear">Clear</button></div>
<div class="results" id="dy-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="dy-annualsell">-</div>
</div>
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="dy-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="dy-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="dy-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="dy-copy">Copy Results</button></div>
</div>
</section>
<!-- Client Multiplier -->
<section id="multiplier" class="panel">
<div class="grid">
<div class="field"><label for="cm-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="cm-buy"
placeholder="e.g., 78000" /></div>
<div class="field"><label for="cm-mult">Agreed Client Multiplier</label><input type="number" id="cm-mult"
placeholder="e.g., 1.5" step="0.01" /></div>
<div class="field"><label for="cm-country">Employment Country</label><select id="cm-country"></select></div>
<div class="field"><label for="cm-currency">Currency</label><select id="cm-currency"></select></div>
</div>
<div class="actions"><button class="btn-primary" id="cm-calc">Calculate</button><button class="btn-secondary"
id="cm-clear">Clear</button></div>
<div class="results" id="cm-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="cm-ctc">-</div>
</div>
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="cm-annualsell">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="cm-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="cm-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="cm-copy">Copy Results</button></div>
</div>
</section>
<!-- Overhead / Clientable -->
<section id="overhead" class="panel">
<div class="grid">
<div class="field"><label for="oh-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="oh-buy"
placeholder="e.g., 720000" /></div>
<div class="field"><label for="oh-country">Employment Country</label><select id="oh-country"></select></div>
<div class="field"><label for="oh-currency">Currency</label><select id="oh-currency"></select></div>
</div>
<div class="actions"><button class="btn-primary" id="oh-calc">Calculate</button><button class="btn-secondary"
id="oh-clear">Clear</button></div>
<div class="results" id="oh-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="oh-ctc">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="oh-copy">Copy Results</button></div>
</div>
</section>
<!-- Mix Monthly -->
<section id="mix-monthly" class="panel">
<div class="grid">
<div class="field"><label for="mm-mgt-sell">Sell Rate - Mgt Fee (Annual)</label><input type="number"
id="mm-mgt-sell" placeholder="e.g., 147375" /></div>
<div class="field"><label for="mm-mgt-time">Time % (Mgt Fee)</label><input type="number" id="mm-mgt-time"
placeholder="e.g., 79" min="0" max="100" /></div>
<div class="field"><label for="mm-pjt-sell">Sell Rate - Pjt Fee (Monthly)</label><input type="number"
id="mm-pjt-sell" placeholder="e.g., 40103" /></div>
<div class="field"><label for="mm-pjt-time">Time % (Pjt Fee)</label><input type="number" id="mm-pjt-time"
placeholder="e.g., 21" min="0" max="100" /></div>
<div class="field"><label for="mm-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="mm-buy"
placeholder="e.g., 75000" /></div>
<div class="field"><label for="mm-country">Employment Country</label><select id="mm-country"></select></div>
<div class="field"><label for="mm-currency">Currency</label><select id="mm-currency"></select></div>
</div>
<div class="error" id="mm-error" style="display:none;">Time % must total 100%</div>
<div class="actions"><button class="btn-primary" id="mm-calc" disabled>Calculate</button><button
class="btn-secondary" id="mm-clear">Clear</button></div>
<div class="results" id="mm-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="mm-annualsell">-</div>
</div>
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="mm-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="mm-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="mm-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="mm-copy">Copy Results</button></div>
</div>
</section>
<!-- Mix Hourly -->
<section id="mix-hourly" class="panel">
<div class="grid">
<div class="field"><label for="mh-mgt-sell">Sell Rate - Mgt Fee (Annual)</label><input type="number"
id="mh-mgt-sell" placeholder="e.g., 76000" /></div>
<div class="field"><label for="mh-mgt-time">Time % (Mgt Fee)</label><input type="number" id="mh-mgt-time"
placeholder="e.g., 30" min="0" max="100" /></div>
<div class="field"><label for="mh-pjt-sell">Sell Rate - Pjt Fee (Hourly)</label><input type="number"
id="mh-pjt-sell" placeholder="e.g., 52" /></div>
<div class="field"><label for="mh-pjt-time">Time % (Pjt Fee)</label><input type="number" id="mh-pjt-time"
placeholder="e.g., 70" min="0" max="100" /></div>
<div class="field"><label for="mh-util">Forecasted Utilisation (%)</label><input type="number" id="mh-util"
placeholder="e.g., 100" min="0" max="100" /></div>
<div class="field"><label for="mh-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="mh-buy"
placeholder="e.g., 39000" /></div>
<div class="field"><label for="mh-country">Employment Country</label><select id="mh-country"></select></div>
<div class="field"><label for="mh-currency">Currency</label><select id="mh-currency"></select></div>
</div>
<div class="error" id="mh-error" style="display:none;">Time % must total 100%</div>
<div class="actions"><button class="btn-primary" id="mh-calc" disabled>Calculate</button><button
class="btn-secondary" id="mh-clear">Clear</button></div>
<div class="results" id="mh-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="mh-annualsell">-</div>
</div>
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="mh-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="mh-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="mh-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="mh-copy">Copy Results</button></div>
</div>
</section>
<!-- Mix Daily -->
<section id="mix-daily" class="panel">
<div class="grid">
<div class="field"><label for="md-mgt-sell">Sell Rate - Mgt Fee (Annual)</label><input type="number"
id="md-mgt-sell" placeholder="e.g., 65000" /></div>
<div class="field"><label for="md-mgt-time">Time % (Mgt Fee)</label><input type="number" id="md-mgt-time"
placeholder="e.g., 100" min="0" max="100" /></div>
<div class="field"><label for="md-pjt-sell">Sell Rate - Pjt Fee (Daily)</label><input type="number"
id="md-pjt-sell" placeholder="e.g., 90" /></div>
<div class="field"><label for="md-pjt-time">Time % (Pjt Fee)</label><input type="number" id="md-pjt-time"
placeholder="e.g., 0" min="0" max="100" /></div>
<div class="field"><label for="md-util">Forecasted Utilisation (%)</label><input type="number" id="md-util"
placeholder="e.g., 100" min="0" max="100" /></div>
<div class="field"><label for="md-buy">Buy Rate (Annual Gross Salary)</label><input type="number" id="md-buy"
placeholder="e.g., 35000" /></div>
<div class="field"><label for="md-country">Employment Country</label><select id="md-country"></select></div>
<div class="field"><label for="md-currency">Currency</label><select id="md-currency"></select></div>
</div>
<div class="error" id="md-error" style="display:none;">Time % must total 100%</div>
<div class="actions"><button class="btn-primary" id="md-calc" disabled>Calculate</button><button
class="btn-secondary" id="md-clear">Clear</button></div>
<div class="results" id="md-results" style="display:none;">
<h3>Results</h3>
<div class="res-row">
<div class="res-item">
<div class="label">Annual Sell Rate</div>
<div class="value" id="md-annualsell">-</div>
</div>
<div class="res-item">
<div class="label">Total CTC</div>
<div class="value" id="md-ctc">-</div>
</div>
<div class="res-item res-highlight">
<div class="label">Gross Profit</div>
<div class="value" id="md-gp">-</div>
</div>
<div class="res-item">
<div class="label">Role Project Margin %</div>
<div class="margin-value" id="md-margin">-</div>
</div>
</div>
<div class="actions"><button class="btn-primary" id="md-copy">Copy Results</button></div>
</div>
</section>
<!-- Admin Panel -->
<section id="adminPanel">
<h2>Admin Panel</h2>
<p class="muted">Manage the country list for the current session. Changes will be lost on page refresh.</p>
<div class="admin-section">
<h3>Add Country</h3>
<div class="grid">
<div class="field"><label for="ad-region">Region</label><input type="text" id="ad-region"
placeholder="e.g., EUROPE" /></div>
<div class="field"><label for="ad-country">Country</label><input type="text" id="ad-country"
placeholder="e.g., New Country" /></div>
<div class="field"><label for="ad-currency">Currency</label><input type="text" id="ad-currency"
placeholder="e.g., NWC" /></div>
<div class="field"><label for="ad-mult">Multiplier</label><input type="number" id="ad-mult" step="0.0001"
placeholder="e.g., 1.2500" /></div>
<div class="field"><label for="ad-days">Working Days</label><input type="number" id="ad-days" step="1"
placeholder="e.g., 220" /></div>
<div class="field"><label for="ad-hours">Hours Per Day</label><input type="number" id="ad-hours" step="0.1"
placeholder="e.g., 8" /></div>
</div>
<div class="actions"><button class="btn-primary" id="ad-add">Add Country</button></div>
</div>
<div class="admin-section">
<h3>Edit Country Data</h3>
<div class="grid">
<div class="field"><label for="ed-country">Country</label><select id="ed-country"></select></div>
<div class="field"><label for="ed-mult">New Multiplier</label><input type="number" id="ed-mult"
step="0.0001" /></div>
<div class="field"><label for="ed-days">New Working Days</label><input type="number" id="ed-days"
step="1" />
</div>
<div class="field"><label for="ed-hours">New Hours Per Day</label><input type="number" id="ed-hours"
step="0.1" /></div>
</div>
<div class="actions"><button class="btn-primary" id="ed-save">Update Country</button></div>
</div>
<div class="admin-section">
<h3>Delete Country</h3>
<div class="grid">
<div class="field"><label for="del-country">Country</label><select id="del-country"></select></div>
</div>
<div class="actions"><button class="btn-secondary" id="del-delete">Delete</button></div>
</div>
<div class="muted" id="admin-status"></div>
</section>
</div>
</div>
<div id="toast">✓ Results copied to clipboard</div>
<script>
// ====== Configuration ======
const API_BASE = 'https://ai-sandbox.oliver.solutions/hiring_calculator';
let COUNTRY_DATA = [];
let ADMIN_TOKEN = null; // We'll store the password here for the session as a simple token
let ACCESS_TOKEN = null;
// ====== Utilities ======
const byId = (id) => document.getElementById(id);
const fmt = (n, currency = '') => {
if (isNaN(n) || n === null) return '-';
const num = Math.round((Number(n) + Number.EPSILON) * 100) / 100;
const parts = num.toFixed(2).split('.');
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return currency ? `${currency} ${parts.join('.')}` : parts.join('.');
};
const parse = (id) => { const v = parseFloat(byId(id).value); return isNaN(v) ? 0 : v; };
let toastTimeout;
const showToast = (msg) => {
const t = byId('toast');
t.textContent = msg;
t.style.display = 'block';
t.style.opacity = '1';
t.style.bottom = '40px';
if (toastTimeout) clearTimeout(toastTimeout);
toastTimeout = setTimeout(() => {
t.style.opacity = '0';
t.style.bottom = '20px';
setTimeout(() => { t.style.display = 'none'; }, 300);
}, 3000);
};
// ====== MSAL Configuration ======
const msalConfig = {
auth: {
clientId: "9079054c-9620-4757-a256-23413042f1ef",
authority: "https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385",
redirectUri: "https://ai-sandbox.oliver.solutions/hiring_calculator"
},
cache: { cacheLocation: "sessionStorage", storeAuthStateInCookie: false }
};
const msalInstance = new msal.PublicClientApplication(msalConfig);
async function signIn() {
try {
const loginResponse = await msalInstance.loginPopup({
scopes: ["User.Read"]
});
ACCESS_TOKEN = loginResponse.idToken;
sessionStorage.setItem('app_logged_in', 'true');
showApp();
} catch (err) {
console.error(err);
alert("Login failed: " + err.message);
}
}
function showApp() {
byId('login-overlay').style.display = 'none';
byId('app-container').style.display = 'block';
boot();
}
// Initialize MSAL
(async () => {
await msalInstance.initialize();
// Check if already logged in
const accounts = msalInstance.getAllAccounts();
const appLoggedIn = sessionStorage.getItem('app_logged_in');
if (accounts.length > 0 && appLoggedIn === 'true') {
try {
const response = await msalInstance.acquireTokenSilent({
account: accounts[0],
scopes: ["User.Read"]
});
ACCESS_TOKEN = response.idToken;
showApp();
} catch (err) {
console.warn("Silent token acquisition failed", err);
}
}
})();
byId('login-btn').addEventListener('click', signIn);
byId('logoutBtn').addEventListener('click', signOut);
function signOut() {
ACCESS_TOKEN = null;
ADMIN_TOKEN = null;
sessionStorage.removeItem('app_logged_in');
byId('app-container').style.display = 'none';
byId('login-overlay').style.display = 'flex';
}
// ====== Utilities ======
const sumTo100 = (a, b) => Math.round((a + b) * 1000) / 1000 === 100;
function getSortedCountriesUKFirst() {
const sorted = [...COUNTRY_DATA].sort((a, b) => a.Country.localeCompare(b.Country));
const idx = sorted.findIndex(x => x.Country === 'United Kingdom');
if (idx > -1) {
const [uk] = sorted.splice(idx, 1);
sorted.unshift(uk);
}
return sorted;
}
function getCountryCurrency(countryName) {
const row = COUNTRY_DATA.find(c => c.Country === countryName);
return row ? row.Currency : '';
}
function populateAllDropdowns() {
const pairs = [
['mgmt-country', 'mgmt-currency'], ['hr-country', 'hr-currency'], ['dy-country', 'dy-currency'],
['cm-country', 'cm-currency'], ['oh-country', 'oh-currency'], ['mm-country', 'mm-currency'],
['mh-country', 'mh-currency'], ['md-country', 'md-currency']
];
pairs.forEach(([c, cu]) => populateCountryAndCurrency(c, cu));
}
function populateCountryAndCurrency(selectCountryId, selectCurrencyId) {
const selCountry = byId(selectCountryId);
const selCurrency = byId(selectCurrencyId);
const sorted = getSortedCountriesUKFirst();
// Preserve current selection if possible
const currentVal = selCountry.value;
selCountry.innerHTML = `<option value="">Select a country</option>` + sorted.map(c => `<option value="${c.Country}">${c.Country}</option>`).join('');
if (sorted.some(c => c.Country === currentVal)) {
selCountry.value = currentVal;
}
const currencies = Array.from(new Set(COUNTRY_DATA.map(c => c.Currency))).sort();
selCurrency.innerHTML = currencies.map(cu => `<option value="${cu}">${cu}</option>`).join('');
selCountry.addEventListener('change', () => {
const currency = getCountryCurrency(selCountry.value);
if (currency) { selCurrency.value = currency; }
});
}
function loadLogo() {
const img = byId('logoImg');
if (!img) return;
const url = 'hiring_calculator/oliver_logo.png';
const test = new Image();
test.onload = () => { if (img) img.src = url; };
test.onerror = () => { console.error('Logo failed to load:', url); };
test.src = url;
}
// ====== API Calls ======
async function calculate(endpoint, payload, resultIds, currencyId) {
try {
const response = await fetch(`${API_BASE}/api/calculate/${endpoint}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify(payload)
});
const result = await response.json();
const currency = byId(currencyId).value;
if (resultIds.ctc) byId(resultIds.ctc).textContent = fmt(result.ctc, currency);
if (resultIds.gp) byId(resultIds.gp).textContent = fmt(result.gp, currency);
if (resultIds.margin) byId(resultIds.margin).textContent = isNaN(result.margin) ? 'N/A' : `${result.margin.toFixed(1)}%`;
if (resultIds.annualSell) byId(resultIds.annualSell).textContent = fmt(result.annualSell, currency);
if (resultIds.container) byId(resultIds.container).style.display = 'block';
} catch (e) {
console.error(e);
alert('Calculation failed. Ensure backend is running.');
}
}
// ====== Event Listener Setup ======
function setupMgmt() {
byId('mgmt-calc').addEventListener('click', () => {
const payload = {
sellAnnual: parse('mgmt-sell'), buyAnnual: parse('mgmt-buy'), country: byId('mgmt-country').value
};
calculate('mgmt', payload, { ctc: 'mgmt-ctc', gp: 'mgmt-gp', margin: 'mgmt-margin', container: 'mgmt-results' }, 'mgmt-currency');
});
byId('mgmt-clear').addEventListener('click', () => {
['mgmt-sell', 'mgmt-buy'].forEach(id => byId(id).value = '');
byId('mgmt-results').style.display = 'none';
});
byId('mgmt-copy').addEventListener('click', () => copyResults('mgmt'));
}
function setupHourly() {
byId('hr-calc').addEventListener('click', () => {
const payload = {
rateHourly: parse('hr-sell'), utilPct: parse('hr-util'), buyAnnual: parse('hr-buy'), country: byId('hr-country').value
};
calculate('hourly', payload, { annualSell: 'hr-annualsell', ctc: 'hr-ctc', gp: 'hr-gp', margin: 'hr-margin', container: 'hr-results' }, 'hr-currency');
});
byId('hr-clear').addEventListener('click', () => {
['hr-sell', 'hr-util', 'hr-buy'].forEach(id => byId(id).value = '');
byId('hr-results').style.display = 'none';
});
byId('hr-copy').addEventListener('click', () => copyResults('hr'));
}
function setupDaily() {
byId('dy-calc').addEventListener('click', () => {
const payload = {
rateDaily: parse('dy-sell'), utilPct: parse('dy-util'), buyAnnual: parse('dy-buy'), country: byId('dy-country').value
};
calculate('daily', payload, { annualSell: 'dy-annualsell', ctc: 'dy-ctc', gp: 'dy-gp', margin: 'dy-margin', container: 'dy-results' }, 'dy-currency');
});
byId('dy-clear').addEventListener('click', () => {
['dy-sell', 'dy-util', 'dy-buy'].forEach(id => byId(id).value = '');
byId('dy-results').style.display = 'none';
});
byId('dy-copy').addEventListener('click', () => copyResults('dy'));
}
function setupClientMultiplier() {
byId('cm-calc').addEventListener('click', () => {
const payload = {
buyAnnual: parse('cm-buy'), country: byId('cm-country').value, agreedMult: parse('cm-mult')
};
calculate('multiplier', payload, { ctc: 'cm-ctc', annualSell: 'cm-annualsell', gp: 'cm-gp', margin: 'cm-margin', container: 'cm-results' }, 'cm-currency');
});
byId('cm-clear').addEventListener('click', () => {
['cm-buy', 'cm-mult'].forEach(id => byId(id).value = '');
byId('cm-results').style.display = 'none';
});
byId('cm-copy').addEventListener('click', () => copyResults('cm'));
}
function setupOverhead() {
byId('oh-calc').addEventListener('click', async () => {
const payload = { buyAnnual: parse('oh-buy'), country: byId('oh-country').value };
try {
const response = await fetch(`${API_BASE}/api/calculate/overhead`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify(payload)
});
const result = await response.json();
const currency = byId('oh-currency').value;
byId('oh-ctc').textContent = fmt(result.ctc, currency);
byId('oh-results').style.display = 'block';
} catch (e) { console.error(e); }
});
byId('oh-clear').addEventListener('click', () => {
byId('oh-buy').value = '';
byId('oh-results').style.display = 'none';
});
byId('oh-copy').addEventListener('click', () => copyResults('oh'));
}
function setupMixPanel(prefix, endpointSuffix) {
function validate() {
const ok = sumTo100(parse(`${prefix}-mgt-time`), parse(`${prefix}-pjt-time`));
byId(`${prefix}-error`).style.display = ok ? 'none' : 'block';
byId(`${prefix}-calc`).disabled = !ok;
}
[`${prefix}-mgt-time`, `${prefix}-pjt-time`].forEach(id => byId(id).addEventListener('input', validate));
validate();
byId(`${prefix}-calc`).addEventListener('click', () => {
const payload = {
mgtAnnual: parse(`${prefix}-mgt-sell`), mgtPct: parse(`${prefix}-mgt-time`),
pjtPct: parse(`${prefix}-pjt-time`), buyAnnual: parse(`${prefix}-buy`),
country: byId(`${prefix}-country`).value, utilPct: parse(`${prefix}-util`)
};
if (prefix === 'mm') payload.pjtMonthly = parse('mm-pjt-sell');
if (prefix === 'mh') payload.pjtHourly = parse('mh-pjt-sell');
if (prefix === 'md') payload.pjtDaily = parse('md-pjt-sell');
calculate(endpointSuffix, payload, { annualSell: `${prefix}-annualsell`, ctc: `${prefix}-ctc`, gp: `${prefix}-gp`, margin: `${prefix}-margin`, container: `${prefix}-results` }, `${prefix}-currency`);
});
byId(`${prefix}-clear`).addEventListener('click', () => {
document.querySelectorAll(`#${prefix} input[type=number]`).forEach(el => el.value = '');
byId(`${prefix}-results`).style.display = 'none';
byId(`${prefix}-calc`).disabled = true;
});
byId(`${prefix}-copy`).addEventListener('click', () => copyResults(prefix));
}
// ====== Admin Panel Setup ======
function setupAdmin() {
const toggle = byId('adminToggle');
const panel = byId('adminPanel');
// Admin UI State
let adminData = [];
async function login() {
const pwd = prompt('Enter admin password:');
if (!pwd) return false;
try {
const res = await fetch(`${API_BASE}/api/admin/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ password: pwd })
});
const data = await res.json();
if (data.success) {
ADMIN_TOKEN = pwd; // Store password as token for this session
return true;
} else {
alert('Incorrect password.');
return false;
}
} catch (e) {
alert('Login error.');
return false;
}
}
async function fetchAdminData() {
if (!ADMIN_TOKEN) return;
const res = await fetch(`${API_BASE}/api/admin/data`, {
headers: {
'X-Admin-Password': ADMIN_TOKEN,
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
});
if (res.ok) {
adminData = await res.json();
refreshAdminCountrySelects();
}
}
toggle.addEventListener('click', async () => {
if (!ADMIN_TOKEN) {
const success = await login();
if (!success) return;
}
panel.style.display = (panel.style.display === 'none' || panel.style.display === '') ? 'block' : 'none';
if (panel.style.display === 'block') {
await fetchAdminData();
}
});
byId('ad-add').addEventListener('click', async () => {
const newCountry = {
Region: byId('ad-region').value.trim(), Country: byId('ad-country').value.trim(),
Currency: byId('ad-currency').value.trim(), Multiplier: parse('ad-mult'),
WorkingDays: parse('ad-days'), HoursPerDay: parse('ad-hours')
};
if (!newCountry.Country || !newCountry.Currency) { alert('Country and Currency are required.'); return; }
const res = await fetch(`${API_BASE}/api/admin/country`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Admin-Password': ADMIN_TOKEN,
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify(newCountry)
});
if (res.ok) {
showToast(`Added ${newCountry.Country}`);
await fetchAdminData(); // Refresh data
// Also refresh public data? Ideally yes, but for now let's just reload the page or re-fetch public data
boot();
}
});
byId('ed-country').addEventListener('change', (e) => {
const country = adminData.find(c => c.Country === e.target.value);
if (country) {
byId('ed-mult').value = country.Multiplier;
byId('ed-days').value = country.WorkingDays;
byId('ed-hours').value = country.HoursPerDay;
}
});
byId('ed-save').addEventListener('click', async () => {
const countryName = byId('ed-country').value;
const country = adminData.find(c => c.Country === countryName);
if (country) {
const update = {
Multiplier: parse('ed-mult'),
WorkingDays: parse('ed-days'),
HoursPerDay: parse('ed-hours')
};
const res = await fetch(`${API_BASE}/api/admin/country/${country.Id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Admin-Password': ADMIN_TOKEN,
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify(update)
});
if (res.ok) {
showToast(`${countryName} updated.`);
await fetchAdminData();
}
}
});
byId('del-delete').addEventListener('click', async () => {
const countryName = byId('del-country').value;
if (!confirm(`Delete ${countryName}?`)) return;
const res = await fetch(`${API_BASE}/api/admin/country/delete`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Admin-Password': ADMIN_TOKEN,
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ Country: countryName })
});
if (res.ok) {
showToast(`${countryName} deleted.`);
await fetchAdminData();
boot();
}
});
function refreshAdminCountrySelects() {
const sorted = [...adminData].sort((a, b) => a.Country.localeCompare(b.Country));
const options = sorted.map(c => `<option value="${c.Country}">${c.Country}</option>`).join('');
byId('ed-country').innerHTML = options;
byId('del-country').innerHTML = options;
// Trigger change to populate the edit fields for the first item
if (sorted.length > 0) byId('ed-country').dispatchEvent(new Event('change'));
}
}
// ====== Copy Results to Clipboard Function ======
function copyResults(prefix) {
// Panel name mapping
const panelNames = {
'mgmt': 'Management Fee',
'hr': 'Project Fee (Hourly)',
'dy': 'Project Fee (Daily)',
'cm': 'Client Multiplier',
'oh': 'Overhead / Clientable',
'mm': 'Mix (Monthly)',
'mh': 'Mix (Hourly)',
'md': 'Mix (Daily)'
};
// Field configuration
const panelFields = {
'mgmt': ['ctc', 'gp', 'margin'],
'hr': ['annualsell', 'ctc', 'gp', 'margin'],
'dy': ['annualsell', 'ctc', 'gp', 'margin'],
'cm': ['ctc', 'annualsell', 'gp', 'margin'],
'oh': ['ctc'],
'mm': ['annualsell', 'ctc', 'gp', 'margin'],
'mh': ['annualsell', 'ctc', 'gp', 'margin'],
'md': ['annualsell', 'ctc', 'gp', 'margin']
};
// Field label mapping
const fieldLabels = {
'annualsell': 'Annual Sell Rate',
'ctc': 'Total CTC',
'gp': 'Gross Profit',
'margin': 'Role Project Margin %'
};
// Get panel configuration
const panelName = panelNames[prefix];
const fields = panelFields[prefix];
if (!panelName || !fields) {
console.error(`Unknown panel prefix: ${prefix}`);
showToast('Copy failed!');
return;
}
// Get the copy button
const copyButton = byId(`${prefix}-copy`);
const originalText = copyButton.textContent;
// Build the formatted text output
let text = `${panelName} Results:\n`;
// Iterate through each field for this panel
fields.forEach(field => {
const elementId = `${prefix}-${field}`;
const element = byId(elementId);
if (element) {
const value = element.textContent.trim();
const label = fieldLabels[field];
text += `- ${label}: ${value}\n`;
}
});
// Use fallback method for reliability
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
// Change button text
copyButton.textContent = '✓ Results Copied';
showToast('✓ Results copied!');
// Reset button text after 3 seconds
setTimeout(() => {
copyButton.textContent = originalText;
}, 3000);
} else {
showToast('Copy failed!');
}
} catch (err) {
document.body.removeChild(textArea);
console.error('Copy failed:', err);
showToast('Copy failed!');
}
}
// ====== Boot ======
async function boot() {
try {
const res = await fetch(`${API_BASE}/api/countries`, {
headers: { 'Authorization': `Bearer ${ACCESS_TOKEN}` }
});
COUNTRY_DATA = await res.json();
} catch (e) {
console.error('Failed to load country data', e);
alert('Failed to load country data. Is the server running?');
}
loadLogo();
document.querySelectorAll('.tab').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(b => b.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
byId(btn.dataset.panel).classList.add('active');
});
});
populateAllDropdowns();
setupMgmt();
setupHourly();
setupDaily();
setupClientMultiplier();
setupOverhead();
setupMixPanel('mm', 'mix-monthly');
setupMixPanel('mh', 'mix-hourly');
setupMixPanel('md', 'mix-daily');
setupAdmin();
}
</script>
</body>
</html>
```