App
App
jsonify, session
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required,
logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash
import bcrypt
from datetime import datetime
import matplotlib
matplotlib.use('Agg') # Set the backend before importing pyplot
import matplotlib.pyplot as plt
from io import BytesIO
import base64
from config import Config
from sqlalchemy.orm import aliased
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy import func
import numpy as np
import random
from matplotlib.colors import LinearSegmentedColormap
import os
import uuid
from werkzeug.utils import secure_filename
from flask import current_app
app = Flask(__name__)
app.config.from_object(Config)
app.config['UPLOAD_FOLDER'] = 'uploads'
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5MB limit
ALLOWED_EXTENSIONS = {'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'}
# Initialize SQLAlchemy
db = SQLAlchemy(app)
# Initialize Flask-Mail
mail = Mail(app)
def check_territory_funds(territory):
today = datetime.utcnow()
if today.month >= 4: # April or later
financial_year = f"{today.year}-{today.year+1}"
else:
financial_year = f"{today.year-1}-{today.year}"
allocation = TerritoryFundAllocation.query.filter_by(
territory=territory,
financial_year=financial_year
).first()
if not allocation:
return False, "No allocation set for this financial year"
if remaining <= 0:
# Check if 3 months have passed since last request
last_request = InvestmentRequest.query.join(
User, InvestmentRequest.requestor_id == User.id
).filter(
User.territory == territory
).order_by(
InvestmentRequest.created_at.desc()
).first()
def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
# Flask-Login setup
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'
# Models
def __repr__(self):
return f'<User {self.username}>'
class InvestmentRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
requestor_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
title = db.Column(db.String(255), nullable=False)
description = db.Column(db.Text, nullable=False)
amount = db.Column(db.Float, nullable=False)
currency = db.Column(db.String(3), default='USD')
strategic_dimension = db.Column(db.String(50), nullable=False)
expected_roi = db.Column(db.Float)
supporting_docs = db.Column(db.String(255))
status = db.Column(db.String(50), default='draft')
territory_admin_id = db.Column(db.Integer, db.ForeignKey('user.id'))
global_admin_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow)
document_filename = db.Column(db.String(255), nullable=True)
class Notification(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
message = db.Column(db.Text, nullable=False)
link = db.Column(db.String(255))
is_read = db.Column(db.Boolean, default=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class RemittanceRequest(db.Model):
id = db.Column(db.Integer, primary_key=True)
investment_request_id = db.Column(db.Integer,
db.ForeignKey('investment_request.id'), nullable=False)
requestor_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
progress_report = db.Column(db.Text, nullable=False)
actual_roi = db.Column(db.Float)
amount = db.Column(db.Float, nullable=False) # Add this field
currency = db.Column(db.String(3), default='USD') # You might want to add this
too
supporting_docs = db.Column(db.String(255))
status = db.Column(db.String(50), default='draft')
territory_admin_id = db.Column(db.Integer, db.ForeignKey('user.id'))
global_admin_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow,
onupdate=datetime.utcnow)
investment_request = db.relationship('InvestmentRequest',
foreign_keys=[investment_request_id])
requestor = db.relationship('User', foreign_keys=[requestor_id])
territory_admin = db.relationship('User', foreign_keys=[territory_admin_id])
global_admin = db.relationship('User', foreign_keys=[global_admin_id])
class ActivityLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
action = db.Column(db.String(255), nullable=False)
entity_type = db.Column(db.String(50))
entity_id = db.Column(db.Integer)
details = db.Column(db.Text)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
class TerritoryFundAllocation(db.Model):
id = db.Column(db.Integer, primary_key=True)
territory = db.Column(db.String(50), nullable=False)
financial_year = db.Column(db.String(9), nullable=False) # Format: "2023-2024"
allocated_amount = db.Column(db.Float, nullable=False)
allocated_by = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
allocated_at = db.Column(db.DateTime, default=datetime.utcnow)
is_active = db.Column(db.Boolean, default=True)
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.context_processor
def inject_notifications():
if current_user.is_authenticated:
unread_count = Notification.query.filter_by(
user_id=current_user.id,
is_read=False
).count()
return {'unread_notifications': unread_count}
return {'unread_notifications': 0}
# Helper functions
def log_activity(user_id, action, entity_type=None, entity_id=None, details=None):
activity = ActivityLog(
user_id=user_id,
action=action,
entity_type=entity_type,
entity_id=entity_id,
details=details
)
db.session.add(activity)
db.session.commit()
# Routes
@app.route('/download/<filename>')
@login_required
def download_document(filename):
# Verify the user has permission to access this file
if not current_user.is_authenticated:
abort(403)
safe_filename = secure_filename(filename)
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'], safe_filename)
if not os.path.exists(file_path):
abort(404)
# Send email
reset_url = url_for('reset_password', token=token, _external=True)
msg = Message('Password Reset Request',
sender=app.config['MAIL_DEFAULT_SENDER'],
recipients=[email])
msg.body = f'''To reset your password, visit the following link:
{reset_url}
If you did not make this request, simply ignore this email.
'''
mail.send(msg)
return render_template('forgot_password.html')
user = User.query.filter_by(email=email).first()
if not user:
flash('Invalid user.', 'danger')
return redirect(url_for('forgot_password'))
if request.method == 'POST':
password = request.form['password']
confirm_password = request.form['confirm_password']
if password != confirm_password:
flash('Passwords do not match.', 'danger')
return redirect(request.url)
# Update password
user.password = bcrypt.hashpw(password.encode('utf-8'),
bcrypt.gensalt()).decode('utf-8')
db.session.commit()
@app.route('/')
def home():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
return render_template('login.html')
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
user = User.query.filter_by(username=username).first()
if not user:
flash('Invalid username or password', 'danger')
return redirect(url_for('login'))
try:
if bcrypt.checkpw(password.encode('utf-8'), user.password.encode('utf-
8')):
login_user(user, remember=True)
session.permanent = True # Make session persistent
log_activity(user.id, 'login')
flash('Logged in successfully!', 'success')
next_page = request.args.get('next')
return redirect(next_page or url_for('dashboard'))
else:
flash('Invalid username or password', 'danger')
except Exception as e:
flash('Login error occurred', 'danger')
print(f"Login error: {str(e)}")
return render_template('login.html')
@app.route('/logout')
@login_required
def logout():
log_activity(current_user.id, 'logout')
logout_user()
flash('Logged out successfully!', 'success')
return redirect(url_for('login'))
if request.method == 'POST':
username = request.form['username']
email = request.form['email']
password = bcrypt.hashpw(request.form['password'].encode('utf-8'),
bcrypt.gensalt()).decode('utf-8')
role = request.form['role']
territory = request.form['territory'] if role in ['territory_admin',
'requestor'] else None
try:
new_user = User(
username=username,
email=email,
password=password,
role=role,
territory=territory
)
db.session.add(new_user)
db.session.commit()
flash('User registered successfully!', 'success')
log_activity(current_user.id, 'register_user', 'user', new_user.id)
return redirect(url_for('dashboard'))
except Exception as e:
db.session.rollback()
flash(f'Error: {str(e)}', 'danger')
return render_template('register.html')
if existing_allocation:
flash('Allocation for this financial year already submitted',
'warning')
return redirect(url_for('dashboard'))
if request.method == 'POST':
try:
amount = float(request.form.get('amount'))
except ValueError:
flash('Invalid amount entered', 'danger')
except SQLAlchemyError as e:
db.session.rollback()
flash('Database error occurred while saving allocation', 'danger')
app.logger.error(f"Allocation submission error: {str(e)}")
try:
territory_admin = User.query.filter_by(
role='territory_admin',
territory=current_user.territory
).first()
if not territory_admin:
flash('No territory admin found for your region', 'danger')
return redirect(url_for('new_request'))
file.save(file_path)
document_filename = unique_filename
elif file.filename != '':
flash('Invalid file type. Allowed formats: PDF, Word, Excel,
PowerPoint', 'warning')
new_request = InvestmentRequest(
requestor_id=current_user.id,
title=title,
description=description,
amount=amount,
strategic_dimension=strategic_dimension,
expected_roi=expected_roi,
territory_admin_id=territory_admin.id,
status='submitted',
document_filename=document_filename # Store the filename in
database
)
db.session.add(new_request)
db.session.commit()
create_notification(
territory_admin.id,
f'New investment request from {current_user.username}',
url_for('view_request', request_id=new_request.id)
)
return redirect(url_for('dashboard'))
except Exception as e:
db.session.rollback()
# Clean up uploaded file if there was an error
if document_filename:
try:
os.remove(os.path.join(current_app.config['UPLOAD_FOLDER'],
document_filename))
except OSError:
pass
flash(f'Error: {str(e)}', 'danger')
return render_template('request_form.html')
@app.route('/requests')
@login_required
def requests():
status_filter = request.args.get('status', 'all')
territory_filter = request.args.get('territory', 'all')
return render_template('request_list.html',
requests=requests_list,
status_filter=status_filter,
territory_filter=territory_filter,
territories=territories)
@app.route('/request/<int:request_id>/action', methods=['POST'])
@login_required
def request_action(request_id):
action = request.form['action']
comment = request.form.get('comment', '')
request_data = InvestmentRequest.query.get(request_id)
if not request_data:
flash('Request not found', 'danger')
return redirect(url_for('requests'))
if action == 'approve':
if current_user.role == 'territory_admin' and request_data.status ==
'submitted':
new_status = 'territory_approved'
notification_msg = f'Your investment request has been approved by
territory admin and is pending global approval'
notification_user_id = request_data.requestor_id
if not new_status:
flash('Invalid action for current request status or user role', 'danger')
return redirect(url_for('view_request', request_id=request_id))
if current_user.role == 'global_admin':
request_data.global_admin_id = current_user.id
# Log activity
log_activity(current_user.id, action, 'investment_request', request_id,
comment)
# Create notification
if notification_user_id:
create_notification(
notification_user_id,
notification_msg,
url_for('view_request', request_id=request_id)
)
db.session.commit()
@app.route('/request/<int:request_id>')
@login_required
def view_request(request_id):
try:
# Create aliases for User table
requestor = aliased(User, name='requestor')
territory_admin = aliased(User, name='territory_admin')
global_admin = aliased(User, name='global_admin')
if not request_data:
flash('Request not found', 'danger')
return redirect(url_for('requests'))
# Check permissions
if current_user.role == 'requestor' and investment_request.requestor_id !=
current_user.id:
flash('You are not authorized to view this request', 'danger')
return redirect(url_for('requests'))
return render_template('request_detail.html',
request=investment_request,
requestor_name=requestor_name,
territory=territory,
territory_admin_name=territory_admin_name,
global_admin_name=global_admin_name,
total_approved_remittances=total_approved_remittances,
remittance_requests=remittance_requests,
stats=stats,
requestor_id=investment_request.requestor_id)
except SQLAlchemyError as e:
db.session.rollback()
flash('Database error occurred', 'danger')
app.logger.error(f"Database error in view_request: {str(e)}")
return redirect(url_for('dashboard'))
except Exception as e:
db.session.rollback()
flash('An unexpected error occurred', 'danger')
app.logger.error(f"Unexpected error in view_request: {str(e)}")
return redirect(url_for('dashboard'))
@app.route('/dashboard')
@login_required
def dashboard():
# Initialize variables
total_requests = 0
pending_requests = 0
recent_requests = []
territory_funds = None
financial_year = None
try:
# Get current financial year (April 1 to March 31)
today = datetime.utcnow()
if today.month >= 4: # April or later
financial_year = f"{today.year}-{today.year+1}"
else:
financial_year = f"{today.year-1}-{today.year}"
if current_user.role == 'requestor':
# Requestor dashboard
total_requests = InvestmentRequest.query.filter_by(
requestor_id=current_user.id
).count()
pending_requests = InvestmentRequest.query.filter(
InvestmentRequest.requestor_id == current_user.id,
InvestmentRequest.status.in_(['submitted', 'territory_approved'])
).count()
recent_requests = InvestmentRequest.query.filter_by(
requestor_id=current_user.id
).order_by(
InvestmentRequest.created_at.desc()
).limit(5).all()
pending_requests = db.session.query(InvestmentRequest).join(
User, InvestmentRequest.requestor_id == User.id
).filter(
User.territory == current_user.territory,
InvestmentRequest.status.in_(['submitted', 'territory_approved'])
).count()
if allocation:
# Calculate used funds (sum of all globally approved remittances)
used_funds = db.session.query(
func.coalesce(func.sum(RemittanceRequest.amount), 0)
).filter(
RemittanceRequest.status == 'global_approved',
RemittanceRequest.investment_request.has(
InvestmentRequest.requestor.has(
User.territory == current_user.territory
)
),
RemittanceRequest.created_at >= datetime(today.year - (0 if
today.month >=4 else 1), 4, 1),
RemittanceRequest.created_at <= datetime(today.year + (1 if
today.month >=4 else 0), 3, 31)
).scalar() or 0
territory_funds = {
'allocated': allocation.allocated_amount,
'used': used_funds,
'remaining': allocation.allocated_amount - used_funds,
'allocation_exists': True
}
else:
territory_funds = {
'allocation_exists': False
}
pending_requests = InvestmentRequest.query.filter_by(
status='territory_approved'
).count()
except SQLAlchemyError as e:
db.session.rollback()
flash('Database error occurred', 'danger')
app.logger.error(f"Dashboard error: {str(e)}")
unread_notifications = 0
return render_template('dashboard.html',
total_requests=total_requests,
pending_requests=pending_requests,
recent_requests=recent_requests,
unread_notifications=unread_notifications,
territory_funds=territory_funds,
financial_year=financial_year)
if existing_remittance:
flash('An active remittance request already exists for this investment',
'danger')
return redirect(url_for('view_request', request_id=request_id))
if request.method == 'POST':
progress_report = request.form['progress_report']
actual_roi = request.form.get('actual_roi', None)
amount = float(request.form['amount'])
# Validate amount
if amount <= 0:
flash('Amount must be greater than zero', 'danger')
return redirect(url_for('new_remittance', request_id=request_id))
try:
territory_admin = User.query.filter_by(
role='territory_admin',
territory=current_user.territory
).first()
if not territory_admin:
flash('No territory admin found for your region', 'danger')
return redirect(url_for('new_remittance', request_id=request_id))
new_remittance = RemittanceRequest(
investment_request_id=request_id,
requestor_id=current_user.id,
progress_report=progress_report,
actual_roi=actual_roi,
amount=amount,
currency=investment_request.currency,
territory_admin_id=territory_admin.id,
status='submitted'
)
db.session.add(new_remittance)
db.session.commit()
# Create notification
create_notification(
territory_admin.id,
f'New remittance request from {current_user.username} for
investment {investment_request.title}',
url_for('view_request', request_id=request_id)
)
# Log activity
log_activity(current_user.id, 'create_remittance',
'remittance_request', new_remittance.id)
# Send email
subject = "New Remittance Request Created"
recipient = current_user.email
body = f"""Your remittance request for investment
'{investment_request.title}' has been successfully created.
Details:
- Amount: {amount} {investment_request.currency}
- ROI: {actual_roi if actual_roi else 'Not specified'}%
"""
send_email(subject, recipient, body)
except Exception as e:
db.session.rollback()
flash(f'Error creating remittance request: {str(e)}', 'danger')
return render_template('remittance_form.html',
request_id=request_id,
investment_request=investment_request)
@app.route('/remittance/<int:remittance_id>/action', methods=['POST'])
@login_required
def remittance_action(remittance_id):
action = request.form['action']
comment = request.form.get('comment', '')
remittance_data = db.session.query(
RemittanceRequest,
InvestmentRequest.requestor_id,
InvestmentRequest.title
).join(
InvestmentRequest,
RemittanceRequest.investment_request_id == InvestmentRequest.id
).filter(
RemittanceRequest.id == remittance_id
).first()
if not remittance_data:
flash('Remittance not found', 'danger')
return redirect(url_for('requests'))
if action == 'approve':
if current_user.role == 'territory_admin' and
remittance_data.RemittanceRequest.status == 'submitted':
new_status = 'territory_approved'
notification_msg = f'Your remittance request for
"{remittance_data.title}" has been approved by territory admin and is pending
global approval'
notification_user_id = remittance_data.requestor_id
if not new_status:
flash('Invalid action for current remittance status or user role',
'danger')
return redirect(url_for('view_request',
request_id=remittance_data.RemittanceRequest.investment_request_id))
if current_user.role == 'global_admin':
remittance_data.RemittanceRequest.global_admin_id = current_user.id
# Log activity
log_activity(current_user.id, action, 'remittance_request', remittance_id,
comment)
# Create notification
if notification_user_id:
create_notification(
notification_user_id,
notification_msg,
url_for('view_request',
request_id=remittance_data.RemittanceRequest.investment_request_id)
)
db.session.commit()
@app.route('/notifications')
@login_required
def notifications():
notifications = Notification.query.filter_by(
user_id=current_user.id
).order_by(
Notification.created_at.desc()
).all()
@app.route('/analytics')
@login_required
def analytics():
if current_user.role != 'global_admin':
flash('Only global admins can access analytics', 'danger')
return redirect(url_for('dashboard'))
charts = {}
filters = {
'financial_year': selected_year,
'territory': selected_territory,
'strategic_dimension': selected_dimension
}
try:
# Base query for filtering
base_query = db.session.query(InvestmentRequest)
if selected_year:
base_query = base_query.filter(
db.func.strftime('%Y', InvestmentRequest.created_at).in_(
[selected_year[:4], str(int(selected_year[:4]) + 1)]
)
)
if selected_territory:
base_query = base_query.join(
User, InvestmentRequest.requestor_id == User.id
).filter(User.territory == selected_territory)
if selected_dimension:
base_query = base_query.filter(
InvestmentRequest.strategic_dimension == selected_dimension
)
# Requests by status
status_query = base_query.with_entities(
InvestmentRequest.status,
db.func.count(InvestmentRequest.id).label('count')
).group_by(InvestmentRequest.status)
status_data = status_query.all()
if status_data:
labels = [d[0] for d in status_data]
sizes = [d[1] for d in status_data]
plt.figure(figsize=(5, 5))
plt.pie(sizes, labels=labels, autopct='%1.1f%%', startangle=140,
colors=COLOR_PALETTE[:len(status_data)])
plt.title('Requests by Status')
plt.axis('equal')
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['status'] = base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
# Requests by territory
territory_query = db.session.query(
User.territory,
db.func.count(InvestmentRequest.id).label('count')
).join(
InvestmentRequest, User.id == InvestmentRequest.requestor_id
)
if selected_year:
territory_query = territory_query.filter(
db.func.strftime('%Y', InvestmentRequest.created_at).in_(
[selected_year[:4], str(int(selected_year[:4]) + 1)]
)
)
if selected_dimension:
territory_query = territory_query.filter(
InvestmentRequest.strategic_dimension == selected_dimension
)
territory_data = territory_query.group_by(User.territory).all()
if territory_data:
territories = [d[0] for d in territory_data]
counts = [d[1] for d in territory_data]
plt.figure(figsize=(8, 4))
plt.bar(territories, counts, color=COLOR_PALETTE[0])
plt.title('Requests by Territory')
plt.xlabel('Territory')
plt.ylabel('Number of Requests')
plt.xticks(rotation=45)
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['territory'] = base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
dimension_data = dimension_query.all()
if dimension_data:
dimensions = [d[0] for d in dimension_data]
counts = [d[1] for d in dimension_data]
plt.figure(figsize=(8, 4))
plt.bar(dimensions, counts, color=COLOR_PALETTE[1])
plt.title('Requests by Strategic Dimension')
plt.xlabel('Strategic Dimension')
plt.ylabel('Number of Requests')
plt.xticks(rotation=45)
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['dimension'] = base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
# Monthly requests
monthly_query = base_query.with_entities(
db.func.strftime('%Y-%m', InvestmentRequest.created_at).label('month'),
db.func.count(InvestmentRequest.id).label('count')
).group_by('month').order_by('month')
monthly_data = monthly_query.all()
if monthly_data:
months = [d[0] for d in monthly_data]
counts = [d[1] for d in monthly_data]
plt.figure(figsize=(10, 4))
if not selected_year:
# If no year filter, group by calendar year
years = sorted(list({m[:4] for m in months}))
yearly_counts = {}
plt.ylabel('Number of Requests')
plt.xticks(rotation=45)
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['monthly'] = base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
if selected_year:
allocation_query = allocation_query.filter(
TerritoryFundAllocation.financial_year == selected_year
)
allocation_data = allocation_query.all()
if allocation_data:
# Group by territory and year
allocations = {}
for territory, year, amount in allocation_data:
if territory not in allocations:
allocations[territory] = {}
allocations[territory][year] = amount
territories = sorted(allocations.keys())
years = sorted({year for terr in allocations.values() for year in
terr.keys()})
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['territory_allocations'] =
base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
if selected_year:
spending_query = spending_query.filter(
db.func.strftime('%Y', RemittanceRequest.created_at).in_(
[selected_year[:4], str(int(selected_year[:4]) + 1)]
)
)
if selected_territory:
spending_query = spending_query.filter(User.territory ==
selected_territory)
spending_data = spending_query.group_by(User.territory).all()
if spending_data:
territories = [d[0] for d in spending_data]
amounts = [d[1] for d in spending_data]
plt.figure(figsize=(10, 5))
plt.bar(territories, amounts, color=COLOR_PALETTE[3])
plt.title('Territory-wise Spent Funds (Approved Remittances)')
plt.xlabel('Territory')
plt.ylabel('Amount Spent')
plt.xticks(rotation=45)
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['territory_spending'] =
base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
# Allocation vs Usage
if allocation_data and spending_data:
# Get allocations for the selected year or all years
if selected_year:
alloc_dict = {terr: amt for terr, year, amt in allocation_data
if year == selected_year}
else:
alloc_dict = {}
for terr, year, amt in allocation_data:
if terr not in alloc_dict:
alloc_dict[terr] = 0
alloc_dict[terr] += amt
# Get spending
spend_dict = {terr: amt for terr, amt in spending_data}
# Calculate remaining
remaining_values = [alloc - spend if alloc > spend else 0
for alloc, spend in zip(alloc_values, spend_values)]
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['allocation_vs_usage'] =
base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
if selected_year:
dim_analysis_query = dim_analysis_query.filter(
db.func.strftime('%Y', InvestmentRequest.created_at).in_(
[selected_year[:4], str(int(selected_year[:4]) + 1)]
)
)
if selected_territory:
dim_analysis_query = dim_analysis_query.filter(User.territory ==
selected_territory)
dim_analysis_data = dim_analysis_query.group_by(
User.territory, InvestmentRequest.strategic_dimension
).all()
if dim_analysis_data:
# Prepare data for heatmap
territories = sorted(list({d[0] for d in dim_analysis_data}))
dimensions = sorted(list({d[1] for d in dim_analysis_data}))
img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['dimension_analysis'] =
base64.b64encode(img.getvalue()).decode('utf8')
plt.close()
except Exception as e:
flash(f'Error generating analytics: {str(e)}', 'danger')
return redirect(url_for('dashboard'))
return render_template('analytics.html',
charts=charts,
financial_years=financial_years,
territories=territories,
strategic_dimensions=strategic_dimensions,
selected_year=selected_year,
selected_territory=selected_territory,
selected_dimension=selected_dimension)
@app.route('/chatbot', methods=['POST'])
@login_required
def chatbot():
try:
message = request.form.get('message', '').lower().strip()
responses = {
'how to submit': 'Go to Dashboard → Click "New Request" → Fill the form
→ Submit',
'status': 'Check your "Requests" page for submission statuses.',
'remittance': 'Submit remittance after approval by global admin, from
the request details page.',
'documents': 'A PDF/PPTX document supporting your investment request is
appreciated. It would help speed up the process.',
'contact': 'Email [email protected] / [email protected] or
call +1-500-4030-200',
'criteria': 'Requests must be cross-territory with measurable
outcomes.',
'process': '1. Submit → 2. Territory Review → 3. Global Review → 4.
Approval',
'hello': 'Hello! How can I help you today?',
'hi': 'Hi there! Ask me about submissions or request status.',
'timeline': 'Typically a request would take 3-5 business days to get a
decision. If you have not heard back until then, you may contact us through email
or phone.',
'bye': 'Goodbye! Thank you for using the chatbot :)'
}
response = "I can help with: " + ", ".join([f"'{key}'" for key in
responses.keys()])
for key in responses:
if key in message:
response = responses[key]
break
return jsonify({
'response': response,
'status': 'success'
})
except Exception as e:
return jsonify({
'response': f"Error: {str(e)}",
'status': 'error'
}), 500
@app.route('/admin/users')
@login_required
def admin_users():
if current_user.role != 'global_admin':
flash('Unauthorized access', 'danger')
return redirect(url_for('dashboard'))
# Base query
query = User.query
# Apply filters
if role_filter != 'all':
query = query.filter_by(role=role_filter)
if territory_filter != 'all':
query = query.filter_by(territory=territory_filter)
users = query.order_by(User.username.asc()).all()
return render_template('admin_users.html',
users=users,
roles=roles,
territories=territories,
current_role=role_filter,
current_territory=territory_filter)
@app.route('/admin/activity-logs')
@login_required
def admin_activity_logs():
if current_user.role != 'global_admin':
flash('Unauthorized access', 'danger')
return redirect(url_for('dashboard'))
# Apply filters
if user_filter != 'all':
query = query.filter(ActivityLog.user_id == user_filter)
if action_filter != 'all':
query = query.filter(ActivityLog.action == action_filter)
logs = query.limit(100).all()
return render_template('admin_activity_logs.html',
logs=logs,
users=users,
actions=actions,
current_user_filter=user_filter,
current_action_filter=action_filter)
user = User.query.get_or_404(user_id)
if request.method == 'POST':
try:
user.username = request.form['username']
user.email = request.form['email']
user.role = request.form['role']
user.territory = request.form['territory'] if request.form['role'] in
['territory_admin', 'requestor'] else None
return render_template('edit_user.html',
user=user,
roles=roles,
territories=territories)
@app.route('/profile')
@login_required
def profile():
return render_template('profile.html', user=current_user)
if len(new_password) < 8:
flash('Password must be at least 8 characters', 'danger')
return redirect(url_for('change_password'))
# Update password
try:
current_user.password = bcrypt.hashpw(new_password.encode('utf-8'),
bcrypt.gensalt()).decode('utf-8')
db.session.commit()
flash('Password changed successfully!', 'success')
return redirect(url_for('profile'))
except Exception as e:
db.session.rollback()
flash(f'Error changing password: {str(e)}', 'danger')
return render_template('change_password.html')
@app.before_request
def initialize_database():
try:
db.create_all()
# Only populate test data if we just created the admin (first run)
populate_test_data()
except Exception as e:
print(f"Database initialization error: {str(e)}")
def populate_test_data():
"""Populate the database with test users and sample requests"""
try:
print("Populating test data...")
territories = [
'australia', 'brazil', 'china', 'hong kong', 'india',
'japan', 'mexico', 'singapore', 'united states', 'canada'
]
strategic_dimensions = ['Clients', 'Demand Generation', 'People',
'Engagement Delivery']
status_options_old = ['global_approved', 'global_rejected',
'territory_rejected']
status_options_current = status_options_old + ['submitted',
'territory_approved']
financial_years = [f'{year}-{str(year+1)[-2:]}' for year in range(2018,
2026)]
# Requestor
if not User.query.filter_by(role='requestor',
territory=territory).first():
requestor = User(
username=f'{territory.replace(" ", "_")}_requestor',
email=f'{territory.replace(" ", "_")}[email protected]',
password=bcrypt.hashpw('password'.encode('utf-8'),
bcrypt.gensalt()).decode('utf-8'),
role='requestor',
territory=territory
)
db.session.add(requestor)
db.session.commit()
db.session.commit()
for _ in range(random.randint(250,350)):
territory = random.choice(territories)
territory_admin = User.query.filter_by(territory=territory,
role='territory_admin').first()
requestor = User.query.filter_by(territory=territory,
role='requestor').first()
allocation =
TerritoryFundAllocation.query.filter_by(territory=territory,
financial_year=year).first()
created_at = fake.date_time_between(start_date=start_date,
end_date=end_date)
status = random.choice(status_options_current if year ==
financial_years[-1] and created_at.month == 8 else status_options_old)
request = InvestmentRequest(
requestor_id=requestor.id,
title=fake.sentence(),
description=fake.text(),
amount=amount,
currency='USD',
strategic_dimension=random.choice(strategic_dimensions),
expected_roi=random.uniform(0.5, 5.0),
status=status,
territory_admin_id=territory_admin_id,
global_admin_id=global_admin_id,
created_at=created_at,
updated_at=created_at + timedelta(days=random.randint(1, 30))
)
db.session.add(request)
all_requests.append((request, status, territory_admin, requestor))
db.session.commit()
print("Test data populated successfully")
except Exception as e:
db.session.rollback()
print(f"Error populating test data: {str(e)}")
if __name__ == '__main__':
app.run(debug=True)