359 lines
18 KiB
Python
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)
|