hiring_calculator/app.py

359 lines
18 KiB
Python

import os
from flask import Flask, request, jsonify, send_from_directory
import jwt
from jwt import PyJWKClient
from functools import wraps
app = Flask(__name__, static_folder='.')
# Azure AD Configuration
AZURE_TENANT_ID = "e519c2e6-bc6d-4fdf-8d9c-923c2f002385"
AZURE_CLIENT_ID = "9079054c-9620-4757-a256-23413042f1ef"
JWKS_URI = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/discovery/v2.0/keys"
ISSUER = f"https://login.microsoftonline.com/{AZURE_TENANT_ID}/v2.0"
def verify_token(token):
try:
jwks_client = PyJWKClient(JWKS_URI)
signing_key = jwks_client.get_signing_key_from_jwt(token)
data = jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=AZURE_CLIENT_ID,
issuer=ISSUER
)
return data
except Exception as e:
print(f"Token verification failed: {e}")
return None
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Missing or invalid token'}), 401
token = auth_header.split(' ')[1]
user_data = verify_token(token)
if not user_data:
return jsonify({'error': 'Invalid token'}), 401
return f(*args, **kwargs)
return decorated
# ====== Configuration ======
ADMIN_PASSWORD = 'OLIVERAdmin2025'
DEFAULT_COUNTRY_SETTINGS = {'Multiplier': 1.0, 'WorkingDays': 253, 'HoursPerDay': 7.5}
# ====== Data Source ======
# Storing as list of dictionaries for easier manipulation
COUNTRY_DATA = [
{"Region": "EUROPE", "Country": "United Arab Emirates", "Currency": "AED", "Multiplier": 1.0350, "WorkingDays": 223, "HoursPerDay": 7.5, "Id": 0},
{"Region": "LATAM", "Country": "Argentina", "Currency": "ARS", "Multiplier": 1.5500, "WorkingDays": 230, "HoursPerDay": 8.0, "Id": 1},
{"Region": "SEAPAC", "Country": "Australia", "Currency": "AUD", "Multiplier": 1.1900, "WorkingDays": 218, "HoursPerDay": 7.5, "Id": 2},
{"Region": "EUROPE", "Country": "Bulgaria", "Currency": "BGL", "Multiplier": 1.0000, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 3},
{"Region": "LATAM", "Country": "Brazil", "Currency": "BRL", "Multiplier": 1.8450, "WorkingDays": 225, "HoursPerDay": 8.0, "Id": 4},
{"Region": "NA", "Country": "Canada", "Currency": "CAD", "Multiplier": 1.2400, "WorkingDays": 225, "HoursPerDay": 8.0, "Id": 5},
{"Region": "EUROPE", "Country": "Switzerland", "Currency": "CHF", "Multiplier": 1.0000, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 6},
{"Region": "LATAM", "Country": "Chile", "Currency": "CLP", "Multiplier": 1.0000, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 7},
{"Region": "CHINA", "Country": "China", "Currency": "CNY", "Multiplier": 1.4300, "WorkingDays": 238, "HoursPerDay": 8.0, "Id": 8},
{"Region": "LATAM", "Country": "Colombia", "Currency": "COP", "Multiplier": 1.3596, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 9},
{"Region": "EUROPE", "Country": "Czech Republic", "Currency": "CZK", "Multiplier": 1.3380, "WorkingDays": 222, "HoursPerDay": 8.0, "Id": 10},
{"Region": "EUROPE", "Country": "Denmark", "Currency": "DKK", "Multiplier": 1.1250, "WorkingDays": 220, "HoursPerDay": 7.5, "Id": 11},
{"Region": "MEA", "Country": "Egypt", "Currency": "EGP", "Multiplier": 1.2000, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 12},
{"Region": "EUROPE", "Country": "Austria", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 260, "HoursPerDay": 8.0, "Id": 13},
{"Region": "EUROPE", "Country": "France", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 219, "HoursPerDay": 7.5, "Id": 14},
{"Region": "EUROPE", "Country": "Germany", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 216, "HoursPerDay": 7.5, "Id": 15},
{"Region": "EUROPE", "Country": "Greece", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 223, "HoursPerDay": 7.5, "Id": 16},
{"Region": "EUROPE", "Country": "Ireland", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 223, "HoursPerDay": 7.5, "Id": 17},
{"Region": "EUROPE", "Country": "Italy", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 213, "HoursPerDay": 7.5, "Id": 18},
{"Region": "EUROPE", "Country": "Netherlands", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 222, "HoursPerDay": 7.5, "Id": 19},
{"Region": "EUROPE", "Country": "Portugal", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 220, "HoursPerDay": 8.0, "Id": 20},
{"Region": "EUROPE", "Country": "Slovakia", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 220, "HoursPerDay": 7.5, "Id": 21},
{"Region": "EUROPE", "Country": "Spain", "Currency": "EUR", "Multiplier": 1.3618, "WorkingDays": 219, "HoursPerDay": 7.5, "Id": 22},
{"Region": "EUROPE", "Country": "United Kingdom", "Currency": "GBP", "Multiplier": 1.1755, "WorkingDays": 222, "HoursPerDay": 7.5, "Id": 23},
{"Region": "CHINA", "Country": "Hong Kong", "Currency": "HKD", "Multiplier": 1.1000, "WorkingDays": 225, "HoursPerDay": 8.0, "Id": 24},
{"Region": "EUROPE", "Country": "Croatia", "Currency": "HRK", "Multiplier": 1.1650, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 25},
{"Region": "EUROPE", "Country": "Hungary", "Currency": "HUF", "Multiplier": 1.1850, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 26},
{"Region": "SEAPAC", "Country": "Indonesia", "Currency": "IDR", "Multiplier": 1.6333, "WorkingDays": 219, "HoursPerDay": 8.0, "Id": 27},
{"Region": "MEA", "Country": "Israel", "Currency": "ILS", "Multiplier": 1.3339, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 28},
{"Region": "SEAPAC", "Country": "India", "Currency": "INR", "Multiplier": 1.4060, "WorkingDays": 211, "HoursPerDay": 8.0, "Id": 29},
{"Region": "SEAPAC", "Country": "Japan", "Currency": "JPY", "Multiplier": 1.5550, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 30},
{"Region": "SEAPAC", "Country": "South Korea", "Currency": "KRW", "Multiplier": 1.2565, "WorkingDays": 221, "HoursPerDay": 8.0, "Id": 31},
{"Region": "EUROPE", "Country": "Morocco", "Currency": "MAD", "Multiplier": 1.0674, "WorkingDays": 242, "HoursPerDay": 8.0, "Id": 32},
{"Region": "LATAM", "Country": "Mexico", "Currency": "MXN", "Multiplier": 1.5500, "WorkingDays": 238, "HoursPerDay": 8.0, "Id": 33},
{"Region": "SEAPAC", "Country": "Malaysia", "Currency": "MYR", "Multiplier": 1.2530, "WorkingDays": 237, "HoursPerDay": 8.0, "Id": 34},
{"Region": "EUROPE", "Country": "Norway", "Currency": "NOK", "Multiplier": 1.1620, "WorkingDays": 220, "HoursPerDay": 7.5, "Id": 35},
{"Region": "SEAPAC", "Country": "Philippines", "Currency": "PHP", "Multiplier": 1.2167, "WorkingDays": 219, "HoursPerDay": 8.0, "Id": 36},
{"Region": "EUROPE", "Country": "Poland", "Currency": "PLN", "Multiplier": 1.2100, "WorkingDays": 222, "HoursPerDay": 8.0, "Id": 37},
{"Region": "EUROPE", "Country": "Romania", "Currency": "RON", "Multiplier": 1.3920, "WorkingDays": 220, "HoursPerDay": 7.5, "Id": 38},
{"Region": "EUROPE", "Country": "Russia", "Currency": "RUB", "Multiplier": 1.3620, "WorkingDays": 214, "HoursPerDay": 8.0, "Id": 39},
{"Region": "EUROPE", "Country": "Sweden", "Currency": "SEK", "Multiplier": 1.3710, "WorkingDays": 217, "HoursPerDay": 7.5, "Id": 40},
{"Region": "SEAPAC", "Country": "Singapore", "Currency": "SGD", "Multiplier": 1.5060, "WorkingDays": 218, "HoursPerDay": 8.0, "Id": 41},
{"Region": "SEAPAC", "Country": "Thailand", "Currency": "THB", "Multiplier": 1.1330, "WorkingDays": 236, "HoursPerDay": 8.0, "Id": 42},
{"Region": "EUROPE", "Country": "Turkey", "Currency": "TRY", "Multiplier": 1.3250, "WorkingDays": 223, "HoursPerDay": 8.0, "Id": 43},
{"Region": "SEAPAC", "Country": "Taiwan", "Currency": "TWD", "Multiplier": 1.1585, "WorkingDays": 253, "HoursPerDay": 7.5, "Id": 44},
{"Region": "NA", "Country": "Panama", "Currency": "USD", "Multiplier": 1.1585, "WorkingDays": 214, "HoursPerDay": 8.0, "Id": 45},
{"Region": "NA", "Country": "United States", "Currency": "USD", "Multiplier": 1.1585, "WorkingDays": 231, "HoursPerDay": 8.0, "Id": 46},
{"Region": "SEAPAC", "Country": "Vietnam", "Currency": "VND", "Multiplier": 1.3230, "WorkingDays": 234, "HoursPerDay": 8.0, "Id": 47},
{"Region": "MEA", "Country": "South Africa", "Currency": "ZAR", "Multiplier": 1.0300, "WorkingDays": 221, "HoursPerDay": 7.5, "Id": 48}
]
# ====== Helpers ======
def get_country_details(country_name):
for c in COUNTRY_DATA:
if c['Country'] == country_name:
return c
return DEFAULT_COUNTRY_SETTINGS
# ====== Routes ======
@app.route('/')
def index():
return send_from_directory('.', 'index.html')
@app.route('/<path:path>')
def static_files(path):
return send_from_directory('.', path)
@app.route('/api/countries', methods=['GET'])
@require_auth
def get_countries():
# Return only non-sensitive data
public_data = [{'Country': c['Country'], 'Currency': c['Currency'], 'Region': c['Region']} for c in COUNTRY_DATA]
return jsonify(public_data)
# --- Calculation Endpoints ---
@app.route('/api/calculate/mgmt', methods=['POST'])
@require_auth
def calculate_mgmt():
data = request.json
sell_annual = float(data.get('sellAnnual', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
mult = details.get('Multiplier', 1.0)
ctc = buy_annual * mult
gp = sell_annual - ctc
margin = (gp / sell_annual * 100) if sell_annual > 0 else 0
return jsonify({'ctc': ctc, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/hourly', methods=['POST'])
@require_auth
def calculate_hourly():
data = request.json
rate_hourly = float(data.get('rateHourly', 0))
util_pct = float(data.get('utilPct', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
hours = details.get('WorkingDays', 253) * details.get('HoursPerDay', 7.5)
annual_sell = rate_hourly * hours * (util_pct / 100)
ctc = buy_annual * details.get('Multiplier', 1.0)
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'annualSell': annual_sell, 'ctc': ctc, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/daily', methods=['POST'])
@require_auth
def calculate_daily():
data = request.json
rate_daily = float(data.get('rateDaily', 0))
util_pct = float(data.get('utilPct', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
days = details.get('WorkingDays', 253)
annual_sell = rate_daily * days * (util_pct / 100)
ctc = buy_annual * details.get('Multiplier', 1.0)
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'annualSell': annual_sell, 'ctc': ctc, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/multiplier', methods=['POST'])
@require_auth
def calculate_multiplier():
data = request.json
buy_annual = float(data.get('buyAnnual', 0))
agreed_mult = float(data.get('agreedMult', 0))
country = data.get('country')
details = get_country_details(country)
ctc = buy_annual * details.get('Multiplier', 1.0)
annual_sell = ctc * agreed_mult
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'ctc': ctc, 'annualSell': annual_sell, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/overhead', methods=['POST'])
@require_auth
def calculate_overhead():
data = request.json
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
ctc = buy_annual * details.get('Multiplier', 1.0)
return jsonify({'ctc': ctc})
@app.route('/api/calculate/mix-monthly', methods=['POST'])
@require_auth
def calculate_mix_monthly():
data = request.json
mgt_annual = float(data.get('mgtAnnual', 0))
mgt_pct = float(data.get('mgtPct', 0))
pjt_monthly = float(data.get('pjtMonthly', 0))
pjt_pct = float(data.get('pjtPct', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
ctc = buy_annual * details.get('Multiplier', 1.0)
annual_sell = (mgt_annual * (mgt_pct / 100)) + (pjt_monthly * 12 * (pjt_pct / 100))
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'annualSell': annual_sell, 'ctc': ctc, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/mix-hourly', methods=['POST'])
@require_auth
def calculate_mix_hourly():
data = request.json
mgt_annual = float(data.get('mgtAnnual', 0))
mgt_pct = float(data.get('mgtPct', 0))
pjt_hourly = float(data.get('pjtHourly', 0))
pjt_pct = float(data.get('pjtPct', 0))
util_pct = float(data.get('utilPct', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
ctc = buy_annual * details.get('Multiplier', 1.0)
hours = details.get('WorkingDays', 253) * details.get('HoursPerDay', 7.5)
pjt_annual = pjt_hourly * hours * (util_pct / 100)
annual_sell = (mgt_annual * (mgt_pct / 100)) + (pjt_annual * (pjt_pct / 100))
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'annualSell': annual_sell, 'ctc': ctc, 'gp': gp, 'margin': margin})
@app.route('/api/calculate/mix-daily', methods=['POST'])
@require_auth
def calculate_mix_daily():
data = request.json
mgt_annual = float(data.get('mgtAnnual', 0))
mgt_pct = float(data.get('mgtPct', 0))
pjt_daily = float(data.get('pjtDaily', 0))
pjt_pct = float(data.get('pjtPct', 0))
util_pct = float(data.get('utilPct', 0))
buy_annual = float(data.get('buyAnnual', 0))
country = data.get('country')
details = get_country_details(country)
ctc = buy_annual * details.get('Multiplier', 1.0)
days = details.get('WorkingDays', 253)
pjt_annual = pjt_daily * days * (util_pct / 100)
annual_sell = (mgt_annual * (mgt_pct / 100)) + (pjt_annual * (pjt_pct / 100))
gp = annual_sell - ctc
margin = (gp / annual_sell * 100) if annual_sell > 0 else 0
return jsonify({'annualSell': annual_sell, 'ctc': ctc, 'gp': gp, 'margin': margin})
# --- Admin Endpoints ---
@app.route('/api/admin/login', methods=['POST'])
@require_auth
def admin_login():
data = request.json
password = data.get('password')
if password == ADMIN_PASSWORD:
return jsonify({'success': True})
return jsonify({'success': False}), 401
@app.route('/api/admin/data', methods=['GET'])
@require_auth
def get_admin_data():
# In a real app, check for session/token here.
# For this simple implementation, we'll assume the frontend handles the "login state"
# but ideally we should check a header or cookie.
# Given the constraints, we'll just expose this endpoint but the frontend only calls it after password check.
# To be slightly more secure, we could require the password in the header.
auth_header = request.headers.get('X-Admin-Password')
if auth_header != ADMIN_PASSWORD:
return jsonify({'error': 'Unauthorized'}), 401
return jsonify(COUNTRY_DATA)
@app.route('/api/admin/country', methods=['POST'])
@require_auth
def add_country():
auth_header = request.headers.get('X-Admin-Password')
if auth_header != ADMIN_PASSWORD:
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
new_country = {
"Region": data.get('Region'),
"Country": data.get('Country'),
"Currency": data.get('Currency'),
"Multiplier": float(data.get('Multiplier', 0)),
"WorkingDays": int(data.get('WorkingDays', 0)),
"HoursPerDay": float(data.get('HoursPerDay', 0)),
"Id": len(COUNTRY_DATA) # Simple ID generation
}
COUNTRY_DATA.append(new_country)
return jsonify({'success': True, 'country': new_country})
@app.route('/api/admin/country/<int:country_id>', methods=['PUT'])
@require_auth
def update_country(country_id):
auth_header = request.headers.get('X-Admin-Password')
if auth_header != ADMIN_PASSWORD:
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
# Find by ID or Country Name? The frontend uses Name primarily, but we added IDs.
# Let's find by ID.
for c in COUNTRY_DATA:
if c['Id'] == country_id:
c['Multiplier'] = float(data.get('Multiplier', c['Multiplier']))
c['WorkingDays'] = int(data.get('WorkingDays', c['WorkingDays']))
c['HoursPerDay'] = float(data.get('HoursPerDay', c['HoursPerDay']))
return jsonify({'success': True, 'country': c})
# Fallback: try to find by name if ID mismatch (legacy support if needed)
return jsonify({'error': 'Country not found'}), 404
@app.route('/api/admin/country/delete', methods=['POST'])
@require_auth
def delete_country():
auth_header = request.headers.get('X-Admin-Password')
if auth_header != ADMIN_PASSWORD:
return jsonify({'error': 'Unauthorized'}), 401
data = request.json
country_name = data.get('Country')
global COUNTRY_DATA
initial_len = len(COUNTRY_DATA)
COUNTRY_DATA = [c for c in COUNTRY_DATA if c['Country'] != country_name]
if len(COUNTRY_DATA) < initial_len:
return jsonify({'success': True})
return jsonify({'error': 'Country not found'}), 404
if __name__ == '__main__':
app.run(debug=True, port=5238)