1310 lines
No EOL
48 KiB
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>
|
|
``` |