0% found this document useful (0 votes)
114 views35 pages

App

Uploaded by

Deep Samanta
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
114 views35 pages

App

Uploaded by

Deep Samanta
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd

from flask import Flask, render_template, request, redirect, url_for, flash,

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

from flask_mail import Mail, Message


from itsdangerous import URLSafeTimedSerializer
import secrets
import string

import os
import uuid
from werkzeug.utils import secure_filename
from flask import current_app

from datetime import datetime, timedelta


from faker import Faker
fake = Faker()

from flask import send_from_directory, abort

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"

# 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 == 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

remaining = allocation.allocated_amount - used_funds

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()

if last_request and (today - last_request.created_at).days < 90:


# Notify global admin
global_admins = User.query.filter_by(role='global_admin').all()
for admin in global_admins:
create_notification(
admin.id,
f'Territory {territory} has exhausted its funds allocation',
url_for('territory_funds', territory=territory)
)
return False, "Territory funds exhausted. Cannot process new requests
for 3 months after last request."

return True, remaining

def send_email(subject, recipients, body, html=None):


msg = Message(subject=subject,
recipients=[recipients],
body=body,
html=html,
sender=app.config['MAIL_USERNAME'])
mail.send(msg)

def allowed_file(filename):
return '.' in filename and \
filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

# Initialize token serializer


def generate_token_serializer():
return URLSafeTimedSerializer(app.config['SECRET_KEY'])

# Flask-Login setup
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

# Models

class User(db.Model, UserMixin):


id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password = db.Column(db.String(120), nullable=False)
role = db.Column(db.String(20), nullable=False)
territory = db.Column(db.String(50))

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)

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 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)

user = db.relationship('User', foreign_keys=[user_id])

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)

user = db.relationship('User', foreign_keys=[user_id])

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)

allocated_user = db.relationship('User', foreign_keys=[allocated_by])

@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()

def create_notification(user_id, message, link=None):


notification = Notification(
user_id=user_id,
message=message,
link=link
)
db.session.add(notification)
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)

# For PDF previews, don't force download


if filename.lower().endswith('.pdf') and request.args.get('download') !=
'true':
return send_from_directory(
current_app.config['UPLOAD_FOLDER'],
safe_filename,
mimetype='application/pdf'
)

# For other files or explicit downloads


return send_from_directory(
current_app.config['UPLOAD_FOLDER'],
safe_filename,
as_attachment=True
)

# Password reset routes


@app.route('/forgot_password', methods=['GET', 'POST'])
def forgot_password():
if request.method == 'POST':
email = request.form['email']
user = User.query.filter_by(email=email).first()
if user:
# Generate reset token
serializer = generate_token_serializer()
token = serializer.dumps(email, salt='password-reset-salt')

# 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)

flash('An email with password reset instructions has been sent.',


'info')
return redirect(url_for('login'))

flash('Email address not found.', 'danger')

return render_template('forgot_password.html')

@app.route('/reset_password/<token>', methods=['GET', 'POST'])


def reset_password(token):
try:
serializer = generate_token_serializer()
email = serializer.loads(
token,
salt='password-reset-salt',
max_age=app.config['RESET_TOKEN_EXPIRATION']
)
except:
flash('The password reset link is invalid or has expired.', 'danger')
return redirect(url_for('forgot_password'))

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()

flash('Your password has been updated!', 'success')


return redirect(url_for('login'))
return render_template('reset_password.html', token=token)

# Helper function to generate random password


def generate_random_password(length=12):
characters = string.ascii_letters + string.digits + string.punctuation
return ''.join(secrets.choice(characters) for _ in range(length))

@app.route('/')
def home():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))
return render_template('login.html')

@app.route('/login', methods=['GET', 'POST'])


def login():
if current_user.is_authenticated:
return redirect(url_for('dashboard'))

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'))

@app.route('/register', methods=['GET', 'POST'])


@login_required
def register():
if current_user.role != 'global_admin':
flash('Only global admins can register new users', 'danger')
return redirect(url_for('dashboard'))

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')

@app.route('/submit_allocation', methods=['GET', 'POST'])


@login_required
# @role_required('territory_admin')
def submit_allocation():
if current_user.role == 'territory_admin':
# Get current financial year
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}"

# Check if allocation already exists


existing_allocation = TerritoryFundAllocation.query.filter_by(
territory=current_user.territory,
financial_year=financial_year
).first()

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'))

# Create new allocation


allocation = TerritoryFundAllocation(
territory=current_user.territory,
financial_year=financial_year,
allocated_amount=amount,
allocated_by=current_user.id
)
db.session.add(allocation)
db.session.commit()

flash('Fund allocation submitted successfully', 'success')


return redirect(url_for('dashboard'))

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)}")

return render_template('submit_allocation.html', financial_year=financial_year)

@app.route('/new_request', methods=['GET', 'POST'])


@login_required
def new_request():
if request.method == 'POST':
title = request.form['title']
description = request.form['description']
amount = request.form['amount']
strategic_dimension = request.form['strategic_dimension']
expected_roi = request.form['expected_roi']
document_filename = None # Initialize as None

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'))

# Handle file upload if present


if 'document' in request.files:
file = request.files['document']
if file.filename != '' and allowed_file(file.filename):
# Secure the filename and make it unique
filename = secure_filename(file.filename)
unique_filename = f"{uuid.uuid4().hex}_{filename}"
file_path = os.path.join(current_app.config['UPLOAD_FOLDER'],
unique_filename)

# Ensure upload folder exists


os.makedirs(current_app.config['UPLOAD_FOLDER'], exist_ok=True)

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()

subject = "New Investment Request Created"


recipient = current_user.email
body = f"Your investment request '{title}' for amount ${amount} in
strategic dimension '{strategic_dimension}' and expected ROI of {expected_roi}% has
been successfully created. Next step: Approval from Territory Admin."
send_email(subject, recipient, body)

create_notification(
territory_admin.id,
f'New investment request from {current_user.username}',
url_for('view_request', request_id=new_request.id)
)

log_activity(current_user.id, 'create_request', 'investment_request',


new_request.id)
flash('Investment request submitted successfully!', 'success')

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')

# Create aliases for User table


requestor = aliased(User, name='requestor')
territory_admin = aliased(User, name='territory_admin')
global_admin = aliased(User, name='global_admin')

# Subquery to count active remittance requests for each investment


remittance_subquery = db.session.query(
RemittanceRequest.investment_request_id,
func.count('*').label('active_remittance_count')
).filter(
RemittanceRequest.status != 'global_approved'
).group_by(
RemittanceRequest.investment_request_id
).subquery()

# Base query with proper aliasing and remittance info


query = db.session.query(
InvestmentRequest,
requestor.username.label('requestor_name'),
requestor.territory,
territory_admin.username.label('territory_admin_name'),
global_admin.username.label('global_admin_name'),
func.coalesce(remittance_subquery.c.active_remittance_count,
0).label('active_remittance_count'),
db.session.query(func.count(RemittanceRequest.id)).filter(
RemittanceRequest.investment_request_id == InvestmentRequest.id,
RemittanceRequest.status == 'global_approved'
).label('approved_remittance_count')
).join(
requestor, InvestmentRequest.requestor_id == requestor.id
).outerjoin(
territory_admin, InvestmentRequest.territory_admin_id == territory_admin.id
).outerjoin(
global_admin, InvestmentRequest.global_admin_id == global_admin.id
).outerjoin(
remittance_subquery, remittance_subquery.c.investment_request_id ==
InvestmentRequest.id
)

# Apply filters based on user role


if current_user.role == 'requestor':
query = query.filter(InvestmentRequest.requestor_id == current_user.id)
elif current_user.role == 'territory_admin':
query = query.filter(
requestor.territory == current_user.territory
)

# Apply status filter


if status_filter != 'all':
if status_filter == 'pending':
if current_user.role == 'requestor':
query = query.filter(InvestmentRequest.status.in_(['submitted',
'territory_approved']))
elif current_user.role == 'territory_admin':
query = query.filter(InvestmentRequest.status.in_(['submitted',
'territory_approved']))
elif current_user.role == 'global_admin':
query = query.filter(InvestmentRequest.status ==
'territory_approved')
else:
query = query.filter(InvestmentRequest.status == status_filter)

# Apply territory filter (only for global admins)


if territory_filter != 'all' and current_user.role == 'global_admin':
query = query.filter(requestor.territory == territory_filter)

# Execute query and prepare results


results = query.order_by(InvestmentRequest.created_at.desc()).all()

# Convert to list of dictionaries for template


requests_list = []
for result in results:
request_dict = {
'id': result.InvestmentRequest.id,
'title': result.InvestmentRequest.title,
'amount': result.InvestmentRequest.amount,
'strategic_dimension': result.InvestmentRequest.strategic_dimension,
'status': result.InvestmentRequest.status,
'created_at': result.InvestmentRequest.created_at,
'requestor_name': result.requestor_name,
'territory': result.territory,
'has_active_remittance': result.active_remittance_count > 0,
'approved_remittance_count': result.approved_remittance_count
}
requests_list.append(request_dict)

# Get territories for filter (only for global admins)


territories = []
if current_user.role == 'global_admin':
territories = [t[0] for t in db.session.query(User.territory).filter(
User.territory.isnot(None)
).distinct().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'))

# Check permissions and valid transitions


new_status = None
notification_msg = ""
notification_user_id = None

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

# Notify global admins


global_admins = User.query.filter_by(role='global_admin').all()
for admin in global_admins:
create_notification(
admin.id,
f'New investment request approved by territory admin and needs
your review',
url_for('view_request', request_id=request_id)
)

elif current_user.role == 'global_admin' and request_data.status ==


'territory_approved':
new_status = 'global_approved'
notification_msg = f'Your investment request has been fully approved!'
notification_user_id = request_data.requestor_id

elif action == 'reject':


if current_user.role == 'territory_admin' and request_data.status ==
'submitted':
new_status = 'territory_rejected'
notification_msg = f'Your investment request has been rejected by
territory admin'
notification_user_id = request_data.requestor_id

elif current_user.role == 'global_admin' and request_data.status ==


'territory_approved':
new_status = 'global_rejected'
notification_msg = f'Your investment request has been rejected by
global admin'
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))

# Update request status


request_data.status = new_status
request_data.updated_at = datetime.utcnow()

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()

flash(f'Request {action}d successfully!', 'success')


return redirect(url_for('view_request', request_id=request_id))

@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')

# Query with proper aliasing


request_data = db.session.query(
InvestmentRequest,
requestor.username.label('requestor_name'),
requestor.territory,
territory_admin.username.label('territory_admin_name'),
global_admin.username.label('global_admin_name')
).join(
requestor, InvestmentRequest.requestor_id == requestor.id
).outerjoin(
territory_admin, InvestmentRequest.territory_admin_id ==
territory_admin.id
).outerjoin(
global_admin, InvestmentRequest.global_admin_id == global_admin.id
).filter(
InvestmentRequest.id == request_id
).first()

if not request_data:
flash('Request not found', 'danger')
return redirect(url_for('requests'))

# Unpack the result tuple


investment_request, requestor_name, territory, territory_admin_name,
global_admin_name = request_data

# 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'))

if current_user.role == 'territory_admin' and \


investment_request.territory_admin_id != current_user.id and \
investment_request.status != 'submitted':
flash('You are not authorized to view this request', 'danger')
return redirect(url_for('requests'))

# Calculate total approved remittance amounts


total_approved_remittances = db.session.query(
db.func.coalesce(db.func.sum(RemittanceRequest.amount), 0.0)
).filter(
RemittanceRequest.investment_request_id == request_id,
RemittanceRequest.status == 'global_approved'
).scalar() or 0.0
# Get similar requests statistics
stats = db.session.query(
db.func.count(InvestmentRequest.id).label('total_requests'),
db.func.sum(
db.case(
(InvestmentRequest.status == 'global_approved', 1),
else_=0
)
).label('approved'),
db.func.sum(
db.case(
(InvestmentRequest.status.in_(['global_rejected',
'territory_rejected']), 1),
else_=0
)
).label('rejected'),
db.func.avg(InvestmentRequest.amount).label('avg_amount')
).filter(
InvestmentRequest.requestor_id == investment_request.requestor_id,
InvestmentRequest.strategic_dimension ==
investment_request.strategic_dimension
).first()

# Get remittance requests


remittance_requests = RemittanceRequest.query.filter_by(
investment_request_id=request_id
).order_by(
RemittanceRequest.created_at.desc()
).all()

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()

elif current_user.role == 'territory_admin':


# Territory admin dashboard - only show requests from their territory
total_requests = db.session.query(InvestmentRequest).join(
User, InvestmentRequest.requestor_id == User.id
).filter(
User.territory == current_user.territory
).count()

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()

# Use eager loading to prevent N+1 queries


recent_requests = db.session.query(InvestmentRequest).options(
db.joinedload(InvestmentRequest.requestor)
).join(
User, InvestmentRequest.requestor_id == User.id
).filter(
User.territory == current_user.territory
).order_by(
InvestmentRequest.created_at.desc()
).limit(5).all()

# Get territory fund allocation info


allocation = TerritoryFundAllocation.query.filter_by(
territory=current_user.territory,
financial_year=financial_year
).first()

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
}

elif current_user.role == 'global_admin':


# Global admin dashboard
total_requests = InvestmentRequest.query.count()

pending_requests = InvestmentRequest.query.filter_by(
status='territory_approved'
).count()

# Eager load all relationships


recent_requests = InvestmentRequest.query.options(
db.joinedload(InvestmentRequest.requestor),
db.joinedload(InvestmentRequest.territory_admin),
db.joinedload(InvestmentRequest.global_admin)
).order_by(
InvestmentRequest.created_at.desc()
).limit(5).all()

# Get unread notifications count


unread_notifications = Notification.query.filter_by(
user_id=current_user.id,
is_read=False
).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)

@app.route('/request/<int:request_id>/new_remittance', methods=['GET', 'POST'])


@login_required
def new_remittance(request_id):
investment_request = InvestmentRequest.query.get_or_404(request_id)

# Check if investment is eligible for remittance


if investment_request.status != 'global_approved':
flash('Cannot create remittance for this investment request', 'danger')
return redirect(url_for('requests'))

# Check for existing active remittance requests


existing_remittance = RemittanceRequest.query.filter_by(
investment_request_id=request_id
).filter(
RemittanceRequest.status.in_(['submitted', 'territory_approved'])
).first()

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))

if amount > investment_request.amount:


flash(f'Remittance amount cannot exceed the investment amount of
{investment_request.amount} {investment_request.currency}', '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)

flash('Remittance request submitted successfully!', 'success')


return redirect(url_for('view_request', request_id=request_id))

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'))

# Check permissions and valid transitions


new_status = None
notification_msg = ""
notification_user_id = None

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

# Notify global admins


global_admins = User.query.filter_by(role='global_admin').all()
for admin in global_admins:
create_notification(
admin.id,
f'New remittance request approved by territory admin and needs
your review',
url_for('view_request',
request_id=remittance_data.RemittanceRequest.investment_request_id)
)

elif current_user.role == 'global_admin' and


remittance_data.RemittanceRequest.status == 'territory_approved':
new_status = 'global_approved'
notification_msg = f'Your remittance request for
"{remittance_data.title}" has been fully approved!'
notification_user_id = remittance_data.requestor_id

elif action == 'reject':


if current_user.role == 'territory_admin' and
remittance_data.RemittanceRequest.status == 'submitted':
new_status = 'territory_rejected'
notification_msg = f'Your remittance request for
"{remittance_data.title}" has been rejected by territory admin'
notification_user_id = remittance_data.requestor_id

elif current_user.role == 'global_admin' and


remittance_data.RemittanceRequest.status == 'territory_approved':
new_status = 'global_rejected'
notification_msg = f'Your remittance request for
"{remittance_data.title}" has been rejected by global admin'
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))

# Update remittance status


remittance_data.RemittanceRequest.status = new_status
remittance_data.RemittanceRequest.updated_at = datetime.utcnow()

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()

flash(f'Remittance request {action}d successfully!', 'success')


return redirect(url_for('view_request',
request_id=remittance_data.RemittanceRequest.investment_request_id))

@app.route('/notifications')
@login_required
def notifications():
notifications = Notification.query.filter_by(
user_id=current_user.id
).order_by(
Notification.created_at.desc()
).all()

# Mark all as read


Notification.query.filter_by(
user_id=current_user.id,
is_read=False
).update({'is_read': True})
db.session.commit()

return render_template('notifications.html', notifications=notifications)

@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'))

# Define the color palette


COLOR_PALETTE = ['#ffb600', '#eb8c00', '#d04a02', '#db536a', '#e0301e',
'#000000', '#2d2d2d', '#464646', '#7d7d7d', '#dedede']

# Get filter parameters


selected_year = request.args.get('financial_year', '')
selected_territory = request.args.get('territory', '')
selected_dimension = request.args.get('strategic_dimension', '')

# Get filter options for dropdowns


financial_years = sorted([row[0] for row in db.session.query(
TerritoryFundAllocation.financial_year
).distinct().all()], reverse=True)

territories = sorted([row[0] for row in db.session.query(


User.territory
).filter(User.territory.isnot(None)).distinct().all()])

strategic_dimensions = sorted([row[0] for row in db.session.query(


InvestmentRequest.strategic_dimension
).distinct().all()])

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()

# Requests by strategic dimension


dimension_query = base_query.with_entities(
InvestmentRequest.strategic_dimension,
db.func.count(InvestmentRequest.id).label('count')
).group_by(InvestmentRequest.strategic_dimension)

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 = {}

for month, count in zip(months, counts):


year = month[:4]
if year not in yearly_counts:
yearly_counts[year] = 0
yearly_counts[year] += count

# Create bar chart by year


plt.bar(years, [yearly_counts[y] for y in years],
color=COLOR_PALETTE[0])
plt.title('Yearly Requests Trend')
plt.xlabel('Year')
else:
# If year filter is applied, show financial year (April-March)
financial_year_months = [
f"{int(selected_year[:4])}-04", f"{int(selected_year[:4])}-05",
f"{int(selected_year[:4])}-06",
f"{int(selected_year[:4])}-07", f"{int(selected_year[:4])}-08",
f"{int(selected_year[:4])}-09",
f"{int(selected_year[:4])}-10", f"{int(selected_year[:4])}-11",
f"{int(selected_year[:4])}-12",
f"{int(selected_year[:4])+1}-01", f"{int(selected_year[:4])+1}-
02", f"{int(selected_year[:4])+1}-03"
]

# Create month labels for display


month_labels = [
'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep',
'Oct', 'Nov', 'Dec', 'Jan', 'Feb', 'Mar'
]

# Initialize counts for each financial year month


financial_year_counts = [0] * 12

# Map months to their position in the financial year


for month, count in zip(months, counts):
if month in financial_year_months:
idx = financial_year_months.index(month)
financial_year_counts[idx] = count

# Create line plot for financial year


plt.plot(month_labels, financial_year_counts,
marker='o', color=COLOR_PALETTE[2],
linestyle='-', linewidth=2, markersize=8)
plt.title(f'Monthly Requests Trend - FY {selected_year}')
plt.xlabel('Month (April to March)')

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()

# Territory-wise allocated funds


allocation_query = db.session.query(
TerritoryFundAllocation.territory,
TerritoryFundAllocation.financial_year,
TerritoryFundAllocation.allocated_amount
).filter(TerritoryFundAllocation.is_active == True)

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()})

# Prepare data for stacked bar chart


fig, ax = plt.subplots(figsize=(10, 5))
bottom = None
for i, year in enumerate(years):
amounts = [allocations.get(terr, {}).get(year, 0) for terr in
territories]
if bottom is None:
ax.bar(territories, amounts, label=year, color=COLOR_PALETTE[i
% len(COLOR_PALETTE)])
bottom = amounts
else:
ax.bar(territories, amounts, bottom=bottom, label=year,
color=COLOR_PALETTE[i % len(COLOR_PALETTE)])
bottom = [b + a for b, a in zip(bottom, amounts)]

ax.set_title('Territory-wise Allocated Funds')


ax.set_xlabel('Territory')
ax.set_ylabel('Amount Allocated')
ax.legend(title='Financial Year')
plt.xticks(rotation=45)

img = BytesIO()
plt.savefig(img, format='png', bbox_inches='tight')
img.seek(0)
charts['territory_allocations'] =
base64.b64encode(img.getvalue()).decode('utf8')
plt.close()

# Territory-wise spent funds (approved remittances)


spending_query = db.session.query(
User.territory,
db.func.sum(RemittanceRequest.amount).label('total_spent')
).join(
InvestmentRequest, RemittanceRequest.investment_request_id ==
InvestmentRequest.id
).join(
User, InvestmentRequest.requestor_id == User.id
).filter(
RemittanceRequest.status == 'global_approved'
)

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}

# Get common territories


all_territories =
sorted(set(alloc_dict.keys()).union(set(spend_dict.keys())))
alloc_values = [alloc_dict.get(terr, 0) for terr in all_territories]
spend_values = [spend_dict.get(terr, 0) for terr in all_territories]

# Calculate remaining
remaining_values = [alloc - spend if alloc > spend else 0
for alloc, spend in zip(alloc_values, spend_values)]

# Create stacked bar chart


fig, ax = plt.subplots(figsize=(10, 5))
ax.bar(all_territories, spend_values, label='Spent',
color=COLOR_PALETTE[3])
ax.bar(all_territories, remaining_values, bottom=spend_values,
label='Remaining', color=COLOR_PALETTE[0])

ax.set_title('Territory Fund Allocation vs Usage')


ax.set_xlabel('Territory')
ax.set_ylabel('Amount')
ax.legend()
plt.xticks(rotation=45)

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()

# Strategic Dimension Analysis by Territory


dim_analysis_query = db.session.query(
User.territory,
InvestmentRequest.strategic_dimension,
db.func.avg(InvestmentRequest.amount).label('avg_amount'),
db.func.count(InvestmentRequest.id).label('count')
).join(
InvestmentRequest, User.id == InvestmentRequest.requestor_id
)

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}))

# Create matrix for average amount


avg_amount_matrix = np.zeros((len(territories), len(dimensions)))
count_matrix = np.zeros((len(territories), len(dimensions)))

for terr, dim, avg_amt, count in dim_analysis_data:


terr_idx = territories.index(terr)
dim_idx = dimensions.index(dim)
avg_amount_matrix[terr_idx][dim_idx] = avg_amt
count_matrix[terr_idx][dim_idx] = count

# Create heatmap for average amount


fig, ax = plt.subplots(figsize=(12, 6))
im = ax.imshow(avg_amount_matrix,
cmap=LinearSegmentedColormap.from_list("custom",
[COLOR_PALETTE[9], COLOR_PALETTE[0]]))

# Show all ticks and label them


ax.set_xticks(np.arange(len(dimensions)))
ax.set_yticks(np.arange(len(territories)))
ax.set_xticklabels(dimensions)
ax.set_yticklabels(territories)

# Rotate the tick labels and set their alignment


plt.setp(ax.get_xticklabels(), rotation=45, ha="right",
rotation_mode="anchor")

# Loop over data dimensions and create text annotations


for i in range(len(territories)):
for j in range(len(dimensions)):
text = ax.text(j, i, f"${avg_amount_matrix[i, j]:,.0f}\
n({int(count_matrix[i, j])})",
ha="center", va="center", color="black",
fontsize=8)

ax.set_title("Avg Request Amount by Territory and Dimension\n(Number of


requests in parentheses)")
fig.tight_layout()

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)

# Help Center Routes


@app.route('/help')
@login_required
def help_center():
return render_template('help_center.html')

@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'))

# Get filter parameters


role_filter = request.args.get('role', 'all')
territory_filter = request.args.get('territory', 'all')

# 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()

# Get unique values for filters


roles = [r[0] for r in db.session.query(User.role).distinct().all()]
territories = [t[0] for t in db.session.query(User.territory).filter(
User.territory.isnot(None)).distinct().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'))

# Get filter parameters


user_filter = request.args.get('user', 'all')
action_filter = request.args.get('action', 'all')

# Base query with joined user data


query = db.session.query(
ActivityLog,
User.username.label('username')
).join(
User, ActivityLog.user_id == User.id
).order_by(
ActivityLog.created_at.desc()
)

# 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()

# Get unique values for filters


users = User.query.order_by(User.username.asc()).all()
actions = [a[0] for a in db.session.query(
ActivityLog.action).distinct().all()]

return render_template('admin_activity_logs.html',
logs=logs,
users=users,
actions=actions,
current_user_filter=user_filter,
current_action_filter=action_filter)

@app.route('/user/<int:user_id>/edit', methods=['GET', 'POST'])


@login_required
def edit_user(user_id):
# Only global admins can edit users
if current_user.role != 'global_admin':
flash('Only global admins can edit users', 'danger')
return redirect(url_for('user_management'))

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

# Only update password if provided


if request.form['password']:
user.password = bcrypt.hashpw(request.form['password'].encode('utf-
8'), bcrypt.gensalt()).decode('utf-8')
db.session.commit()
flash('User updated successfully!', 'success')
return redirect(url_for('admin_users'))
except Exception as e:
db.session.rollback()
flash(f'Error updating user: {str(e)}', 'danger')

# Get all available roles and territories for the form


roles = ['global_admin', 'territory_admin', 'requestor']
territories =
db.session.query(User.territory).filter(User.territory.isnot(None)).distinct().all(
)
territories = [t[0] for t in territories]

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)

@app.route('/profile/change_password', methods=['GET', 'POST'])


@login_required
def change_password():
if request.method == 'POST':
current_password = request.form['current_password']
new_password = request.form['new_password']
confirm_password = request.form['confirm_password']

# Verify current password


if not bcrypt.checkpw(current_password.encode('utf-8'),
current_user.password.encode('utf-8')):
flash('Current password is incorrect', 'danger')
return redirect(url_for('change_password'))

# Validate new password


if new_password != confirm_password:
flash('New passwords do not match', 'danger')
return redirect(url_for('change_password'))

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()

# Create global admin if not exists


if not User.query.filter_by(role='global_admin').first():
admin = User(
username='global_admin',
email='[email protected]',
password=bcrypt.hashpw('password'.encode('utf-8'),
bcrypt.gensalt()).decode('utf-8'),
role='global_admin'
)
db.session.add(admin)
db.session.commit()
print("Created default admin user")

# 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)]

# Create territory admins and requestors


for territory in territories:
# Territory admin
if not User.query.filter_by(role='territory_admin',
territory=territory).first():
admin = User(
username=f'{territory.replace(" ", "_")}_admin',
email=f'{territory.replace(" ", "_")}[email protected]',
password=bcrypt.hashpw('password'.encode('utf-8'),
bcrypt.gensalt()).decode('utf-8'),
role='territory_admin',
territory=territory
)
db.session.add(admin)

# 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()

# Allocate funds to territories


global_admin = User.query.filter_by(role='global_admin').first()
for year in financial_years:
for territory in territories:
if not TerritoryFundAllocation.query.filter_by(territory=territory,
financial_year=year).first():
allocation = TerritoryFundAllocation(
territory=territory,
financial_year=year,
allocated_amount=random.randint(1000, 10000),
allocated_by=global_admin.id,
is_active=True
)
db.session.add(allocation)

db.session.commit()

# Create investment requests


all_requests = [] # Store requests to process remittances later
for year in financial_years:
start_year = int(year[:4])
end_year = start_year + 1
start_date = datetime(start_year, 4, 1) # April 1 of start year
end_date = datetime(end_year, 3, 31) if year != financial_years[-1]
else datetime(2025, 8, 4)

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)

territory_admin_id = territory_admin.id if status in


['territory_approved', 'territory_rejected', 'global_approved', 'global_rejected']
else None
global_admin_id = global_admin.id if status in ['global_approved',
'global_rejected'] else None

amount = random.uniform(allocation.allocated_amount * 0.01,


allocation.allocated_amount * 0.05)

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() # Commit all investment requests first

# Now create remittance requests for approved investments


for request, status, territory_admin, requestor in all_requests:
if status == 'global_approved' and random.random() < 0.5:
remittance_status = random.choice(['submitted',
'territory_approved', 'global_approved'])
remittance = RemittanceRequest(
investment_request_id=request.id, # Now request.id exists
requestor_id=requestor.id,
progress_report=fake.text(),
actual_roi=random.uniform(0.1, request.expected_roi * 1.2),
amount=request.amount * random.uniform(0.8, 1.0),
currency='USD',
status=remittance_status,
territory_admin_id=territory_admin.id if remittance_status in
['territory_approved', 'global_approved'] else None,
global_admin_id=global_admin.id if remittance_status ==
'global_approved' else None,
created_at=request.updated_at +
timedelta(days=random.randint(30, 180))
)
db.session.add(remittance)

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)

You might also like